@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 +72 -81
- package/dist/doctor.js +12 -3
- package/dist/init/index.js +43 -37
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 [
|
package/dist/init/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
|
|
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 : '
|
|
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 : '
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|