neon 8.12.0 → 8.12.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 +1 -1
- package/dist/analytics.js +3 -1
- package/dist/index.js +11 -2
- package/dist/skills/freshness.js +59 -11
- package/dist/skills/run.js +2 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1578,7 +1578,7 @@ Global options are supported with any Neon CLI command.
|
|
|
1578
1578
|
|
|
1579
1579
|
- <a id="analytics"></a>`--analytics`
|
|
1580
1580
|
|
|
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`. 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`.
|
|
1582
1582
|
|
|
1583
1583
|
- <a id="version"></a>`-v, --version`
|
|
1584
1584
|
|
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/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";
|
|
@@ -208,6 +208,15 @@ 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);
|
|
@@ -216,7 +225,7 @@ async function handleError(msg, err, canRetry) {
|
|
|
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
230
|
output: typeof args.output === "string" ? args.output : "table"
|
|
222
231
|
};
|
package/dist/skills/freshness.js
CHANGED
|
@@ -24,9 +24,6 @@ const isRecord = (value) => typeof value === "object" && value !== null && !Arra
|
|
|
24
24
|
const SKILL_NAME = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
25
25
|
const skillFolder = (skillPath) => skillPath.replace(/\\/g, "/").replace(/\/?SKILL\.md$/i, "");
|
|
26
26
|
/**
|
|
27
|
-
* Entries pinned to a `ref` are left out: `neon skills` never pins one, and a pinned skill
|
|
28
|
-
* tracks that ref rather than the repository's default branch.
|
|
29
|
-
*
|
|
30
27
|
* The global lock's `skillFolderHash` is usually a tree SHA, but the skills CLI stores a
|
|
31
28
|
* content SHA-256 there when it installed from a Git clone; it tells them apart by length.
|
|
32
29
|
*/
|
|
@@ -39,7 +36,7 @@ const parseSkillsLock = (raw, scope) => {
|
|
|
39
36
|
if (!isRecord(entry)) continue;
|
|
40
37
|
const { source, skillPath, ref } = entry;
|
|
41
38
|
const hash = entry[field];
|
|
42
|
-
if (!SKILL_NAME.test(skill) || typeof source !== "string" || !NEON_SOURCES.has(source) ||
|
|
39
|
+
if (!SKILL_NAME.test(skill) || typeof source !== "string" || !NEON_SOURCES.has(source) || typeof skillPath !== "string" || typeof hash !== "string" || hash === "") continue;
|
|
43
40
|
const folder = skillFolder(skillPath);
|
|
44
41
|
if (folder === "") continue;
|
|
45
42
|
const hashKind = scope === "global" && /^[0-9a-f]{40}$/i.test(hash) ? "tree" : "content";
|
|
@@ -49,12 +46,13 @@ const parseSkillsLock = (raw, scope) => {
|
|
|
49
46
|
source,
|
|
50
47
|
folder,
|
|
51
48
|
hash,
|
|
52
|
-
hashKind
|
|
49
|
+
hashKind,
|
|
50
|
+
pinned: ref !== void 0
|
|
53
51
|
});
|
|
54
52
|
}
|
|
55
53
|
return skills;
|
|
56
54
|
};
|
|
57
|
-
const
|
|
55
|
+
const readAllLockedSkills = (path, scope) => {
|
|
58
56
|
if (!existsSync(path)) return [];
|
|
59
57
|
const raw = readLockFile(path);
|
|
60
58
|
try {
|
|
@@ -63,6 +61,8 @@ const readLockedSkills = (path, scope) => {
|
|
|
63
61
|
throw new Error(`${path} is not valid JSON.`);
|
|
64
62
|
}
|
|
65
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
66
|
const MAX_LOCK_BYTES = 1048576;
|
|
67
67
|
/**
|
|
68
68
|
* A project lock is repository content: a FIFO would block the open and `/dev/zero` the read,
|
|
@@ -303,26 +303,74 @@ const spawnSkillsCheckWorker = (cachePath, cwd) => {
|
|
|
303
303
|
log.debug("Could not start the skills check: %s", error instanceof Error ? error.message : String(error));
|
|
304
304
|
}
|
|
305
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
|
+
};
|
|
306
354
|
/**
|
|
307
355
|
* Warns at most once a day when a Neon skill in the user-level lock or this directory's
|
|
308
356
|
* `skills-lock.json` is behind its source, from a cache a background worker refreshes daily.
|
|
309
357
|
*/
|
|
310
358
|
const notifyIfSkillsOutdated = ({ configDir, cwd, now = Date.now() }) => {
|
|
311
|
-
|
|
312
|
-
let skills;
|
|
359
|
+
let locks;
|
|
313
360
|
try {
|
|
314
|
-
|
|
361
|
+
locks = readLocks(cwd);
|
|
315
362
|
} catch (error) {
|
|
316
363
|
log.debug("Skipping the skills check: %s", error instanceof Error ? error.message : String(error));
|
|
317
364
|
return;
|
|
318
365
|
}
|
|
366
|
+
const skills = [...locks.global.skills, ...locks.project.skills].filter((skill) => !skill.pinned);
|
|
319
367
|
if (skills.length === 0) return;
|
|
320
368
|
const cachePath = skillsCheckCachePath(configDir);
|
|
321
369
|
const cache = readSkillsCheckCache(cachePath);
|
|
322
370
|
const plan = planSkillsCheck({
|
|
323
371
|
skills,
|
|
324
372
|
cache,
|
|
325
|
-
lockModifiedAt: Math.max(
|
|
373
|
+
lockModifiedAt: Math.max(locks.global.modifiedAt, locks.project.modifiedAt),
|
|
326
374
|
now
|
|
327
375
|
});
|
|
328
376
|
const notice = formatSkillsNotice(plan.outdated);
|
|
@@ -346,4 +394,4 @@ const notifyIfSkillsOutdated = ({ configDir, cwd, now = Date.now() }) => {
|
|
|
346
394
|
}, "skills check cache")) spawnSkillsCheckWorker(cachePath, cwd);
|
|
347
395
|
};
|
|
348
396
|
//#endregion
|
|
349
|
-
export { formatSkillsNotice, globalLockPath, lockPath, neonSkillsUpdateCommand, notifyIfSkillsOutdated, parseSkillsCheckCache, parseSkillsLock, planSkillsCheck, projectLockPath, readLockedSkills, refreshSkillsCheckCache, scopeLabel, skillContentHash, skillStatus, skillsCheckCachePath, skillsStatusRows };
|
|
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
|
@@ -63,8 +63,8 @@ const quoteNpxArg = (part) => /[\s"'\\*]/.test(part) ? `'${part.replace(/'/g, `'
|
|
|
63
63
|
const npxCommand = (args) => `npx ${args.map(quoteNpxArg).join(" ")}`;
|
|
64
64
|
const ANSI = /\u001b\[[0-9;]*[A-Za-z]/g;
|
|
65
65
|
const UPDATE_BANNER = /^Checking for skill updates/i;
|
|
66
|
-
const UPDATE_NOTHING = /No (?:project|global) skills (?:to update|to check|tracked in lock file|can be updated in place)|All global skills are up to date/i;
|
|
67
|
-
const UPDATE_RESULT = /No (?:project|global) skills (?:to update|to check|tracked in lock file|can be updated in place)\.?|All global skills are up to date|(?:✓\s*)?Updated \d+ skills?(?:\(s\))?|(?:✗\s*)?Failed to update \d+ skills?(?:\(s\))?/i;
|
|
66
|
+
const UPDATE_NOTHING = /No (?:project|global) skills (?:to update|to check|tracked in lock file|can be updated in place)|All (?:project|global) skills are up to date/i;
|
|
67
|
+
const UPDATE_RESULT = /No (?:project|global) skills (?:to update|to check|tracked in lock file|can be updated in place)\.?|All (?:project|global) skills are up to date|(?:✓\s*)?Updated \d+ skills?(?:\(s\))?|(?:✗\s*)?Failed to update \d+ skills?(?:\(s\))?/i;
|
|
68
68
|
const stripAnsi = (text) => text.replace(ANSI, "").replace(/\r/g, "");
|
|
69
69
|
const skillsUpdateHadNothing = (output) => UPDATE_NOTHING.test(stripAnsi(output));
|
|
70
70
|
const skillsUpdateDetail = (output) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.2",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -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/e2e-harness": "0.0.0",
|
|
116
116
|
"@neon/env": "1.6.5",
|
|
117
|
-
"@neon
|
|
118
|
-
"@neon/
|
|
117
|
+
"@neon/functions": "0.11.0",
|
|
118
|
+
"@neon/e2e-harness": "0.0.0"
|
|
119
119
|
},
|
|
120
120
|
"publishConfig": {
|
|
121
121
|
"access": "public",
|