@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.
Files changed (115) hide show
  1. package/dist/cli-g8qs2tj9.js +9777 -0
  2. package/dist/cli-pwbvtes8.js +2312 -0
  3. package/dist/cli-qszcpda3.js +284 -0
  4. package/dist/cli.js +4775 -1159
  5. package/dist/index-6h0nc1r5.js +2765 -0
  6. package/dist/provision-j5gq8cvx.js +72 -0
  7. package/package.json +3 -2
  8. package/skills/integration/index.json +2 -2
  9. package/dist/agent-schemas.d.ts +0 -3
  10. package/dist/agent-schemas.js +0 -358
  11. package/dist/agent.d.ts +0 -5
  12. package/dist/agent.js +0 -477
  13. package/dist/analyst-tui.d.ts +0 -81
  14. package/dist/analyst-tui.js +0 -314
  15. package/dist/api-client.d.ts +0 -46
  16. package/dist/api-client.js +0 -120
  17. package/dist/ask-stream.d.ts +0 -146
  18. package/dist/ask-stream.js +0 -473
  19. package/dist/auth-storage.d.ts +0 -35
  20. package/dist/auth-storage.js +0 -159
  21. package/dist/auth.d.ts +0 -50
  22. package/dist/auth.js +0 -694
  23. package/dist/cli-session.d.ts +0 -26
  24. package/dist/cli-session.js +0 -169
  25. package/dist/cli.d.ts +0 -44
  26. package/dist/command-context.d.ts +0 -62
  27. package/dist/command-context.js +0 -57
  28. package/dist/command-registry.d.ts +0 -22
  29. package/dist/command-registry.js +0 -15
  30. package/dist/config.d.ts +0 -60
  31. package/dist/config.js +0 -110
  32. package/dist/doctor.d.ts +0 -79
  33. package/dist/doctor.js +0 -1182
  34. package/dist/errors.d.ts +0 -50
  35. package/dist/errors.js +0 -64
  36. package/dist/exit-funnel.d.ts +0 -14
  37. package/dist/exit-funnel.js +0 -53
  38. package/dist/harness-artifact-bodies.d.ts +0 -28
  39. package/dist/harness-artifact-bodies.js +0 -276
  40. package/dist/harness-candidates.d.ts +0 -77
  41. package/dist/harness-candidates.js +0 -482
  42. package/dist/harness-remote.d.ts +0 -135
  43. package/dist/harness-remote.js +0 -540
  44. package/dist/harness-report.d.ts +0 -145
  45. package/dist/harness-report.js +0 -1579
  46. package/dist/harness-sync-limits.d.ts +0 -8
  47. package/dist/harness-sync-limits.js +0 -21
  48. package/dist/harness.d.ts +0 -463
  49. package/dist/harness.js +0 -4009
  50. package/dist/init/index.d.ts +0 -78
  51. package/dist/init/index.js +0 -1442
  52. package/dist/init/mcp.d.ts +0 -2
  53. package/dist/init/mcp.js +0 -27
  54. package/dist/init/progress-board.d.ts +0 -80
  55. package/dist/init/progress-board.js +0 -225
  56. package/dist/init/rules.d.ts +0 -6
  57. package/dist/init/rules.js +0 -171
  58. package/dist/init/sdk-catalog.d.ts +0 -10
  59. package/dist/init/sdk-catalog.js +0 -80
  60. package/dist/init/sdk-guide.d.ts +0 -19
  61. package/dist/init/sdk-guide.js +0 -185
  62. package/dist/init/sdk-integration.d.ts +0 -53
  63. package/dist/init/sdk-integration.js +0 -835
  64. package/dist/init/sdk-protocol.d.ts +0 -135
  65. package/dist/init/sdk-protocol.js +0 -152
  66. package/dist/init/sdk-verification.d.ts +0 -27
  67. package/dist/init/sdk-verification.js +0 -123
  68. package/dist/init/tenant.d.ts +0 -67
  69. package/dist/init/tenant.js +0 -421
  70. package/dist/intelligence-schema.d.ts +0 -349
  71. package/dist/intelligence-schema.js +0 -174
  72. package/dist/keys.d.ts +0 -47
  73. package/dist/keys.js +0 -136
  74. package/dist/manifest.d.ts +0 -5
  75. package/dist/manifest.js +0 -114
  76. package/dist/markdown-terminal.d.ts +0 -49
  77. package/dist/markdown-terminal.js +0 -212
  78. package/dist/output.d.ts +0 -191
  79. package/dist/output.js +0 -624
  80. package/dist/production-intelligence.d.ts +0 -175
  81. package/dist/production-intelligence.js +0 -1430
  82. package/dist/profile-commands.d.ts +0 -10
  83. package/dist/profile-commands.js +0 -342
  84. package/dist/profiles.d.ts +0 -112
  85. package/dist/profiles.js +0 -573
  86. package/dist/prompts-ab.d.ts +0 -71
  87. package/dist/prompts-ab.js +0 -313
  88. package/dist/prompts-propose.d.ts +0 -15
  89. package/dist/prompts-propose.js +0 -212
  90. package/dist/prompts.d.ts +0 -58
  91. package/dist/prompts.js +0 -418
  92. package/dist/provision.d.ts +0 -28
  93. package/dist/provision.js +0 -104
  94. package/dist/redact.d.ts +0 -2
  95. package/dist/redact.js +0 -90
  96. package/dist/schemas.d.ts +0 -225
  97. package/dist/schemas.js +0 -126
  98. package/dist/skills-bundled.d.ts +0 -35
  99. package/dist/skills-bundled.js +0 -157
  100. package/dist/skills-manifest.d.ts +0 -33
  101. package/dist/skills-manifest.js +0 -244
  102. package/dist/skills.d.ts +0 -57
  103. package/dist/skills.js +0 -777
  104. package/dist/sync.d.ts +0 -5
  105. package/dist/sync.js +0 -36
  106. package/dist/telemetry.d.ts +0 -56
  107. package/dist/telemetry.js +0 -167
  108. package/dist/transforms.d.ts +0 -25
  109. package/dist/transforms.js +0 -102
  110. package/dist/tui.d.ts +0 -48
  111. package/dist/tui.js +0 -393
  112. package/dist/update-check.d.ts +0 -31
  113. package/dist/update-check.js +0 -141
  114. package/dist/workspace.d.ts +0 -12
  115. package/dist/workspace.js +0 -1294
