neon 5.0.1 → 6.1.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 +34 -27
- package/dist/commands/bootstrap.js +19 -17
- package/dist/commands/config.js +38 -17
- package/dist/commands/dev.js +1 -1
- package/dist/commands/index.js +3 -3
- package/dist/commands/init.js +45 -302
- package/dist/commands/mcp.js +68 -41
- package/dist/commands/plugins.js +56 -35
- package/dist/commands/skills.js +56 -35
- package/dist/commands/snapshots.js +23 -7
- package/dist/index.js +8 -0
- package/dist/init/auth.js +84 -0
- package/dist/init/cancelled.js +41 -0
- package/dist/init/choices.js +10 -21
- package/dist/init/chrome.js +59 -24
- package/dist/init/copy.js +133 -0
- package/dist/init/detect.js +58 -0
- package/dist/init/funnel.js +115 -0
- package/dist/init/mode.js +30 -0
- package/dist/init/plan.js +135 -32
- package/dist/init/run.js +645 -0
- package/dist/init/tooling.js +104 -22
- package/dist/init/wizard.js +207 -48
- package/dist/mcp/plan.js +1 -1
- package/dist/mcp/wizard.js +1 -27
- package/dist/plugins/plan.js +1 -1
- package/dist/skills/plan.js +1 -1
- package/dist/skills/wizard.js +1 -25
- package/dist/test_utils/npm_env.js +26 -0
- package/dist/update_check_worker.js +47 -0
- package/dist/update_notifier.js +145 -0
- package/dist/utils/package_manager.js +8 -1
- package/dist/utils/service_picker.js +34 -8
- package/package.json +7 -5
- package/dist/init/child.js +0 -37
- package/dist/plugins/wizard.js +0 -36
package/dist/commands/plugins.js
CHANGED
|
@@ -9,16 +9,18 @@ import { NEON_MCP_URL } from "../mcp/install.js";
|
|
|
9
9
|
import "../init/agents.js";
|
|
10
10
|
import { pluginsInstallableAgents } from "../plugins/targets.js";
|
|
11
11
|
import { canPickAgentsInteractively } from "../utils/agent_picker.js";
|
|
12
|
-
import { NEON_PLUGIN_NAME, PLUGIN_SKILLS, PLUGIN_SOURCE, neonPluginsRetryCommand, pluginsAddArgs, runPluginsCli } from "../plugins/run.js";
|
|
13
12
|
import { resolvePluginsPlan } from "../plugins/plan.js";
|
|
14
|
-
import {
|
|
13
|
+
import { NEON_PLUGIN_NAME, PLUGIN_SKILLS, PLUGIN_SOURCE, neonPluginsRetryCommand, pluginsAddArgs, runPluginsCli } from "../plugins/run.js";
|
|
15
14
|
//#region src/commands/plugins.ts
|
|
16
15
|
var plugins_exports = /* @__PURE__ */ __exportAll({
|
|
17
16
|
aliases: () => aliases,
|
|
18
17
|
builder: () => builder,
|
|
19
18
|
command: () => command,
|
|
20
19
|
describe: () => describe,
|
|
21
|
-
handler: () => handler
|
|
20
|
+
handler: () => handler,
|
|
21
|
+
installPlugins: () => installPlugins,
|
|
22
|
+
pluginsInstallError: () => pluginsInstallError,
|
|
23
|
+
reportPluginsInstall: () => reportPluginsInstall
|
|
22
24
|
});
|
|
23
25
|
const coerceAgents = (value) => {
|
|
24
26
|
if (value === void 0) return [];
|
|
@@ -54,38 +56,36 @@ const builder = (argv) => argv.usage("$0 plugins [options]").options({
|
|
|
54
56
|
describe: "Coding agent to install into (repeatable). Skips the agent picker. Values listed below",
|
|
55
57
|
coerce: coerceAgents
|
|
56
58
|
}
|
|
57
|
-
}).example("$0 plugins", "Interactive: agents, then
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
}).example("$0 plugins", "Interactive: pick agents, then install").example("$0 plugins -y", "Detected agents (project folders, else the host CLI agent), skip prompts").example("$0 plugins --agent cursor --agent claude-code", "Install into specific agents").example("$0 plugins --global", "Install user-level").epilogue(helpEpilogue(helpCsv("Supported agents at project scope", pluginProjectAgents), helpCsv("Also with --global", pluginGlobalOnlyAgents), `Currently one plugin: ${NEON_PLUGIN_NAME} from ${PLUGIN_SOURCE}.`, `It includes the Neon MCP server (${NEON_MCP_URL})`, helpCsv("and these skills", PLUGIN_SKILLS))).strict().check(noPassthrough("plugins"));
|
|
60
|
+
/**
|
|
61
|
+
* Plans and runs the plugin install for every resolved target. No CLI-only concerns here
|
|
62
|
+
* (no `writer` table, no `recordCommandSuccessExtras`): a caller that only wants the
|
|
63
|
+
* outcome — `neon init` / `neon bootstrap` — gets it without rendering a result table.
|
|
64
|
+
*/
|
|
65
|
+
const installPlugins = async (options) => {
|
|
66
|
+
const cwd = options.cwd ?? process.cwd();
|
|
67
|
+
const yes = options.yes === true;
|
|
61
68
|
const interactive = canPickAgentsInteractively() && !yes;
|
|
62
69
|
const plan = await resolvePluginsPlan({
|
|
63
|
-
global:
|
|
64
|
-
agents:
|
|
70
|
+
global: options.global === true,
|
|
71
|
+
agents: options.agents ?? [],
|
|
65
72
|
yes,
|
|
66
73
|
cwd,
|
|
67
74
|
interactive
|
|
68
75
|
});
|
|
69
76
|
for (const agent of plan.skipped) log.warning("Skipping %s: no plugins mapping.", getAgentDisplayName(agent));
|
|
70
77
|
for (const agent of plan.userScopeSkipped) log.warning("Skipping %s: plugins are user-level. Pass --global.", getAgentDisplayName(agent));
|
|
71
|
-
if (interactive) {
|
|
72
|
-
if (!await confirmPluginsInstall({
|
|
73
|
-
scope: plan.scope,
|
|
74
|
-
agents: plan.agents
|
|
75
|
-
})) {
|
|
76
|
-
log.info("Aborted. Nothing was written.");
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
78
|
const rows = [];
|
|
81
79
|
const failed = [];
|
|
82
80
|
const scope = scopeLabel(plan.scope);
|
|
83
|
-
for (const mapped of plan.targets) {
|
|
81
|
+
for (const [index, mapped] of plan.targets.entries()) {
|
|
84
82
|
const args = pluginsAddArgs({
|
|
85
83
|
target: mapped.target,
|
|
86
84
|
global: plan.scope === "global"
|
|
87
85
|
});
|
|
88
86
|
const agent = mapped.agents.join(", ");
|
|
87
|
+
const displayNames = mapped.agents.map((id) => getAgentDisplayName(id)).join(", ");
|
|
88
|
+
log.info("Installing the Neon plugin for %s (%d/%d)...", displayNames, index + 1, plan.targets.length);
|
|
89
89
|
try {
|
|
90
90
|
await runPluginsCli({
|
|
91
91
|
args,
|
|
@@ -112,8 +112,31 @@ const handler = async (props) => {
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
return {
|
|
116
|
+
scope: plan.scope,
|
|
117
|
+
rows,
|
|
118
|
+
failed
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
/** The exact error `neon plugins` throws for a failed outcome, or `undefined` on success. */
|
|
122
|
+
const pluginsInstallError = (outcome) => {
|
|
123
|
+
const { failed, rows, scope } = outcome;
|
|
124
|
+
if (failed.length === 0) return;
|
|
125
|
+
const first = failed[0];
|
|
126
|
+
if (first === void 0) return /* @__PURE__ */ new Error("Failed to install the Neon plugin.");
|
|
127
|
+
const retry = neonPluginsRetryCommand({
|
|
128
|
+
agents: failed.flatMap((row) => row.agents),
|
|
129
|
+
global: scope === "global"
|
|
130
|
+
});
|
|
131
|
+
if (first.message.includes("needs npx (Node.js)")) return new Error(first.message);
|
|
132
|
+
const detail = failed.map((row) => row.message).join("\n");
|
|
133
|
+
if (failed.length === rows.length) return /* @__PURE__ */ new Error(`${detail}\nRetry with: ${retry}`);
|
|
134
|
+
return /* @__PURE__ */ new Error(`Failed to install the Neon plugin for: ${failed.flatMap((row) => row.agents).join(", ")}.\n${detail}\nRetry with: ${retry}`);
|
|
135
|
+
};
|
|
136
|
+
/** Writes the results table and, on success, the "Installed the Neon plugin" line. Shared by `neon plugins` and any in-process caller that wants the same human output. */
|
|
137
|
+
const reportPluginsInstall = (props, outcome) => {
|
|
115
138
|
const out = writer(props);
|
|
116
|
-
out.write(rows, {
|
|
139
|
+
out.write(outcome.rows, {
|
|
117
140
|
fields: [
|
|
118
141
|
"scope",
|
|
119
142
|
"plugin",
|
|
@@ -124,21 +147,19 @@ const handler = async (props) => {
|
|
|
124
147
|
title: "Plugins"
|
|
125
148
|
});
|
|
126
149
|
out.end();
|
|
127
|
-
if (failed.length === 0)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
agents: failed.flatMap((row) => row.agents),
|
|
136
|
-
global: plan.scope === "global"
|
|
150
|
+
if (outcome.failed.length === 0) log.info(outcome.scope === "project" ? "Installed the Neon plugin (project)." : "Installed the Neon plugin (user).");
|
|
151
|
+
};
|
|
152
|
+
const handler = async (props) => {
|
|
153
|
+
const outcome = await installPlugins({
|
|
154
|
+
cwd: props.cwd,
|
|
155
|
+
yes: props.yes,
|
|
156
|
+
global: props.global,
|
|
157
|
+
agents: props.agent
|
|
137
158
|
});
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
|
|
159
|
+
reportPluginsInstall(props, outcome);
|
|
160
|
+
const error = pluginsInstallError(outcome);
|
|
161
|
+
if (error) throw error;
|
|
162
|
+
recordCommandSuccessExtras({ scope: outcome.scope });
|
|
142
163
|
};
|
|
143
164
|
//#endregion
|
|
144
|
-
export { aliases, builder, command, describe, handler, plugins_exports as t };
|
|
165
|
+
export { aliases, builder, command, describe, handler, installPlugins, pluginsInstallError, reportPluginsInstall, plugins_exports as t };
|
package/dist/commands/skills.js
CHANGED
|
@@ -9,7 +9,7 @@ import "../init/agents.js";
|
|
|
9
9
|
import { mappedSkillsAgentNames, skillsInstallableAgents } from "../skills/targets.js";
|
|
10
10
|
import { canPickAgentsInteractively } from "../utils/agent_picker.js";
|
|
11
11
|
import { skillsHelpValues, skillsYesHelp } from "../skills/catalog.js";
|
|
12
|
-
import {
|
|
12
|
+
import { confirmSkillsUpdate } from "../skills/wizard.js";
|
|
13
13
|
import { assertSkillsCanRun, resolveSkillsPlan } from "../skills/plan.js";
|
|
14
14
|
import { assertSkillsNode, neonSkillsRetryCommand, neonSkillsUpdateRetryCommand, runSkillsCli, skillsAddArgs, skillsMetadata, skillsUpdateArgs, skillsUpdateDetail, skillsUpdateHadNothing } from "../skills/run.js";
|
|
15
15
|
//#region src/commands/skills.ts
|
|
@@ -18,7 +18,10 @@ var skills_exports = /* @__PURE__ */ __exportAll({
|
|
|
18
18
|
builder: () => builder,
|
|
19
19
|
command: () => command,
|
|
20
20
|
describe: () => describe,
|
|
21
|
-
handler: () => handler
|
|
21
|
+
handler: () => handler,
|
|
22
|
+
installSkills: () => installSkills,
|
|
23
|
+
reportSkillsInstall: () => reportSkillsInstall,
|
|
24
|
+
skillsInstallError: () => skillsInstallError
|
|
22
25
|
});
|
|
23
26
|
const scopeLabel = (scope) => scope === "project" ? "this directory" : "user-level";
|
|
24
27
|
const coerceAgents = (value) => {
|
|
@@ -82,32 +85,27 @@ const builder = (argv) => argv.usage("$0 skills [command] [options]").command("u
|
|
|
82
85
|
describe: "Skill to install (repeatable). Skips the skill picker. Does not select agents. Values listed below",
|
|
83
86
|
coerce: coerceSkills
|
|
84
87
|
}
|
|
85
|
-
}).example("$0 skills", "Interactive: this directory, agents,
|
|
86
|
-
|
|
88
|
+
}).example("$0 skills", "Interactive: this directory, then agents, then skills").example("$0 skills -y", "This directory, detected agents (project folders, else the host CLI agent), the default skills").example("$0 skills -y -s neon -s neon-ai-gateway", "Named skills into detected agents").example("$0 skills -s neon -s neon-ai-gateway --agent cursor", "Named skills into a named agent").example("$0 skills --global", "Install user-level skills").epilogue(helpEpilogue(helpCsv("Supported agents", skillsInstallableAgents()), "Supported skills, by source repo:", skillsHelpValues(), skillsYesHelp())).strict().check(noPassthrough("skills"));
|
|
89
|
+
/**
|
|
90
|
+
* Plans and runs the skills install for every resolved source/invocation. No CLI-only
|
|
91
|
+
* concerns here (no `writer` table, no `recordCommandSuccessExtras`): a caller that only
|
|
92
|
+
* wants the outcome — `neon init` / `neon bootstrap` — gets it without rendering a table.
|
|
93
|
+
*/
|
|
94
|
+
const installSkills = async (options) => {
|
|
87
95
|
assertSkillsNode();
|
|
88
|
-
const cwd = process.cwd();
|
|
89
|
-
const yes =
|
|
96
|
+
const cwd = options.cwd ?? process.cwd();
|
|
97
|
+
const yes = options.yes === true;
|
|
90
98
|
const interactive = canPickAgentsInteractively() && !yes;
|
|
91
99
|
const plan = await resolveSkillsPlan({
|
|
92
|
-
global:
|
|
93
|
-
agents:
|
|
94
|
-
skills:
|
|
100
|
+
global: options.global === true,
|
|
101
|
+
agents: options.agents ?? [],
|
|
102
|
+
skills: options.skills ?? [],
|
|
95
103
|
yes,
|
|
96
104
|
cwd,
|
|
97
105
|
interactive
|
|
98
106
|
});
|
|
99
107
|
const mapped = mappedSkillsAgentNames(plan.agents);
|
|
100
108
|
for (const agent of plan.skipped) log.warning("Skipping %s: no skills mapping.", getAgentDisplayName(agent));
|
|
101
|
-
if (interactive) {
|
|
102
|
-
if (!await confirmSkillsInstall({
|
|
103
|
-
scope: plan.scope,
|
|
104
|
-
agents: plan.agents,
|
|
105
|
-
invocations: plan.invocations
|
|
106
|
-
})) {
|
|
107
|
-
log.info("Aborted. Nothing was written.");
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
109
|
const metadata = skillsMetadata("skills");
|
|
112
110
|
const rows = [];
|
|
113
111
|
const failed = [];
|
|
@@ -148,8 +146,32 @@ const handler = async (props) => {
|
|
|
148
146
|
});
|
|
149
147
|
}
|
|
150
148
|
}
|
|
149
|
+
return {
|
|
150
|
+
scope: plan.scope,
|
|
151
|
+
agents: plan.agents,
|
|
152
|
+
rows,
|
|
153
|
+
failed
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
/** The exact error `neon skills` throws for a failed outcome, or `undefined` on success. */
|
|
157
|
+
const skillsInstallError = (outcome) => {
|
|
158
|
+
const { failed, rows, scope, agents } = outcome;
|
|
159
|
+
if (failed.length === 0) return;
|
|
160
|
+
const first = failed[0];
|
|
161
|
+
if (first === void 0) return /* @__PURE__ */ new Error("Failed to install Neon agent skills.");
|
|
162
|
+
const retry = neonSkillsRetryCommand({
|
|
163
|
+
skills: failed.flatMap((row) => row.skills),
|
|
164
|
+
agents,
|
|
165
|
+
global: scope === "global"
|
|
166
|
+
});
|
|
167
|
+
if (first.message.includes("needs npx (Node.js)") || first.message.includes("needs Node.js")) return new Error(first.message);
|
|
168
|
+
if (failed.length === rows.length) return /* @__PURE__ */ new Error(`${first.message}\nRetry with: ${retry}`);
|
|
169
|
+
return /* @__PURE__ */ new Error(`Failed to install Neon agent skills for: ${failed.map((row) => row.label).join(", ")}.\n${first.message}\nRetry with: ${retry}`);
|
|
170
|
+
};
|
|
171
|
+
/** Writes the results table and, on success, the "Wrote ... skills" line. Shared by `neon skills` and any in-process caller that wants the same human output. */
|
|
172
|
+
const reportSkillsInstall = (props, outcome) => {
|
|
151
173
|
const out = writer(props);
|
|
152
|
-
out.write(rows, {
|
|
174
|
+
out.write(outcome.rows, {
|
|
153
175
|
fields: [
|
|
154
176
|
"scope",
|
|
155
177
|
"skills",
|
|
@@ -160,21 +182,20 @@ const handler = async (props) => {
|
|
|
160
182
|
title: "Skills"
|
|
161
183
|
});
|
|
162
184
|
out.end();
|
|
163
|
-
if (failed.length === 0)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
skills:
|
|
172
|
-
agents: plan.agents,
|
|
173
|
-
global: plan.scope === "global"
|
|
185
|
+
if (outcome.failed.length === 0) log.info(outcome.scope === "project" ? "Wrote skills in this directory." : "Wrote user-level skills.");
|
|
186
|
+
};
|
|
187
|
+
const handler = async (props) => {
|
|
188
|
+
const outcome = await installSkills({
|
|
189
|
+
cwd: props.cwd,
|
|
190
|
+
yes: props.yes,
|
|
191
|
+
global: props.global,
|
|
192
|
+
agents: props.agent,
|
|
193
|
+
skills: props.skill
|
|
174
194
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
reportSkillsInstall(props, outcome);
|
|
196
|
+
const error = skillsInstallError(outcome);
|
|
197
|
+
if (error) throw error;
|
|
198
|
+
recordCommandSuccessExtras({ scope: outcome.scope });
|
|
178
199
|
};
|
|
179
200
|
const updateHandler = async (props) => {
|
|
180
201
|
assertSkillsNode();
|
|
@@ -229,4 +250,4 @@ const updateHandler = async (props) => {
|
|
|
229
250
|
if (failure !== void 0) throw new Error(failure.includes("needs npx (Node.js)") || failure.includes("needs Node.js") ? failure : `${failure}\nRetry with: ${neonSkillsUpdateRetryCommand(scope === "global")}`);
|
|
230
251
|
};
|
|
231
252
|
//#endregion
|
|
232
|
-
export { aliases, builder, command, describe, handler, skills_exports as t };
|
|
253
|
+
export { aliases, builder, command, describe, handler, installSkills, reportSkillsInstall, skillsInstallError, skills_exports as t };
|
|
@@ -17,6 +17,7 @@ var snapshots_exports = /* @__PURE__ */ __exportAll({
|
|
|
17
17
|
const SNAPSHOT_FIELDS = [
|
|
18
18
|
"id",
|
|
19
19
|
"name",
|
|
20
|
+
"slug",
|
|
20
21
|
"source_branch_id",
|
|
21
22
|
"expires_at",
|
|
22
23
|
"created_at"
|
|
@@ -38,6 +39,12 @@ const SNAPSHOT_FREQUENCIES = [
|
|
|
38
39
|
"weekly",
|
|
39
40
|
"monthly"
|
|
40
41
|
];
|
|
42
|
+
/** Matches createSnapshot.query.slug in the Management API spec. */
|
|
43
|
+
const SNAPSHOT_SLUG = /^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
44
|
+
const SNAPSHOT_REF = {
|
|
45
|
+
describe: "Snapshot id, unique name, or slug. Lookup order: id, unique name, slug.",
|
|
46
|
+
type: "string"
|
|
47
|
+
};
|
|
41
48
|
/** Narrow an arbitrary string to a supported {@link SnapshotFrequency}. */
|
|
42
49
|
const isSnapshotFrequency = (value) => SNAPSHOT_FREQUENCIES.some((frequency) => frequency === value);
|
|
43
50
|
const command = "snapshots";
|
|
@@ -46,7 +53,7 @@ const aliases = ["snapshot"];
|
|
|
46
53
|
const builder = (argv) => argv.usage("$0 snapshots <sub-command> [options]").options({ "project-id": {
|
|
47
54
|
describe: "Project ID",
|
|
48
55
|
type: "string"
|
|
49
|
-
} }).middleware(fillSingleProject).command("list", "List snapshots in the project", (yargs) => yargs, (args) => list(args)).command("get <id>", "Get a snapshot by id or
|
|
56
|
+
} }).middleware(fillSingleProject).command("list", "List snapshots in the project", (yargs) => yargs, (args) => list(args)).command("get <id>", "Get a snapshot by id, unique name, or slug", (yargs) => yargs.positional("id", SNAPSHOT_REF), (args) => get(args)).command("create", "Create a snapshot from a branch", (yargs) => yargs.options({
|
|
50
57
|
branch: {
|
|
51
58
|
alias: "b",
|
|
52
59
|
describe: "Branch id or name to snapshot. Defaults to the branch in your context, or the project's default branch.",
|
|
@@ -56,6 +63,10 @@ const builder = (argv) => argv.usage("$0 snapshots <sub-command> [options]").opt
|
|
|
56
63
|
describe: "A name for the snapshot",
|
|
57
64
|
type: "string"
|
|
58
65
|
},
|
|
66
|
+
slug: {
|
|
67
|
+
describe: "User-defined resource ID, unique in the project (1-63 characters: start with a lowercase letter, then lowercase letters, digits, or hyphens, ending with a letter or digit). Omit to let the API generate one. It cannot be changed later.",
|
|
68
|
+
type: "string"
|
|
69
|
+
},
|
|
59
70
|
timestamp: {
|
|
60
71
|
describe: "Take the snapshot at this point in time (RFC 3339, e.g. 2025-01-01T00:00:00Z). Must fall within the branch's restore window. Mutually exclusive with --lsn.",
|
|
61
72
|
type: "string"
|
|
@@ -71,9 +82,10 @@ const builder = (argv) => argv.usage("$0 snapshots <sub-command> [options]").opt
|
|
|
71
82
|
}).conflicts("timestamp", "lsn").example([
|
|
72
83
|
["$0 snapshots create", "Snapshot the head of the context/default branch"],
|
|
73
84
|
["$0 snapshots create --branch main --name pre-migration", "Snapshot the head of main with a name"],
|
|
85
|
+
["$0 snapshots create --branch main --name \"Before migration\" --slug before-migration", "Snapshot main with a display name and a slug"],
|
|
74
86
|
["$0 snapshots create --branch main --timestamp 2025-01-01T00:00:00Z", "Snapshot main at a point in time"],
|
|
75
87
|
["$0 snapshots create --branch main --lsn 0/1F3C8A0 --expires-at 2025-12-31T23:59:59Z", "Snapshot main at an LSN, auto-deleting at the given time"]
|
|
76
|
-
]), (args) => create(args)).command("update <id>", "Update a snapshot's name or expiration", (yargs) => yargs.options({
|
|
88
|
+
]), (args) => create(args)).command("update <id>", "Update a snapshot's name or expiration by id, unique name, or slug", (yargs) => yargs.positional("id", SNAPSHOT_REF).options({
|
|
77
89
|
name: {
|
|
78
90
|
describe: "Rename the snapshot",
|
|
79
91
|
type: "string"
|
|
@@ -86,7 +98,7 @@ const builder = (argv) => argv.usage("$0 snapshots <sub-command> [options]").opt
|
|
|
86
98
|
describe: "Clear the expiration so the snapshot is kept indefinitely.",
|
|
87
99
|
type: "boolean"
|
|
88
100
|
}
|
|
89
|
-
}).conflicts("expires-at", "clear-expiration"), (args) => update(args)).command("delete <id>", "Delete a snapshot by id or
|
|
101
|
+
}).conflicts("expires-at", "clear-expiration"), (args) => update(args)).command("delete <id>", "Delete a snapshot by id, unique name, or slug", (yargs) => yargs.positional("id", SNAPSHOT_REF), (args) => deleteSnapshot(args)).command("restore <id>", "Restore a snapshot (id, unique name, or slug) into a branch", (yargs) => yargs.positional("id", SNAPSHOT_REF).options({
|
|
90
102
|
name: {
|
|
91
103
|
describe: "Name for the newly restored branch. Auto-generated when omitted.",
|
|
92
104
|
type: "string"
|
|
@@ -155,9 +167,8 @@ const toIso = (value, flag) => {
|
|
|
155
167
|
return new Date(ms).toISOString();
|
|
156
168
|
};
|
|
157
169
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* hard error asking the user to disambiguate by id.
|
|
170
|
+
* Unique names precede slugs so existing name-based commands keep targeting
|
|
171
|
+
* the named snapshot when another snapshot's slug matches that name.
|
|
161
172
|
*/
|
|
162
173
|
const resolveSnapshot = async (props) => {
|
|
163
174
|
const { data: { snapshots } } = await props.apiClient.listSnapshots(props.projectId);
|
|
@@ -166,8 +177,11 @@ const resolveSnapshot = async (props) => {
|
|
|
166
177
|
const byName = snapshots.filter((s) => s.name === props.id);
|
|
167
178
|
if (byName.length === 1) return byName[0];
|
|
168
179
|
if (byName.length > 1) throw new Error(`Multiple snapshots are named "${props.id}". Re-run with the snapshot id:\n${byName.map((s) => ` ${s.id}`).join("\n")}`);
|
|
169
|
-
|
|
180
|
+
const bySlug = snapshots.find((s) => s.slug === props.id);
|
|
181
|
+
if (bySlug) return bySlug;
|
|
182
|
+
throw new Error(`Snapshot "${props.id}" not found.\nAvailable snapshots: ${snapshots.map((s) => formatSnapshotRef(s)).join(", ") || "none"}`);
|
|
170
183
|
};
|
|
184
|
+
const formatSnapshotRef = (snapshot) => snapshot.slug ? `${snapshot.name} (${snapshot.id}, slug: ${snapshot.slug})` : `${snapshot.name} (${snapshot.id})`;
|
|
171
185
|
const list = async (props) => {
|
|
172
186
|
const { data: { snapshots } } = await props.apiClient.listSnapshots(props.projectId);
|
|
173
187
|
writer(props).end(snapshots, {
|
|
@@ -187,12 +201,14 @@ const get = async (props) => {
|
|
|
187
201
|
const create = async (props) => {
|
|
188
202
|
if (props.lsn !== void 0 && !looksLikeLSN(props.lsn)) throw new Error(`Invalid --lsn value: "${props.lsn}". Expected an LSN like 0/1F3C8A0.`);
|
|
189
203
|
if (props.timestamp !== void 0 && !looksLikeTimestamp(props.timestamp)) throw new Error(`Invalid --timestamp value: "${props.timestamp}". Use an RFC 3339 timestamp, e.g. 2025-01-01T00:00:00Z.`);
|
|
204
|
+
if (props.slug !== void 0 && !SNAPSHOT_SLUG.test(props.slug)) throw new Error(`Invalid --slug value: "${props.slug}". Use 1-63 characters: start with a lowercase letter, then lowercase letters, digits, or hyphens, and end with a letter or digit.`);
|
|
190
205
|
const { branchId } = await resolveBranchRef({
|
|
191
206
|
...props,
|
|
192
207
|
branch: props.branch
|
|
193
208
|
});
|
|
194
209
|
const { data } = await retryOnLock(() => props.apiClient.createSnapshot(props.projectId, branchId, {
|
|
195
210
|
name: props.name,
|
|
211
|
+
slug: props.slug,
|
|
196
212
|
timestamp: props.timestamp,
|
|
197
213
|
lsn: props.lsn,
|
|
198
214
|
expires_at: props.expiresAt ? toIso(props.expiresAt, "--expires-at") : void 0
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { recoverFrom401 } from "./auth_recovery.js";
|
|
|
14
14
|
import { showHelp } from "./help.js";
|
|
15
15
|
import { rewriteUnknownAgentArg } from "./init/plan.js";
|
|
16
16
|
import commands_default from "./commands/index.js";
|
|
17
|
+
import { notifyIfUpdateAvailable } from "./update_notifier.js";
|
|
17
18
|
import { fillInArgs, resolveApiKeyFromEnv } from "./utils/middlewares.js";
|
|
18
19
|
import yargs from "yargs";
|
|
19
20
|
import { hideBin } from "yargs/helpers";
|
|
@@ -193,6 +194,13 @@ async function handleError(msg, err, canRetry) {
|
|
|
193
194
|
process.exit(0);
|
|
194
195
|
}
|
|
195
196
|
await closeAnalytics();
|
|
197
|
+
notifyIfUpdateAvailable({
|
|
198
|
+
commandPath: args._.map(String),
|
|
199
|
+
configDir: typeof args.configDir === "string" ? args.configDir : defaultDir,
|
|
200
|
+
currentBranch: Boolean(args.currentBranch),
|
|
201
|
+
currentVersion: pkg_default.version,
|
|
202
|
+
output: typeof args.output === "string" ? args.output : "table"
|
|
203
|
+
});
|
|
196
204
|
break;
|
|
197
205
|
} catch (err) {
|
|
198
206
|
takeCommandSuccessExtras();
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isNeonApiError } from "../api.js";
|
|
2
|
+
import { r as defaultDir } from "../_chunks/paths-DMq0Lt7a.js";
|
|
3
|
+
import "../config.js";
|
|
4
|
+
import "../auth.js";
|
|
5
|
+
import { ensureAuth } from "../commands/auth.js";
|
|
6
|
+
import { recoverFrom401 } from "../auth_recovery.js";
|
|
7
|
+
import { pull } from "../commands/env.js";
|
|
8
|
+
import { setupNeonMcp } from "../commands/mcp.js";
|
|
9
|
+
//#region src/init/auth.ts
|
|
10
|
+
const isUnauthorized = (error) => isNeonApiError(error) && error.status === 401;
|
|
11
|
+
const authenticationFailed = () => /* @__PURE__ */ new Error("Authentication failed.");
|
|
12
|
+
/**
|
|
13
|
+
* Fills in the fields `ensureAuth` requires beyond `InitAuthOptions`, and sets `_` so
|
|
14
|
+
* `ensureAuth`'s per-command branches (`isMcpCommand`, `isSkillsCommand`, ...) treat this
|
|
15
|
+
* exactly like the standalone command it names — mirrors `init/link.ts`'s `runAuthenticatedLink`.
|
|
16
|
+
*/
|
|
17
|
+
const authContext = (options, command) => ({
|
|
18
|
+
...options,
|
|
19
|
+
_: [...command],
|
|
20
|
+
configDir: options.configDir ?? defaultDir,
|
|
21
|
+
oauthHost: options.oauthHost ?? process.env.NEON_OAUTH_HOST ?? "https://oauth2.neon.tech",
|
|
22
|
+
clientId: options.clientId ?? "neonctl",
|
|
23
|
+
help: false
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Resolves Neon auth the same way a standalone `neon mcp` invocation would, then runs
|
|
27
|
+
* `setupNeonMcp`. `init`/`bootstrap` never resolve a shared `apiClient` of their own for
|
|
28
|
+
* this (see `InitAuthOptions` above), so this is what does it, per call.
|
|
29
|
+
*
|
|
30
|
+
* Skips `ensureAuth` entirely when `options.oauth` is true: `setupNeonMcp`'s oauth branch
|
|
31
|
+
* never reads `apiClient`/`apiKey`, and `ensureAuth`'s own `isMcpOauth` skip reads the real
|
|
32
|
+
* `process.argv` for `--oauth` (documented there as required because that middleware runs
|
|
33
|
+
* before flag parsing) — which never contains it for a `neon init`/`bootstrap` process, so
|
|
34
|
+
* `ensureAuth` would otherwise try to resolve credentials it doesn't need and can fail on
|
|
35
|
+
* an unrelated bad profile or expired session.
|
|
36
|
+
*/
|
|
37
|
+
const runAuthenticatedMcp = async (options) => {
|
|
38
|
+
if (options.oauth === true) return setupNeonMcp(options);
|
|
39
|
+
const authenticated = authContext(options, ["mcp"]);
|
|
40
|
+
await ensureAuth(authenticated);
|
|
41
|
+
try {
|
|
42
|
+
return await setupNeonMcp(authenticated);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (!isUnauthorized(error)) throw error;
|
|
45
|
+
if (!await recoverFrom401(true)) throw authenticationFailed();
|
|
46
|
+
}
|
|
47
|
+
await ensureAuth(authenticated);
|
|
48
|
+
try {
|
|
49
|
+
return await setupNeonMcp(authenticated);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (!isUnauthorized(error)) throw error;
|
|
52
|
+
await recoverFrom401(false);
|
|
53
|
+
throw authenticationFailed();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Resolves Neon auth the same way a standalone `neon env pull` invocation would, then runs
|
|
58
|
+
* `pull` — the same function `link`/`checkout` already call directly via
|
|
59
|
+
* `autoPullEnvAfterPin` (see `commands/env.ts`).
|
|
60
|
+
*/
|
|
61
|
+
const pullInitEnv = async (props) => {
|
|
62
|
+
const authenticated = authContext(props, ["env", "pull"]);
|
|
63
|
+
await ensureAuth(authenticated);
|
|
64
|
+
const opts = {
|
|
65
|
+
announce: true,
|
|
66
|
+
implyAiGateway: true
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
return await pull(authenticated, opts);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (!isUnauthorized(error)) throw error;
|
|
72
|
+
if (!await recoverFrom401(true)) throw authenticationFailed();
|
|
73
|
+
}
|
|
74
|
+
await ensureAuth(authenticated);
|
|
75
|
+
try {
|
|
76
|
+
return await pull(authenticated, opts);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (!isUnauthorized(error)) throw error;
|
|
79
|
+
await recoverFrom401(false);
|
|
80
|
+
throw authenticationFailed();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { pullInitEnv, runAuthenticatedMcp };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/init/cancelled.ts
|
|
2
|
+
var InitCancelled = class extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super("Aborted.");
|
|
5
|
+
this.name = "InitCancelled";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Rejects with {@link InitCancelled} on SIGINT while `work` is pending, so a step that runs
|
|
10
|
+
* fully in-process (no spawned child of its own to die and report the signal back) still
|
|
11
|
+
* unwinds through `runInit`'s normal cancellation handling — the "Neon setup cancelled."
|
|
12
|
+
* summary and the funnel's final telemetry — instead of Node's default SIGINT disposition
|
|
13
|
+
* killing the process with no chance to run either.
|
|
14
|
+
*
|
|
15
|
+
* Does not cancel `work` itself: there is no cooperative cancellation to ask for here, so a
|
|
16
|
+
* lost race leaves it running in the background, its eventual settlement discarded. That is
|
|
17
|
+
* fine — the process is already unwinding to exit on the `InitCancelled` path.
|
|
18
|
+
*/
|
|
19
|
+
const raceSigint = (work) => new Promise((resolve, reject) => {
|
|
20
|
+
const onSigint = () => reject(new InitCancelled());
|
|
21
|
+
process.once("SIGINT", onSigint);
|
|
22
|
+
const stop = () => {
|
|
23
|
+
process.removeListener("SIGINT", onSigint);
|
|
24
|
+
};
|
|
25
|
+
work.then((value) => {
|
|
26
|
+
stop();
|
|
27
|
+
resolve(value);
|
|
28
|
+
}, (error) => {
|
|
29
|
+
stop();
|
|
30
|
+
reject(error);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
const restoreCursor = () => {
|
|
34
|
+
process.stdout.write("\x1B[?25h");
|
|
35
|
+
process.stdout.write("\n");
|
|
36
|
+
};
|
|
37
|
+
const restoreCursorVisibilityOnAbort = (state) => {
|
|
38
|
+
if (state.aborted) process.stdout.write("\x1B[?25h");
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { InitCancelled, raceSigint, restoreCursor, restoreCursorVisibilityOnAbort };
|
package/dist/init/choices.js
CHANGED
|
@@ -1,25 +1,5 @@
|
|
|
1
|
-
import { getCliName } from "../utils/cli_name.js";
|
|
2
1
|
//#region src/init/choices.ts
|
|
3
|
-
const INIT_TEMPLATE_CONFLICT = "--skip-template cannot be combined with --template.";
|
|
4
2
|
const INIT_CONFIG_SERVICES_CONFLICT = "--no-config cannot be combined with --services.";
|
|
5
|
-
const INIT_TEMPLATE_KEEPS_CONFIG = "A scaffolded template keeps the neon.ts it ships. --config, --no-config, and --services apply when you skip the template or set up an existing app.";
|
|
6
|
-
const initTemplateInNonEmptyMessage = () => `--template is only for an empty directory. This directory already has files, so there is nothing to scaffold. Omit --template, or run \`${getCliName()} bootstrap\` in an empty folder.`;
|
|
7
|
-
const resolveInitTemplateChoice = (input) => {
|
|
8
|
-
const id = input.template?.trim();
|
|
9
|
-
const hasTemplate = id !== void 0 && id.length > 0;
|
|
10
|
-
if (input.skipTemplate && hasTemplate) throw new Error(INIT_TEMPLATE_CONFLICT);
|
|
11
|
-
if (!input.empty) {
|
|
12
|
-
if (hasTemplate) throw new Error(initTemplateInNonEmptyMessage());
|
|
13
|
-
return { kind: "existing" };
|
|
14
|
-
}
|
|
15
|
-
if (input.skipTemplate) return { kind: "skip" };
|
|
16
|
-
if (hasTemplate) return {
|
|
17
|
-
kind: "template",
|
|
18
|
-
id
|
|
19
|
-
};
|
|
20
|
-
if (input.yes) return { kind: "default" };
|
|
21
|
-
return { kind: "ask" };
|
|
22
|
-
};
|
|
23
3
|
const resolveInitConfigChoice = (input) => {
|
|
24
4
|
if (input.flag === false && input.services !== void 0) throw new Error(INIT_CONFIG_SERVICES_CONFLICT);
|
|
25
5
|
if (input.flag === false) return { kind: "skip" };
|
|
@@ -41,6 +21,15 @@ const configPlanFromResolution = (resolution, accepted) => {
|
|
|
41
21
|
if (resolution.kind === "skip") return { kind: "skip" };
|
|
42
22
|
return resolution;
|
|
43
23
|
};
|
|
24
|
+
const isBareInitServices = (services) => {
|
|
25
|
+
if (services === void 0 || services.length === 0) return true;
|
|
26
|
+
return services.every((service) => service === "none");
|
|
27
|
+
};
|
|
44
28
|
const shouldRefreshEnvAfterNewConfig = (input) => input.wroteNewFile && typeof input.projectId === "string" && input.projectId.length > 0 && typeof input.branch === "string" && input.branch.length > 0;
|
|
29
|
+
const shouldPullEnvAfterInitConfig = (input) => !input.extraServices && !input.alreadyPulled && shouldRefreshEnvAfterNewConfig({
|
|
30
|
+
wroteNewFile: input.wroteNewFile,
|
|
31
|
+
...input.projectId !== void 0 ? { projectId: input.projectId } : {},
|
|
32
|
+
...input.branch !== void 0 ? { branch: input.branch } : {}
|
|
33
|
+
});
|
|
45
34
|
//#endregion
|
|
46
|
-
export { INIT_CONFIG_SERVICES_CONFLICT,
|
|
35
|
+
export { INIT_CONFIG_SERVICES_CONFLICT, configPlanFromResolution, isBareInitServices, resolveInitConfigChoice, shouldPullEnvAfterInitConfig, shouldRefreshEnvAfterNewConfig };
|