@moda-ai/cli 1.55.94 → 1.55.96
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/README.md +23 -0
- package/dist/{agent-pncdccck.js → agent-wa8tx02f.js} +6 -5
- package/dist/{change-impact-2hqk7kj0.js → change-impact-3n23nhd8.js} +3 -3
- package/dist/{cli-5c37h7n5.js → cli-1sdz17sn.js} +40 -16
- package/dist/{cli-dk08ywkn.js → cli-764m5azg.js} +2 -1031
- package/dist/cli-92zfpz9f.js +1037 -0
- package/dist/{cli-gdm94hh4.js → cli-abfcqf4p.js} +2 -2
- package/dist/{index-61yya1cm.js → cli-f1sx9dsa.js} +3 -21
- package/dist/cli-gcqpbes6.js +1660 -0
- package/dist/cli.js +31 -17
- package/dist/{doctor-kxk2j3j2.js → doctor-ec2cqgnv.js} +5 -4
- package/dist/{harness-github-actions-75z9c68t.js → harness-github-actions-h9b973z0.js} +3 -3
- package/dist/{harness-wnye9acm.js → harness-jka07f99.js} +3 -3
- package/dist/{index-09t1rsf2.js → index-0na4ydkc.js} +3 -3
- package/dist/{index-e0b4bd74.js → index-w2hs49tb.js} +12 -11
- package/dist/index-zp7y6mft.js +31 -0
- package/dist/{production-intelligence-1y884f69.js → production-intelligence-3fdpsxce.js} +2 -2
- package/dist/{prompt-attribution-6zeqqqfy.js → prompt-attribution-armk5gqt.js} +3 -3
- package/dist/prompt-source-mcp.js +3 -3
- package/dist/{prompts-9ngt4wyh.js → prompts-x3vdeax6.js} +4 -3
- package/dist/{skills-hf8fa7w7.js → skills-1pv2mp9v.js} +3 -3
- package/dist/{sync-zsyy15p9.js → sync-4zeyb408.js} +8 -7
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/dist/{cli-wm0p9kn2.js → cli-9kxz65fe.js} +3 -3
- package/dist/{cli-t8vmcbe0.js → cli-f0z97a7t.js} +3 -3
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computePromptContentHash,
|
|
3
|
+
stableJson
|
|
4
|
+
} from "./cli-mm48br00.js";
|
|
5
|
+
import {
|
|
6
|
+
require_out
|
|
7
|
+
} from "./cli-764m5azg.js";
|
|
8
|
+
import {
|
|
9
|
+
createCommandContext
|
|
10
|
+
} from "./cli-13zdcv4w.js";
|
|
11
|
+
import {
|
|
12
|
+
assertFlags,
|
|
13
|
+
assertPositionals
|
|
14
|
+
} from "./cli-6ga6kmdp.js";
|
|
15
|
+
import {
|
|
16
|
+
callControlAPI
|
|
17
|
+
} from "./cli-dxajrq9h.js";
|
|
18
|
+
import {
|
|
19
|
+
CliInputError
|
|
20
|
+
} from "./cli-k4tqwah4.js";
|
|
21
|
+
import {
|
|
22
|
+
$isSeq,
|
|
23
|
+
$parseDocument,
|
|
24
|
+
$stringify
|
|
25
|
+
} from "./cli-9kfxkxmk.js";
|
|
26
|
+
import {
|
|
27
|
+
__require,
|
|
28
|
+
__toESM
|
|
29
|
+
} from "./cli-0v6na3yp.js";
|
|
30
|
+
|
|
31
|
+
// src/prompts.ts
|
|
32
|
+
var import_fast_glob = __toESM(require_out(), 1);
|
|
33
|
+
import { execFileSync } from "node:child_process";
|
|
34
|
+
import { createHash } from "node:crypto";
|
|
35
|
+
import {
|
|
36
|
+
existsSync,
|
|
37
|
+
lstatSync,
|
|
38
|
+
mkdirSync,
|
|
39
|
+
readFileSync,
|
|
40
|
+
realpathSync,
|
|
41
|
+
renameSync,
|
|
42
|
+
rmSync,
|
|
43
|
+
writeFileSync
|
|
44
|
+
} from "node:fs";
|
|
45
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
46
|
+
var promptSourcesModule;
|
|
47
|
+
async function loadPromptSources() {
|
|
48
|
+
promptSourcesModule ??= await import("./prompt-sources-cyqr5m4h.js");
|
|
49
|
+
return promptSourcesModule;
|
|
50
|
+
}
|
|
51
|
+
var MODA_DIR = ".moda";
|
|
52
|
+
var MANIFEST_PATH = join(MODA_DIR, "prompts.yml");
|
|
53
|
+
var LOCK_PATH = join(MODA_DIR, "prompts.lock.json");
|
|
54
|
+
var DISCOVERY_IGNORE = ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.env*", "**/*.pem", "**/*.key"];
|
|
55
|
+
var DEFAULT_PROMPT_PATHS = [
|
|
56
|
+
"prompts/**/*.prompt.md",
|
|
57
|
+
"prompts/**/*.prompt.json",
|
|
58
|
+
"prompts/**/*.prompt.yaml",
|
|
59
|
+
"prompts/**/*.prompt.yml"
|
|
60
|
+
];
|
|
61
|
+
var PROMPT_WRITE_SUBCOMMAND_FLAGS = {
|
|
62
|
+
init: ["from-harness"],
|
|
63
|
+
status: [],
|
|
64
|
+
diff: [],
|
|
65
|
+
migrate: ["dry-run", "discover"],
|
|
66
|
+
sync: [
|
|
67
|
+
"dry-run",
|
|
68
|
+
"from-harness",
|
|
69
|
+
"no-analyze",
|
|
70
|
+
"allow-untrack",
|
|
71
|
+
"watch",
|
|
72
|
+
"interval",
|
|
73
|
+
"analyst",
|
|
74
|
+
"assist",
|
|
75
|
+
"analyst-command",
|
|
76
|
+
"analyst-model"
|
|
77
|
+
],
|
|
78
|
+
promote: ["label", "version", "version-id"]
|
|
79
|
+
};
|
|
80
|
+
async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
|
|
81
|
+
const context = Array.isArray(input) ? createCommandContext({
|
|
82
|
+
command: "prompts",
|
|
83
|
+
positional: input[0],
|
|
84
|
+
positionals: input,
|
|
85
|
+
flags: legacyFlags ?? {}
|
|
86
|
+
}, {
|
|
87
|
+
commandFlags: legacyFlags ?? {},
|
|
88
|
+
outputMode: legacyFlags?.agent === "true" ? "agent" : legacyFlags?.json === "true" ? "json" : "human",
|
|
89
|
+
env: legacyProfileOptions?.env
|
|
90
|
+
}) : input;
|
|
91
|
+
const positionals = context.positionals;
|
|
92
|
+
const flags = context.flags;
|
|
93
|
+
const profileOptions = legacyProfileOptions ?? { profile: context.profile, env: context.env };
|
|
94
|
+
const subcommand = positionals[0] || "status";
|
|
95
|
+
const writeFlags = PROMPT_WRITE_SUBCOMMAND_FLAGS[subcommand];
|
|
96
|
+
if (writeFlags)
|
|
97
|
+
assertFlags(`prompts ${subcommand}`, flags, writeFlags);
|
|
98
|
+
switch (subcommand) {
|
|
99
|
+
case "init": {
|
|
100
|
+
const imported = flags["from-harness"] ? await importHarnessPromptSources(flags["from-harness"]) : undefined;
|
|
101
|
+
const init = await initPrompts({ silent: true });
|
|
102
|
+
context.output.writeData({
|
|
103
|
+
manifest: MANIFEST_PATH,
|
|
104
|
+
manifestPath: init.manifestPath,
|
|
105
|
+
lockfile: LOCK_PATH,
|
|
106
|
+
lockPath: init.lockPath,
|
|
107
|
+
cwd: init.cwd,
|
|
108
|
+
created: init.created,
|
|
109
|
+
promptPaths: init.promptPaths,
|
|
110
|
+
imported
|
|
111
|
+
}, {
|
|
112
|
+
artifacts: [{
|
|
113
|
+
kind: "prompt_manifest",
|
|
114
|
+
path: init.manifestPath,
|
|
115
|
+
description: init.created ? "Prompt discovery manifest created by this run." : "Prompt discovery manifest already existed; left unchanged."
|
|
116
|
+
}],
|
|
117
|
+
...imported?.skipped.length ? { warnings: [harnessSkipWarning(imported)] } : {}
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "status": {
|
|
122
|
+
const status = await getPromptStatus();
|
|
123
|
+
const warnings = promptManifestWarnings(status);
|
|
124
|
+
context.output.writeData(status, warnings.length > 0 ? { warnings } : undefined);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case "migrate": {
|
|
128
|
+
assertPositionals("prompts migrate", positionals, 1, "moda prompts migrate [--dry-run] [--discover]");
|
|
129
|
+
const result = await migratePromptManifest({
|
|
130
|
+
dryRun: flags["dry-run"] === "true",
|
|
131
|
+
discover: flags.discover === "true",
|
|
132
|
+
env: profileOptions.env
|
|
133
|
+
});
|
|
134
|
+
context.output.writeData(result, {
|
|
135
|
+
summary: {
|
|
136
|
+
text: !result.changed ? `${MANIFEST_PATH} is already version 2 with nothing to add.` : `${result.dryRun ? "Would migrate" : "Migrated"} ${MANIFEST_PATH} from version ${result.fromVersion} to 2: ${result.addedPaths.length} legacy-discovered path(s), ${result.addedSources.length} discovered source(s) added.`,
|
|
137
|
+
confidence: "high"
|
|
138
|
+
},
|
|
139
|
+
...result.changed && !result.dryRun ? {
|
|
140
|
+
artifacts: [{ kind: "prompt_manifest", path: result.manifestPath, description: "Prompt manifest rewritten to version 2." }]
|
|
141
|
+
} : {},
|
|
142
|
+
...migrateWarnings(result).length > 0 ? { warnings: migrateWarnings(result) } : {}
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case "diff": {
|
|
147
|
+
const status = await getPromptStatus();
|
|
148
|
+
const differing = status.prompts.filter((row) => row.state !== "unchanged");
|
|
149
|
+
const tracked = existsSync(MANIFEST_PATH);
|
|
150
|
+
const diffWarnings = tracked ? promptManifestWarnings(status) : [
|
|
151
|
+
`No ${MANIFEST_PATH} here, so diff has no last sync to compare against: it only covers prompts tracked by \`moda prompts sync\`${differing.length > 0 ? ", and every discovered prompt file lists as new" : ""}. For a registry manifest, preview with \`moda registry push --dry-run\`.`
|
|
152
|
+
];
|
|
153
|
+
context.output.writeData({
|
|
154
|
+
...status,
|
|
155
|
+
unchanged: status.total - differing.length,
|
|
156
|
+
prompts: differing,
|
|
157
|
+
...tracked ? {} : { tracked: false }
|
|
158
|
+
}, {
|
|
159
|
+
summary: {
|
|
160
|
+
text: !tracked ? differing.length === 0 ? `Not a prompts-sync project (no ${MANIFEST_PATH}) and no prompt files found; diff has nothing to compare.` : `Not a prompts-sync project (no ${MANIFEST_PATH}): ${differing.length} discovered prompt file(s) are untracked and would be new on \`moda prompts sync\`.` : differing.length === 0 ? "No prompt differs from the last sync." : `${differing.length} prompt(s) differ from the last sync (changed ${status.changed}, new ${status.new}, deleted ${status.deleted}).`,
|
|
161
|
+
confidence: "high"
|
|
162
|
+
},
|
|
163
|
+
...diffWarnings.length > 0 ? { warnings: diffWarnings } : {}
|
|
164
|
+
});
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "sync":
|
|
168
|
+
await syncPrompts(flags, profileOptions, context);
|
|
169
|
+
break;
|
|
170
|
+
case "promote":
|
|
171
|
+
await promotePrompt(positionals, flags, profileOptions, context);
|
|
172
|
+
break;
|
|
173
|
+
case "list":
|
|
174
|
+
case "show":
|
|
175
|
+
case "proposals":
|
|
176
|
+
case "usage":
|
|
177
|
+
case "decide":
|
|
178
|
+
await runPromptRegistryRead(subcommand, positionals, flags, profileOptions, context);
|
|
179
|
+
break;
|
|
180
|
+
default:
|
|
181
|
+
throw new CliInputError(`Unknown prompts command '${subcommand}'. Use init, status, diff, migrate, sync, promote, list, show, proposals, usage, or decide.`, undefined, "unknown_subcommand");
|
|
182
|
+
}
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
async function initPrompts(options = {}) {
|
|
186
|
+
const cwd = process.cwd();
|
|
187
|
+
const manifestPath = resolve(cwd, MANIFEST_PATH);
|
|
188
|
+
const lockPath = resolve(cwd, LOCK_PATH);
|
|
189
|
+
if (existsSync(MODA_DIR) && lstatSync(MODA_DIR).isSymbolicLink())
|
|
190
|
+
throw new CliInputError(".moda must not be a symlink.");
|
|
191
|
+
mkdirSync(MODA_DIR, { recursive: true });
|
|
192
|
+
let created = false;
|
|
193
|
+
if (!existsSync(MANIFEST_PATH)) {
|
|
194
|
+
assertContainedSource(cwd, MODA_DIR);
|
|
195
|
+
try {
|
|
196
|
+
writeFileSync(manifestPath, ["version: 2", "prompt_paths:", ...DEFAULT_PROMPT_PATHS.map((path) => ` - "${path}"`), "sources: []", ""].join(`
|
|
197
|
+
`), { flag: "wx" });
|
|
198
|
+
created = true;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error.code !== "EEXIST")
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!existsSync(manifestPath)) {
|
|
205
|
+
throw new Error(`Could not write ${manifestPath}: the file is missing after init. Check directory permissions and available disk space.`);
|
|
206
|
+
}
|
|
207
|
+
const result = {
|
|
208
|
+
manifestPath,
|
|
209
|
+
lockPath,
|
|
210
|
+
cwd,
|
|
211
|
+
created,
|
|
212
|
+
promptPaths: readManifest().promptPaths
|
|
213
|
+
};
|
|
214
|
+
if (!options.silent) {
|
|
215
|
+
printJson({
|
|
216
|
+
manifest: MANIFEST_PATH,
|
|
217
|
+
manifestPath,
|
|
218
|
+
lockfile: LOCK_PATH,
|
|
219
|
+
lockPath,
|
|
220
|
+
cwd,
|
|
221
|
+
created: result.created,
|
|
222
|
+
promptPaths: result.promptPaths
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
async function syncPrompts(flags, profileOptions, context) {
|
|
228
|
+
if (flags.watch === "true") {
|
|
229
|
+
await watchPromptSync(flags, profileOptions);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
await syncPromptsOnce(flags, profileOptions, context);
|
|
233
|
+
}
|
|
234
|
+
async function runPromptSync(flags, profileOptions, options = {}) {
|
|
235
|
+
if (flags["dry-run"] === "true")
|
|
236
|
+
return runPromptSyncExclusive(flags, profileOptions, options);
|
|
237
|
+
mkdirSync(dirname(MANIFEST_PATH), { recursive: true });
|
|
238
|
+
const syncLock = resolve(dirname(MANIFEST_PATH), "prompts.sync.lock");
|
|
239
|
+
try {
|
|
240
|
+
mkdirSync(syncLock);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error.code === "EEXIST") {
|
|
243
|
+
throw new Error(`Another prompt sync owns ${syncLock}. Wait for it to finish. If it crashed, remove this directory only after confirming no sync is running.`);
|
|
244
|
+
}
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
return await runPromptSyncExclusive(flags, profileOptions, options);
|
|
249
|
+
} finally {
|
|
250
|
+
rmSync(syncLock, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function runPromptSyncExclusive(flags, profileOptions, options) {
|
|
254
|
+
let harnessImport;
|
|
255
|
+
if (flags["from-harness"] && flags["dry-run"] !== "true")
|
|
256
|
+
harnessImport = await importHarnessPromptSources(flags["from-harness"]);
|
|
257
|
+
if (flags["dry-run"] !== "true")
|
|
258
|
+
await initPrompts({ silent: true });
|
|
259
|
+
const manifest = readManifest();
|
|
260
|
+
const originalManifestHash = hashFile(MANIFEST_PATH);
|
|
261
|
+
let extraSources = [];
|
|
262
|
+
if (flags["from-harness"] && flags["dry-run"] === "true") {
|
|
263
|
+
const read = await readHarnessSources(flags["from-harness"]);
|
|
264
|
+
extraSources = read.sources;
|
|
265
|
+
harnessImport = read.result;
|
|
266
|
+
}
|
|
267
|
+
mergePromptSources(manifest.sources, extraSources);
|
|
268
|
+
const established = await discoverPrompts(extraSources);
|
|
269
|
+
let discovery;
|
|
270
|
+
const analyzedDefinitions = new Map;
|
|
271
|
+
if (flags["no-analyze"] !== "true") {
|
|
272
|
+
options.onProgress?.("Analyzing prompt sources before sync.");
|
|
273
|
+
const { analyzeHarnessPrompts } = await import("./harness-jka07f99.js");
|
|
274
|
+
const result = await analyzeHarnessPrompts({
|
|
275
|
+
rootDir: process.cwd(),
|
|
276
|
+
existing: established.flatMap((prompt) => prompt.sourceDefinition ? [prompt.sourceDefinition.source] : []),
|
|
277
|
+
flags,
|
|
278
|
+
env: profileOptions.env ?? process.env,
|
|
279
|
+
dryRun: flags["dry-run"] === "true",
|
|
280
|
+
onProgress: options.onProgress
|
|
281
|
+
});
|
|
282
|
+
const legacyPaths = new Set(established.filter((prompt) => !prompt.sourceDefinition).map((prompt) => resolve(prompt.sourcePath)));
|
|
283
|
+
const exclusions = manifest.promptPaths.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
284
|
+
const excludedPaths = new Set(exclusions.length ? import_fast_glob.default.sync(exclusions, { cwd: process.cwd(), onlyFiles: true, dot: true, followSymbolicLinks: false }).map((path) => resolve(path)) : []);
|
|
285
|
+
const found = result.sources.filter((source) => !legacyPaths.has(resolve(source.path)) && !excludedPaths.has(resolve(source.path)));
|
|
286
|
+
for (const source of found) {
|
|
287
|
+
const analyzed = result.resolved.find((item) => item.key === source.key);
|
|
288
|
+
if (!analyzed)
|
|
289
|
+
throw new Error(`Prompt discovery omitted resolved source '${source.key}'.`);
|
|
290
|
+
analyzedDefinitions.set(source.key, stableJson(analyzed.sourceDefinition));
|
|
291
|
+
}
|
|
292
|
+
extraSources = mergePromptSources(extraSources, found);
|
|
293
|
+
const { resolved: _resolved, ...diagnostics } = result;
|
|
294
|
+
discovery = { ...diagnostics, sources: found };
|
|
295
|
+
}
|
|
296
|
+
if (originalManifestHash !== hashFile(MANIFEST_PATH))
|
|
297
|
+
throw new Error("Prompt manifest changed during analysis. Retry with a stable checkout.");
|
|
298
|
+
const mergedSources = mergePromptSources(manifest.sources, extraSources);
|
|
299
|
+
const manifestUpdate = stableJson(mergedSources) === stableJson(mergePromptSources(manifest.sources, [])) ? undefined : $stringify({ version: 2, prompt_paths: manifest.promptPaths, sources: mergedSources });
|
|
300
|
+
const prompts = await discoverPrompts(extraSources);
|
|
301
|
+
const { checkPromptAttribution } = await import("./prompt-attribution-armk5gqt.js");
|
|
302
|
+
const attribution = checkPromptAttribution(process.cwd(), prompts.map((prompt) => prompt.key));
|
|
303
|
+
const brokenAttribution = attribution.references.filter((reference) => reference.status === "missing");
|
|
304
|
+
if (brokenAttribution.length) {
|
|
305
|
+
throw new CliInputError(`Prompt attribution keys are absent from this sync: ${brokenAttribution.map((reference) => `'${reference.key}' at ${reference.path}:${reference.line}`).join(", ")}. Map the referenced key to its verified source in .moda/prompts.yml, or update the runtime lookup to the intended mapped key. No prompt versions were uploaded.`);
|
|
306
|
+
}
|
|
307
|
+
for (const prompt of prompts) {
|
|
308
|
+
const analyzed = analyzedDefinitions.get(prompt.key);
|
|
309
|
+
if (analyzed && analyzed !== stableJson(prompt.sourceDefinition))
|
|
310
|
+
throw new Error("Prompt source changed after analysis. Retry with a stable checkout.");
|
|
311
|
+
}
|
|
312
|
+
const previous = readLock();
|
|
313
|
+
const previousLockHash = hashFile(LOCK_PATH);
|
|
314
|
+
const missing = Object.keys(previous?.prompts ?? {}).filter((key) => !prompts.some((prompt) => prompt.key === key));
|
|
315
|
+
if (missing.length && flags["allow-untrack"] !== "true") {
|
|
316
|
+
throw new CliInputError(`Previously synced prompts are absent: ${missing.join(", ")}. Restore their sources, or use --allow-untrack to explicitly remove local tracking.`);
|
|
317
|
+
}
|
|
318
|
+
const sourceCommit = getGitCommit();
|
|
319
|
+
const manifestHash = manifestUpdate === undefined ? originalManifestHash : sha256(manifestUpdate);
|
|
320
|
+
const dryRun = flags["dry-run"] === "true";
|
|
321
|
+
if (prompts.length === 0) {
|
|
322
|
+
if (!dryRun && flags["allow-untrack"] === "true")
|
|
323
|
+
writeLock([], []);
|
|
324
|
+
return { synced: [], dryRun, discovery, attribution, ...harnessImport ? { harnessImport } : {} };
|
|
325
|
+
}
|
|
326
|
+
const signature = stableJson(prompts);
|
|
327
|
+
const response = await callControlAPI("/prompts/sync", {
|
|
328
|
+
method: "POST",
|
|
329
|
+
body: JSON.stringify({
|
|
330
|
+
prompts: prompts.map((prompt) => ({
|
|
331
|
+
...prompt,
|
|
332
|
+
sourceCommit,
|
|
333
|
+
manifestHash
|
|
334
|
+
})),
|
|
335
|
+
sourceCommit,
|
|
336
|
+
manifestHash,
|
|
337
|
+
dryRun
|
|
338
|
+
})
|
|
339
|
+
}, profileOptions);
|
|
340
|
+
if (signature !== stableJson(await discoverPrompts(extraSources)) || originalManifestHash !== hashFile(MANIFEST_PATH)) {
|
|
341
|
+
throw new Error("Prompt sources changed during sync. Remote writes may have completed; the prior lockfile was preserved. Retry with a stable checkout.");
|
|
342
|
+
}
|
|
343
|
+
const synced = response.synced;
|
|
344
|
+
if (!Array.isArray(synced) || synced.length !== prompts.length || new Set(synced.map((row) => row.key)).size !== prompts.length || prompts.some((prompt) => !synced.some((row) => row.key === prompt.key && row.promptId && row.versionId && row.contentHash === prompt.contentHash))) {
|
|
345
|
+
throw new Error("Prompt sync returned incomplete or mismatched versions. Remote writes may have completed; the prior lockfile was preserved.");
|
|
346
|
+
}
|
|
347
|
+
if (!dryRun) {
|
|
348
|
+
if (hashFile(LOCK_PATH) !== previousLockHash)
|
|
349
|
+
throw new Error("Prompt lockfile changed during sync. Another writer may have synced concurrently; its lockfile was preserved. Retry.");
|
|
350
|
+
if (originalManifestHash !== hashFile(MANIFEST_PATH))
|
|
351
|
+
throw new Error("Prompt manifest changed during sync. Retry with a stable checkout.");
|
|
352
|
+
if (manifestUpdate !== undefined)
|
|
353
|
+
atomicWrite(MANIFEST_PATH, manifestUpdate);
|
|
354
|
+
writeLock(prompts, synced);
|
|
355
|
+
}
|
|
356
|
+
return { synced: response.synced ?? [], dryRun, discovery, attribution, ...harnessImport ? { harnessImport } : {} };
|
|
357
|
+
}
|
|
358
|
+
function writePromptSyncProgress(message, context, quiet = false) {
|
|
359
|
+
if (quiet || context?.quiet)
|
|
360
|
+
return;
|
|
361
|
+
if (context?.outputMode === "agent-stream") {
|
|
362
|
+
context.output.writeEvent({ event: "progress", phase: "prompt_discovery", message, elapsed_ms: Date.now() - context.startedAt });
|
|
363
|
+
} else {
|
|
364
|
+
process.stderr.write(`[moda prompts] ${message.replace(/[\x00-\x1f\x7f]/g, " ")}
|
|
365
|
+
`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function syncPromptsOnce(flags, profileOptions, context) {
|
|
369
|
+
const result = await runPromptSync(flags, profileOptions, {
|
|
370
|
+
onProgress: (message) => writePromptSyncProgress(message, context, flags.quiet === "true")
|
|
371
|
+
});
|
|
372
|
+
const payload = {
|
|
373
|
+
synced: result.synced,
|
|
374
|
+
discovery: result.discovery,
|
|
375
|
+
attribution: result.attribution,
|
|
376
|
+
...result.harnessImport ? { harnessImport: result.harnessImport } : {},
|
|
377
|
+
lockfile: result.dryRun ? undefined : LOCK_PATH
|
|
378
|
+
};
|
|
379
|
+
const skippedWarning = result.harnessImport?.skipped.length ? [harnessSkipWarning(result.harnessImport)] : [];
|
|
380
|
+
if (context)
|
|
381
|
+
context.output.writeData(payload, skippedWarning.length ? { warnings: skippedWarning } : undefined);
|
|
382
|
+
else
|
|
383
|
+
printJson(payload);
|
|
384
|
+
if (!result.dryRun && result.synced.length === 0 && result.discovery?.candidates?.length) {
|
|
385
|
+
writePromptSyncProgress("No eligible prompt sources were synced. Review discovery.pending and discovery.related.", context, flags.quiet === "true");
|
|
386
|
+
process.exitCode = 1;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function watchPromptSync(flags, profileOptions) {
|
|
390
|
+
await syncPromptsOnce(flags, profileOptions);
|
|
391
|
+
console.error("Watching mapped prompt files. Press Ctrl+C to stop.");
|
|
392
|
+
let lastSignature = "";
|
|
393
|
+
const intervalMs = Math.max(1000, Number(flags.interval ?? 2500));
|
|
394
|
+
while (true) {
|
|
395
|
+
const status = await getPromptStatus();
|
|
396
|
+
const signature = stableJson(status.prompts);
|
|
397
|
+
if (signature !== lastSignature) {
|
|
398
|
+
lastSignature = signature;
|
|
399
|
+
if (status.changed > 0 || status.new > 0 || status.deleted > 0) {
|
|
400
|
+
await syncPromptsOnce({ ...flags, watch: "false", "no-analyze": "true" }, profileOptions);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function promotePrompt(positionals, flags, profileOptions, context) {
|
|
407
|
+
const key = positionals[1];
|
|
408
|
+
const label = flags.label;
|
|
409
|
+
const versionId = flags.version || flags["version-id"];
|
|
410
|
+
if (!key)
|
|
411
|
+
throw new CliInputError("Usage: moda prompts promote <prompt_key> --label=prod --version=<version_id>", undefined, "missing_positional");
|
|
412
|
+
if (!label)
|
|
413
|
+
throw new CliInputError("Missing required flag: --label=dev|staging|prod");
|
|
414
|
+
if (!versionId)
|
|
415
|
+
throw new CliInputError("Missing required flag: --version=<version_id>");
|
|
416
|
+
const response = await callControlAPI(`/prompts/${encodeURIComponent(key)}/labels`, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
body: JSON.stringify({ label, versionId })
|
|
419
|
+
}, profileOptions);
|
|
420
|
+
context.output.writeData(response);
|
|
421
|
+
}
|
|
422
|
+
async function getPromptStatus() {
|
|
423
|
+
const prompts = await discoverPrompts();
|
|
424
|
+
const lock = readLock();
|
|
425
|
+
const rows = prompts.map((prompt) => {
|
|
426
|
+
const locked = lock?.prompts[prompt.key];
|
|
427
|
+
const state = !locked ? "new" : locked.contentHash === prompt.contentHash ? "unchanged" : "changed";
|
|
428
|
+
return {
|
|
429
|
+
key: prompt.key,
|
|
430
|
+
sourcePath: prompt.sourcePath,
|
|
431
|
+
contentHash: prompt.contentHash,
|
|
432
|
+
lockedHash: locked?.contentHash ?? null,
|
|
433
|
+
versionId: locked?.versionId ?? null,
|
|
434
|
+
state
|
|
435
|
+
};
|
|
436
|
+
});
|
|
437
|
+
const discoveredKeys = new Set(prompts.map((prompt) => prompt.key));
|
|
438
|
+
for (const [key, locked] of Object.entries(lock?.prompts ?? {})) {
|
|
439
|
+
if (!discoveredKeys.has(key)) {
|
|
440
|
+
rows.push({
|
|
441
|
+
key,
|
|
442
|
+
sourcePath: locked.sourcePath,
|
|
443
|
+
contentHash: null,
|
|
444
|
+
lockedHash: locked.contentHash,
|
|
445
|
+
versionId: locked.versionId ?? null,
|
|
446
|
+
state: "deleted"
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const inspection = inspectPromptManifest();
|
|
451
|
+
return {
|
|
452
|
+
manifest: MANIFEST_PATH,
|
|
453
|
+
lockfile: LOCK_PATH,
|
|
454
|
+
manifestVersion: inspection.manifestVersion,
|
|
455
|
+
legacyManifest: inspection.legacyManifest,
|
|
456
|
+
zeroMatchPatterns: inspection.zeroMatchPatterns,
|
|
457
|
+
legacyOnlyFiles: inspection.legacyOnlyFiles,
|
|
458
|
+
sources: inspection.sources,
|
|
459
|
+
total: rows.length,
|
|
460
|
+
changed: rows.filter((row) => row.state === "changed").length,
|
|
461
|
+
new: rows.filter((row) => row.state === "new").length,
|
|
462
|
+
deleted: rows.filter((row) => row.state === "deleted").length,
|
|
463
|
+
prompts: rows
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
async function discoverPrompts(extraSources = []) {
|
|
467
|
+
return discoverManifestPrompts(readManifest(), extraSources);
|
|
468
|
+
}
|
|
469
|
+
var NATIVE_PROMPT_FILE_RE = /\.prompt\.(md|json)$/i;
|
|
470
|
+
async function discoverManifestPrompts(manifest, extraSources = []) {
|
|
471
|
+
const root = process.cwd();
|
|
472
|
+
for (const pattern of manifest.promptPaths) {
|
|
473
|
+
const positive = pattern.startsWith("!") ? pattern.slice(1) : pattern;
|
|
474
|
+
if (isAbsolute(positive) || positive.split(/[\\/]/).includes(".."))
|
|
475
|
+
throw new CliInputError(`Prompt pattern must stay inside the repository: ${pattern}`);
|
|
476
|
+
if (!pattern.startsWith("!") && !import_fast_glob.default.isDynamicPattern(pattern) && !existsSync(resolve(root, pattern))) {
|
|
477
|
+
throw new CliInputError(`Configured prompt file is missing: ${pattern}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const paths = import_fast_glob.default.sync(manifest.promptPaths, {
|
|
481
|
+
cwd: root,
|
|
482
|
+
onlyFiles: true,
|
|
483
|
+
followSymbolicLinks: false,
|
|
484
|
+
dot: true,
|
|
485
|
+
unique: true,
|
|
486
|
+
ignore: DISCOVERY_IGNORE
|
|
487
|
+
}).sort();
|
|
488
|
+
const sources = new Map(manifest.sources.map((source) => [source.key, source]));
|
|
489
|
+
for (const source of extraSources)
|
|
490
|
+
sources.set(source.key, source);
|
|
491
|
+
const selectedPaths = new Set([...sources.values()].map((source) => resolve(root, source.path)));
|
|
492
|
+
const owned = paths.filter((path) => !selectedPaths.has(resolve(root, path)));
|
|
493
|
+
const resolver = sources.size > 0 || owned.some((path) => !NATIVE_PROMPT_FILE_RE.test(path)) ? (await loadPromptSources()).resolvePromptSource : undefined;
|
|
494
|
+
const prompts = owned.map((path) => {
|
|
495
|
+
assertContainedSource(root, path);
|
|
496
|
+
if (NATIVE_PROMPT_FILE_RE.test(path))
|
|
497
|
+
return parsePromptFile(resolve(root, path));
|
|
498
|
+
return normalizePrompt(resolver(root, { key: keyFromPath(path), path }));
|
|
499
|
+
});
|
|
500
|
+
for (const source of sources.values())
|
|
501
|
+
prompts.push(normalizePrompt(resolver(root, source)));
|
|
502
|
+
const seen = new Set;
|
|
503
|
+
for (const prompt of prompts) {
|
|
504
|
+
if (seen.has(prompt.key))
|
|
505
|
+
throw new CliInputError(`Duplicate prompt key '${prompt.key}'. Give each source a unique stable key.`);
|
|
506
|
+
seen.add(prompt.key);
|
|
507
|
+
}
|
|
508
|
+
return prompts.sort((a, b) => a.key.localeCompare(b.key));
|
|
509
|
+
}
|
|
510
|
+
var LEGACY_PROMPT_SUFFIXES = [".prompt.md", ".prompt.json", ".prompt.yaml", ".prompt.yml"];
|
|
511
|
+
function repoPath(root, path) {
|
|
512
|
+
return relative(root, resolve(root, path)).replaceAll("\\", "/");
|
|
513
|
+
}
|
|
514
|
+
function globPromptFiles(root, patterns, ignore = []) {
|
|
515
|
+
if (patterns.length === 0)
|
|
516
|
+
return [];
|
|
517
|
+
return import_fast_glob.default.sync(patterns, {
|
|
518
|
+
cwd: root,
|
|
519
|
+
onlyFiles: true,
|
|
520
|
+
followSymbolicLinks: false,
|
|
521
|
+
dot: true,
|
|
522
|
+
unique: true,
|
|
523
|
+
ignore: [...DISCOVERY_IGNORE, ...ignore]
|
|
524
|
+
}).sort();
|
|
525
|
+
}
|
|
526
|
+
function inspectPromptManifest(root = process.cwd()) {
|
|
527
|
+
const manifest = readManifest(root);
|
|
528
|
+
const safe = (pattern) => !isAbsolute(pattern) && !pattern.split(/[\\/]/).includes("..");
|
|
529
|
+
const positives = manifest.promptPaths.filter((pattern) => !pattern.startsWith("!") && safe(pattern));
|
|
530
|
+
const negations = manifest.promptPaths.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
531
|
+
const patterns = manifest.promptPaths.filter((pattern) => !pattern.startsWith("!")).map((pattern) => ({
|
|
532
|
+
pattern,
|
|
533
|
+
matches: safe(pattern) ? globPromptFiles(root, [pattern], negations).length : 0
|
|
534
|
+
}));
|
|
535
|
+
const matched = new Set(globPromptFiles(root, positives, negations));
|
|
536
|
+
const owned = new Set(manifest.sources.map((source) => repoPath(root, source.path)));
|
|
537
|
+
const suffixes = [...new Set(manifest.promptPaths.filter((pattern) => pattern.includes(".prompt.")).flatMap((pattern) => LEGACY_PROMPT_SUFFIXES.filter((suffix) => pattern.endsWith(suffix))))];
|
|
538
|
+
const legacySuffixes = suffixes.length ? suffixes : [".prompt.md", ".prompt.json"];
|
|
539
|
+
const tracked = manifest.version === 2 ? lockedSourcePaths(root) : null;
|
|
540
|
+
const legacyOnlyFiles = manifest.version === null ? [] : globPromptFiles(root, legacySuffixes.map((suffix) => `**/*${suffix}`), negations).filter((path) => !matched.has(path) && !owned.has(path) && (!tracked || tracked.has(path)));
|
|
541
|
+
return {
|
|
542
|
+
manifestExists: manifest.version !== null,
|
|
543
|
+
manifestVersion: manifest.version,
|
|
544
|
+
legacyManifest: manifest.version === 1,
|
|
545
|
+
patterns,
|
|
546
|
+
zeroMatchPatterns: patterns.filter((row) => row.matches === 0).map((row) => row.pattern),
|
|
547
|
+
sources: manifest.sources.length,
|
|
548
|
+
matchedFiles: matched.size,
|
|
549
|
+
legacyOnlyFiles
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function lockedSourcePaths(root) {
|
|
553
|
+
const lockPath = resolve(root, LOCK_PATH);
|
|
554
|
+
if (!existsSync(lockPath))
|
|
555
|
+
return new Set;
|
|
556
|
+
try {
|
|
557
|
+
const lock = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
558
|
+
return new Set(Object.values(lock.prompts ?? {}).filter((entry) => entry.sourcePath).map((entry) => repoPath(root, String(entry.sourcePath))));
|
|
559
|
+
} catch {
|
|
560
|
+
return new Set;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function promptManifestWarnings(inspection) {
|
|
564
|
+
if (inspection.manifestVersion === null)
|
|
565
|
+
return [];
|
|
566
|
+
const warnings = [];
|
|
567
|
+
const legacyCount = inspection.legacyOnlyFiles.length;
|
|
568
|
+
if (legacyCount > 0) {
|
|
569
|
+
const sample = inspection.legacyOnlyFiles.slice(0, 3).join(", ");
|
|
570
|
+
warnings.push(`${legacyCount} prompt file(s) (${sample}${legacyCount > 3 ? ", ..." : ""}) were discovered by older CLIs but match no ${MANIFEST_PATH} pattern now: patterns are exact globs from the repository root. Run \`moda prompts migrate --dry-run\`, then \`moda prompts migrate\`, to keep them tracked under their existing keys.`);
|
|
571
|
+
}
|
|
572
|
+
if (inspection.zeroMatchPatterns.length > 0) {
|
|
573
|
+
warnings.push(`prompt_paths pattern(s) match no files: ${inspection.zeroMatchPatterns.join(", ")}. Fix the globs (relative to the repository root), map code-defined prompts under \`sources:\`, or preview discovery with \`moda prompts sync --dry-run\`.`);
|
|
574
|
+
}
|
|
575
|
+
if (inspection.legacyManifest && warnings.length > 0) {
|
|
576
|
+
warnings.push(`${MANIFEST_PATH} is a version 1 manifest; \`moda prompts migrate\` rewrites it to version 2 (explicit prompt_paths plus a sources: list).`);
|
|
577
|
+
}
|
|
578
|
+
return warnings;
|
|
579
|
+
}
|
|
580
|
+
function migrateWarnings(result) {
|
|
581
|
+
const warnings = [];
|
|
582
|
+
if (result.zeroMatchPatterns.length > 0) {
|
|
583
|
+
warnings.push(`Kept prompt_paths pattern(s) that match no files: ${result.zeroMatchPatterns.join(", ")}. Fix or remove them.`);
|
|
584
|
+
}
|
|
585
|
+
if (result.pendingSources.length > 0) {
|
|
586
|
+
const sample = result.pendingSources.slice(0, 3).map((source) => source.key).join(", ");
|
|
587
|
+
warnings.push(`${result.pendingSources.length} code-defined prompt candidate(s) (${sample}${result.pendingSources.length > 3 ? ", ..." : ""}) were found but not added: --discover never runs an analyst, and code mappings need independent verification. Run \`${HARNESS_REANALYZE_COMMAND}\`, then \`moda prompts sync --from-harness\`, or map them under \`sources:\` yourself. See pendingSources.`);
|
|
588
|
+
}
|
|
589
|
+
return warnings;
|
|
590
|
+
}
|
|
591
|
+
async function migratePromptManifest(options = {}) {
|
|
592
|
+
const root = process.cwd();
|
|
593
|
+
const manifestPath = resolve(root, MANIFEST_PATH);
|
|
594
|
+
if (!existsSync(manifestPath)) {
|
|
595
|
+
throw new CliInputError(`No ${MANIFEST_PATH} to migrate in ${root}.`, "Run `moda prompts init` to create a version 2 manifest.");
|
|
596
|
+
}
|
|
597
|
+
const original = readFileSync(manifestPath, "utf8");
|
|
598
|
+
const manifest = readManifest(root);
|
|
599
|
+
const inspection = inspectPromptManifest(root);
|
|
600
|
+
const addedPaths = inspection.legacyOnlyFiles;
|
|
601
|
+
const addedPatterns = addedPaths.map((path) => import_fast_glob.default.escapePath(path));
|
|
602
|
+
const promptPaths = [...manifest.promptPaths, ...addedPatterns];
|
|
603
|
+
let sources = manifest.sources;
|
|
604
|
+
let addedSources = [];
|
|
605
|
+
let pendingSources = [];
|
|
606
|
+
if (options.discover) {
|
|
607
|
+
const { analyzeHarnessPrompts } = await import("./harness-jka07f99.js");
|
|
608
|
+
const result = await analyzeHarnessPrompts({
|
|
609
|
+
rootDir: root,
|
|
610
|
+
existing: manifest.sources,
|
|
611
|
+
flags: { analyst: "local-scan" },
|
|
612
|
+
env: options.env ?? process.env,
|
|
613
|
+
dryRun: true
|
|
614
|
+
});
|
|
615
|
+
const negations = promptPaths.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
616
|
+
const claimed = new Set([
|
|
617
|
+
...globPromptFiles(root, promptPaths.filter((pattern) => !pattern.startsWith("!"))),
|
|
618
|
+
...globPromptFiles(root, negations)
|
|
619
|
+
]);
|
|
620
|
+
const existingKeys = new Set(sources.map((source) => source.key));
|
|
621
|
+
const found = result.sources.filter((source) => !existingKeys.has(source.key) && !claimed.has(repoPath(root, source.path)));
|
|
622
|
+
sources = mergePromptSources(sources, found);
|
|
623
|
+
addedSources = found.map((source) => source.key).sort();
|
|
624
|
+
pendingSources = (result.pending ?? []).filter((candidate) => !existingKeys.has(candidate.source.key) && !claimed.has(repoPath(root, candidate.source.path))).map((candidate) => ({
|
|
625
|
+
key: candidate.source.key,
|
|
626
|
+
path: candidate.source.path,
|
|
627
|
+
...candidate.source.selector?.symbol ? { symbol: candidate.source.selector.symbol } : {},
|
|
628
|
+
reason: candidate.reason ?? "Awaiting independent semantic verification"
|
|
629
|
+
})).sort((a, b) => a.key.localeCompare(b.key));
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
await discoverManifestPrompts({ version: 2, promptPaths, sources });
|
|
633
|
+
} catch (error) {
|
|
634
|
+
throw new CliInputError(`Cannot migrate ${MANIFEST_PATH}: ${error instanceof Error ? error.message : String(error)}`, `Give the conflicting prompt a unique \`key:\` (frontmatter or ${MANIFEST_PATH} sources), then rerun \`moda prompts migrate --dry-run\`. Nothing was written.`);
|
|
635
|
+
}
|
|
636
|
+
const expected = stableJson({ version: 2, prompt_paths: promptPaths, sources });
|
|
637
|
+
const serialize = (inPlace) => {
|
|
638
|
+
const doc = $parseDocument(original, { uniqueKeys: true });
|
|
639
|
+
doc.set("version", 2);
|
|
640
|
+
const pathsNode = doc.get("prompt_paths", true);
|
|
641
|
+
if (inPlace && $isSeq(pathsNode)) {
|
|
642
|
+
for (const pattern of addedPatterns)
|
|
643
|
+
pathsNode.items.push(doc.createNode(pattern));
|
|
644
|
+
} else {
|
|
645
|
+
doc.set("prompt_paths", promptPaths);
|
|
646
|
+
}
|
|
647
|
+
const existingKeys = new Set(manifest.sources.map((source) => source.key));
|
|
648
|
+
const newSources = sources.filter((source) => !existingKeys.has(source.key));
|
|
649
|
+
const sourcesNode = doc.get("sources", true);
|
|
650
|
+
if (inPlace && $isSeq(sourcesNode)) {
|
|
651
|
+
for (const source of newSources)
|
|
652
|
+
sourcesNode.items.push(doc.createNode(source));
|
|
653
|
+
} else if (!inPlace || newSources.length > 0 || sourcesNode === undefined) {
|
|
654
|
+
doc.set("sources", sources);
|
|
655
|
+
}
|
|
656
|
+
return doc.toString();
|
|
657
|
+
};
|
|
658
|
+
const matchesExpected = (text) => {
|
|
659
|
+
const parsed = $parseDocument(text, { uniqueKeys: true }).toJS({ maxAliasCount: 20 }) ?? {};
|
|
660
|
+
return stableJson({ version: parsed.version, prompt_paths: parsed.prompt_paths ?? [], sources: parsed.sources ?? [] }) === expected;
|
|
661
|
+
};
|
|
662
|
+
let content = serialize(true);
|
|
663
|
+
if (!matchesExpected(content))
|
|
664
|
+
content = serialize(false);
|
|
665
|
+
if (!matchesExpected(content)) {
|
|
666
|
+
throw new CliInputError(`Cannot migrate ${MANIFEST_PATH}: the rewritten manifest does not round-trip (shared YAML anchors or aliases?). Nothing was written.`, `Replace anchors/aliases in prompt_paths and sources with plain lists, then rerun \`moda prompts migrate --dry-run\`.`);
|
|
667
|
+
}
|
|
668
|
+
const changed = content !== original;
|
|
669
|
+
if (!options.dryRun && changed) {
|
|
670
|
+
if (hashFile(MANIFEST_PATH) !== sha256(original))
|
|
671
|
+
throw new Error(`${MANIFEST_PATH} changed during migration. Retry.`);
|
|
672
|
+
atomicWrite(MANIFEST_PATH, content);
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
manifest: MANIFEST_PATH,
|
|
676
|
+
manifestPath,
|
|
677
|
+
dryRun: options.dryRun === true,
|
|
678
|
+
changed,
|
|
679
|
+
fromVersion: manifest.version ?? 2,
|
|
680
|
+
toVersion: 2,
|
|
681
|
+
addedPaths,
|
|
682
|
+
addedSources,
|
|
683
|
+
pendingSources,
|
|
684
|
+
zeroMatchPatterns: inspection.zeroMatchPatterns,
|
|
685
|
+
promptPaths,
|
|
686
|
+
content
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function assertContainedSource(root, path) {
|
|
690
|
+
const abs = resolve(root, path);
|
|
691
|
+
const rel = relative(realpathSync(root), realpathSync(abs));
|
|
692
|
+
if (rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel))
|
|
693
|
+
throw new CliInputError(`Prompt source escapes repository: ${path}`);
|
|
694
|
+
let cursor = abs;
|
|
695
|
+
while (cursor !== resolve(root)) {
|
|
696
|
+
if (lstatSync(cursor).isSymbolicLink())
|
|
697
|
+
throw new CliInputError(`Prompt source uses a symlink: ${path}`);
|
|
698
|
+
const next = dirname(cursor);
|
|
699
|
+
if (next === cursor)
|
|
700
|
+
throw new CliInputError(`Prompt source escapes repository: ${path}`);
|
|
701
|
+
cursor = next;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function parsePromptFile(absPath) {
|
|
705
|
+
const sourcePath = relative(process.cwd(), absPath);
|
|
706
|
+
const raw = readFileSync(absPath, "utf8");
|
|
707
|
+
if (absPath.endsWith(".json")) {
|
|
708
|
+
const document = $parseDocument(raw, { uniqueKeys: true });
|
|
709
|
+
if (document.errors.length)
|
|
710
|
+
throw new CliInputError(`${sourcePath}: ${document.errors[0].message}`);
|
|
711
|
+
let parsed;
|
|
712
|
+
try {
|
|
713
|
+
parsed = JSON.parse(raw);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
throw new CliInputError(`${sourcePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
716
|
+
}
|
|
717
|
+
const key2 = parsed.key || keyFromPath(sourcePath);
|
|
718
|
+
const hasContent = typeof parsed.content === "string" && parsed.content.trim().length > 0;
|
|
719
|
+
const hasSystemPrompt = typeof parsed.systemPrompt === "string" && parsed.systemPrompt.trim().length > 0;
|
|
720
|
+
const hasTemplate = typeof parsed.template === "string" && parsed.template.trim().length > 0;
|
|
721
|
+
const hasMessages = Array.isArray(parsed.messages) && parsed.messages.some((message) => {
|
|
722
|
+
if (!message || typeof message !== "object" || !("role" in message) || !["system", "developer", "user", "assistant", "tool"].includes(String(message.role)) || !("content" in message))
|
|
723
|
+
return false;
|
|
724
|
+
const content2 = message.content;
|
|
725
|
+
return typeof content2 === "string" ? content2.trim().length > 0 : Array.isArray(content2) && content2.some((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
|
|
726
|
+
});
|
|
727
|
+
if (!hasContent && !hasSystemPrompt && !hasMessages && !hasTemplate) {
|
|
728
|
+
throw new CliInputError(`${sourcePath}: prompt JSON requires non-empty content, systemPrompt, messages, or a text template.`);
|
|
729
|
+
}
|
|
730
|
+
const content = hasContent ? parsed.content : hasSystemPrompt ? parsed.systemPrompt : hasMessages ? JSON.stringify(parsed.messages, null, 2) : parsed.template;
|
|
731
|
+
return normalizePrompt({ ...parsed, key: key2, name: parsed.name || key2, sourcePath, content });
|
|
732
|
+
}
|
|
733
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
734
|
+
const key = frontmatter.key || keyFromPath(sourcePath);
|
|
735
|
+
return normalizePrompt({
|
|
736
|
+
key,
|
|
737
|
+
name: frontmatter.name || key,
|
|
738
|
+
description: frontmatter.description,
|
|
739
|
+
category: frontmatter.category,
|
|
740
|
+
systemPrompt: frontmatter.system_prompt || frontmatter.systemPrompt,
|
|
741
|
+
modelConfig: frontmatter.model ? { model: frontmatter.model } : undefined,
|
|
742
|
+
sourcePath,
|
|
743
|
+
content: body.trim()
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
function normalizePrompt(prompt) {
|
|
747
|
+
return {
|
|
748
|
+
key: prompt.key,
|
|
749
|
+
name: prompt.name || prompt.key,
|
|
750
|
+
description: prompt.description,
|
|
751
|
+
sourcePath: prompt.sourcePath,
|
|
752
|
+
sourceDefinition: prompt.sourceDefinition,
|
|
753
|
+
content: prompt.content,
|
|
754
|
+
systemPrompt: prompt.systemPrompt,
|
|
755
|
+
messages: prompt.messages,
|
|
756
|
+
variables: prompt.variables,
|
|
757
|
+
variablesSchema: prompt.variablesSchema,
|
|
758
|
+
modelConfig: prompt.modelConfig,
|
|
759
|
+
toolRefs: prompt.toolRefs,
|
|
760
|
+
responseSchema: prompt.responseSchema,
|
|
761
|
+
contentHash: computePromptContentHash(prompt)
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function readManifest(root = process.cwd()) {
|
|
765
|
+
if (!existsSync(resolve(root, MANIFEST_PATH)))
|
|
766
|
+
return { version: null, promptPaths: DEFAULT_PROMPT_PATHS, sources: [] };
|
|
767
|
+
assertContainedSource(root, MANIFEST_PATH);
|
|
768
|
+
const doc = $parseDocument(readFileSync(resolve(root, MANIFEST_PATH), "utf8"), { uniqueKeys: true });
|
|
769
|
+
if (doc.errors.length)
|
|
770
|
+
throw new CliInputError(`Invalid ${MANIFEST_PATH}: ${doc.errors[0].message}`);
|
|
771
|
+
const parsed = doc.toJS({ maxAliasCount: 20 });
|
|
772
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || ![1, 2].includes(parsed.version))
|
|
773
|
+
throw new CliInputError(`${MANIFEST_PATH} requires version: 1 or 2.`);
|
|
774
|
+
if (Object.keys(parsed).some((key) => !["version", "prompt_paths", "sources"].includes(key)))
|
|
775
|
+
throw new CliInputError("Unknown prompt manifest field. Use version, prompt_paths, and sources.");
|
|
776
|
+
if (parsed.prompt_paths !== undefined && (!Array.isArray(parsed.prompt_paths) || parsed.prompt_paths.some((p) => typeof p !== "string" || !p.trim())))
|
|
777
|
+
throw new CliInputError("prompt_paths must be an array of nonempty patterns.");
|
|
778
|
+
if (parsed.sources !== undefined && !Array.isArray(parsed.sources))
|
|
779
|
+
throw new CliInputError("sources must be an array of prompt source descriptors.");
|
|
780
|
+
const sources = parsed.sources ?? [];
|
|
781
|
+
const seen = new Set;
|
|
782
|
+
for (const source of sources) {
|
|
783
|
+
if (!source || typeof source.key !== "string" || !source.key.trim() || typeof source.path !== "string" || !source.path.trim())
|
|
784
|
+
throw new CliInputError("Every prompt source requires a stable key and path.");
|
|
785
|
+
if (seen.has(source.key))
|
|
786
|
+
throw new CliInputError(`Duplicate prompt source key '${source.key}'.`);
|
|
787
|
+
seen.add(source.key);
|
|
788
|
+
}
|
|
789
|
+
return { version: parsed.version, promptPaths: parsed.prompt_paths ?? (parsed.version === 1 ? DEFAULT_PROMPT_PATHS : []), sources };
|
|
790
|
+
}
|
|
791
|
+
var HARNESS_REANALYZE_COMMAND = "moda harness analyze --experimental --concern=prompts";
|
|
792
|
+
async function readHarnessSources(reportPath) {
|
|
793
|
+
const selected = reportPath === "true" ? ".moda/harness-report.json" : reportPath;
|
|
794
|
+
assertContainedSource(process.cwd(), selected);
|
|
795
|
+
let raw;
|
|
796
|
+
try {
|
|
797
|
+
raw = JSON.parse(readFileSync(resolve(selected), "utf8"));
|
|
798
|
+
} catch (error) {
|
|
799
|
+
throw new CliInputError(`Could not read harness report ${selected}: ${error instanceof Error ? error.message : String(error)}.`, `Regenerate it with \`${HARNESS_REANALYZE_COMMAND}\`.`);
|
|
800
|
+
}
|
|
801
|
+
const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
802
|
+
const graph = record.graph && typeof record.graph === "object" ? record.graph : undefined;
|
|
803
|
+
const schemaVersion = typeof record.schemaVersion === "string" ? record.schemaVersion : typeof graph?.schemaVersion === "string" ? graph.schemaVersion : typeof graph?.graphSchemaVersion === "string" ? graph.graphSchemaVersion : null;
|
|
804
|
+
const schemaLabel = schemaVersion ?? "no schemaVersion";
|
|
805
|
+
if (!graph) {
|
|
806
|
+
throw new CliInputError(`${selected} is not a harness report with a graph (schema: ${schemaLabel}). Regenerate it with \`${HARNESS_REANALYZE_COMMAND}\`.`);
|
|
807
|
+
}
|
|
808
|
+
const { normalizeHarnessReport } = await import("./harness-report-hn2aarv2.js");
|
|
809
|
+
const { backfillPromptSource, resolvePromptSource } = await loadPromptSources();
|
|
810
|
+
const report = normalizeHarnessReport(raw);
|
|
811
|
+
const artifacts = (report.graph.artifacts ?? []).filter((artifact) => artifact.type === "prompt");
|
|
812
|
+
const sources = [];
|
|
813
|
+
const skipped = [];
|
|
814
|
+
const tracked = [];
|
|
815
|
+
const root = process.cwd();
|
|
816
|
+
const manifest = readManifest(root);
|
|
817
|
+
const negations = manifest.promptPaths.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
818
|
+
const globbed = new Set(globPromptFiles(root, manifest.promptPaths.filter((pattern) => !pattern.startsWith("!")), negations));
|
|
819
|
+
const ownedKeys = new Map(manifest.sources.map((source) => [repoPath(root, source.path), source.key]));
|
|
820
|
+
for (const artifact of artifacts) {
|
|
821
|
+
const label = String(artifact.name ?? artifact.id ?? "unnamed prompt");
|
|
822
|
+
let mapping;
|
|
823
|
+
try {
|
|
824
|
+
mapping = backfillPromptSource(process.cwd(), artifact);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
skipped.push({ artifact: label, reason: `source mapping could not be derived: ${error instanceof Error ? error.message : String(error)}` });
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (!mapping) {
|
|
830
|
+
skipped.push({
|
|
831
|
+
artifact: label,
|
|
832
|
+
reason: artifact.sourcePath ? `no promptSource, and no single definition in ${artifact.sourcePath} matches its name` : "no promptSource or sourcePath (legacy body snippets cannot be registered)"
|
|
833
|
+
});
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (!artifact.promptSource && !mapping.selector) {
|
|
837
|
+
const path = repoPath(root, mapping.path);
|
|
838
|
+
const nativeFile = /\.prompt\.(md|json)$/i.test(path);
|
|
839
|
+
const ownedKey = ownedKeys.get(path);
|
|
840
|
+
if (ownedKey !== undefined || globbed.has(path)) {
|
|
841
|
+
let key = ownedKey ?? keyFromPath(path);
|
|
842
|
+
try {
|
|
843
|
+
if (ownedKey === undefined && nativeFile)
|
|
844
|
+
key = parsePromptFile(resolve(root, path)).key;
|
|
845
|
+
} catch {}
|
|
846
|
+
tracked.push({ artifact: label, key, path });
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (nativeFile) {
|
|
850
|
+
skipped.push({
|
|
851
|
+
artifact: label,
|
|
852
|
+
reason: `${path} is a dedicated prompt file; add it to prompt_paths (\`moda prompts migrate\` adds files older CLIs discovered) so it keeps its own key and parser`
|
|
853
|
+
});
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (/\.prompt\.ya?ml$/i.test(path))
|
|
857
|
+
mapping = { ...mapping, key: keyFromPath(path) };
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
sources.push(resolvePromptSource(process.cwd(), mapping).sourceDefinition.source);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
skipped.push({ artifact: label, reason: error instanceof Error ? error.message : String(error) });
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (!sources.length && !tracked.length) {
|
|
866
|
+
const detail = skipped.length ? ` ${skipped.length} prompt artifact(s) skipped: ${skipped.slice(0, 5).map((skip) => `'${skip.artifact}' (${skip.reason})`).join("; ")}${skipped.length > 5 ? "; ..." : ""}.` : " It contains no prompt artifacts.";
|
|
867
|
+
throw new CliInputError(`Harness report ${selected} (schema: ${schemaLabel}) has no prompt that maps to current source.${detail} Regenerate it with \`${HARNESS_REANALYZE_COMMAND}\`.`, `Or map prompts directly under \`sources:\` in ${MANIFEST_PATH}.`);
|
|
868
|
+
}
|
|
869
|
+
return { sources, result: { report: selected, schemaVersion, registered: sources.map((source) => source.key), skipped, tracked } };
|
|
870
|
+
}
|
|
871
|
+
function harnessSkipWarning(result) {
|
|
872
|
+
return `${result.skipped.length} harness prompt(s) skipped (registered ${result.registered.length}): ${result.skipped.slice(0, 3).map((skip) => `'${skip.artifact}' (${skip.reason})`).join("; ")}${result.skipped.length > 3 ? "; ..." : ""}. See harnessImport.skipped; regenerate the report with \`${HARNESS_REANALYZE_COMMAND}\` to map them.`;
|
|
873
|
+
}
|
|
874
|
+
async function importHarnessPromptSources(reportPath = ".moda/harness-report.json") {
|
|
875
|
+
const { sources, result } = await readHarnessSources(reportPath);
|
|
876
|
+
const manifest = readManifest();
|
|
877
|
+
const merged = mergePromptSources(manifest.sources, sources);
|
|
878
|
+
await discoverPrompts(sources);
|
|
879
|
+
atomicWrite(MANIFEST_PATH, $stringify({ version: 2, prompt_paths: manifest.promptPaths, sources: merged }));
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
function mergePromptSources(existingSources, sources) {
|
|
883
|
+
const merged = new Map(existingSources.map((source) => [source.key, source]));
|
|
884
|
+
for (const source of sources) {
|
|
885
|
+
const existing = merged.get(source.key);
|
|
886
|
+
if (existing && stableJson(existing) !== stableJson(source)) {
|
|
887
|
+
throw new CliInputError(`Harness source '${source.key}' conflicts with an existing mapping. Edit ${MANIFEST_PATH} explicitly to change its source; no mappings were imported.`);
|
|
888
|
+
}
|
|
889
|
+
merged.set(source.key, source);
|
|
890
|
+
}
|
|
891
|
+
return [...merged.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
892
|
+
}
|
|
893
|
+
function atomicWrite(path, content) {
|
|
894
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
895
|
+
assertContainedSource(process.cwd(), dirname(path));
|
|
896
|
+
if (existsSync(path) && lstatSync(path).isSymbolicLink())
|
|
897
|
+
throw new CliInputError(`Refusing to overwrite symlink: ${path}`);
|
|
898
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
899
|
+
try {
|
|
900
|
+
writeFileSync(temp, content, { flag: "wx" });
|
|
901
|
+
renameSync(temp, path);
|
|
902
|
+
} finally {
|
|
903
|
+
rmSync(temp, { force: true });
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function readLock() {
|
|
907
|
+
if (!existsSync(LOCK_PATH))
|
|
908
|
+
return null;
|
|
909
|
+
assertContainedSource(process.cwd(), LOCK_PATH);
|
|
910
|
+
return JSON.parse(readFileSync(LOCK_PATH, "utf8"));
|
|
911
|
+
}
|
|
912
|
+
function writeLock(prompts, synced) {
|
|
913
|
+
const syncedByKey = new Map(synced.map((item) => [item.key, item]));
|
|
914
|
+
const lock = {
|
|
915
|
+
version: 1,
|
|
916
|
+
generatedAt: new Date().toISOString(),
|
|
917
|
+
prompts: Object.fromEntries(prompts.map((prompt) => {
|
|
918
|
+
const result = syncedByKey.get(prompt.key);
|
|
919
|
+
return [prompt.key, {
|
|
920
|
+
key: prompt.key,
|
|
921
|
+
sourcePath: prompt.sourcePath,
|
|
922
|
+
contentHash: prompt.contentHash,
|
|
923
|
+
promptId: result?.promptId,
|
|
924
|
+
versionId: result?.versionId
|
|
925
|
+
}];
|
|
926
|
+
}))
|
|
927
|
+
};
|
|
928
|
+
mkdirSync(dirname(LOCK_PATH), { recursive: true });
|
|
929
|
+
atomicWrite(LOCK_PATH, `${JSON.stringify(lock, null, 2)}
|
|
930
|
+
`);
|
|
931
|
+
}
|
|
932
|
+
function parseFrontmatter(raw) {
|
|
933
|
+
if (!raw.startsWith("---"))
|
|
934
|
+
return { frontmatter: {}, body: raw };
|
|
935
|
+
const end = raw.indexOf(`
|
|
936
|
+
---`, 3);
|
|
937
|
+
if (end === -1)
|
|
938
|
+
return { frontmatter: {}, body: raw };
|
|
939
|
+
const frontmatterText = raw.slice(3, end).trim();
|
|
940
|
+
const frontmatter = Object.fromEntries(frontmatterText.split(`
|
|
941
|
+
`).map((line) => line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)).filter((match) => Boolean(match)).map((match) => [match[1], match[2].replace(/^["']|["']$/g, "")]));
|
|
942
|
+
return { frontmatter, body: raw.slice(end + 4) };
|
|
943
|
+
}
|
|
944
|
+
function keyFromPath(path) {
|
|
945
|
+
return path.replace(/^prompts\//, "").replace(/(?:\.prompt)?\.(md|txt|json|ya?ml|jinja2?|j2|hbs|tmpl|prompt)$/i, "").replace(/[\\/]/g, ".");
|
|
946
|
+
}
|
|
947
|
+
function hashFile(path) {
|
|
948
|
+
return existsSync(path) ? sha256(readFileSync(path, "utf8")) : "";
|
|
949
|
+
}
|
|
950
|
+
function getGitCommit() {
|
|
951
|
+
try {
|
|
952
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
953
|
+
encoding: "utf8",
|
|
954
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
955
|
+
}).trim();
|
|
956
|
+
} catch {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function sha256(input) {
|
|
961
|
+
return createHash("sha256").update(input).digest("hex");
|
|
962
|
+
}
|
|
963
|
+
function printJson(value) {
|
|
964
|
+
console.log(JSON.stringify(value, null, 2));
|
|
965
|
+
}
|
|
966
|
+
var PROMPT_LABELS = ["prod", "staging", "dev"];
|
|
967
|
+
async function runPromptRegistryRead(subcommand, positionals, flags, profileOptions, context) {
|
|
968
|
+
const allowed = {
|
|
969
|
+
list: ["label", "search"],
|
|
970
|
+
show: [],
|
|
971
|
+
proposals: [],
|
|
972
|
+
usage: [],
|
|
973
|
+
decide: ["action", "dry-run"]
|
|
974
|
+
};
|
|
975
|
+
assertFlags(`prompts ${subcommand}`, flags, allowed[subcommand] ?? []);
|
|
976
|
+
const maxPositionals = subcommand === "list" ? 1 : subcommand === "decide" ? 3 : 2;
|
|
977
|
+
assertPositionals(`prompts ${subcommand}`, positionals, maxPositionals, `moda prompts ${subcommand}${subcommand === "list" ? "" : " <key>"}${subcommand === "decide" ? " <proposal_id> --action=merge|reject|reopen" : ""}`);
|
|
978
|
+
const keyOrId = positionals[1];
|
|
979
|
+
const needKey = (usage) => {
|
|
980
|
+
if (!keyOrId)
|
|
981
|
+
throw new CliInputError("<prompt key or id> is required.", `Usage: ${usage}`);
|
|
982
|
+
return encodeURIComponent(keyOrId);
|
|
983
|
+
};
|
|
984
|
+
switch (subcommand) {
|
|
985
|
+
case "list": {
|
|
986
|
+
const label = flags.label;
|
|
987
|
+
if (label && label !== "unlabeled" && !PROMPT_LABELS.includes(label)) {
|
|
988
|
+
throw new CliInputError(`--label must be one of ${PROMPT_LABELS.join(", ")}, or unlabeled.`);
|
|
989
|
+
}
|
|
990
|
+
const response = asRecordValue(await callControlAPI("/prompts", {}, profileOptions));
|
|
991
|
+
const all = Array.isArray(response.prompts) ? response.prompts.map(asRecordValue) : [];
|
|
992
|
+
const search = flags.search?.toLowerCase();
|
|
993
|
+
const prompts = all.filter((prompt) => {
|
|
994
|
+
if (search && !`${prompt.key ?? ""} ${prompt.name ?? ""} ${prompt.description ?? ""}`.toLowerCase().includes(search))
|
|
995
|
+
return false;
|
|
996
|
+
if (!label)
|
|
997
|
+
return true;
|
|
998
|
+
const labelField = (name) => name === "dev" ? prompt.currentVersion ?? prompt.current_version : prompt[`${name}Version`] ?? prompt[`${name}_version`];
|
|
999
|
+
const labels = PROMPT_LABELS.filter((name) => labelField(name));
|
|
1000
|
+
return label === "unlabeled" ? labels.length === 0 : labels.includes(label);
|
|
1001
|
+
});
|
|
1002
|
+
context.output.writeData({ prompts, total: prompts.length, ...label || search ? { filtered_from: all.length } : {} });
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
case "show":
|
|
1006
|
+
context.output.writeData(await callControlAPI(`/prompts/${needKey("moda prompts show <key>")}`, {}, profileOptions));
|
|
1007
|
+
return;
|
|
1008
|
+
case "proposals":
|
|
1009
|
+
context.output.writeData(await callControlAPI(`/prompts/${needKey("moda prompts proposals <key>")}/proposals`, {}, profileOptions));
|
|
1010
|
+
return;
|
|
1011
|
+
case "usage":
|
|
1012
|
+
context.output.writeData(await callControlAPI(`/prompts/${needKey("moda prompts usage <key>")}/usage`, {}, profileOptions));
|
|
1013
|
+
return;
|
|
1014
|
+
case "decide": {
|
|
1015
|
+
const usage = "moda prompts decide <key> <proposal_id> --action=merge|reject|reopen";
|
|
1016
|
+
const id = needKey(usage);
|
|
1017
|
+
const proposalId = positionals[2];
|
|
1018
|
+
const action = flags.action;
|
|
1019
|
+
if (!proposalId)
|
|
1020
|
+
throw new CliInputError("<proposal_id> is required.", `Usage: ${usage}`);
|
|
1021
|
+
if (!action || !["merge", "reject", "reopen"].includes(action)) {
|
|
1022
|
+
throw new CliInputError("--action must be merge, reject, or reopen.", `Usage: ${usage}`);
|
|
1023
|
+
}
|
|
1024
|
+
if (context.dryRun) {
|
|
1025
|
+
context.output.writeData({ dryRun: true, planned: { method: "PATCH", endpoint: `/api/prompts/${id}/proposals/${encodeURIComponent(proposalId)}`, body: { action } } });
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
context.output.writeData(await callControlAPI(`/prompts/${id}/proposals/${encodeURIComponent(proposalId)}`, { method: "PATCH", body: JSON.stringify({ action }) }, profileOptions, { retries: 0 }));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function asRecordValue(value) {
|
|
1034
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export { runPromptsCommand, initPrompts, runPromptSync, writePromptSyncProgress, discoverPrompts, inspectPromptManifest, promptManifestWarnings, migratePromptManifest, parsePromptFile, importHarnessPromptSources };
|