@moda-ai/cli 1.22.0 → 1.22.2
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-g8qs2tj9.js +9777 -0
- package/dist/cli-pwbvtes8.js +2312 -0
- package/dist/cli-qszcpda3.js +284 -0
- package/dist/cli.js +4775 -1159
- package/dist/index-6h0nc1r5.js +2765 -0
- package/dist/provision-j5gq8cvx.js +72 -0
- package/package.json +3 -2
- package/skills/integration/index.json +2 -2
- package/dist/agent-schemas.d.ts +0 -3
- package/dist/agent-schemas.js +0 -358
- package/dist/agent.d.ts +0 -5
- package/dist/agent.js +0 -477
- package/dist/analyst-tui.d.ts +0 -81
- package/dist/analyst-tui.js +0 -314
- package/dist/api-client.d.ts +0 -46
- package/dist/api-client.js +0 -120
- package/dist/ask-stream.d.ts +0 -146
- package/dist/ask-stream.js +0 -473
- package/dist/auth-storage.d.ts +0 -35
- package/dist/auth-storage.js +0 -159
- package/dist/auth.d.ts +0 -50
- package/dist/auth.js +0 -694
- package/dist/cli-session.d.ts +0 -26
- package/dist/cli-session.js +0 -169
- package/dist/cli.d.ts +0 -44
- package/dist/command-context.d.ts +0 -62
- package/dist/command-context.js +0 -57
- package/dist/command-registry.d.ts +0 -22
- package/dist/command-registry.js +0 -15
- package/dist/config.d.ts +0 -60
- package/dist/config.js +0 -110
- package/dist/doctor.d.ts +0 -79
- package/dist/doctor.js +0 -1182
- package/dist/errors.d.ts +0 -50
- package/dist/errors.js +0 -64
- package/dist/exit-funnel.d.ts +0 -14
- package/dist/exit-funnel.js +0 -53
- package/dist/harness-artifact-bodies.d.ts +0 -28
- package/dist/harness-artifact-bodies.js +0 -276
- package/dist/harness-candidates.d.ts +0 -77
- package/dist/harness-candidates.js +0 -482
- package/dist/harness-remote.d.ts +0 -135
- package/dist/harness-remote.js +0 -540
- package/dist/harness-report.d.ts +0 -145
- package/dist/harness-report.js +0 -1579
- package/dist/harness-sync-limits.d.ts +0 -8
- package/dist/harness-sync-limits.js +0 -21
- package/dist/harness.d.ts +0 -463
- package/dist/harness.js +0 -4009
- package/dist/init/index.d.ts +0 -78
- package/dist/init/index.js +0 -1442
- package/dist/init/mcp.d.ts +0 -2
- package/dist/init/mcp.js +0 -27
- package/dist/init/progress-board.d.ts +0 -80
- package/dist/init/progress-board.js +0 -225
- package/dist/init/rules.d.ts +0 -6
- package/dist/init/rules.js +0 -171
- package/dist/init/sdk-catalog.d.ts +0 -10
- package/dist/init/sdk-catalog.js +0 -80
- package/dist/init/sdk-guide.d.ts +0 -19
- package/dist/init/sdk-guide.js +0 -185
- package/dist/init/sdk-integration.d.ts +0 -53
- package/dist/init/sdk-integration.js +0 -835
- package/dist/init/sdk-protocol.d.ts +0 -135
- package/dist/init/sdk-protocol.js +0 -152
- package/dist/init/sdk-verification.d.ts +0 -27
- package/dist/init/sdk-verification.js +0 -123
- package/dist/init/tenant.d.ts +0 -67
- package/dist/init/tenant.js +0 -421
- package/dist/intelligence-schema.d.ts +0 -349
- package/dist/intelligence-schema.js +0 -174
- package/dist/keys.d.ts +0 -47
- package/dist/keys.js +0 -136
- package/dist/manifest.d.ts +0 -5
- package/dist/manifest.js +0 -114
- package/dist/markdown-terminal.d.ts +0 -49
- package/dist/markdown-terminal.js +0 -212
- package/dist/output.d.ts +0 -191
- package/dist/output.js +0 -624
- package/dist/production-intelligence.d.ts +0 -175
- package/dist/production-intelligence.js +0 -1430
- package/dist/profile-commands.d.ts +0 -10
- package/dist/profile-commands.js +0 -342
- package/dist/profiles.d.ts +0 -112
- package/dist/profiles.js +0 -573
- package/dist/prompts-ab.d.ts +0 -71
- package/dist/prompts-ab.js +0 -313
- package/dist/prompts-propose.d.ts +0 -15
- package/dist/prompts-propose.js +0 -212
- package/dist/prompts.d.ts +0 -58
- package/dist/prompts.js +0 -418
- package/dist/provision.d.ts +0 -28
- package/dist/provision.js +0 -104
- package/dist/redact.d.ts +0 -2
- package/dist/redact.js +0 -90
- package/dist/schemas.d.ts +0 -225
- package/dist/schemas.js +0 -126
- package/dist/skills-bundled.d.ts +0 -35
- package/dist/skills-bundled.js +0 -157
- package/dist/skills-manifest.d.ts +0 -33
- package/dist/skills-manifest.js +0 -244
- package/dist/skills.d.ts +0 -57
- package/dist/skills.js +0 -777
- package/dist/sync.d.ts +0 -5
- package/dist/sync.js +0 -36
- package/dist/telemetry.d.ts +0 -56
- package/dist/telemetry.js +0 -167
- package/dist/transforms.d.ts +0 -25
- package/dist/transforms.js +0 -102
- package/dist/tui.d.ts +0 -48
- package/dist/tui.js +0 -393
- package/dist/update-check.d.ts +0 -31
- package/dist/update-check.js +0 -141
- package/dist/workspace.d.ts +0 -12
- package/dist/workspace.js +0 -1294
|
@@ -0,0 +1,2312 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
class CliInputError extends Error {
|
|
6
|
+
usage;
|
|
7
|
+
constructor(message, usage) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.usage = usage;
|
|
10
|
+
this.name = "CliInputError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class CliAccessError extends Error {
|
|
15
|
+
guidance;
|
|
16
|
+
constructor(message, options = {}) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CliAccessError";
|
|
19
|
+
this.guidance = options.guidance;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class CliInputRequiredError extends Error {
|
|
24
|
+
input_request;
|
|
25
|
+
constructor(message, input_request, usage) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.input_request = input_request;
|
|
28
|
+
this.name = "CliInputRequiredError";
|
|
29
|
+
if (usage)
|
|
30
|
+
this.usage = usage;
|
|
31
|
+
}
|
|
32
|
+
usage;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class CliAuthError extends Error {
|
|
36
|
+
auth;
|
|
37
|
+
constructor(message, auth, usage) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.auth = auth;
|
|
40
|
+
this.name = "CliAuthError";
|
|
41
|
+
if (usage)
|
|
42
|
+
this.usage = usage;
|
|
43
|
+
}
|
|
44
|
+
usage;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class CliCancellationError extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "CliCancellationError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function isNetworkLikeError(error) {
|
|
54
|
+
return error instanceof Error && (error.message.startsWith("Network error:") || error.message.startsWith("Request timed out") || error.name === "AbortError" || /(?:ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|socket hang up)/i.test(error.message));
|
|
55
|
+
}
|
|
56
|
+
function isApiLikeError(error) {
|
|
57
|
+
return error instanceof Error && (error.name === "ApiError" || ("statusCode" in error) && typeof error.statusCode === "number");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/config.ts
|
|
61
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
62
|
+
|
|
63
|
+
// src/profiles.ts
|
|
64
|
+
import {
|
|
65
|
+
chmodSync,
|
|
66
|
+
existsSync,
|
|
67
|
+
mkdirSync,
|
|
68
|
+
readFileSync,
|
|
69
|
+
rmSync,
|
|
70
|
+
statSync,
|
|
71
|
+
writeFileSync
|
|
72
|
+
} from "node:fs";
|
|
73
|
+
import { homedir } from "node:os";
|
|
74
|
+
import { dirname, isAbsolute, join, normalize, relative } from "node:path";
|
|
75
|
+
var PROFILE_CONFIG_VERSION = 1;
|
|
76
|
+
var PROD_BASE_URL = "https://moda.dev";
|
|
77
|
+
var DEFAULT_INGEST_URL = "https://moda-ingest.modas.workers.dev";
|
|
78
|
+
var DEFAULT_PROFILE_NAME = "prod";
|
|
79
|
+
function getEnv(options = {}) {
|
|
80
|
+
return options.env ?? process.env;
|
|
81
|
+
}
|
|
82
|
+
function resolveModaHome(options = {}) {
|
|
83
|
+
const env = getEnv(options);
|
|
84
|
+
return options.home || env.HOME || env.USERPROFILE || homedir();
|
|
85
|
+
}
|
|
86
|
+
function getModaPaths(options = {}) {
|
|
87
|
+
const home = resolveModaHome(options);
|
|
88
|
+
const dir = join(home, ".moda");
|
|
89
|
+
return {
|
|
90
|
+
home,
|
|
91
|
+
dir,
|
|
92
|
+
legacyConfigPath: join(dir, "config.json"),
|
|
93
|
+
legacyAuthPath: join(dir, "auth.json"),
|
|
94
|
+
profileConfigPath: join(dir, "profiles.json"),
|
|
95
|
+
secretsDir: join(dir, "secrets")
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function getModaDir(options = {}) {
|
|
99
|
+
return getModaPaths(options).dir;
|
|
100
|
+
}
|
|
101
|
+
function isRecord(value) {
|
|
102
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
function optionalString(value) {
|
|
105
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
106
|
+
}
|
|
107
|
+
function readJsonFile(path) {
|
|
108
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
109
|
+
}
|
|
110
|
+
function safeReadRecord(path) {
|
|
111
|
+
try {
|
|
112
|
+
if (!existsSync(path))
|
|
113
|
+
return;
|
|
114
|
+
const parsed = readJsonFile(path);
|
|
115
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function loadLegacyCliConfig(options = {}) {
|
|
121
|
+
const parsed = safeReadRecord(getModaPaths(options).legacyConfigPath);
|
|
122
|
+
if (!parsed)
|
|
123
|
+
return {};
|
|
124
|
+
return {
|
|
125
|
+
api_key: optionalString(parsed.api_key),
|
|
126
|
+
ingest_url: optionalString(parsed.ingest_url),
|
|
127
|
+
tenant_id: optionalString(parsed.tenant_id)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function writeLegacyCliConfig(update, options = {}) {
|
|
131
|
+
const paths = getModaPaths(options);
|
|
132
|
+
mkdirSync(paths.dir, { recursive: true });
|
|
133
|
+
const merged = { ...loadLegacyCliConfig(options), ...update };
|
|
134
|
+
writeSecretJsonFile(paths.legacyConfigPath, merged);
|
|
135
|
+
return merged;
|
|
136
|
+
}
|
|
137
|
+
function profileDefaults(name, environment, now) {
|
|
138
|
+
const local = environment === "local";
|
|
139
|
+
const staging = environment === "staging";
|
|
140
|
+
return {
|
|
141
|
+
name,
|
|
142
|
+
environment,
|
|
143
|
+
base_url: local ? "http://localhost:3000" : staging ? "https://staging.moda.dev" : PROD_BASE_URL,
|
|
144
|
+
ingest_url: local ? "http://localhost:8787" : DEFAULT_INGEST_URL,
|
|
145
|
+
metadata: {
|
|
146
|
+
created_at: now.toISOString(),
|
|
147
|
+
source: "default"
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function createDefaultProfiles(options = {}) {
|
|
152
|
+
const now = options.now ?? new Date;
|
|
153
|
+
return {
|
|
154
|
+
local: profileDefaults("local", "local", now),
|
|
155
|
+
staging: profileDefaults("staging", "staging", now),
|
|
156
|
+
prod: profileDefaults("prod", "prod", now)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function isLoopbackHostname(hostname) {
|
|
160
|
+
const host = hostname.toLowerCase();
|
|
161
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
162
|
+
}
|
|
163
|
+
function isLoopbackBaseUrl(url) {
|
|
164
|
+
try {
|
|
165
|
+
return isLoopbackHostname(new URL(url).hostname);
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function defaultIngestUrlForEnvironment(environment) {
|
|
171
|
+
if (environment === "local")
|
|
172
|
+
return "http://localhost:8787";
|
|
173
|
+
return DEFAULT_INGEST_URL;
|
|
174
|
+
}
|
|
175
|
+
function environmentAllowsLoopback(environment) {
|
|
176
|
+
return environment !== "prod";
|
|
177
|
+
}
|
|
178
|
+
function sanitizeProfileBaseUrl(url, environment) {
|
|
179
|
+
const normalized = normalizeModaBaseUrl(url);
|
|
180
|
+
if (environmentAllowsLoopback(environment) || !isLoopbackBaseUrl(normalized)) {
|
|
181
|
+
return normalized;
|
|
182
|
+
}
|
|
183
|
+
return PROD_BASE_URL;
|
|
184
|
+
}
|
|
185
|
+
function sanitizeProfileIngestUrl(url, environment) {
|
|
186
|
+
if (!url)
|
|
187
|
+
return url;
|
|
188
|
+
if (environmentAllowsLoopback(environment) || !isLoopbackBaseUrl(url)) {
|
|
189
|
+
return url;
|
|
190
|
+
}
|
|
191
|
+
return DEFAULT_INGEST_URL;
|
|
192
|
+
}
|
|
193
|
+
function enforceNonLoopbackBaseUrl(url, environment) {
|
|
194
|
+
return sanitizeProfileBaseUrl(url, environment);
|
|
195
|
+
}
|
|
196
|
+
function enforceNonLoopbackIngestUrl(url, environment) {
|
|
197
|
+
return sanitizeProfileIngestUrl(url, environment) ?? defaultIngestUrlForEnvironment(environment);
|
|
198
|
+
}
|
|
199
|
+
function activeProfileEnvironment(options) {
|
|
200
|
+
try {
|
|
201
|
+
return getActiveProfile(options).environment;
|
|
202
|
+
} catch {
|
|
203
|
+
return "prod";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function normalizeEnvironment(value) {
|
|
207
|
+
if (value === "local" || value === "dev" || value === "staging" || value === "prod") {
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
return "prod";
|
|
211
|
+
}
|
|
212
|
+
function isSecretReference(value) {
|
|
213
|
+
return value.startsWith("env:") || value.startsWith("file:") || value === "legacy:config.api_key" || value === "legacy:auth.access_token";
|
|
214
|
+
}
|
|
215
|
+
function normalizeSecretReference(value) {
|
|
216
|
+
if (typeof value !== "string")
|
|
217
|
+
return;
|
|
218
|
+
return isSecretReference(value) ? value : undefined;
|
|
219
|
+
}
|
|
220
|
+
function assertValidProfileName(name) {
|
|
221
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) {
|
|
222
|
+
throw new Error(`Invalid profile name '${name}'. Use letters, numbers, dots, dashes, or underscores.`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function normalizeProfile(fallbackName, value, options) {
|
|
226
|
+
if (!isRecord(value))
|
|
227
|
+
return;
|
|
228
|
+
const name = optionalString(value.name) ?? fallbackName;
|
|
229
|
+
try {
|
|
230
|
+
assertValidProfileName(name);
|
|
231
|
+
} catch {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const defaults = profileDefaults(name, normalizeEnvironment(value.environment), options.now ?? new Date);
|
|
235
|
+
return {
|
|
236
|
+
...defaults,
|
|
237
|
+
base_url: sanitizeProfileBaseUrl(optionalString(value.base_url) ?? defaults.base_url, normalizeEnvironment(value.environment)),
|
|
238
|
+
ingest_url: sanitizeProfileIngestUrl(optionalString(value.ingest_url) ?? defaults.ingest_url, normalizeEnvironment(value.environment)),
|
|
239
|
+
tenant_id: optionalString(value.tenant_id),
|
|
240
|
+
workspace_id: optionalString(value.workspace_id),
|
|
241
|
+
api_key_ref: normalizeSecretReference(value.api_key_ref),
|
|
242
|
+
auth_token_ref: normalizeSecretReference(value.auth_token_ref),
|
|
243
|
+
metadata: isRecord(value.metadata) ? value.metadata : defaults.metadata
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function inspectLegacyAuth(path) {
|
|
247
|
+
const parsed = safeReadRecord(path);
|
|
248
|
+
const expiresAt = typeof parsed?.expires_at === "number" ? parsed.expires_at : undefined;
|
|
249
|
+
return {
|
|
250
|
+
path,
|
|
251
|
+
present: Boolean(parsed),
|
|
252
|
+
has_access_token: typeof parsed?.access_token === "string",
|
|
253
|
+
expires_at: expiresAt,
|
|
254
|
+
expired: expiresAt === undefined ? undefined : Date.now() / 1000 > expiresAt - 60
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function inspectProfileMigrationState(options = {}) {
|
|
258
|
+
const paths = getModaPaths(options);
|
|
259
|
+
const legacyConfig = loadLegacyCliConfig(options);
|
|
260
|
+
return {
|
|
261
|
+
legacy_config: {
|
|
262
|
+
path: paths.legacyConfigPath,
|
|
263
|
+
present: existsSync(paths.legacyConfigPath),
|
|
264
|
+
has_api_key: Boolean(legacyConfig.api_key),
|
|
265
|
+
has_ingest_url: Boolean(legacyConfig.ingest_url),
|
|
266
|
+
has_tenant_id: Boolean(legacyConfig.tenant_id)
|
|
267
|
+
},
|
|
268
|
+
legacy_auth: inspectLegacyAuth(paths.legacyAuthPath)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function applyLegacyAwareness(config, options) {
|
|
272
|
+
const legacy = loadLegacyCliConfig(options);
|
|
273
|
+
const migration = inspectProfileMigrationState(options);
|
|
274
|
+
const prod = config.profiles.prod;
|
|
275
|
+
if (prod && (legacy.api_key || legacy.ingest_url || legacy.tenant_id)) {
|
|
276
|
+
const migratedIngestUrl = legacy.ingest_url && !isLoopbackBaseUrl(legacy.ingest_url) ? legacy.ingest_url : prod.ingest_url;
|
|
277
|
+
config.profiles.prod = {
|
|
278
|
+
...prod,
|
|
279
|
+
ingest_url: migratedIngestUrl,
|
|
280
|
+
tenant_id: legacy.tenant_id ?? prod.tenant_id,
|
|
281
|
+
api_key_ref: legacy.api_key ? "legacy:config.api_key" : prod.api_key_ref,
|
|
282
|
+
auth_token_ref: migration.legacy_auth.has_access_token ? "legacy:auth.access_token" : prod.auth_token_ref,
|
|
283
|
+
metadata: {
|
|
284
|
+
...prod.metadata,
|
|
285
|
+
source: "migration",
|
|
286
|
+
legacy_config_path: migration.legacy_config.path,
|
|
287
|
+
legacy_auth_path: migration.legacy_auth.path
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
...config,
|
|
293
|
+
migrations: migration
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function normalizeProfileConfig(raw, options = {}) {
|
|
297
|
+
const profiles = createDefaultProfiles(options);
|
|
298
|
+
let activeProfile = DEFAULT_PROFILE_NAME;
|
|
299
|
+
if (isRecord(raw)) {
|
|
300
|
+
if (isRecord(raw.profiles)) {
|
|
301
|
+
for (const [name, profile] of Object.entries(raw.profiles)) {
|
|
302
|
+
const normalized = normalizeProfile(name, profile, options);
|
|
303
|
+
if (normalized)
|
|
304
|
+
profiles[normalized.name] = normalized;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const configuredActive = optionalString(raw.active_profile);
|
|
308
|
+
if (configuredActive && profiles[configuredActive]) {
|
|
309
|
+
activeProfile = configuredActive;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return applyLegacyAwareness({
|
|
313
|
+
version: PROFILE_CONFIG_VERSION,
|
|
314
|
+
active_profile: activeProfile,
|
|
315
|
+
profiles,
|
|
316
|
+
migrations: inspectProfileMigrationState(options)
|
|
317
|
+
}, options);
|
|
318
|
+
}
|
|
319
|
+
function loadProfileConfig(options = {}) {
|
|
320
|
+
const path = getModaPaths(options).profileConfigPath;
|
|
321
|
+
const parsed = safeReadRecord(path);
|
|
322
|
+
return normalizeProfileConfig(parsed, options);
|
|
323
|
+
}
|
|
324
|
+
function saveProfileConfig(config, options = {}) {
|
|
325
|
+
const active = config.active_profile || DEFAULT_PROFILE_NAME;
|
|
326
|
+
if (!config.profiles[active]) {
|
|
327
|
+
throw new Error(`Cannot activate missing profile '${active}'.`);
|
|
328
|
+
}
|
|
329
|
+
const path = getModaPaths(options).profileConfigPath;
|
|
330
|
+
writeSecretJsonFile(path, {
|
|
331
|
+
version: PROFILE_CONFIG_VERSION,
|
|
332
|
+
active_profile: active,
|
|
333
|
+
profiles: config.profiles,
|
|
334
|
+
migrations: inspectProfileMigrationState(options)
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function getActiveProfile(options = {}) {
|
|
338
|
+
const config = loadProfileConfig(options);
|
|
339
|
+
const envProfile = optionalString(getEnv(options).MODA_PROFILE);
|
|
340
|
+
const requested = options.profile || envProfile || config.active_profile;
|
|
341
|
+
const profile = config.profiles[requested];
|
|
342
|
+
if (!profile) {
|
|
343
|
+
throw new Error(`Moda profile '${requested}' does not exist.`);
|
|
344
|
+
}
|
|
345
|
+
return profile;
|
|
346
|
+
}
|
|
347
|
+
function setActiveProfile(name, options = {}) {
|
|
348
|
+
assertValidProfileName(name);
|
|
349
|
+
const config = loadProfileConfig(options);
|
|
350
|
+
if (!config.profiles[name]) {
|
|
351
|
+
throw new Error(`Moda profile '${name}' does not exist.`);
|
|
352
|
+
}
|
|
353
|
+
const next = { ...config, active_profile: name };
|
|
354
|
+
saveProfileConfig(next, options);
|
|
355
|
+
return next;
|
|
356
|
+
}
|
|
357
|
+
function upsertProfile(profile, options = {}) {
|
|
358
|
+
assertValidProfileName(profile.name);
|
|
359
|
+
const config = loadProfileConfig(options);
|
|
360
|
+
const next = {
|
|
361
|
+
...config,
|
|
362
|
+
profiles: {
|
|
363
|
+
...config.profiles,
|
|
364
|
+
[profile.name]: {
|
|
365
|
+
...profile,
|
|
366
|
+
metadata: {
|
|
367
|
+
...profile.metadata,
|
|
368
|
+
updated_at: (options.now ?? new Date).toISOString()
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
saveProfileConfig(next, options);
|
|
374
|
+
return next;
|
|
375
|
+
}
|
|
376
|
+
function syncLegacyConfigToProfile(legacy, options = {}) {
|
|
377
|
+
const active = getActiveProfile(options);
|
|
378
|
+
return upsertProfile({
|
|
379
|
+
...active,
|
|
380
|
+
ingest_url: legacy.ingest_url ?? active.ingest_url,
|
|
381
|
+
tenant_id: legacy.tenant_id ?? active.tenant_id,
|
|
382
|
+
api_key_ref: legacy.api_key ? "legacy:config.api_key" : active.api_key_ref,
|
|
383
|
+
metadata: {
|
|
384
|
+
...active.metadata,
|
|
385
|
+
source: "migration",
|
|
386
|
+
legacy_config_path: getModaPaths(options).legacyConfigPath
|
|
387
|
+
}
|
|
388
|
+
}, options);
|
|
389
|
+
}
|
|
390
|
+
function clearStoredApiKeyCredentials(options = {}) {
|
|
391
|
+
let cleared = false;
|
|
392
|
+
const paths = getModaPaths(options);
|
|
393
|
+
const legacy = loadLegacyCliConfig(options);
|
|
394
|
+
if (legacy.api_key || legacy.tenant_id) {
|
|
395
|
+
writeSecretJsonFile(paths.legacyConfigPath, legacy.ingest_url ? { ingest_url: legacy.ingest_url } : {});
|
|
396
|
+
cleared = true;
|
|
397
|
+
}
|
|
398
|
+
const config = loadProfileConfig(options);
|
|
399
|
+
const profileName = options.profile || optionalString(getEnv(options).MODA_PROFILE) || config.active_profile;
|
|
400
|
+
const profile = config.profiles[profileName];
|
|
401
|
+
if (profile && (profile.api_key_ref || profile.tenant_id)) {
|
|
402
|
+
if (profile.api_key_ref?.startsWith("file:")) {
|
|
403
|
+
const secretPath = resolveFileSecretPath(profile.api_key_ref, options);
|
|
404
|
+
if (secretPath)
|
|
405
|
+
rmSync(secretPath, { force: true });
|
|
406
|
+
}
|
|
407
|
+
const { api_key_ref: _key, tenant_id: _tenant, ...rest } = profile;
|
|
408
|
+
saveProfileConfig({ ...config, profiles: { ...config.profiles, [profileName]: rest } }, options);
|
|
409
|
+
cleared = true;
|
|
410
|
+
}
|
|
411
|
+
return cleared;
|
|
412
|
+
}
|
|
413
|
+
function resolveFileSecretPath(reference, options) {
|
|
414
|
+
if (!reference.startsWith("file:"))
|
|
415
|
+
return;
|
|
416
|
+
const rawPath = reference.slice("file:".length);
|
|
417
|
+
if (!rawPath)
|
|
418
|
+
return;
|
|
419
|
+
const paths = getModaPaths(options);
|
|
420
|
+
const secretPath = isAbsolute(rawPath) ? normalize(rawPath) : normalize(join(paths.dir, rawPath));
|
|
421
|
+
const rel = relative(paths.dir, secretPath);
|
|
422
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
423
|
+
throw new Error(`Secret reference escapes the Moda directory: ${reference}`);
|
|
424
|
+
}
|
|
425
|
+
return secretPath;
|
|
426
|
+
}
|
|
427
|
+
function secretFilePathFromRef(reference, options = {}) {
|
|
428
|
+
return resolveFileSecretPath(reference, options);
|
|
429
|
+
}
|
|
430
|
+
function resolveSecretReference(reference, options = {}) {
|
|
431
|
+
if (!reference)
|
|
432
|
+
return;
|
|
433
|
+
const env = getEnv(options);
|
|
434
|
+
if (reference.startsWith("env:")) {
|
|
435
|
+
return env[reference.slice("env:".length)] || undefined;
|
|
436
|
+
}
|
|
437
|
+
if (reference === "legacy:config.api_key") {
|
|
438
|
+
return loadLegacyCliConfig(options).api_key;
|
|
439
|
+
}
|
|
440
|
+
if (reference === "legacy:auth.access_token") {
|
|
441
|
+
const parsed2 = safeReadRecord(getModaPaths(options).legacyAuthPath);
|
|
442
|
+
return optionalString(parsed2?.access_token);
|
|
443
|
+
}
|
|
444
|
+
const filePath = resolveFileSecretPath(reference, options);
|
|
445
|
+
if (!filePath)
|
|
446
|
+
return;
|
|
447
|
+
const parsed = safeReadRecord(filePath);
|
|
448
|
+
if (parsed) {
|
|
449
|
+
return optionalString(parsed.value) ?? optionalString(parsed.api_key) ?? optionalString(parsed.access_token);
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const raw = readFileSync(filePath, "utf8").trim();
|
|
453
|
+
return raw || undefined;
|
|
454
|
+
} catch {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function resolveApiKeyFromActiveProfile(options = {}) {
|
|
459
|
+
try {
|
|
460
|
+
return resolveSecretReference(getActiveProfile(options).api_key_ref, options);
|
|
461
|
+
} catch {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function normalizeModaBaseUrl(url) {
|
|
466
|
+
try {
|
|
467
|
+
const parsed = new URL(url);
|
|
468
|
+
if (parsed.hostname === "www.moda.dev") {
|
|
469
|
+
parsed.hostname = "moda.dev";
|
|
470
|
+
return parsed.toString().replace(/\/$/, "");
|
|
471
|
+
}
|
|
472
|
+
} catch {}
|
|
473
|
+
return url.replace(/\/$/, "");
|
|
474
|
+
}
|
|
475
|
+
function resolveBaseUrl(options = {}) {
|
|
476
|
+
const environment = activeProfileEnvironment(options);
|
|
477
|
+
const envBase = optionalString(getEnv(options).MODA_BASE_URL);
|
|
478
|
+
if (envBase) {
|
|
479
|
+
return enforceNonLoopbackBaseUrl(normalizeModaBaseUrl(envBase), environment);
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
return enforceNonLoopbackBaseUrl(normalizeModaBaseUrl(getActiveProfile(options).base_url || PROD_BASE_URL), environment);
|
|
483
|
+
} catch {
|
|
484
|
+
return PROD_BASE_URL;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function resolveIngestUrl(options = {}) {
|
|
488
|
+
const environment = activeProfileEnvironment(options);
|
|
489
|
+
const envIngest = optionalString(getEnv(options).MODA_INGEST_URL);
|
|
490
|
+
if (envIngest) {
|
|
491
|
+
return enforceNonLoopbackIngestUrl(envIngest, environment);
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const profileIngest = getActiveProfile(options).ingest_url;
|
|
495
|
+
if (profileIngest) {
|
|
496
|
+
return enforceNonLoopbackIngestUrl(profileIngest, environment);
|
|
497
|
+
}
|
|
498
|
+
} catch {}
|
|
499
|
+
const legacyIngest = optionalString(loadLegacyCliConfig(options).ingest_url);
|
|
500
|
+
if (legacyIngest) {
|
|
501
|
+
return enforceNonLoopbackIngestUrl(legacyIngest, environment);
|
|
502
|
+
}
|
|
503
|
+
return defaultIngestUrlForEnvironment(environment);
|
|
504
|
+
}
|
|
505
|
+
function resolveTenantIdFromActiveProfile(options = {}) {
|
|
506
|
+
try {
|
|
507
|
+
return getActiveProfile(options).tenant_id;
|
|
508
|
+
} catch {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function modeString(mode) {
|
|
513
|
+
return `0${(mode & 511).toString(8).padStart(3, "0")}`;
|
|
514
|
+
}
|
|
515
|
+
function ensureSecretFilePermissions(path, repair = true) {
|
|
516
|
+
if (!existsSync(path)) {
|
|
517
|
+
return { path, exists: false, secure: false, repaired: false };
|
|
518
|
+
}
|
|
519
|
+
const before = statSync(path);
|
|
520
|
+
if (!before.isFile()) {
|
|
521
|
+
return {
|
|
522
|
+
path,
|
|
523
|
+
exists: true,
|
|
524
|
+
secure: false,
|
|
525
|
+
repaired: false,
|
|
526
|
+
mode: modeString(before.mode),
|
|
527
|
+
reason: "not_file"
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if ((before.mode & 63) !== 0 && repair) {
|
|
531
|
+
try {
|
|
532
|
+
chmodSync(path, 384);
|
|
533
|
+
const after = statSync(path);
|
|
534
|
+
return {
|
|
535
|
+
path,
|
|
536
|
+
exists: true,
|
|
537
|
+
secure: (after.mode & 63) === 0,
|
|
538
|
+
repaired: true,
|
|
539
|
+
mode: modeString(after.mode)
|
|
540
|
+
};
|
|
541
|
+
} catch {
|
|
542
|
+
return {
|
|
543
|
+
path,
|
|
544
|
+
exists: true,
|
|
545
|
+
secure: false,
|
|
546
|
+
repaired: false,
|
|
547
|
+
mode: modeString(before.mode),
|
|
548
|
+
reason: "chmod_failed"
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
path,
|
|
554
|
+
exists: true,
|
|
555
|
+
secure: (before.mode & 63) === 0,
|
|
556
|
+
repaired: false,
|
|
557
|
+
mode: modeString(before.mode)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function writeSecretJsonFile(path, value) {
|
|
561
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
562
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
563
|
+
`, { mode: 384 });
|
|
564
|
+
try {
|
|
565
|
+
chmodSync(path, 384);
|
|
566
|
+
} catch {}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/config.ts
|
|
570
|
+
var pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
571
|
+
var VERSION = pkg.version;
|
|
572
|
+
var MODA_BASE_URL = resolveBaseUrl();
|
|
573
|
+
function loadCliConfig(options = {}) {
|
|
574
|
+
return loadLegacyCliConfig(options);
|
|
575
|
+
}
|
|
576
|
+
function saveCliConfig(update, options = {}) {
|
|
577
|
+
const merged = writeLegacyCliConfig(update, options);
|
|
578
|
+
try {
|
|
579
|
+
syncLegacyConfigToProfile(merged, options);
|
|
580
|
+
} catch {}
|
|
581
|
+
}
|
|
582
|
+
function resolveApiKey(options = {}) {
|
|
583
|
+
const env = options.env ?? process.env;
|
|
584
|
+
return env.MODA_API_KEY || resolveApiKeyFromActiveProfile(options) || loadCliConfig(options).api_key || undefined;
|
|
585
|
+
}
|
|
586
|
+
var SIGNED_API_KEY_PREFIX = "moda_sk_";
|
|
587
|
+
var SIGNED_API_KEY_VERSION = "v1";
|
|
588
|
+
function embeddedTenantIdFromApiKey(key) {
|
|
589
|
+
if (!key.startsWith(SIGNED_API_KEY_PREFIX))
|
|
590
|
+
return;
|
|
591
|
+
try {
|
|
592
|
+
const decoded = Buffer.from(key.slice(SIGNED_API_KEY_PREFIX.length), "base64url").toString("utf8");
|
|
593
|
+
const parts = decoded.split(":");
|
|
594
|
+
if (parts.length !== 5 || parts[0] !== SIGNED_API_KEY_VERSION)
|
|
595
|
+
return;
|
|
596
|
+
return parts[2] || undefined;
|
|
597
|
+
} catch {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function resolveIngestUrl2(options = {}) {
|
|
602
|
+
return resolveIngestUrl(options);
|
|
603
|
+
}
|
|
604
|
+
function resolveTenantId(options = {}) {
|
|
605
|
+
const env = options.env ?? process.env;
|
|
606
|
+
return env.MODA_TENANT_ID || resolveTenantIdFromActiveProfile(options) || loadCliConfig(options).tenant_id || undefined;
|
|
607
|
+
}
|
|
608
|
+
var MODA_API_KEY = process.env.MODA_API_KEY;
|
|
609
|
+
var MODA_INGEST_URL = process.env.MODA_INGEST_URL || DEFAULT_INGEST_URL;
|
|
610
|
+
function resolveModaBaseUrl(options = {}) {
|
|
611
|
+
return resolveBaseUrl(options) || PROD_BASE_URL;
|
|
612
|
+
}
|
|
613
|
+
function validateConfig() {
|
|
614
|
+
if (!resolveApiKey()) {
|
|
615
|
+
throw new CliAuthError('No Moda API key found. Set MODA_API_KEY or run "moda init".', {
|
|
616
|
+
reason: "no_stored_credentials",
|
|
617
|
+
login_command: ["moda", "init"]
|
|
618
|
+
}, "Get your API key from https://moda.dev/settings");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/output.ts
|
|
623
|
+
import { randomUUID } from "node:crypto";
|
|
624
|
+
|
|
625
|
+
// ../../packages/harness-snapshot/src/policy.ts
|
|
626
|
+
var MAX_SCANNED_FILE_BYTES = 512 * 1024;
|
|
627
|
+
var DEFAULT_MAX_INVENTORY_FILES = 12000;
|
|
628
|
+
var DEFAULT_MAX_INVENTORY_TEXT_BYTES = 48 * 1024 * 1024;
|
|
629
|
+
var MAX_SNAPSHOT_BODY_BYTES = 4500000;
|
|
630
|
+
var MAX_RECORDED_SKIPS = 1000;
|
|
631
|
+
var SKIPPED_DIRS = new Set([
|
|
632
|
+
".angular",
|
|
633
|
+
".cache",
|
|
634
|
+
".expo",
|
|
635
|
+
".git",
|
|
636
|
+
".hg",
|
|
637
|
+
".idea",
|
|
638
|
+
".svn",
|
|
639
|
+
".next",
|
|
640
|
+
".nuxt",
|
|
641
|
+
".output",
|
|
642
|
+
".parcel-cache",
|
|
643
|
+
".pytest_cache",
|
|
644
|
+
".ruff_cache",
|
|
645
|
+
".serverless",
|
|
646
|
+
".sst",
|
|
647
|
+
".turbo",
|
|
648
|
+
".wrangler",
|
|
649
|
+
".wrangler-shared",
|
|
650
|
+
".moda",
|
|
651
|
+
".moda-test",
|
|
652
|
+
".moda-test-home",
|
|
653
|
+
".moda-cli-workspaces",
|
|
654
|
+
".moda-cli-benchmarks",
|
|
655
|
+
"node_modules",
|
|
656
|
+
"dist",
|
|
657
|
+
"build",
|
|
658
|
+
"coverage",
|
|
659
|
+
"out",
|
|
660
|
+
"target",
|
|
661
|
+
"logs",
|
|
662
|
+
"tmp",
|
|
663
|
+
"temp",
|
|
664
|
+
"__pycache__",
|
|
665
|
+
".venv",
|
|
666
|
+
"venv"
|
|
667
|
+
]);
|
|
668
|
+
var SKIPPED_PATH_PREFIXES = [
|
|
669
|
+
".claude/worktrees",
|
|
670
|
+
".codex/environments"
|
|
671
|
+
];
|
|
672
|
+
function posixNormalize(path) {
|
|
673
|
+
const absolute = path.startsWith("/");
|
|
674
|
+
const parts = path.split("/").filter((part) => part !== "" && part !== ".");
|
|
675
|
+
const normalized = parts.join("/");
|
|
676
|
+
return absolute ? `/${normalized}` : normalized;
|
|
677
|
+
}
|
|
678
|
+
function basename(path) {
|
|
679
|
+
const normalized = posixNormalize(path);
|
|
680
|
+
const index = normalized.lastIndexOf("/");
|
|
681
|
+
return index === -1 ? normalized : normalized.slice(index + 1);
|
|
682
|
+
}
|
|
683
|
+
function extname(path) {
|
|
684
|
+
const name = basename(path);
|
|
685
|
+
const index = name.lastIndexOf(".");
|
|
686
|
+
return index <= 0 ? "" : name.slice(index);
|
|
687
|
+
}
|
|
688
|
+
function isSkippedDir(path) {
|
|
689
|
+
return SKIPPED_DIRS.has(basename(path));
|
|
690
|
+
}
|
|
691
|
+
function isSkippedPathPrefix(path) {
|
|
692
|
+
const normalized = posixNormalize(path);
|
|
693
|
+
return SKIPPED_PATH_PREFIXES.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`));
|
|
694
|
+
}
|
|
695
|
+
function isSourceFile(path) {
|
|
696
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(path);
|
|
697
|
+
}
|
|
698
|
+
function isPackageOrLockfile(path) {
|
|
699
|
+
return path === "package.json" || path === "pyproject.toml" || path === "bun.lock" || path === "bun.lockb" || path === "pnpm-lock.yaml" || path === "yarn.lock";
|
|
700
|
+
}
|
|
701
|
+
function isPromptLikePath(path) {
|
|
702
|
+
return /(^|\/)prompts?\//i.test(path) && /\.(prompt\.)?(md|json|ya?ml)$/i.test(path);
|
|
703
|
+
}
|
|
704
|
+
function isSkillLikePath(path) {
|
|
705
|
+
return /(^|\/)(\.claude\/skills|\.cursor\/rules|skills?)\//i.test(path) && /\.(md|mdc)$/i.test(path);
|
|
706
|
+
}
|
|
707
|
+
function isEvalLikePath(path) {
|
|
708
|
+
return /(^|\/)(evals?|tests?|__tests__)\//i.test(path) && /\.(ts|tsx|js|mjs|py|json|ya?ml)$/i.test(path);
|
|
709
|
+
}
|
|
710
|
+
function isDeploymentLikePath(path) {
|
|
711
|
+
return /(^|\/)(wrangler\.toml|vercel\.json|render\.ya?ml|railway\.json|Dockerfile|docker-compose\.ya?ml)$/i.test(path) || /^\.github\/workflows\/[^/]+\.ya?ml$/i.test(path);
|
|
712
|
+
}
|
|
713
|
+
function isDeveloperAgentConfigPath(path) {
|
|
714
|
+
return path === ".claude/settings.json" || path === ".codex/hooks.json" || path === ".cursor/hooks.json";
|
|
715
|
+
}
|
|
716
|
+
function isReadmeOrAgents(path) {
|
|
717
|
+
const name = basename(path);
|
|
718
|
+
return name === "README.md" || name === "AGENTS.md";
|
|
719
|
+
}
|
|
720
|
+
function shouldInventoryFile(path) {
|
|
721
|
+
const normalized = posixNormalize(path);
|
|
722
|
+
const name = basename(normalized);
|
|
723
|
+
const extension = extname(normalized).toLowerCase();
|
|
724
|
+
if (isSourceFile(normalized))
|
|
725
|
+
return true;
|
|
726
|
+
if (isPackageOrLockfile(normalized))
|
|
727
|
+
return true;
|
|
728
|
+
if (isPromptLikePath(normalized))
|
|
729
|
+
return true;
|
|
730
|
+
if (isSkillLikePath(normalized))
|
|
731
|
+
return true;
|
|
732
|
+
if (isEvalLikePath(normalized))
|
|
733
|
+
return true;
|
|
734
|
+
if (isDeploymentLikePath(normalized))
|
|
735
|
+
return true;
|
|
736
|
+
if (isDeveloperAgentConfigPath(normalized))
|
|
737
|
+
return true;
|
|
738
|
+
if (isReadmeOrAgents(normalized))
|
|
739
|
+
return true;
|
|
740
|
+
return extension === ".toml" && (name === "pyproject.toml" || name === "wrangler.toml");
|
|
741
|
+
}
|
|
742
|
+
function isTextFile(path) {
|
|
743
|
+
const extension = extname(path).toLowerCase();
|
|
744
|
+
return [
|
|
745
|
+
"",
|
|
746
|
+
".cjs",
|
|
747
|
+
".js",
|
|
748
|
+
".json",
|
|
749
|
+
".jsx",
|
|
750
|
+
".md",
|
|
751
|
+
".mdc",
|
|
752
|
+
".mjs",
|
|
753
|
+
".py",
|
|
754
|
+
".toml",
|
|
755
|
+
".ts",
|
|
756
|
+
".tsx",
|
|
757
|
+
".txt",
|
|
758
|
+
".yaml",
|
|
759
|
+
".yml"
|
|
760
|
+
].includes(extension);
|
|
761
|
+
}
|
|
762
|
+
function isSecretishPath(path) {
|
|
763
|
+
const name = basename(path).toLowerCase();
|
|
764
|
+
return name === ".env" || name.startsWith(".env.") || name.includes("secret") || name.includes("private-key") || name.endsWith(".pem");
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ../../packages/harness-snapshot/src/redact.ts
|
|
768
|
+
var REDACTED = "[REDACTED]";
|
|
769
|
+
var MAX_DEPTH = 256;
|
|
770
|
+
var SECRET_KEYS = new Set([
|
|
771
|
+
"api_key",
|
|
772
|
+
"apikey",
|
|
773
|
+
"token",
|
|
774
|
+
"access_token",
|
|
775
|
+
"authorization",
|
|
776
|
+
"secret",
|
|
777
|
+
"password",
|
|
778
|
+
"cli_token"
|
|
779
|
+
]);
|
|
780
|
+
var SECRET_PATTERNS = [
|
|
781
|
+
{ pattern: /\bmoda_sk_[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
|
|
782
|
+
{ pattern: /\bmoda_ct_[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
|
|
783
|
+
{ pattern: /\bBearer\s+([A-Za-z0-9._~+/-]+=*)\b/gi, prefixLength: 8 },
|
|
784
|
+
{ pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
|
|
785
|
+
{ pattern: /\bsk-ant-api\d{2}-[A-Za-z0-9_-]{20,}\b/g, prefixLength: 12 },
|
|
786
|
+
{ pattern: /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}\b/g, prefixLength: 8 },
|
|
787
|
+
{ pattern: /\bAKIA[A-Z0-9]{16}\b/g, prefixLength: 8 },
|
|
788
|
+
{ pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, prefixLength: 8 },
|
|
789
|
+
{ pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g, prefixLength: 8 },
|
|
790
|
+
{ pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, prefixLength: 8 }
|
|
791
|
+
];
|
|
792
|
+
function redactDeep(value) {
|
|
793
|
+
return redactValue(value, 0, new WeakSet);
|
|
794
|
+
}
|
|
795
|
+
function redactValue(value, depth, seen) {
|
|
796
|
+
if (typeof value === "string")
|
|
797
|
+
return redactString(value);
|
|
798
|
+
if (value === null || typeof value !== "object")
|
|
799
|
+
return value;
|
|
800
|
+
if (depth >= MAX_DEPTH)
|
|
801
|
+
return "[MaxDepth]";
|
|
802
|
+
if (seen.has(value))
|
|
803
|
+
return "[Circular]";
|
|
804
|
+
seen.add(value);
|
|
805
|
+
if (Array.isArray(value)) {
|
|
806
|
+
try {
|
|
807
|
+
return value.map((item) => redactValue(item, depth + 1, seen));
|
|
808
|
+
} finally {
|
|
809
|
+
seen.delete(value);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const out = {};
|
|
813
|
+
try {
|
|
814
|
+
for (const [key, item] of Object.entries(value)) {
|
|
815
|
+
out[key] = shouldRedactKey(key) ? redactSecretValue(item) : redactValue(item, depth + 1, seen);
|
|
816
|
+
}
|
|
817
|
+
} finally {
|
|
818
|
+
seen.delete(value);
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
function shouldRedactKey(key) {
|
|
823
|
+
return SECRET_KEYS.has(key.replace(/[-\s]/g, "_").toLowerCase());
|
|
824
|
+
}
|
|
825
|
+
function redactSecretValue(value) {
|
|
826
|
+
if (typeof value !== "string")
|
|
827
|
+
return REDACTED;
|
|
828
|
+
const redacted = redactString(value);
|
|
829
|
+
if (redacted !== value)
|
|
830
|
+
return redacted;
|
|
831
|
+
if (/^(configured|missing|not_configured|none|present|absent)$/i.test(value))
|
|
832
|
+
return value;
|
|
833
|
+
return REDACTED;
|
|
834
|
+
}
|
|
835
|
+
function redactStringWithCount(input) {
|
|
836
|
+
let text = input;
|
|
837
|
+
let redactionCount = 0;
|
|
838
|
+
for (const { pattern, prefixLength } of SECRET_PATTERNS) {
|
|
839
|
+
text = text.replace(pattern, (match) => {
|
|
840
|
+
redactionCount += 1;
|
|
841
|
+
if (/^Bearer\s+/i.test(match)) {
|
|
842
|
+
const token = match.replace(/^Bearer\s+/i, "");
|
|
843
|
+
return `Bearer ${token.slice(0, prefixLength)}...[REDACTED]`;
|
|
844
|
+
}
|
|
845
|
+
return `${match.slice(0, prefixLength)}...[REDACTED]`;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
return { text, redactionCount };
|
|
849
|
+
}
|
|
850
|
+
function redactString(input) {
|
|
851
|
+
return redactStringWithCount(input).text;
|
|
852
|
+
}
|
|
853
|
+
function redactSnapshotText(text) {
|
|
854
|
+
const parts = text.split(/(\r\n|\r|\n)/);
|
|
855
|
+
let redactionCount = 0;
|
|
856
|
+
for (let index = 0;index < parts.length; index += 2) {
|
|
857
|
+
const redacted = redactStringWithCount(parts[index] ?? "");
|
|
858
|
+
parts[index] = redacted.text;
|
|
859
|
+
redactionCount += redacted.redactionCount;
|
|
860
|
+
}
|
|
861
|
+
return { text: parts.join(""), redactionCount };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ../../packages/harness-snapshot/src/pack.ts
|
|
865
|
+
function utf8ByteLength(value) {
|
|
866
|
+
let bytes = 0;
|
|
867
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
868
|
+
const code = value.charCodeAt(index);
|
|
869
|
+
if (code <= 127) {
|
|
870
|
+
bytes += 1;
|
|
871
|
+
} else if (code <= 2047) {
|
|
872
|
+
bytes += 2;
|
|
873
|
+
} else if (code >= 55296 && code <= 56319) {
|
|
874
|
+
const next = value.charCodeAt(index + 1);
|
|
875
|
+
if (next >= 56320 && next <= 57343) {
|
|
876
|
+
bytes += 4;
|
|
877
|
+
index += 1;
|
|
878
|
+
} else {
|
|
879
|
+
bytes += 3;
|
|
880
|
+
}
|
|
881
|
+
} else {
|
|
882
|
+
bytes += 3;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return bytes;
|
|
886
|
+
}
|
|
887
|
+
function isValidRelativePath(path) {
|
|
888
|
+
if (path.length === 0 || path.startsWith("/") || path.includes("\\"))
|
|
889
|
+
return false;
|
|
890
|
+
return !path.split("/").some((part) => part === "..");
|
|
891
|
+
}
|
|
892
|
+
function packSnapshot(inputs, opts) {
|
|
893
|
+
const maxFiles = opts.maxFiles ?? DEFAULT_MAX_INVENTORY_FILES;
|
|
894
|
+
const maxTextBytes = opts.maxTextBytes ?? DEFAULT_MAX_INVENTORY_TEXT_BYTES;
|
|
895
|
+
const maxSnapshotBytes = opts.maxSnapshotBytes ?? MAX_SNAPSHOT_BODY_BYTES;
|
|
896
|
+
const files = [];
|
|
897
|
+
const skips = [];
|
|
898
|
+
let inventoryTextBytes = 0;
|
|
899
|
+
let serializedBytes = 0;
|
|
900
|
+
let byteCount = 0;
|
|
901
|
+
let truncated = false;
|
|
902
|
+
const recordSkip = (path, reason) => {
|
|
903
|
+
if (skips.length < MAX_RECORDED_SKIPS)
|
|
904
|
+
skips.push({ path, reason });
|
|
905
|
+
};
|
|
906
|
+
const sorted = [...inputs].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
907
|
+
for (const input of sorted) {
|
|
908
|
+
if (!isValidRelativePath(input.path)) {
|
|
909
|
+
recordSkip(input.path, "invalid relative path");
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const path = posixNormalize(input.path);
|
|
913
|
+
if (input.isSymlink) {
|
|
914
|
+
recordSkip(path, "symlink skipped for safety");
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (isSecretishPath(path)) {
|
|
918
|
+
recordSkip(path, "secret-bearing path");
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (!shouldInventoryFile(path)) {
|
|
922
|
+
recordSkip(path, "not an inventory file");
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (!isTextFile(path)) {
|
|
926
|
+
recordSkip(path, "non-text file");
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (input.byteSize > MAX_SCANNED_FILE_BYTES) {
|
|
930
|
+
recordSkip(path, "file too large");
|
|
931
|
+
truncated = true;
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (files.length >= maxFiles || inventoryTextBytes + input.byteSize > maxTextBytes) {
|
|
935
|
+
recordSkip(path, "inventory budget");
|
|
936
|
+
truncated = true;
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
const text = opts.redact === false ? input.text : redactSnapshotText(input.text).text;
|
|
940
|
+
const file = { path, text };
|
|
941
|
+
const serializedCost = utf8ByteLength(JSON.stringify(file)) + 1;
|
|
942
|
+
if (serializedBytes + serializedCost > maxSnapshotBytes) {
|
|
943
|
+
recordSkip(path, "snapshot budget");
|
|
944
|
+
truncated = true;
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
files.push(file);
|
|
948
|
+
inventoryTextBytes += input.byteSize;
|
|
949
|
+
serializedBytes += serializedCost;
|
|
950
|
+
byteCount += utf8ByteLength(text);
|
|
951
|
+
}
|
|
952
|
+
return {
|
|
953
|
+
snapshot: {
|
|
954
|
+
repo: { name: opts.repoName, root: "." },
|
|
955
|
+
files,
|
|
956
|
+
truncated,
|
|
957
|
+
fileCount: files.length,
|
|
958
|
+
byteCount,
|
|
959
|
+
...opts.gitCommit ? { gitCommit: opts.gitCommit } : {}
|
|
960
|
+
},
|
|
961
|
+
skips
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
// src/output.ts
|
|
965
|
+
var AGENT_SCHEMA_VERSION = "moda.agent.v1";
|
|
966
|
+
var AGENT_EVENT_SCHEMA_VERSION = "moda.agent_event.v1";
|
|
967
|
+
var FEEDBACK_TIP = 'Help us improve! If a result in this response looks wrong or is missing data, flag it: moda feedback "<describe what looked wrong>" [--category=...] [--conversation-id=... | --cluster-id=... | --tool-name=...]. Only submit genuine observations.';
|
|
968
|
+
var EXIT_OK = 0;
|
|
969
|
+
var EXIT_ERROR = 1;
|
|
970
|
+
var EXIT_DEGRADED = 3;
|
|
971
|
+
var EXIT_AUTH_REQUIRED = 4;
|
|
972
|
+
var EXIT_INPUT_REQUIRED = 5;
|
|
973
|
+
var EXIT_CANCELLED = 130;
|
|
974
|
+
var AGENT_ERROR_CODES = [
|
|
975
|
+
"input_error",
|
|
976
|
+
"api_error",
|
|
977
|
+
"network_error",
|
|
978
|
+
"auth_required",
|
|
979
|
+
"input_required",
|
|
980
|
+
"cancelled",
|
|
981
|
+
"command_error",
|
|
982
|
+
"access_denied",
|
|
983
|
+
"unknown_error",
|
|
984
|
+
"cloud_unavailable"
|
|
985
|
+
];
|
|
986
|
+
var FALLBACK_RUN_ID = randomUUID();
|
|
987
|
+
var fallbackSeq = 0;
|
|
988
|
+
var stdoutClosed = false;
|
|
989
|
+
var stderrClosed = false;
|
|
990
|
+
process.stdout.on("error", (error) => {
|
|
991
|
+
if (error.code === "EPIPE") {
|
|
992
|
+
stdoutClosed = true;
|
|
993
|
+
process.exitCode = EXIT_CANCELLED;
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
throw error;
|
|
997
|
+
});
|
|
998
|
+
process.stderr.on("error", (error) => {
|
|
999
|
+
if (error.code === "EPIPE") {
|
|
1000
|
+
stderrClosed = true;
|
|
1001
|
+
process.exitCode = EXIT_CANCELLED;
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
throw error;
|
|
1005
|
+
});
|
|
1006
|
+
function wasOutputCancelled() {
|
|
1007
|
+
return stdoutClosed || stderrClosed;
|
|
1008
|
+
}
|
|
1009
|
+
function safeWriteStdout(text) {
|
|
1010
|
+
if (stdoutClosed)
|
|
1011
|
+
return false;
|
|
1012
|
+
try {
|
|
1013
|
+
if (false) {} else {
|
|
1014
|
+
process.stdout.write(text);
|
|
1015
|
+
}
|
|
1016
|
+
return true;
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
if (error.code === "EPIPE") {
|
|
1019
|
+
stdoutClosed = true;
|
|
1020
|
+
process.exitCode = EXIT_CANCELLED;
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
throw error;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function safeWriteStderr(text) {
|
|
1027
|
+
if (stderrClosed)
|
|
1028
|
+
return false;
|
|
1029
|
+
try {
|
|
1030
|
+
if (false) {} else {
|
|
1031
|
+
process.stderr.write(text);
|
|
1032
|
+
}
|
|
1033
|
+
return true;
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
if (error.code === "EPIPE") {
|
|
1036
|
+
stderrClosed = true;
|
|
1037
|
+
process.exitCode = EXIT_CANCELLED;
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
throw error;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function explicitModeFromFlags(flags) {
|
|
1044
|
+
if (flags.agent === "true" && (flags.stream === "true" || flags.format === "ndjson"))
|
|
1045
|
+
return "agent-stream";
|
|
1046
|
+
if (flags.agent === "true")
|
|
1047
|
+
return "agent";
|
|
1048
|
+
if (flags.json === "true")
|
|
1049
|
+
return "json";
|
|
1050
|
+
if (flags.human === "true" || flags.prose === "true")
|
|
1051
|
+
return "human";
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
function outputModeFromFlags(flags) {
|
|
1055
|
+
return explicitModeFromFlags(flags) ?? "human";
|
|
1056
|
+
}
|
|
1057
|
+
function outputModeFromEnv(env) {
|
|
1058
|
+
const fmt = (env.MODA_FORMAT ?? "").trim().toLowerCase();
|
|
1059
|
+
if (fmt === "human" || fmt === "json" || fmt === "agent")
|
|
1060
|
+
return fmt;
|
|
1061
|
+
if (fmt === "agent-stream" || fmt === "ndjson")
|
|
1062
|
+
return "agent-stream";
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
function truthyEnv(value) {
|
|
1066
|
+
return value != null && value !== "" && value !== "0" && value.toLowerCase() !== "false";
|
|
1067
|
+
}
|
|
1068
|
+
function isAgentEnvironment(env) {
|
|
1069
|
+
return truthyEnv(env.CLAUDECODE) || truthyEnv(env.CURSOR) || truthyEnv(env.CI) || truthyEnv(env.MODA_AGENT);
|
|
1070
|
+
}
|
|
1071
|
+
function resolveOutputMode(flags, opts = {}) {
|
|
1072
|
+
const explicit = explicitModeFromFlags(flags);
|
|
1073
|
+
if (explicit)
|
|
1074
|
+
return explicit;
|
|
1075
|
+
const env = opts.env ?? process.env;
|
|
1076
|
+
const fromEnv = outputModeFromEnv(env);
|
|
1077
|
+
if (fromEnv)
|
|
1078
|
+
return fromEnv;
|
|
1079
|
+
if (isAgentEnvironment(env))
|
|
1080
|
+
return "agent";
|
|
1081
|
+
const isTTY = opts.isTTY ?? Boolean(process.stdout.isTTY);
|
|
1082
|
+
if (!isTTY)
|
|
1083
|
+
return "agent";
|
|
1084
|
+
return "human";
|
|
1085
|
+
}
|
|
1086
|
+
function createCliOutput(options) {
|
|
1087
|
+
const state = {
|
|
1088
|
+
runId: options.runId ?? randomUUID(),
|
|
1089
|
+
seq: 0,
|
|
1090
|
+
started: false,
|
|
1091
|
+
terminal: false,
|
|
1092
|
+
startedAt: Date.now(),
|
|
1093
|
+
lastEventAt: Date.now()
|
|
1094
|
+
};
|
|
1095
|
+
const emitEvent = (event) => {
|
|
1096
|
+
if (options.quiet || options.mode !== "agent-stream")
|
|
1097
|
+
return;
|
|
1098
|
+
if (state.terminal)
|
|
1099
|
+
return;
|
|
1100
|
+
if (event.event === "started" && state.started)
|
|
1101
|
+
return;
|
|
1102
|
+
if (event.event === "started")
|
|
1103
|
+
state.started = true;
|
|
1104
|
+
if (isTerminalEvent(event.event)) {
|
|
1105
|
+
state.terminal = true;
|
|
1106
|
+
clearHeartbeat(state);
|
|
1107
|
+
}
|
|
1108
|
+
state.lastEventAt = Date.now();
|
|
1109
|
+
safeWriteStdout(`${JSON.stringify(createAgentEvent(options.command, event, state))}
|
|
1110
|
+
`);
|
|
1111
|
+
};
|
|
1112
|
+
const ensureStarted = () => {
|
|
1113
|
+
if (options.mode !== "agent-stream" || state.started || options.quiet)
|
|
1114
|
+
return;
|
|
1115
|
+
startHeartbeat(options.command, state);
|
|
1116
|
+
emitEvent({
|
|
1117
|
+
event: "started",
|
|
1118
|
+
phase: options.command,
|
|
1119
|
+
message: `Starting ${options.command}.`,
|
|
1120
|
+
elapsed_ms: 0
|
|
1121
|
+
});
|
|
1122
|
+
};
|
|
1123
|
+
const writeEnvelope = (data, writeOptions = {}) => {
|
|
1124
|
+
return createAgentEnvelope({
|
|
1125
|
+
command: options.command,
|
|
1126
|
+
status: writeOptions.status === "error" ? "error" : writeOptions.status === "degraded" ? "degraded" : "ok",
|
|
1127
|
+
data,
|
|
1128
|
+
meta: {
|
|
1129
|
+
...writeOptions.meta ?? {},
|
|
1130
|
+
run_id: state.runId,
|
|
1131
|
+
...options.command === "feedback" ? {} : { tip: FEEDBACK_TIP }
|
|
1132
|
+
},
|
|
1133
|
+
summary: writeOptions.summary,
|
|
1134
|
+
findings: writeOptions.findings,
|
|
1135
|
+
evidenceRefs: writeOptions.evidenceRefs,
|
|
1136
|
+
recommendedActions: writeOptions.recommendedActions,
|
|
1137
|
+
nextCommands: writeOptions.nextCommands,
|
|
1138
|
+
artifacts: writeOptions.artifacts,
|
|
1139
|
+
warnings: writeOptions.warnings,
|
|
1140
|
+
errors: writeOptions.errors
|
|
1141
|
+
});
|
|
1142
|
+
};
|
|
1143
|
+
const output = {
|
|
1144
|
+
mode: options.mode,
|
|
1145
|
+
quiet: options.quiet === true,
|
|
1146
|
+
writeData(data, writeOptions = {}) {
|
|
1147
|
+
if (options.quiet)
|
|
1148
|
+
return;
|
|
1149
|
+
if (options.mode === "agent" || options.mode === "agent-stream") {
|
|
1150
|
+
if (state.terminal)
|
|
1151
|
+
return;
|
|
1152
|
+
const envelope = writeEnvelope(data, writeOptions);
|
|
1153
|
+
if (options.mode === "agent-stream") {
|
|
1154
|
+
ensureStarted();
|
|
1155
|
+
emitEvent({
|
|
1156
|
+
event: envelope.status === "error" ? "error" : "completed",
|
|
1157
|
+
status: envelope.status,
|
|
1158
|
+
result: envelope,
|
|
1159
|
+
...envelope.errors[0] ? { error: envelope.errors[0] } : {}
|
|
1160
|
+
});
|
|
1161
|
+
} else {
|
|
1162
|
+
state.terminal = true;
|
|
1163
|
+
safeWriteStdout(`${JSON.stringify(envelope)}
|
|
1164
|
+
`);
|
|
1165
|
+
}
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
safeWriteStdout(`${JSON.stringify(redactDeep(data), null, 2)}
|
|
1169
|
+
`);
|
|
1170
|
+
},
|
|
1171
|
+
writeEvent(event) {
|
|
1172
|
+
if (options.quiet)
|
|
1173
|
+
return;
|
|
1174
|
+
if (options.mode !== "agent-stream")
|
|
1175
|
+
return;
|
|
1176
|
+
if (event.event === "started") {
|
|
1177
|
+
startHeartbeat(options.command, state);
|
|
1178
|
+
emitEvent(event);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
ensureStarted();
|
|
1182
|
+
emitEvent(event);
|
|
1183
|
+
},
|
|
1184
|
+
writeError(error) {
|
|
1185
|
+
if (options.quiet || state.terminal)
|
|
1186
|
+
return;
|
|
1187
|
+
const agentError = errorToAgentError(error);
|
|
1188
|
+
const envelope = createAgentErrorEnvelope(options.command || "unknown", agentError, state.runId);
|
|
1189
|
+
if (options.mode === "agent-stream") {
|
|
1190
|
+
ensureStarted();
|
|
1191
|
+
if (error instanceof CliInputRequiredError) {
|
|
1192
|
+
emitEvent({
|
|
1193
|
+
event: "needs_input",
|
|
1194
|
+
status: "error",
|
|
1195
|
+
message: error.message,
|
|
1196
|
+
error: agentError,
|
|
1197
|
+
input_request: error.input_request,
|
|
1198
|
+
result: envelope
|
|
1199
|
+
});
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (error instanceof CliAuthError) {
|
|
1203
|
+
emitEvent({
|
|
1204
|
+
event: "auth_required",
|
|
1205
|
+
status: "error",
|
|
1206
|
+
message: error.message,
|
|
1207
|
+
error: agentError,
|
|
1208
|
+
auth: error.auth,
|
|
1209
|
+
result: envelope
|
|
1210
|
+
});
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (error instanceof CliCancellationError) {
|
|
1214
|
+
emitEvent({
|
|
1215
|
+
event: "cancelled",
|
|
1216
|
+
status: "error",
|
|
1217
|
+
message: error.message,
|
|
1218
|
+
error: agentError,
|
|
1219
|
+
result: envelope
|
|
1220
|
+
});
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
emitEvent({
|
|
1224
|
+
event: "error",
|
|
1225
|
+
status: "error",
|
|
1226
|
+
message: agentError.message,
|
|
1227
|
+
error: agentError,
|
|
1228
|
+
result: envelope
|
|
1229
|
+
});
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (options.mode === "agent" || options.mode === "json") {
|
|
1233
|
+
state.terminal = true;
|
|
1234
|
+
safeWriteStdout(`${JSON.stringify(envelope)}
|
|
1235
|
+
`);
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
writeCancelled(reason = "Command cancelled.") {
|
|
1239
|
+
output.writeError(new CliCancellationError(reason));
|
|
1240
|
+
},
|
|
1241
|
+
hasTerminalOutput() {
|
|
1242
|
+
return state.terminal;
|
|
1243
|
+
},
|
|
1244
|
+
close() {
|
|
1245
|
+
clearHeartbeat(state);
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
return output;
|
|
1249
|
+
}
|
|
1250
|
+
function createAgentEnvelope(options) {
|
|
1251
|
+
const safeData = redactDeep(options.data);
|
|
1252
|
+
const bounded = boundForAgent(safeData);
|
|
1253
|
+
const envelope = {
|
|
1254
|
+
schema_version: AGENT_SCHEMA_VERSION,
|
|
1255
|
+
command: options.command,
|
|
1256
|
+
status: options.status,
|
|
1257
|
+
generated_at: new Date().toISOString(),
|
|
1258
|
+
...options.profile ? { profile: options.profile } : {},
|
|
1259
|
+
summary: normalizeSummary(options.summary, options.command, options.status, safeData),
|
|
1260
|
+
findings: options.findings ?? [],
|
|
1261
|
+
evidence_refs: options.evidenceRefs ?? [],
|
|
1262
|
+
recommended_actions: options.recommendedActions ?? [],
|
|
1263
|
+
next_commands: normalizeNextCommands(options.nextCommands ?? []),
|
|
1264
|
+
artifacts: options.artifacts ?? [],
|
|
1265
|
+
warnings: options.warnings ?? inferWarnings(safeData),
|
|
1266
|
+
errors: options.errors ?? [],
|
|
1267
|
+
data: bounded.data,
|
|
1268
|
+
...options.meta ? { meta: options.meta } : {},
|
|
1269
|
+
...bounded.truncation ? { truncation: bounded.truncation } : {}
|
|
1270
|
+
};
|
|
1271
|
+
return redactDeep(envelope);
|
|
1272
|
+
}
|
|
1273
|
+
function createAgentErrorEnvelope(command, error, runId) {
|
|
1274
|
+
return createAgentEnvelope({
|
|
1275
|
+
command,
|
|
1276
|
+
status: "error",
|
|
1277
|
+
data: null,
|
|
1278
|
+
meta: runId ? { run_id: runId } : undefined,
|
|
1279
|
+
summary: {
|
|
1280
|
+
text: error.message,
|
|
1281
|
+
confidence: "high"
|
|
1282
|
+
},
|
|
1283
|
+
errors: [error],
|
|
1284
|
+
nextCommands: (error.suggested_commands ?? []).map((commandText) => ({
|
|
1285
|
+
command: commandText,
|
|
1286
|
+
purpose: "Suggested recovery command.",
|
|
1287
|
+
mutability: commandText.includes(" sync") || commandText.startsWith("moda sync") ? "sync" : "read",
|
|
1288
|
+
requires_approval: commandText.includes(" --yes") || commandText.includes(" sync")
|
|
1289
|
+
}))
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
function createAgentEvent(command, event, state) {
|
|
1293
|
+
const stamped = {
|
|
1294
|
+
schema_version: AGENT_EVENT_SCHEMA_VERSION,
|
|
1295
|
+
command,
|
|
1296
|
+
generated_at: new Date().toISOString(),
|
|
1297
|
+
run_id: state?.runId ?? FALLBACK_RUN_ID,
|
|
1298
|
+
seq: state ? ++state.seq : ++fallbackSeq,
|
|
1299
|
+
event_version: event.event_version ?? 1,
|
|
1300
|
+
...event
|
|
1301
|
+
};
|
|
1302
|
+
return redactDeep(stamped);
|
|
1303
|
+
}
|
|
1304
|
+
function errorToAgentError(error) {
|
|
1305
|
+
if (error instanceof CliInputRequiredError) {
|
|
1306
|
+
return {
|
|
1307
|
+
code: "input_required",
|
|
1308
|
+
message: error.message,
|
|
1309
|
+
cause: error.usage,
|
|
1310
|
+
suggested_commands: error.input_request.resume_command ? [error.input_request.resume_command.join(" ")] : undefined,
|
|
1311
|
+
input_request: error.input_request,
|
|
1312
|
+
blocking: true
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
if (error instanceof CliAuthError) {
|
|
1316
|
+
const suggested = [error.auth.login_command.join(" ")];
|
|
1317
|
+
if (error.auth.resume_command)
|
|
1318
|
+
suggested.push(error.auth.resume_command.join(" "));
|
|
1319
|
+
return {
|
|
1320
|
+
code: "auth_required",
|
|
1321
|
+
message: error.message,
|
|
1322
|
+
cause: error.usage,
|
|
1323
|
+
suggested_commands: suggested,
|
|
1324
|
+
auth: error.auth,
|
|
1325
|
+
blocking: true
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
if (error instanceof CliCancellationError) {
|
|
1329
|
+
return {
|
|
1330
|
+
code: "cancelled",
|
|
1331
|
+
message: error.message,
|
|
1332
|
+
blocking: true
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
if (error instanceof CliAccessError) {
|
|
1336
|
+
return {
|
|
1337
|
+
code: "access_denied",
|
|
1338
|
+
message: error.message,
|
|
1339
|
+
cause: error.guidance,
|
|
1340
|
+
blocking: true
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
if (error instanceof Error) {
|
|
1344
|
+
if (error instanceof CliInputError || error.name === "ZodError") {
|
|
1345
|
+
return {
|
|
1346
|
+
code: "input_error",
|
|
1347
|
+
message: error.message,
|
|
1348
|
+
cause: "usage" in error && typeof error.usage === "string" ? error.usage : undefined,
|
|
1349
|
+
suggested_commands: "usage" in error && typeof error.usage === "string" ? extractSuggestedCommands(error.usage) : undefined,
|
|
1350
|
+
blocking: true
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
if (isApiLikeError(error)) {
|
|
1354
|
+
const statusCode = "statusCode" in error ? error.statusCode : undefined;
|
|
1355
|
+
return {
|
|
1356
|
+
code: "api_error",
|
|
1357
|
+
message: error.message,
|
|
1358
|
+
cause: typeof statusCode === "number" ? `HTTP ${statusCode}` : undefined,
|
|
1359
|
+
blocking: true
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
if (isNetworkLikeError(error)) {
|
|
1363
|
+
return {
|
|
1364
|
+
code: "network_error",
|
|
1365
|
+
message: error.message,
|
|
1366
|
+
blocking: true
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
return {
|
|
1370
|
+
code: "command_error",
|
|
1371
|
+
message: error.message,
|
|
1372
|
+
cause: "usage" in error && typeof error.usage === "string" ? error.usage : undefined,
|
|
1373
|
+
suggested_commands: "usage" in error && typeof error.usage === "string" ? extractSuggestedCommands(error.usage) : undefined,
|
|
1374
|
+
blocking: true
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
return {
|
|
1378
|
+
code: "unknown_error",
|
|
1379
|
+
message: String(error),
|
|
1380
|
+
blocking: true
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function isTerminalEvent(event) {
|
|
1384
|
+
return event === "completed" || event === "error" || event === "cancelled" || event === "needs_input" || event === "auth_required";
|
|
1385
|
+
}
|
|
1386
|
+
function startHeartbeat(command, state) {
|
|
1387
|
+
if (state.heartbeat)
|
|
1388
|
+
return;
|
|
1389
|
+
const intervalMs = Number(process.env.MODA_HEARTBEAT_MS ?? 1e4);
|
|
1390
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0)
|
|
1391
|
+
return;
|
|
1392
|
+
state.heartbeat = setInterval(() => {
|
|
1393
|
+
if (state.terminal || stdoutClosed)
|
|
1394
|
+
return;
|
|
1395
|
+
const now = Date.now();
|
|
1396
|
+
if (now - state.lastEventAt < intervalMs)
|
|
1397
|
+
return;
|
|
1398
|
+
state.lastEventAt = now;
|
|
1399
|
+
safeWriteStdout(`${JSON.stringify(createAgentEvent(command, {
|
|
1400
|
+
event: "heartbeat",
|
|
1401
|
+
elapsed_ms: now - state.startedAt
|
|
1402
|
+
}, state))}
|
|
1403
|
+
`);
|
|
1404
|
+
}, intervalMs);
|
|
1405
|
+
state.heartbeat.unref();
|
|
1406
|
+
}
|
|
1407
|
+
function clearHeartbeat(state) {
|
|
1408
|
+
if (!state.heartbeat)
|
|
1409
|
+
return;
|
|
1410
|
+
clearInterval(state.heartbeat);
|
|
1411
|
+
state.heartbeat = undefined;
|
|
1412
|
+
}
|
|
1413
|
+
function normalizeSummary(summary, command, status, data) {
|
|
1414
|
+
if (typeof summary === "string") {
|
|
1415
|
+
return { text: summary, confidence: "high" };
|
|
1416
|
+
}
|
|
1417
|
+
if (summary?.text) {
|
|
1418
|
+
return {
|
|
1419
|
+
text: summary.text,
|
|
1420
|
+
confidence: summary.confidence ?? "high"
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
return inferSummary(command, status, data);
|
|
1424
|
+
}
|
|
1425
|
+
function inferSummary(command, status, data) {
|
|
1426
|
+
const record = asRecord(data);
|
|
1427
|
+
if (status === "error") {
|
|
1428
|
+
return { text: `${command} failed.`, confidence: "high" };
|
|
1429
|
+
}
|
|
1430
|
+
const summary = asRecord(record?.summary);
|
|
1431
|
+
if (summary) {
|
|
1432
|
+
const statusValue = typeof summary.status === "string" ? summary.status : undefined;
|
|
1433
|
+
const fail = typeof summary.fail === "number" ? summary.fail : undefined;
|
|
1434
|
+
const warning = typeof summary.warning === "number" ? summary.warning : undefined;
|
|
1435
|
+
if (statusValue && fail !== undefined && warning !== undefined) {
|
|
1436
|
+
return {
|
|
1437
|
+
text: `${command} completed with status ${statusValue}: ${fail} failing checks and ${warning} warnings.`,
|
|
1438
|
+
confidence: "high"
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
const runtimeAgentCount = typeof summary.runtimeAgentCount === "number" ? summary.runtimeAgentCount : undefined;
|
|
1442
|
+
if (runtimeAgentCount !== undefined) {
|
|
1443
|
+
return {
|
|
1444
|
+
text: `${command} completed. Harness summary includes ${runtimeAgentCount} runtime agent(s).`,
|
|
1445
|
+
confidence: "high"
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (typeof record?.status === "string") {
|
|
1450
|
+
return { text: `${command} completed with status ${record.status}.`, confidence: "high" };
|
|
1451
|
+
}
|
|
1452
|
+
return { text: `${command} completed.`, confidence: "medium" };
|
|
1453
|
+
}
|
|
1454
|
+
function normalizeNextCommands(commands) {
|
|
1455
|
+
return commands.map((command) => {
|
|
1456
|
+
if (typeof command !== "string")
|
|
1457
|
+
return command;
|
|
1458
|
+
return {
|
|
1459
|
+
command,
|
|
1460
|
+
purpose: "Suggested next Moda command.",
|
|
1461
|
+
mutability: inferMutability(command),
|
|
1462
|
+
requires_approval: command.includes(" --yes") || command.includes(" sync")
|
|
1463
|
+
};
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
function inferMutability(command) {
|
|
1467
|
+
if (/\b(sync|approve|init|install|pull|gen|remove|promote)\b/.test(command)) {
|
|
1468
|
+
if (/\bsync\b/.test(command))
|
|
1469
|
+
return "sync";
|
|
1470
|
+
return "write";
|
|
1471
|
+
}
|
|
1472
|
+
return "read";
|
|
1473
|
+
}
|
|
1474
|
+
function inferWarnings(data) {
|
|
1475
|
+
const record = asRecord(data);
|
|
1476
|
+
const direct = record?.warnings;
|
|
1477
|
+
if (Array.isArray(direct))
|
|
1478
|
+
return direct.filter((item) => typeof item === "string");
|
|
1479
|
+
const validation = asRecord(record?.validation);
|
|
1480
|
+
if (Array.isArray(validation?.warnings)) {
|
|
1481
|
+
return validation.warnings.filter((item) => typeof item === "string");
|
|
1482
|
+
}
|
|
1483
|
+
return [];
|
|
1484
|
+
}
|
|
1485
|
+
function boundForAgent(data) {
|
|
1486
|
+
const maxBytes = Number(process.env.MODA_AGENT_MAX_OUTPUT_BYTES ?? 200000);
|
|
1487
|
+
const serialized = JSON.stringify(data);
|
|
1488
|
+
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
1489
|
+
if (bytes <= maxBytes)
|
|
1490
|
+
return { data };
|
|
1491
|
+
const preview = serialized.slice(0, Math.max(0, maxBytes - 256));
|
|
1492
|
+
return {
|
|
1493
|
+
data: {
|
|
1494
|
+
truncated: true,
|
|
1495
|
+
original_bytes: bytes,
|
|
1496
|
+
max_bytes: maxBytes,
|
|
1497
|
+
preview
|
|
1498
|
+
},
|
|
1499
|
+
truncation: {
|
|
1500
|
+
data: {
|
|
1501
|
+
original_bytes: bytes,
|
|
1502
|
+
max_bytes: maxBytes,
|
|
1503
|
+
reason: "agent_output_byte_limit"
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
function asRecord(value) {
|
|
1509
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
1510
|
+
}
|
|
1511
|
+
function extractSuggestedCommands(text) {
|
|
1512
|
+
const commands = [...text.matchAll(/`(moda [^`]+)`/g)].map((match) => match[1]);
|
|
1513
|
+
return commands.length > 0 ? commands : undefined;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/command-context.ts
|
|
1517
|
+
function flagEnabled(flags, key) {
|
|
1518
|
+
return flags[key] === "true";
|
|
1519
|
+
}
|
|
1520
|
+
function stringOption(value) {
|
|
1521
|
+
if (value === undefined || value === null)
|
|
1522
|
+
return;
|
|
1523
|
+
const text = String(value).trim();
|
|
1524
|
+
return text.length > 0 ? text : undefined;
|
|
1525
|
+
}
|
|
1526
|
+
function createCommandContext(input, options = {}) {
|
|
1527
|
+
const flags = options.commandFlags ?? input.flags;
|
|
1528
|
+
const rawFlags = options.rawFlags ?? input.flags;
|
|
1529
|
+
const outputMode = options.outputMode ?? outputModeFromFlags(flags);
|
|
1530
|
+
const quiet = flagEnabled(flags, "quiet");
|
|
1531
|
+
const interactive = outputMode === "human" && !quiet && !flagEnabled(flags, "yes") && process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
1532
|
+
const telemetryMeta = {};
|
|
1533
|
+
return {
|
|
1534
|
+
command: options.commandName ?? input.command,
|
|
1535
|
+
positional: input.positional,
|
|
1536
|
+
positionals: input.positionals,
|
|
1537
|
+
flags,
|
|
1538
|
+
rawFlags,
|
|
1539
|
+
startedAt: options.startedAt ?? Date.now(),
|
|
1540
|
+
cwd: options.cwd ?? process.cwd(),
|
|
1541
|
+
env: options.env ?? process.env,
|
|
1542
|
+
outputMode,
|
|
1543
|
+
output: createCliOutput({
|
|
1544
|
+
command: options.commandName ?? input.command,
|
|
1545
|
+
mode: outputMode,
|
|
1546
|
+
quiet
|
|
1547
|
+
}),
|
|
1548
|
+
profile: flags.profile,
|
|
1549
|
+
dryRun: flagEnabled(flags, "dry-run"),
|
|
1550
|
+
yes: flagEnabled(flags, "yes"),
|
|
1551
|
+
quiet,
|
|
1552
|
+
verbose: flagEnabled(flags, "verbose"),
|
|
1553
|
+
interactive,
|
|
1554
|
+
recordTelemetry(meta) {
|
|
1555
|
+
Object.assign(telemetryMeta, meta);
|
|
1556
|
+
},
|
|
1557
|
+
getTelemetryMeta() {
|
|
1558
|
+
return telemetryMeta;
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/auth-storage.ts
|
|
1564
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, rmSync as rmSync2 } from "node:fs";
|
|
1565
|
+
function tokenPath(options = {}) {
|
|
1566
|
+
return getModaPaths(options).legacyAuthPath;
|
|
1567
|
+
}
|
|
1568
|
+
function authTokenRefForProfile(profile) {
|
|
1569
|
+
return `file:secrets/auth/${profile}.json`;
|
|
1570
|
+
}
|
|
1571
|
+
function parseStoredAuth(value) {
|
|
1572
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
const data = value;
|
|
1576
|
+
if (typeof data.access_token !== "string" || typeof data.expires_at !== "number") {
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
access_token: data.access_token,
|
|
1581
|
+
expires_at: data.expires_at,
|
|
1582
|
+
profile: typeof data.profile === "string" ? data.profile : undefined,
|
|
1583
|
+
token_type: data.token_type === "bearer" ? "bearer" : undefined,
|
|
1584
|
+
token_kind: data.token_kind === "moda_ct" || data.token_kind === "auth0" ? data.token_kind : undefined,
|
|
1585
|
+
user_email: typeof data.user_email === "string" ? data.user_email : undefined
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
function isModaCtSession(session) {
|
|
1589
|
+
return session.token_kind === "moda_ct" || session.access_token.startsWith("moda_ct_");
|
|
1590
|
+
}
|
|
1591
|
+
function isAuthSessionValid(data, now = Date.now()) {
|
|
1592
|
+
if (!data)
|
|
1593
|
+
return false;
|
|
1594
|
+
return now / 1000 <= data.expires_at - 60;
|
|
1595
|
+
}
|
|
1596
|
+
function readStoredAuth(path) {
|
|
1597
|
+
try {
|
|
1598
|
+
if (!existsSync2(path))
|
|
1599
|
+
return null;
|
|
1600
|
+
return parseStoredAuth(JSON.parse(readFileSync3(path, "utf8")));
|
|
1601
|
+
} catch {
|
|
1602
|
+
return null;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
function loadToken(options = {}) {
|
|
1606
|
+
return loadAuthSession(options)?.access_token ?? null;
|
|
1607
|
+
}
|
|
1608
|
+
function loadAuthSession(options = {}) {
|
|
1609
|
+
const profileSession = loadProfileAuthSession(options);
|
|
1610
|
+
if (profileSession)
|
|
1611
|
+
return profileSession;
|
|
1612
|
+
const legacy = readStoredAuth(tokenPath(options));
|
|
1613
|
+
if (!legacy)
|
|
1614
|
+
return null;
|
|
1615
|
+
if (!options.includeExpired && !isAuthSessionValid(legacy)) {
|
|
1616
|
+
return null;
|
|
1617
|
+
}
|
|
1618
|
+
return legacy;
|
|
1619
|
+
}
|
|
1620
|
+
function loadProfileAuthSession(options) {
|
|
1621
|
+
const profileName = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
|
|
1622
|
+
if (!profileName)
|
|
1623
|
+
return null;
|
|
1624
|
+
try {
|
|
1625
|
+
const profile = loadProfileConfig(options).profiles[profileName];
|
|
1626
|
+
const tokenRef = profile?.auth_token_ref;
|
|
1627
|
+
if (!tokenRef)
|
|
1628
|
+
return null;
|
|
1629
|
+
const path = secretFilePathFromRef(tokenRef, options);
|
|
1630
|
+
if (!path)
|
|
1631
|
+
return null;
|
|
1632
|
+
const session = readStoredAuth(path);
|
|
1633
|
+
if (!session)
|
|
1634
|
+
return null;
|
|
1635
|
+
if (!options.includeExpired && !isAuthSessionValid(session))
|
|
1636
|
+
return null;
|
|
1637
|
+
return session;
|
|
1638
|
+
} catch {
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
function resolveStorageProfile(options) {
|
|
1643
|
+
return options.profile ?? (options.env ?? process.env).MODA_PROFILE;
|
|
1644
|
+
}
|
|
1645
|
+
function saveAuthSession(accessToken, expiresIn, options = {}, extras = {}) {
|
|
1646
|
+
return persistAuthSession({
|
|
1647
|
+
access_token: accessToken,
|
|
1648
|
+
expires_at: Math.floor(Date.now() / 1000) + expiresIn,
|
|
1649
|
+
profile: resolveStorageProfile(options),
|
|
1650
|
+
token_type: "bearer",
|
|
1651
|
+
...extras.token_kind ? { token_kind: extras.token_kind } : {},
|
|
1652
|
+
...extras.user_email ? { user_email: extras.user_email } : {}
|
|
1653
|
+
}, options);
|
|
1654
|
+
}
|
|
1655
|
+
function persistAuthSession(data, options = {}) {
|
|
1656
|
+
const profile = data.profile;
|
|
1657
|
+
if (profile) {
|
|
1658
|
+
const ref = authTokenRefForProfile(profile);
|
|
1659
|
+
const path = secretFilePathFromRef(ref, options);
|
|
1660
|
+
if (!path)
|
|
1661
|
+
throw new Error(`Unsupported auth token reference: ${ref}`);
|
|
1662
|
+
const config = loadProfileConfig(options);
|
|
1663
|
+
const existing = config.profiles[profile];
|
|
1664
|
+
if (!existing) {
|
|
1665
|
+
throw new Error(`Moda profile '${profile}' does not exist.`);
|
|
1666
|
+
}
|
|
1667
|
+
writeSecretJsonFile(path, data);
|
|
1668
|
+
saveProfileConfig({
|
|
1669
|
+
...config,
|
|
1670
|
+
profiles: {
|
|
1671
|
+
...config.profiles,
|
|
1672
|
+
[profile]: {
|
|
1673
|
+
...existing,
|
|
1674
|
+
auth_token_ref: ref
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}, options);
|
|
1678
|
+
return data;
|
|
1679
|
+
}
|
|
1680
|
+
writeSecretJsonFile(tokenPath(options), data);
|
|
1681
|
+
return data;
|
|
1682
|
+
}
|
|
1683
|
+
function clearAuthSession(options = {}) {
|
|
1684
|
+
const profile = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
|
|
1685
|
+
if (profile) {
|
|
1686
|
+
const ref = authTokenRefForProfile(profile);
|
|
1687
|
+
const path = secretFilePathFromRef(ref, options);
|
|
1688
|
+
if (path)
|
|
1689
|
+
rmSync2(path, { force: true });
|
|
1690
|
+
const config = loadProfileConfig(options);
|
|
1691
|
+
const existing = config.profiles[profile];
|
|
1692
|
+
if (existing) {
|
|
1693
|
+
const { auth_token_ref: _drop, ...profileWithoutToken } = existing;
|
|
1694
|
+
saveProfileConfig({
|
|
1695
|
+
...config,
|
|
1696
|
+
profiles: {
|
|
1697
|
+
...config.profiles,
|
|
1698
|
+
[profile]: profileWithoutToken
|
|
1699
|
+
}
|
|
1700
|
+
}, options);
|
|
1701
|
+
}
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
rmSync2(tokenPath(options), { force: true });
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/auth.ts
|
|
1708
|
+
import { createServer } from "node:http";
|
|
1709
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
1710
|
+
import { hostname } from "node:os";
|
|
1711
|
+
import { createInterface } from "node:readline";
|
|
1712
|
+
|
|
1713
|
+
// src/cli-session.ts
|
|
1714
|
+
import { request as httpRequest } from "node:http";
|
|
1715
|
+
import { request as httpsRequest } from "node:https";
|
|
1716
|
+
var RENEW_WINDOW_SECONDS = 7 * 86400;
|
|
1717
|
+
var RENEW_TIMEOUT_MS = 1e4;
|
|
1718
|
+
var RENEW_PROBE_TIMEOUT_MS = 2000;
|
|
1719
|
+
var REVOKE_TIMEOUT_MS = 2000;
|
|
1720
|
+
function postCliSessionEndpoint(path, token, timeoutMs, opts = {}) {
|
|
1721
|
+
return new Promise((resolve, reject) => {
|
|
1722
|
+
let url;
|
|
1723
|
+
try {
|
|
1724
|
+
url = new URL(`${resolveModaBaseUrl(opts.authOptions)}${path}`);
|
|
1725
|
+
} catch (err) {
|
|
1726
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
const requestFn = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
1730
|
+
const req = requestFn(url, {
|
|
1731
|
+
method: "POST",
|
|
1732
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1733
|
+
timeout: timeoutMs
|
|
1734
|
+
}, (res) => {
|
|
1735
|
+
const chunks = [];
|
|
1736
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
1737
|
+
res.on("end", () => {
|
|
1738
|
+
resolve({
|
|
1739
|
+
status: res.statusCode ?? 0,
|
|
1740
|
+
body: Buffer.concat(chunks).toString("utf8")
|
|
1741
|
+
});
|
|
1742
|
+
});
|
|
1743
|
+
res.on("error", reject);
|
|
1744
|
+
});
|
|
1745
|
+
if (opts.unrefSocket) {
|
|
1746
|
+
req.on("socket", (socket) => {
|
|
1747
|
+
socket.unref();
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
req.on("timeout", () => {
|
|
1751
|
+
req.destroy(new Error(`CLI session request timed out after ${timeoutMs}ms`));
|
|
1752
|
+
});
|
|
1753
|
+
req.on("error", reject);
|
|
1754
|
+
req.end();
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
function persistRenewedExpiry(session, body, authOptions) {
|
|
1758
|
+
try {
|
|
1759
|
+
const parsed = JSON.parse(body);
|
|
1760
|
+
const expiresAtMs = typeof parsed.expires_at === "string" ? Date.parse(parsed.expires_at) : NaN;
|
|
1761
|
+
if (!Number.isFinite(expiresAtMs))
|
|
1762
|
+
return;
|
|
1763
|
+
persistAuthSession({ ...session, expires_at: Math.floor(expiresAtMs / 1000) }, authOptions);
|
|
1764
|
+
} catch {}
|
|
1765
|
+
}
|
|
1766
|
+
var renewInFlight = false;
|
|
1767
|
+
function scheduleRenewalIfNearExpiry(session, authOptions) {
|
|
1768
|
+
if (!isModaCtSession(session))
|
|
1769
|
+
return;
|
|
1770
|
+
if (session.expires_at - Date.now() / 1000 >= RENEW_WINDOW_SECONDS)
|
|
1771
|
+
return;
|
|
1772
|
+
if (renewInFlight)
|
|
1773
|
+
return;
|
|
1774
|
+
renewInFlight = true;
|
|
1775
|
+
(async () => {
|
|
1776
|
+
try {
|
|
1777
|
+
const result = await postCliSessionEndpoint("/api/cli/auth/renew", session.access_token, RENEW_TIMEOUT_MS, { unrefSocket: true, authOptions });
|
|
1778
|
+
if (result.status !== 200)
|
|
1779
|
+
return;
|
|
1780
|
+
persistRenewedExpiry(session, result.body, authOptions);
|
|
1781
|
+
} catch {} finally {
|
|
1782
|
+
renewInFlight = false;
|
|
1783
|
+
}
|
|
1784
|
+
})();
|
|
1785
|
+
}
|
|
1786
|
+
function cliSessionExpiredError() {
|
|
1787
|
+
return new CliAuthError('The stored CLI session token has expired. Run "moda auth login" to sign in again.', { reason: "token_expired", login_command: ["moda", "auth", "login"] });
|
|
1788
|
+
}
|
|
1789
|
+
async function classifyModaCtUnauthorized(session, authOptions) {
|
|
1790
|
+
const loginCommand = ["moda", "auth", "login"];
|
|
1791
|
+
if (Date.now() / 1000 > session.expires_at) {
|
|
1792
|
+
return cliSessionExpiredError();
|
|
1793
|
+
}
|
|
1794
|
+
let probe;
|
|
1795
|
+
try {
|
|
1796
|
+
probe = await postCliSessionEndpoint("/api/cli/auth/renew", session.access_token, RENEW_PROBE_TIMEOUT_MS, { authOptions });
|
|
1797
|
+
} catch {
|
|
1798
|
+
return new CliAuthError("The Moda API rejected the stored CLI session token, and the renewal check could not reach the server. " + "The token has not expired, so this is likely transient: retry the command instead of logging in again.", { reason: "renew_failed", login_command: loginCommand });
|
|
1799
|
+
}
|
|
1800
|
+
if (probe.status === 401) {
|
|
1801
|
+
return new CliAuthError('The stored CLI session token has been revoked. Run "moda auth login" to sign in again.', { reason: "token_revoked", login_command: loginCommand });
|
|
1802
|
+
}
|
|
1803
|
+
if (probe.status === 200) {
|
|
1804
|
+
persistRenewedExpiry(session, probe.body, authOptions);
|
|
1805
|
+
return null;
|
|
1806
|
+
}
|
|
1807
|
+
return new CliAuthError("The Moda API rejected the stored CLI session token, and the renewal check failed " + `(HTTP ${probe.status}). The token has not expired, so this is likely transient: ` + "retry the command instead of logging in again.", { reason: "renew_failed", login_command: loginCommand });
|
|
1808
|
+
}
|
|
1809
|
+
async function revokeCliSessionToken(options = {}) {
|
|
1810
|
+
const session = loadAuthSession({ ...options, includeExpired: true });
|
|
1811
|
+
if (!session || !isModaCtSession(session))
|
|
1812
|
+
return;
|
|
1813
|
+
try {
|
|
1814
|
+
await postCliSessionEndpoint("/api/cli/auth/revoke", session.access_token, REVOKE_TIMEOUT_MS, { authOptions: options });
|
|
1815
|
+
} catch {}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// src/auth.ts
|
|
1819
|
+
var CALLBACK_PORT = 8432;
|
|
1820
|
+
var CALLBACK_PORT_LADDER = [CALLBACK_PORT, 8433, 8434, 8435];
|
|
1821
|
+
var DEFAULT_AUTH_TIMEOUT_MS = 600000;
|
|
1822
|
+
var AUTH_TIMEOUT_MIN_MS = 30000;
|
|
1823
|
+
var AUTH_TIMEOUT_MAX_MS = 30 * 60000;
|
|
1824
|
+
var DEFAULT_EXPIRES_IN_SECONDS = 86400;
|
|
1825
|
+
var EXCHANGE_TIMEOUT_MS = 1e4;
|
|
1826
|
+
function resolveAuthTimeoutMs(options = {}) {
|
|
1827
|
+
if (typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs)) {
|
|
1828
|
+
return Math.max(1, Math.floor(options.timeoutMs));
|
|
1829
|
+
}
|
|
1830
|
+
const env = options.env ?? process.env;
|
|
1831
|
+
const raw = env.MODA_AUTH_TIMEOUT_MS?.trim();
|
|
1832
|
+
if (raw) {
|
|
1833
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1834
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1835
|
+
return Math.min(AUTH_TIMEOUT_MAX_MS, Math.max(AUTH_TIMEOUT_MIN_MS, parsed));
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return DEFAULT_AUTH_TIMEOUT_MS;
|
|
1839
|
+
}
|
|
1840
|
+
function escapeHtml(value) {
|
|
1841
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1842
|
+
}
|
|
1843
|
+
function safeEqual(a, b) {
|
|
1844
|
+
const aBuf = Buffer.from(a);
|
|
1845
|
+
const bBuf = Buffer.from(b);
|
|
1846
|
+
if (aBuf.length !== bBuf.length)
|
|
1847
|
+
return false;
|
|
1848
|
+
return timingSafeEqual(aBuf, bBuf);
|
|
1849
|
+
}
|
|
1850
|
+
function looksLikeOneTimeCode(value) {
|
|
1851
|
+
return /^[A-Za-z0-9_-]{32,96}$/.test(value);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
class CodeExchangeError extends Error {
|
|
1855
|
+
reason;
|
|
1856
|
+
constructor(message, reason) {
|
|
1857
|
+
super(message);
|
|
1858
|
+
this.reason = reason;
|
|
1859
|
+
this.name = "CodeExchangeError";
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
function exchangeFailureReason(error) {
|
|
1863
|
+
return error instanceof CodeExchangeError ? error.reason : "exchange_failed";
|
|
1864
|
+
}
|
|
1865
|
+
function computePkceChallenge(codeVerifier) {
|
|
1866
|
+
return createHash("sha256").update(codeVerifier).digest("base64url");
|
|
1867
|
+
}
|
|
1868
|
+
function generatePkceVerifier() {
|
|
1869
|
+
return randomBytes(32).toString("base64url");
|
|
1870
|
+
}
|
|
1871
|
+
var cachedLoginContext = null;
|
|
1872
|
+
function getCachedLoginContext(options = {}) {
|
|
1873
|
+
if (!cachedLoginContext)
|
|
1874
|
+
return null;
|
|
1875
|
+
if (cachedLoginContext.profile !== resolveStorageProfile(options))
|
|
1876
|
+
return null;
|
|
1877
|
+
return cachedLoginContext.context;
|
|
1878
|
+
}
|
|
1879
|
+
var SUCCESS_HTML = `<!DOCTYPE html>
|
|
1880
|
+
<html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#fafafa">
|
|
1881
|
+
<div style="text-align:center">
|
|
1882
|
+
<h2>Authenticated</h2>
|
|
1883
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
1884
|
+
</div>
|
|
1885
|
+
<script>setTimeout(()=>window.close(),2000)</script>
|
|
1886
|
+
</body></html>`;
|
|
1887
|
+
var FRAGMENT_EXTRACTOR_HTML = `<!DOCTYPE html>
|
|
1888
|
+
<html><body>
|
|
1889
|
+
<script>
|
|
1890
|
+
var h = window.location.hash.substring(1);
|
|
1891
|
+
if (h) {
|
|
1892
|
+
window.location.replace('/callback-token?' + h);
|
|
1893
|
+
} else {
|
|
1894
|
+
document.body.innerText = 'No token received.';
|
|
1895
|
+
}
|
|
1896
|
+
</script>
|
|
1897
|
+
</body></html>`;
|
|
1898
|
+
async function openAuthUrl(authUrl) {
|
|
1899
|
+
const open = (await import("open")).default;
|
|
1900
|
+
await open(authUrl);
|
|
1901
|
+
}
|
|
1902
|
+
function closeAuthServer(server) {
|
|
1903
|
+
server.close((err) => {
|
|
1904
|
+
if (err && err.code !== "ERR_SERVER_NOT_RUNNING") {}
|
|
1905
|
+
});
|
|
1906
|
+
server.closeIdleConnections?.();
|
|
1907
|
+
}
|
|
1908
|
+
async function login(screen = "login", options = {}) {
|
|
1909
|
+
return new Promise((resolve, reject) => {
|
|
1910
|
+
cachedLoginContext = null;
|
|
1911
|
+
let settled = false;
|
|
1912
|
+
const portsToTry = options.callbackPort !== undefined ? [options.callbackPort] : [...CALLBACK_PORT_LADDER];
|
|
1913
|
+
let portIndex = 0;
|
|
1914
|
+
let localPort = portsToTry[0];
|
|
1915
|
+
let timeout;
|
|
1916
|
+
let promptInput;
|
|
1917
|
+
let promptStream;
|
|
1918
|
+
let promptStreamIsTTY = false;
|
|
1919
|
+
let pendingBarePaste = null;
|
|
1920
|
+
let exchangeInFlight = false;
|
|
1921
|
+
const state = randomBytes(16).toString("hex");
|
|
1922
|
+
const codeVerifier = generatePkceVerifier();
|
|
1923
|
+
const codeChallenge = computePkceChallenge(codeVerifier);
|
|
1924
|
+
const storageOptions = {
|
|
1925
|
+
home: options.home,
|
|
1926
|
+
profile: options.profile,
|
|
1927
|
+
now: options.now,
|
|
1928
|
+
env: options.env
|
|
1929
|
+
};
|
|
1930
|
+
const targetProfile = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
|
|
1931
|
+
if (targetProfile && !loadProfileConfig(storageOptions).profiles[targetProfile]) {
|
|
1932
|
+
reject(new CliInputError(`Moda profile '${targetProfile}' does not exist.`, 'Create it with "moda profiles create <name>" or check "moda profiles list".'));
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
const server = createServer(async (req, res) => {
|
|
1936
|
+
if (settled) {
|
|
1937
|
+
res.writeHead(410, { Connection: "close" });
|
|
1938
|
+
res.end("Login attempt already completed");
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
const url = new URL(req.url || "/", `http://localhost:${localPort}`);
|
|
1942
|
+
if (url.pathname === "/callback") {
|
|
1943
|
+
const code = url.searchParams.get("code");
|
|
1944
|
+
if (code) {
|
|
1945
|
+
const callbackState2 = url.searchParams.get("state");
|
|
1946
|
+
if (!callbackState2 || !safeEqual(callbackState2, state)) {
|
|
1947
|
+
res.writeHead(400, { "Content-Type": "text/html", Connection: "close" });
|
|
1948
|
+
res.end("<html><body><h2>Error</h2><p>Invalid authentication state.</p></body></html>", () => {
|
|
1949
|
+
fail(new Error("Invalid authentication state."));
|
|
1950
|
+
});
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
if (exchangeInFlight) {
|
|
1954
|
+
res.writeHead(409, { Connection: "close" });
|
|
1955
|
+
res.end("Login code exchange already in progress");
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
exchangeInFlight = true;
|
|
1959
|
+
(async () => {
|
|
1960
|
+
try {
|
|
1961
|
+
const cliToken = await exchangeCodeForSession(code);
|
|
1962
|
+
res.writeHead(302, {
|
|
1963
|
+
Location: `${resolveModaBaseUrl(storageOptions)}/auth/cli/done?status=success`,
|
|
1964
|
+
Connection: "close"
|
|
1965
|
+
});
|
|
1966
|
+
res.end(() => {
|
|
1967
|
+
succeed(cliToken);
|
|
1968
|
+
});
|
|
1969
|
+
} catch (err) {
|
|
1970
|
+
exchangeInFlight = false;
|
|
1971
|
+
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
1972
|
+
res.writeHead(302, {
|
|
1973
|
+
Location: `${resolveModaBaseUrl(storageOptions)}/auth/cli/done?status=error&reason=${exchangeFailureReason(err)}`,
|
|
1974
|
+
Connection: "close"
|
|
1975
|
+
});
|
|
1976
|
+
res.end(() => {
|
|
1977
|
+
fail(error2);
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
})();
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
|
|
1984
|
+
res.end(FRAGMENT_EXTRACTOR_HTML);
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
if (url.pathname !== "/callback-token") {
|
|
1988
|
+
res.writeHead(404, { Connection: "close" });
|
|
1989
|
+
res.end("Not found");
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
const token = url.searchParams.get("token");
|
|
1993
|
+
const expiresIn = url.searchParams.get("expires_in");
|
|
1994
|
+
const error = url.searchParams.get("error");
|
|
1995
|
+
const callbackState = url.searchParams.get("state");
|
|
1996
|
+
if (!callbackState || !safeEqual(callbackState, state)) {
|
|
1997
|
+
res.writeHead(400, { "Content-Type": "text/html", Connection: "close" });
|
|
1998
|
+
res.end("<html><body><h2>Error</h2><p>Invalid authentication state.</p></body></html>", () => {
|
|
1999
|
+
fail(new Error("Invalid authentication state."));
|
|
2000
|
+
});
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
if (error) {
|
|
2004
|
+
res.writeHead(400, { "Content-Type": "text/html", Connection: "close" });
|
|
2005
|
+
res.end(`<html><body><h2>Error</h2><p>${escapeHtml(error)}</p></body></html>`, () => {
|
|
2006
|
+
fail(new Error(`Authentication error: ${error}`));
|
|
2007
|
+
});
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
if (!token) {
|
|
2011
|
+
res.writeHead(400, { "Content-Type": "text/html", Connection: "close" });
|
|
2012
|
+
res.end("<html><body><h2>Error</h2><p>No token received.</p></body></html>", () => {
|
|
2013
|
+
fail(new Error("No token received"));
|
|
2014
|
+
});
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
try {
|
|
2018
|
+
saveAuthSession(token, parseInt(expiresIn || String(DEFAULT_EXPIRES_IN_SECONDS), 10), storageOptions);
|
|
2019
|
+
} catch (err) {
|
|
2020
|
+
res.writeHead(500, { "Content-Type": "text/html", Connection: "close" });
|
|
2021
|
+
res.end("<html><body><h2>Error</h2><p>Authenticated, but the CLI could not store the token. Return to your terminal.</p></body></html>", () => {
|
|
2022
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
2023
|
+
});
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
|
|
2027
|
+
res.end(SUCCESS_HTML, () => {
|
|
2028
|
+
succeed(token);
|
|
2029
|
+
});
|
|
2030
|
+
});
|
|
2031
|
+
function removeSignalHandlers() {
|
|
2032
|
+
process.off("SIGINT", handleSigint);
|
|
2033
|
+
process.off("SIGTERM", handleSigterm);
|
|
2034
|
+
}
|
|
2035
|
+
function closePromptInput() {
|
|
2036
|
+
if (!promptInput)
|
|
2037
|
+
return;
|
|
2038
|
+
promptInput.close();
|
|
2039
|
+
promptInput = undefined;
|
|
2040
|
+
if (promptStream === process.stdin) {
|
|
2041
|
+
process.stdin.pause();
|
|
2042
|
+
}
|
|
2043
|
+
promptStream = undefined;
|
|
2044
|
+
}
|
|
2045
|
+
function finish(callback) {
|
|
2046
|
+
if (settled)
|
|
2047
|
+
return;
|
|
2048
|
+
settled = true;
|
|
2049
|
+
if (timeout)
|
|
2050
|
+
clearTimeout(timeout);
|
|
2051
|
+
removeSignalHandlers();
|
|
2052
|
+
closePromptInput();
|
|
2053
|
+
closeAuthServer(server);
|
|
2054
|
+
callback();
|
|
2055
|
+
}
|
|
2056
|
+
function succeed(token) {
|
|
2057
|
+
finish(() => resolve(token));
|
|
2058
|
+
}
|
|
2059
|
+
function fail(error) {
|
|
2060
|
+
finish(() => reject(error));
|
|
2061
|
+
}
|
|
2062
|
+
function handleSigint() {
|
|
2063
|
+
fail(new Error("Authentication cancelled."));
|
|
2064
|
+
}
|
|
2065
|
+
function handleSigterm() {
|
|
2066
|
+
fail(new Error("Authentication terminated."));
|
|
2067
|
+
}
|
|
2068
|
+
function completeWithToken(token, expiresIn) {
|
|
2069
|
+
try {
|
|
2070
|
+
saveAuthSession(token, expiresIn, storageOptions);
|
|
2071
|
+
} catch (err) {
|
|
2072
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
succeed(token);
|
|
2076
|
+
}
|
|
2077
|
+
async function exchangeCodeForSession(code) {
|
|
2078
|
+
let response;
|
|
2079
|
+
try {
|
|
2080
|
+
response = await fetch(`${resolveModaBaseUrl(storageOptions)}/api/cli/auth/exchange`, {
|
|
2081
|
+
method: "POST",
|
|
2082
|
+
headers: { "Content-Type": "application/json" },
|
|
2083
|
+
body: JSON.stringify({
|
|
2084
|
+
code,
|
|
2085
|
+
code_verifier: codeVerifier,
|
|
2086
|
+
label: hostname()
|
|
2087
|
+
}),
|
|
2088
|
+
redirect: "manual",
|
|
2089
|
+
signal: AbortSignal.timeout(EXCHANGE_TIMEOUT_MS)
|
|
2090
|
+
});
|
|
2091
|
+
} catch {
|
|
2092
|
+
throw new CodeExchangeError("Could not reach the Moda API to exchange the login code. Check your network and try again.", "unreachable");
|
|
2093
|
+
}
|
|
2094
|
+
if (response.status >= 300 && response.status < 400) {
|
|
2095
|
+
throw new CodeExchangeError(`The Moda API redirected the login code exchange (HTTP ${response.status}). Refusing to re-send the login code across a redirect. Check MODA_BASE_URL (use https://moda.dev, not www.moda.dev) and try again.`, "exchange_failed");
|
|
2096
|
+
}
|
|
2097
|
+
if (response.status === 401) {
|
|
2098
|
+
throw new CodeExchangeError("The login code was rejected (invalid, expired, or already used). Run the login command again.", "code_rejected");
|
|
2099
|
+
}
|
|
2100
|
+
if (!response.ok) {
|
|
2101
|
+
throw new CodeExchangeError(`Login code exchange failed (HTTP ${response.status}). Try again.`, "exchange_failed");
|
|
2102
|
+
}
|
|
2103
|
+
let payload;
|
|
2104
|
+
try {
|
|
2105
|
+
payload = await response.json();
|
|
2106
|
+
} catch {
|
|
2107
|
+
throw new CodeExchangeError("The login code exchange returned an unreadable response. Try again.", "exchange_failed");
|
|
2108
|
+
}
|
|
2109
|
+
const cliToken = typeof payload.cli_token === "string" ? payload.cli_token : undefined;
|
|
2110
|
+
const expiresAtMs = typeof payload.expires_at === "string" ? Date.parse(payload.expires_at) : NaN;
|
|
2111
|
+
if (!cliToken || !Number.isFinite(expiresAtMs)) {
|
|
2112
|
+
throw new CodeExchangeError("The login code exchange returned an incomplete response. Try again.", "exchange_failed");
|
|
2113
|
+
}
|
|
2114
|
+
const userEmail = typeof payload.user?.email === "string" ? payload.user.email : undefined;
|
|
2115
|
+
persistAuthSession({
|
|
2116
|
+
access_token: cliToken,
|
|
2117
|
+
expires_at: Math.floor(expiresAtMs / 1000),
|
|
2118
|
+
profile: resolveStorageProfile(storageOptions),
|
|
2119
|
+
token_type: "bearer",
|
|
2120
|
+
token_kind: "moda_ct",
|
|
2121
|
+
...userEmail ? { user_email: userEmail } : {}
|
|
2122
|
+
}, storageOptions);
|
|
2123
|
+
cachedLoginContext = {
|
|
2124
|
+
profile: resolveStorageProfile(storageOptions),
|
|
2125
|
+
context: {
|
|
2126
|
+
user: {
|
|
2127
|
+
email: userEmail ?? "",
|
|
2128
|
+
name: typeof payload.user?.name === "string" ? payload.user.name : null
|
|
2129
|
+
},
|
|
2130
|
+
tenants: Array.isArray(payload.tenants) ? payload.tenants : []
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
return cliToken;
|
|
2134
|
+
}
|
|
2135
|
+
async function completeWithCode(code) {
|
|
2136
|
+
if (exchangeInFlight)
|
|
2137
|
+
return;
|
|
2138
|
+
exchangeInFlight = true;
|
|
2139
|
+
try {
|
|
2140
|
+
const cliToken = await exchangeCodeForSession(code);
|
|
2141
|
+
succeed(cliToken);
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
exchangeInFlight = false;
|
|
2144
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
function handlePastedLine(line) {
|
|
2148
|
+
if (settled)
|
|
2149
|
+
return;
|
|
2150
|
+
const trimmed = line.trim();
|
|
2151
|
+
if (pendingBarePaste !== null) {
|
|
2152
|
+
const pending = pendingBarePaste;
|
|
2153
|
+
pendingBarePaste = null;
|
|
2154
|
+
if (/^y(es)?$/i.test(trimmed)) {
|
|
2155
|
+
if (pending.kind === "code") {
|
|
2156
|
+
completeWithCode(pending.value);
|
|
2157
|
+
} else {
|
|
2158
|
+
completeWithToken(pending.value, DEFAULT_EXPIRES_IN_SECONDS);
|
|
2159
|
+
}
|
|
2160
|
+
} else {
|
|
2161
|
+
fail(new Error(pending.kind === "code" ? "Pasted code rejected: state cannot be verified." : "Bare token rejected: state cannot be verified."));
|
|
2162
|
+
}
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
if (!trimmed)
|
|
2166
|
+
return;
|
|
2167
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
2168
|
+
let pastedUrl;
|
|
2169
|
+
try {
|
|
2170
|
+
pastedUrl = new URL(trimmed);
|
|
2171
|
+
} catch {
|
|
2172
|
+
fail(new Error("Could not parse the pasted URL. Paste the full callback URL from the browser."));
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
const params = pastedUrl.hash.length > 1 ? new URLSearchParams(pastedUrl.hash.slice(1)) : pastedUrl.searchParams;
|
|
2176
|
+
const token = params.get("token");
|
|
2177
|
+
const code = params.get("code");
|
|
2178
|
+
const expiresIn = params.get("expires_in");
|
|
2179
|
+
const pastedState = params.get("state");
|
|
2180
|
+
if (!pastedState || !safeEqual(pastedState, state)) {
|
|
2181
|
+
fail(new Error("Invalid authentication state."));
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (token) {
|
|
2185
|
+
completeWithToken(token, parseInt(expiresIn || String(DEFAULT_EXPIRES_IN_SECONDS), 10));
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
if (code) {
|
|
2189
|
+
completeWithCode(code);
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
fail(new Error("No token received"));
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
const kind = looksLikeOneTimeCode(trimmed) ? "code" : "token";
|
|
2196
|
+
if (promptStreamIsTTY) {
|
|
2197
|
+
pendingBarePaste = { kind, value: trimmed };
|
|
2198
|
+
console.error(kind === "code" ? " State cannot be verified for a pasted code. Continue? [y/N]" : " State cannot be verified for a bare token. Continue? [y/N]");
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
fail(new Error(kind === "code" ? "State cannot be verified for a pasted code in non-interactive mode. Paste the full callback URL instead." : "State cannot be verified for a bare token in non-interactive mode. Paste the full callback URL instead."));
|
|
2202
|
+
}
|
|
2203
|
+
function attachPromptInput() {
|
|
2204
|
+
if (settled || promptInput)
|
|
2205
|
+
return;
|
|
2206
|
+
const stream = options.promptStream ?? (process.stdin && (process.stdin.isTTY || !process.stdin.destroyed) ? process.stdin : undefined);
|
|
2207
|
+
if (!stream)
|
|
2208
|
+
return;
|
|
2209
|
+
promptStream = stream;
|
|
2210
|
+
promptStreamIsTTY = Boolean(stream.isTTY);
|
|
2211
|
+
promptInput = createInterface({ input: stream, terminal: false });
|
|
2212
|
+
promptInput.on("line", handlePastedLine);
|
|
2213
|
+
stream.unref?.();
|
|
2214
|
+
}
|
|
2215
|
+
timeout = setTimeout(() => {
|
|
2216
|
+
fail(new Error("Authentication timed out. Please try again."));
|
|
2217
|
+
}, resolveAuthTimeoutMs({ timeoutMs: options.timeoutMs, env: options.env }));
|
|
2218
|
+
server.on("error", (err) => {
|
|
2219
|
+
if (settled)
|
|
2220
|
+
return;
|
|
2221
|
+
if (err.code === "EADDRINUSE") {
|
|
2222
|
+
portIndex += 1;
|
|
2223
|
+
if (portIndex < portsToTry.length) {
|
|
2224
|
+
localPort = portsToTry[portIndex];
|
|
2225
|
+
server.listen(localPort, "127.0.0.1");
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
if (options.callbackPort !== undefined) {
|
|
2229
|
+
fail(new Error(`Port ${options.callbackPort} is in use. Close the process using it and try again.`));
|
|
2230
|
+
} else {
|
|
2231
|
+
fail(new Error(`Ports ${CALLBACK_PORT_LADDER.join(", ")} are all in use. Close a process using one of them and try again.`));
|
|
2232
|
+
}
|
|
2233
|
+
} else {
|
|
2234
|
+
fail(err);
|
|
2235
|
+
}
|
|
2236
|
+
});
|
|
2237
|
+
async function onListening() {
|
|
2238
|
+
if (settled)
|
|
2239
|
+
return;
|
|
2240
|
+
const address = server.address();
|
|
2241
|
+
if (address && typeof address === "object") {
|
|
2242
|
+
localPort = address.port;
|
|
2243
|
+
}
|
|
2244
|
+
const authUrl = `${resolveModaBaseUrl(storageOptions)}/auth/cli?port=${localPort}&screen=${screen}&state=${state}&code_challenge=${codeChallenge}&challenge_method=S256`;
|
|
2245
|
+
attachPromptInput();
|
|
2246
|
+
try {
|
|
2247
|
+
await (options.openUrl ?? openAuthUrl)(authUrl);
|
|
2248
|
+
} catch {}
|
|
2249
|
+
console.error(` Open this URL in your browser if it did not open automatically:
|
|
2250
|
+
${authUrl}`);
|
|
2251
|
+
console.error(` Paste the callback URL or code here if the browser can't reach this machine
|
|
2252
|
+
(if the browser page failed to redirect, copy the URL from the browser's address bar):`);
|
|
2253
|
+
}
|
|
2254
|
+
server.once("listening", onListening);
|
|
2255
|
+
server.listen(localPort, "127.0.0.1");
|
|
2256
|
+
process.once("SIGINT", handleSigint);
|
|
2257
|
+
process.once("SIGTERM", handleSigterm);
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
async function authFetch(path, options = {}, authOptions = {}) {
|
|
2261
|
+
const session = loadAuthSession(authOptions);
|
|
2262
|
+
if (!session) {
|
|
2263
|
+
const expired = loadAuthSession({ ...authOptions, includeExpired: true });
|
|
2264
|
+
if (expired && isModaCtSession(expired)) {
|
|
2265
|
+
throw cliSessionExpiredError();
|
|
2266
|
+
}
|
|
2267
|
+
throw new CliAuthError('Not authenticated. Run "moda init" to log in.', {
|
|
2268
|
+
reason: "no_stored_credentials",
|
|
2269
|
+
login_command: ["moda", "init"]
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
const token = session.access_token;
|
|
2273
|
+
scheduleRenewalIfNearExpiry(session, authOptions);
|
|
2274
|
+
const headers = new Headers(options.headers);
|
|
2275
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
2276
|
+
if (!headers.has("Content-Type") && options.body) {
|
|
2277
|
+
headers.set("Content-Type", "application/json");
|
|
2278
|
+
}
|
|
2279
|
+
const baseUrl = resolveModaBaseUrl(authOptions);
|
|
2280
|
+
const origin = new URL(baseUrl).origin;
|
|
2281
|
+
let url = `${baseUrl}${path}`;
|
|
2282
|
+
const MAX_REDIRECTS = 5;
|
|
2283
|
+
for (let hop = 0;; hop++) {
|
|
2284
|
+
const response = await fetch(url, { ...options, headers, redirect: "manual" });
|
|
2285
|
+
if (response.status < 300 || response.status >= 400) {
|
|
2286
|
+
if (response.status === 401 && isModaCtSession(session)) {
|
|
2287
|
+
const authError = await classifyModaCtUnauthorized(session, authOptions);
|
|
2288
|
+
if (authError)
|
|
2289
|
+
throw authError;
|
|
2290
|
+
}
|
|
2291
|
+
return response;
|
|
2292
|
+
}
|
|
2293
|
+
const location = response.headers.get("location");
|
|
2294
|
+
const target = location ? safeResolveUrl(location, url) : null;
|
|
2295
|
+
if (!target || target.origin !== origin) {
|
|
2296
|
+
throw new Error(`Moda API redirected (${response.status}) to ${location ?? "(unknown)"}. Cross-host redirects strip Authorization and break moda init. Use MODA_BASE_URL=https://moda.dev (not www.moda.dev).`);
|
|
2297
|
+
}
|
|
2298
|
+
if (hop >= MAX_REDIRECTS) {
|
|
2299
|
+
throw new Error(`Moda API exceeded ${MAX_REDIRECTS} same-host redirects for ${path}.`);
|
|
2300
|
+
}
|
|
2301
|
+
url = target.toString();
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
function safeResolveUrl(location, base) {
|
|
2305
|
+
try {
|
|
2306
|
+
return new URL(location, base);
|
|
2307
|
+
} catch {
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
export { __require, PROD_BASE_URL, DEFAULT_INGEST_URL, getModaPaths, getModaDir, isLoopbackBaseUrl, assertValidProfileName, loadProfileConfig, getActiveProfile, setActiveProfile, upsertProfile, clearStoredApiKeyCredentials, secretFilePathFromRef, resolveSecretReference, resolveApiKeyFromActiveProfile, resolveTenantIdFromActiveProfile, ensureSecretFilePermissions, writeSecretJsonFile, CliInputError, CliAccessError, CliInputRequiredError, CliAuthError, CliCancellationError, isNetworkLikeError, isApiLikeError, VERSION, loadCliConfig, saveCliConfig, resolveApiKey, embeddedTenantIdFromApiKey, resolveIngestUrl2 as resolveIngestUrl, resolveTenantId, resolveModaBaseUrl, validateConfig, MAX_SCANNED_FILE_BYTES, DEFAULT_MAX_INVENTORY_FILES, DEFAULT_MAX_INVENTORY_TEXT_BYTES, MAX_RECORDED_SKIPS, SKIPPED_DIRS, isSkippedDir, isSkippedPathPrefix, isEvalLikePath, shouldInventoryFile, isTextFile, isSecretishPath, redactDeep, packSnapshot, EXIT_OK, EXIT_ERROR, EXIT_DEGRADED, EXIT_AUTH_REQUIRED, EXIT_INPUT_REQUIRED, EXIT_CANCELLED, AGENT_ERROR_CODES, wasOutputCancelled, safeWriteStdout, safeWriteStderr, resolveOutputMode, createCliOutput, stringOption, createCommandContext, isAuthSessionValid, loadToken, loadAuthSession, clearAuthSession, revokeCliSessionToken, getCachedLoginContext, login, authFetch };
|