@moda-ai/cli 1.3.0 → 1.4.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/dist/cli.js +15 -1
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +141 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { runSyncCommand } from './sync.js';
|
|
|
14
14
|
import { runDoctorCommand, runStatusCommand } from './doctor.js';
|
|
15
15
|
import { CliInputError } from './errors.js';
|
|
16
16
|
import { isTelemetryDisabled, sendCliUsageTelemetry, stripTelemetryFlag, } from './telemetry.js';
|
|
17
|
+
import { isUpdateCheckDisabled, printUpdateNotice, startUpdateCheck, stripUpdateCheckFlag, } from './update-check.js';
|
|
17
18
|
import { createCommandContext } from './command-context.js';
|
|
18
19
|
import { createCommandRegistry } from './command-registry.js';
|
|
19
20
|
import { createAgentErrorEnvelope, errorToAgentError, outputModeFromFlags, } from './output.js';
|
|
@@ -178,6 +179,7 @@ Common flags:
|
|
|
178
179
|
--yes Accept explicit automation defaults
|
|
179
180
|
--tui Force the human analyst dashboard when supported
|
|
180
181
|
--no-tui Show raw analyst stream instead of dashboard
|
|
182
|
+
--no-update-check Skip the daily new-version check (or MODA_CLI_UPDATE_CHECK=0)
|
|
181
183
|
|
|
182
184
|
Runs flags:
|
|
183
185
|
--outcome=OUTCOME Filter by outcome (running|succeeded|partial|failed|loop|abandoned|unknown)
|
|
@@ -961,8 +963,9 @@ function printAgentCliError(command, error) {
|
|
|
961
963
|
}
|
|
962
964
|
export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
963
965
|
const { command, flags } = parsed;
|
|
964
|
-
const commandFlags = stripTelemetryFlag(flags);
|
|
966
|
+
const commandFlags = stripUpdateCheckFlag(stripTelemetryFlag(flags));
|
|
965
967
|
const telemetryDisabled = isTelemetryDisabled(flags);
|
|
968
|
+
const updateCheckDisabled = isUpdateCheckDisabled(flags);
|
|
966
969
|
if (command === '--version' || command === '-V' || flags.version === 'true') {
|
|
967
970
|
console.log(`moda-cli ${VERSION}`);
|
|
968
971
|
return 0;
|
|
@@ -989,6 +992,11 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
989
992
|
if (definition.name === 'agent' && commandFlags.json !== 'true') {
|
|
990
993
|
commandFlags.agent = 'true';
|
|
991
994
|
}
|
|
995
|
+
// Kick off the registry check before the handler so the (at most daily)
|
|
996
|
+
// request overlaps with command work instead of delaying exit.
|
|
997
|
+
if (!updateCheckDisabled) {
|
|
998
|
+
void startUpdateCheck();
|
|
999
|
+
}
|
|
992
1000
|
if (definition.validateConfigBeforeRun) {
|
|
993
1001
|
validateConfig();
|
|
994
1002
|
}
|
|
@@ -1014,6 +1022,9 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1014
1022
|
apiRequestCount: getApiRequestCount(),
|
|
1015
1023
|
});
|
|
1016
1024
|
}
|
|
1025
|
+
if (!updateCheckDisabled) {
|
|
1026
|
+
printUpdateNotice();
|
|
1027
|
+
}
|
|
1017
1028
|
return result.exitCode;
|
|
1018
1029
|
}
|
|
1019
1030
|
catch (error) {
|
|
@@ -1035,6 +1046,9 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1035
1046
|
else {
|
|
1036
1047
|
printCliError(error);
|
|
1037
1048
|
}
|
|
1049
|
+
if (!updateCheckDisabled) {
|
|
1050
|
+
printUpdateNotice();
|
|
1051
|
+
}
|
|
1038
1052
|
return 1;
|
|
1039
1053
|
}
|
|
1040
1054
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ProfileIOOptions } from './profiles.js';
|
|
2
|
+
export declare function stripUpdateCheckFlag(flags: Record<string, string>): Record<string, string>;
|
|
3
|
+
export declare function isUpdateCheckDisabled(flags: Record<string, string>, env?: NodeJS.ProcessEnv): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Numeric semver comparison ignoring prerelease/build suffixes; npm's
|
|
6
|
+
* `latest` dist-tag only carries stable releases, so this is sufficient
|
|
7
|
+
* without pulling in a semver dependency.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isNewerVersion(candidate: string, current: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Refresh the cached latest version from the npm registry at most once per
|
|
12
|
+
* 24 hours. Fire-and-forget from the CLI dispatcher. The abort timer is
|
|
13
|
+
* unref'd so it can't keep the process alive after the socket closes (it does
|
|
14
|
+
* not unref the in-flight fetch itself — a fast command may still wait up to
|
|
15
|
+
* REGISTRY_TIMEOUT_MS for the request to resolve, matching the telemetry
|
|
16
|
+
* module's trade-off). Every failure path is swallowed — an update check must
|
|
17
|
+
* never break a command.
|
|
18
|
+
*/
|
|
19
|
+
export declare function startUpdateCheck(options?: ProfileIOOptions, now?: number): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Returns the upgrade notice when the cached registry version is newer than
|
|
22
|
+
* the running build, or undefined. Reads only the local cache — never the
|
|
23
|
+
* network — so it is safe to call synchronously at the end of every command.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getUpdateNotice(options?: ProfileIOOptions): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Print the upgrade notice to stderr in interactive terminals only: stdout
|
|
28
|
+
* stays machine-readable, and piped/CI invocations stay noise-free.
|
|
29
|
+
*/
|
|
30
|
+
export declare function printUpdateNotice(options?: ProfileIOOptions): void;
|
|
31
|
+
//# sourceMappingURL=update-check.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/update-check.ts — Daily npm-registry version check with an upgrade
|
|
2
|
+
// notice. Mirrors the telemetry contract: fire-and-forget, short abort
|
|
3
|
+
// timeout, never throws, and never touches stdout (notice goes to stderr so
|
|
4
|
+
// JSON/agent output stays machine-readable).
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { VERSION } from './config.js';
|
|
8
|
+
import { getModaDir } from './profiles.js';
|
|
9
|
+
const UPDATE_CHECK_FLAG = 'no-update-check';
|
|
10
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const REGISTRY_TIMEOUT_MS = 1_500;
|
|
12
|
+
const REGISTRY_LATEST_URL = 'https://registry.npmjs.org/@moda-ai/cli/latest';
|
|
13
|
+
// npm's `latest` dist-tag is always a stable semver. Anchored, no control
|
|
14
|
+
// chars: a registry response intercepted by a proxy can't smuggle ANSI escape
|
|
15
|
+
// sequences into the cache (and thus the terminal notice) this way.
|
|
16
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
17
|
+
export function stripUpdateCheckFlag(flags) {
|
|
18
|
+
const { [UPDATE_CHECK_FLAG]: _drop, ...rest } = flags;
|
|
19
|
+
return rest;
|
|
20
|
+
}
|
|
21
|
+
export function isUpdateCheckDisabled(flags, env = process.env) {
|
|
22
|
+
return (flags[UPDATE_CHECK_FLAG] === 'true' ||
|
|
23
|
+
env.MODA_CLI_UPDATE_CHECK === '0' ||
|
|
24
|
+
env.DO_NOT_TRACK === '1' ||
|
|
25
|
+
// Treat CI as set only when it carries a real truthy value: `CI=false`
|
|
26
|
+
// and `CI=0` are common ways to simulate a non-CI shell locally.
|
|
27
|
+
(env.CI !== undefined &&
|
|
28
|
+
env.CI !== '' &&
|
|
29
|
+
env.CI !== 'false' &&
|
|
30
|
+
env.CI !== '0'));
|
|
31
|
+
}
|
|
32
|
+
function cachePath(options = {}) {
|
|
33
|
+
return join(getModaDir(options), 'update-check.json');
|
|
34
|
+
}
|
|
35
|
+
function readCache(options = {}) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(readFileSync(cachePath(options), 'utf8'));
|
|
38
|
+
if (parsed &&
|
|
39
|
+
typeof parsed === 'object' &&
|
|
40
|
+
typeof parsed.checked_at === 'number' &&
|
|
41
|
+
typeof parsed.latest === 'string') {
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Missing or corrupt cache means "never checked".
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function writeCache(cache, options = {}) {
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(getModaDir(options), { recursive: true });
|
|
53
|
+
writeFileSync(cachePath(options), `${JSON.stringify(cache, null, 2)}\n`, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Best-effort: an unwritable cache only means we re-check next run.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Numeric semver comparison ignoring prerelease/build suffixes; npm's
|
|
61
|
+
* `latest` dist-tag only carries stable releases, so this is sufficient
|
|
62
|
+
* without pulling in a semver dependency.
|
|
63
|
+
*/
|
|
64
|
+
export function isNewerVersion(candidate, current) {
|
|
65
|
+
const parse = (v) => v.split(/[-+]/)[0].split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
66
|
+
const a = parse(candidate);
|
|
67
|
+
const b = parse(current);
|
|
68
|
+
for (let i = 0; i < 3; i++) {
|
|
69
|
+
if ((a[i] ?? 0) > (b[i] ?? 0))
|
|
70
|
+
return true;
|
|
71
|
+
if ((a[i] ?? 0) < (b[i] ?? 0))
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Refresh the cached latest version from the npm registry at most once per
|
|
78
|
+
* 24 hours. Fire-and-forget from the CLI dispatcher. The abort timer is
|
|
79
|
+
* unref'd so it can't keep the process alive after the socket closes (it does
|
|
80
|
+
* not unref the in-flight fetch itself — a fast command may still wait up to
|
|
81
|
+
* REGISTRY_TIMEOUT_MS for the request to resolve, matching the telemetry
|
|
82
|
+
* module's trade-off). Every failure path is swallowed — an update check must
|
|
83
|
+
* never break a command.
|
|
84
|
+
*/
|
|
85
|
+
export async function startUpdateCheck(options = {}, now = Date.now()) {
|
|
86
|
+
const cache = readCache(options);
|
|
87
|
+
if (cache && now - cache.checked_at < CHECK_INTERVAL_MS) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
const timeout = setTimeout(() => controller.abort(), REGISTRY_TIMEOUT_MS);
|
|
92
|
+
if (typeof timeout.unref === 'function') {
|
|
93
|
+
timeout.unref();
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(REGISTRY_LATEST_URL, {
|
|
97
|
+
headers: { Accept: 'application/json' },
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
});
|
|
100
|
+
if (!res.ok)
|
|
101
|
+
return;
|
|
102
|
+
const body = (await res.json());
|
|
103
|
+
if (typeof body.version !== 'string' || !SEMVER_RE.test(body.version))
|
|
104
|
+
return;
|
|
105
|
+
writeCache({ checked_at: now, latest: body.version }, options);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Network/timeout errors are expected offline; try again next run.
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns the upgrade notice when the cached registry version is newer than
|
|
116
|
+
* the running build, or undefined. Reads only the local cache — never the
|
|
117
|
+
* network — so it is safe to call synchronously at the end of every command.
|
|
118
|
+
*/
|
|
119
|
+
export function getUpdateNotice(options = {}) {
|
|
120
|
+
const cache = readCache(options);
|
|
121
|
+
if (!cache || !isNewerVersion(cache.latest, VERSION)) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return [
|
|
125
|
+
'',
|
|
126
|
+
`Update available: moda-cli ${VERSION} → ${cache.latest}`,
|
|
127
|
+
'Run `npm install -g @moda-ai/cli@latest` to update.',
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Print the upgrade notice to stderr in interactive terminals only: stdout
|
|
132
|
+
* stays machine-readable, and piped/CI invocations stay noise-free.
|
|
133
|
+
*/
|
|
134
|
+
export function printUpdateNotice(options = {}) {
|
|
135
|
+
if (!process.stderr.isTTY)
|
|
136
|
+
return;
|
|
137
|
+
const notice = getUpdateNotice(options);
|
|
138
|
+
if (notice)
|
|
139
|
+
console.error(notice);
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=update-check.js.map
|