@moda-ai/cli 1.2.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
@@ -5,13 +5,16 @@ import { callDataAPI, ApiError, getApiRequestCount, resetApiRequestCount, } from
5
5
  import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, WorkflowsSchema, RunsSchema, RunDetailSchema, LoopsSchema, LoopDetailSchema, } from './schemas.js';
6
6
  import { withFrustrationAnchors, withToolFailureAnchors, embeddedWindowSize, asRecord, asString, asNumber } from './transforms.js';
7
7
  import { runPromptsCommand } from './prompts.js';
8
- import { runHooksInstall, runSparkCommand, runWorkspaceCommand } from './workspace.js';
8
+ // Coding-agent capture (shadow workspace / Shadow Spark / trace hooks) is temporarily
9
+ // disabled while the feature is finished — see the commented-out command registrations below.
10
+ // import { runHooksInstall, runSparkCommand, runWorkspaceCommand } from './workspace.js';
9
11
  import { runSkillsCommand } from './skills.js';
10
12
  import { runHarnessCommand } from './harness.js';
11
13
  import { runSyncCommand } from './sync.js';
12
14
  import { runDoctorCommand, runStatusCommand } from './doctor.js';
13
15
  import { CliInputError } from './errors.js';
14
16
  import { isTelemetryDisabled, sendCliUsageTelemetry, stripTelemetryFlag, } from './telemetry.js';
17
+ import { isUpdateCheckDisabled, printUpdateNotice, startUpdateCheck, stripUpdateCheckFlag, } from './update-check.js';
15
18
  import { createCommandContext } from './command-context.js';
16
19
  import { createCommandRegistry } from './command-registry.js';
17
20
  import { createAgentErrorEnvelope, errorToAgentError, outputModeFromFlags, } from './output.js';
@@ -154,25 +157,6 @@ Commands:
154
157
  prompts sync --watch Upload again when prompt files change
155
158
  prompts promote <key> Move a dev/staging/prod label
156
159
 
157
- Shadow workspace:
158
- spark status Show Shadow Spark workspace state
159
- spark explain Explain what Shadow Spark captures and posts
160
- spark snapshot Manual / hook-invoked Shadow Spark snapshot
161
- spark snapshot --dry-run Show file inventory without copy/commit/post
162
- spark log Show Shadow Spark history
163
- spark bundle Bundle a shadow commit range for replay
164
- spark remove --yes Delete this repo's local shadow workspace
165
- workspace init Create shadow workspace for current repo
166
- workspace snapshot Manual / hook-invoked sync + shadow commit
167
- workspace status Show shadow workspace state
168
- workspace explain Explain shadow workspace behavior
169
- workspace log Show Moda shadow history (not user git log)
170
- workspace bundle Bundle a shadow commit range for replay
171
- hooks install Install Claude Code shadow-workspace hooks
172
- hooks install --agent=codex Install Codex hooks
173
- hooks install --agent=cursor Install Cursor hooks
174
- hooks install --agent=all Install Claude Code, Codex, and Cursor hooks
175
-
176
160
  Skills:
177
161
  skills gen Kick off tenant-wide skill generation
178
162
  skills pull Pull tenant-generated skills into .claude/skills (and .cursor/rules)
@@ -195,6 +179,7 @@ Common flags:
195
179
  --yes Accept explicit automation defaults
196
180
  --tui Force the human analyst dashboard when supported
197
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)
198
183
 
199
184
  Runs flags:
200
185
  --outcome=OUTCOME Filter by outcome (running|succeeded|partial|failed|loop|abandoned|unknown)
@@ -207,8 +192,6 @@ Init flags:
207
192
  Choose harness analyst adapter
208
193
  --tui Show full-screen analyst dashboard in human TTYs
209
194
  --no-tui Show raw external analyst output
210
- --coding-agent-traces Install Claude/Codex/Cursor trace hooks
211
- --no-coding-agent-traces Skip Claude/Codex/Cursor trace hook installation
212
195
  --sdk-guide Write .moda/SDK_INTEGRATION.md
213
196
  --no-sdk-guide Skip SDK integration guide creation
214
197
  --ci-cd Create GitHub Actions Moda validation workflow
@@ -241,8 +224,6 @@ Examples:
241
224
  moda sync harness --profile=prod