package/dist/prompts.js DELETED
@@ -1,418 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
3
- import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from 'node:fs';
4
- import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
- import { callControlAPI } from './api-client.js';
6
- import { createCommandContext } from './command-context.js';
7
- import { runPromptAb } from './prompts-ab.js';
8
- import { runPromptPropose } from './prompts-propose.js';
9
- const MODA_DIR = '.moda';
10
- const MANIFEST_PATH = join(MODA_DIR, 'prompts.yml');
11
- const LOCK_PATH = join(MODA_DIR, 'prompts.lock.json');
12
- const DEFAULT_PROMPT_PATHS = [
13
- 'prompts/**/*.prompt.md',
14
- 'prompts/**/*.prompt.json',
15
- 'prompts/**/*.prompt.yaml',
16
- 'prompts/**/*.prompt.yml',
17
- ];
18
- export async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
19
- const context = Array.isArray(input)
20
- ? createCommandContext({
21
- command: 'prompts',
22
- positional: input[0],
23
- positionals: input,
24
- flags: legacyFlags ?? {},
25
- }, {
26
- commandFlags: legacyFlags ?? {},
27
- outputMode: legacyFlags?.agent === 'true' ? 'agent' : legacyFlags?.json === 'true' ? 'json' : 'human',
28
- env: legacyProfileOptions?.env,
29
- })
30
- : input;
31
- const positionals = context.positionals;
32
- const flags = context.flags;
33
- const profileOptions = legacyProfileOptions ?? { profile: context.profile, env: context.env };
34
- const subcommand = positionals[0] || 'status';
35
- switch (subcommand) {
36
- case 'init':
37
- await initPrompts({ silent: true });
38
- context.output.writeData({
39
- manifest: MANIFEST_PATH,
40
- lockfile: LOCK_PATH,
41
- promptPaths: readManifest().promptPaths,
42
- });
43
- break;
44
- case 'status':
45
- context.output.writeData(await getPromptStatus());
46
- break;
47
- case 'diff':
48
- context.output.writeData(await getPromptStatus());
49
- break;
50
- case 'sync':
51
- await syncPrompts(flags, profileOptions, context);
52
- break;
53
- case 'promote':
54
- await promotePrompt(positionals, flags, profileOptions, context);
55
- break;
56
- case 'ab':
57
- await runPromptAb(flags, profileOptions, context);
58
- break;
59
- case 'propose':
60
- await runPromptPropose(positionals, flags, profileOptions, context);
61
- break;
62
- default:
63
- throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, sync, promote, ab, or propose.`);
64
- }
65
- }
66
- export async function initPrompts(options = {}) {
67
- mkdirSync(MODA_DIR, { recursive: true });
68
- if (!existsSync(MANIFEST_PATH)) {
69
- writeFileSync(MANIFEST_PATH, [
70
- 'version: 1',
71
- 'prompt_paths:',
72
- ...DEFAULT_PROMPT_PATHS.map((path) => ` - "${path}"`),
73
- '',
74
- ].join('\n'));
75
- }
76
- if (!options.silent) {
77
- printJson({
78
- manifest: MANIFEST_PATH,
79
- lockfile: LOCK_PATH,
80
- promptPaths: readManifest().promptPaths,
81
- });
82
- }
83
- }
84
- async function syncPrompts(flags, profileOptions, context) {
85
- if (flags.watch === 'true') {
86
- await watchPromptSync(flags, profileOptions);
87
- return;
88
- }
89
- await syncPromptsOnce(flags, profileOptions, context);
90
- }
91
- /**
92
- * Sync the manifest's prompt files to the backend and update the lockfile.
93
- * Returns the synced list without printing, so callers (e.g. `moda init`)
94
- * control their own output. `moda prompts sync` wraps this with JSON output.
95
- */
96
- export async function runPromptSync(flags, profileOptions) {
97
- await initPrompts({ silent: true });
98
- const prompts = discoverPrompts();
99
- const sourceCommit = getGitCommit();
100
- const manifestHash = hashFile(MANIFEST_PATH);
101
- const dryRun = flags['dry-run'] === 'true';
102
- const response = await callControlAPI('/prompts/sync', {
103
- method: 'POST',
104
- body: JSON.stringify({
105
- prompts: prompts.map((prompt) => ({
106
- ...prompt,
107
- sourceCommit,
108
- manifestHash,
109
- })),
110
- sourceCommit,
111
- manifestHash,
112
- dryRun,
113
- }),
114
- }, profileOptions);
115
- if (!dryRun) {
116
- writeLock(prompts, response.synced ?? []);
117
- }
118
- return { synced: response.synced ?? [], dryRun };
119
- }
120
- async function syncPromptsOnce(flags, profileOptions, context) {
121
- const result = await runPromptSync(flags, profileOptions);
122
- const payload = {
123
- synced: result.synced,
124
- lockfile: result.dryRun ? undefined : LOCK_PATH,
125
- };
126
- if (context)
127
- context.output.writeData(payload);
128
- else
129
- printJson(payload);
130
- }
131
- async function watchPromptSync(flags, profileOptions) {
132
- console.error('Watching prompt files. Press Ctrl+C to stop.');
133
- let lastSignature = '';
134
- const intervalMs = Math.max(1000, Number(flags.interval ?? 2500));
135
- while (true) {
136
- const status = await getPromptStatus();
137
- const signature = stableJson(status.prompts);
138
- if (signature !== lastSignature) {
139
- lastSignature = signature;
140
- if (status.changed > 0 || status.new > 0 || status.deleted > 0) {
141
- await syncPromptsOnce({ ...flags, watch: 'false' }, profileOptions);
142
- }
143
- }
144
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
145
- }
146
- }
147
- async function promotePrompt(positionals, flags, profileOptions, context) {
148
- const key = positionals[1];
149
- const label = flags.label;
150
- const versionId = flags.version || flags['version-id'];
151
- if (!key)
152
- throw new Error('Usage: moda prompts promote <prompt_key> --label=prod --version=<version_id>');
153
- if (!label)
154
- throw new Error('Missing required flag: --label=dev|staging|prod');
155
- if (!versionId)
156
- throw new Error('Missing required flag: --version=<version_id>');
157
- const response = await callControlAPI(`/prompts/${encodeURIComponent(key)}/labels`, {
158
- method: 'POST',
159
- body: JSON.stringify({ label, versionId }),
160
- }, profileOptions);
161
- context.output.writeData(response);
162
- }
163
- async function getPromptStatus() {
164
- await initPrompts({ silent: true });
165
- const prompts = discoverPrompts();
166
- const lock = readLock();
167
- const rows = prompts.map((prompt) => {
168
- const locked = lock?.prompts[prompt.key];
169
- const state = !locked
170
- ? 'new'
171
- : locked.contentHash === prompt.contentHash
172
- ? 'unchanged'
173
- : 'changed';
174
- return {
175
- key: prompt.key,
176
- sourcePath: prompt.sourcePath,
177
- contentHash: prompt.contentHash,
178
- lockedHash: locked?.contentHash ?? null,
179
- versionId: locked?.versionId ?? null,
180
- state,
181
- };
182
- });
183
- const discoveredKeys = new Set(prompts.map((prompt) => prompt.key));
184
- for (const [key, locked] of Object.entries(lock?.prompts ?? {})) {
185
- if (!discoveredKeys.has(key)) {
186
- rows.push({
187
- key,
188
- sourcePath: locked.sourcePath,
189
- contentHash: null,
190
- lockedHash: locked.contentHash,
191
- versionId: locked.versionId ?? null,
192
- state: 'deleted',
193
- });
194
- }
195
- }
196
- return {
197
- manifest: MANIFEST_PATH,
198
- lockfile: LOCK_PATH,
199
- total: rows.length,
200
- changed: rows.filter((row) => row.state === 'changed').length,
201
- new: rows.filter((row) => row.state === 'new').length,
202
- deleted: rows.filter((row) => row.state === 'deleted').length,
203
- prompts: rows,
204
- };
205
- }
206
- export function discoverPrompts() {
207
- const manifest = readManifest();
208
- // A pattern like `src/prompts/**/*.prompt.md` should *only* match files under
209
- // src/prompts. Extract both the base directory (everything up to the first
210
- // glob segment) and the file suffix so we can scope `walk()` accordingly.
211
- const rules = manifest.promptPaths
212
- .map((pattern) => {
213
- const suffixMatch = pattern.match(/\.prompt\.(md|json|ya?ml)$/i);
214
- if (!suffixMatch)
215
- return null;
216
- const suffix = `.prompt.${suffixMatch[1]}`;
217
- const globIndex = pattern.search(/[*?[]/);
218
- const baseRaw = globIndex === -1 ? dirname(pattern) : pattern.slice(0, globIndex);
219
- const base = baseRaw.replace(/[\\/]+$/, '') || '.';
220
- const baseAbs = isAbsolute(base) ? base : resolve(process.cwd(), base);
221
- return { baseAbs, suffix };
222
- })
223
- .filter((rule) => Boolean(rule));
224
- const effectiveRules = rules.length
225
- ? rules
226
- : [
227
- { baseAbs: process.cwd(), suffix: '.prompt.md' },
228
- { baseAbs: process.cwd(), suffix: '.prompt.json' },
229
- ];
230
- const discovered = new Set();
231
- for (const { baseAbs, suffix } of effectiveRules) {
232
- if (!existsSync(baseAbs))
233
- continue;
234
- for (const file of walk(baseAbs)) {
235
- if (file.endsWith(suffix))
236
- discovered.add(file);
237
- }
238
- }
239
- return [...discovered].sort().map(parsePromptFile);
240
- }
241
- function parsePromptFile(absPath) {
242
- const sourcePath = relative(process.cwd(), absPath);
243
- const raw = readFileSync(absPath, 'utf8');
244
- if (absPath.endsWith('.json')) {
245
- const parsed = JSON.parse(raw);
246
- const key = parsed.key || keyFromPath(sourcePath);
247
- const content = parsed.content || JSON.stringify(parsed.messages ?? [], null, 2);
248
- return normalizePrompt({ ...parsed, key, name: parsed.name || key, sourcePath, content });
249
- }
250
- const { frontmatter, body } = parseFrontmatter(raw);
251
- const key = frontmatter.key || keyFromPath(sourcePath);
252
- return normalizePrompt({
253
- key,
254
- name: frontmatter.name || key,
255
- description: frontmatter.description,
256
- category: frontmatter.category,
257
- systemPrompt: frontmatter.system_prompt || frontmatter.systemPrompt,
258
- modelConfig: frontmatter.model ? { model: frontmatter.model } : undefined,
259
- sourcePath,
260
- content: body.trim(),
261
- });
262
- }
263
- function normalizePrompt(prompt) {
264
- const normalized = stableJson({
265
- key: prompt.key,
266
- content: prompt.content,
267
- systemPrompt: prompt.systemPrompt ?? null,
268
- messages: prompt.messages ?? [],
269
- variables: prompt.variables ?? [],
270
- variablesSchema: prompt.variablesSchema ?? {},
271
- modelConfig: prompt.modelConfig ?? {},
272
- toolRefs: prompt.toolRefs ?? [],
273
- responseSchema: prompt.responseSchema ?? null,
274
- });
275
- return {
276
- key: prompt.key,
277
- name: prompt.name || prompt.key,
278
- description: prompt.description,
279
- sourcePath: prompt.sourcePath,
280
- content: prompt.content,
281
- systemPrompt: prompt.systemPrompt,
282
- messages: prompt.messages,
283
- variables: prompt.variables,
284
- variablesSchema: prompt.variablesSchema,
285
- modelConfig: prompt.modelConfig,
286
- toolRefs: prompt.toolRefs,
287
- responseSchema: prompt.responseSchema,
288
- contentHash: sha256(normalized),
289
- };
290
- }
291
- function readManifest() {
292
- if (!existsSync(MANIFEST_PATH)) {
293
- return { promptPaths: DEFAULT_PROMPT_PATHS };
294
- }
295
- const text = readFileSync(MANIFEST_PATH, 'utf8');
296
- const promptPaths = [...text.matchAll(/^\s*-\s*["']?([^"'\n]+)["']?\s*$/gm)]
297
- .map((match) => match[1])
298
- .filter((path) => path.includes('.prompt.'));
299
- return { promptPaths: promptPaths.length ? promptPaths : DEFAULT_PROMPT_PATHS };
300
- }
301
- export function readPromptLock() {
302
- return readLock();
303
- }
304
- /** Parse a prompt file for A/B runs (body text + registry key). */
305
- export function parsePromptFileForAb(absPath) {
306
- const parsed = parsePromptFile(absPath);
307
- return { key: parsed.key, content: parsed.content };
308
- }
309
- function readLock() {
310
- if (!existsSync(LOCK_PATH))
311
- return null;
312
- return JSON.parse(readFileSync(LOCK_PATH, 'utf8'));
313
- }
314
- function writeLock(prompts, synced) {
315
- const syncedByKey = new Map(synced.map((item) => [item.key, item]));
316
- const lock = {
317
- version: 1,
318
- generatedAt: new Date().toISOString(),
319
- prompts: Object.fromEntries(prompts.map((prompt) => {
320
- const result = syncedByKey.get(prompt.key);
321
- return [prompt.key, {
322
- key: prompt.key,
323
- sourcePath: prompt.sourcePath,
324
- contentHash: prompt.contentHash,
325
- promptId: result?.promptId,
326
- versionId: result?.versionId,
327
- }];
328
- })),
329
- };
330
- mkdirSync(dirname(LOCK_PATH), { recursive: true });
331
- writeFileSync(LOCK_PATH, `${JSON.stringify(lock, null, 2)}\n`);
332
- }
333
- function walk(dir) {
334
- let entries;
335
- try {
336
- entries = readdirSync(dir);
337
- }
338
- catch {
339
- return [];
340
- }
341
- const files = [];
342
- for (const entry of entries) {
343
- if (entry === 'node_modules' || entry === '.git' || entry === 'dist' || entry === 'build')
344
- continue;
345
- const fullPath = join(dir, entry);
346
- // lstatSync — never follow symlinks. A symlink in the tree could otherwise
347
- // (a) loop into an ancestor and recurse forever, or (b) silently point at a
348
- // sensitive file like .env that would then get uploaded as prompt content.
349
- let stat;
350
- try {
351
- stat = lstatSync(fullPath);
352
- }
353
- catch {
354
- continue;
355
- }
356
- if (stat.isSymbolicLink())
357
- continue;
358
- if (stat.isDirectory())
359
- files.push(...walk(fullPath));
360
- else if (stat.isFile())
361
- files.push(fullPath);
362
- }
363
- return files;
364
- }
365
- function parseFrontmatter(raw) {
366
- if (!raw.startsWith('---'))
367
- return { frontmatter: {}, body: raw };
368
- const end = raw.indexOf('\n---', 3);
369
- if (end === -1)
370
- return { frontmatter: {}, body: raw };
371
- const frontmatterText = raw.slice(3, end).trim();
372
- const frontmatter = Object.fromEntries(frontmatterText
373
- .split('\n')
374
- .map((line) => line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/))
375
- .filter((match) => Boolean(match))
376
- .map((match) => [match[1], match[2].replace(/^["']|["']$/g, '')]));
377
- return { frontmatter, body: raw.slice(end + 4) };
378
- }
379
- function keyFromPath(path) {
380
- return path
381
- .replace(/^prompts\//, '')
382
- .replace(/\.prompt\.(md|json|ya?ml)$/i, '')
383
- .replace(/[\\/]/g, '.');
384
- }
385
- function hashFile(path) {
386
- return existsSync(path) ? sha256(readFileSync(path, 'utf8')) : '';
387
- }
388
- function getGitCommit() {
389
- try {
390
- return execFileSync('git', ['rev-parse', 'HEAD'], {
391
- encoding: 'utf8',
392
- stdio: ['ignore', 'pipe', 'ignore'],
393
- }).trim();
394
- }
395
- catch {
396
- return undefined;
397
- }
398
- }
399
- function stableJson(value) {
400
- return JSON.stringify(sortJson(value));
401
- }
402
- function sortJson(value) {
403
- if (Array.isArray(value))
404
- return value.map(sortJson);
405
- if (value && typeof value === 'object') {
406
- return Object.fromEntries(Object.entries(value)
407
- .sort(([a], [b]) => a.localeCompare(b))
408
- .map(([key, item]) => [key, sortJson(item)]));
409
- }
410
- return value;
411
- }
412
- function sha256(input) {
413
- return createHash('sha256').update(input).digest('hex');
414
- }
415
- function printJson(value) {
416
- console.log(JSON.stringify(value, null, 2));
417
- }
418
- //# sourceMappingURL=prompts.js.map
@@ -1,28 +0,0 @@
1
- import { type CommandContext } from './command-context.js';
2
- export interface ProvisionRunOptions {
3
- /**
4
- * Original invocation as an argv array (e.g. ['moda', 'provision',
5
- * '--save']), threaded into recoverable failures so the caller knows
6
- * exactly how to resume after logging in.
7
- */
8
- resumeCommand?: string[];
9
- }
10
- export interface ProvisionResult {
11
- exitCode: number;
12
- apiKey?: string;
13
- }
14
- /**
15
- * The credentials document printed to stdout. Field names are a public
16
- * contract (`moda provision | jq -r .apiKey`); do not rename.
17
- */
18
- export interface ProvisionPayload {
19
- apiKey: string;
20
- tenantId: string;
21
- tenantName: string;
22
- tenantSlug: string;
23
- ingestUrl: string;
24
- baseUrl: string;
25
- reused: boolean;
26
- }
27
- export declare function runProvision(context: CommandContext, options?: ProvisionRunOptions): Promise<ProvisionResult>;
28
- //# sourceMappingURL=provision.d.ts.map
package/dist/provision.js DELETED
@@ -1,104 +0,0 @@
1
- // src/provision.ts — moda provision: headless tenant + API key provisioning
2
- // for CI jobs and agents. Requires an existing CLI session (moda auth login
3
- // run previously); browserless signup is explicitly out of scope (decision
4
- // D3), so a missing session fails fast with CliAuthError instead of opening
5
- // a browser.
6
- import { loadAuthSession } from './auth.js';
7
- import { stringOption } from './command-context.js';
8
- import { resolveIngestUrl, resolveModaBaseUrl } from './config.js';
9
- import { CliAuthError } from './errors.js';
10
- import { selectTenantAndCreateKey } from './init/tenant.js';
11
- export async function runProvision(context, options = {}) {
12
- const resumeCommand = options.resumeCommand ?? ['moda', 'provision'];
13
- // Pre-coercion raw values (like init's tenantIdFlag/profileFlag): the
14
- // dispatcher's flagsToArgs number-coerces numeric-looking values, which
15
- // must never happen to tenant ids, profile names, or labels.
16
- const profileFlag = stringOption(context.rawFlags.profile);
17
- const authOptions = { profile: profileFlag, env: context.env };
18
- // Provision never opens a browser (headless by contract), so the session
19
- // check happens up front: no stored session is a recoverable exit-4
20
- // failure with explicit instructions, not a login prompt.
21
- const session = loadAuthSession(authOptions);
22
- if (!session) {
23
- const expiredSession = loadAuthSession({ ...authOptions, includeExpired: true });
24
- throw new CliAuthError('moda provision requires an existing CLI session and never opens a browser. ' +
25
- 'Run "moda auth login" on a machine with a browser first, then re-run provision here ' +
26
- '(or skip provisioning and set MODA_API_KEY directly).', {
27
- reason: expiredSession ? 'token_expired' : 'no_stored_credentials',
28
- login_command: ['moda', 'auth', 'login'],
29
- resume_command: resumeCommand,
30
- });
31
- }
32
- const flagTenantId = stringOption(context.rawFlags['tenant-id'] ?? context.rawFlags.tenant_id);
33
- const envTenantId = stringOption(context.env.MODA_TENANT_ID);
34
- const label = stringOption(context.rawFlags.label);
35
- const save = context.flags.save === 'true';
36
- // Reuses init's dedup + bootstrap machinery (stored-key liveness probe,
37
- // Idempotency-Key persistence, label, cap/viewer errors). Multi-tenant
38
- // accounts without --tenant-id/MODA_TENANT_ID throw CliInputRequiredError
39
- // (exit 5) with per-tenant resume flags — even on a TTY, provision is
40
- // headless by contract and never prompts.
41
- let result;
42
- try {
43
- result = await selectTenantAndCreateKey({
44
- tenantId: flagTenantId ?? envTenantId,
45
- tenantIdSource: flagTenantId !== undefined ? 'flag' : envTenantId !== undefined ? 'env' : undefined,
46
- // Unconditional (init derives this from flags/TTY): provision is
47
- // headless by contract even on an interactive terminal.
48
- nonInteractive: true,
49
- // stdout must stay exactly one JSON credentials document; all provision
50
- // prose goes to stderr, so suppress the tenant-selection prose entirely.
51
- silent: true,
52
- // Multi-tenant runs surface CliInputRequiredError (exit 5); echo this
53
- // invocation's resume command so the caller knows how to continue.
54
- resumeCommand,
55
- label,
56
- // Headless callers own their credential storage; --save opts into the
57
- // init-style ~/.moda/config.json persistence.
58
- persistConfig: save,
59
- profile: profileFlag,
60
- env: context.env,
61
- });
62
- }
63
- catch (error) {
64
- // A mid-flight 401 classified by authFetch (token_expired /
65
- // token_revoked / renew_failed during the context or bootstrap call)
66
- // throws a CliAuthError that carries only the login command. Re-throw
67
- // with this invocation's resume command attached so the caller knows how
68
- // to continue after recovering, matching the upfront session gate above.
69
- if (error instanceof CliAuthError && !error.auth.resume_command) {
70
- throw new CliAuthError(error.message, {
71
- reason: error.auth.reason,
72
- login_command: error.auth.login_command,
73
- resume_command: resumeCommand,
74
- });
75
- }
76
- throw error;
77
- }
78
- const payload = {
79
- apiKey: result.apiKey,
80
- tenantId: result.tenantId,
81
- tenantName: result.tenantName,
82
- tenantSlug: result.tenantSlug,
83
- ingestUrl: resolveIngestUrl(authOptions),
84
- baseUrl: resolveModaBaseUrl(authOptions),
85
- reused: result.reused,
86
- };
87
- // Security note on stderr, never stdout: stdout carries a live credential
88
- // and must stay exactly one parseable JSON document.
89
- console.error('Output contains a live API key. Avoid piping to logs.');
90
- if (save) {
91
- console.error('Saved API key + tenant to ~/.moda/config.json (--save).');
92
- }
93
- // `moda provision` exists to be composed: `KEY=$(moda provision | jq -r
94
- // .apiKey)`. Command substitution IS a pipe, so stdout is a non-TTY and
95
- // the CLI would auto-select agent mode and wrap the payload in a
96
- // moda.agent.v1 envelope, burying apiKey under `.data`. Like `moda auth
97
- // token`, this command therefore deliberately bypasses the CliOutput
98
- // envelope machinery in EVERY output mode (human/json/agent) and writes
99
- // exactly one JSON document to stdout — nothing else. Human TTY runs get
100
- // the same machine-parseable JSON: credentials output is the whole point.
101
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
102
- return { exitCode: 0, apiKey: result.apiKey };
103
- }
104
- //# sourceMappingURL=provision.js.map
package/dist/redact.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export declare function redactDeep<T>(value: T): T;
2
- //# sourceMappingURL=redact.d.ts.map
package/dist/redact.js DELETED
@@ -1,90 +0,0 @@
1
- const REDACTED = '[REDACTED]';
2
- // Pure stack-safety bound. Cycles are handled separately by the `seen` WeakSet,
3
- // so this only needs to be high enough that legitimate nested payloads (the
4
- // agent-envelope wrapper plus arbitrary customer tool-call JSON) are never
5
- // silently truncated to '[MaxDepth]'. 256 is far below any real recursion limit.
6
- const MAX_DEPTH = 256;
7
- const SECRET_KEYS = new Set([
8
- 'api_key',
9
- 'apikey',
10
- 'token',
11
- 'access_token',
12
- 'authorization',
13
- 'secret',
14
- 'password',
15
- 'cli_token',
16
- ]);
17
- const SECRET_PATTERNS = [
18
- { pattern: /\bmoda_sk_[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
19
- { pattern: /\bmoda_ct_[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
20
- { pattern: /\bBearer\s+([A-Za-z0-9._~+/-]+=*)\b/gi, prefixLength: 8 },
21
- { pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
22
- // Third-party provider credentials that can appear inline in scanned source
23
- // (e.g. harness-candidate excerpts) — the detectors deliberately fire on the
24
- // very lines most likely to carry hardcoded keys.
25
- { pattern: /\bsk-ant-api\d{2}-[A-Za-z0-9_-]{20,}\b/g, prefixLength: 12 }, // Anthropic
26
- { pattern: /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}\b/g, prefixLength: 8 }, // OpenAI
27
- { pattern: /\bAKIA[A-Z0-9]{16}\b/g, prefixLength: 8 }, // AWS access key id
28
- { pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, prefixLength: 8 }, // Google API key
29
- { pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g, prefixLength: 8 }, // GitHub token
30
- { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, prefixLength: 8 }, // Slack token
31
- ];
32
- export function redactDeep(value) {
33
- return redactValue(value, 0, new WeakSet());
34
- }
35
- function redactValue(value, depth, seen) {
36
- if (typeof value === 'string')
37
- return redactString(value);
38
- if (value === null || typeof value !== 'object')
39
- return value;
40
- if (depth >= MAX_DEPTH)
41
- return '[MaxDepth]';
42
- if (seen.has(value))
43
- return '[Circular]';
44
- seen.add(value);
45
- if (Array.isArray(value)) {
46
- try {
47
- return value.map((item) => redactValue(item, depth + 1, seen));
48
- }
49
- finally {
50
- seen.delete(value);
51
- }
52
- }
53
- const out = {};
54
- try {
55
- for (const [key, item] of Object.entries(value)) {
56
- out[key] = shouldRedactKey(key) ? redactSecretValue(item) : redactValue(item, depth + 1, seen);
57
- }
58
- }
59
- finally {
60
- seen.delete(value);
61
- }
62
- return out;
63
- }
64
- function shouldRedactKey(key) {
65
- return SECRET_KEYS.has(key.replace(/[-\s]/g, '_').toLowerCase());
66
- }
67
- function redactSecretValue(value) {
68
- if (typeof value !== 'string')
69
- return REDACTED;
70
- const redacted = redactString(value);
71
- if (redacted !== value)
72
- return redacted;
73
- if (/^(configured|missing|not_configured|none|present|absent)$/i.test(value))
74
- return value;
75
- return REDACTED;
76
- }
77
- function redactString(input) {
78
- let output = input;
79
- for (const { pattern, prefixLength } of SECRET_PATTERNS) {
80
- output = output.replace(pattern, (match) => {
81
- if (/^Bearer\s+/i.test(match)) {
82
- const token = match.replace(/^Bearer\s+/i, '');
83
- return `Bearer ${token.slice(0, prefixLength)}...[REDACTED]`;
84
- }
85
- return `${match.slice(0, prefixLength)}...[REDACTED]`;
86
- });
87
- }
88
- return output;
89
- }
90
- //# sourceMappingURL=redact.js.map