@moda-ai/cli 1.22.1 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/cli-c4se7a5d.js +762 -0
  2. package/dist/cli-mnqa4rp6.js +1563 -0
  3. package/dist/cli-stmjdgvy.js +2842 -0
  4. package/dist/cli-y55bgg14.js +6980 -0
  5. package/dist/cli-ysegdrm5.js +286 -0
  6. package/dist/cli.js +4779 -1159
  7. package/dist/harness-github-actions-7t3cncnt.js +95 -0
  8. package/dist/index-4ser01zr.js +2769 -0
  9. package/dist/provision-hm1ppwj6.js +74 -0
  10. package/package.json +3 -2
  11. package/skills/integration/index.json +2 -2
  12. package/dist/agent-schemas.d.ts +0 -3
  13. package/dist/agent-schemas.js +0 -358
  14. package/dist/agent.d.ts +0 -5
  15. package/dist/agent.js +0 -477
  16. package/dist/analyst-tui.d.ts +0 -81
  17. package/dist/analyst-tui.js +0 -314
  18. package/dist/api-client.d.ts +0 -46
  19. package/dist/api-client.js +0 -120
  20. package/dist/ask-stream.d.ts +0 -146
  21. package/dist/ask-stream.js +0 -473
  22. package/dist/auth-storage.d.ts +0 -35
  23. package/dist/auth-storage.js +0 -159
  24. package/dist/auth.d.ts +0 -50
  25. package/dist/auth.js +0 -694
  26. package/dist/cli-session.d.ts +0 -26
  27. package/dist/cli-session.js +0 -169
  28. package/dist/cli.d.ts +0 -44
  29. package/dist/command-context.d.ts +0 -62
  30. package/dist/command-context.js +0 -57
  31. package/dist/command-registry.d.ts +0 -22
  32. package/dist/command-registry.js +0 -15
  33. package/dist/config.d.ts +0 -60
  34. package/dist/config.js +0 -110
  35. package/dist/doctor.d.ts +0 -79
  36. package/dist/doctor.js +0 -1182
  37. package/dist/errors.d.ts +0 -50
  38. package/dist/errors.js +0 -64
  39. package/dist/exit-funnel.d.ts +0 -14
  40. package/dist/exit-funnel.js +0 -53
  41. package/dist/harness-artifact-bodies.d.ts +0 -28
  42. package/dist/harness-artifact-bodies.js +0 -276
  43. package/dist/harness-candidates.d.ts +0 -77
  44. package/dist/harness-candidates.js +0 -482
  45. package/dist/harness-remote.d.ts +0 -135
  46. package/dist/harness-remote.js +0 -540
  47. package/dist/harness-report.d.ts +0 -145
  48. package/dist/harness-report.js +0 -1579
  49. package/dist/harness-sync-limits.d.ts +0 -8
  50. package/dist/harness-sync-limits.js +0 -21
  51. package/dist/harness.d.ts +0 -463
  52. package/dist/harness.js +0 -4009
  53. package/dist/init/index.d.ts +0 -78
  54. package/dist/init/index.js +0 -1442
  55. package/dist/init/mcp.d.ts +0 -2
  56. package/dist/init/mcp.js +0 -27
  57. package/dist/init/progress-board.d.ts +0 -80
  58. package/dist/init/progress-board.js +0 -225
  59. package/dist/init/rules.d.ts +0 -6
  60. package/dist/init/rules.js +0 -171
  61. package/dist/init/sdk-catalog.d.ts +0 -10
  62. package/dist/init/sdk-catalog.js +0 -80
  63. package/dist/init/sdk-guide.d.ts +0 -19
  64. package/dist/init/sdk-guide.js +0 -185
  65. package/dist/init/sdk-integration.d.ts +0 -53
  66. package/dist/init/sdk-integration.js +0 -835
  67. package/dist/init/sdk-protocol.d.ts +0 -135
  68. package/dist/init/sdk-protocol.js +0 -152
  69. package/dist/init/sdk-verification.d.ts +0 -27
  70. package/dist/init/sdk-verification.js +0 -123
  71. package/dist/init/tenant.d.ts +0 -67
  72. package/dist/init/tenant.js +0 -421
  73. package/dist/intelligence-schema.d.ts +0 -349
  74. package/dist/intelligence-schema.js +0 -174
  75. package/dist/keys.d.ts +0 -47
  76. package/dist/keys.js +0 -136
  77. package/dist/manifest.d.ts +0 -5
  78. package/dist/manifest.js +0 -114
  79. package/dist/markdown-terminal.d.ts +0 -49
  80. package/dist/markdown-terminal.js +0 -212
  81. package/dist/output.d.ts +0 -191
  82. package/dist/output.js +0 -624
  83. package/dist/production-intelligence.d.ts +0 -175
  84. package/dist/production-intelligence.js +0 -1430
  85. package/dist/profile-commands.d.ts +0 -10
  86. package/dist/profile-commands.js +0 -342
  87. package/dist/profiles.d.ts +0 -112
  88. package/dist/profiles.js +0 -573
  89. package/dist/prompts-ab.d.ts +0 -71
  90. package/dist/prompts-ab.js +0 -313
  91. package/dist/prompts-propose.d.ts +0 -15
  92. package/dist/prompts-propose.js +0 -212
  93. package/dist/prompts.d.ts +0 -58
  94. package/dist/prompts.js +0 -418
  95. package/dist/provision.d.ts +0 -28
  96. package/dist/provision.js +0 -104
  97. package/dist/redact.d.ts +0 -2
  98. package/dist/redact.js +0 -90
  99. package/dist/schemas.d.ts +0 -225
  100. package/dist/schemas.js +0 -126
  101. package/dist/skills-bundled.d.ts +0 -35
  102. package/dist/skills-bundled.js +0 -157
  103. package/dist/skills-manifest.d.ts +0 -33
  104. package/dist/skills-manifest.js +0 -244
  105. package/dist/skills.d.ts +0 -57
  106. package/dist/skills.js +0 -783
  107. package/dist/sync.d.ts +0 -5
  108. package/dist/sync.js +0 -36
  109. package/dist/telemetry.d.ts +0 -56
  110. package/dist/telemetry.js +0 -167
  111. package/dist/transforms.d.ts +0 -25
  112. package/dist/transforms.js +0 -102
  113. package/dist/tui.d.ts +0 -48
  114. package/dist/tui.js +0 -393
  115. package/dist/update-check.d.ts +0 -31
  116. package/dist/update-check.js +0 -141
  117. package/dist/workspace.d.ts +0 -12
  118. 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