neon 7.0.0 → 7.0.2
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 +9 -2
- package/dist/commands/bucket.js +2 -2
- package/dist/commands/config.js +1 -1
- package/dist/commands/functions.js +1 -1
- package/dist/commands/snapshots.js +1 -1
- package/dist/commands/vpc_endpoints.js +5 -6
- package/dist/help.js +51 -12
- package/dist/index.js +6 -3
- package/dist/notices.js +47 -0
- package/dist/update_notifier.js +109 -47
- package/dist/utils/json_file.js +20 -0
- package/dist/utils/package_manager.js +17 -6
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -47,10 +47,17 @@ brew upgrade neonctl
|
|
|
47
47
|
To upgrade a binary version, download the latest binary file, as described above, and replace your old binary with the new one.
|
|
48
48
|
|
|
49
49
|
Global npm, pnpm, Bun, and Homebrew installs check for updates once per day.
|
|
50
|
-
When an update is available, terminal commands
|
|
51
|
-
|
|
50
|
+
When an update is available, terminal commands print the matching upgrade command
|
|
51
|
+
every three days. Set `NO_UPDATE_NOTIFIER=1` to disable these checks. The notifier
|
|
52
52
|
does not run in CI, for machine-readable output, or from standalone binaries.
|
|
53
53
|
|
|
54
|
+
On macOS and Linux, the same terminal commands warn once a day when more than one
|
|
55
|
+
global `neon` install is on PATH, listing each one in PATH order. A project's
|
|
56
|
+
`node_modules/.bin`, which `npx` and package scripts add, doesn't count. When one
|
|
57
|
+
of them is Homebrew, the warning recommends `brew uninstall neonctl`, which
|
|
58
|
+
leaves `neon` running the other install, such as npm. While duplicates remain, the update notice is held back;
|
|
59
|
+
`NO_UPDATE_NOTIFIER=1` silences both.
|
|
60
|
+
|
|
54
61
|
## Connect
|
|
55
62
|
|
|
56
63
|
Run the following command to authenticate a connection to Neon:
|
package/dist/commands/bucket.js
CHANGED
|
@@ -82,7 +82,7 @@ const builder = (argv) => argv.usage("$0 bucket <sub-command> [options]").option
|
|
|
82
82
|
demandOption: true
|
|
83
83
|
}).options(scopeOptions),
|
|
84
84
|
handler: (args) => deleteBucket(args)
|
|
85
|
-
}).command("object
|
|
85
|
+
}).command("object", "List, download, upload or delete objects in a bucket", (yargs) => yargs.usage("$0 bucket object <sub-command> [options]").command({
|
|
86
86
|
command: "list <target>",
|
|
87
87
|
aliases: ["ls"],
|
|
88
88
|
describe: "List objects in a bucket. By default folders are collapsed (like \"aws s3 ls\"); pass --recursive for a flat listing of every key",
|
|
@@ -153,7 +153,7 @@ const builder = (argv) => argv.usage("$0 bucket <sub-command> [options]").option
|
|
|
153
153
|
}
|
|
154
154
|
}),
|
|
155
155
|
handler: (args) => deleteObject(args)
|
|
156
|
-
}).
|
|
156
|
+
}).strictCommands());
|
|
157
157
|
const handler = (args) => {
|
|
158
158
|
return args;
|
|
159
159
|
};
|
package/dist/commands/config.js
CHANGED
|
@@ -476,7 +476,7 @@ const builder = (argv) => argv.usage("$0 config <sub-command> [options]").option
|
|
|
476
476
|
kind: "bucket",
|
|
477
477
|
name: args.name,
|
|
478
478
|
access: args.access
|
|
479
|
-
})))
|
|
479
|
+
})))).command("init", "Scaffold a neon.ts policy and install the Neon config packages", (yargs) => yargs.options({
|
|
480
480
|
install: {
|
|
481
481
|
describe: "Install @neon/config and @neon/env if they're missing. On by default; use --no-install to just print the command.",
|
|
482
482
|
type: "boolean",
|
|
@@ -129,7 +129,7 @@ const builder = (argv) => argv.usage("$0 function <sub-command> [options]").opti
|
|
|
129
129
|
describe: "Function slug",
|
|
130
130
|
type: "string",
|
|
131
131
|
demandOption: true
|
|
132
|
-
}), (args) => deleteFn(args)).command(["domains", "domain"], "Manage custom domains on the branch (beta)", (yargs) => yargs.
|
|
132
|
+
}), (args) => deleteFn(args)).command(["domains", "domain"], "Manage custom domains on the branch (beta)", (yargs) => yargs.command("list", "List custom domains on the branch", (yargs) => yargs, (args) => listDomains(args)).command("register <domain>", "Point a domain you already own at a function on the branch", (yargs) => yargs.positional("domain", {
|
|
133
133
|
describe: "Domain you already own (for example docs.example.com)",
|
|
134
134
|
type: "string",
|
|
135
135
|
demandOption: true
|
|
@@ -146,7 +146,7 @@ const builder = (argv) => argv.usage("$0 snapshots <sub-command> [options]").opt
|
|
|
146
146
|
describe: "Full schedule as JSON, for multi-entry schedules, e.g. '[{\"frequency\":\"daily\",\"hour\":3,\"retention_seconds\":604800}]'. Overrides the single-entry flags.",
|
|
147
147
|
type: "string"
|
|
148
148
|
}
|
|
149
|
-
}).example([["$0 snapshots schedule set --branch main --frequency daily --hour 3 --retention 604800", "A daily 03:00 snapshot kept for 7 days"], ["$0 snapshots schedule set --branch main --schedule '[{\"frequency\":\"weekly\",\"day\":1,\"hour\":2},{\"frequency\":\"daily\",\"hour\":3}]'", "A multi-entry schedule via JSON"]]), (args) => scheduleSet(args))
|
|
149
|
+
}).example([["$0 snapshots schedule set --branch main --frequency daily --hour 3 --retention 604800", "A daily 03:00 snapshot kept for 7 days"], ["$0 snapshots schedule set --branch main --schedule '[{\"frequency\":\"weekly\",\"day\":1,\"hour\":2},{\"frequency\":\"daily\",\"hour\":3}]'", "A multi-entry schedule via JSON"]]), (args) => scheduleSet(args)), () => {});
|
|
150
150
|
const handler = (args) => {
|
|
151
151
|
return args;
|
|
152
152
|
};
|
|
@@ -28,10 +28,9 @@ const builder = (argv) => {
|
|
|
28
28
|
},
|
|
29
29
|
"region-id": {
|
|
30
30
|
describe: `The region ID. Possible values: ${REGIONS.join(", ")}`,
|
|
31
|
-
type: "string"
|
|
32
|
-
demandOption: true
|
|
31
|
+
type: "string"
|
|
33
32
|
}
|
|
34
|
-
}).middleware(fillSingleOrg).command("list", "List configured VPC endpoints for this organization.", (yargs) => yargs, async (args) => {
|
|
33
|
+
}).middleware(fillSingleOrg).command("list", "List configured VPC endpoints for this organization.", (yargs) => yargs.demandOption("region-id"), async (args) => {
|
|
35
34
|
await listOrg(args);
|
|
36
35
|
}).command({
|
|
37
36
|
command: "assign <id>",
|
|
@@ -40,13 +39,13 @@ const builder = (argv) => {
|
|
|
40
39
|
builder: (yargs) => yargs.options({ label: {
|
|
41
40
|
describe: "An optional descriptive label for the VPC endpoint",
|
|
42
41
|
type: "string"
|
|
43
|
-
} }),
|
|
42
|
+
} }).demandOption("region-id"),
|
|
44
43
|
handler: async (args) => {
|
|
45
44
|
await assignOrg(args);
|
|
46
45
|
}
|
|
47
|
-
}).command("remove <id>", "Remove a VPC endpoint from this organization.", (yargs) => yargs, async (args) => {
|
|
46
|
+
}).command("remove <id>", "Remove a VPC endpoint from this organization.", (yargs) => yargs.demandOption("region-id"), async (args) => {
|
|
48
47
|
await removeOrg(args);
|
|
49
|
-
}).command("status <id>", "Get the status of a VPC endpoint for this organization.", (yargs) => yargs, async (args) => {
|
|
48
|
+
}).command("status <id>", "Get the status of a VPC endpoint for this organization.", (yargs) => yargs.demandOption("region-id"), async (args) => {
|
|
50
49
|
await statusOrg(args);
|
|
51
50
|
});
|
|
52
51
|
}).command("project", "Manage project-level VPC endpoint restrictions.\nBy default, connections are accepted from any VPC configured at the organization level.\nA project-level VPC endpoint restriction can be used to restrict connections to a specific VPC.", (yargs) => {
|
package/dist/help.js
CHANGED
|
@@ -232,19 +232,58 @@ const formatHelp = (help) => {
|
|
|
232
232
|
}
|
|
233
233
|
return [...result, ...lines];
|
|
234
234
|
};
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
resolve();
|
|
245
|
-
});
|
|
235
|
+
const writeHelp = (help, stream) => new Promise((resolve, reject) => {
|
|
236
|
+
stream.write(`${formatHelp(help).join("\n")}\n`, (err) => {
|
|
237
|
+
if (err) {
|
|
238
|
+
reject(err);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
resolve();
|
|
246
242
|
});
|
|
243
|
+
});
|
|
244
|
+
const showHelp = async (argv) => {
|
|
245
|
+
await writeHelp(await argv.getHelp(), process.stdout);
|
|
247
246
|
process.exit(0);
|
|
248
247
|
};
|
|
248
|
+
/**
|
|
249
|
+
* A yargs validation failure: a missing required option or positional, an unknown
|
|
250
|
+
* subcommand, a value outside `choices`. Carries the failing command's help, captured
|
|
251
|
+
* in `.fail` because yargs has restored the parent's context by the time the error
|
|
252
|
+
* reaches the caller.
|
|
253
|
+
*/
|
|
254
|
+
var UsageError = class extends Error {
|
|
255
|
+
constructor(message, help) {
|
|
256
|
+
super(message);
|
|
257
|
+
this.help = help;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
/** On stderr, so `-o json` stdout stays empty. */
|
|
261
|
+
const showUsageErrorHelp = (err) => writeHelp(err.help, process.stderr);
|
|
262
|
+
/**
|
|
263
|
+
* The yargs `.fail` handler. yargs passes its usage instance as the third argument
|
|
264
|
+
* (`@types/yargs` declares it as `Argv`). Errors thrown by a handler, a `coerce`, or the
|
|
265
|
+
* parser arrive with their own `err` and are rethrown unchanged.
|
|
266
|
+
*/
|
|
267
|
+
const failOnUsageError = (msg, err, usage) => {
|
|
268
|
+
if (err) throw err;
|
|
269
|
+
if (typeof usage !== "object" || usage === null || !("help" in usage) || typeof usage.help !== "function") throw new Error("yargs no longer passes its usage instance to .fail(); update failOnUsageError for the installed yargs");
|
|
270
|
+
const help = usage.help();
|
|
271
|
+
if (typeof help !== "string") throw new Error("yargs usage.help() did not return a string");
|
|
272
|
+
throw new UsageError(msg ?? "Invalid usage", help);
|
|
273
|
+
};
|
|
274
|
+
function assertYargsInternals(argv) {
|
|
275
|
+
if (!("getInternalMethods" in argv) || typeof argv.getInternalMethods !== "function") throw new Error("yargs no longer exposes getInternalMethods(); update isBareParentCommand for the installed yargs");
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* True when the command yargs is running groups subcommands and none was given, at any
|
|
279
|
+
* depth: `neon config add`, `neon snapshots schedule`. `@types/yargs` doesn't declare the
|
|
280
|
+
* running command's context, so this reads yargs 17's internals.
|
|
281
|
+
*/
|
|
282
|
+
const isBareParentCommand = (argv, positionals) => {
|
|
283
|
+
assertYargsInternals(argv);
|
|
284
|
+
const internals = argv.getInternalMethods();
|
|
285
|
+
const path = internals.getContext().commands;
|
|
286
|
+
return path.length > 0 && positionals.length === path.length && internals.getCommandInstance().getCommands().length > 0;
|
|
287
|
+
};
|
|
249
288
|
//#endregion
|
|
250
|
-
export { showHelp };
|
|
289
|
+
export { UsageError, failOnUsageError, isBareParentCommand, showHelp, showUsageErrorHelp };
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { AuthRefreshError, defaultClientID } from "./auth.js";
|
|
|
11
11
|
import { getCliName } from "./utils/cli_name.js";
|
|
12
12
|
import { ensureAuth } from "./commands/auth.js";
|
|
13
13
|
import { recoverFrom401 } from "./auth_recovery.js";
|
|
14
|
-
import { showHelp } from "./help.js";
|
|
14
|
+
import { UsageError, failOnUsageError, isBareParentCommand, showHelp, showUsageErrorHelp } from "./help.js";
|
|
15
15
|
import { rewriteUnknownAgentArg } from "./init/plan.js";
|
|
16
16
|
import commands_default from "./commands/index.js";
|
|
17
17
|
import { recoverFromSSO } from "./sso_recovery.js";
|
|
@@ -125,13 +125,16 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
|
|
|
125
125
|
type: "boolean",
|
|
126
126
|
default: false
|
|
127
127
|
}).alias("help", "h").middleware(async (args) => {
|
|
128
|
-
if (args.help
|
|
129
|
-
}
|
|
128
|
+
if (args.help) await showHelp(builder);
|
|
129
|
+
}, true).middleware(async (args) => {
|
|
130
|
+
if (args._.length === 1 && !NO_SUBCOMMANDS_VERBS.includes(args._[0]) || args._.length > 1 && isBareParentCommand(builder, args._)) await showHelp(builder);
|
|
131
|
+
}).middleware(ensureAuth).middleware(enrichFromContext).middleware(analyticsMiddleware).command(commands_default).strictCommands().version(pkg_default.version).group("version", "Global options:").alias("version", "v").completion().scriptName(getCliName()).epilog("For more information, visit https://neon.com/docs/reference/neon-cli").wrap(null).fail(failOnUsageError);
|
|
130
132
|
async function handleError(msg, err, canRetry) {
|
|
131
133
|
if (process.argv.some((arg) => arg === "--help" || arg === "-h")) {
|
|
132
134
|
await showHelp(builder);
|
|
133
135
|
process.exit(0);
|
|
134
136
|
}
|
|
137
|
+
if (err instanceof UsageError) await showUsageErrorHelp(err);
|
|
135
138
|
if (err instanceof Error && err.stack) log.debug("Stack: %s", err.stack);
|
|
136
139
|
if (err instanceof Error) {
|
|
137
140
|
const rewritten = rewriteUnknownAgentArg({
|
package/dist/notices.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { log } from "./log.js";
|
|
2
|
+
import { writeJsonFile } from "./utils/json_file.js";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
//#region src/notices.ts
|
|
6
|
+
/**
|
|
7
|
+
* When each terminal notice was last shown, so a notice repeats on its own interval rather
|
|
8
|
+
* than on every command. Kept apart from the update cache: that cache is tied to one install
|
|
9
|
+
* source and rewritten by a background worker, while a notice can come from any copy.
|
|
10
|
+
*
|
|
11
|
+
* Throttling is best effort. Two commands started together can both show a due notice.
|
|
12
|
+
*/
|
|
13
|
+
const NOTICES_FILE = "notices.json";
|
|
14
|
+
/**
|
|
15
|
+
* Keeps ids this version doesn't know, so an older and a newer CLI sharing one config
|
|
16
|
+
* directory don't erase each other's timestamps.
|
|
17
|
+
*/
|
|
18
|
+
const parseNoticeLog = (raw) => {
|
|
19
|
+
let value;
|
|
20
|
+
try {
|
|
21
|
+
value = JSON.parse(raw);
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
26
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "number" && Number.isFinite(entry[1])));
|
|
27
|
+
};
|
|
28
|
+
const isNoticeDue = (shownAt, now, intervalMs) => shownAt === void 0 || now - shownAt >= intervalMs;
|
|
29
|
+
const readNoticeLog = (path) => {
|
|
30
|
+
try {
|
|
31
|
+
return parseNoticeLog(readFileSync(path, "utf8"));
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const showNotice = ({ configDir, id, intervalMs, message, now = Date.now() }) => {
|
|
37
|
+
const path = join(configDir, NOTICES_FILE);
|
|
38
|
+
const shown = readNoticeLog(path);
|
|
39
|
+
if (!isNoticeDue(shown[id], now, intervalMs)) return;
|
|
40
|
+
log.warning("%s", message);
|
|
41
|
+
writeJsonFile(path, {
|
|
42
|
+
...shown,
|
|
43
|
+
[id]: now
|
|
44
|
+
}, "CLI notice log");
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { isNoticeDue, parseNoticeLog, showNotice };
|
package/dist/update_notifier.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { isCi } from "./env.js";
|
|
2
2
|
import { log } from "./log.js";
|
|
3
|
-
import { recommendedCliUpgradeCommand } from "./utils/package_manager.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { detectCliInstallMethod, recommendedCliUpgradeCommand } from "./utils/package_manager.js";
|
|
4
|
+
import { writeJsonFile } from "./utils/json_file.js";
|
|
5
|
+
import { showNotice } from "./notices.js";
|
|
6
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
6
8
|
import { fileURLToPath } from "node:url";
|
|
7
9
|
import { spawn } from "node:child_process";
|
|
10
|
+
import which from "which";
|
|
8
11
|
import semver from "semver";
|
|
9
12
|
//#region src/update_notifier.ts
|
|
10
|
-
const
|
|
11
|
-
const
|
|
13
|
+
const DAY_MS = 864e5;
|
|
14
|
+
const CHECK_INTERVAL_MS = DAY_MS;
|
|
15
|
+
const UPDATE_NOTICE_INTERVAL_MS = 3 * DAY_MS;
|
|
16
|
+
const DUPLICATE_NOTICE_INTERVAL_MS = DAY_MS;
|
|
12
17
|
const CACHE_FILE = "update-check.json";
|
|
13
18
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
19
|
const parseUpdateCheckCache = (raw) => {
|
|
15
20
|
try {
|
|
16
21
|
const value = JSON.parse(raw);
|
|
17
|
-
if (!isRecord(value) || typeof value.checkedAt !== "number" || !Number.isFinite(value.checkedAt) || value.source !== "homebrew" && value.source !== "npm" || value.latestVersion !== void 0 && typeof value.latestVersion !== "string"
|
|
22
|
+
if (!isRecord(value) || typeof value.checkedAt !== "number" || !Number.isFinite(value.checkedAt) || value.source !== "homebrew" && value.source !== "npm" || value.latestVersion !== void 0 && typeof value.latestVersion !== "string") return;
|
|
18
23
|
return {
|
|
19
24
|
checkedAt: value.checkedAt,
|
|
20
25
|
source: value.source,
|
|
21
|
-
...value.latestVersion === void 0 ? {} : { latestVersion: value.latestVersion }
|
|
22
|
-
...value.notifiedAt === void 0 ? {} : { notifiedAt: value.notifiedAt }
|
|
26
|
+
...value.latestVersion === void 0 ? {} : { latestVersion: value.latestVersion }
|
|
23
27
|
};
|
|
24
28
|
} catch {
|
|
25
29
|
return;
|
|
@@ -32,20 +36,7 @@ const readUpdateCheckCache = (cachePath) => {
|
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
34
38
|
};
|
|
35
|
-
const writeUpdateCheckCache = (cachePath, cache) =>
|
|
36
|
-
const temporaryPath = `${cachePath}.${process.pid}.tmp`;
|
|
37
|
-
try {
|
|
38
|
-
writeFileSync(temporaryPath, `${JSON.stringify(cache)}\n`, { mode: 384 });
|
|
39
|
-
renameSync(temporaryPath, cachePath);
|
|
40
|
-
return true;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
try {
|
|
43
|
-
rmSync(temporaryPath, { force: true });
|
|
44
|
-
} catch {}
|
|
45
|
-
log.debug("Could not write the CLI update cache: %s", error instanceof Error ? error.message : String(error));
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
39
|
+
const writeUpdateCheckCache = (cachePath, cache) => writeJsonFile(cachePath, cache, "CLI update cache");
|
|
49
40
|
const formatUpdateNotice = ({ currentVersion, latestVersion, upgradeCommand }) => {
|
|
50
41
|
if (semver.valid(currentVersion) === null || semver.valid(latestVersion) === null || !semver.gt(latestVersion, currentVersion)) return;
|
|
51
42
|
return `Neon CLI update available: ${currentVersion} → ${latestVersion}\n${upgradeCommand}`;
|
|
@@ -84,6 +75,71 @@ const spawnUpdateWorker = (cachePath, source) => {
|
|
|
84
75
|
log.debug("Could not start the CLI update check: %s", error instanceof Error ? error.message : String(error));
|
|
85
76
|
}
|
|
86
77
|
};
|
|
78
|
+
const INSTALL_LABELS = {
|
|
79
|
+
homebrew: "Homebrew",
|
|
80
|
+
bun: "Bun",
|
|
81
|
+
pnpm: "pnpm",
|
|
82
|
+
npm: "npm"
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* `npm exec`, `npx`, package scripts, and `dlx` prepend a project's or a cache's
|
|
86
|
+
* `node_modules/.bin`, which is gone from PATH once the command ends.
|
|
87
|
+
*/
|
|
88
|
+
const isPackageBin = (executable) => {
|
|
89
|
+
const bin = dirname(executable);
|
|
90
|
+
return basename(bin) === ".bin" && basename(dirname(bin)) === "node_modules";
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Every distinct `command` executable on `path`, in PATH order, leaving out
|
|
94
|
+
* package-local `node_modules/.bin` entries. Entries that resolve to the same
|
|
95
|
+
* file (a repeated PATH directory, Homebrew's `bin` and `opt` links) count once.
|
|
96
|
+
*/
|
|
97
|
+
const findCliInstalls = (command, path) => {
|
|
98
|
+
const seen = /* @__PURE__ */ new Set();
|
|
99
|
+
const installs = [];
|
|
100
|
+
for (const executable of which.sync(command, {
|
|
101
|
+
all: true,
|
|
102
|
+
nothrow: true,
|
|
103
|
+
path
|
|
104
|
+
}) ?? []) {
|
|
105
|
+
if (isPackageBin(executable)) continue;
|
|
106
|
+
const target = realpathSync(executable);
|
|
107
|
+
if (seen.has(target)) continue;
|
|
108
|
+
seen.add(target);
|
|
109
|
+
installs.push({
|
|
110
|
+
method: detectCliInstallMethod(target),
|
|
111
|
+
path: executable
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return installs;
|
|
115
|
+
};
|
|
116
|
+
const formatDuplicateInstallNotice = ({ installs, otherNeonctl }) => {
|
|
117
|
+
if (installs.length < 2) return void 0;
|
|
118
|
+
const lines = ["Multiple Neon CLI installs found on PATH:", ...installs.map(({ method, path }, index) => {
|
|
119
|
+
const labels = [...method === void 0 ? [] : [INSTALL_LABELS[method]], ...index === 0 ? ["first on PATH"] : []];
|
|
120
|
+
return labels.length === 0 ? ` ${path}` : ` ${path} (${labels.join(", ")})`;
|
|
121
|
+
})];
|
|
122
|
+
if (!installs.some(({ method }) => method === "homebrew")) {
|
|
123
|
+
lines.push("Keep one and uninstall the others.");
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
lines.push(installs.some(({ method }) => method === "npm") ? "Remove the Homebrew install to use npm:" : "Remove the Homebrew install:", " brew uninstall neonctl");
|
|
127
|
+
if (!otherNeonctl) lines.push("This also removes the `neonctl` command; run `neon` instead.");
|
|
128
|
+
lines.push("Then open a new shell.");
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
};
|
|
131
|
+
const duplicateInstallNotice = () => {
|
|
132
|
+
try {
|
|
133
|
+
const installs = findCliInstalls("neon", process.env.PATH);
|
|
134
|
+
return formatDuplicateInstallNotice({
|
|
135
|
+
installs,
|
|
136
|
+
otherNeonctl: findCliInstalls("neonctl", process.env.PATH).some(({ method }) => method !== "homebrew")
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
log.debug("Could not check PATH for other Neon CLI installs: %s", error instanceof Error ? error.message : String(error));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
87
143
|
const isPackagedExecutable = () => "pkg" in process;
|
|
88
144
|
const shouldRunUpdateNotifier = ({ commandPath, currentBranch, disabled, isCi: runningInCi, isPackaged, output, stderrIsTty, stdoutIsTty }) => !runningInCi && !disabled && !isPackaged && output === "table" && stdoutIsTty && stderrIsTty && commandPath.length > 0 && commandPath[0] !== "completion" && !isCurrentBranchProbe({
|
|
89
145
|
commandPath,
|
|
@@ -100,37 +156,43 @@ const notifyIfUpdateAvailable = ({ commandPath, configDir, currentBranch, curren
|
|
|
100
156
|
stderrIsTty: process.stderr.isTTY === true,
|
|
101
157
|
stdoutIsTty: process.stdout.isTTY === true
|
|
102
158
|
})) return;
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
if (process.platform !== "win32") {
|
|
161
|
+
const duplicateNotice = duplicateInstallNotice();
|
|
162
|
+
if (duplicateNotice !== void 0) {
|
|
163
|
+
showNotice({
|
|
164
|
+
configDir,
|
|
165
|
+
id: "duplicate-installs",
|
|
166
|
+
intervalMs: DUPLICATE_NOTICE_INTERVAL_MS,
|
|
167
|
+
message: duplicateNotice,
|
|
168
|
+
now
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
103
173
|
const command = recommendedCliUpgradeCommand(fileURLToPath(import.meta.url));
|
|
104
174
|
if (command === void 0) return;
|
|
105
175
|
const cachePath = join(configDir, CACHE_FILE);
|
|
106
|
-
const now = Date.now();
|
|
107
176
|
const source = updateSource(command);
|
|
108
177
|
const cache = cacheForUpdateSource(readUpdateCheckCache(cachePath), source);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!shouldRefreshUpdateCheck(cache, now)) {
|
|
125
|
-
if (nextCache !== cache && nextCache !== void 0) writeUpdateCheckCache(cachePath, nextCache);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
nextCache = {
|
|
129
|
-
...nextCache,
|
|
178
|
+
const notice = cache?.latestVersion === void 0 ? void 0 : formatUpdateNotice({
|
|
179
|
+
currentVersion,
|
|
180
|
+
latestVersion: cache.latestVersion,
|
|
181
|
+
upgradeCommand: command
|
|
182
|
+
});
|
|
183
|
+
if (notice !== void 0) showNotice({
|
|
184
|
+
configDir,
|
|
185
|
+
id: "update",
|
|
186
|
+
intervalMs: UPDATE_NOTICE_INTERVAL_MS,
|
|
187
|
+
message: notice,
|
|
188
|
+
now
|
|
189
|
+
});
|
|
190
|
+
if (!shouldRefreshUpdateCheck(cache, now)) return;
|
|
191
|
+
if (writeUpdateCheckCache(cachePath, {
|
|
192
|
+
...cache,
|
|
130
193
|
checkedAt: now,
|
|
131
194
|
source
|
|
132
|
-
};
|
|
133
|
-
if (writeUpdateCheckCache(cachePath, nextCache)) spawnUpdateWorker(cachePath, source);
|
|
195
|
+
})) spawnUpdateWorker(cachePath, source);
|
|
134
196
|
};
|
|
135
197
|
const recordLatestVersion = (cachePath, source, latestVersion) => {
|
|
136
198
|
if (semver.valid(latestVersion) === null) return;
|
|
@@ -142,4 +204,4 @@ const recordLatestVersion = (cachePath, source, latestVersion) => {
|
|
|
142
204
|
});
|
|
143
205
|
};
|
|
144
206
|
//#endregion
|
|
145
|
-
export { cacheForUpdateSource, formatUpdateNotice, notifyIfUpdateAvailable, parseUpdateCheckCache, recordLatestVersion, shouldRefreshUpdateCheck, shouldRunUpdateNotifier, spawnUpdateWorkerProcess, writeUpdateCheckCache };
|
|
207
|
+
export { cacheForUpdateSource, findCliInstalls, formatDuplicateInstallNotice, formatUpdateNotice, notifyIfUpdateAvailable, parseUpdateCheckCache, recordLatestVersion, shouldRefreshUpdateCheck, shouldRunUpdateNotifier, spawnUpdateWorkerProcess, writeUpdateCheckCache };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { log } from "../log.js";
|
|
2
|
+
import { renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
//#region src/utils/json_file.ts
|
|
4
|
+
/** Renames a temporary file into place so a concurrent reader never sees a partial write. */
|
|
5
|
+
const writeJsonFile = (path, value, description) => {
|
|
6
|
+
const temporaryPath = `${path}.${process.pid}.tmp`;
|
|
7
|
+
try {
|
|
8
|
+
writeFileSync(temporaryPath, `${JSON.stringify(value)}\n`, { mode: 384 });
|
|
9
|
+
renameSync(temporaryPath, path);
|
|
10
|
+
return true;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
try {
|
|
13
|
+
rmSync(temporaryPath, { force: true });
|
|
14
|
+
} catch {}
|
|
15
|
+
log.debug("Could not write the %s: %s", description, error instanceof Error ? error.message : String(error));
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
export { writeJsonFile };
|
|
@@ -159,12 +159,23 @@ const resolvePackageManager = (cwd) => inferPackageManager(cwd) ?? installedPack
|
|
|
159
159
|
* as {@link resolvePackageManager} minus the lockfile walk.
|
|
160
160
|
*/
|
|
161
161
|
const resolveInvokingPackageManager = () => detectInvokingPackageManager() ?? installedPackageManagers()[0] ?? "npm";
|
|
162
|
+
/** How the CLI file at `path` was installed globally, or undefined when it can't be told. */
|
|
163
|
+
const detectCliInstallMethod = (path) => {
|
|
164
|
+
const normalized = path.replace(/\\/g, "/").toLowerCase();
|
|
165
|
+
if (normalized.includes("/cellar/neonctl/")) return "homebrew";
|
|
166
|
+
if (normalized.includes("/.bun/install/global/node_modules/neon/")) return "bun";
|
|
167
|
+
if (normalized.includes("/pnpm/global/") && normalized.includes("/.pnpm/neon@") && normalized.includes("/node_modules/neon/")) return "pnpm";
|
|
168
|
+
if (normalized.includes("/lib/node_modules/neon/") || normalized.includes("/npm/node_modules/neon/")) return "npm";
|
|
169
|
+
};
|
|
170
|
+
const CLI_UPGRADE_COMMANDS = {
|
|
171
|
+
homebrew: "brew upgrade neonctl",
|
|
172
|
+
bun: "bun i -g neon@latest",
|
|
173
|
+
pnpm: "pnpm i -g neon@latest",
|
|
174
|
+
npm: "npm i -g neon@latest"
|
|
175
|
+
};
|
|
162
176
|
const recommendedCliUpgradeCommand = (modulePath) => {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
if (path.includes("/.bun/install/global/node_modules/neon/")) return "bun i -g neon@latest";
|
|
166
|
-
if (path.includes("/pnpm/global/") && path.includes("/.pnpm/neon@") && path.includes("/node_modules/neon/")) return "pnpm i -g neon@latest";
|
|
167
|
-
if (path.includes("/lib/node_modules/neon/") || path.includes("/npm/node_modules/neon/")) return "npm i -g neon@latest";
|
|
177
|
+
const method = detectCliInstallMethod(modulePath);
|
|
178
|
+
return method === void 0 ? void 0 : CLI_UPGRADE_COMMANDS[method];
|
|
168
179
|
};
|
|
169
180
|
/**
|
|
170
181
|
* The argv for an install with `pm`: every dependency in the manifest when
|
|
@@ -333,4 +344,4 @@ const runCommand = (cmd, args, cwd, env) => new Promise((resolvePromise) => {
|
|
|
333
344
|
});
|
|
334
345
|
});
|
|
335
346
|
//#endregion
|
|
336
|
-
export { DO_NOT_SUBSTITUTE_HINT, MISSING_BINARY_HINT, commandEnv, detectInvokingPackageManager, detectProjectPackageManager, formatExecCommand, formatInstallCommand, globalInstallCommand, inferPackageManager, installArgs, installedPackageManagers, recommendedCliUpgradeCommand, resolveInvokingPackageManager, resolvePackageManager, runCommand };
|
|
347
|
+
export { DO_NOT_SUBSTITUTE_HINT, MISSING_BINARY_HINT, commandEnv, detectCliInstallMethod, detectInvokingPackageManager, detectProjectPackageManager, formatExecCommand, formatInstallCommand, globalInstallCommand, inferPackageManager, installArgs, installedPackageManagers, recommendedCliUpgradeCommand, resolveInvokingPackageManager, resolvePackageManager, runCommand };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.2",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"yargs": "17.7.2",
|
|
66
66
|
"yoctocolors": "^2.1.2",
|
|
67
67
|
"@neon/config": "1.8.3",
|
|
68
|
-
"@neon/
|
|
69
|
-
"@neon/
|
|
68
|
+
"@neon/sdk": "7.0.0",
|
|
69
|
+
"@neon/config-runtime": "1.7.3"
|
|
70
70
|
},
|
|
71
71
|
"optionalDependencies": {
|
|
72
72
|
"@napi-rs/keyring": "1.3.0",
|