@moda-ai/cli 1.13.7 → 1.13.8
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/auth.d.ts +16 -0
- package/dist/auth.js +27 -4
- package/dist/doctor.js +78 -1
- package/dist/harness-artifact-bodies.d.ts +7 -0
- package/dist/harness-artifact-bodies.js +78 -11
- package/dist/harness.d.ts +6 -0
- package/dist/harness.js +28 -12
- package/dist/init/index.d.ts +3 -2
- package/dist/init/index.js +11 -3
- package/dist/init/tenant.js +2 -1
- package/dist/manifest.js +1 -0
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
package/dist/auth.d.ts
CHANGED
|
@@ -2,6 +2,17 @@ import { type ProfileIOOptions } from './profiles.js';
|
|
|
2
2
|
import { type AuthSessionOptions } from './auth-storage.js';
|
|
3
3
|
export { clearAuthSession, isAuthSessionValid, loadAuthSession, loadToken, saveAuthSession, type AuthSessionOptions, type StoredAuth, type StoredAuthTokenKind, } from './auth-storage.js';
|
|
4
4
|
export { revokeCliSessionToken } from './cli-session.js';
|
|
5
|
+
/** Default wait for browser Auth0/consent during `moda auth login` / init. */
|
|
6
|
+
export declare const DEFAULT_AUTH_TIMEOUT_MS = 600000;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the browser-auth wait. Explicit LoginOptions.timeoutMs wins;
|
|
9
|
+
* otherwise MODA_AUTH_TIMEOUT_MS (clamped); otherwise a 10-minute default that
|
|
10
|
+
* covers real Auth0 + MFA + slow consent without abandoning mid-login.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveAuthTimeoutMs(options?: {
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
}): number;
|
|
5
16
|
/** base64url(sha256(verifier)) — the S256 code challenge for a verifier. */
|
|
6
17
|
export declare function computePkceChallenge(codeVerifier: string): string;
|
|
7
18
|
export interface CliLoginContext {
|
|
@@ -20,6 +31,11 @@ export declare function getCachedLoginContext(options?: ProfileIOOptions): CliLo
|
|
|
20
31
|
export interface LoginOptions extends ProfileIOOptions {
|
|
21
32
|
/** Use this exact port for the localhost callback (no fallback ladder). */
|
|
22
33
|
callbackPort?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Explicit browser-auth wait in ms. Unlike `MODA_AUTH_TIMEOUT_MS` (clamped
|
|
36
|
+
* 30s–30m), an explicit value bypasses that clamp (only floored at 1ms) so
|
|
37
|
+
* programmatic/test callers can set very short or very long waits.
|
|
38
|
+
*/
|
|
23
39
|
timeoutMs?: number;
|
|
24
40
|
openUrl?: (url: string) => Promise<void>;
|
|
25
41
|
/**
|
package/dist/auth.js
CHANGED
|
@@ -20,10 +20,33 @@ const CALLBACK_PORT = 8432;
|
|
|
20
20
|
// callback redirect is port-parameterized, so any of these works as long as
|
|
21
21
|
// the auth URL carries the port that actually got bound.
|
|
22
22
|
const CALLBACK_PORT_LADDER = [CALLBACK_PORT, 8433, 8434, 8435];
|
|
23
|
-
|
|
23
|
+
/** Default wait for browser Auth0/consent during `moda auth login` / init. */
|
|
24
|
+
export const DEFAULT_AUTH_TIMEOUT_MS = 600_000;
|
|
25
|
+
/** Floor / ceiling for MODA_AUTH_TIMEOUT_MS (30s .. 30m). */
|
|
26
|
+
const AUTH_TIMEOUT_MIN_MS = 30_000;
|
|
27
|
+
const AUTH_TIMEOUT_MAX_MS = 30 * 60_000;
|
|
24
28
|
const DEFAULT_EXPIRES_IN_SECONDS = 86_400;
|
|
25
29
|
// PKCE code exchange against {base}/api/cli/auth/exchange.
|
|
26
30
|
const EXCHANGE_TIMEOUT_MS = 10_000;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the browser-auth wait. Explicit LoginOptions.timeoutMs wins;
|
|
33
|
+
* otherwise MODA_AUTH_TIMEOUT_MS (clamped); otherwise a 10-minute default that
|
|
34
|
+
* covers real Auth0 + MFA + slow consent without abandoning mid-login.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveAuthTimeoutMs(options = {}) {
|
|
37
|
+
if (typeof options.timeoutMs === 'number' && Number.isFinite(options.timeoutMs)) {
|
|
38
|
+
return Math.max(1, Math.floor(options.timeoutMs));
|
|
39
|
+
}
|
|
40
|
+
const env = options.env ?? process.env;
|
|
41
|
+
const raw = env.MODA_AUTH_TIMEOUT_MS?.trim();
|
|
42
|
+
if (raw) {
|
|
43
|
+
const parsed = Number.parseInt(raw, 10);
|
|
44
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
45
|
+
return Math.min(AUTH_TIMEOUT_MAX_MS, Math.max(AUTH_TIMEOUT_MIN_MS, parsed));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return DEFAULT_AUTH_TIMEOUT_MS;
|
|
49
|
+
}
|
|
27
50
|
function escapeHtml(value) {
|
|
28
51
|
return value
|
|
29
52
|
.replace(/&/g, '&')
|
|
@@ -209,7 +232,7 @@ export async function login(screen = 'login', options = {}) {
|
|
|
209
232
|
// flips. The flag only needs to clear on failure, where a
|
|
210
233
|
// retry paste is still legitimate.
|
|
211
234
|
res.writeHead(302, {
|
|
212
|
-
Location: `${resolveModaBaseUrl()}/auth/cli/done?status=success`,
|
|
235
|
+
Location: `${resolveModaBaseUrl(storageOptions)}/auth/cli/done?status=success`,
|
|
213
236
|
Connection: 'close',
|
|
214
237
|
});
|
|
215
238
|
res.end(() => {
|
|
@@ -220,7 +243,7 @@ export async function login(screen = 'login', options = {}) {
|
|
|
220
243
|
exchangeInFlight = false;
|
|
221
244
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
222
245
|
res.writeHead(302, {
|
|
223
|
-
Location: `${resolveModaBaseUrl()}/auth/cli/done` +
|
|
246
|
+
Location: `${resolveModaBaseUrl(storageOptions)}/auth/cli/done` +
|
|
224
247
|
`?status=error&reason=${exchangeFailureReason(err)}`,
|
|
225
248
|
Connection: 'close',
|
|
226
249
|
});
|
|
@@ -538,7 +561,7 @@ export async function login(screen = 'login', options = {}) {
|
|
|
538
561
|
}
|
|
539
562
|
timeout = setTimeout(() => {
|
|
540
563
|
fail(new Error('Authentication timed out. Please try again.'));
|
|
541
|
-
}, options.timeoutMs
|
|
564
|
+
}, resolveAuthTimeoutMs({ timeoutMs: options.timeoutMs, env: options.env }));
|
|
542
565
|
server.on('error', (err) => {
|
|
543
566
|
if (settled)
|
|
544
567
|
return;
|
package/dist/doctor.js
CHANGED
|
@@ -7,7 +7,7 @@ import { isAuthSessionValid, loadAuthSession } from './auth.js';
|
|
|
7
7
|
import { resolveApiKey, resolveIngestUrl, resolveModaBaseUrl, resolveTenantId } from './config.js';
|
|
8
8
|
import { readHarnessSyncState, scanHarness } from './harness.js';
|
|
9
9
|
import { HARNESS_REPORT_APPROVAL_PATH, HARNESS_REPORT_APPROVAL_SCHEMA_VERSION, HARNESS_REPORT_PATH, hashHarnessReport, readHarnessReport, validateHarnessReport, } from './harness-report.js';
|
|
10
|
-
import { ensureSecretFilePermissions, getActiveProfile, getModaPaths, loadProfileConfig, resolveSecretReference, secretFilePathFromRef, } from './profiles.js';
|
|
10
|
+
import { ensureSecretFilePermissions, getActiveProfile, getModaPaths, isLoopbackBaseUrl, loadProfileConfig, resolveSecretReference, secretFilePathFromRef, } from './profiles.js';
|
|
11
11
|
import { renderDoctorHuman, renderStatusHuman } from './tui.js';
|
|
12
12
|
export async function runDoctorCommand(context) {
|
|
13
13
|
const report = await buildDoctorReport(context, {
|
|
@@ -132,6 +132,7 @@ export async function buildDoctorReport(context, options) {
|
|
|
132
132
|
}
|
|
133
133
|
checks.push(urlCheck('base_url_valid', 'config', 'Base URL is valid', baseUrl));
|
|
134
134
|
checks.push(urlCheck('ingest_url_valid', 'config', 'Ingest URL is valid', ingestUrl));
|
|
135
|
+
checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
|
|
135
136
|
const manifestPath = join(context.cwd, '.moda', 'harness.json');
|
|
136
137
|
const manifestExists = existsSync(manifestPath);
|
|
137
138
|
checks.push({
|
|
@@ -287,6 +288,82 @@ function urlCheck(id, group, title, value) {
|
|
|
287
288
|
nextCommand: valid ? undefined : 'moda profiles create',
|
|
288
289
|
};
|
|
289
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Local stacks mount `/auth/cli` and `/api/*` CLI/control proxies on the
|
|
293
|
+
* frontend (`:3000` by default). Pointing `base_url` at the backend (`:3001`)
|
|
294
|
+
* breaks browser login even though some Nest routes still answer there.
|
|
295
|
+
*/
|
|
296
|
+
function localBaseUrlCheck(environment, baseUrl) {
|
|
297
|
+
// Only the `local` profile targets a loopback worktree stack. `dev` defaults to
|
|
298
|
+
// remote `https://moda.dev`, so including it here warns valid remote configs.
|
|
299
|
+
if (environment !== 'local') {
|
|
300
|
+
return {
|
|
301
|
+
id: 'local_base_url_frontend',
|
|
302
|
+
group: 'config',
|
|
303
|
+
title: 'Local base URL targets the frontend',
|
|
304
|
+
status: 'skipped',
|
|
305
|
+
severity: 'info',
|
|
306
|
+
evidence: [{ label: 'environment', value: environment }],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
let port = null;
|
|
310
|
+
let loopback = false;
|
|
311
|
+
try {
|
|
312
|
+
const parsed = new URL(baseUrl);
|
|
313
|
+
loopback = isLoopbackBaseUrl(baseUrl);
|
|
314
|
+
port = parsed.port || (parsed.protocol === 'https:' ? '443' : '80');
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return {
|
|
318
|
+
id: 'local_base_url_frontend',
|
|
319
|
+
group: 'config',
|
|
320
|
+
title: 'Local base URL targets the frontend',
|
|
321
|
+
status: 'fail',
|
|
322
|
+
severity: 'critical',
|
|
323
|
+
evidence: [{ label: 'base_url', value: baseUrl }],
|
|
324
|
+
remediation: 'Set the local profile base URL to the frontend origin (http://localhost:3000).',
|
|
325
|
+
nextCommand: 'moda profiles create local --environment local --base-url http://localhost:3000',
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const looksLikeBackend = loopback && (port === '3001' || port === '3002');
|
|
329
|
+
if (looksLikeBackend) {
|
|
330
|
+
return {
|
|
331
|
+
id: 'local_base_url_frontend',
|
|
332
|
+
group: 'config',
|
|
333
|
+
title: 'Local base URL targets the frontend',
|
|
334
|
+
status: 'warning',
|
|
335
|
+
severity: 'warning',
|
|
336
|
+
evidence: [
|
|
337
|
+
{ label: 'base_url', value: baseUrl },
|
|
338
|
+
{ label: 'port', value: port ?? 'unknown' },
|
|
339
|
+
],
|
|
340
|
+
remediation: 'Local CLI auth and control APIs are served by the frontend. ' +
|
|
341
|
+
'Use http://localhost:3000 (or your worktree frontend port), not the backend :3001.',
|
|
342
|
+
nextCommand: 'moda profiles create local --environment local --base-url http://localhost:3000 --use',
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (!loopback) {
|
|
346
|
+
return {
|
|
347
|
+
id: 'local_base_url_frontend',
|
|
348
|
+
group: 'config',
|
|
349
|
+
title: 'Local base URL targets the frontend',
|
|
350
|
+
status: 'warning',
|
|
351
|
+
severity: 'warning',
|
|
352
|
+
evidence: [{ label: 'base_url', value: baseUrl }],
|
|
353
|
+
remediation: 'Local profile should use a loopback frontend URL (http://localhost:3000). ' +
|
|
354
|
+
'Activate with `moda profiles use local` or set MODA_PROFILE=local.',
|
|
355
|
+
nextCommand: 'moda profiles use local',
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
id: 'local_base_url_frontend',
|
|
360
|
+
group: 'config',
|
|
361
|
+
title: 'Local base URL targets the frontend',
|
|
362
|
+
status: 'pass',
|
|
363
|
+
severity: 'info',
|
|
364
|
+
evidence: [{ label: 'base_url', value: baseUrl }],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
290
367
|
function harnessCurrentCheck(cwd, manifestPath, manifestExists, tenantId) {
|
|
291
368
|
if (!manifestExists) {
|
|
292
369
|
return {
|
|
@@ -11,6 +11,13 @@ export interface ArtifactBodyFileItem {
|
|
|
11
11
|
text?: string;
|
|
12
12
|
}
|
|
13
13
|
export declare function extractToolBodyFromText(text: string, toolName: string): string | undefined;
|
|
14
|
+
export declare function annotatedSourceCandidates(sourcePath: string, annotation: ArtifactBodyAnnotation): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Resolve an annotated artifact `path=` to a repo-relative sourcePath.
|
|
17
|
+
* Prefer the nested annotation-relative file when present so later whole-file
|
|
18
|
+
* hydration and content hashing look in the right place.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveAnnotatedSourcePath(sourcePath: string, annotation: ArtifactBodyAnnotation): string;
|
|
14
21
|
export declare function readPromptOrToolBody(type: HarnessArtifactType, sourcePath: string, annotation: ArtifactBodyAnnotation): string | undefined;
|
|
15
22
|
export declare function extractToolBody(file: ArtifactBodyFileItem, tool: {
|
|
16
23
|
name: string;
|
|
@@ -26,6 +26,21 @@ function isSecretishPath(path) {
|
|
|
26
26
|
function isOutsideRoot(rel) {
|
|
27
27
|
return rel === '' || rel === '..' || rel.startsWith('../') || isAbsolute(rel);
|
|
28
28
|
}
|
|
29
|
+
/** Walk from `fromAbs` up to `rootAbs` (inclusive), bounded by `maxDepth`. */
|
|
30
|
+
function walkAncestorDirs(fromAbs, rootAbs, maxDepth = 8) {
|
|
31
|
+
const dirs = [];
|
|
32
|
+
let cursor = fromAbs;
|
|
33
|
+
for (let depth = 0; depth < maxDepth; depth += 1) {
|
|
34
|
+
dirs.push(cursor);
|
|
35
|
+
if (normalizePath(cursor) === normalizePath(rootAbs))
|
|
36
|
+
break;
|
|
37
|
+
const parent = dirname(cursor);
|
|
38
|
+
if (parent === cursor)
|
|
39
|
+
break;
|
|
40
|
+
cursor = parent;
|
|
41
|
+
}
|
|
42
|
+
return dirs;
|
|
43
|
+
}
|
|
29
44
|
function readBoundedRepoText(absPath, rootDir) {
|
|
30
45
|
if (!existsSync(absPath))
|
|
31
46
|
return undefined;
|
|
@@ -113,18 +128,31 @@ export function extractToolBodyFromText(text, toolName) {
|
|
|
113
128
|
}
|
|
114
129
|
return undefined;
|
|
115
130
|
}
|
|
116
|
-
function
|
|
131
|
+
export function annotatedSourceCandidates(sourcePath, annotation) {
|
|
117
132
|
const explicitSourcePath = normalizePath(sourcePath) !== normalizePath(annotation.evidence.path);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
// Relative `path=` annotations in nested packages usually mean "relative to the
|
|
134
|
+
// package/harness root that owns the annotated file", not the file's own directory.
|
|
135
|
+
// Example: harnesses/boardy-ops/src/ops.ts with path=prompts/ops-system.prompt.md
|
|
136
|
+
// resolves to harnesses/boardy-ops/prompts/ops-system.prompt.md (one level up from
|
|
137
|
+
// src/), not harnesses/boardy-ops/src/prompts/... Walk parents between the
|
|
138
|
+
// annotated file and the scan root, then fall back to the scan-root candidate
|
|
139
|
+
// for flat repos that annotate from the repository root.
|
|
140
|
+
if (!explicitSourcePath) {
|
|
141
|
+
return [
|
|
142
|
+
resolve(annotation.rootDir, sourcePath),
|
|
143
|
+
resolve(dirname(annotation.sourceAbsPath), sourcePath),
|
|
144
|
+
annotation.sourceAbsPath,
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
const rootAbs = resolve(annotation.rootDir);
|
|
148
|
+
const candidates = walkAncestorDirs(dirname(annotation.sourceAbsPath), rootAbs)
|
|
149
|
+
.map((dir) => resolve(dir, sourcePath));
|
|
150
|
+
candidates.push(resolve(annotation.rootDir, sourcePath));
|
|
151
|
+
return candidates;
|
|
152
|
+
}
|
|
153
|
+
function readAnnotatedSourceText(sourcePath, annotation) {
|
|
126
154
|
const seen = new Set();
|
|
127
|
-
for (const candidate of
|
|
155
|
+
for (const candidate of annotatedSourceCandidates(sourcePath, annotation)) {
|
|
128
156
|
if (seen.has(candidate))
|
|
129
157
|
continue;
|
|
130
158
|
seen.add(candidate);
|
|
@@ -134,6 +162,25 @@ function readAnnotatedSourceText(sourcePath, annotation) {
|
|
|
134
162
|
}
|
|
135
163
|
return undefined;
|
|
136
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Resolve an annotated artifact `path=` to a repo-relative sourcePath.
|
|
167
|
+
* Prefer the nested annotation-relative file when present so later whole-file
|
|
168
|
+
* hydration and content hashing look in the right place.
|
|
169
|
+
*/
|
|
170
|
+
export function resolveAnnotatedSourcePath(sourcePath, annotation) {
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
for (const candidate of annotatedSourceCandidates(sourcePath, annotation)) {
|
|
173
|
+
if (seen.has(candidate))
|
|
174
|
+
continue;
|
|
175
|
+
seen.add(candidate);
|
|
176
|
+
if (readBoundedRepoText(candidate, annotation.rootDir) === undefined)
|
|
177
|
+
continue;
|
|
178
|
+
const rel = normalizePath(relative(annotation.rootDir, candidate));
|
|
179
|
+
if (!isOutsideRoot(rel))
|
|
180
|
+
return rel;
|
|
181
|
+
}
|
|
182
|
+
return sourcePath;
|
|
183
|
+
}
|
|
137
184
|
export function readPromptOrToolBody(type, sourcePath, annotation) {
|
|
138
185
|
if (!requiresArtifactBody(type))
|
|
139
186
|
return undefined;
|
|
@@ -154,7 +201,8 @@ export function extractToolBody(file, tool) {
|
|
|
154
201
|
}
|
|
155
202
|
function hydrateArtifactBody(artifact, rootDir) {
|
|
156
203
|
if (artifact.sourcePath) {
|
|
157
|
-
const wholeFile = readRepoRelativeText(rootDir, artifact.sourcePath)
|
|
204
|
+
const wholeFile = readRepoRelativeText(rootDir, artifact.sourcePath)
|
|
205
|
+
?? readSourcePathRelativeToEvidence(artifact, rootDir);
|
|
158
206
|
if (wholeFile !== undefined) {
|
|
159
207
|
if (artifact.type === 'prompt' || isDedicatedToolFilePath(artifact.sourcePath)) {
|
|
160
208
|
return wholeFile;
|
|
@@ -174,6 +222,25 @@ function hydrateArtifactBody(artifact, rootDir) {
|
|
|
174
222
|
}
|
|
175
223
|
return undefined;
|
|
176
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Fallback for nested relative `sourcePath` values that were recorded before the
|
|
227
|
+
* annotation-relative resolver existed: walk parent dirs beside the evidence file
|
|
228
|
+
* looking for the package-local prompt/tool path.
|
|
229
|
+
*/
|
|
230
|
+
function readSourcePathRelativeToEvidence(artifact, rootDir) {
|
|
231
|
+
if (!artifact.sourcePath)
|
|
232
|
+
return undefined;
|
|
233
|
+
const evidencePath = firstEvidencePath(artifact);
|
|
234
|
+
if (!evidencePath)
|
|
235
|
+
return undefined;
|
|
236
|
+
const rootAbs = resolve(rootDir);
|
|
237
|
+
for (const dir of walkAncestorDirs(dirname(resolve(rootDir, evidencePath)), rootAbs)) {
|
|
238
|
+
const text = readBoundedRepoText(resolve(dir, artifact.sourcePath), rootDir);
|
|
239
|
+
if (text !== undefined)
|
|
240
|
+
return text;
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
177
244
|
export function hydratePromptAndToolBodies(graph, rootDir) {
|
|
178
245
|
for (const artifact of graph.artifacts) {
|
|
179
246
|
if (!requiresArtifactBody(artifact.type) || artifactHasBody(artifact)) {
|
package/dist/harness.d.ts
CHANGED
|
@@ -273,6 +273,12 @@ export declare function emitWrittenArtifacts(context: CommandContext, written: s
|
|
|
273
273
|
export declare function repairExternalAnalystReport(report: HarnessReport, rootDir: string): HarnessReport;
|
|
274
274
|
export declare function sleep(ms: number): Promise<void>;
|
|
275
275
|
export declare function summarizeInvalidAnalystReport(report: unknown): Record<string, unknown>;
|
|
276
|
+
/**
|
|
277
|
+
* The analyst spellings that select the deterministic local scanner (no LLM,
|
|
278
|
+
* no remote analyze). Shared by `normalizeAnalystAdapter` and `moda init`'s
|
|
279
|
+
* `resolveHarnessRemote` so the set cannot drift between the two.
|
|
280
|
+
*/
|
|
281
|
+
export declare function isDeterministicLocalAnalyst(raw: string | undefined): boolean;
|
|
276
282
|
export type CodingAgentAdapter = ExternalHarnessAnalystAdapter;
|
|
277
283
|
export interface CodingAgentRun {
|
|
278
284
|
adapter: CodingAgentAdapter;
|
package/dist/harness.js
CHANGED
|
@@ -10,7 +10,7 @@ import { resolveApiKey, resolveTenantId } from './config.js';
|
|
|
10
10
|
import { CliInputError, CliInputRequiredError } from './errors.js';
|
|
11
11
|
import { isRemoteHarnessAnalyze, runRemoteHarnessAnalyst } from './harness-remote.js';
|
|
12
12
|
import { getActiveProfile } from './profiles.js';
|
|
13
|
-
import { enrichScanCaptureMetadata, extractToolBody, hydratePromptAndToolBodies, readPromptOrToolBody, } from './harness-artifact-bodies.js';
|
|
13
|
+
import { enrichScanCaptureMetadata, annotatedSourceCandidates, extractToolBody, hydratePromptAndToolBodies, readPromptOrToolBody, resolveAnnotatedSourcePath, } from './harness-artifact-bodies.js';
|
|
14
14
|
import { approveHarnessReport, createHarnessReport, graphForReport, hashHarnessReport, hashStableJson as hashHarnessReportStableJson, HARNESS_REPORT_PATH, loadHarnessReportForSync, normalizeHarnessReport, readHarnessReport, resolveHarnessReportPath, validateHarnessReport, writeHarnessReportArtifacts, } from './harness-report.js';
|
|
15
15
|
import { assertHarnessGraphSyncSize } from './harness-sync-limits.js';
|
|
16
16
|
import { renderHarnessAnalystChoice, renderHarnessAnalystLaunch, renderHarnessAnalystPreflight, renderHarnessAnalyzeHumanResult, renderHarnessProgress, } from './tui.js';
|
|
@@ -1201,6 +1201,15 @@ function shouldPromptForHarnessAnalyst(context) {
|
|
|
1201
1201
|
!context.flags['analyst-command'] &&
|
|
1202
1202
|
!process.env.MODA_HARNESS_ANALYST_COMMAND;
|
|
1203
1203
|
}
|
|
1204
|
+
/**
|
|
1205
|
+
* The analyst spellings that select the deterministic local scanner (no LLM,
|
|
1206
|
+
* no remote analyze). Shared by `normalizeAnalystAdapter` and `moda init`'s
|
|
1207
|
+
* `resolveHarnessRemote` so the set cannot drift between the two.
|
|
1208
|
+
*/
|
|
1209
|
+
export function isDeterministicLocalAnalyst(raw) {
|
|
1210
|
+
const value = raw?.trim().toLowerCase();
|
|
1211
|
+
return value === 'local-scan' || value === 'local' || value === 'scripted' || value === 'moda';
|
|
1212
|
+
}
|
|
1204
1213
|
function normalizeAnalystAdapter(raw) {
|
|
1205
1214
|
const value = (raw && raw !== 'true' ? raw : 'auto').trim().toLowerCase();
|
|
1206
1215
|
if (value === 'auto')
|
|
@@ -1211,7 +1220,7 @@ function normalizeAnalystAdapter(raw) {
|
|
|
1211
1220
|
return 'codex';
|
|
1212
1221
|
if (value === 'cursor' || value === 'cursor-cli' || value === 'cursor_cli')
|
|
1213
1222
|
return 'cursor';
|
|
1214
|
-
if (value
|
|
1223
|
+
if (isDeterministicLocalAnalyst(value))
|
|
1215
1224
|
return 'local-scan';
|
|
1216
1225
|
throw new CliInputError(`Unknown harness analyst '${raw}'.`, 'Use `--analyst=claude|codex|cursor|local-scan`.');
|
|
1217
1226
|
}
|
|
@@ -2439,9 +2448,17 @@ function channelFromAnnotation(annotation) {
|
|
|
2439
2448
|
function artifactFromAnnotation(annotation) {
|
|
2440
2449
|
const values = annotation.values;
|
|
2441
2450
|
const type = artifactType(first(values.kind) ?? first(values.type));
|
|
2442
|
-
const
|
|
2451
|
+
const annotatedPath = first(values.path) ?? annotation.evidence.path;
|
|
2452
|
+
// Canonicalize nested relative path= annotations to a repo-relative location
|
|
2453
|
+
// so later whole-file body hydration and hashing find the real prompt/tool file.
|
|
2454
|
+
const sourcePath = resolveAnnotatedSourcePath(annotatedPath, annotation);
|
|
2443
2455
|
const id = first(values.id) ?? stableId(type, first(values.name) ?? sourcePath);
|
|
2444
|
-
|
|
2456
|
+
// Body reads from the RAW annotated path on purpose: readPromptOrToolBody walks
|
|
2457
|
+
// the annotation's ancestor dirs itself to locate nested-package files, so it
|
|
2458
|
+
// must not be handed the already-canonicalized sourcePath. sourcePath and
|
|
2459
|
+
// contentHash use the canonical path so downstream whole-file hydration and
|
|
2460
|
+
// hashing resolve the same file the body came from.
|
|
2461
|
+
const body = readPromptOrToolBody(type, annotatedPath, annotation);
|
|
2445
2462
|
return {
|
|
2446
2463
|
id,
|
|
2447
2464
|
type,
|
|
@@ -3420,14 +3437,13 @@ function upsertDeployment(graph, next) {
|
|
|
3420
3437
|
existing.environmentId = existing.environmentId ?? next.environmentId;
|
|
3421
3438
|
}
|
|
3422
3439
|
function contentHashForAnnotatedSource(sourcePath, annotation) {
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
annotation.sourceAbsPath,
|
|
3440
|
+
// Reuse the annotation-relative candidate resolver; prefer the canonicalized
|
|
3441
|
+
// repo-relative path first so hashing matches hydrated artifact bodies.
|
|
3442
|
+
const bodyAnnotation = annotation;
|
|
3443
|
+
const rootCandidate = resolve(annotation.rootDir, sourcePath);
|
|
3444
|
+
const candidates = [
|
|
3445
|
+
rootCandidate,
|
|
3446
|
+
...annotatedSourceCandidates(sourcePath, bodyAnnotation).filter((c) => c !== rootCandidate),
|
|
3431
3447
|
];
|
|
3432
3448
|
const seen = new Set();
|
|
3433
3449
|
for (const candidate of candidates) {
|
package/dist/init/index.d.ts
CHANGED
|
@@ -30,8 +30,9 @@ export interface InitRunOptions {
|
|
|
30
30
|
export declare function runInit(flags: Record<string, unknown>, options?: InitRunOptions): Promise<InitResult>;
|
|
31
31
|
/**
|
|
32
32
|
* Onboarding runs harness discovery SERVER-SIDE by default so the customer
|
|
33
|
-
* needs no local coding agent. Opt out with `--local`, `--no-remote`,
|
|
34
|
-
* `--remote=false
|
|
33
|
+
* needs no local coding agent. Opt out with `--local`, `--no-remote`,
|
|
34
|
+
* `--remote=false`, or an explicit deterministic local analyst
|
|
35
|
+
* (`--analyst=local-scan|local|scripted|moda`).
|
|
35
36
|
*/
|
|
36
37
|
export declare function resolveHarnessRemote(flags: Record<string, unknown>): boolean;
|
|
37
38
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/init/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { runSdkIntegration } from './sdk-integration.js';
|
|
|
15
15
|
import { createCommandContext } from '../command-context.js';
|
|
16
16
|
import { VERSION } from '../config.js';
|
|
17
17
|
import { CliAuthError, CliInputError } from '../errors.js';
|
|
18
|
-
import { runHarnessCommand } from '../harness.js';
|
|
18
|
+
import { isDeterministicLocalAnalyst, runHarnessCommand } from '../harness.js';
|
|
19
19
|
import { buildDoctorReport } from '../doctor.js';
|
|
20
20
|
import { renderInitBanner, renderInitComplete, renderInitNote, renderInitSetupPlan, renderInitStep, } from '../tui.js';
|
|
21
21
|
export async function runInit(flags, options = {}) {
|
|
@@ -676,8 +676,9 @@ function isTruthyFlag(value) {
|
|
|
676
676
|
}
|
|
677
677
|
/**
|
|
678
678
|
* Onboarding runs harness discovery SERVER-SIDE by default so the customer
|
|
679
|
-
* needs no local coding agent. Opt out with `--local`, `--no-remote`,
|
|
680
|
-
* `--remote=false
|
|
679
|
+
* needs no local coding agent. Opt out with `--local`, `--no-remote`,
|
|
680
|
+
* `--remote=false`, or an explicit deterministic local analyst
|
|
681
|
+
* (`--analyst=local-scan|local|scripted|moda`).
|
|
681
682
|
*/
|
|
682
683
|
export function resolveHarnessRemote(flags) {
|
|
683
684
|
if (isTruthyFlag(flagValue(flags, 'local')))
|
|
@@ -687,6 +688,13 @@ export function resolveHarnessRemote(flags) {
|
|
|
687
688
|
const remote = parseOptionalBoolean(flagValue(flags, 'remote'));
|
|
688
689
|
if (remote !== undefined)
|
|
689
690
|
return remote;
|
|
691
|
+
// Scenarios and users that pin `--analyst=local-scan` mean "run the local
|
|
692
|
+
// deterministic scanner", not "upload to hosted analyze and poll". Treat that
|
|
693
|
+
// as an implicit local opt-out so moda-test live/onboarding suites and
|
|
694
|
+
// developer workflows do not hang waiting on remote analyze.
|
|
695
|
+
if (isDeterministicLocalAnalyst(stringFlag(flagValue(flags, 'analyst')))) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
690
698
|
return true;
|
|
691
699
|
}
|
|
692
700
|
function flagValue(flags, ...keys) {
|
package/dist/init/tenant.js
CHANGED
|
@@ -360,7 +360,8 @@ export async function selectTenantAndCreateKey(options = {}) {
|
|
|
360
360
|
'This organization has reached its CLI API key limit.', {
|
|
361
361
|
// Command-agnostic: this path is shared by `moda init` and
|
|
362
362
|
// `moda provision`.
|
|
363
|
-
guidance: 'Revoke
|
|
363
|
+
guidance: 'Revoke unused "CLI - ..." keys in the dashboard (Settings → API Keys), ' +
|
|
364
|
+
'or reuse the existing key already saved in ~/.moda/config.json from a previous moda init / moda provision --save, then retry.',
|
|
364
365
|
});
|
|
365
366
|
}
|
|
366
367
|
throw new Error(`Failed to create API key (409): ${body}`);
|
package/dist/manifest.js
CHANGED
package/package.json
CHANGED