@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
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
|
package/dist/provision.d.ts
DELETED
|
@@ -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
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
|