neon 8.11.4 → 8.12.1
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 +14 -1
- package/dist/_chunks/{skills-DyiBXKS3.js → skills-BP6pIm7O.js} +35 -4
- package/dist/analytics.js +3 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/index.js +24 -5
- package/dist/init/tooling.js +1 -1
- package/dist/skills/check_worker.js +17 -0
- package/dist/skills/freshness.js +397 -0
- package/dist/skills/run.js +1 -2
- package/dist/test_utils/fixtures.js +1 -0
- package/dist/update_notifier.js +15 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -58,6 +58,13 @@ of them is Homebrew, the warning recommends `brew uninstall neonctl`, which
|
|
|
58
58
|
leaves `neon` running the other install, such as npm. While duplicates remain, the update notice is held back;
|
|
59
59
|
`NO_UPDATE_NOTIFIER=1` silences both.
|
|
60
60
|
|
|
61
|
+
Commands also warn once a day when Neon agent skills are behind their source, in this
|
|
62
|
+
directory's `skills-lock.json` or the user-level skills lock (see
|
|
63
|
+
[`neon skills status`](#install-neon-agent-skills-skills)). This warning runs without a
|
|
64
|
+
terminal too, so coding agents see it. It checks GitHub once a day in the background and
|
|
65
|
+
follows the same CI, machine-readable output, standalone binary, and
|
|
66
|
+
`NO_UPDATE_NOTIFIER=1` rules.
|
|
67
|
+
|
|
61
68
|
## Connect
|
|
62
69
|
|
|
63
70
|
Run the following command to authenticate a connection to Neon:
|
|
@@ -973,8 +980,14 @@ $ neon skills --global
|
|
|
973
980
|
$ neon skills update
|
|
974
981
|
$ neon skills update -y
|
|
975
982
|
$ neon skills update --global -y
|
|
983
|
+
|
|
984
|
+
# Check whether installed Neon skills are behind their source.
|
|
985
|
+
$ neon skills status
|
|
986
|
+
$ neon skills status --global
|
|
976
987
|
```
|
|
977
988
|
|
|
989
|
+
`neon skills status` compares each Neon skill in this directory's `skills-lock.json` (or, with `--global`, the user-level skills lock) with the skill folder on its source repository's default branch, and prints `up to date`, `outdated`, or `not found upstream` per skill. It reads GitHub anonymously; set `GITHUB_TOKEN` or `GH_TOKEN` if the GitHub API rate limit is reached. Skills installed through an agent plugin are not in either lock and are not listed.
|
|
990
|
+
|
|
978
991
|
On a TTY the command asks which agents and which skills, then installs. Detected agents start selected from project-folder markers such as `.cursor`. Default skills start selected. `neon-postgres-agent-platforms` is offered and starts unselected.
|
|
979
992
|
|
|
980
993
|
`-y` skips those questions and installs the default skills into detected agents: project-folder markers such as `.cursor`, else the agent driving the CLI. `--agent` / `-a` names coding agents and skips the agent picker. `--skill` / `-s` names specific skills and skips the skill picker; it does not select agents. `--global -y` uses installed apps, else the host CLI agent. Without a TTY, pass `-y`, or `--skill <name>` (add `--agent <name>` to name agents). If `-y` finds no agent, it exits: pass `--agent <name>`, run from a supported agent, or omit `-y` in a terminal to pick.
|
|
@@ -1565,7 +1578,7 @@ Global options are supported with any Neon CLI command.
|
|
|
1565
1578
|
|
|
1566
1579
|
- <a id="analytics"></a>`--analytics`
|
|
1567
1580
|
|
|
1568
|
-
Analytics are enabled by default to gather information about the CLI commands and options that are used by our customers. This data collection assists in offering support, and allows for a better understanding of typical usage patterns so that we can improve user experience. Neon does not collect user-defined data, such as project IDs or command payloads, except the question passed to `neon ask --prompt` and the query passed to `neon docs search`. The Neon search service records each `neon docs search` query with the CLI version and the detected coding agent; with `--no-analytics` it records nothing. When the CLI fails on an internal bug (a JavaScript `TypeError`, `ReferenceError`, `RangeError`, or `SyntaxError`), it also sends that error to Neon's Sentry: the error type, the message only when it is a standard JavaScript message that names nothing but code (any other message is replaced), the stack trace with file paths cut to the package (`neon/dist/...`), the CLI and Node.js versions, and whether it ran in CI or a coding agent. To opt-out of analytics data collection and error reports, specify `--no-analytics` or `--analytics false`.
|
|
1581
|
+
Analytics are enabled by default to gather information about the CLI commands and options that are used by our customers. This data collection assists in offering support, and allows for a better understanding of typical usage patterns so that we can improve user experience. Neon does not collect user-defined data, such as project IDs or command payloads, except the question passed to `neon ask --prompt` and the query passed to `neon docs search`. Each successful command also reports the Neon agent skills recorded in the user-level skills lock and in the current directory's `skills-lock.json`: the skill names, their lock hashes, and whether each is behind its source, read from local files without a network request. The Neon search service records each `neon docs search` query with the CLI version and the detected coding agent; with `--no-analytics` it records nothing. When the CLI fails on an internal bug (a JavaScript `TypeError`, `ReferenceError`, `RangeError`, or `SyntaxError`), it also sends that error to Neon's Sentry: the error type, the message only when it is a standard JavaScript message that names nothing but code (any other message is replaced), the stack trace with file paths cut to the package (`neon/dist/...`), the CLI and Node.js versions, and whether it ran in CI or a coding agent. To opt-out of analytics data collection and error reports, specify `--no-analytics` or `--analytics false`.
|
|
1569
1582
|
|
|
1570
1583
|
- <a id="version"></a>`-v, --version`
|
|
1571
1584
|
|
|
@@ -10,9 +10,10 @@ import { noPassthrough } from "../utils/flags.js";
|
|
|
10
10
|
import { AGENT_SKILLS_SKIP_MESSAGE } from "../init/copy.js";
|
|
11
11
|
import { canPickAgentsInteractively, skippableAgentPicker } from "../utils/agent_picker.js";
|
|
12
12
|
import { skillsHelpValues, skillsYesHelp } from "../skills/catalog.js";
|
|
13
|
+
import { lockPath, neonSkillsUpdateCommand, readLockedSkills, refreshSkillsCheckCache, scopeLabel, skillsCheckCachePath, skillsStatusRows } from "../skills/freshness.js";
|
|
13
14
|
import { confirmSkillsUpdate } from "../skills/wizard.js";
|
|
14
15
|
import { assertSkillsCanRun, resolveSkillsPlan } from "../skills/plan.js";
|
|
15
|
-
import { assertSkillsNode, neonSkillsRetryCommand,
|
|
16
|
+
import { assertSkillsNode, neonSkillsRetryCommand, runSkillsCli, skillsAddArgs, skillsMetadata, skillsUpdateArgs, skillsUpdateDetail, skillsUpdateHadNothing } from "../skills/run.js";
|
|
16
17
|
//#region src/commands/skills.ts
|
|
17
18
|
var skills_exports = /* @__PURE__ */ __exportAll({
|
|
18
19
|
aliases: () => aliases,
|
|
@@ -24,7 +25,6 @@ var skills_exports = /* @__PURE__ */ __exportAll({
|
|
|
24
25
|
reportSkillsInstall: () => reportSkillsInstall,
|
|
25
26
|
skillsInstallError: () => skillsInstallError
|
|
26
27
|
});
|
|
27
|
-
const scopeLabel = (scope) => scope === "project" ? "this directory" : "user-level";
|
|
28
28
|
const coerceAgents = (value) => {
|
|
29
29
|
if (value === void 0) return [];
|
|
30
30
|
const list = Array.isArray(value) ? value : [value];
|
|
@@ -60,7 +60,17 @@ const builder = (argv) => argv.usage("$0 skills [command] [options]").command("u
|
|
|
60
60
|
const skills = args.skill;
|
|
61
61
|
if (Array.isArray(skills) && skills.length > 0) throw new Error("neon skills update does not take --skill. It refreshes every installed skill in this directory (or --global).");
|
|
62
62
|
return true;
|
|
63
|
-
}), (args) => updateHandler(args)).options({
|
|
63
|
+
}), (args) => updateHandler(args)).command("status", "Check whether installed Neon skills match their source", (yargs) => yargs.usage("$0 skills status [options]").options({ global: {
|
|
64
|
+
type: "boolean",
|
|
65
|
+
default: false,
|
|
66
|
+
describe: "Check user-level skills (skills CLI -g). Default is this directory"
|
|
67
|
+
} }).example("$0 skills status", "Neon skills in this directory's skills-lock.json").example("$0 skills status --global", "User-level Neon skills").strict().hide("agent").hide("skill").hide("yes").check((args) => {
|
|
68
|
+
noPassthrough("skills status")(args);
|
|
69
|
+
const agents = args.agent;
|
|
70
|
+
const skills = args.skill;
|
|
71
|
+
if (Array.isArray(agents) && agents.length > 0 || Array.isArray(skills) && skills.length > 0) throw new Error("neon skills status does not take --agent or --skill. It checks every Neon skill in this directory (or --global).");
|
|
72
|
+
return true;
|
|
73
|
+
}), (args) => statusHandler(args)).options({
|
|
64
74
|
yes: {
|
|
65
75
|
alias: "y",
|
|
66
76
|
type: "boolean",
|
|
@@ -249,7 +259,28 @@ const updateHandler = async (props) => {
|
|
|
249
259
|
title: "Skills"
|
|
250
260
|
});
|
|
251
261
|
out.end();
|
|
252
|
-
if (failure !== void 0) throw new Error(failure.includes("needs npx (Node.js)") || failure.includes("needs Node.js") ? failure : `${failure}\nRetry with: ${
|
|
262
|
+
if (failure !== void 0) throw new Error(failure.includes("needs npx (Node.js)") || failure.includes("needs Node.js") ? failure : `${failure}\nRetry with: ${neonSkillsUpdateCommand(scope)}`);
|
|
263
|
+
};
|
|
264
|
+
const statusHandler = async (props) => {
|
|
265
|
+
const scope = props.global === true ? "global" : "project";
|
|
266
|
+
const skills = readLockedSkills(lockPath(scope, process.cwd()), scope);
|
|
267
|
+
const folders = skills.length === 0 ? {} : await refreshSkillsCheckCache({
|
|
268
|
+
cachePath: skillsCheckCachePath(props.configDir),
|
|
269
|
+
skills
|
|
270
|
+
});
|
|
271
|
+
const rows = skillsStatusRows(skills, folders);
|
|
272
|
+
const out = writer(props);
|
|
273
|
+
out.write(rows, {
|
|
274
|
+
fields: [
|
|
275
|
+
"scope",
|
|
276
|
+
"skill",
|
|
277
|
+
"status"
|
|
278
|
+
],
|
|
279
|
+
title: "Skills",
|
|
280
|
+
emptyMessage: scope === "project" ? "No Neon skills in this directory's skills-lock.json. For user-level skills, run: neon skills status --global" : "No user-level Neon skills in the skills CLI lock."
|
|
281
|
+
});
|
|
282
|
+
out.end();
|
|
283
|
+
if (rows.some((row) => row.status === "outdated")) log.info("Update them: %s", neonSkillsUpdateCommand(scope));
|
|
253
284
|
};
|
|
254
285
|
//#endregion
|
|
255
286
|
export { handler as a, skillsInstallError as c, describe as i, skills_exports as l, builder as n, installSkills as o, command as r, reportSkillsInstall as s, aliases as t };
|
package/dist/analytics.js
CHANGED
|
@@ -296,6 +296,8 @@ const trackEvent = (event, properties) => {
|
|
|
296
296
|
});
|
|
297
297
|
log.debug("Sent CLI event: %s", event);
|
|
298
298
|
};
|
|
299
|
+
/** Whether this process sends telemetry: analytics is on and the client was created. */
|
|
300
|
+
const isAnalyticsActive = () => client !== void 0;
|
|
299
301
|
/**
|
|
300
302
|
* Tracks cli_command_success. The account fields come from the lookup, which may still be in
|
|
301
303
|
* flight when the command finishes, so they are read when the event is sent.
|
|
@@ -354,4 +356,4 @@ const getAnalyticsEventProperties = (args) => ({
|
|
|
354
356
|
githubEnvVars: getGithubEnvVars(process.env)
|
|
355
357
|
});
|
|
356
358
|
//#endregion
|
|
357
|
-
export { analyticsIdentity, analyticsMiddleware, closeAnalytics, commandSuccessProperties, getAnalyticsEventProperties, getErrorAnalyticsEventProperties, initAnalyticsClientMiddleware, recordCommandSuccessExtras, recordScaffoldedTemplate, sendError, storedCredentialAttribution, takeCommandSuccessExtras, telemetryCredential, trackCommandSuccess, trackEvent, useAnalyticsClientForTests };
|
|
359
|
+
export { analyticsIdentity, analyticsMiddleware, closeAnalytics, commandSuccessProperties, getAnalyticsEventProperties, getErrorAnalyticsEventProperties, initAnalyticsClientMiddleware, isAnalyticsActive, recordCommandSuccessExtras, recordScaffoldedTemplate, sendError, storedCredentialAttribution, takeCommandSuccessExtras, telemetryCredential, trackCommandSuccess, trackEvent, useAnalyticsClientForTests };
|
package/dist/commands/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { u as projects_exports } from "../_chunks/projects-BwI2Uo0z.js";
|
|
|
8
8
|
import { a as link_exports } from "../_chunks/link-BgtkOBws.js";
|
|
9
9
|
import { o as mcp_exports } from "../_chunks/mcp-Bo-7TKc7.js";
|
|
10
10
|
import { c as plugins_exports } from "../_chunks/plugins-DqD7BDO1.js";
|
|
11
|
-
import { l as skills_exports } from "../_chunks/skills-
|
|
11
|
+
import { l as skills_exports } from "../_chunks/skills-BP6pIm7O.js";
|
|
12
12
|
import { t as bootstrap_exports } from "../_chunks/bootstrap-wPZWa1sT.js";
|
|
13
13
|
import { r as branches_exports } from "../_chunks/branches-BNa1xX1L.js";
|
|
14
14
|
import { n as bucket_exports } from "../_chunks/bucket-BCdryTkD.js";
|
package/dist/commands/skills.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as aliases, n as builder, r as command, i as describe, a as handler, o as installSkills, s as reportSkillsInstall, c as skillsInstallError } from "../_chunks/skills-
|
|
1
|
+
export { t as aliases, n as builder, r as command, i as describe, a as handler, o as installSkills, s as reportSkillsInstall, c as skillsInstallError } from "../_chunks/skills-BP6pIm7O.js";
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { r as defaultDir } from "./_chunks/paths-DMq0Lt7a.js";
|
|
|
6
6
|
import { ensureConfigDir } from "./config.js";
|
|
7
7
|
import { currentContextFile, enrichFromContext } from "./context.js";
|
|
8
8
|
import { NETWORK_ERROR_MESSAGE, isNetworkError, matchErrorCode } from "./errors.js";
|
|
9
|
-
import { analyticsMiddleware, closeAnalytics, initAnalyticsClientMiddleware, sendError, takeCommandSuccessExtras, trackCommandSuccess } from "./analytics.js";
|
|
9
|
+
import { analyticsMiddleware, closeAnalytics, initAnalyticsClientMiddleware, isAnalyticsActive, recordCommandSuccessExtras, sendError, takeCommandSuccessExtras, trackCommandSuccess } from "./analytics.js";
|
|
10
10
|
import { AuthRefreshError, defaultClientID } from "./auth.js";
|
|
11
11
|
import { getCliName } from "./utils/cli_name.js";
|
|
12
12
|
import { c as ensureAuth } from "./_chunks/auth-CIUtXByB.js";
|
|
@@ -15,7 +15,7 @@ import { UsageError, failOnUsageError, isBareParentCommand, showHelp, showUsageE
|
|
|
15
15
|
import { lazyCommands, preloadCommand } from "./commands/manifest.js";
|
|
16
16
|
import { rewriteUnknownAgentArg } from "./init/plan.js";
|
|
17
17
|
import { recoverFromSSO } from "./sso_recovery.js";
|
|
18
|
-
import { notifyIfUpdateAvailable } from "./update_notifier.js";
|
|
18
|
+
import { notifyIfUpdateAvailable, shouldRunSkillsNotifier } from "./update_notifier.js";
|
|
19
19
|
import { fillInArgs, resolveApiKeyFromEnv } from "./utils/middlewares.js";
|
|
20
20
|
import { collapseRepeatedFlags, declaredOptions } from "./utils/repeated_flags.js";
|
|
21
21
|
import { stagingFeaturesEnabledForArgv } from "./utils/staging.js";
|
|
@@ -208,19 +208,38 @@ async function handleError(msg, err, canRetry) {
|
|
|
208
208
|
await preloadCommand(builder, hideBin(process.argv));
|
|
209
209
|
while (attempts < MAX_ATTEMPTS) try {
|
|
210
210
|
const args = await builder.argv;
|
|
211
|
+
const configDir = typeof args.configDir === "string" ? args.configDir : defaultDir;
|
|
212
|
+
if (isAnalyticsActive()) {
|
|
213
|
+
const { readSkillsTelemetry } = await import("./skills/freshness.js");
|
|
214
|
+
const skills = readSkillsTelemetry({
|
|
215
|
+
configDir,
|
|
216
|
+
cwd: process.cwd()
|
|
217
|
+
});
|
|
218
|
+
if (skills !== void 0) recordCommandSuccessExtras({ skills });
|
|
219
|
+
}
|
|
211
220
|
trackCommandSuccess(args);
|
|
212
221
|
if (args._.length === 0 || args.help) {
|
|
213
222
|
await showHelp(builder);
|
|
214
223
|
process.exit(0);
|
|
215
224
|
}
|
|
216
225
|
await closeAnalytics();
|
|
217
|
-
|
|
226
|
+
const notifier = {
|
|
218
227
|
commandPath: args._.map(String),
|
|
219
|
-
configDir
|
|
228
|
+
configDir,
|
|
220
229
|
currentBranch: Boolean(args.currentBranch),
|
|
221
|
-
currentVersion: pkg_default.version,
|
|
222
230
|
output: typeof args.output === "string" ? args.output : "table"
|
|
231
|
+
};
|
|
232
|
+
notifyIfUpdateAvailable({
|
|
233
|
+
...notifier,
|
|
234
|
+
currentVersion: pkg_default.version
|
|
223
235
|
});
|
|
236
|
+
if (shouldRunSkillsNotifier(notifier)) {
|
|
237
|
+
const { notifyIfSkillsOutdated } = await import("./skills/freshness.js");
|
|
238
|
+
notifyIfSkillsOutdated({
|
|
239
|
+
configDir: notifier.configDir,
|
|
240
|
+
cwd: process.cwd()
|
|
241
|
+
});
|
|
242
|
+
}
|
|
224
243
|
break;
|
|
225
244
|
} catch (err) {
|
|
226
245
|
takeCommandSuccessExtras();
|
package/dist/init/tooling.js
CHANGED
|
@@ -7,7 +7,7 @@ import { detectAgent } from "./detect_host.js";
|
|
|
7
7
|
import { AgentSelectionSkipped, canPickAgentsInteractively } from "../utils/agent_picker.js";
|
|
8
8
|
import { a as mcpInstallError, s as reportMcpInstall } from "../_chunks/mcp-Bo-7TKc7.js";
|
|
9
9
|
import { l as reportPluginsInstall, o as installPlugins, s as pluginsInstallError } from "../_chunks/plugins-DqD7BDO1.js";
|
|
10
|
-
import { c as skillsInstallError, o as installSkills, s as reportSkillsInstall } from "../_chunks/skills-
|
|
10
|
+
import { c as skillsInstallError, o as installSkills, s as reportSkillsInstall } from "../_chunks/skills-BP6pIm7O.js";
|
|
11
11
|
import { runAuthenticatedMcp } from "./auth.js";
|
|
12
12
|
import { raceSigint } from "./cancelled.js";
|
|
13
13
|
import { pickAgentSetupInteractively } from "./wizard.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { globalLockPath, projectLockPath, readLockedSkills, refreshSkillsCheckCache } from "./freshness.js";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
//#region src/skills/check_worker.ts
|
|
4
|
+
const run = async () => {
|
|
5
|
+
const [cachePath, cwd] = process.argv.slice(2);
|
|
6
|
+
if (cachePath === void 0 || cwd === void 0) return;
|
|
7
|
+
try {
|
|
8
|
+
await refreshSkillsCheckCache({
|
|
9
|
+
cachePath,
|
|
10
|
+
skills: [...readLockedSkills(globalLockPath(), "global"), ...readLockedSkills(projectLockPath(cwd), "project")]
|
|
11
|
+
});
|
|
12
|
+
} catch {}
|
|
13
|
+
};
|
|
14
|
+
const entry = process.argv[1];
|
|
15
|
+
if (entry !== void 0 && import.meta.url === pathToFileURL(entry).href) run();
|
|
16
|
+
//#endregion
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { log } from "../log.js";
|
|
2
|
+
import { writeJsonFile } from "../utils/json_file.js";
|
|
3
|
+
import { showNotice } from "../notices.js";
|
|
4
|
+
import { AGENT_SKILLS_SOURCE, PLATFORMS_SKILLS_SOURCE } from "./catalog.js";
|
|
5
|
+
import { closeSync, constants, existsSync, fstatSync, openSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
//#region src/skills/freshness.ts
|
|
12
|
+
const DAY_MS = 864e5;
|
|
13
|
+
const CACHE_FILE = "skills-check.json";
|
|
14
|
+
const FETCH_TIMEOUT_MS = 1e4;
|
|
15
|
+
const NEON_SOURCES = /* @__PURE__ */ new Set([AGENT_SKILLS_SOURCE, PLATFORMS_SKILLS_SOURCE]);
|
|
16
|
+
const scopeLabel = (scope) => scope === "project" ? "this directory" : "user-level";
|
|
17
|
+
const neonSkillsUpdateCommand = (scope) => scope === "global" ? "neon skills update --global -y" : "neon skills update -y";
|
|
18
|
+
/** Where the skills CLI keeps its user-level lock. */
|
|
19
|
+
const globalLockPath = (env = process.env) => env.XDG_STATE_HOME ? join(env.XDG_STATE_HOME, "skills", ".skill-lock.json") : join(homedir(), ".agents", ".skill-lock.json");
|
|
20
|
+
const projectLockPath = (cwd) => join(cwd, "skills-lock.json");
|
|
21
|
+
const lockPath = (scope, cwd) => scope === "global" ? globalLockPath() : projectLockPath(cwd);
|
|
22
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
/** The Agent Skills name rule. A lock is repository content, and names reach the terminal. */
|
|
24
|
+
const SKILL_NAME = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
25
|
+
const skillFolder = (skillPath) => skillPath.replace(/\\/g, "/").replace(/\/?SKILL\.md$/i, "");
|
|
26
|
+
/**
|
|
27
|
+
* The global lock's `skillFolderHash` is usually a tree SHA, but the skills CLI stores a
|
|
28
|
+
* content SHA-256 there when it installed from a Git clone; it tells them apart by length.
|
|
29
|
+
*/
|
|
30
|
+
const parseSkillsLock = (raw, scope) => {
|
|
31
|
+
const value = JSON.parse(raw);
|
|
32
|
+
if (!isRecord(value) || !isRecord(value.skills)) return [];
|
|
33
|
+
const field = scope === "global" ? "skillFolderHash" : "computedHash";
|
|
34
|
+
const skills = [];
|
|
35
|
+
for (const [skill, entry] of Object.entries(value.skills)) {
|
|
36
|
+
if (!isRecord(entry)) continue;
|
|
37
|
+
const { source, skillPath, ref } = entry;
|
|
38
|
+
const hash = entry[field];
|
|
39
|
+
if (!SKILL_NAME.test(skill) || typeof source !== "string" || !NEON_SOURCES.has(source) || typeof skillPath !== "string" || typeof hash !== "string" || hash === "") continue;
|
|
40
|
+
const folder = skillFolder(skillPath);
|
|
41
|
+
if (folder === "") continue;
|
|
42
|
+
const hashKind = scope === "global" && /^[0-9a-f]{40}$/i.test(hash) ? "tree" : "content";
|
|
43
|
+
skills.push({
|
|
44
|
+
scope,
|
|
45
|
+
skill,
|
|
46
|
+
source,
|
|
47
|
+
folder,
|
|
48
|
+
hash,
|
|
49
|
+
hashKind,
|
|
50
|
+
pinned: ref !== void 0
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return skills;
|
|
54
|
+
};
|
|
55
|
+
const readAllLockedSkills = (path, scope) => {
|
|
56
|
+
if (!existsSync(path)) return [];
|
|
57
|
+
const raw = readLockFile(path);
|
|
58
|
+
try {
|
|
59
|
+
return parseSkillsLock(raw, scope);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(`${path} is not valid JSON.`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/** The skills the update check covers: every Neon skill in the lock except pinned ones. */
|
|
65
|
+
const readLockedSkills = (path, scope) => readAllLockedSkills(path, scope).filter((skill) => !skill.pinned);
|
|
66
|
+
const MAX_LOCK_BYTES = 1048576;
|
|
67
|
+
/**
|
|
68
|
+
* A project lock is repository content: a FIFO would block the open and `/dev/zero` the read,
|
|
69
|
+
* on every command run in that directory.
|
|
70
|
+
*/
|
|
71
|
+
const readLockFile = (path) => {
|
|
72
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
73
|
+
try {
|
|
74
|
+
const stat = fstatSync(fd);
|
|
75
|
+
if (!stat.isFile()) throw new Error(`${path} is not a regular file.`);
|
|
76
|
+
if (stat.size > MAX_LOCK_BYTES) throw new Error(`${path} is larger than ${MAX_LOCK_BYTES} bytes.`);
|
|
77
|
+
return readFileSync(fd, "utf8");
|
|
78
|
+
} finally {
|
|
79
|
+
closeSync(fd);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const folderKey = ({ source, folder }) => `${source}:${folder}`;
|
|
83
|
+
/** `undefined` when the remote side has not been fetched yet. */
|
|
84
|
+
const skillStatus = (skill, remote) => {
|
|
85
|
+
if (remote?.missing === true) return "not found upstream";
|
|
86
|
+
const latest = skill.hashKind === "tree" ? remote?.tree : remote?.content;
|
|
87
|
+
if (latest === void 0) return void 0;
|
|
88
|
+
return latest === skill.hash ? "up to date" : "outdated";
|
|
89
|
+
};
|
|
90
|
+
const parseRemoteFolder = (value) => {
|
|
91
|
+
if (!isRecord(value)) return void 0;
|
|
92
|
+
const { tree, content, missing, pending } = value;
|
|
93
|
+
if (tree !== void 0 && typeof tree !== "string" || content !== void 0 && typeof content !== "string" || missing !== void 0 && missing !== true || pending !== void 0 && pending !== true) return;
|
|
94
|
+
return {
|
|
95
|
+
...tree === void 0 ? {} : { tree },
|
|
96
|
+
...content === void 0 ? {} : { content },
|
|
97
|
+
...missing === void 0 ? {} : { missing },
|
|
98
|
+
...pending === void 0 ? {} : { pending }
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
const parseSkillsCheckCache = (raw) => {
|
|
102
|
+
try {
|
|
103
|
+
const value = JSON.parse(raw);
|
|
104
|
+
if (!isRecord(value) || typeof value.checkedAt !== "number" || !Number.isFinite(value.checkedAt) || !isRecord(value.folders)) return;
|
|
105
|
+
const folders = {};
|
|
106
|
+
for (const [key, entry] of Object.entries(value.folders)) {
|
|
107
|
+
const folder = parseRemoteFolder(entry);
|
|
108
|
+
if (folder !== void 0) folders[key] = folder;
|
|
109
|
+
}
|
|
110
|
+
const { attemptedAt } = value;
|
|
111
|
+
return {
|
|
112
|
+
checkedAt: value.checkedAt,
|
|
113
|
+
...typeof attemptedAt === "number" && Number.isFinite(attemptedAt) ? { attemptedAt } : {},
|
|
114
|
+
folders
|
|
115
|
+
};
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const readSkillsCheckCache = (cachePath) => {
|
|
121
|
+
try {
|
|
122
|
+
return parseSkillsCheckCache(readFileSync(cachePath, "utf8"));
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const skillsCheckCachePath = (configDir) => join(configDir, CACHE_FILE);
|
|
128
|
+
/**
|
|
129
|
+
* The digest the skills CLI writes to `computedHash`: every file's folder-relative path and
|
|
130
|
+
* then its bytes, in `localeCompare` order of path.
|
|
131
|
+
*/
|
|
132
|
+
const skillContentHash = (files) => {
|
|
133
|
+
const hash = createHash("sha256");
|
|
134
|
+
for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
135
|
+
hash.update(file.path);
|
|
136
|
+
hash.update(file.bytes);
|
|
137
|
+
}
|
|
138
|
+
return hash.digest("hex");
|
|
139
|
+
};
|
|
140
|
+
const gitBlobSha = (bytes) => createHash("sha1").update(`blob ${bytes.byteLength}\0`).update(bytes).digest("hex");
|
|
141
|
+
const isTreeEntry = (value) => isRecord(value) && typeof value.path === "string" && typeof value.mode === "string" && typeof value.type === "string" && typeof value.sha === "string";
|
|
142
|
+
const githubHeaders = (env) => {
|
|
143
|
+
const token = env.GITHUB_TOKEN || env.GH_TOKEN;
|
|
144
|
+
return {
|
|
145
|
+
Accept: "application/vnd.github+json",
|
|
146
|
+
"User-Agent": "neon-cli",
|
|
147
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const fetchTree = async (source, env) => {
|
|
151
|
+
const response = await fetch(`https://api.github.com/repos/${source}/git/trees/HEAD?recursive=1`, {
|
|
152
|
+
headers: githubHeaders(env),
|
|
153
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
if ((response.status === 403 || response.status === 429) && response.headers.get("x-ratelimit-remaining") === "0") throw new Error(`GitHub API rate limit reached while checking ${source}. Set GITHUB_TOKEN and retry.`);
|
|
157
|
+
throw new Error(`GitHub returned ${response.status} for the file tree of ${source}.`);
|
|
158
|
+
}
|
|
159
|
+
const body = await response.json();
|
|
160
|
+
if (!isRecord(body) || !Array.isArray(body.tree)) throw new Error(`GitHub returned an unexpected file tree for ${source}.`);
|
|
161
|
+
if (body.truncated === true) throw new Error(`GitHub truncated the file tree of ${source}.`);
|
|
162
|
+
return body.tree.filter(isTreeEntry);
|
|
163
|
+
};
|
|
164
|
+
/** The files the skills CLI hashes: regular files, outside `.git` and `node_modules`. */
|
|
165
|
+
const folderFiles = (tree, folder) => tree.filter((entry) => {
|
|
166
|
+
if (entry.type !== "blob" || entry.mode === "120000") return false;
|
|
167
|
+
if (!entry.path.startsWith(`${folder}/`)) return false;
|
|
168
|
+
const dirs = entry.path.slice(folder.length + 1).split("/").slice(0, -1);
|
|
169
|
+
return !dirs.includes(".git") && !dirs.includes("node_modules");
|
|
170
|
+
});
|
|
171
|
+
const fetchContentHash = async (source, folder, tree) => {
|
|
172
|
+
const files = await Promise.all(folderFiles(tree, folder).map(async (entry) => {
|
|
173
|
+
const path = entry.path.split("/").map(encodeURIComponent).join("/");
|
|
174
|
+
const response = await fetch(`https://raw.githubusercontent.com/${source}/HEAD/${path}`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
175
|
+
if (!response.ok) throw new Error(`GitHub returned ${response.status} for ${entry.path} in ${source}.`);
|
|
176
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
177
|
+
if (gitBlobSha(bytes) !== entry.sha) throw new Error(`${source} changed during the check. Retry.`);
|
|
178
|
+
return {
|
|
179
|
+
path: entry.path.slice(folder.length + 1),
|
|
180
|
+
bytes
|
|
181
|
+
};
|
|
182
|
+
}));
|
|
183
|
+
return skillContentHash(files);
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* The folders to fetch: the given skills, plus every folder already in the cache so
|
|
187
|
+
* results for other directories stay current.
|
|
188
|
+
*/
|
|
189
|
+
const folderRequests = (skills, previous) => {
|
|
190
|
+
const requests = /* @__PURE__ */ new Map();
|
|
191
|
+
const add = (request) => {
|
|
192
|
+
const key = folderKey(request);
|
|
193
|
+
const existing = requests.get(key);
|
|
194
|
+
requests.set(key, {
|
|
195
|
+
...request,
|
|
196
|
+
content: request.content || existing?.content === true
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
for (const [key, remote] of Object.entries(previous)) {
|
|
200
|
+
const split = key.lastIndexOf(":");
|
|
201
|
+
const source = key.slice(0, split);
|
|
202
|
+
if (split <= 0 || !NEON_SOURCES.has(source)) continue;
|
|
203
|
+
add({
|
|
204
|
+
source,
|
|
205
|
+
folder: key.slice(split + 1),
|
|
206
|
+
content: remote.content !== void 0
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
for (const skill of skills) add({
|
|
210
|
+
source: skill.source,
|
|
211
|
+
folder: skill.folder,
|
|
212
|
+
content: skill.hashKind === "content"
|
|
213
|
+
});
|
|
214
|
+
return [...requests.values()];
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Fetches the default-branch state of every requested skill folder and writes it to the
|
|
218
|
+
* cache. File contents are downloaded only for folders whose tree changed since the cache.
|
|
219
|
+
*/
|
|
220
|
+
const refreshSkillsCheckCache = async ({ cachePath, skills, env = process.env, now = Date.now() }) => {
|
|
221
|
+
const previous = readSkillsCheckCache(cachePath)?.folders ?? {};
|
|
222
|
+
const requests = folderRequests(skills, previous);
|
|
223
|
+
const folders = {};
|
|
224
|
+
for (const source of new Set(requests.map((request) => request.source))) {
|
|
225
|
+
const tree = await fetchTree(source, env);
|
|
226
|
+
for (const request of requests) {
|
|
227
|
+
if (request.source !== source) continue;
|
|
228
|
+
const key = folderKey(request);
|
|
229
|
+
const node = tree.find((entry) => entry.type === "tree" && entry.path === request.folder);
|
|
230
|
+
if (node === void 0) {
|
|
231
|
+
folders[key] = { missing: true };
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const prior = previous[key];
|
|
235
|
+
let content = prior?.tree === node.sha ? prior.content : void 0;
|
|
236
|
+
if (request.content && content === void 0) content = await fetchContentHash(source, request.folder, tree);
|
|
237
|
+
folders[key] = {
|
|
238
|
+
tree: node.sha,
|
|
239
|
+
...content === void 0 ? {} : { content }
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
writeJsonFile(cachePath, {
|
|
244
|
+
checkedAt: now,
|
|
245
|
+
folders
|
|
246
|
+
}, "skills check cache");
|
|
247
|
+
return folders;
|
|
248
|
+
};
|
|
249
|
+
const skillsStatusRows = (skills, folders) => skills.map((skill) => {
|
|
250
|
+
const status = skillStatus(skill, folders[folderKey(skill)]);
|
|
251
|
+
if (status === void 0) throw new Error(`No result for ${skill.skill} from ${skill.source}. Retry the check.`);
|
|
252
|
+
return {
|
|
253
|
+
scope: scopeLabel(skill.scope),
|
|
254
|
+
skill: skill.skill,
|
|
255
|
+
status
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
const formatSkillsNotice = (outdated) => {
|
|
259
|
+
if (outdated.length === 0) return void 0;
|
|
260
|
+
const scopes = ["global", "project"].filter((scope) => outdated.some((skill) => skill.scope === scope));
|
|
261
|
+
return [
|
|
262
|
+
"Neon agent skills have updates:",
|
|
263
|
+
...scopes.map((scope) => ` ${scopeLabel(scope)}: ${outdated.filter((skill) => skill.scope === scope).map((skill) => skill.skill).join(", ")}`),
|
|
264
|
+
"Update them:",
|
|
265
|
+
...scopes.map((scope) => ` ${neonSkillsUpdateCommand(scope)}`)
|
|
266
|
+
].join("\n");
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* `pending` folders are left alone until the next daily check. A lock written after the last
|
|
270
|
+
* check (an install or an update) holds back the warning and checks every skill again,
|
|
271
|
+
* because the cache can be older than what was just installed.
|
|
272
|
+
*/
|
|
273
|
+
const planSkillsCheck = ({ skills, cache, lockModifiedAt, now }) => {
|
|
274
|
+
const remote = (skill) => cache?.folders[folderKey(skill)];
|
|
275
|
+
const settled = (skill) => remote(skill)?.pending !== true;
|
|
276
|
+
const lockChanged = cache !== void 0 && lockModifiedAt > cache.checkedAt;
|
|
277
|
+
const outdated = lockChanged ? [] : skills.filter((skill) => settled(skill) && skillStatus(skill, remote(skill)) === "outdated");
|
|
278
|
+
const toCheck = skills.filter((skill) => settled(skill) && (lockChanged || skillStatus(skill, remote(skill)) === void 0));
|
|
279
|
+
return {
|
|
280
|
+
outdated,
|
|
281
|
+
toCheck,
|
|
282
|
+
refresh: cache === void 0 || toCheck.length > 0 || now - (cache.attemptedAt ?? cache.checkedAt) >= DAY_MS
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
const spawnSkillsCheckWorker = (cachePath, cwd) => {
|
|
286
|
+
const workerPath = fileURLToPath(new URL("./check_worker.js", import.meta.url));
|
|
287
|
+
if (!existsSync(workerPath)) return;
|
|
288
|
+
try {
|
|
289
|
+
const child = spawn(process.execPath, [
|
|
290
|
+
workerPath,
|
|
291
|
+
cachePath,
|
|
292
|
+
cwd
|
|
293
|
+
], {
|
|
294
|
+
detached: true,
|
|
295
|
+
stdio: "ignore",
|
|
296
|
+
windowsHide: true
|
|
297
|
+
});
|
|
298
|
+
child.on("error", (error) => {
|
|
299
|
+
log.debug("Skills check failed to start: %s", error.message);
|
|
300
|
+
});
|
|
301
|
+
child.unref();
|
|
302
|
+
} catch (error) {
|
|
303
|
+
log.debug("Could not start the skills check: %s", error instanceof Error ? error.message : String(error));
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
/** Every Neon skill in the lock, pinned ones included. */
|
|
307
|
+
const readLock = (path, scope) => ({
|
|
308
|
+
skills: readAllLockedSkills(path, scope),
|
|
309
|
+
modifiedAt: statSync(path, { throwIfNoEntry: false })?.mtimeMs ?? 0
|
|
310
|
+
});
|
|
311
|
+
const readLocks = (cwd) => ({
|
|
312
|
+
global: readLock(globalLockPath(), "global"),
|
|
313
|
+
project: readLock(projectLockPath(cwd), "project")
|
|
314
|
+
});
|
|
315
|
+
/**
|
|
316
|
+
* `outdated` is `null` when the cache cannot answer: no successful check yet, a check still
|
|
317
|
+
* pending for the folder, a lock written after the last check, a folder gone upstream, or a
|
|
318
|
+
* pinned skill.
|
|
319
|
+
*/
|
|
320
|
+
const skillsTelemetry = (locks, cache) => {
|
|
321
|
+
const scope = ({ skills, modifiedAt }) => {
|
|
322
|
+
if (skills.length === 0) return { installed: false };
|
|
323
|
+
const known = cache !== void 0 && cache.checkedAt > 0 && modifiedAt <= cache.checkedAt;
|
|
324
|
+
return {
|
|
325
|
+
installed: true,
|
|
326
|
+
skills: Object.fromEntries(skills.map((skill) => {
|
|
327
|
+
const remote = cache?.folders[folderKey(skill)];
|
|
328
|
+
const status = known && !skill.pinned && remote?.pending !== true ? skillStatus(skill, remote) : void 0;
|
|
329
|
+
return [skill.skill, {
|
|
330
|
+
hash: skill.hash,
|
|
331
|
+
outdated: status === "outdated" ? true : status === "up to date" ? false : null
|
|
332
|
+
}];
|
|
333
|
+
}))
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
checkedAt: cache !== void 0 && cache.checkedAt > 0 ? cache.checkedAt : null,
|
|
338
|
+
global: scope(locks.global),
|
|
339
|
+
project: scope(locks.project)
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Reads only local files. `undefined` when a lock can't be read, which leaves the property
|
|
344
|
+
* off the event rather than reporting skills as not installed.
|
|
345
|
+
*/
|
|
346
|
+
const readSkillsTelemetry = ({ configDir, cwd }) => {
|
|
347
|
+
try {
|
|
348
|
+
return skillsTelemetry(readLocks(cwd), readSkillsCheckCache(skillsCheckCachePath(configDir)));
|
|
349
|
+
} catch (error) {
|
|
350
|
+
log.debug("Leaving skills out of telemetry: %s", error instanceof Error ? error.message : String(error));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* Warns at most once a day when a Neon skill in the user-level lock or this directory's
|
|
356
|
+
* `skills-lock.json` is behind its source, from a cache a background worker refreshes daily.
|
|
357
|
+
*/
|
|
358
|
+
const notifyIfSkillsOutdated = ({ configDir, cwd, now = Date.now() }) => {
|
|
359
|
+
let locks;
|
|
360
|
+
try {
|
|
361
|
+
locks = readLocks(cwd);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
log.debug("Skipping the skills check: %s", error instanceof Error ? error.message : String(error));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const skills = [...locks.global.skills, ...locks.project.skills].filter((skill) => !skill.pinned);
|
|
367
|
+
if (skills.length === 0) return;
|
|
368
|
+
const cachePath = skillsCheckCachePath(configDir);
|
|
369
|
+
const cache = readSkillsCheckCache(cachePath);
|
|
370
|
+
const plan = planSkillsCheck({
|
|
371
|
+
skills,
|
|
372
|
+
cache,
|
|
373
|
+
lockModifiedAt: Math.max(locks.global.modifiedAt, locks.project.modifiedAt),
|
|
374
|
+
now
|
|
375
|
+
});
|
|
376
|
+
const notice = formatSkillsNotice(plan.outdated);
|
|
377
|
+
if (notice !== void 0) showNotice({
|
|
378
|
+
configDir,
|
|
379
|
+
id: "skills",
|
|
380
|
+
intervalMs: DAY_MS,
|
|
381
|
+
message: notice,
|
|
382
|
+
now
|
|
383
|
+
});
|
|
384
|
+
if (!plan.refresh) return;
|
|
385
|
+
const folders = { ...cache?.folders };
|
|
386
|
+
for (const skill of plan.toCheck) folders[folderKey(skill)] = {
|
|
387
|
+
...folders[folderKey(skill)],
|
|
388
|
+
pending: true
|
|
389
|
+
};
|
|
390
|
+
if (writeJsonFile(cachePath, {
|
|
391
|
+
checkedAt: cache?.checkedAt ?? 0,
|
|
392
|
+
attemptedAt: now,
|
|
393
|
+
folders
|
|
394
|
+
}, "skills check cache")) spawnSkillsCheckWorker(cachePath, cwd);
|
|
395
|
+
};
|
|
396
|
+
//#endregion
|
|
397
|
+
export { formatSkillsNotice, globalLockPath, lockPath, neonSkillsUpdateCommand, notifyIfSkillsOutdated, parseSkillsCheckCache, parseSkillsLock, planSkillsCheck, projectLockPath, readLockedSkills, readSkillsTelemetry, refreshSkillsCheckCache, scopeLabel, skillContentHash, skillStatus, skillsCheckCachePath, skillsStatusRows, skillsTelemetry };
|
package/dist/skills/run.js
CHANGED
|
@@ -59,7 +59,6 @@ const neonSkillsRetryCommand = (options) => {
|
|
|
59
59
|
args.push("-y");
|
|
60
60
|
return `neon ${args.map(quoteNpxArg).join(" ")}`;
|
|
61
61
|
};
|
|
62
|
-
const neonSkillsUpdateRetryCommand = (global) => global ? "neon skills update --global -y" : "neon skills update -y";
|
|
63
62
|
const quoteNpxArg = (part) => /[\s"'\\*]/.test(part) ? `'${part.replace(/'/g, `'\\''`)}'` : part;
|
|
64
63
|
const npxCommand = (args) => `npx ${args.map(quoteNpxArg).join(" ")}`;
|
|
65
64
|
const ANSI = /\u001b\[[0-9;]*[A-Za-z]/g;
|
|
@@ -116,4 +115,4 @@ const runSkillsCli = async (options) => {
|
|
|
116
115
|
const isCommandMissing = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
117
116
|
const isExecaFailure = (error) => typeof error === "object" && error !== null && "shortMessage" in error && typeof error.shortMessage === "string";
|
|
118
117
|
//#endregion
|
|
119
|
-
export { SKILLS_MIN_NODE, assertSkillsNode, neonSkillsRetryCommand,
|
|
118
|
+
export { SKILLS_MIN_NODE, assertSkillsNode, neonSkillsRetryCommand, nodeMeetsMinimum, npxCommand, quoteNpxArg, runSkillsCli, skillsAddArgs, skillsChildEnv, skillsMetadata, skillsUpdateArgs, skillsUpdateDetail, skillsUpdateHadNothing };
|
package/dist/update_notifier.js
CHANGED
|
@@ -145,6 +145,20 @@ const shouldRunUpdateNotifier = ({ commandPath, currentBranch, disabled, isCi: r
|
|
|
145
145
|
commandPath,
|
|
146
146
|
currentBranch
|
|
147
147
|
});
|
|
148
|
+
/**
|
|
149
|
+
* The skills notice drops the terminal checks: coding agents run the CLI without a TTY,
|
|
150
|
+
* and they are the ones that act on it. `neon skills` itself reports skill state.
|
|
151
|
+
*/
|
|
152
|
+
const shouldRunSkillsNotifier = ({ commandPath, currentBranch, output }) => commandPath[0] !== "skills" && commandPath[0] !== "skill" && shouldRunUpdateNotifier({
|
|
153
|
+
commandPath,
|
|
154
|
+
currentBranch,
|
|
155
|
+
disabled: "NO_UPDATE_NOTIFIER" in process.env,
|
|
156
|
+
isCi: isCi(),
|
|
157
|
+
isPackaged: isPackagedExecutable(),
|
|
158
|
+
output,
|
|
159
|
+
stderrIsTty: true,
|
|
160
|
+
stdoutIsTty: true
|
|
161
|
+
});
|
|
148
162
|
const notifyIfUpdateAvailable = ({ commandPath, configDir, currentBranch, currentVersion, output }) => {
|
|
149
163
|
if (!shouldRunUpdateNotifier({
|
|
150
164
|
commandPath,
|
|
@@ -204,4 +218,4 @@ const recordLatestVersion = (cachePath, source, latestVersion) => {
|
|
|
204
218
|
});
|
|
205
219
|
};
|
|
206
220
|
//#endregion
|
|
207
|
-
export { cacheForUpdateSource, findCliInstalls, formatDuplicateInstallNotice, formatUpdateNotice, notifyIfUpdateAvailable, parseUpdateCheckCache, recordLatestVersion, shouldRefreshUpdateCheck, shouldRunUpdateNotifier, spawnUpdateWorkerProcess, writeUpdateCheckCache };
|
|
221
|
+
export { cacheForUpdateSource, findCliInstalls, formatDuplicateInstallNotice, formatUpdateNotice, notifyIfUpdateAvailable, parseUpdateCheckCache, recordLatestVersion, shouldRefreshUpdateCheck, shouldRunSkillsNotifier, shouldRunUpdateNotifier, spawnUpdateWorkerProcess, writeUpdateCheckCache };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.12.1",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"yaml": "^2.9.0",
|
|
73
73
|
"yargs": "17.7.2",
|
|
74
74
|
"yoctocolors": "^2.1.2",
|
|
75
|
-
"@neon/config-runtime": "1.10.3",
|
|
76
75
|
"@neon/config": "1.9.4",
|
|
77
|
-
"@neon/
|
|
78
|
-
"@neon/realtime": "0.1.2"
|
|
76
|
+
"@neon/config-runtime": "1.10.3",
|
|
77
|
+
"@neon/realtime": "0.1.2",
|
|
78
|
+
"@neon/sdk": "7.5.0"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
81
|
"@napi-rs/keyring": "1.3.0",
|
|
@@ -111,11 +111,11 @@
|
|
|
111
111
|
"tsx": "4.22.3",
|
|
112
112
|
"typescript": "^5.9.0",
|
|
113
113
|
"vitest": "^3.0.9",
|
|
114
|
+
"@neon-internals/cli-core": "0.0.0",
|
|
114
115
|
"@neon-internals/env-core": "0.0.35",
|
|
115
|
-
"@neon/functions": "0.11.0",
|
|
116
|
-
"@neon/env": "1.6.5",
|
|
117
116
|
"@neon/e2e-harness": "0.0.0",
|
|
118
|
-
"@neon
|
|
117
|
+
"@neon/env": "1.6.5",
|
|
118
|
+
"@neon/functions": "0.11.0"
|
|
119
119
|
},
|
|
120
120
|
"publishConfig": {
|
|
121
121
|
"access": "public",
|