242
225
  moda harness scan --json
243
226
  moda harness explain --agent=agent_support
244
- moda init --coding-agent-traces
245
- moda init --no-coding-agent-traces
246
227
  moda init --ci-cd
247
228
  moda overview --days-back=30
248
229
  moda clusters --time-range=7d
@@ -258,11 +239,6 @@ Examples:
258
239
  moda prompts status
259
240
  moda prompts sync
260
241
  moda prompts promote support.triage --label=prod --version=pver_abc123
261
- moda spark explain
262
- moda spark snapshot --dry-run --json
263
- moda workspace snapshot --json
264
- moda hooks install
265
- moda hooks install --agent=all
266
242
  moda skills pull`);
267
243
  }
268
244
  // --- Command Handlers ---
@@ -870,57 +846,60 @@ export const commandRegistry = createCommandRegistry([
870
846
  resetApiRequestCountBeforeRun: true,
871
847
  telemetry: 'result-and-error',
872
848
  }),
873
- {
874
- name: 'workspace',
875
- description: 'Manage Moda shadow workspace snapshots',
876
- examples: ['moda workspace init', 'moda workspace snapshot --json', 'moda workspace explain'],
877
- auth: 'none',
878
- outputModes: ['human', 'json', 'agent'],
879
- mutability: 'write',
880
- validateConfigBeforeRun: false,
881
- resetApiRequestCountBeforeRun: false,
882
- telemetry: 'none',
883
- handler: async (context) => ({
884
- exitCode: await runWorkspaceCommand({
885
- command: context.command,
886
- positional: context.positional,
887
- positionals: context.positionals,
888
- flags: context.flags,
889
- }),
890
- }),
891
- },
892
- {
893
- name: 'spark',
894
- aliases: ['shadow'],
895
- description: 'Manage Shadow Spark workspace snapshots',
896
- examples: ['moda spark status', 'moda spark explain', 'moda spark snapshot --dry-run --json'],
897
- auth: 'none',
898
- outputModes: ['human', 'json', 'agent'],
899
- mutability: 'write',
900
- validateConfigBeforeRun: false,
901
- resetApiRequestCountBeforeRun: false,
902
- telemetry: 'none',
903
- handler: runSparkCommand,
904
- },
905
- {
906
- name: 'hooks',
907
- description: 'Install coding-agent trace hooks',
908
- examples: ['moda hooks install', 'moda hooks install --agent=all'],
909
- auth: 'none',
910
- outputModes: ['human', 'json', 'agent'],
911
- mutability: 'write',
912
- validateConfigBeforeRun: false,
913
- resetApiRequestCountBeforeRun: false,
914
- telemetry: 'none',
915
- handler: async (context) => ({
916
- exitCode: await runHooksInstall({
917
- command: context.command,
918
- positional: context.positional,
919
- positionals: context.positionals,
920
- flags: context.flags,
921
- }),
922
- }),
923
- },
849
+ // Coding-agent capture commands disabled until the feature is production-ready.
850
+ // Re-enable by restoring these registrations, the workspace.js import above, the
851
+ // "Shadow workspace" help section, the init hooks phase, and the doctor spark/hook checks.
852
+ // {
853
+ // name: 'workspace',
854
+ // description: 'Manage Moda shadow workspace snapshots',
855
+ // examples: ['moda workspace init', 'moda workspace snapshot --json', 'moda workspace explain'],
856
+ // auth: 'none',
857
+ // outputModes: ['human', 'json', 'agent'],
858
+ // mutability: 'write',
859
+ // validateConfigBeforeRun: false,
860
+ // resetApiRequestCountBeforeRun: false,
861
+ // telemetry: 'none',
862
+ // handler: async (context) => ({
863
+ // exitCode: await runWorkspaceCommand({
864
+ // command: context.command,
865
+ // positional: context.positional,
866
+ // positionals: context.positionals,
867
+ // flags: context.flags,
868
+ // }),
869
+ // }),
870
+ // },
871
+ // {
872
+ // name: 'spark',
873
+ // aliases: ['shadow'],
874
+ // description: 'Manage Shadow Spark workspace snapshots',
875
+ // examples: ['moda spark status', 'moda spark explain', 'moda spark snapshot --dry-run --json'],
876
+ // auth: 'none',
877
+ // outputModes: ['human', 'json', 'agent'],
878
+ // mutability: 'write',
879
+ // validateConfigBeforeRun: false,
880
+ // resetApiRequestCountBeforeRun: false,
881
+ // telemetry: 'none',
882
+ // handler: runSparkCommand,
883
+ // },
884
+ // {
885
+ // name: 'hooks',
886
+ // description: 'Install coding-agent trace hooks',
887
+ // examples: ['moda hooks install', 'moda hooks install --agent=all'],
888
+ // auth: 'none',
889
+ // outputModes: ['human', 'json', 'agent'],
890
+ // mutability: 'write',
891
+ // validateConfigBeforeRun: false,
892
+ // resetApiRequestCountBeforeRun: false,
893
+ // telemetry: 'none',
894
+ // handler: async (context) => ({
895
+ // exitCode: await runHooksInstall({
896
+ // command: context.command,
897
+ // positional: context.positional,
898
+ // positionals: context.positionals,
899
+ // flags: context.flags,
900
+ // }),
901
+ // }),
902
+ // },
924
903
  {
925
904
  name: 'skills',
926
905
  description: 'Generate and pull Moda skills',
@@ -984,8 +963,9 @@ function printAgentCliError(command, error) {
984
963
  }
985
964
  export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
986
965
  const { command, flags } = parsed;
987
- const commandFlags = stripTelemetryFlag(flags);
966
+ const commandFlags = stripUpdateCheckFlag(stripTelemetryFlag(flags));
988
967
  const telemetryDisabled = isTelemetryDisabled(flags);
968
+ const updateCheckDisabled = isUpdateCheckDisabled(flags);
989
969
  if (command === '--version' || command === '-V' || flags.version === 'true') {
990
970
  console.log(`moda-cli ${VERSION}`);
991
971
  return 0;
@@ -1012,6 +992,11 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
1012
992
  if (definition.name === 'agent' && commandFlags.json !== 'true') {
1013
993
  commandFlags.agent = 'true';
1014
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
+ }
1015
1000
  if (definition.validateConfigBeforeRun) {
1016
1001
  validateConfig();
1017
1002
  }
@@ -1037,6 +1022,9 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
1037
1022
  apiRequestCount: getApiRequestCount(),
1038
1023
  });
1039
1024
  }
1025
+ if (!updateCheckDisabled) {
1026
+ printUpdateNotice();
1027
+ }
1040
1028
  return result.exitCode;
1041
1029
  }
1042
1030
  catch (error) {
@@ -1058,6 +1046,9 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
1058
1046
  else {
1059
1047
  printCliError(error);
1060
1048
  }
1049
+ if (!updateCheckDisabled) {
1050
+ printUpdateNotice();
1051
+ }
1061
1052
  return 1;
1062
1053
  }
1063
1054
  }
package/dist/doctor.js CHANGED
@@ -162,9 +162,12 @@ export async function buildDoctorReport(context, options) {
162
162
  '__tests__',
163
163
  ]));
164
164
  checks.push(sdkInitializationCheck(context.cwd));
165
- checks.push(shadowSparkWorkspaceCheck(context.cwd, context.env));
166
- checks.push(hookCheck(context.env));
167
- checks.push(...hookTelemetryChecks(context.env, ingestUrl));
165
+ // Coding-agent capture (Shadow Spark + trace hooks) is temporarily disabled, so doctor
166
+ // must not recommend its commands. `shadowSpark` in the status payload falls back to
167
+ // 'skipped' via checkStatus.
168
+ // checks.push(shadowSparkWorkspaceCheck(context.cwd, context.env));
169
+ // checks.push(hookCheck(context.env));
170
+ // checks.push(...hookTelemetryChecks(context.env, ingestUrl));
168
171
  checks.push(validationEventPathCheck(ingestUrl));
169
172
  if (options.offline || !apiKey) {
170
173
  checks.push(...offlineCloudChecks(options.offline));
@@ -519,6 +522,8 @@ function sdkInitializationCheck(cwd) {
519
522
  nextCommand: 'moda init',
520
523
  };
521
524
  }
525
+ // Unused while coding-agent capture is disabled; kept for re-enablement.
526
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
522
527
  function shadowSparkWorkspaceCheck(cwd, env) {
523
528
  const repo = detectGitRepo(cwd);
524
529
  if (!repo) {
@@ -552,6 +557,8 @@ function shadowSparkWorkspaceCheck(cwd, env) {
552
557
  nextCommand: healthy ? undefined : 'moda spark snapshot --json',
553
558
  };
554
559
  }
560
+ // Unused while coding-agent capture is disabled; kept for re-enablement.
561
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
555
562
  function hookCheck(env) {
556
563
  const paths = getModaPaths({ env });
557
564
  const hookCandidates = [
@@ -571,6 +578,8 @@ function hookCheck(env) {
571
578
  nextCommand: found.length > 0 ? undefined : 'moda hooks install --agent=all',
572
579
  };
573
580
  }
581
+ // Unused while coding-agent capture is disabled; kept for re-enablement.
582
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
574
583
  function hookTelemetryChecks(env, ingestUrl) {
575
584
  const configs = agentTelemetryConfigs(env);
576
585
  return [
@@ -8,7 +8,8 @@ import { detectAgents } from './mcp.js';
8
8
  import { installRules } from './rules.js';
9
9
  import { initPrompts } from '../prompts.js';
10
10
  import { pullSkills } from '../skills.js';
11
- import { runHooksInstall } from '../workspace.js';
11
+ // Coding-agent capture is temporarily disabled — see the hooks phase below.
12
+ // import { runHooksInstall } from '../workspace.js';
12
13
  import { SDK_GUIDE_PATH, writeSdkIntegrationGuide } from './sdk-guide.js';
13
14
  import { createCommandContext } from '../command-context.js';
14
15
  import { CliInputError } from '../errors.js';
@@ -29,7 +30,9 @@ export async function runInit(flags, options = {}) {
29
30
  if (human)
30
31
  console.log(message);
31
32
  };
32
- const warn = (message) => {
33
+ // `warn` is only used by the temporarily-disabled hooks phase below; keep the
34
+ // underscore-prefixed binding so re-enabling it is a rename away.
35
+ const _warn = (message) => {
33
36
  if (human)
34
37
  console.error(message);
35
38
  };
@@ -103,7 +106,9 @@ export async function runInit(flags, options = {}) {
103
106
  // Step 5: Setup plan
104
107
  log(renderInitStep(5, 7, 'Setup Plan', 'Every write/sync action is explicit before it is applied.'));
105
108
  const setupPlan = buildSetupPlan({
106
- installHooks: await shouldIngestCodingAgentTraces(flags, yes),
109
+ // Coding-agent capture is temporarily disabled; never prompt for or install trace hooks.
110
+ // installHooks: await shouldIngestCodingAgentTraces(flags, yes),
111
+ installHooks: false,
107
112
  installSdkGuide: await shouldCreateSdkGuide(flags, yes),
108
113
  configureCiCd: await shouldConfigureCiCd(flags, yes),
109
114
  });
@@ -113,25 +118,25 @@ export async function runInit(flags, options = {}) {
113
118
  // Step 6: Apply approved setup changes.
114
119
  log(renderInitStep(6, 7, 'Apply Approved Changes'));
115
120
  emit('apply', 'Applying approved setup changes.');
116
- const ingestCodingAgentTraces = setupPlan.find((action) => action.category === 'hooks')?.approved === true;
117
- if (ingestCodingAgentTraces) {
118
- try {
119
- await runHooksInstall({
120
- command: 'hooks',
121
- positional: 'install',
122
- positionals: ['install'],
123
- flags: { agent: 'all', ...(human ? {} : { json: 'true' }) },
124
- });
125
- }
126
- catch (err) {
127
- const message = err instanceof Error ? err.message : String(err);
128
- warn(renderInitNote(`hook/telemetry install failed: ${message}`, 'warning'));
129
- warn(' Run `moda hooks install --agent=all` manually to enable traces.');
130
- }
131
- }
132
- else {
133
- log(renderInitNote('Skipped coding-agent trace ingestion', 'skipped'));
134
- }
121
+ // Coding-agent capture is temporarily disabled; the hooks phase never runs.
122
+ // const ingestCodingAgentTraces = setupPlan.find((action) => action.category === 'hooks')?.approved === true;
123
+ const ingestCodingAgentTraces = false;
124
+ // if (ingestCodingAgentTraces) {
125
+ // try {
126
+ // await runHooksInstall({
127
+ // command: 'hooks',
128
+ // positional: 'install',
129
+ // positionals: ['install'],
130
+ // flags: { agent: 'all', ...(human ? {} : { json: 'true' }) },
131
+ // });
132
+ // } catch (err) {
133
+ // const message = err instanceof Error ? err.message : String(err);
134
+ // warn(renderInitNote(`hook/telemetry install failed: ${message}`, 'warning'));
135
+ // warn(' Run `moda hooks install --agent=all` manually to enable traces.');
136
+ // }
137
+ // } else {
138
+ // log(renderInitNote('Skipped coding-agent trace ingestion', 'skipped'));
139
+ // }
135
140
  const agents = detectAgents();
136
141
  if (agents.length > 0) {
137
142
  log(renderInitNote(`Detected coding agents: ${agents.join(', ')}`));
@@ -172,7 +177,7 @@ export async function runInit(flags, options = {}) {
172
177
  }
173
178
  log(renderInitNote('Reusing approved Step 4 harness report; no second analyst run needed'));
174
179
  markApplied(setupPlan, 'harness');
175
- markApplied(setupPlan, 'hooks', ingestCodingAgentTraces ? undefined : 'User skipped coding-agent trace hooks.');
180
+ markApplied(setupPlan, 'hooks', ingestCodingAgentTraces ? undefined : 'Coding-agent trace capture is temporarily unavailable.');
176
181
  markApplied(setupPlan, 'prompts');
177
182
  markApplied(setupPlan, 'skills');
178
183
  emit('apply', 'Approved changes applied.', 'completed');
@@ -226,7 +231,7 @@ function buildSetupPlan(options) {
226
231
  approved: options.installHooks,
227
232
  files: ['~/.claude/settings.json', '~/.codex/hooks.json', '~/.cursor/hooks.json'],
228
233
  command: 'moda hooks install --agent=all',
229
- reason: options.installHooks ? undefined : 'Not approved during init.',
234
+ reason: options.installHooks ? undefined : 'Coding-agent trace capture is temporarily unavailable.',
230
235
  },
231
236
  {
232
237
  category: 'prompts',
@@ -351,19 +356,20 @@ function commandFlags(flags) {
351
356
  function stringFlag(value) {
352
357
  return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
353
358
  }
354
- async function shouldIngestCodingAgentTraces(flags, yes) {
355
- const explicit = parseOptionalBoolean(flagValue(flags, 'coding-agent-traces', 'coding_agent_traces'));
356
- if (explicit !== undefined)
357
- return explicit;
358
- if (isTruthyFlag(flagValue(flags, 'no-coding-agent-traces', 'no_coding_agent_traces')))
359
- return false;
360
- if (yes)
361
- return true;
362
- return confirm({
363
- message: 'Ingest coding-agent traces for Claude Code, Codex, and Cursor?',
364
- default: true,
365
- });
366
- }
359
+ // Coding-agent capture is temporarily disabled; restore this prompt with the hooks phase.
360
+ // async function shouldIngestCodingAgentTraces(
361
+ // flags: Record<string, unknown>,
362
+ // yes: boolean,
363
+ // ): Promise<boolean> {
364
+ // const explicit = parseOptionalBoolean(flagValue(flags, 'coding-agent-traces', 'coding_agent_traces'));
365
+ // if (explicit !== undefined) return explicit;
366
+ // if (isTruthyFlag(flagValue(flags, 'no-coding-agent-traces', 'no_coding_agent_traces'))) return false;
367
+ // if (yes) return true;
368
+ // return confirm({
369
+ // message: 'Ingest coding-agent traces for Claude Code, Codex, and Cursor?',
370
+ // default: true,
371
+ // });
372
+ // }
367
373
  async function shouldConfigureCiCd(flags, yes) {
368
374
  const explicit = parseOptionalBoolean(flagValue(flags, 'ci-cd', 'ci_cd', 'ci'));
369
375
  if (explicit !== undefined)
@@ -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.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {