neon 6.0.0 → 6.2.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 +5 -1
- package/dist/_chunks/{env-DRpFX-_d.js → env-CtOH9xsl.js} +2 -1
- package/dist/_chunks/{env_services-DlXECBPi.js → env-N19A7qND.js} +13 -120
- package/dist/commands/auth.js +3 -1
- package/dist/commands/bootstrap.js +19 -17
- package/dist/commands/checkout.js +76 -6
- package/dist/commands/config.js +41 -0
- package/dist/commands/dev.js +3 -3
- package/dist/commands/env.js +3 -2
- package/dist/commands/git.js +356 -0
- package/dist/commands/index.js +5 -3
- package/dist/commands/init.js +2 -4
- package/dist/commands/link.js +2 -1
- package/dist/commands/mcp.js +67 -25
- package/dist/commands/plugins.js +54 -23
- package/dist/commands/skills.js +54 -23
- package/dist/commands/snapshots.js +23 -7
- package/dist/context.js +92 -3
- package/dist/dev/env.js +1 -1
- package/dist/env_services.js +120 -1
- package/dist/init/auth.js +84 -0
- package/dist/init/cancelled.js +26 -1
- package/dist/init/chrome.js +2 -19
- package/dist/init/detect.js +1 -1
- package/dist/init/plan.js +85 -63
- package/dist/init/run.js +30 -26
- package/dist/init/tooling.js +103 -21
- package/dist/init/wizard.js +1 -1
- package/dist/mcp/plan.js +1 -1
- package/dist/plugins/plan.js +1 -1
- package/dist/skills/plan.js +1 -1
- package/dist/utils/branch_name.js +29 -0
- package/dist/utils/git.js +220 -0
- package/dist/utils/hooks.js +230 -0
- package/package.json +5 -5
- package/dist/init/child.js +0 -49
package/dist/commands/plugins.js
CHANGED
|
@@ -17,7 +17,10 @@ var plugins_exports = /* @__PURE__ */ __exportAll({
|
|
|
17
17
|
builder: () => builder,
|
|
18
18
|
command: () => command,
|
|
19
19
|
describe: () => describe,
|
|
20
|
-
handler: () => handler
|
|
20
|
+
handler: () => handler,
|
|
21
|
+
installPlugins: () => installPlugins,
|
|
22
|
+
pluginsInstallError: () => pluginsInstallError,
|
|
23
|
+
reportPluginsInstall: () => reportPluginsInstall
|
|
21
24
|
});
|
|
22
25
|
const coerceAgents = (value) => {
|
|
23
26
|
if (value === void 0) return [];
|
|
@@ -54,13 +57,18 @@ const builder = (argv) => argv.usage("$0 plugins [options]").options({
|
|
|
54
57
|
coerce: coerceAgents
|
|
55
58
|
}
|
|
56
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"));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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;
|
|
60
68
|
const interactive = canPickAgentsInteractively() && !yes;
|
|
61
69
|
const plan = await resolvePluginsPlan({
|
|
62
|
-
global:
|
|
63
|
-
agents:
|
|
70
|
+
global: options.global === true,
|
|
71
|
+
agents: options.agents ?? [],
|
|
64
72
|
yes,
|
|
65
73
|
cwd,
|
|
66
74
|
interactive
|
|
@@ -70,12 +78,14 @@ const handler = async (props) => {
|
|
|
70
78
|
const rows = [];
|
|
71
79
|
const failed = [];
|
|
72
80
|
const scope = scopeLabel(plan.scope);
|
|
73
|
-
for (const mapped of plan.targets) {
|
|
81
|
+
for (const [index, mapped] of plan.targets.entries()) {
|
|
74
82
|
const args = pluginsAddArgs({
|
|
75
83
|
target: mapped.target,
|
|
76
84
|
global: plan.scope === "global"
|
|
77
85
|
});
|
|
78
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);
|
|
79
89
|
try {
|
|
80
90
|
await runPluginsCli({
|
|
81
91
|
args,
|
|
@@ -102,8 +112,31 @@ const handler = async (props) => {
|
|
|
102
112
|
});
|
|
103
113
|
}
|
|
104
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) => {
|
|
105
138
|
const out = writer(props);
|
|
106
|
-
out.write(rows, {
|
|
139
|
+
out.write(outcome.rows, {
|
|
107
140
|
fields: [
|
|
108
141
|
"scope",
|
|
109
142
|
"plugin",
|
|
@@ -114,21 +147,19 @@ const handler = async (props) => {
|
|
|
114
147
|
title: "Plugins"
|
|
115
148
|
});
|
|
116
149
|
out.end();
|
|
117
|
-
if (failed.length === 0)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
agents: failed.flatMap((row) => row.agents),
|
|
126
|
-
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
|
|
127
158
|
});
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
159
|
+
reportPluginsInstall(props, outcome);
|
|
160
|
+
const error = pluginsInstallError(outcome);
|
|
161
|
+
if (error) throw error;
|
|
162
|
+
recordCommandSuccessExtras({ scope: outcome.scope });
|
|
132
163
|
};
|
|
133
164
|
//#endregion
|
|
134
|
-
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
|
@@ -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) => {
|
|
@@ -83,15 +86,20 @@ const builder = (argv) => argv.usage("$0 skills [command] [options]").command("u
|
|
|
83
86
|
coerce: coerceSkills
|
|
84
87
|
}
|
|
85
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"));
|
|
86
|
-
|
|
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
|
|
@@ -138,8 +146,32 @@ const handler = async (props) => {
|
|
|
138
146
|
});
|
|
139
147
|
}
|
|
140
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) => {
|
|
141
173
|
const out = writer(props);
|
|
142
|
-
out.write(rows, {
|
|
174
|
+
out.write(outcome.rows, {
|
|
143
175
|
fields: [
|
|
144
176
|
"scope",
|
|
145
177
|
"skills",
|
|
@@ -150,21 +182,20 @@ const handler = async (props) => {
|
|
|
150
182
|
title: "Skills"
|
|
151
183
|
});
|
|
152
184
|
out.end();
|
|
153
|
-
if (failed.length === 0)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
skills:
|
|
162
|
-
agents: plan.agents,
|
|
163
|
-
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
|
|
164
194
|
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
195
|
+
reportSkillsInstall(props, outcome);
|
|
196
|
+
const error = skillsInstallError(outcome);
|
|
197
|
+
if (error) throw error;
|
|
198
|
+
recordCommandSuccessExtras({ scope: outcome.scope });
|
|
168
199
|
};
|
|
169
200
|
const updateHandler = async (props) => {
|
|
170
201
|
assertSkillsNode();
|
|
@@ -219,4 +250,4 @@ const updateHandler = async (props) => {
|
|
|
219
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")}`);
|
|
220
251
|
};
|
|
221
252
|
//#endregion
|
|
222
|
-
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/context.js
CHANGED
|
@@ -52,6 +52,34 @@ const isClaimCommand = (args) => args._[0] === "claim" || args._[0] === "claimab
|
|
|
52
52
|
*/
|
|
53
53
|
const isApiKeysCommand = (args) => args._[0] === "api-keys" || args._[0] === "api-key";
|
|
54
54
|
const isMcpCommand = (args) => args._[0] === "mcp";
|
|
55
|
+
/**
|
|
56
|
+
* `neon git install/uninstall/status` only touch the local git hooks directory and `.neon`
|
|
57
|
+
* — never the Neon API — so they must work (and never pop a browser login) without
|
|
58
|
+
* credentials, the same reasoning as {@link isProfileCommand}. `sync` and `cleanup` are
|
|
59
|
+
* excluded: both can reach the API (`sync` always delegates to `checkout`; `cleanup` does
|
|
60
|
+
* too with `--prune-neon-branches`).
|
|
61
|
+
*/
|
|
62
|
+
const isGitLocalCommand = (args) => args._[0] === "git" && (args._[1] === "install" || args._[1] === "uninstall" || args._[1] === "status");
|
|
63
|
+
/** The env var the installed `post-checkout` hook sets before invoking `neon git sync`. */
|
|
64
|
+
const GIT_HOOK_ENV_FLAG = "NEON_GIT_HOOK";
|
|
65
|
+
/**
|
|
66
|
+
* True only for a `neon git sync` invocation triggered by the installed `post-checkout`
|
|
67
|
+
* hook, in a repo that has **not** itself run `neon git install` — the case that must never
|
|
68
|
+
* touch the network (auth included). Checked before authentication runs at all, since
|
|
69
|
+
* `core.hooksPath` can point at a directory shared by several repositories: the hook script
|
|
70
|
+
* firing here does not by itself mean *this* repo opted in.
|
|
71
|
+
*
|
|
72
|
+
* The `.neon` read is deliberately **not** the usual walk-up resolution
|
|
73
|
+
* ({@link currentContextFile}): a `post-checkout` hook always runs with `cwd` at the
|
|
74
|
+
* repository's top level, and opt-in must be recorded by *this* repo's own `.neon` — an
|
|
75
|
+
* ancestor directory's `.neon` (found by the walk-up, e.g. a parent monorepo folder that
|
|
76
|
+
* happens to hold one for an unrelated project) must never authorize it.
|
|
77
|
+
*/
|
|
78
|
+
const isUnfollowedGitHookSync = (args, cwd = process.cwd()) => {
|
|
79
|
+
if (!(args._[0] === "git" && args._[1] === "sync")) return false;
|
|
80
|
+
if (process.env[GIT_HOOK_ENV_FLAG] !== "1") return false;
|
|
81
|
+
return readContextFile(resolve(cwd, CONTEXT_FILE)).git?.follow !== true;
|
|
82
|
+
};
|
|
55
83
|
const isSkillsCommand = (args) => args._[0] === "skills" || args._[0] === "skill";
|
|
56
84
|
const isPluginsCommand = (args) => args._[0] === "plugins" || args._[0] === "plugin";
|
|
57
85
|
const isAskCommand = (args) => args._[0] === "ask";
|
|
@@ -163,12 +191,73 @@ const updateContextFile = (file, context) => {
|
|
|
163
191
|
* `.neon` we never touch `.gitignore`, so a user who deliberately un-ignored
|
|
164
192
|
* the file (e.g. to commit shared context) won't have the entry re-added on
|
|
165
193
|
* every subsequent command.
|
|
194
|
+
*
|
|
195
|
+
* The {@link Context.git} block is additionally **preserved** when the caller doesn't
|
|
196
|
+
* provide one, so a plain `link` / `set-context` / `checkout` never wipes the git → Neon
|
|
197
|
+
* mapping a user (or `neon git sync`) built up. Pass an explicit `git` to replace it.
|
|
198
|
+
*
|
|
199
|
+
* That preservation is skipped when `context.projectId` differs from the file's existing
|
|
200
|
+
* `projectId`: the mapping's Neon branch **names** are only meaningful within the project
|
|
201
|
+
* they were recorded against, and carrying them into a different project could point
|
|
202
|
+
* `neon git cleanup --prune-neon-branches` at a same-named branch that was never actually
|
|
203
|
+
* mapped there.
|
|
166
204
|
*/
|
|
167
205
|
const applyContext = (file, context) => {
|
|
168
206
|
const isNewFile = !existsSync(file);
|
|
169
|
-
|
|
207
|
+
const existing = isNewFile ? {} : readContextFile(file);
|
|
208
|
+
const projectChanged = existing.projectId !== void 0 && context.projectId !== existing.projectId;
|
|
209
|
+
const merged = context.git === void 0 && existing.git !== void 0 && !projectChanged ? {
|
|
210
|
+
...context,
|
|
211
|
+
git: existing.git
|
|
212
|
+
} : context;
|
|
213
|
+
updateContextFile(file, merged);
|
|
170
214
|
if (isNewFile) ensureGitignored(file);
|
|
171
215
|
};
|
|
216
|
+
/** Look up the Neon branch a git branch maps to in the persisted `.neon` context. */
|
|
217
|
+
const gitBranchMapping = (context, gitBranch) => context.git?.map?.[gitBranch];
|
|
218
|
+
/** The full git → Neon mapping from the context (a copy; empty when unset). */
|
|
219
|
+
const gitBranchMap = (context) => ({ ...context.git?.map });
|
|
220
|
+
/** Replace the git → Neon mapping in `.neon`, preserving `git.follow` and the rest. */
|
|
221
|
+
const setGitBranchMap = (file, map) => {
|
|
222
|
+
const context = readContextFile(file);
|
|
223
|
+
applyContext(file, {
|
|
224
|
+
...context,
|
|
225
|
+
git: {
|
|
226
|
+
...context.git,
|
|
227
|
+
map
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Record a git branch -> Neon branch mapping in `.neon`, merging into any existing map and
|
|
233
|
+
* preserving the rest of the context. Passes an explicit `git` so {@link applyContext}
|
|
234
|
+
* replaces (rather than just preserves) the block with the new entry folded in.
|
|
235
|
+
*/
|
|
236
|
+
const setGitBranchMapping = (file, gitBranch, neonBranch) => {
|
|
237
|
+
const context = readContextFile(file);
|
|
238
|
+
const git = {
|
|
239
|
+
...context.git,
|
|
240
|
+
map: {
|
|
241
|
+
...context.git?.map,
|
|
242
|
+
[gitBranch]: neonBranch
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
applyContext(file, {
|
|
246
|
+
...context,
|
|
247
|
+
git
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
/** Set the `git.follow` flag (whether the post-checkout hook drives `neon git sync`). */
|
|
251
|
+
const setGitFollow = (file, follow) => {
|
|
252
|
+
const context = readContextFile(file);
|
|
253
|
+
applyContext(file, {
|
|
254
|
+
...context,
|
|
255
|
+
git: {
|
|
256
|
+
...context.git,
|
|
257
|
+
follow
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
};
|
|
172
261
|
/**
|
|
173
262
|
* Low-level writer for callers that already hold the resolved identifiers and
|
|
174
263
|
* just need to record them — e.g. `init` or `projects create`, which create a
|
|
@@ -231,7 +320,7 @@ const ensureGitignoreEntry = (path, entry) => {
|
|
|
231
320
|
};
|
|
232
321
|
const basenameOf = (file) => {
|
|
233
322
|
const parts = file.split(/[\\/]/);
|
|
234
|
-
return parts[parts.length - 1] ||
|
|
323
|
+
return parts[parts.length - 1] || ".neon";
|
|
235
324
|
};
|
|
236
325
|
const hasGitignoreEntry = (content, entry) => {
|
|
237
326
|
return content.split(/\r?\n/).some((line) => gitignoreCovers(line.trim(), entry));
|
|
@@ -261,4 +350,4 @@ const globToRegExp = (pattern) => {
|
|
|
261
350
|
return new RegExp(`^${source}$`);
|
|
262
351
|
};
|
|
263
352
|
//#endregion
|
|
264
|
-
export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureDirectoryGitignored, ensureGitignored, isApiKeysCommand, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isInspectDbUrl, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
|
|
353
|
+
export { CONTEXT_FILE, applyContext, contextBranch, currentContextFile, enrichFromContext, ensureDirectoryGitignored, ensureGitignored, gitBranchMap, gitBranchMapping, isApiKeysCommand, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isGitLocalCommand, isInspectDbUrl, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, isUnfollowedGitHookSync, readContextFile, setContext, setGitBranchMap, setGitBranchMapping, setGitFollow, updateContextFile, walkContextFile };
|
package/dist/dev/env.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as resolveDevEnv, i as dropClaimableUnsupported, n as MissingBranchContextError, o as resolveNeonEnvVars, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-
|
|
1
|
+
import { a as resolveDevEnv, i as dropClaimableUnsupported, n as MissingBranchContextError, o as resolveNeonEnvVars, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-CtOH9xsl.js";
|
|
2
2
|
export { DevEnvMismatchError, MissingBranchContextError, ServiceNotOnBranchError, dropClaimableUnsupported, resolveDevEnv, resolveNeonEnvVars };
|
package/dist/env_services.js
CHANGED
|
@@ -1,2 +1,121 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NEON_SERVICES } from "./neon_services.js";
|
|
2
|
+
import { f as parseFunctionBaseUrlKey, t as NEON_ENV_VAR_KEYS, u as isFunctionBaseUrlKey } from "./_chunks/env-N19A7qND.js";
|
|
3
|
+
//#region src/env_services.ts
|
|
4
|
+
const ENV_PULL_SERVICES = NEON_SERVICES;
|
|
5
|
+
const ENV_PULL_KEYS = [
|
|
6
|
+
...Object.values(NEON_ENV_VAR_KEYS.postgres),
|
|
7
|
+
NEON_ENV_VAR_KEYS.branch.name,
|
|
8
|
+
...Object.values(NEON_ENV_VAR_KEYS.auth),
|
|
9
|
+
...Object.values(NEON_ENV_VAR_KEYS.dataApi),
|
|
10
|
+
...Object.values(NEON_ENV_VAR_KEYS.storage),
|
|
11
|
+
...Object.values(NEON_ENV_VAR_KEYS.aiGateway)
|
|
12
|
+
];
|
|
13
|
+
const ENV_PULL_KEY_HELP = `${ENV_PULL_KEYS.join(", ")}, or NEON_FUNCTION_<SLUG>_BASE_URL`;
|
|
14
|
+
/** The OS-level env vars each service contributes to a pulled `.env`. */
|
|
15
|
+
const SERVICE_ENV_KEYS = {
|
|
16
|
+
postgres: Object.values(NEON_ENV_VAR_KEYS.postgres),
|
|
17
|
+
auth: Object.values(NEON_ENV_VAR_KEYS.auth),
|
|
18
|
+
"data-api": Object.values(NEON_ENV_VAR_KEYS.dataApi),
|
|
19
|
+
"object-storage": Object.values(NEON_ENV_VAR_KEYS.storage),
|
|
20
|
+
"ai-gateway": Object.values(NEON_ENV_VAR_KEYS.aiGateway),
|
|
21
|
+
functions: []
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The subset of {@link SERVICE_ENV_KEYS} a pull *owns*, and so may prune from the target file
|
|
25
|
+
* when the branch no longer has it. Object storage is deliberately absent: it is emitted under
|
|
26
|
+
* the third-party `AWS_*` names, which collide with credentials a user may set by hand, so
|
|
27
|
+
* `env pull` only ever writes them. Function URL keys cannot live here because slugs are
|
|
28
|
+
* discovered at runtime.
|
|
29
|
+
*/
|
|
30
|
+
const SERVICE_OWNED_ENV_KEYS = {
|
|
31
|
+
...SERVICE_ENV_KEYS,
|
|
32
|
+
"object-storage": [],
|
|
33
|
+
functions: []
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Branch identity. Not a service — every branch has a name — so a scoped pull refreshes it
|
|
37
|
+
* alongside whatever services were selected.
|
|
38
|
+
*/
|
|
39
|
+
const BRANCH_ENV_KEY = NEON_ENV_VAR_KEYS.branch.name;
|
|
40
|
+
const ENV_KEY_SERVICE = {
|
|
41
|
+
DATABASE_URL: "postgres",
|
|
42
|
+
DATABASE_URL_UNPOOLED: "postgres",
|
|
43
|
+
NEON_BRANCH: null,
|
|
44
|
+
NEON_AUTH_BASE_URL: "auth",
|
|
45
|
+
NEON_AUTH_JWKS_URL: "auth",
|
|
46
|
+
NEON_DATA_API_URL: "data-api",
|
|
47
|
+
AWS_ACCESS_KEY_ID: "object-storage",
|
|
48
|
+
AWS_SECRET_ACCESS_KEY: "object-storage",
|
|
49
|
+
AWS_ENDPOINT_URL_S3: "object-storage",
|
|
50
|
+
AWS_REGION: "object-storage",
|
|
51
|
+
NEON_AI_GATEWAY_TOKEN: "ai-gateway",
|
|
52
|
+
NEON_AI_GATEWAY_BASE_URL: "ai-gateway"
|
|
53
|
+
};
|
|
54
|
+
const serviceForEnvKey = (key) => isFunctionBaseUrlKey(key) ? "functions" : ENV_KEY_SERVICE[key];
|
|
55
|
+
/** Services that must be resolved to produce the selected env keys. */
|
|
56
|
+
const servicesForEnvKeys = (keys) => ENV_PULL_SERVICES.filter((service) => keys.some((key) => serviceForEnvKey(key) === service));
|
|
57
|
+
/** Every env var the selected services contribute, plus branch identity. */
|
|
58
|
+
const envServiceKeys = (services) => {
|
|
59
|
+
const keys = /* @__PURE__ */ new Set([BRANCH_ENV_KEY]);
|
|
60
|
+
for (const service of services) for (const key of SERVICE_ENV_KEYS[service]) keys.add(key);
|
|
61
|
+
return keys;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The env vars a pull scoped to `services` may prune. Narrower than the unscoped set on
|
|
65
|
+
* purpose: `env pull -s ai-gateway` says nothing about `DATABASE_URL`, so it must leave it
|
|
66
|
+
* alone rather than treat its absence from this pull as "the branch no longer has it".
|
|
67
|
+
* Function URL keys cannot live here because slugs are discovered at runtime.
|
|
68
|
+
*/
|
|
69
|
+
const ownedEnvServiceKeys = (services) => services.flatMap((service) => SERVICE_OWNED_ENV_KEYS[service]);
|
|
70
|
+
const envKeysForSelection = (services, envKeys) => {
|
|
71
|
+
const selected = services.length > 0 ? envServiceKeys(services) : /* @__PURE__ */ new Set();
|
|
72
|
+
for (const key of envKeys) selected.add(key);
|
|
73
|
+
if (selected.has(NEON_ENV_VAR_KEYS.storage.accessKeyId) !== selected.has(NEON_ENV_VAR_KEYS.storage.secretAccessKey)) throw new Error("AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be selected together: they are two halves of one newly issued object-storage credential. Add the missing key to --env, or use --service object-storage.");
|
|
74
|
+
const staticKeys = ENV_PULL_KEYS.filter((key) => selected.has(key));
|
|
75
|
+
const functionKeys = [...selected].filter(isFunctionBaseUrlKey).sort((left, right) => left.localeCompare(right));
|
|
76
|
+
return [...staticKeys, ...functionKeys];
|
|
77
|
+
};
|
|
78
|
+
/** Parse repeated or comma-separated `--env` values into canonical env-key order. */
|
|
79
|
+
const parseEnvPullKeys = (raw, flag) => {
|
|
80
|
+
const names = raw.flatMap((value) => value.split(",")).map((name) => name.trim()).filter((name) => name !== "");
|
|
81
|
+
const supported = `Supported values: ${ENV_PULL_KEY_HELP}.`;
|
|
82
|
+
if (names.length === 0) throw new Error(`${flag} needs at least one env variable. ${supported}`);
|
|
83
|
+
const unknown = names.filter((name) => !ENV_PULL_KEYS.some((key) => key === name) && !isFunctionBaseUrlKey(name));
|
|
84
|
+
if (unknown.length > 0) {
|
|
85
|
+
const displayNames = unknown.map(redactUnknownEnvValue);
|
|
86
|
+
const suggestions = [...new Set(unknown.map(suggestEnvPullKey).filter((key) => key !== null))];
|
|
87
|
+
const suggestion = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
|
|
88
|
+
throw new Error(`Unknown env variable${unknown.length === 1 ? "" : "s"} ${displayNames.join(", ")}.${suggestion} ${supported}`);
|
|
89
|
+
}
|
|
90
|
+
const staticKeys = ENV_PULL_KEYS.filter((key) => names.includes(key));
|
|
91
|
+
const functionKeys = [...new Set(names.filter(isFunctionBaseUrlKey))].sort((left, right) => left.localeCompare(right));
|
|
92
|
+
return [...staticKeys, ...functionKeys];
|
|
93
|
+
};
|
|
94
|
+
const isKnownEnvPullKey = (key) => ENV_PULL_KEYS.some((supportedKey) => supportedKey === key) || isFunctionBaseUrlKey(key);
|
|
95
|
+
const redactUnknownEnvValue = (value) => {
|
|
96
|
+
const separator = value.indexOf("=");
|
|
97
|
+
if (separator !== -1) {
|
|
98
|
+
const key = value.slice(0, separator);
|
|
99
|
+
return isKnownEnvPullKey(key) ? `${key}=<redacted>` : "<redacted invalid value>";
|
|
100
|
+
}
|
|
101
|
+
return "<redacted invalid value>";
|
|
102
|
+
};
|
|
103
|
+
const suggestEnvPullKey = (value) => {
|
|
104
|
+
if (value.includes("=")) return null;
|
|
105
|
+
if (parseFunctionBaseUrlKey(value) !== null) return null;
|
|
106
|
+
const closest = ENV_PULL_KEYS.map((key) => [key, editDistance(value, key)]).sort((a, b) => a[1] - b[1])[0];
|
|
107
|
+
return closest && closest[1] <= 2 ? closest[0] : null;
|
|
108
|
+
};
|
|
109
|
+
const editDistance = (left, right) => {
|
|
110
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
111
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
112
|
+
const current = [leftIndex];
|
|
113
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) current[rightIndex] = Math.min((previous[rightIndex] ?? 0) + 1, (current[rightIndex - 1] ?? 0) + 1, (previous[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
|
|
114
|
+
previous = current;
|
|
115
|
+
}
|
|
116
|
+
return previous[right.length] ?? right.length;
|
|
117
|
+
};
|
|
118
|
+
/** Narrow yargs' array option value without accepting any other runtime shape. */
|
|
119
|
+
const envPullFlagValue = (value) => Array.isArray(value) ? value.map(String) : void 0;
|
|
120
|
+
//#endregion
|
|
2
121
|
export { BRANCH_ENV_KEY, ENV_PULL_KEYS, ENV_PULL_KEY_HELP, ENV_PULL_SERVICES, envKeysForSelection, envPullFlagValue, envServiceKeys, ownedEnvServiceKeys, parseEnvPullKeys, serviceForEnvKey, servicesForEnvKeys };
|