@moda-ai/cli 1.17.2 → 1.18.1
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/analyst-tui.js +1 -1
- package/dist/cli.js +4 -0
- package/dist/harness-remote.d.ts +102 -0
- package/dist/harness-remote.js +314 -52
- package/dist/harness.d.ts +35 -0
- package/dist/harness.js +140 -22
- package/dist/init/index.js +469 -185
- package/dist/init/progress-board.d.ts +80 -0
- package/dist/init/progress-board.js +225 -0
- package/dist/init/sdk-integration.d.ts +9 -1
- package/dist/init/sdk-integration.js +12 -2
- package/dist/manifest.js +1 -0
- package/dist/prompts-ab.d.ts +53 -0
- package/dist/prompts-ab.js +80 -10
- package/dist/prompts-propose.d.ts +8 -3
- package/dist/prompts-propose.js +145 -9
- package/dist/schemas.d.ts +2 -2
- package/dist/tui.d.ts +12 -4
- package/dist/tui.js +24 -17
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
package/dist/init/index.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
// src/init/index.ts
|
|
1
|
+
// src/init/index.ts. Full moda init wizard orchestrator.
|
|
2
|
+
//
|
|
3
|
+
// Five visible steps: Sign in, Workspace, Plan, Set up, Verify. Every prompt
|
|
4
|
+
// lives in Step 3 (Plan); Step 4 then runs unattended, firing the hosted
|
|
5
|
+
// harness analysis first and overlapping it with the fast local tasks and the
|
|
6
|
+
// SDK integration agent while a live progress board (progress-board.ts)
|
|
7
|
+
// renders each lane.
|
|
2
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
9
|
import { dirname, join } from 'node:path';
|
|
4
|
-
import { confirm, select } from '@inquirer/prompts';
|
|
10
|
+
import { checkbox, confirm, select } from '@inquirer/prompts';
|
|
5
11
|
import { login, loadToken } from '../auth.js';
|
|
6
12
|
import { selectTenantAndCreateKey } from './tenant.js';
|
|
7
13
|
import { detectAgents } from './mcp.js';
|
|
@@ -10,12 +16,18 @@ import { initPrompts, runPromptSync } from '../prompts.js';
|
|
|
10
16
|
import { runSkillSync } from '../skills.js';
|
|
11
17
|
import { SDK_GUIDE_PATH, writeSdkIntegrationGuide } from './sdk-guide.js';
|
|
12
18
|
import { runSdkIntegration } from './sdk-integration.js';
|
|
19
|
+
import { InitProgressBoard } from './progress-board.js';
|
|
13
20
|
import { createCommandContext } from '../command-context.js';
|
|
14
|
-
import { VERSION } from '../config.js';
|
|
21
|
+
import { resolveModaBaseUrl, VERSION } from '../config.js';
|
|
15
22
|
import { CliAuthError, CliInputError } from '../errors.js';
|
|
16
|
-
import { isDeterministicLocalAnalyst, runHarnessCommand } from '../harness.js';
|
|
23
|
+
import { detectAvailableCodingAgents, isDeterministicLocalAnalyst, runHarnessCommand, } from '../harness.js';
|
|
24
|
+
import { pollRemoteAnalyzeUntilDone, startRemoteAnalyze, } from '../harness-remote.js';
|
|
17
25
|
import { buildDoctorReport } from '../doctor.js';
|
|
18
26
|
import { renderInitBanner, renderInitComplete, renderInitNote, renderInitSetupPlan, renderInitStep, } from '../tui.js';
|
|
27
|
+
/** Recovery command printed whenever init detaches from a hosted analysis run. */
|
|
28
|
+
const HARNESS_PULL_RECOVERY = 'moda harness pull --sync';
|
|
29
|
+
/** Minimum gap between board-driven progress events per lane in machine modes. */
|
|
30
|
+
const LANE_EVENT_THROTTLE_MS = 2_000;
|
|
19
31
|
export async function runInit(flags, options = {}) {
|
|
20
32
|
const explicitYes = flags.yes === true ||
|
|
21
33
|
flags.yes === 'true' ||
|
|
@@ -37,17 +49,29 @@ export async function runInit(flags, options = {}) {
|
|
|
37
49
|
const emit = (phase, message, status) => {
|
|
38
50
|
options.onEvent?.({ phase, message, status });
|
|
39
51
|
};
|
|
52
|
+
// Board-driven lane updates arrive as fast as the underlying stream; machine
|
|
53
|
+
// modes get them as progress events throttled to one per lane per 2s so
|
|
54
|
+
// agent NDJSON stays readable (the human board repaints at 150ms instead).
|
|
55
|
+
const laneEventAt = new Map();
|
|
56
|
+
const emitLaneProgress = (phase, laneId, message) => {
|
|
57
|
+
if (!options.onEvent || human)
|
|
58
|
+
return;
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (now - (laneEventAt.get(laneId) ?? 0) < LANE_EVENT_THROTTLE_MS)
|
|
61
|
+
return;
|
|
62
|
+
laneEventAt.set(laneId, now);
|
|
63
|
+
emit(phase, message);
|
|
64
|
+
};
|
|
40
65
|
log(renderInitBanner());
|
|
41
|
-
log(renderInitStep(1, 7, 'Welcome', 'No environment variable files are written. File changes are listed before completion.'));
|
|
42
66
|
emit('welcome', 'Onboarding this project as a Moda agent harness.', 'completed');
|
|
43
67
|
phases.push({ id: 'welcome', title: 'Welcome', status: 'completed' });
|
|
44
|
-
// Step
|
|
68
|
+
// Step 1: Sign in.
|
|
45
69
|
// Every auth touchpoint below (cached-token check, browser login, tenant
|
|
46
70
|
// setup) must share the same --profile context, or `moda init --profile X`
|
|
47
71
|
// logs in / stores under the default profile while the rest of init reads
|
|
48
|
-
// from X
|
|
72
|
+
// from X, and the session then looks unauthenticated or uses the wrong key.
|
|
49
73
|
const profile = stringFlag(flags.profile);
|
|
50
|
-
log(renderInitStep(
|
|
74
|
+
log(renderInitStep(1, 5, 'Sign in', 'No environment variable files are written. File changes are listed before completion.'));
|
|
51
75
|
let token = loadToken({ profile });
|
|
52
76
|
if (!token) {
|
|
53
77
|
if (json || agent) {
|
|
@@ -76,9 +100,9 @@ export async function runInit(flags, options = {}) {
|
|
|
76
100
|
log(renderInitNote('Already authenticated (cached token)'));
|
|
77
101
|
}
|
|
78
102
|
emit('auth', 'Authentication complete.', 'completed');
|
|
79
|
-
phases.push({ id: 'auth', title: '
|
|
80
|
-
// Step
|
|
81
|
-
log(renderInitStep(
|
|
103
|
+
phases.push({ id: 'auth', title: 'Sign in', status: 'completed' });
|
|
104
|
+
// Step 2: Workspace (tenant + API key).
|
|
105
|
+
log(renderInitStep(2, 5, 'Workspace'));
|
|
82
106
|
emit('workspace', 'Selecting workspace and creating API key.');
|
|
83
107
|
// --tenant-id flag wins over MODA_TENANT_ID; the source drives the
|
|
84
108
|
// env-shadow warning so we don't warn when the env var was the selection.
|
|
@@ -86,7 +110,7 @@ export async function runInit(flags, options = {}) {
|
|
|
86
110
|
// Normalize the env var the same way as the flag: an empty/whitespace
|
|
87
111
|
// MODA_TENANT_ID (e.g. `export MODA_TENANT_ID=`, or a harness that blanks it
|
|
88
112
|
// to clear an inherited value) means "unset", not an explicit empty-id
|
|
89
|
-
// selection
|
|
113
|
+
// selection; otherwise the exact-id match below fails with
|
|
90
114
|
// "No organization with id ''" instead of auto-selecting a sole org.
|
|
91
115
|
const tenantIdEnv = stringFlag(process.env.MODA_TENANT_ID);
|
|
92
116
|
const { apiKey, tenantName } = await selectTenantAndCreateKey({
|
|
@@ -99,211 +123,431 @@ export async function runInit(flags, options = {}) {
|
|
|
99
123
|
});
|
|
100
124
|
log(renderInitNote(`Active workspace: ${tenantName}`));
|
|
101
125
|
emit('workspace', `Active workspace: ${tenantName}`, 'completed');
|
|
102
|
-
phases.push({ id: 'workspace', title: 'Workspace
|
|
103
|
-
// Step
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
126
|
+
phases.push({ id: 'workspace', title: 'Workspace', status: 'completed', details: tenantName });
|
|
127
|
+
// Step 3: Plan. Every remaining decision lives in this single prompt beat;
|
|
128
|
+
// Step 4 then runs unattended with zero prompts. Explicit flags keep
|
|
129
|
+
// overriding exactly as before, and --yes/--json/--agent never prompt
|
|
130
|
+
// (optional items default off, snapshot consent implied).
|
|
131
|
+
log(renderInitStep(3, 5, 'Plan', 'One confirmation covers everything below; setup then runs unattended.'));
|
|
132
|
+
let remote = resolveHarnessRemote(flags);
|
|
133
|
+
const remotePinned = hasExplicitRemoteChoice(flags);
|
|
134
|
+
const sdkFlag = resolveSdkIntegrateFlag(flags);
|
|
135
|
+
const ciCdFlag = resolveCiCdFlag(flags);
|
|
136
|
+
const autosyncFlag = resolveAutosyncFlag(flags);
|
|
137
|
+
// SDK integration installs packages and edits source via a coding agent, so
|
|
138
|
+
// it defaults ON only when an agent CLI is installed to do the work; it
|
|
139
|
+
// never runs unattended without an explicit --sdk-integrate.
|
|
140
|
+
const codingAgentAvailable = detectAvailableCodingAgents().length > 0;
|
|
141
|
+
let integrateSdk = sdkFlag !== undefined ? sdkFlag : yes ? false : codingAgentAvailable;
|
|
142
|
+
let configureCiCd = ciCdFlag ?? false;
|
|
143
|
+
let configureAutosync = autosyncFlag ?? false;
|
|
144
|
+
let setupPlan = [];
|
|
145
|
+
const renderPlan = () => {
|
|
146
|
+
setupPlan = buildSetupPlan({
|
|
147
|
+
installHooks: false,
|
|
148
|
+
integrateSdk,
|
|
149
|
+
configureCiCd,
|
|
150
|
+
configureAutosync,
|
|
151
|
+
remote,
|
|
152
|
+
});
|
|
153
|
+
log(renderInitSetupPlan(setupPlan, { snapshotConsent: remote }));
|
|
154
|
+
};
|
|
155
|
+
renderPlan();
|
|
156
|
+
if (!yes) {
|
|
157
|
+
const proceed = await confirm({ message: 'Proceed with this plan?', default: true });
|
|
158
|
+
if (!proceed) {
|
|
159
|
+
const choices = [];
|
|
160
|
+
if (sdkFlag === undefined) {
|
|
161
|
+
choices.push({
|
|
162
|
+
name: 'SDK integration via your coding agent (installs packages, edits source)',
|
|
163
|
+
value: 'sdk',
|
|
164
|
+
checked: integrateSdk,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (!remotePinned) {
|
|
168
|
+
choices.push({
|
|
169
|
+
name: "Harness analysis on Moda's servers (uncheck to run a local coding agent analyst)",
|
|
170
|
+
value: 'remote',
|
|
171
|
+
checked: remote,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (ciCdFlag === undefined) {
|
|
175
|
+
choices.push({
|
|
176
|
+
name: 'CI/CD validation workflow (.github/workflows/moda-validation.yml)',
|
|
177
|
+
value: 'ci-cd',
|
|
178
|
+
checked: configureCiCd,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (autosyncFlag === undefined) {
|
|
182
|
+
choices.push({
|
|
183
|
+
name: 'Autosync prompts and skills on push to main (.github/workflows/moda-autosync.yml)',
|
|
184
|
+
value: 'autosync',
|
|
185
|
+
checked: configureAutosync,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (choices.length > 0) {
|
|
189
|
+
const selected = await checkbox({
|
|
190
|
+
message: 'Choose what runs (space toggles, enter confirms)',
|
|
191
|
+
choices,
|
|
192
|
+
});
|
|
193
|
+
if (sdkFlag === undefined)
|
|
194
|
+
integrateSdk = selected.includes('sdk');
|
|
195
|
+
if (!remotePinned)
|
|
196
|
+
remote = selected.includes('remote');
|
|
197
|
+
if (ciCdFlag === undefined)
|
|
198
|
+
configureCiCd = selected.includes('ci-cd');
|
|
199
|
+
if (autosyncFlag === undefined)
|
|
200
|
+
configureAutosync = selected.includes('autosync');
|
|
201
|
+
renderPlan();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (json || agent) {
|
|
206
|
+
const skipped = setupPlan
|
|
207
|
+
.filter((action) => (action.category === 'sdk' || action.category === 'ci-cd' || action.category === 'autosync') &&
|
|
208
|
+
action.approved === false)
|
|
209
|
+
.map((action) => `${action.category}: ${action.command ?? 'explicit flag required'}`);
|
|
210
|
+
if (skipped.length > 0) {
|
|
211
|
+
emit('plan', `Non-interactive init skipped optional actions (${skipped.join('; ')}).`, 'warning');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
emit('plan', 'Setup plan ready.', 'completed');
|
|
215
|
+
phases.push({ id: 'plan', title: 'Plan', status: 'completed' });
|
|
216
|
+
// Step 4: Set up. Zero prompts from here on. The hosted harness analysis is
|
|
217
|
+
// fired FIRST and polled concurrently while the fast local tasks and the SDK
|
|
218
|
+
// integration agent run; a live progress board renders each lane in human
|
|
219
|
+
// mode. Local-analyst mode stays sequential (AnalystTui provides its own
|
|
220
|
+
// live progress).
|
|
221
|
+
log(renderInitStep(4, 5, 'Set up', remote
|
|
222
|
+
? 'Harness analysis runs on Moda while local setup continues. No further input is needed.'
|
|
223
|
+
: 'Runs unattended. No further input is needed.'));
|
|
224
|
+
const rootDir = process.cwd();
|
|
225
|
+
// An explicit --remote pin means "never fall back to a local coding agent".
|
|
226
|
+
const allowLocalFallback = !isTruthyFlag(flagValue(flags, 'remote'));
|
|
227
|
+
const nestedHarnessFlags = (extra) => commandFlags({
|
|
228
|
+
summary: true,
|
|
229
|
+
// The Step 3 plan confirm covers the broad local read and (for remote)
|
|
230
|
+
// the snapshot upload, so nested harness commands must never re-prompt.
|
|
231
|
+
yes: true,
|
|
232
|
+
json: json || agent,
|
|
233
|
+
agent,
|
|
234
|
+
quiet: true,
|
|
235
|
+
'no-telemetry': true,
|
|
236
|
+
profile,
|
|
237
|
+
...extra,
|
|
238
|
+
});
|
|
110
239
|
const runAnalyze = (useRemote) => runHarnessCommand(createCommandContext({
|
|
111
240
|
command: 'harness',
|
|
112
241
|
positional: 'analyze',
|
|
113
242
|
positionals: ['analyze'],
|
|
114
|
-
flags:
|
|
115
|
-
summary: true,
|
|
116
|
-
// In machine mode the nested harness scan must not emit its own
|
|
117
|
-
// `needs_input` with a `moda harness analyze --yes` resume command — an
|
|
118
|
-
// orchestrator obeying it would resume the scan and silently skip the
|
|
119
|
-
// rest of init. The broad local read is part of the init flow the caller
|
|
120
|
-
// already opted into, so auto-approve it here; human init still confirms.
|
|
121
|
-
yes: yes || json || agent,
|
|
122
|
-
json: json || agent,
|
|
123
|
-
agent,
|
|
243
|
+
flags: nestedHarnessFlags({
|
|
124
244
|
analyst: stringFlag(flags.analyst) ?? 'auto',
|
|
125
245
|
// Pass the remote/local decision EXPLICITLY. `commandFlags` drops
|
|
126
246
|
// `remote: false`, so a bare `remote: useRemote` would leave the child
|
|
127
247
|
// with no signal and it would re-derive remoteness from
|
|
128
|
-
// `MODA_HARNESS_REMOTE
|
|
129
|
-
// remote-failure fallback
|
|
130
|
-
// and takes precedence over the env in `isRemoteHarnessAnalyze`.
|
|
248
|
+
// `MODA_HARNESS_REMOTE`, silently overriding `--local` and the
|
|
249
|
+
// remote-failure fallback. `--no-remote` survives (it's `true`).
|
|
131
250
|
remote: useRemote ? true : undefined,
|
|
132
251
|
'no-remote': useRemote ? undefined : true,
|
|
133
|
-
quiet: true,
|
|
134
252
|
progress: true,
|
|
135
|
-
'no-telemetry': true,
|
|
136
253
|
}),
|
|
137
254
|
}));
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
255
|
+
let board;
|
|
256
|
+
const startBoard = () => {
|
|
257
|
+
if (!human)
|
|
258
|
+
return undefined;
|
|
259
|
+
board ??= new InitProgressBoard();
|
|
260
|
+
return board;
|
|
261
|
+
};
|
|
262
|
+
const validationFailure = () => new CliInputError('Moda Analyst produced a harness report that did not pass validation.', 'Review .moda/harness-report-validation.json, then rerun `moda init --local --analyst=claude|cursor|codex` or `moda harness analyze --analyst=local-scan --summary`.');
|
|
263
|
+
let harnessPhase = { id: 'harness', title: 'Harness discovery', status: 'completed' };
|
|
264
|
+
let harnessDeferred = false;
|
|
265
|
+
// Upload the cited harness graph so Moda's backend has the understanding,
|
|
266
|
+
// not just local artifacts. `harness sync --from-report` refuses an
|
|
267
|
+
// unapproved report, so approve it first. Best-effort: a network blip on
|
|
268
|
+
// either call must not abort onboarding.
|
|
269
|
+
const approveAndSyncHarness = async () => {
|
|
270
|
+
try {
|
|
271
|
+
await runHarnessCommand(createCommandContext({
|
|
272
|
+
command: 'harness',
|
|
273
|
+
positional: 'approve',
|
|
274
|
+
positionals: ['approve'],
|
|
275
|
+
flags: nestedHarnessFlags({}),
|
|
276
|
+
}));
|
|
277
|
+
await runHarnessCommand(createCommandContext({
|
|
278
|
+
command: 'harness',
|
|
279
|
+
positional: 'sync',
|
|
280
|
+
positionals: ['sync'],
|
|
281
|
+
flags: nestedHarnessFlags({ 'from-report': true }),
|
|
282
|
+
}));
|
|
283
|
+
log(renderInitNote('Synced harness graph to Moda'));
|
|
284
|
+
emit('harness', 'Harness graph synced to Moda.', 'completed');
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
288
|
+
warn(renderInitNote(`Harness sync skipped: ${message}`, 'warning'));
|
|
289
|
+
warn(' Run `moda harness approve --yes && moda harness sync --from-report` later to upload.');
|
|
290
|
+
emit('harness', 'Harness sync skipped.', 'warning');
|
|
291
|
+
harnessPhase = { id: 'harness', title: 'Harness discovery', status: 'warning', details: 'sync skipped' };
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const finishLocalAnalyze = async () => {
|
|
295
|
+
emit('harness', 'Running Moda Analyst harness discovery.');
|
|
296
|
+
const result = await runAnalyze(false);
|
|
297
|
+
if (result.exitCode !== 0)
|
|
298
|
+
throw validationFailure();
|
|
299
|
+
log(renderInitNote('Wrote or verified .moda/harness-report.json, .moda/harness.json, and .moda/HARNESS.md'));
|
|
300
|
+
if (human)
|
|
301
|
+
printHarnessDetectionSummary();
|
|
302
|
+
emit('harness', 'Harness discovery complete.', 'completed');
|
|
303
|
+
await approveAndSyncHarness();
|
|
304
|
+
};
|
|
305
|
+
// Fire the hosted analysis first so it overlaps everything below.
|
|
306
|
+
let pollPromise;
|
|
307
|
+
let pollError;
|
|
308
|
+
let remoteRunId;
|
|
309
|
+
// Covers the whole overlapped window (fast tasks, SDK agent, final await):
|
|
310
|
+
// a Ctrl-C leaves the hosted run resumable and prints the recovery command.
|
|
311
|
+
const onRemoteSigint = () => {
|
|
312
|
+
board?.stop();
|
|
313
|
+
process.stderr.write(`\nHarness analysis keeps running on Moda. Resume anytime with \`${HARNESS_PULL_RECOVERY}\`.\n`);
|
|
314
|
+
process.exit(130);
|
|
315
|
+
};
|
|
316
|
+
if (remote) {
|
|
317
|
+
emit('harness', 'Running Moda-hosted harness discovery.');
|
|
318
|
+
try {
|
|
319
|
+
const harnessContext = createCommandContext({
|
|
320
|
+
command: 'harness',
|
|
321
|
+
positional: 'analyze',
|
|
322
|
+
positionals: ['analyze'],
|
|
323
|
+
flags: nestedHarnessFlags({
|
|
324
|
+
analyst: stringFlag(flags.analyst) ?? 'auto',
|
|
325
|
+
remote: true,
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
const started = await startRemoteAnalyze({ context: harnessContext, rootDir });
|
|
329
|
+
remoteRunId = started.runId;
|
|
330
|
+
// From this point Ctrl-C is safe and resumable: the state file already
|
|
331
|
+
// says `running`, so exiting early just detaches from the hosted run.
|
|
332
|
+
process.once('SIGINT', onRemoteSigint);
|
|
333
|
+
emit('harness', `Moda accepted harness analysis run ${started.runId}.`);
|
|
334
|
+
startBoard()?.startLane('harness', 'Harness analysis on Moda', {
|
|
335
|
+
detail: 'waiting for the hosted analyst',
|
|
336
|
+
expectation: 'usually 2-4 min',
|
|
337
|
+
});
|
|
338
|
+
pollPromise = pollRemoteAnalyzeUntilDone({
|
|
339
|
+
context: harnessContext,
|
|
340
|
+
runId: started.runId,
|
|
341
|
+
onProgress: ({ status, progress }) => {
|
|
342
|
+
if (status === 'completed' || status === 'failed') {
|
|
343
|
+
// The hosted run just turned terminal, but the lane is only
|
|
344
|
+
// finalized after the SDK lane below. Freeze it now so the board
|
|
345
|
+
// does not keep a live spinner and climbing elapsed for minutes
|
|
346
|
+
// after the analysis actually finished; the post-setup pull
|
|
347
|
+
// refines the note (the board allows terminal refinements).
|
|
348
|
+
if (status === 'completed') {
|
|
349
|
+
board?.complete('harness', 'analysis finished on Moda; finalizing after setup');
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
board?.warn('harness', 'hosted run finished with issues; finalizing after setup');
|
|
353
|
+
}
|
|
354
|
+
emitLaneProgress('harness', 'harness', `Hosted analysis ${status}; finalizing after setup.`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const message = progress?.message
|
|
358
|
+
?? (typeof progress?.tool_calls === 'number'
|
|
359
|
+
? `${progress.tool_calls} tool call(s) so far`
|
|
360
|
+
: `analysis ${status}`);
|
|
361
|
+
board?.update('harness', message);
|
|
362
|
+
emitLaneProgress('harness', 'harness', message);
|
|
363
|
+
},
|
|
364
|
+
}).catch((error) => {
|
|
365
|
+
// Captured, not thrown: an unhandled rejection here would crash init
|
|
366
|
+
// in the middle of the fast tasks. Inspected after the SDK lane.
|
|
367
|
+
pollError = error;
|
|
368
|
+
return undefined;
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
// A remote failure (service down, upload error) must not sink
|
|
373
|
+
// onboarding; fall back to the local analyst path unless --remote was
|
|
374
|
+
// pinned explicitly.
|
|
375
|
+
if (!allowLocalFallback)
|
|
376
|
+
throw error;
|
|
153
377
|
warn(renderInitNote(`Server-side harness discovery unavailable (${error instanceof Error ? error.message : String(error)}); falling back to local analyst.`, 'warning'));
|
|
154
378
|
emit('harness', 'Server-side discovery failed; falling back to local analyst.', 'warning');
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
throw error;
|
|
379
|
+
remote = false;
|
|
159
380
|
}
|
|
160
381
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (human)
|
|
166
|
-
printHarnessDetectionSummary();
|
|
167
|
-
emit('harness', 'Harness discovery complete.', 'completed');
|
|
168
|
-
// Upload the cited harness graph so Moda's backend has the understanding, not
|
|
169
|
-
// just local artifacts. `harness sync --from-report` refuses an unapproved
|
|
170
|
-
// report, so approve it first. Best-effort: a network blip on either call must
|
|
171
|
-
// not abort onboarding — auth + key provisioning already succeeded.
|
|
172
|
-
try {
|
|
173
|
-
await runHarnessCommand(createCommandContext({
|
|
174
|
-
command: 'harness',
|
|
175
|
-
positional: 'approve',
|
|
176
|
-
positionals: ['approve'],
|
|
177
|
-
flags: commandFlags({ yes: true, json: json || agent, agent, quiet: true, 'no-telemetry': true }),
|
|
178
|
-
}));
|
|
179
|
-
await runHarnessCommand(createCommandContext({
|
|
180
|
-
command: 'harness',
|
|
181
|
-
positional: 'sync',
|
|
182
|
-
positionals: ['sync'],
|
|
183
|
-
flags: commandFlags({ 'from-report': true, yes: true, json: json || agent, agent, quiet: true, 'no-telemetry': true }),
|
|
184
|
-
}));
|
|
185
|
-
log(renderInitNote('Synced harness graph to Moda'));
|
|
186
|
-
emit('harness', 'Harness graph synced to Moda.', 'completed');
|
|
187
|
-
}
|
|
188
|
-
catch (err) {
|
|
189
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
190
|
-
warn(renderInitNote(`Harness sync skipped: ${message}`, 'warning'));
|
|
191
|
-
warn(' Run `moda harness approve --yes && moda harness sync --from-report` later to upload.');
|
|
192
|
-
emit('harness', 'Harness sync skipped.', 'warning');
|
|
193
|
-
}
|
|
194
|
-
phases.push({ id: 'harness', title: 'Harness Discovery', status: 'completed' });
|
|
195
|
-
// Step 5: Setup plan
|
|
196
|
-
log(renderInitStep(5, 7, 'Setup Plan', 'Every write/sync action is explicit before it is applied.'));
|
|
197
|
-
const setupPlan = buildSetupPlan({
|
|
198
|
-
installHooks: false,
|
|
199
|
-
integrateSdk: await shouldIntegrateSdk(flags, yes),
|
|
200
|
-
configureCiCd: await shouldConfigureCiCd(flags, yes),
|
|
201
|
-
configureAutosync: await shouldConfigureAutosync(flags, yes),
|
|
202
|
-
});
|
|
203
|
-
if (json || agent) {
|
|
204
|
-
const skipped = setupPlan
|
|
205
|
-
.filter((action) => (action.category === 'sdk' || action.category === 'ci-cd' || action.category === 'autosync') &&
|
|
206
|
-
action.approved === false)
|
|
207
|
-
.map((action) => `${action.category}: ${action.command ?? 'explicit flag required'}`);
|
|
208
|
-
if (skipped.length > 0) {
|
|
209
|
-
emit('plan', `Non-interactive init skipped optional actions (${skipped.join('; ')}).`, 'warning');
|
|
210
|
-
}
|
|
382
|
+
// Local-analyst mode keeps today's sequential behavior: analyze (AnalystTui
|
|
383
|
+
// renders live progress), then approve + sync, then the fast tasks below.
|
|
384
|
+
if (!remote) {
|
|
385
|
+
await finishLocalAnalyze();
|
|
211
386
|
}
|
|
212
|
-
log(renderInitSetupPlan(setupPlan));
|
|
213
|
-
emit('plan', 'Setup plan ready.', 'completed');
|
|
214
|
-
phases.push({ id: 'plan', title: 'Setup Plan', status: 'completed' });
|
|
215
|
-
// Step 6: Apply approved setup changes.
|
|
216
|
-
log(renderInitStep(6, 7, 'Apply Approved Changes'));
|
|
217
387
|
emit('apply', 'Applying approved setup changes.');
|
|
388
|
+
const laneBoard = startBoard();
|
|
218
389
|
const ingestCodingAgentTraces = false;
|
|
219
390
|
const agents = detectAgents();
|
|
391
|
+
laneBoard?.startLane('agent-skills', 'Agent skills');
|
|
220
392
|
if (agents.length > 0) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// persistent skill.
|
|
225
|
-
await installRules(agents, { yes, silent: !human });
|
|
393
|
+
// The Step 3 plan already covered the skill install; never re-prompt.
|
|
394
|
+
await installRules(agents, { yes: true, silent: true });
|
|
395
|
+
laneBoard?.complete('agent-skills', `moda-cli skill installed for ${agents.join(', ')}`);
|
|
226
396
|
}
|
|
227
397
|
else {
|
|
228
|
-
|
|
398
|
+
laneBoard?.complete('agent-skills', 'no coding agents detected');
|
|
229
399
|
}
|
|
230
400
|
await initPrompts({ silent: true });
|
|
231
|
-
|
|
401
|
+
laneBoard?.startLane('prompts', 'Prompts', { detail: 'Created or verified .moda/prompts.yml' });
|
|
232
402
|
// Push the manifest's prompt files to Moda so production traces can attribute
|
|
233
|
-
// managed prompt versions. A fresh repo has none yet
|
|
403
|
+
// managed prompt versions. A fresh repo has none yet; that's a harmless no-op.
|
|
234
404
|
try {
|
|
235
|
-
const promptSync = await runPromptSync({}, { profile
|
|
236
|
-
|
|
237
|
-
?
|
|
238
|
-
: '
|
|
405
|
+
const promptSync = await runPromptSync({}, { profile });
|
|
406
|
+
laneBoard?.complete('prompts', promptSync.synced.length > 0
|
|
407
|
+
? `${promptSync.synced.length} prompt(s) synced`
|
|
408
|
+
: 'manifest ready (no prompt files to sync yet)');
|
|
239
409
|
}
|
|
240
410
|
catch (err) {
|
|
241
411
|
const message = err instanceof Error ? err.message : String(err);
|
|
242
|
-
warn(
|
|
412
|
+
laneBoard?.warn('prompts', `sync skipped: ${message}`);
|
|
243
413
|
}
|
|
244
414
|
// Push locally-authored skills (`.claude/skills/**/SKILL.md`) to Moda, in
|
|
245
|
-
// parallel intent with prompt sync.
|
|
246
|
-
//
|
|
415
|
+
// parallel intent with prompt sync. Best-effort: a network blip must not
|
|
416
|
+
// abort onboarding.
|
|
417
|
+
laneBoard?.startLane('skills', 'Skills');
|
|
247
418
|
try {
|
|
248
|
-
const skillSync = await runSkillSync({}, { profile
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
else {
|
|
253
|
-
log(renderInitNote('No local skills to sync yet (add .claude/skills/<name>/SKILL.md)'));
|
|
254
|
-
}
|
|
419
|
+
const skillSync = await runSkillSync({}, { profile });
|
|
420
|
+
laneBoard?.complete('skills', skillSync.synced.length > 0
|
|
421
|
+
? `${skillSync.synced.length} skill(s) synced`
|
|
422
|
+
: 'no local skills to sync yet');
|
|
255
423
|
}
|
|
256
424
|
catch (err) {
|
|
257
425
|
const message = err instanceof Error ? err.message : String(err);
|
|
258
|
-
warn(
|
|
426
|
+
laneBoard?.warn('skills', `sync skipped: ${message}`);
|
|
427
|
+
}
|
|
428
|
+
if (setupPlan.find((action) => action.category === 'ci-cd')?.approved === true) {
|
|
429
|
+
const workflowPath = writeCiCdWorkflow();
|
|
430
|
+
laneBoard?.startLane('ci-cd', 'CI/CD workflow');
|
|
431
|
+
laneBoard?.complete('ci-cd', `wrote ${workflowPath}`);
|
|
432
|
+
markApplied(setupPlan, 'ci-cd');
|
|
433
|
+
}
|
|
434
|
+
if (setupPlan.find((action) => action.category === 'autosync')?.approved === true) {
|
|
435
|
+
const workflowPath = writeAutosyncWorkflow();
|
|
436
|
+
laneBoard?.startLane('autosync', 'Autosync workflow');
|
|
437
|
+
laneBoard?.complete('autosync', `wrote ${workflowPath}`);
|
|
438
|
+
markApplied(setupPlan, 'autosync');
|
|
259
439
|
}
|
|
260
440
|
const sdkAction = setupPlan.find((action) => action.category === 'sdk');
|
|
261
441
|
if (sdkAction?.approved === true) {
|
|
262
|
-
|
|
263
|
-
|
|
442
|
+
laneBoard?.startLane('sdk', 'SDK integration', {
|
|
443
|
+
detail: 'launching your coding agent',
|
|
444
|
+
expectation: 'usually 3-10 min',
|
|
445
|
+
});
|
|
446
|
+
emit('apply', 'Running your coding agent to integrate the Moda SDK.');
|
|
447
|
+
const result = await runSdkIntegration({
|
|
448
|
+
onProgress: (message) => {
|
|
449
|
+
laneBoard?.update('sdk', message);
|
|
450
|
+
emitLaneProgress('apply', 'sdk', message);
|
|
451
|
+
},
|
|
452
|
+
// Presence of onEvent selects the streaming agent invocation so tool
|
|
453
|
+
// cards arrive incrementally; onProgress above receives the rendered
|
|
454
|
+
// cards (describeCodingAgentEvent), which is all the lane needs.
|
|
455
|
+
onEvent: () => { },
|
|
456
|
+
});
|
|
264
457
|
if (result.status === 'integrated') {
|
|
265
|
-
|
|
458
|
+
laneBoard?.complete('sdk', `integrated with ${result.adapter}; review the diff before committing`);
|
|
266
459
|
markApplied(setupPlan, 'sdk');
|
|
267
460
|
}
|
|
268
461
|
else if (result.status === 'no-agent') {
|
|
269
462
|
const guidePath = writeSdkIntegrationGuide();
|
|
270
463
|
sdkAction.files = [guidePath];
|
|
271
|
-
|
|
464
|
+
laneBoard?.warn('sdk', `no coding agent CLI found; wrote ${guidePath} to integrate by hand`);
|
|
272
465
|
markApplied(setupPlan, 'sdk');
|
|
273
466
|
}
|
|
274
467
|
else {
|
|
275
468
|
const guidePath = writeSdkIntegrationGuide();
|
|
276
469
|
sdkAction.files = [guidePath];
|
|
277
|
-
warn(
|
|
278
|
-
log(renderInitNote(`Wrote ${guidePath} so you can finish the integration manually`));
|
|
470
|
+
laneBoard?.warn('sdk', `${result.adapter} did not complete; wrote ${guidePath} to finish manually`);
|
|
279
471
|
markApplied(setupPlan, 'sdk');
|
|
280
472
|
}
|
|
281
473
|
}
|
|
282
474
|
else {
|
|
283
|
-
log(renderInitNote('Skipped SDK integration', 'skipped'));
|
|
284
475
|
markApplied(setupPlan, 'sdk', 'User skipped SDK integration.');
|
|
285
476
|
}
|
|
286
|
-
const configureCiCd = setupPlan.find((action) => action.category === 'ci-cd')?.approved === true;
|
|
287
|
-
if (configureCiCd) {
|
|
288
|
-
const workflowPath = writeCiCdWorkflow();
|
|
289
|
-
log(renderInitNote(`Wrote ${workflowPath}`));
|
|
290
|
-
markApplied(setupPlan, 'ci-cd');
|
|
291
|
-
}
|
|
292
|
-
const configureAutosync = setupPlan.find((action) => action.category === 'autosync')?.approved === true;
|
|
293
|
-
if (configureAutosync) {
|
|
294
|
-
const workflowPath = writeAutosyncWorkflow();
|
|
295
|
-
log(renderInitNote(`Wrote ${workflowPath}`));
|
|
296
|
-
markApplied(setupPlan, 'autosync');
|
|
297
|
-
}
|
|
298
|
-
log(renderInitNote('Reusing approved Step 4 harness report; no second analyst run needed'));
|
|
299
|
-
markApplied(setupPlan, 'harness');
|
|
300
477
|
markApplied(setupPlan, 'hooks', ingestCodingAgentTraces ? undefined : 'Coding-agent trace capture is temporarily unavailable.');
|
|
301
478
|
markApplied(setupPlan, 'prompts');
|
|
302
479
|
markApplied(setupPlan, 'skills');
|
|
480
|
+
// Await the hosted analysis last: it has been polling concurrently since the
|
|
481
|
+
// top of this step. Ctrl-C or a poll deadline leaves the run resumable
|
|
482
|
+
// (state file stays `running`) and prints the pull recovery command.
|
|
483
|
+
if (remote && pollPromise && remoteRunId) {
|
|
484
|
+
const final = await pollPromise;
|
|
485
|
+
process.removeListener('SIGINT', onRemoteSigint);
|
|
486
|
+
const fallBackToLocal = async (error) => {
|
|
487
|
+
board?.fail('harness', 'hosted analysis failed');
|
|
488
|
+
board?.stop();
|
|
489
|
+
if (!allowLocalFallback)
|
|
490
|
+
throw error;
|
|
491
|
+
warn(renderInitNote(`Server-side harness discovery failed (${error instanceof Error ? error.message : String(error)}); falling back to local analyst.`, 'warning'));
|
|
492
|
+
emit('harness', 'Server-side discovery failed; falling back to local analyst.', 'warning');
|
|
493
|
+
await finishLocalAnalyze();
|
|
494
|
+
};
|
|
495
|
+
if (pollError !== undefined) {
|
|
496
|
+
await fallBackToLocal(pollError);
|
|
497
|
+
}
|
|
498
|
+
else if (!final || (final.status !== 'completed' && final.status !== 'failed')) {
|
|
499
|
+
// The poll deadline passed while the hosted run kept going. Expected on
|
|
500
|
+
// very large repos, and safe: the run state file stays `running`.
|
|
501
|
+
harnessDeferred = true;
|
|
502
|
+
harnessPhase = { id: 'harness', title: 'Harness discovery', status: 'warning', details: 'still running on Moda' };
|
|
503
|
+
markApplied(setupPlan, 'harness', `Analysis still running on Moda; run \`${HARNESS_PULL_RECOVERY}\` to fetch and sync it.`);
|
|
504
|
+
board?.warn('harness', `still running on Moda; run \`${HARNESS_PULL_RECOVERY}\` in a few minutes`);
|
|
505
|
+
board?.stop();
|
|
506
|
+
log(renderInitNote(`Harness analysis is still running on Moda. Run \`${HARNESS_PULL_RECOVERY}\` in a few minutes to fetch and sync the result.`, 'info'));
|
|
507
|
+
emit('harness', `Harness analysis still running on Moda; resume with \`${HARNESS_PULL_RECOVERY}\`.`, 'warning');
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
board?.update('harness', 'writing local artifacts');
|
|
511
|
+
let pullResult;
|
|
512
|
+
try {
|
|
513
|
+
// `harness pull` finalizes exactly like `harness analyze --remote`
|
|
514
|
+
// (repair, local re-validation, .moda artifact writes) from the
|
|
515
|
+
// already-terminal hosted run.
|
|
516
|
+
pullResult = await runHarnessCommand(createCommandContext({
|
|
517
|
+
command: 'harness',
|
|
518
|
+
positional: 'pull',
|
|
519
|
+
positionals: ['pull'],
|
|
520
|
+
flags: nestedHarnessFlags({ 'run-id': remoteRunId }),
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
await fallBackToLocal(error);
|
|
525
|
+
}
|
|
526
|
+
if (pullResult) {
|
|
527
|
+
if (pullResult.exitCode !== 0) {
|
|
528
|
+
board?.fail('harness', 'report validation failed');
|
|
529
|
+
board?.stop();
|
|
530
|
+
throw validationFailure();
|
|
531
|
+
}
|
|
532
|
+
board?.complete('harness', 'report written to .moda/');
|
|
533
|
+
board?.stop();
|
|
534
|
+
log(renderInitNote('Wrote or verified .moda/harness-report.json, .moda/harness.json, and .moda/HARNESS.md'));
|
|
535
|
+
if (human)
|
|
536
|
+
printHarnessDetectionSummary();
|
|
537
|
+
emit('harness', 'Harness discovery complete.', 'completed');
|
|
538
|
+
await approveAndSyncHarness();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
board?.stop();
|
|
543
|
+
if (!harnessDeferred)
|
|
544
|
+
markApplied(setupPlan, 'harness');
|
|
545
|
+
phases.push(harnessPhase);
|
|
303
546
|
emit('apply', 'Approved changes applied.', 'completed');
|
|
304
|
-
phases.push({ id: 'apply', title: '
|
|
547
|
+
phases.push({ id: 'apply', title: 'Set up', status: 'completed' });
|
|
548
|
+
// Step 5: Verify.
|
|
305
549
|
const offlineValidation = isTruthyFlag(flagValue(flags, 'offline'));
|
|
306
|
-
log(renderInitStep(
|
|
550
|
+
log(renderInitStep(5, 5, 'Verify', offlineValidation
|
|
307
551
|
? 'Running local doctor checks so the setup leaves a clear state.'
|
|
308
552
|
: 'Running Moda doctor online to confirm setup and that telemetry is reaching Moda.'));
|
|
309
553
|
emit('validate', `Running Moda doctor diagnostics (${offlineValidation ? 'offline' : 'online'}).`);
|
|
@@ -315,20 +559,33 @@ export async function runInit(flags, options = {}) {
|
|
|
315
559
|
}), { offline: offlineValidation });
|
|
316
560
|
markApplied(setupPlan, 'validation');
|
|
317
561
|
log(renderInitNote(`Validation checks completed: ${doctorReport.summary.pass} passing, ${doctorReport.summary.warning} warning, ${doctorReport.summary.fail} failing`));
|
|
562
|
+
let telemetrySeen;
|
|
318
563
|
if (!offlineValidation) {
|
|
319
564
|
const cloud = doctorReport.checks.find((check) => check.id === 'cloud_data_flow');
|
|
320
565
|
if (cloud) {
|
|
321
|
-
|
|
566
|
+
telemetrySeen = cloud.status === 'pass';
|
|
567
|
+
if (telemetrySeen) {
|
|
568
|
+
log(renderInitNote('Moda Cloud telemetry: flowing'));
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
// Expected before the app has run with the SDK installed; surfaced as
|
|
572
|
+
// a next step on the completion screen, not a warning badge.
|
|
573
|
+
log(renderInitNote('No telemetry yet. Expected until your app runs with the SDK; see next steps.', 'info'));
|
|
574
|
+
}
|
|
322
575
|
}
|
|
323
576
|
}
|
|
324
577
|
emit('validate', 'Validation checks complete.', doctorReport.summary.fail > 0 ? 'warning' : 'completed');
|
|
325
|
-
phases.push({ id: 'validate', title: '
|
|
326
|
-
// Done
|
|
578
|
+
phases.push({ id: 'validate', title: 'Verify', status: 'completed' });
|
|
579
|
+
// Done: end on the payoff.
|
|
327
580
|
log(renderInitComplete({
|
|
328
581
|
tenantName,
|
|
582
|
+
dashboardUrl: `${resolveModaBaseUrl({ profile })}/dashboard`,
|
|
583
|
+
harness: harnessDeferred
|
|
584
|
+
? { state: 'pending' }
|
|
585
|
+
: { state: 'mapped', ...readHarnessGraphCounts() },
|
|
329
586
|
filesChanged: changedFiles(setupPlan),
|
|
330
587
|
skipped: setupPlan.filter((item) => item.status === 'skipped' || item.status === 'manual'),
|
|
331
|
-
|
|
588
|
+
telemetrySeen,
|
|
332
589
|
}));
|
|
333
590
|
return { apiKey, tenantName, phases, setupPlan };
|
|
334
591
|
}
|
|
@@ -336,7 +593,9 @@ function buildSetupPlan(options) {
|
|
|
336
593
|
return [
|
|
337
594
|
{
|
|
338
595
|
category: 'harness',
|
|
339
|
-
title:
|
|
596
|
+
title: options.remote
|
|
597
|
+
? "Analyze the harness on Moda's servers, write the cited report, and sync the graph to Moda"
|
|
598
|
+
: 'Analyze the harness locally, write the cited report, and sync the graph to Moda',
|
|
340
599
|
status: 'planned',
|
|
341
600
|
approved: true,
|
|
342
601
|
files: [
|
|
@@ -346,7 +605,9 @@ function buildSetupPlan(options) {
|
|
|
346
605
|
'.moda/harness.json',
|
|
347
606
|
'.moda/HARNESS.md',
|
|
348
607
|
],
|
|
349
|
-
command:
|
|
608
|
+
command: options.remote
|
|
609
|
+
? 'moda harness analyze --remote --summary && moda harness approve --yes && moda harness sync --from-report'
|
|
610
|
+
: 'moda harness analyze --analyst=auto --summary && moda harness approve --yes && moda harness sync --from-report',
|
|
350
611
|
},
|
|
351
612
|
{
|
|
352
613
|
category: 'hooks',
|
|
@@ -436,7 +697,7 @@ function changedFiles(setupPlan) {
|
|
|
436
697
|
// The CLI version generated workflows pin to. Pinning (rather than `@latest`,
|
|
437
698
|
// which resolves at CI run time) keeps a future compromised `@moda-ai/cli`
|
|
438
699
|
// publish from silently executing in adopters' CI with their MODA_API_KEY /
|
|
439
|
-
// MODA_TENANT_ID present
|
|
700
|
+
// MODA_TENANT_ID present. Especially important for autosync, which *writes* to
|
|
440
701
|
// the Moda registry. The pin tracks the CLI that generated the file, so each
|
|
441
702
|
// `moda init` upgrade is an explicit, reviewable bump of the workflow.
|
|
442
703
|
const GENERATED_WORKFLOW_CLI = `@moda-ai/cli@${VERSION}`;
|
|
@@ -541,6 +802,22 @@ function printHarnessDetectionSummary(cwd = process.cwd()) {
|
|
|
541
802
|
console.log(' Detected harness summary is available in .moda/HARNESS.md');
|
|
542
803
|
}
|
|
543
804
|
}
|
|
805
|
+
/** Agent/artifact counts for the completion screen's harness payoff line. */
|
|
806
|
+
function readHarnessGraphCounts(cwd = process.cwd()) {
|
|
807
|
+
const manifestPath = join(cwd, '.moda', 'harness.json');
|
|
808
|
+
if (!existsSync(manifestPath))
|
|
809
|
+
return {};
|
|
810
|
+
try {
|
|
811
|
+
const graph = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
812
|
+
return {
|
|
813
|
+
agents: Array.isArray(graph.runtimeAgents) ? graph.runtimeAgents.length : 0,
|
|
814
|
+
artifacts: Array.isArray(graph.artifacts) ? graph.artifacts.length : 0,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
return {};
|
|
819
|
+
}
|
|
820
|
+
}
|
|
544
821
|
function commandFlags(flags) {
|
|
545
822
|
return Object.fromEntries(Object.entries(flags)
|
|
546
823
|
.filter(([, value]) => value !== undefined && value !== false)
|
|
@@ -579,47 +856,41 @@ function resumeFlagsForInit(flags) {
|
|
|
579
856
|
}
|
|
580
857
|
return out;
|
|
581
858
|
}
|
|
582
|
-
|
|
859
|
+
/**
|
|
860
|
+
* Explicit CI/CD decision from flags, or undefined when the Plan beat decides
|
|
861
|
+
* (interactive default off; --yes/--json/--agent default off).
|
|
862
|
+
*/
|
|
863
|
+
function resolveCiCdFlag(flags) {
|
|
583
864
|
const explicit = parseOptionalBoolean(flagValue(flags, 'ci-cd', 'ci_cd', 'ci'));
|
|
584
865
|
if (explicit !== undefined)
|
|
585
866
|
return explicit;
|
|
586
867
|
if (isTruthyFlag(flagValue(flags, 'no-ci-cd', 'no_ci_cd')))
|
|
587
868
|
return false;
|
|
588
|
-
|
|
589
|
-
return false;
|
|
590
|
-
return confirm({
|
|
591
|
-
message: 'Create a GitHub Actions workflow for Moda validation?',
|
|
592
|
-
default: false,
|
|
593
|
-
});
|
|
869
|
+
return undefined;
|
|
594
870
|
}
|
|
595
|
-
|
|
871
|
+
/** Explicit autosync decision from flags, or undefined when the Plan beat decides. */
|
|
872
|
+
function resolveAutosyncFlag(flags) {
|
|
596
873
|
const explicit = parseOptionalBoolean(flagValue(flags, 'autosync', 'auto-sync', 'auto_sync'));
|
|
597
874
|
if (explicit !== undefined)
|
|
598
875
|
return explicit;
|
|
599
876
|
if (isTruthyFlag(flagValue(flags, 'no-autosync', 'no_autosync', 'no-auto-sync', 'no_auto_sync')))
|
|
600
877
|
return false;
|
|
601
|
-
|
|
602
|
-
return false;
|
|
603
|
-
return confirm({
|
|
604
|
-
message: 'Create a GitHub Actions workflow to autosync prompts and skills to Moda on push to main?',
|
|
605
|
-
default: false,
|
|
606
|
-
});
|
|
878
|
+
return undefined;
|
|
607
879
|
}
|
|
608
|
-
|
|
880
|
+
/**
|
|
881
|
+
* Explicit SDK-integration decision from flags, or undefined when the Plan
|
|
882
|
+
* beat decides (interactive default: on when a coding-agent CLI is installed;
|
|
883
|
+
* --yes/--json/--agent default off because the agent edits source and installs
|
|
884
|
+
* packages, which must never happen unattended without an explicit opt-in).
|
|
885
|
+
*/
|
|
886
|
+
function resolveSdkIntegrateFlag(flags) {
|
|
609
887
|
const explicit = parseOptionalBoolean(flagValue(flags, 'sdk-integrate', 'sdk_integrate', 'sdk-guide', 'sdk_guide', 'sdk'));
|
|
610
888
|
if (explicit !== undefined)
|
|
611
889
|
return explicit;
|
|
612
890
|
if (isTruthyFlag(flagValue(flags, 'no-sdk-integrate', 'no_sdk_integrate', 'no-sdk-guide', 'no_sdk_guide', 'no-sdk', 'no_sdk'))) {
|
|
613
891
|
return false;
|
|
614
892
|
}
|
|
615
|
-
|
|
616
|
-
// Never do that unattended (--yes/--json/--agent); require an explicit opt-in.
|
|
617
|
-
if (yes)
|
|
618
|
-
return false;
|
|
619
|
-
return confirm({
|
|
620
|
-
message: 'Run your coding agent now to install the Moda SDK and instrument your LLM calls? (Edits source, installs packages.)',
|
|
621
|
-
default: true,
|
|
622
|
-
});
|
|
893
|
+
return undefined;
|
|
623
894
|
}
|
|
624
895
|
function parseOptionalBoolean(value) {
|
|
625
896
|
if (value === undefined)
|
|
@@ -661,6 +932,19 @@ export function resolveHarnessRemote(flags) {
|
|
|
661
932
|
}
|
|
662
933
|
return true;
|
|
663
934
|
}
|
|
935
|
+
/**
|
|
936
|
+
* True when the user pinned the remote/local decision (flags or a
|
|
937
|
+
* deterministic analyst), so the Plan checkbox must not offer to re-toggle it.
|
|
938
|
+
*/
|
|
939
|
+
function hasExplicitRemoteChoice(flags) {
|
|
940
|
+
if (isTruthyFlag(flagValue(flags, 'local')))
|
|
941
|
+
return true;
|
|
942
|
+
if (isTruthyFlag(flagValue(flags, 'no-remote', 'no_remote')))
|
|
943
|
+
return true;
|
|
944
|
+
if (parseOptionalBoolean(flagValue(flags, 'remote')) !== undefined)
|
|
945
|
+
return true;
|
|
946
|
+
return isDeterministicLocalAnalyst(stringFlag(flagValue(flags, 'analyst')));
|
|
947
|
+
}
|
|
664
948
|
function flagValue(flags, ...keys) {
|
|
665
949
|
for (const key of keys) {
|
|
666
950
|
if (flags[key] !== undefined)
|