@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 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {