@moda-ai/cli 1.3.0 → 1.6.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 +23 -6
- package/dist/harness.d.ts +31 -0
- package/dist/harness.js +53 -9
- package/dist/init/index.js +130 -59
- package/dist/init/sdk-guide.d.ts +18 -0
- package/dist/init/sdk-guide.js +2 -2
- package/dist/init/sdk-integration.d.ts +26 -0
- package/dist/init/sdk-integration.js +80 -0
- package/dist/prompts.d.ts +16 -0
- package/dist/prompts.js +12 -3
- package/dist/skills.d.ts +26 -0
- package/dist/skills.js +160 -2
- 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';
|
|
@@ -160,6 +161,8 @@ Commands:
|
|
|
160
161
|
skills gen Kick off tenant-wide skill generation
|
|
161
162
|
skills pull Pull tenant-generated skills into .claude/skills (and .cursor/rules)
|
|
162
163
|
skills pull --status=proposed Include not-yet-promoted skills (default: approved)
|
|
164
|
+
skills sync Push local .claude/skills/**/SKILL.md up to Moda
|
|
165
|
+
skills sync --dry-run Show what would be pushed without writing
|
|
163
166
|
|
|
164
167
|
Options:
|
|
165
168
|
--help Show this help message
|
|
@@ -178,6 +181,7 @@ Common flags:
|
|
|
178
181
|
--yes Accept explicit automation defaults
|
|
179
182
|
--tui Force the human analyst dashboard when supported
|
|
180
183
|
--no-tui Show raw analyst stream instead of dashboard
|
|
184
|
+
--no-update-check Skip the daily new-version check (or MODA_CLI_UPDATE_CHECK=0)
|
|
181
185
|
|
|
182
186
|
Runs flags:
|
|
183
187
|
--outcome=OUTCOME Filter by outcome (running|succeeded|partial|failed|loop|abandoned|unknown)
|
|
@@ -190,10 +194,11 @@ Init flags:
|
|
|
190
194
|
Choose harness analyst adapter
|
|
191
195
|
--tui Show full-screen analyst dashboard in human TTYs
|
|
192
196
|
--no-tui Show raw external analyst output
|
|
193
|
-
--sdk-
|
|
194
|
-
--no-sdk-
|
|
197
|
+
--sdk-integrate Run your coding agent to install the Moda SDK and instrument LLM calls
|
|
198
|
+
--no-sdk-integrate Skip SDK integration (alias: --no-sdk-guide)
|
|
195
199
|
--ci-cd Create GitHub Actions Moda validation workflow
|
|
196
200
|
--no-ci-cd Skip CI/CD workflow creation
|
|
201
|
+
--offline Run final validation offline (skip the online telemetry check)
|
|
197
202
|
|
|
198
203
|
Examples:
|
|
199
204
|
moda init
|
|
@@ -576,7 +581,7 @@ export const commandRegistry = createCommandRegistry([
|
|
|
576
581
|
{
|
|
577
582
|
name: 'init',
|
|
578
583
|
description: 'Set up Moda in your project',
|
|
579
|
-
examples: ['moda init', 'moda init --
|
|
584
|
+
examples: ['moda init', 'moda init --sdk-integrate', 'moda init --ci-cd'],
|
|
580
585
|
auth: 'none',
|
|
581
586
|
outputModes: ['human', 'json', 'agent'],
|
|
582
587
|
mutability: 'write',
|
|
@@ -900,8 +905,8 @@ export const commandRegistry = createCommandRegistry([
|
|
|
900
905
|
// },
|
|
901
906
|
{
|
|
902
907
|
name: 'skills',
|
|
903
|
-
description: 'Generate and
|
|
904
|
-
examples: ['moda skills gen', 'moda skills pull'],
|
|
908
|
+
description: 'Generate, pull, and sync Moda skills',
|
|
909
|
+
examples: ['moda skills gen', 'moda skills pull', 'moda skills sync'],
|
|
905
910
|
auth: 'none',
|
|
906
911
|
outputModes: ['human', 'json', 'agent'],
|
|
907
912
|
mutability: 'write',
|
|
@@ -961,8 +966,9 @@ function printAgentCliError(command, error) {
|
|
|
961
966
|
}
|
|
962
967
|
export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
963
968
|
const { command, flags } = parsed;
|
|
964
|
-
const commandFlags = stripTelemetryFlag(flags);
|
|
969
|
+
const commandFlags = stripUpdateCheckFlag(stripTelemetryFlag(flags));
|
|
965
970
|
const telemetryDisabled = isTelemetryDisabled(flags);
|
|
971
|
+
const updateCheckDisabled = isUpdateCheckDisabled(flags);
|
|
966
972
|
if (command === '--version' || command === '-V' || flags.version === 'true') {
|
|
967
973
|
console.log(`moda-cli ${VERSION}`);
|
|
968
974
|
return 0;
|
|
@@ -989,6 +995,11 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
989
995
|
if (definition.name === 'agent' && commandFlags.json !== 'true') {
|
|
990
996
|
commandFlags.agent = 'true';
|
|
991
997
|
}
|
|
998
|
+
// Kick off the registry check before the handler so the (at most daily)
|
|
999
|
+
// request overlaps with command work instead of delaying exit.
|
|
1000
|
+
if (!updateCheckDisabled) {
|
|
1001
|
+
void startUpdateCheck();
|
|
1002
|
+
}
|
|
992
1003
|
if (definition.validateConfigBeforeRun) {
|
|
993
1004
|
validateConfig();
|
|
994
1005
|
}
|
|
@@ -1014,6 +1025,9 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1014
1025
|
apiRequestCount: getApiRequestCount(),
|
|
1015
1026
|
});
|
|
1016
1027
|
}
|
|
1028
|
+
if (!updateCheckDisabled) {
|
|
1029
|
+
printUpdateNotice();
|
|
1030
|
+
}
|
|
1017
1031
|
return result.exitCode;
|
|
1018
1032
|
}
|
|
1019
1033
|
catch (error) {
|
|
@@ -1035,6 +1049,9 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1035
1049
|
else {
|
|
1036
1050
|
printCliError(error);
|
|
1037
1051
|
}
|
|
1052
|
+
if (!updateCheckDisabled) {
|
|
1053
|
+
printUpdateNotice();
|
|
1054
|
+
}
|
|
1038
1055
|
return 1;
|
|
1039
1056
|
}
|
|
1040
1057
|
}
|
package/dist/harness.d.ts
CHANGED
|
@@ -222,9 +222,40 @@ export interface HarnessSyncState {
|
|
|
222
222
|
privacy: string[];
|
|
223
223
|
written: string[];
|
|
224
224
|
}
|
|
225
|
+
type HarnessAnalystAdapter = 'claude' | 'codex' | 'cursor' | 'local-scan';
|
|
226
|
+
type ExternalHarnessAnalystAdapter = Exclude<HarnessAnalystAdapter, 'local-scan'>;
|
|
227
|
+
export type CodingAgentMode = 'read-only' | 'write';
|
|
225
228
|
export declare function runHarnessCommand(context: CommandContext): Promise<{
|
|
226
229
|
exitCode: number;
|
|
227
230
|
}>;
|
|
231
|
+
export type CodingAgentAdapter = ExternalHarnessAnalystAdapter;
|
|
232
|
+
export interface CodingAgentRun {
|
|
233
|
+
adapter: CodingAgentAdapter;
|
|
234
|
+
command: string;
|
|
235
|
+
status: number | null;
|
|
236
|
+
stdout: string;
|
|
237
|
+
stderr: string;
|
|
238
|
+
timedOut: boolean;
|
|
239
|
+
failedToStart: boolean;
|
|
240
|
+
}
|
|
241
|
+
/** Coding-agent CLIs found on PATH, ranked best-first (claude > codex > cursor). */
|
|
242
|
+
export declare function detectAvailableCodingAgents(): Array<{
|
|
243
|
+
adapter: CodingAgentAdapter;
|
|
244
|
+
command: string;
|
|
245
|
+
}>;
|
|
246
|
+
export declare function codingAgentDisplayName(adapter: CodingAgentAdapter): string;
|
|
247
|
+
/** Spawn an installed coding-agent CLI with a prompt; capture its output. */
|
|
248
|
+
export declare function runCodingAgent(options: {
|
|
249
|
+
adapter: CodingAgentAdapter;
|
|
250
|
+
prompt: string;
|
|
251
|
+
rootDir: string;
|
|
252
|
+
mode: CodingAgentMode;
|
|
253
|
+
env?: NodeJS.ProcessEnv;
|
|
254
|
+
stream?: boolean;
|
|
255
|
+
timeoutMs?: number;
|
|
256
|
+
progressToStderr?: boolean;
|
|
257
|
+
onProgress?: (message: string, phase?: string) => void;
|
|
258
|
+
}): Promise<CodingAgentRun>;
|
|
228
259
|
export declare function scanHarness(options?: ScanOptions): HarnessGraph;
|
|
229
260
|
export declare function summarizeHarness(graph: HarnessGraph): HarnessSummary;
|
|
230
261
|
export declare function readHarnessSyncState(rootDir: string): HarnessSyncState | undefined;
|
package/dist/harness.js
CHANGED
|
@@ -1215,6 +1215,7 @@ function normalizeAnalystAdapter(raw) {
|
|
|
1215
1215
|
throw new CliInputError(`Unknown harness analyst '${raw}'.`, 'Use `--analyst=claude|codex|cursor|local-scan`.');
|
|
1216
1216
|
}
|
|
1217
1217
|
function resolveExternalHarnessAnalystInvocation(options) {
|
|
1218
|
+
const mode = options.mode ?? 'read-only';
|
|
1218
1219
|
const override = options.flags['analyst-command'] || process.env.MODA_HARNESS_ANALYST_COMMAND;
|
|
1219
1220
|
if (override) {
|
|
1220
1221
|
const [command, ...args] = splitCommandLine(override);
|
|
@@ -1229,16 +1230,19 @@ function resolveExternalHarnessAnalystInvocation(options) {
|
|
|
1229
1230
|
}
|
|
1230
1231
|
if (options.adapter === 'claude') {
|
|
1231
1232
|
const extraArgs = externalAnalystExtraArgs(options.adapter);
|
|
1233
|
+
// Write mode auto-accepts edits and unblocks the tools an integration needs
|
|
1234
|
+
// (Edit/Write/Bash for package installs); read-only stays in plan mode with a
|
|
1235
|
+
// safe-Bash allowlist so the harness analyst can never mutate the tree.
|
|
1236
|
+
const permission = mode === 'write'
|
|
1237
|
+
? ['--permission-mode', 'acceptEdits', '--allowedTools', 'Read,Grep,Glob,Edit,Write,Bash']
|
|
1238
|
+
: ['--permission-mode', 'plan', '--allowedTools', 'Read,Grep,Glob,Bash(pwd),Bash(ls *),Bash(find *),Bash(rg *),Bash(grep *),Bash(cat *),Bash(git status *),Bash(git ls-files *),Bash(git grep *)'];
|
|
1232
1239
|
return {
|
|
1233
1240
|
adapter: options.adapter,
|
|
1234
1241
|
command,
|
|
1235
1242
|
args: [
|
|
1236
1243
|
'--print',
|
|
1237
1244
|
...extraArgs,
|
|
1238
|
-
|
|
1239
|
-
'plan',
|
|
1240
|
-
'--allowedTools',
|
|
1241
|
-
'Read,Grep,Glob,Bash(pwd),Bash(ls *),Bash(find *),Bash(rg *),Bash(grep *),Bash(cat *),Bash(git status *),Bash(git ls-files *),Bash(git grep *)',
|
|
1245
|
+
...permission,
|
|
1242
1246
|
...(options.stream ? ['--verbose'] : []),
|
|
1243
1247
|
'--output-format',
|
|
1244
1248
|
options.stream ? 'stream-json' : 'text',
|
|
@@ -1256,7 +1260,7 @@ function resolveExternalHarnessAnalystInvocation(options) {
|
|
|
1256
1260
|
'exec',
|
|
1257
1261
|
'--skip-git-repo-check',
|
|
1258
1262
|
'--sandbox',
|
|
1259
|
-
'read-only',
|
|
1263
|
+
mode === 'write' ? 'workspace-write' : 'read-only',
|
|
1260
1264
|
...(options.stream ? ['--json'] : []),
|
|
1261
1265
|
'-C',
|
|
1262
1266
|
options.rootDir,
|
|
@@ -1271,10 +1275,7 @@ function resolveExternalHarnessAnalystInvocation(options) {
|
|
|
1271
1275
|
args: cursorSubcommandArgs(command, [
|
|
1272
1276
|
'--print',
|
|
1273
1277
|
'--trust',
|
|
1274
|
-
'--mode',
|
|
1275
|
-
'plan',
|
|
1276
|
-
'--sandbox',
|
|
1277
|
-
'enabled',
|
|
1278
|
+
...(mode === 'write' ? ['--force'] : ['--mode', 'plan', '--sandbox', 'enabled']),
|
|
1278
1279
|
'--workspace',
|
|
1279
1280
|
options.rootDir,
|
|
1280
1281
|
'--output-format',
|
|
@@ -1284,6 +1285,49 @@ function resolveExternalHarnessAnalystInvocation(options) {
|
|
|
1284
1285
|
streamEvents: options.stream,
|
|
1285
1286
|
};
|
|
1286
1287
|
}
|
|
1288
|
+
/** Coding-agent CLIs found on PATH, ranked best-first (claude > codex > cursor). */
|
|
1289
|
+
export function detectAvailableCodingAgents() {
|
|
1290
|
+
return rankHarnessAnalysts(detectAvailableHarnessAnalysts());
|
|
1291
|
+
}
|
|
1292
|
+
export function codingAgentDisplayName(adapter) {
|
|
1293
|
+
return analystDisplayName(adapter);
|
|
1294
|
+
}
|
|
1295
|
+
/** Spawn an installed coding-agent CLI with a prompt; capture its output. */
|
|
1296
|
+
export async function runCodingAgent(options) {
|
|
1297
|
+
const stream = options.stream ?? false;
|
|
1298
|
+
const invocation = resolveExternalHarnessAnalystInvocation({
|
|
1299
|
+
adapter: options.adapter,
|
|
1300
|
+
flags: {},
|
|
1301
|
+
prompt: options.prompt,
|
|
1302
|
+
rootDir: options.rootDir,
|
|
1303
|
+
stream,
|
|
1304
|
+
mode: options.mode,
|
|
1305
|
+
});
|
|
1306
|
+
const timeoutMs = options.timeoutMs ?? numericEnv('MODA_HARNESS_ANALYST_TIMEOUT_MS', 20 * 60 * 1000);
|
|
1307
|
+
const result = await runExternalAnalystWithRetries(invocation, {
|
|
1308
|
+
cwd: options.rootDir,
|
|
1309
|
+
env: buildExternalAnalystEnv(options.env ?? process.env),
|
|
1310
|
+
inheritedOutput: false,
|
|
1311
|
+
timeoutMs,
|
|
1312
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
1313
|
+
progress: Boolean(options.onProgress) || Boolean(options.progressToStderr),
|
|
1314
|
+
progressToStderr: options.progressToStderr ?? false,
|
|
1315
|
+
streamEvents: stream,
|
|
1316
|
+
adapter: invocation.adapter,
|
|
1317
|
+
maxAttempts: externalAnalystMaxAttempts(options.adapter),
|
|
1318
|
+
backoffMs: numericEnvAllowZero('MODA_HARNESS_ANALYST_RETRY_BACKOFF_MS', 6_000),
|
|
1319
|
+
onProgress: options.onProgress,
|
|
1320
|
+
});
|
|
1321
|
+
return {
|
|
1322
|
+
adapter: invocation.adapter,
|
|
1323
|
+
command: invocation.command,
|
|
1324
|
+
status: result.status,
|
|
1325
|
+
stdout: result.stdout,
|
|
1326
|
+
stderr: result.stderr,
|
|
1327
|
+
timedOut: result.timedOut,
|
|
1328
|
+
failedToStart: Boolean(result.error),
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1287
1331
|
function externalAnalystExtraArgs(adapter) {
|
|
1288
1332
|
return splitCommandLine(process.env[`MODA_HARNESS_ANALYST_${adapter.toUpperCase()}_ARGS`] ?? '');
|
|
1289
1333
|
}
|
package/dist/init/index.js
CHANGED
|
@@ -6,15 +6,16 @@ import { login, loadToken } from '../auth.js';
|
|
|
6
6
|
import { selectTenantAndCreateKey } from './tenant.js';
|
|
7
7
|
import { detectAgents } from './mcp.js';
|
|
8
8
|
import { installRules } from './rules.js';
|
|
9
|
-
import { initPrompts } from '../prompts.js';
|
|
10
|
-
import {
|
|
9
|
+
import { initPrompts, runPromptSync } from '../prompts.js';
|
|
10
|
+
import { runSkillSync } from '../skills.js';
|
|
11
11
|
// Coding-agent capture is temporarily disabled — see the hooks phase below.
|
|
12
12
|
// import { runHooksInstall } from '../workspace.js';
|
|
13
13
|
import { SDK_GUIDE_PATH, writeSdkIntegrationGuide } from './sdk-guide.js';
|
|
14
|
+
import { runSdkIntegration } from './sdk-integration.js';
|
|
14
15
|
import { createCommandContext } from '../command-context.js';
|
|
15
16
|
import { CliInputError } from '../errors.js';
|
|
16
17
|
import { runHarnessCommand } from '../harness.js';
|
|
17
|
-
import {
|
|
18
|
+
import { buildDoctorReport } from '../doctor.js';
|
|
18
19
|
import { renderInitBanner, renderInitComplete, renderInitNote, renderInitSetupPlan, renderInitStep, } from '../tui.js';
|
|
19
20
|
export async function runInit(flags, options = {}) {
|
|
20
21
|
const explicitYes = flags.yes === true ||
|
|
@@ -30,9 +31,7 @@ export async function runInit(flags, options = {}) {
|
|
|
30
31
|
if (human)
|
|
31
32
|
console.log(message);
|
|
32
33
|
};
|
|
33
|
-
|
|
34
|
-
// underscore-prefixed binding so re-enabling it is a rename away.
|
|
35
|
-
const _warn = (message) => {
|
|
34
|
+
const warn = (message) => {
|
|
36
35
|
if (human)
|
|
37
36
|
console.error(message);
|
|
38
37
|
};
|
|
@@ -102,6 +101,32 @@ export async function runInit(flags, options = {}) {
|
|
|
102
101
|
if (human)
|
|
103
102
|
printHarnessDetectionSummary();
|
|
104
103
|
emit('harness', 'Harness discovery complete.', 'completed');
|
|
104
|
+
// Upload the cited harness graph so Moda's backend has the understanding, not
|
|
105
|
+
// just local artifacts. `harness sync --from-report` refuses an unapproved
|
|
106
|
+
// report, so approve it first. Best-effort: a network blip on either call must
|
|
107
|
+
// not abort onboarding — auth + key provisioning already succeeded.
|
|
108
|
+
try {
|
|
109
|
+
await runHarnessCommand(createCommandContext({
|
|
110
|
+
command: 'harness',
|
|
111
|
+
positional: 'approve',
|
|
112
|
+
positionals: ['approve'],
|
|
113
|
+
flags: commandFlags({ yes: true, json: json || agent, agent, quiet: true, 'no-telemetry': true }),
|
|
114
|
+
}));
|
|
115
|
+
await runHarnessCommand(createCommandContext({
|
|
116
|
+
command: 'harness',
|
|
117
|
+
positional: 'sync',
|
|
118
|
+
positionals: ['sync'],
|
|
119
|
+
flags: commandFlags({ 'from-report': true, yes: true, json: json || agent, agent, quiet: true, 'no-telemetry': true }),
|
|
120
|
+
}));
|
|
121
|
+
log(renderInitNote('Synced harness graph to Moda'));
|
|
122
|
+
emit('harness', 'Harness graph synced to Moda.', 'completed');
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
126
|
+
warn(renderInitNote(`Harness sync skipped: ${message}`, 'warning'));
|
|
127
|
+
warn(' Run `moda harness approve --yes && moda harness sync --from-report` later to upload.');
|
|
128
|
+
emit('harness', 'Harness sync skipped.', 'warning');
|
|
129
|
+
}
|
|
105
130
|
phases.push({ id: 'harness', title: 'Harness Discovery', status: 'completed' });
|
|
106
131
|
// Step 5: Setup plan
|
|
107
132
|
log(renderInitStep(5, 7, 'Setup Plan', 'Every write/sync action is explicit before it is applied.'));
|
|
@@ -109,7 +134,7 @@ export async function runInit(flags, options = {}) {
|
|
|
109
134
|
// Coding-agent capture is temporarily disabled; never prompt for or install trace hooks.
|
|
110
135
|
// installHooks: await shouldIngestCodingAgentTraces(flags, yes),
|
|
111
136
|
installHooks: false,
|
|
112
|
-
|
|
137
|
+
integrateSdk: await shouldIntegrateSdk(flags, yes),
|
|
113
138
|
configureCiCd: await shouldConfigureCiCd(flags, yes),
|
|
114
139
|
});
|
|
115
140
|
log(renderInitSetupPlan(setupPlan));
|
|
@@ -140,34 +165,69 @@ export async function runInit(flags, options = {}) {
|
|
|
140
165
|
const agents = detectAgents();
|
|
141
166
|
if (agents.length > 0) {
|
|
142
167
|
log(renderInitNote(`Detected coding agents: ${agents.join(', ')}`));
|
|
168
|
+
// Install only the moda-cli skill so those agents can query Moda analytics
|
|
169
|
+
// from the terminal. SDK integration is a one-shot agent run below, not a
|
|
170
|
+
// persistent skill.
|
|
143
171
|
await installRules(agents, { yes, silent: !human });
|
|
144
|
-
// Pull any skills the pipeline has already generated for this tenant into
|
|
145
|
-
// the local agent skill dirs. Best-effort: a brand-new tenant has none yet,
|
|
146
|
-
// and a transient ingest/network error must not fail setup.
|
|
147
|
-
try {
|
|
148
|
-
const pulled = await pullSkills({ silent: true });
|
|
149
|
-
if (pulled.wrote + pulled.updated > 0) {
|
|
150
|
-
log(renderInitNote(`Pulled ${pulled.wrote + pulled.updated} learned skill(s) into agent skills`));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// Silent: skills are additive; never block init on them.
|
|
155
|
-
}
|
|
156
172
|
}
|
|
157
173
|
else {
|
|
158
174
|
log(renderInitNote('No coding agents detected, skipping', 'skipped'));
|
|
159
175
|
}
|
|
160
176
|
await initPrompts({ silent: true });
|
|
161
177
|
log(renderInitNote('Created or verified .moda/prompts.yml'));
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
// Push the manifest's prompt files to Moda so production traces can attribute
|
|
179
|
+
// managed prompt versions. A fresh repo has none yet — that's a harmless no-op.
|
|
180
|
+
try {
|
|
181
|
+
const promptSync = await runPromptSync({}, { profile: stringFlag(flags.profile) });
|
|
182
|
+
log(renderInitNote(promptSync.synced.length > 0
|
|
183
|
+
? `Synced ${promptSync.synced.length} prompt(s) to Moda`
|
|
184
|
+
: 'No prompt files to sync yet (add paths to .moda/prompts.yml)'));
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
+
warn(renderInitNote(`Prompt sync skipped: ${message}`, 'warning'));
|
|
189
|
+
}
|
|
190
|
+
// Push locally-authored skills (`.claude/skills/**/SKILL.md`) to Moda, in
|
|
191
|
+
// parallel intent with prompt sync. A fresh repo has none yet — harmless
|
|
192
|
+
// no-op. Best-effort: a network blip must not abort onboarding.
|
|
193
|
+
try {
|
|
194
|
+
const skillSync = await runSkillSync({}, { profile: stringFlag(flags.profile) });
|
|
195
|
+
if (skillSync.synced.length > 0) {
|
|
196
|
+
log(renderInitNote(`Synced ${skillSync.synced.length} skill(s) to Moda`));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
log(renderInitNote('No local skills to sync yet (add .claude/skills/<name>/SKILL.md)'));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
204
|
+
warn(renderInitNote(`Skill sync skipped: ${message}`, 'warning'));
|
|
205
|
+
}
|
|
206
|
+
const sdkAction = setupPlan.find((action) => action.category === 'sdk');
|
|
207
|
+
if (sdkAction?.approved === true) {
|
|
208
|
+
log(renderInitNote('Running your coding agent to integrate the Moda SDK (this can take a few minutes)...', 'warning'));
|
|
209
|
+
const result = await runSdkIntegration({ human });
|
|
210
|
+
if (result.status === 'integrated') {
|
|
211
|
+
log(renderInitNote(`Integrated the Moda SDK with ${result.adapter}. Review the diff before committing.`));
|
|
212
|
+
markApplied(setupPlan, 'sdk');
|
|
213
|
+
}
|
|
214
|
+
else if (result.status === 'no-agent') {
|
|
215
|
+
const guidePath = writeSdkIntegrationGuide();
|
|
216
|
+
sdkAction.files = [guidePath];
|
|
217
|
+
log(renderInitNote(`No coding agent CLI found; wrote ${guidePath} to integrate by hand`, 'warning'));
|
|
218
|
+
markApplied(setupPlan, 'sdk');
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const guidePath = writeSdkIntegrationGuide();
|
|
222
|
+
sdkAction.files = [guidePath];
|
|
223
|
+
warn(renderInitNote(`SDK integration via ${result.adapter} did not complete: ${result.detail}`, 'warning'));
|
|
224
|
+
log(renderInitNote(`Wrote ${guidePath} so you can finish the integration manually`));
|
|
225
|
+
markApplied(setupPlan, 'sdk');
|
|
226
|
+
}
|
|
167
227
|
}
|
|
168
228
|
else {
|
|
169
|
-
log(renderInitNote('Skipped SDK integration
|
|
170
|
-
markApplied(setupPlan, 'sdk', 'User skipped SDK integration
|
|
229
|
+
log(renderInitNote('Skipped SDK integration', 'skipped'));
|
|
230
|
+
markApplied(setupPlan, 'sdk', 'User skipped SDK integration.');
|
|
171
231
|
}
|
|
172
232
|
const configureCiCd = setupPlan.find((action) => action.category === 'ci-cd')?.approved === true;
|
|
173
233
|
if (configureCiCd) {
|
|
@@ -182,22 +242,26 @@ export async function runInit(flags, options = {}) {
|
|
|
182
242
|
markApplied(setupPlan, 'skills');
|
|
183
243
|
emit('apply', 'Approved changes applied.', 'completed');
|
|
184
244
|
phases.push({ id: 'apply', title: 'Apply Approved Changes', status: 'completed' });
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
245
|
+
const offlineValidation = isTruthyFlag(flagValue(flags, 'offline'));
|
|
246
|
+
log(renderInitStep(7, 7, 'Validate', offlineValidation
|
|
247
|
+
? 'Running local doctor checks so the setup leaves a clear state.'
|
|
248
|
+
: 'Running Moda doctor online to confirm setup and that telemetry is reaching Moda.'));
|
|
249
|
+
emit('validate', `Running Moda doctor diagnostics (${offlineValidation ? 'offline' : 'online'}).`);
|
|
250
|
+
const doctorReport = await buildDoctorReport(createCommandContext({
|
|
188
251
|
command: 'doctor',
|
|
189
252
|
positional: undefined,
|
|
190
253
|
positionals: [],
|
|
191
|
-
flags: commandFlags({
|
|
192
|
-
|
|
193
|
-
quiet: true,
|
|
194
|
-
offline: true,
|
|
195
|
-
'no-telemetry': true,
|
|
196
|
-
}),
|
|
197
|
-
}));
|
|
254
|
+
flags: commandFlags({ json: true, quiet: true, 'no-telemetry': true }),
|
|
255
|
+
}), { offline: offlineValidation });
|
|
198
256
|
markApplied(setupPlan, 'validation');
|
|
199
|
-
log(renderInitNote(
|
|
200
|
-
|
|
257
|
+
log(renderInitNote(`Validation checks completed: ${doctorReport.summary.pass} passing, ${doctorReport.summary.warning} warning, ${doctorReport.summary.fail} failing`));
|
|
258
|
+
if (!offlineValidation) {
|
|
259
|
+
const cloud = doctorReport.checks.find((check) => check.id === 'cloud_data_flow');
|
|
260
|
+
if (cloud) {
|
|
261
|
+
log(renderInitNote(`Moda Cloud telemetry: ${cloud.status}`, cloud.status === 'pass' ? undefined : 'warning'));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
emit('validate', 'Validation checks complete.', doctorReport.summary.fail > 0 ? 'warning' : 'completed');
|
|
201
265
|
phases.push({ id: 'validate', title: 'Validate', status: 'completed' });
|
|
202
266
|
// Done
|
|
203
267
|
log(renderInitComplete({
|
|
@@ -212,7 +276,7 @@ function buildSetupPlan(options) {
|
|
|
212
276
|
return [
|
|
213
277
|
{
|
|
214
278
|
category: 'harness',
|
|
215
|
-
title: 'Analyze harness
|
|
279
|
+
title: 'Analyze harness, write cited report, and sync the graph to Moda',
|
|
216
280
|
status: 'planned',
|
|
217
281
|
approved: true,
|
|
218
282
|
files: [
|
|
@@ -222,7 +286,7 @@ function buildSetupPlan(options) {
|
|
|
222
286
|
'.moda/harness.json',
|
|
223
287
|
'.moda/HARNESS.md',
|
|
224
288
|
],
|
|
225
|
-
command: 'moda harness analyze --analyst=auto --summary',
|
|
289
|
+
command: 'moda harness analyze --analyst=auto --summary && moda harness approve --yes && moda harness sync --from-report',
|
|
226
290
|
},
|
|
227
291
|
{
|
|
228
292
|
category: 'hooks',
|
|
@@ -235,30 +299,34 @@ function buildSetupPlan(options) {
|
|
|
235
299
|
},
|
|
236
300
|
{
|
|
237
301
|
category: 'prompts',
|
|
238
|
-
title: 'Initialize prompt registry manifest',
|
|
302
|
+
title: 'Initialize the prompt registry manifest and sync prompts to Moda',
|
|
239
303
|
status: 'planned',
|
|
240
304
|
approved: true,
|
|
241
305
|
files: ['.moda/prompts.yml'],
|
|
242
|
-
command: 'moda prompts init',
|
|
306
|
+
command: 'moda prompts init && moda prompts sync',
|
|
243
307
|
},
|
|
244
308
|
{
|
|
245
309
|
category: 'skills',
|
|
246
|
-
title: '
|
|
310
|
+
title: 'Install the moda-cli skill into detected coding agents and sync local skills to Moda',
|
|
247
311
|
status: 'planned',
|
|
248
312
|
approved: true,
|
|
249
313
|
files: ['.claude/skills', '.cursor/rules'],
|
|
250
|
-
|
|
314
|
+
// Skill install into detected agents happens inside `moda init`; there is no
|
|
315
|
+
// standalone install command, so surface only the re-runnable sync here.
|
|
316
|
+
command: 'moda skills sync',
|
|
251
317
|
},
|
|
252
318
|
{
|
|
253
319
|
category: 'sdk',
|
|
254
|
-
title: '
|
|
255
|
-
status: options.
|
|
256
|
-
approved: options.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
320
|
+
title: 'Run a coding agent to integrate the Moda SDK',
|
|
321
|
+
status: options.integrateSdk ? 'planned' : 'manual',
|
|
322
|
+
approved: options.integrateSdk,
|
|
323
|
+
// Files are repo-specific and unknown ahead of the agent run; the fallback
|
|
324
|
+
// path rewrites this to the guide path when no agent CLI is available.
|
|
325
|
+
files: [],
|
|
326
|
+
command: 'moda init',
|
|
327
|
+
reason: options.integrateSdk
|
|
328
|
+
? 'Installs the SDK package and edits source via your coding agent; review the diff before committing.'
|
|
329
|
+
: `SDK integration stays manual; see ${SDK_GUIDE_PATH} if you generate the guide.`,
|
|
262
330
|
},
|
|
263
331
|
{
|
|
264
332
|
category: 'ci-cd',
|
|
@@ -271,11 +339,11 @@ function buildSetupPlan(options) {
|
|
|
271
339
|
},
|
|
272
340
|
{
|
|
273
341
|
category: 'validation',
|
|
274
|
-
title: 'Run
|
|
342
|
+
title: 'Run Moda doctor online to confirm telemetry is reaching Moda',
|
|
275
343
|
status: 'planned',
|
|
276
344
|
approved: true,
|
|
277
345
|
files: [],
|
|
278
|
-
command: 'moda doctor',
|
|
346
|
+
command: 'moda doctor --online',
|
|
279
347
|
},
|
|
280
348
|
];
|
|
281
349
|
}
|
|
@@ -383,16 +451,19 @@ async function shouldConfigureCiCd(flags, yes) {
|
|
|
383
451
|
default: false,
|
|
384
452
|
});
|
|
385
453
|
}
|
|
386
|
-
async function
|
|
387
|
-
const explicit = parseOptionalBoolean(flagValue(flags, 'sdk-guide', 'sdk_guide', 'sdk'));
|
|
454
|
+
async function shouldIntegrateSdk(flags, yes) {
|
|
455
|
+
const explicit = parseOptionalBoolean(flagValue(flags, 'sdk-integrate', 'sdk_integrate', 'sdk-guide', 'sdk_guide', 'sdk'));
|
|
388
456
|
if (explicit !== undefined)
|
|
389
457
|
return explicit;
|
|
390
|
-
if (isTruthyFlag(flagValue(flags, 'no-sdk-guide', 'no_sdk_guide')))
|
|
458
|
+
if (isTruthyFlag(flagValue(flags, 'no-sdk-integrate', 'no_sdk_integrate', 'no-sdk-guide', 'no_sdk_guide', 'no-sdk', 'no_sdk'))) {
|
|
391
459
|
return false;
|
|
460
|
+
}
|
|
461
|
+
// SDK integration now installs packages and edits source via a coding agent.
|
|
462
|
+
// Never do that unattended (--yes/--json/--agent); require an explicit opt-in.
|
|
392
463
|
if (yes)
|
|
393
|
-
return
|
|
464
|
+
return false;
|
|
394
465
|
return confirm({
|
|
395
|
-
message: '
|
|
466
|
+
message: 'Run your coding agent now to install the Moda SDK and instrument your LLM calls? (Edits source, installs packages.)',
|
|
396
467
|
default: true,
|
|
397
468
|
});
|
|
398
469
|
}
|
package/dist/init/sdk-guide.d.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
export declare const SDK_GUIDE_PATH = ".moda/SDK_INTEGRATION.md";
|
|
2
|
+
type RuntimeSummary = {
|
|
3
|
+
runtimeAgents: number;
|
|
4
|
+
agentFamilies: number;
|
|
5
|
+
frameworks: string[];
|
|
6
|
+
providers: string[];
|
|
7
|
+
runtimeAgentNames: string[];
|
|
8
|
+
};
|
|
9
|
+
export type SdkGuideContext = {
|
|
10
|
+
languages: string[];
|
|
11
|
+
installCommands: string[];
|
|
12
|
+
providerHints: string[];
|
|
13
|
+
frameworkHints: string[];
|
|
14
|
+
packageManagers: string[];
|
|
15
|
+
runtimeSummary: RuntimeSummary;
|
|
16
|
+
};
|
|
2
17
|
export declare function writeSdkIntegrationGuide(cwd?: string): string;
|
|
18
|
+
export declare function detectSdkGuideContext(cwd: string): SdkGuideContext;
|
|
19
|
+
export declare function renderSdkIntegrationGuide(context: SdkGuideContext): string;
|
|
20
|
+
export {};
|
|
3
21
|
//# sourceMappingURL=sdk-guide.d.ts.map
|
package/dist/init/sdk-guide.js
CHANGED
|
@@ -8,7 +8,7 @@ export function writeSdkIntegrationGuide(cwd = process.cwd()) {
|
|
|
8
8
|
writeFileSync(absolutePath, renderSdkIntegrationGuide(detectSdkGuideContext(cwd)));
|
|
9
9
|
return relativePath;
|
|
10
10
|
}
|
|
11
|
-
function detectSdkGuideContext(cwd) {
|
|
11
|
+
export function detectSdkGuideContext(cwd) {
|
|
12
12
|
const languages = [];
|
|
13
13
|
const installCommands = [];
|
|
14
14
|
const packageManagers = [];
|
|
@@ -64,7 +64,7 @@ function detectSdkGuideContext(cwd) {
|
|
|
64
64
|
runtimeSummary,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
function renderSdkIntegrationGuide(context) {
|
|
67
|
+
export function renderSdkIntegrationGuide(context) {
|
|
68
68
|
const languages = context.languages.length > 0 ? context.languages.join(', ') : 'Not detected yet';
|
|
69
69
|
const packageManagers = context.packageManagers.length > 0 ? context.packageManagers.join(', ') : 'Not detected yet';
|
|
70
70
|
const installCommands = context.installCommands.length > 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CodingAgentAdapter } from '../harness.js';
|
|
2
|
+
export type SdkIntegrationResult = {
|
|
3
|
+
status: 'integrated';
|
|
4
|
+
adapter: CodingAgentAdapter;
|
|
5
|
+
} | {
|
|
6
|
+
status: 'no-agent';
|
|
7
|
+
} | {
|
|
8
|
+
status: 'failed';
|
|
9
|
+
adapter: CodingAgentAdapter;
|
|
10
|
+
detail: string;
|
|
11
|
+
};
|
|
12
|
+
export interface SdkIntegrationOptions {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
/** Explicit adapter override (e.g. from --analyst); otherwise best-ranked installed CLI. */
|
|
15
|
+
adapter?: CodingAgentAdapter;
|
|
16
|
+
/** Stream the agent's progress to the user (human terminals only). */
|
|
17
|
+
human?: boolean;
|
|
18
|
+
onProgress?: (message: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pick an installed coding-agent CLI and run it, in write mode, to integrate the
|
|
22
|
+
* Moda SDK into the repo. Returns `no-agent` (without throwing) when no CLI is
|
|
23
|
+
* installed so the caller can fall back to writing the static guide.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runSdkIntegration(options?: SdkIntegrationOptions): Promise<SdkIntegrationResult>;
|
|
26
|
+
//# sourceMappingURL=sdk-integration.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/init/sdk-integration.ts — Run an installed coding agent to integrate the
|
|
2
|
+
// Moda SDK directly, instead of leaving a guide for the user to action by hand.
|
|
3
|
+
//
|
|
4
|
+
// This reuses the same coding-agent spawn machinery as `moda harness analyze`
|
|
5
|
+
// (see src/harness.ts), but in write mode: the agent is expected to install the
|
|
6
|
+
// SDK package and edit source to instrument LLM calls.
|
|
7
|
+
import { codingAgentDisplayName, detectAvailableCodingAgents, runCodingAgent, } from '../harness.js';
|
|
8
|
+
import { detectSdkGuideContext, renderSdkIntegrationGuide } from './sdk-guide.js';
|
|
9
|
+
/**
|
|
10
|
+
* Pick an installed coding-agent CLI and run it, in write mode, to integrate the
|
|
11
|
+
* Moda SDK into the repo. Returns `no-agent` (without throwing) when no CLI is
|
|
12
|
+
* installed so the caller can fall back to writing the static guide.
|
|
13
|
+
*/
|
|
14
|
+
export async function runSdkIntegration(options = {}) {
|
|
15
|
+
const cwd = options.cwd ?? process.cwd();
|
|
16
|
+
const available = detectAvailableCodingAgents();
|
|
17
|
+
if (available.length === 0)
|
|
18
|
+
return { status: 'no-agent' };
|
|
19
|
+
const adapter = options.adapter && available.some((item) => item.adapter === options.adapter)
|
|
20
|
+
? options.adapter
|
|
21
|
+
: available[0].adapter;
|
|
22
|
+
const prompt = renderSdkIntegrationPrompt(cwd);
|
|
23
|
+
const human = options.human ?? false;
|
|
24
|
+
const emit = (message) => {
|
|
25
|
+
options.onProgress?.(message);
|
|
26
|
+
if (human)
|
|
27
|
+
process.stderr.write(` ${codingAgentDisplayName(adapter)}: ${message}\n`);
|
|
28
|
+
};
|
|
29
|
+
const run = await runCodingAgent({
|
|
30
|
+
adapter,
|
|
31
|
+
prompt,
|
|
32
|
+
rootDir: cwd,
|
|
33
|
+
mode: 'write',
|
|
34
|
+
stream: human,
|
|
35
|
+
progressToStderr: false,
|
|
36
|
+
onProgress: (message) => emit(message),
|
|
37
|
+
});
|
|
38
|
+
if (run.failedToStart) {
|
|
39
|
+
return { status: 'failed', adapter, detail: `${codingAgentDisplayName(adapter)} CLI failed to start.` };
|
|
40
|
+
}
|
|
41
|
+
if (run.timedOut) {
|
|
42
|
+
return { status: 'failed', adapter, detail: `${codingAgentDisplayName(adapter)} timed out during SDK integration.` };
|
|
43
|
+
}
|
|
44
|
+
if (run.status !== 0) {
|
|
45
|
+
const tail = `${run.stdout}\n${run.stderr}`.trim().slice(-800);
|
|
46
|
+
return {
|
|
47
|
+
status: 'failed',
|
|
48
|
+
adapter,
|
|
49
|
+
detail: `${codingAgentDisplayName(adapter)} exited with status ${run.status ?? 'unknown'}.${tail ? `\n${tail}` : ''}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return { status: 'integrated', adapter };
|
|
53
|
+
}
|
|
54
|
+
function renderSdkIntegrationPrompt(cwd) {
|
|
55
|
+
const guide = renderSdkIntegrationGuide(detectSdkGuideContext(cwd));
|
|
56
|
+
return [
|
|
57
|
+
'You are running inside `moda init` as a one-shot SDK integration task. The user has',
|
|
58
|
+
'approved letting you install packages and edit source files in THIS repository to add',
|
|
59
|
+
'Moda LLM observability. Do the integration now — do not just describe it.',
|
|
60
|
+
'',
|
|
61
|
+
'Scope and safety rules:',
|
|
62
|
+
'- Only touch this repository (the working directory you were launched in).',
|
|
63
|
+
'- Install the Moda SDK with the package manager this repo already uses.',
|
|
64
|
+
'- Instrument the real LLM call sites; do not invent files or frameworks that are not here.',
|
|
65
|
+
'- Never write MODA_API_KEY (or any secret) into source files, and never create or edit',
|
|
66
|
+
' .env files. The key belongs in the environment / a secret manager at runtime.',
|
|
67
|
+
'- Make the smallest correct change. Match the existing code style and imports.',
|
|
68
|
+
'- If something is ambiguous or the repo has no LLM calls to instrument, stop and explain',
|
|
69
|
+
' rather than guessing.',
|
|
70
|
+
'',
|
|
71
|
+
'Follow this Moda-authored integration spec exactly (it reflects the detected languages,',
|
|
72
|
+
'package managers, providers, and frameworks for this repo):',
|
|
73
|
+
'',
|
|
74
|
+
guide,
|
|
75
|
+
'',
|
|
76
|
+
'When finished, print a short summary of: the package(s) installed, the files you edited,',
|
|
77
|
+
'and any step a human still needs to do (e.g. setting MODA_API_KEY in the deploy env).',
|
|
78
|
+
].join('\n');
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=sdk-integration.js.map
|
package/dist/prompts.d.ts
CHANGED
|
@@ -3,4 +3,20 @@ export declare function runPromptsCommand(positionals: string[], flags: Record<s
|
|
|
3
3
|
export declare function initPrompts(options?: {
|
|
4
4
|
silent?: boolean;
|
|
5
5
|
}): Promise<void>;
|
|
6
|
+
export interface PromptSyncResult {
|
|
7
|
+
synced: Array<{
|
|
8
|
+
key: string;
|
|
9
|
+
promptId?: string;
|
|
10
|
+
versionId?: string;
|
|
11
|
+
contentHash: string;
|
|
12
|
+
sourcePath?: string;
|
|
13
|
+
}>;
|
|
14
|
+
dryRun: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sync the manifest's prompt files to the backend and update the lockfile.
|
|
18
|
+
* Returns the synced list without printing, so callers (e.g. `moda init`)
|
|
19
|
+
* control their own output. `moda prompts sync` wraps this with JSON output.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runPromptSync(flags: Record<string, string>, profileOptions: ProfileIOOptions): Promise<PromptSyncResult>;
|
|
6
22
|
//# sourceMappingURL=prompts.d.ts.map
|
package/dist/prompts.js
CHANGED
|
@@ -59,7 +59,12 @@ async function syncPrompts(flags, profileOptions) {
|
|
|
59
59
|
}
|
|
60
60
|
await syncPromptsOnce(flags, profileOptions);
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Sync the manifest's prompt files to the backend and update the lockfile.
|
|
64
|
+
* Returns the synced list without printing, so callers (e.g. `moda init`)
|
|
65
|
+
* control their own output. `moda prompts sync` wraps this with JSON output.
|
|
66
|
+
*/
|
|
67
|
+
export async function runPromptSync(flags, profileOptions) {
|
|
63
68
|
await initPrompts({ silent: true });
|
|
64
69
|
const prompts = discoverPrompts();
|
|
65
70
|
const sourceCommit = getGitCommit();
|
|
@@ -81,9 +86,13 @@ async function syncPromptsOnce(flags, profileOptions) {
|
|
|
81
86
|
if (!dryRun) {
|
|
82
87
|
writeLock(prompts, response.synced ?? []);
|
|
83
88
|
}
|
|
89
|
+
return { synced: response.synced ?? [], dryRun };
|
|
90
|
+
}
|
|
91
|
+
async function syncPromptsOnce(flags, profileOptions) {
|
|
92
|
+
const result = await runPromptSync(flags, profileOptions);
|
|
84
93
|
printJson({
|
|
85
|
-
synced:
|
|
86
|
-
lockfile: dryRun ? undefined : LOCK_PATH,
|
|
94
|
+
synced: result.synced,
|
|
95
|
+
lockfile: result.dryRun ? undefined : LOCK_PATH,
|
|
87
96
|
});
|
|
88
97
|
}
|
|
89
98
|
async function watchPromptSync(flags, profileOptions) {
|
package/dist/skills.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ParsedArgs } from './cli.js';
|
|
2
|
+
import type { ProfileIOOptions } from './profiles.js';
|
|
2
3
|
interface PullSummary {
|
|
3
4
|
total: number;
|
|
4
5
|
pruned: number;
|
|
@@ -17,6 +18,31 @@ export declare function pullSkills(options?: {
|
|
|
17
18
|
cwd?: string;
|
|
18
19
|
silent?: boolean;
|
|
19
20
|
}): Promise<PullSummary>;
|
|
21
|
+
export interface SkillSyncResult {
|
|
22
|
+
synced: Array<{
|
|
23
|
+
key: string;
|
|
24
|
+
skillId?: string;
|
|
25
|
+
versionId?: string;
|
|
26
|
+
version?: number;
|
|
27
|
+
contentHash: string;
|
|
28
|
+
changed?: boolean;
|
|
29
|
+
sourcePath?: string;
|
|
30
|
+
}>;
|
|
31
|
+
dryRun: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Push locally-authored `.claude/skills/**\/SKILL.md` files up to Moda via
|
|
35
|
+
* `POST /api/skills/sync`. Modeled on `runPromptSync`: returns the synced list
|
|
36
|
+
* without printing so callers (e.g. `moda init`) control their own output.
|
|
37
|
+
* `moda skills sync` wraps this with JSON output.
|
|
38
|
+
*
|
|
39
|
+
* Skills that Moda itself manages — those previously pulled/generated (tracked
|
|
40
|
+
* in `.moda/skills.yml`) and the shipped `moda-cli` skill — are excluded so we
|
|
41
|
+
* only push skills the user authored.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runSkillSync(flags?: Record<string, string>, profileOptions?: ProfileIOOptions, options?: {
|
|
44
|
+
cwd?: string;
|
|
45
|
+
}): Promise<SkillSyncResult>;
|
|
20
46
|
/**
|
|
21
47
|
* `moda skills <subcommand>` dispatcher.
|
|
22
48
|
*/
|
package/dist/skills.js
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
// Skills are managed artifacts. A pull installs the latest server-published
|
|
16
16
|
// generation snapshot, tracked in .moda/skills.yml, and prunes only files that a
|
|
17
17
|
// previous Moda pull wrote.
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
18
19
|
import { createHash } from 'node:crypto';
|
|
19
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
20
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
20
21
|
import { basename, dirname, isAbsolute, join, normalize, relative, resolve } from 'node:path';
|
|
22
|
+
import { callControlAPI } from './api-client.js';
|
|
21
23
|
import { resolveApiKey, resolveIngestUrl, resolveTenantId } from './config.js';
|
|
22
24
|
import { claudeSkillToCursorRule } from './init/rules.js';
|
|
23
25
|
import { detectAgents } from './init/mcp.js';
|
|
@@ -189,6 +191,157 @@ function recordOutcome(summary, status, log, relativePath) {
|
|
|
189
191
|
break;
|
|
190
192
|
}
|
|
191
193
|
}
|
|
194
|
+
const SKILLS_DIR = join('.claude', 'skills');
|
|
195
|
+
const SKILL_FILE = 'SKILL.md';
|
|
196
|
+
const MAX_SKILL_WALK_DEPTH = 4;
|
|
197
|
+
/**
|
|
198
|
+
* Push locally-authored `.claude/skills/**\/SKILL.md` files up to Moda via
|
|
199
|
+
* `POST /api/skills/sync`. Modeled on `runPromptSync`: returns the synced list
|
|
200
|
+
* without printing so callers (e.g. `moda init`) control their own output.
|
|
201
|
+
* `moda skills sync` wraps this with JSON output.
|
|
202
|
+
*
|
|
203
|
+
* Skills that Moda itself manages — those previously pulled/generated (tracked
|
|
204
|
+
* in `.moda/skills.yml`) and the shipped `moda-cli` skill — are excluded so we
|
|
205
|
+
* only push skills the user authored.
|
|
206
|
+
*/
|
|
207
|
+
export async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
|
|
208
|
+
const cwd = options.cwd ?? process.cwd();
|
|
209
|
+
const dryRun = flags['dry-run'] === 'true';
|
|
210
|
+
const skills = discoverLocalSkills(cwd);
|
|
211
|
+
if (skills.length === 0) {
|
|
212
|
+
return { synced: [], dryRun };
|
|
213
|
+
}
|
|
214
|
+
const sourceCommit = getGitCommit(cwd);
|
|
215
|
+
const response = (await callControlAPI('/skills/sync', {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
skills: skills.map((skill) => ({
|
|
219
|
+
key: skill.key,
|
|
220
|
+
name: skill.name,
|
|
221
|
+
description: skill.description,
|
|
222
|
+
skillMd: skill.skillMd,
|
|
223
|
+
sourcePath: skill.sourcePath,
|
|
224
|
+
contentHash: skill.contentHash,
|
|
225
|
+
sourceCommit,
|
|
226
|
+
})),
|
|
227
|
+
sourceCommit,
|
|
228
|
+
dryRun,
|
|
229
|
+
}),
|
|
230
|
+
}, profileOptions));
|
|
231
|
+
return { synced: response.synced ?? [], dryRun };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Discover user-authored skills under `.claude/skills/`. Excludes Moda-managed
|
|
235
|
+
* skills (the `.moda/skills.yml` managed paths and the shipped `moda-cli`
|
|
236
|
+
* skill) and validates each directory key.
|
|
237
|
+
*/
|
|
238
|
+
function discoverLocalSkills(cwd) {
|
|
239
|
+
const skillsRoot = join(cwd, SKILLS_DIR);
|
|
240
|
+
if (!existsSync(skillsRoot))
|
|
241
|
+
return [];
|
|
242
|
+
const managed = new Set(managedSkillPaths(cwd));
|
|
243
|
+
const skills = [];
|
|
244
|
+
for (const absPath of walkSkillFiles(skillsRoot, 0)) {
|
|
245
|
+
const sourcePath = relative(cwd, absPath).replace(/\\/g, '/');
|
|
246
|
+
if (managed.has(sourcePath))
|
|
247
|
+
continue;
|
|
248
|
+
const key = basename(dirname(absPath));
|
|
249
|
+
// Never re-upload Moda's own shipped skill; only accept plain identifiers.
|
|
250
|
+
if (key === 'moda-cli' || !/^[A-Za-z0-9_-]+$/.test(key))
|
|
251
|
+
continue;
|
|
252
|
+
let content;
|
|
253
|
+
try {
|
|
254
|
+
content = readFileSync(absPath, 'utf8');
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (!content.trim())
|
|
260
|
+
continue;
|
|
261
|
+
const { name, description } = parseSkillFrontmatter(content, key);
|
|
262
|
+
skills.push({
|
|
263
|
+
key,
|
|
264
|
+
name,
|
|
265
|
+
description,
|
|
266
|
+
skillMd: content,
|
|
267
|
+
sourcePath,
|
|
268
|
+
contentHash: sha256(content),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return skills.sort((a, b) => a.key.localeCompare(b.key));
|
|
272
|
+
}
|
|
273
|
+
/** Managed SKILL.md paths recorded by a previous `moda skills pull`. */
|
|
274
|
+
function managedSkillPaths(cwd) {
|
|
275
|
+
const manifest = readSkillsManifest(cwd);
|
|
276
|
+
if (!manifest)
|
|
277
|
+
return [];
|
|
278
|
+
return manifest.managedPaths.filter((path) => path.startsWith('.claude/skills/') && path.endsWith('/SKILL.md'));
|
|
279
|
+
}
|
|
280
|
+
/** Recursively find `SKILL.md` files, never following symlinks. */
|
|
281
|
+
function walkSkillFiles(dir, depth) {
|
|
282
|
+
if (depth > MAX_SKILL_WALK_DEPTH)
|
|
283
|
+
return [];
|
|
284
|
+
let entries;
|
|
285
|
+
try {
|
|
286
|
+
entries = readdirSync(dir);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
const files = [];
|
|
292
|
+
for (const entry of entries) {
|
|
293
|
+
const fullPath = join(dir, entry);
|
|
294
|
+
let stat;
|
|
295
|
+
try {
|
|
296
|
+
stat = lstatSync(fullPath);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (stat.isSymbolicLink())
|
|
302
|
+
continue;
|
|
303
|
+
if (stat.isDirectory()) {
|
|
304
|
+
files.push(...walkSkillFiles(fullPath, depth + 1));
|
|
305
|
+
}
|
|
306
|
+
else if (stat.isFile() && entry === SKILL_FILE) {
|
|
307
|
+
files.push(fullPath);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return files;
|
|
311
|
+
}
|
|
312
|
+
/** Pull `name`/`description` from a SKILL.md YAML frontmatter block. */
|
|
313
|
+
function parseSkillFrontmatter(raw, fallbackKey) {
|
|
314
|
+
let name = fallbackKey;
|
|
315
|
+
let description;
|
|
316
|
+
if (raw.startsWith('---')) {
|
|
317
|
+
const end = raw.indexOf('\n---', 3);
|
|
318
|
+
if (end !== -1) {
|
|
319
|
+
const frontmatter = raw.slice(3, end);
|
|
320
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
321
|
+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
322
|
+
if (nameMatch)
|
|
323
|
+
name = stripQuotes(nameMatch[1].trim()) || fallbackKey;
|
|
324
|
+
if (descMatch)
|
|
325
|
+
description = stripQuotes(descMatch[1].trim()) || undefined;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { name, description };
|
|
329
|
+
}
|
|
330
|
+
function stripQuotes(value) {
|
|
331
|
+
return value.replace(/^["']|["']$/g, '');
|
|
332
|
+
}
|
|
333
|
+
function getGitCommit(cwd) {
|
|
334
|
+
try {
|
|
335
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
336
|
+
cwd,
|
|
337
|
+
encoding: 'utf8',
|
|
338
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
339
|
+
}).trim();
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
192
345
|
/**
|
|
193
346
|
* `moda skills <subcommand>` dispatcher.
|
|
194
347
|
*/
|
|
@@ -218,8 +371,13 @@ export async function runSkillsCommand(args) {
|
|
|
218
371
|
}
|
|
219
372
|
return Number(result.sessions_failed ?? 0) > 0 ? 1 : 0;
|
|
220
373
|
}
|
|
374
|
+
if (sub === 'sync') {
|
|
375
|
+
const result = await runSkillSync(args.flags, { profile: args.flags.profile });
|
|
376
|
+
console.log(JSON.stringify({ synced: result.synced, dryRun: result.dryRun }));
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
221
379
|
if (sub !== 'pull') {
|
|
222
|
-
throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : 'A skills subcommand is required', 'Usage: moda skills gen [--source=all|sdk|coding-agent] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills pull [--status=approved|proposed|all]');
|
|
380
|
+
throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : 'A skills subcommand is required', 'Usage: moda skills gen [--source=all|sdk|coding-agent] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]');
|
|
223
381
|
}
|
|
224
382
|
const status = (args.flags.status ?? 'approved').toLowerCase();
|
|
225
383
|
if (!VALID_STATUSES.has(status)) {
|
|
@@ -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
|