markform 0.1.0 → 0.1.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/DOCS.md +546 -0
- package/README.md +484 -45
- package/SPEC.md +2779 -0
- package/dist/ai-sdk.d.mts +2 -2
- package/dist/ai-sdk.mjs +5 -3
- package/dist/{apply-C0vjijlP.mjs → apply-BfAGTHMh.mjs} +1044 -593
- package/dist/bin.mjs +6 -3
- package/dist/cli-B3NVm6zL.mjs +3904 -0
- package/dist/cli.mjs +6 -3
- package/dist/{coreTypes-T7dAuewt.d.mts → coreTypes-BXhhz9Iq.d.mts} +2795 -685
- package/dist/coreTypes-Dful87E0.mjs +537 -0
- package/dist/index.d.mts +196 -18
- package/dist/index.mjs +5 -3
- package/dist/session-Bqnwi9wp.mjs +110 -0
- package/dist/session-DdAtY2Ni.mjs +4 -0
- package/dist/shared-D7gf27Tr.mjs +3 -0
- package/dist/shared-N_s1M-_K.mjs +176 -0
- package/dist/src-BXRkGFpG.mjs +7587 -0
- package/examples/celebrity-deep-research/celebrity-deep-research.form.md +912 -0
- package/examples/earnings-analysis/earnings-analysis.form.md +6 -1
- package/examples/earnings-analysis/earnings-analysis.valid.ts +119 -59
- package/examples/movie-research/movie-research-basic.form.md +164 -0
- package/examples/movie-research/movie-research-deep.form.md +486 -0
- package/examples/movie-research/movie-research-minimal.form.md +73 -0
- package/examples/simple/simple-mock-filled.form.md +52 -12
- package/examples/simple/simple-skipped-filled.form.md +170 -0
- package/examples/simple/simple-with-skips.session.yaml +189 -0
- package/examples/simple/simple.form.md +34 -12
- package/examples/simple/simple.session.yaml +80 -37
- package/examples/startup-deep-research/startup-deep-research.form.md +456 -0
- package/examples/startup-research/startup-research-mock-filled.form.md +307 -0
- package/examples/startup-research/startup-research.form.md +211 -0
- package/package.json +11 -5
- package/dist/cli-9fvFySww.mjs +0 -2564
- package/dist/src-DBD3Dt4f.mjs +0 -1785
- package/examples/political-research/political-research.form.md +0 -233
- package/examples/political-research/political-research.mock.lincoln.form.md +0 -355
package/dist/cli-9fvFySww.mjs
DELETED
|
@@ -1,2564 +0,0 @@
|
|
|
1
|
-
import { $ as PatchSchema, S as parseRolesFlag, b as USER_ROLE, d as serializeRawMarkdown, f as AGENT_ROLE, g as DEFAULT_PORT, h as DEFAULT_MAX_TURNS, m as DEFAULT_MAX_PATCHES_PER_TURN, p as DEFAULT_MAX_ISSUES, r as inspect, t as applyPatches, u as serialize, x as formatSuggestedLlms, y as SUGGESTED_LLMS } from "./apply-C0vjijlP.mjs";
|
|
2
|
-
import { _ as parseForm, a as resolveModel, c as createMockAgent, h as serializeSession, i as getProviderNames, o as createLiveAgent, r as getProviderInfo, t as VERSION, u as createHarness } from "./src-DBD3Dt4f.mjs";
|
|
3
|
-
import YAML from "yaml";
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import pc from "picocolors";
|
|
6
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
7
|
-
import * as p from "@clack/prompts";
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { exec } from "node:child_process";
|
|
11
|
-
import { createServer } from "node:http";
|
|
12
|
-
|
|
13
|
-
//#region src/cli/lib/naming.ts
|
|
14
|
-
/**
|
|
15
|
-
* Naming convention utilities for JSON/YAML output.
|
|
16
|
-
*
|
|
17
|
-
* Converts between camelCase (TypeScript internal) and snake_case (JSON/YAML output).
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* Convert a camelCase string to snake_case.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* toSnakeCase("fieldCount") // "field_count"
|
|
24
|
-
* toSnakeCase("parentFieldId") // "parent_field_id"
|
|
25
|
-
* toSnakeCase("already_snake") // "already_snake"
|
|
26
|
-
*/
|
|
27
|
-
function toSnakeCase(str) {
|
|
28
|
-
return str.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Recursively convert all object keys from camelCase to snake_case.
|
|
32
|
-
*
|
|
33
|
-
* Handles nested objects and arrays. Primitives are returned unchanged.
|
|
34
|
-
*/
|
|
35
|
-
function convertKeysToSnakeCase(obj) {
|
|
36
|
-
if (obj === null || obj === void 0) return obj;
|
|
37
|
-
if (Array.isArray(obj)) return obj.map(convertKeysToSnakeCase);
|
|
38
|
-
if (typeof obj === "object") {
|
|
39
|
-
const result = {};
|
|
40
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
41
|
-
const snakeKey = toSnakeCase(key);
|
|
42
|
-
result[snakeKey] = convertKeysToSnakeCase(value);
|
|
43
|
-
}
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
return obj;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
//#endregion
|
|
50
|
-
//#region src/cli/lib/shared.ts
|
|
51
|
-
/**
|
|
52
|
-
* Valid format options for Commander choice validation.
|
|
53
|
-
*/
|
|
54
|
-
const OUTPUT_FORMATS = [
|
|
55
|
-
"console",
|
|
56
|
-
"plaintext",
|
|
57
|
-
"yaml",
|
|
58
|
-
"json",
|
|
59
|
-
"markform",
|
|
60
|
-
"markdown"
|
|
61
|
-
];
|
|
62
|
-
/**
|
|
63
|
-
* Extract command context from Commander options.
|
|
64
|
-
*/
|
|
65
|
-
function getCommandContext(command) {
|
|
66
|
-
const opts = command.optsWithGlobals();
|
|
67
|
-
return {
|
|
68
|
-
dryRun: opts.dryRun ?? false,
|
|
69
|
-
verbose: opts.verbose ?? false,
|
|
70
|
-
quiet: opts.quiet ?? false,
|
|
71
|
-
format: opts.format ?? "console"
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Check if output should use colors.
|
|
76
|
-
* Returns true for console format when stdout is a TTY.
|
|
77
|
-
*/
|
|
78
|
-
function shouldUseColors(ctx) {
|
|
79
|
-
if (ctx.format === "plaintext" || ctx.format === "yaml" || ctx.format === "json") return false;
|
|
80
|
-
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Format structured data according to output format.
|
|
84
|
-
*
|
|
85
|
-
* JSON and YAML outputs are converted to snake_case keys for consistency.
|
|
86
|
-
*/
|
|
87
|
-
function formatOutput(ctx, data, consoleFormatter) {
|
|
88
|
-
switch (ctx.format) {
|
|
89
|
-
case "json": return JSON.stringify(convertKeysToSnakeCase(data), null, 2);
|
|
90
|
-
case "yaml": return YAML.stringify(convertKeysToSnakeCase(data));
|
|
91
|
-
case "plaintext":
|
|
92
|
-
case "console":
|
|
93
|
-
default:
|
|
94
|
-
if (consoleFormatter) return consoleFormatter(data, shouldUseColors(ctx));
|
|
95
|
-
return YAML.stringify(convertKeysToSnakeCase(data));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Log a dry-run message.
|
|
100
|
-
*/
|
|
101
|
-
function logDryRun(message, details) {
|
|
102
|
-
console.log(pc.yellow(`[DRY RUN] ${message}`));
|
|
103
|
-
if (details) console.log(pc.dim(JSON.stringify(details, null, 2)));
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Log a verbose message (only shown if --verbose is set).
|
|
107
|
-
*/
|
|
108
|
-
function logVerbose(ctx, message) {
|
|
109
|
-
if (ctx.verbose) console.log(pc.dim(message));
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Log an info message (hidden if --quiet is set).
|
|
113
|
-
*/
|
|
114
|
-
function logInfo(ctx, message) {
|
|
115
|
-
if (!ctx.quiet) console.log(message);
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Log an error message (always shown).
|
|
119
|
-
*/
|
|
120
|
-
function logError(message) {
|
|
121
|
-
console.error(pc.red(`Error: ${message}`));
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Log a success message (hidden if --quiet is set).
|
|
125
|
-
*/
|
|
126
|
-
function logSuccess(ctx, message) {
|
|
127
|
-
if (!ctx.quiet) console.log(pc.green(message));
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Log a timing message (hidden if --quiet is set).
|
|
131
|
-
*/
|
|
132
|
-
function logTiming(ctx, label, durationMs) {
|
|
133
|
-
if (!ctx.quiet) {
|
|
134
|
-
const seconds = (durationMs / 1e3).toFixed(1);
|
|
135
|
-
console.log(pc.cyan(`⏰ ${label}: ${seconds}s`));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Log a warning message (hidden if --quiet is set).
|
|
140
|
-
*/
|
|
141
|
-
function logWarn(ctx, message) {
|
|
142
|
-
if (!ctx.quiet) console.log(pc.yellow(`⚠️ ${message}`));
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Format a file path for display: relative to cwd, colored green.
|
|
146
|
-
* If the path is within the cwd, shows as relative (e.g., "./simple-filled1.form.md")
|
|
147
|
-
* If outside cwd, shows the absolute path.
|
|
148
|
-
*/
|
|
149
|
-
function formatPath(absolutePath, cwd = process.cwd()) {
|
|
150
|
-
const relativePath = relative(cwd, absolutePath);
|
|
151
|
-
const displayPath = relativePath.startsWith("..") ? absolutePath : `./${relativePath}`;
|
|
152
|
-
return pc.green(displayPath);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Read a file and return its contents.
|
|
156
|
-
*/
|
|
157
|
-
async function readFile(filePath) {
|
|
158
|
-
const { readFile: fsReadFile } = await import("node:fs/promises");
|
|
159
|
-
return fsReadFile(filePath, "utf-8");
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Write contents to a file.
|
|
163
|
-
*/
|
|
164
|
-
async function writeFile(filePath, contents) {
|
|
165
|
-
const { writeFile: fsWriteFile } = await import("node:fs/promises");
|
|
166
|
-
await fsWriteFile(filePath, contents, "utf-8");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
//#endregion
|
|
170
|
-
//#region src/cli/commands/apply.ts
|
|
171
|
-
/**
|
|
172
|
-
* Format state badge for console output.
|
|
173
|
-
*/
|
|
174
|
-
function formatState$2(state, useColors) {
|
|
175
|
-
const [text, colorFn] = {
|
|
176
|
-
complete: ["✓ complete", pc.green],
|
|
177
|
-
incomplete: ["○ incomplete", pc.yellow],
|
|
178
|
-
empty: ["◌ empty", pc.dim],
|
|
179
|
-
invalid: ["✗ invalid", pc.red]
|
|
180
|
-
}[state] ?? [state, (s) => s];
|
|
181
|
-
return useColors ? colorFn(text) : text;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Format apply report for console output.
|
|
185
|
-
*/
|
|
186
|
-
function formatConsoleReport$2(report, useColors) {
|
|
187
|
-
const lines = [];
|
|
188
|
-
const bold = useColors ? pc.bold : (s) => s;
|
|
189
|
-
const dim = useColors ? pc.dim : (s) => s;
|
|
190
|
-
const cyan = useColors ? pc.cyan : (s) => s;
|
|
191
|
-
const green = useColors ? pc.green : (s) => s;
|
|
192
|
-
const red = useColors ? pc.red : (s) => s;
|
|
193
|
-
lines.push(bold(cyan("Apply Result")));
|
|
194
|
-
lines.push("");
|
|
195
|
-
const statusColor = report.apply_status === "applied" ? green : red;
|
|
196
|
-
lines.push(`${bold("Status:")} ${statusColor(report.apply_status)}`);
|
|
197
|
-
lines.push(`${bold("Form State:")} ${formatState$2(report.form_state, useColors)}`);
|
|
198
|
-
lines.push(`${bold("Complete:")} ${report.is_complete ? green("yes") : dim("no")}`);
|
|
199
|
-
lines.push("");
|
|
200
|
-
const counts = report.progress.counts;
|
|
201
|
-
lines.push(bold("Progress:"));
|
|
202
|
-
lines.push(` Total fields: ${counts.totalFields}`);
|
|
203
|
-
lines.push(` Complete: ${counts.completeFields}`);
|
|
204
|
-
lines.push(` Incomplete: ${counts.incompleteFields}`);
|
|
205
|
-
lines.push(` Empty (required): ${counts.emptyRequiredFields}`);
|
|
206
|
-
lines.push("");
|
|
207
|
-
if (report.issues.length > 0) {
|
|
208
|
-
lines.push(bold(`Issues (${report.issues.length}):`));
|
|
209
|
-
for (const issue of report.issues) {
|
|
210
|
-
const priority = `P${issue.priority}`;
|
|
211
|
-
lines.push(` ${dim(priority)} ${dim(issue.ref)}: ${issue.message}`);
|
|
212
|
-
}
|
|
213
|
-
} else lines.push(dim("No issues."));
|
|
214
|
-
return lines.join("\n");
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Register the apply command.
|
|
218
|
-
*/
|
|
219
|
-
function registerApplyCommand(program) {
|
|
220
|
-
program.command("apply <file>").description("Apply patches to a form").option("--patch <json>", "JSON array of patches to apply").option("-o, --output <file>", "Output file (defaults to stdout)").option("--report", "Output apply result report instead of modified form").action(async (file, options, cmd) => {
|
|
221
|
-
const ctx = getCommandContext(cmd);
|
|
222
|
-
try {
|
|
223
|
-
if (!options.patch) {
|
|
224
|
-
logError("--patch option is required");
|
|
225
|
-
process.exit(1);
|
|
226
|
-
}
|
|
227
|
-
logVerbose(ctx, `Reading file: ${file}`);
|
|
228
|
-
const content = await readFile(file);
|
|
229
|
-
logVerbose(ctx, "Parsing form...");
|
|
230
|
-
const form = parseForm(content);
|
|
231
|
-
logVerbose(ctx, "Parsing patches...");
|
|
232
|
-
let parsedJson;
|
|
233
|
-
try {
|
|
234
|
-
parsedJson = JSON.parse(options.patch);
|
|
235
|
-
} catch {
|
|
236
|
-
logError("Invalid JSON in --patch option");
|
|
237
|
-
process.exit(1);
|
|
238
|
-
}
|
|
239
|
-
if (!Array.isArray(parsedJson)) {
|
|
240
|
-
logError("--patch must be a JSON array");
|
|
241
|
-
process.exit(1);
|
|
242
|
-
}
|
|
243
|
-
const patches = parsedJson;
|
|
244
|
-
const validatedPatches = [];
|
|
245
|
-
for (let i = 0; i < patches.length; i++) {
|
|
246
|
-
const result$1 = PatchSchema.safeParse(patches[i]);
|
|
247
|
-
if (!result$1.success) {
|
|
248
|
-
logError(`Invalid patch at index ${i}: ${result$1.error.issues[0]?.message ?? "Unknown error"}`);
|
|
249
|
-
process.exit(1);
|
|
250
|
-
}
|
|
251
|
-
validatedPatches.push(result$1.data);
|
|
252
|
-
}
|
|
253
|
-
if (ctx.dryRun) {
|
|
254
|
-
logDryRun(`Would apply ${validatedPatches.length} patches to ${file}`, { patches: validatedPatches });
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
logVerbose(ctx, `Applying ${validatedPatches.length} patches...`);
|
|
258
|
-
const result = applyPatches(form, validatedPatches);
|
|
259
|
-
if (result.applyStatus === "rejected") {
|
|
260
|
-
logError("Patches rejected - structural validation failed");
|
|
261
|
-
const output = formatOutput(ctx, {
|
|
262
|
-
apply_status: result.applyStatus,
|
|
263
|
-
form_state: result.formState,
|
|
264
|
-
is_complete: result.isComplete,
|
|
265
|
-
structure: result.structureSummary,
|
|
266
|
-
progress: result.progressSummary,
|
|
267
|
-
issues: result.issues
|
|
268
|
-
}, (data, useColors) => formatConsoleReport$2(data, useColors));
|
|
269
|
-
console.error(output);
|
|
270
|
-
process.exit(1);
|
|
271
|
-
}
|
|
272
|
-
if (options.report) {
|
|
273
|
-
const output = formatOutput(ctx, {
|
|
274
|
-
apply_status: result.applyStatus,
|
|
275
|
-
form_state: result.formState,
|
|
276
|
-
is_complete: result.isComplete,
|
|
277
|
-
structure: result.structureSummary,
|
|
278
|
-
progress: result.progressSummary,
|
|
279
|
-
issues: result.issues
|
|
280
|
-
}, (data, useColors) => formatConsoleReport$2(data, useColors));
|
|
281
|
-
if (options.output) {
|
|
282
|
-
await writeFile(options.output, output);
|
|
283
|
-
logSuccess(ctx, `Report written to ${options.output}`);
|
|
284
|
-
} else console.log(output);
|
|
285
|
-
} else {
|
|
286
|
-
const output = serialize(form);
|
|
287
|
-
if (options.output) {
|
|
288
|
-
await writeFile(options.output, output);
|
|
289
|
-
logSuccess(ctx, `Modified form written to ${options.output}`);
|
|
290
|
-
} else console.log(output);
|
|
291
|
-
}
|
|
292
|
-
} catch (error) {
|
|
293
|
-
logError(error instanceof Error ? error.message : String(error));
|
|
294
|
-
process.exit(1);
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
//#endregion
|
|
300
|
-
//#region src/cli/commands/dump.ts
|
|
301
|
-
/**
|
|
302
|
-
* Format a field value for console display.
|
|
303
|
-
*/
|
|
304
|
-
function formatFieldValue$1(value, useColors) {
|
|
305
|
-
const dim = useColors ? pc.dim : (s) => s;
|
|
306
|
-
const green = useColors ? pc.green : (s) => s;
|
|
307
|
-
if (!value) return dim("(empty)");
|
|
308
|
-
switch (value.kind) {
|
|
309
|
-
case "string": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
310
|
-
case "number": return value.value !== null ? green(String(value.value)) : dim("(empty)");
|
|
311
|
-
case "string_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
|
|
312
|
-
case "single_select": return value.selected ? green(value.selected) : dim("(none selected)");
|
|
313
|
-
case "multi_select": return value.selected.length > 0 ? green(`[${value.selected.join(", ")}]`) : dim("(none selected)");
|
|
314
|
-
case "checkboxes": {
|
|
315
|
-
const entries = Object.entries(value.values);
|
|
316
|
-
if (entries.length === 0) return dim("(no entries)");
|
|
317
|
-
return entries.map(([k, v]) => `${k}:${v}`).join(", ");
|
|
318
|
-
}
|
|
319
|
-
default: return dim("(unknown)");
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Format values for console output.
|
|
324
|
-
*/
|
|
325
|
-
function formatConsoleValues(values, useColors) {
|
|
326
|
-
const lines = [];
|
|
327
|
-
const bold = useColors ? pc.bold : (s) => s;
|
|
328
|
-
for (const [fieldId, value] of Object.entries(values)) {
|
|
329
|
-
const valueStr = formatFieldValue$1(value, useColors);
|
|
330
|
-
lines.push(`${bold(fieldId)}: ${valueStr}`);
|
|
331
|
-
}
|
|
332
|
-
if (lines.length === 0) {
|
|
333
|
-
const dim = useColors ? pc.dim : (s) => s;
|
|
334
|
-
lines.push(dim("(no values)"));
|
|
335
|
-
}
|
|
336
|
-
return lines.join("\n");
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Convert FieldValue to a plain value for JSON/YAML serialization.
|
|
340
|
-
*/
|
|
341
|
-
function toPlainValue(value) {
|
|
342
|
-
switch (value.kind) {
|
|
343
|
-
case "string": return value.value ?? null;
|
|
344
|
-
case "number": return value.value ?? null;
|
|
345
|
-
case "string_list": return value.items;
|
|
346
|
-
case "single_select": return value.selected ?? null;
|
|
347
|
-
case "multi_select": return value.selected;
|
|
348
|
-
case "checkboxes": return value.values;
|
|
349
|
-
default: return null;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Register the dump command.
|
|
354
|
-
*/
|
|
355
|
-
function registerDumpCommand(program) {
|
|
356
|
-
program.command("dump <file>").description("Extract and display form values only (lightweight inspect)").action(async (file, _options, cmd) => {
|
|
357
|
-
const ctx = getCommandContext(cmd);
|
|
358
|
-
try {
|
|
359
|
-
logVerbose(ctx, `Reading file: ${file}`);
|
|
360
|
-
const content = await readFile(file);
|
|
361
|
-
logVerbose(ctx, "Parsing form...");
|
|
362
|
-
const form = parseForm(content);
|
|
363
|
-
if (ctx.format === "json" || ctx.format === "yaml") {
|
|
364
|
-
const plainValues = {};
|
|
365
|
-
for (const [fieldId, value] of Object.entries(form.valuesByFieldId)) plainValues[fieldId] = toPlainValue(value);
|
|
366
|
-
const output = formatOutput(ctx, plainValues, () => "");
|
|
367
|
-
console.log(output);
|
|
368
|
-
} else {
|
|
369
|
-
const output = formatOutput(ctx, form.valuesByFieldId, (data, useColors) => formatConsoleValues(data, useColors));
|
|
370
|
-
console.log(output);
|
|
371
|
-
}
|
|
372
|
-
} catch (error) {
|
|
373
|
-
logError(error instanceof Error ? error.message : String(error));
|
|
374
|
-
process.exit(1);
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
//#endregion
|
|
380
|
-
//#region src/cli/lib/exportHelpers.ts
|
|
381
|
-
/**
|
|
382
|
-
* Export helpers for multi-format form output.
|
|
383
|
-
*
|
|
384
|
-
* Provides reusable functions for exporting forms to multiple formats:
|
|
385
|
-
* - Markform format (.form.md) - canonical form with directives
|
|
386
|
-
* - Raw markdown (.raw.md) - plain readable markdown
|
|
387
|
-
* - YAML values (.yml) - extracted field values
|
|
388
|
-
*/
|
|
389
|
-
/**
|
|
390
|
-
* Convert field values to plain values for YAML export.
|
|
391
|
-
*
|
|
392
|
-
* Extracts the underlying values from the typed FieldValue wrappers
|
|
393
|
-
* for a cleaner YAML representation.
|
|
394
|
-
*/
|
|
395
|
-
function toPlainValues(form) {
|
|
396
|
-
const result = {};
|
|
397
|
-
for (const [fieldId, value] of Object.entries(form.valuesByFieldId)) switch (value.kind) {
|
|
398
|
-
case "string":
|
|
399
|
-
result[fieldId] = value.value ?? null;
|
|
400
|
-
break;
|
|
401
|
-
case "number":
|
|
402
|
-
result[fieldId] = value.value ?? null;
|
|
403
|
-
break;
|
|
404
|
-
case "string_list":
|
|
405
|
-
result[fieldId] = value.items;
|
|
406
|
-
break;
|
|
407
|
-
case "single_select":
|
|
408
|
-
result[fieldId] = value.selected ?? null;
|
|
409
|
-
break;
|
|
410
|
-
case "multi_select":
|
|
411
|
-
result[fieldId] = value.selected;
|
|
412
|
-
break;
|
|
413
|
-
case "checkboxes":
|
|
414
|
-
result[fieldId] = value.values;
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
return result;
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Derive export paths from a base form path.
|
|
421
|
-
*
|
|
422
|
-
* @param basePath - Path to the .form.md file
|
|
423
|
-
* @returns Object with paths for all export formats
|
|
424
|
-
*/
|
|
425
|
-
function deriveExportPaths(basePath) {
|
|
426
|
-
return {
|
|
427
|
-
formPath: basePath,
|
|
428
|
-
rawPath: basePath.replace(/\.form\.md$/, ".raw.md"),
|
|
429
|
-
yamlPath: basePath.replace(/\.form\.md$/, ".yml")
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Export form to multiple formats.
|
|
434
|
-
*
|
|
435
|
-
* Writes:
|
|
436
|
-
* - Markform format (.form.md) - canonical form with directives
|
|
437
|
-
* - Raw markdown (.raw.md) - plain readable markdown (no directives)
|
|
438
|
-
* - YAML values (.yml) - extracted field values only
|
|
439
|
-
*
|
|
440
|
-
* @param form - The parsed form to export
|
|
441
|
-
* @param basePath - Base path for the .form.md file (other paths are derived)
|
|
442
|
-
* @returns Paths to all exported files
|
|
443
|
-
*/
|
|
444
|
-
function exportMultiFormat(form, basePath) {
|
|
445
|
-
const paths = deriveExportPaths(basePath);
|
|
446
|
-
const formContent = serialize(form);
|
|
447
|
-
writeFileSync(paths.formPath, formContent, "utf-8");
|
|
448
|
-
const rawContent = serializeRawMarkdown(form);
|
|
449
|
-
writeFileSync(paths.rawPath, rawContent, "utf-8");
|
|
450
|
-
const values = toPlainValues(form);
|
|
451
|
-
const yamlContent = YAML.stringify(values);
|
|
452
|
-
writeFileSync(paths.yamlPath, yamlContent, "utf-8");
|
|
453
|
-
return paths;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
//#endregion
|
|
457
|
-
//#region src/cli/examples/exampleRegistry.ts
|
|
458
|
-
/**
|
|
459
|
-
* Example form registry.
|
|
460
|
-
* Provides form content from the examples directory for the examples CLI command.
|
|
461
|
-
*/
|
|
462
|
-
/** Example definitions without content - content is loaded lazily. */
|
|
463
|
-
const EXAMPLE_DEFINITIONS = [
|
|
464
|
-
{
|
|
465
|
-
id: "simple",
|
|
466
|
-
title: "Simple Test Form",
|
|
467
|
-
description: "User and agent roles for testing full workflow. User fills required fields, agent fills optional.",
|
|
468
|
-
filename: "simple.form.md",
|
|
469
|
-
path: "simple/simple.form.md"
|
|
470
|
-
},
|
|
471
|
-
{
|
|
472
|
-
id: "political-research",
|
|
473
|
-
title: "Political Research",
|
|
474
|
-
description: "Biographical research form with one user field (name) and agent-filled details. Uses web search.",
|
|
475
|
-
filename: "political-research.form.md",
|
|
476
|
-
path: "political-research/political-research.form.md"
|
|
477
|
-
},
|
|
478
|
-
{
|
|
479
|
-
id: "earnings-analysis",
|
|
480
|
-
title: "Company Quarterly Analysis",
|
|
481
|
-
description: "Financial analysis with one user field (company) and agent-filled quarterly analysis sections.",
|
|
482
|
-
filename: "earnings-analysis.form.md",
|
|
483
|
-
path: "earnings-analysis/earnings-analysis.form.md"
|
|
484
|
-
}
|
|
485
|
-
];
|
|
486
|
-
/**
|
|
487
|
-
* Get the path to the examples directory.
|
|
488
|
-
* Works both during development and when installed as a package.
|
|
489
|
-
*/
|
|
490
|
-
function getExamplesDir() {
|
|
491
|
-
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
492
|
-
if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "examples");
|
|
493
|
-
return join(dirname(dirname(dirname(thisDir))), "examples");
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* Load the content of an example form.
|
|
497
|
-
* @param exampleId - The example ID (e.g., 'simple', 'political-research')
|
|
498
|
-
* @returns The form content as a string
|
|
499
|
-
* @throws Error if the example is not found
|
|
500
|
-
*/
|
|
501
|
-
function loadExampleContent(exampleId) {
|
|
502
|
-
const example = EXAMPLE_DEFINITIONS.find((e) => e.id === exampleId);
|
|
503
|
-
if (!example) throw new Error(`Unknown example: ${exampleId}`);
|
|
504
|
-
const filePath = join(getExamplesDir(), example.path);
|
|
505
|
-
try {
|
|
506
|
-
return readFileSync(filePath, "utf-8");
|
|
507
|
-
} catch (error) {
|
|
508
|
-
throw new Error(`Failed to load example '${exampleId}' from ${filePath}: ${error}`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Get an example definition by ID.
|
|
513
|
-
*/
|
|
514
|
-
function getExampleById(id) {
|
|
515
|
-
return EXAMPLE_DEFINITIONS.find((e) => e.id === id);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
//#endregion
|
|
519
|
-
//#region src/cli/lib/versioning.ts
|
|
520
|
-
/**
|
|
521
|
-
* Versioned filename utilities for form output.
|
|
522
|
-
*
|
|
523
|
-
* Generates versioned filenames to avoid overwriting existing files.
|
|
524
|
-
* Pattern: name.form.md → name-filled1.form.md → name-filled2.form.md
|
|
525
|
-
*/
|
|
526
|
-
/**
|
|
527
|
-
* Version pattern that matches -filledN or _filledN before the extension.
|
|
528
|
-
* Also matches legacy -vN pattern for backwards compatibility.
|
|
529
|
-
*/
|
|
530
|
-
const VERSION_PATTERN = /^(.+?)(?:[-_]?(?:filled|v)(\d+))?(\.form\.md)$/i;
|
|
531
|
-
/**
|
|
532
|
-
* Extension pattern for fallback matching.
|
|
533
|
-
*/
|
|
534
|
-
const EXTENSION_PATTERN = /^(.+)(\.form\.md)$/i;
|
|
535
|
-
/**
|
|
536
|
-
* Parse a versioned filename into its components.
|
|
537
|
-
*
|
|
538
|
-
* @param filePath - Path to parse
|
|
539
|
-
* @returns Parsed components or null if not a valid form file
|
|
540
|
-
*/
|
|
541
|
-
function parseVersionedPath(filePath) {
|
|
542
|
-
const match = VERSION_PATTERN.exec(filePath);
|
|
543
|
-
if (match) {
|
|
544
|
-
const base = match[1];
|
|
545
|
-
const versionStr = match[2];
|
|
546
|
-
const ext = match[3];
|
|
547
|
-
if (base !== void 0 && ext !== void 0) return {
|
|
548
|
-
base,
|
|
549
|
-
version: versionStr ? parseInt(versionStr, 10) : null,
|
|
550
|
-
extension: ext
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
const extMatch = EXTENSION_PATTERN.exec(filePath);
|
|
554
|
-
if (extMatch) {
|
|
555
|
-
const base = extMatch[1];
|
|
556
|
-
const ext = extMatch[2];
|
|
557
|
-
if (base !== void 0 && ext !== void 0) return {
|
|
558
|
-
base,
|
|
559
|
-
version: null,
|
|
560
|
-
extension: ext
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
return null;
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Generate a versioned filename that doesn't conflict with existing files.
|
|
567
|
-
*
|
|
568
|
-
* Starts from the incremented version and keeps incrementing until
|
|
569
|
-
* a non-existent filename is found.
|
|
570
|
-
*
|
|
571
|
-
* @param filePath - Original file path
|
|
572
|
-
* @returns Path to a non-existent versioned file
|
|
573
|
-
*/
|
|
574
|
-
function generateVersionedPath(filePath) {
|
|
575
|
-
const parsed = parseVersionedPath(filePath);
|
|
576
|
-
if (!parsed) {
|
|
577
|
-
let candidate$1 = `${filePath}-filled1`;
|
|
578
|
-
let version$1 = 1;
|
|
579
|
-
while (existsSync(candidate$1)) {
|
|
580
|
-
version$1++;
|
|
581
|
-
candidate$1 = `${filePath}-filled${version$1}`;
|
|
582
|
-
}
|
|
583
|
-
return candidate$1;
|
|
584
|
-
}
|
|
585
|
-
let version = parsed.version !== null ? parsed.version + 1 : 1;
|
|
586
|
-
let candidate = `${parsed.base}-filled${version}${parsed.extension}`;
|
|
587
|
-
while (existsSync(candidate)) {
|
|
588
|
-
version++;
|
|
589
|
-
candidate = `${parsed.base}-filled${version}${parsed.extension}`;
|
|
590
|
-
}
|
|
591
|
-
return candidate;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
//#endregion
|
|
595
|
-
//#region src/cli/lib/interactivePrompts.ts
|
|
596
|
-
/**
|
|
597
|
-
* Interactive prompts module for console-based form filling.
|
|
598
|
-
*
|
|
599
|
-
* Uses @clack/prompts to provide a rich terminal UI for users to
|
|
600
|
-
* fill form fields interactively.
|
|
601
|
-
*/
|
|
602
|
-
/**
|
|
603
|
-
* Get field description from form docs.
|
|
604
|
-
*/
|
|
605
|
-
function getFieldDescription(form, fieldId) {
|
|
606
|
-
return form.docs.find((d) => d.ref === fieldId && (d.tag === "description" || d.tag === "instructions"))?.bodyMarkdown;
|
|
607
|
-
}
|
|
608
|
-
/**
|
|
609
|
-
* Get a field by ID from the form schema.
|
|
610
|
-
*/
|
|
611
|
-
function getFieldById(form, fieldId) {
|
|
612
|
-
for (const group of form.schema.groups) {
|
|
613
|
-
const field = group.children.find((f) => f.id === fieldId);
|
|
614
|
-
if (field) return field;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
/**
|
|
618
|
-
* Format field label with required indicator and progress.
|
|
619
|
-
*/
|
|
620
|
-
function formatFieldLabel(ctx) {
|
|
621
|
-
const required = ctx.field.required ? pc.red("*") : "";
|
|
622
|
-
const progress = pc.dim(`(${ctx.index} of ${ctx.total})`);
|
|
623
|
-
return `${ctx.field.label}${required} ${progress}`;
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Prompt for a string field value.
|
|
627
|
-
*/
|
|
628
|
-
async function promptForString(ctx) {
|
|
629
|
-
const field = ctx.field;
|
|
630
|
-
const currentVal = ctx.currentValue?.kind === "string" ? ctx.currentValue.value : null;
|
|
631
|
-
const result = await p.text({
|
|
632
|
-
message: formatFieldLabel(ctx),
|
|
633
|
-
placeholder: currentVal ?? (ctx.description ? ctx.description.slice(0, 60) : void 0),
|
|
634
|
-
initialValue: currentVal ?? "",
|
|
635
|
-
validate: (value) => {
|
|
636
|
-
if (field.required && !value.trim()) return "This field is required";
|
|
637
|
-
if (field.minLength && value.length < field.minLength) return `Minimum ${field.minLength} characters required`;
|
|
638
|
-
if (field.maxLength && value.length > field.maxLength) return `Maximum ${field.maxLength} characters allowed`;
|
|
639
|
-
if (field.pattern && !new RegExp(field.pattern).test(value)) return `Must match pattern: ${field.pattern}`;
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
if (p.isCancel(result)) return null;
|
|
643
|
-
if (!result && !field.required) return null;
|
|
644
|
-
return {
|
|
645
|
-
op: "set_string",
|
|
646
|
-
fieldId: field.id,
|
|
647
|
-
value: result || null
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
/**
|
|
651
|
-
* Prompt for a number field value.
|
|
652
|
-
*/
|
|
653
|
-
async function promptForNumber(ctx) {
|
|
654
|
-
const field = ctx.field;
|
|
655
|
-
const currentVal = ctx.currentValue?.kind === "number" ? ctx.currentValue.value : null;
|
|
656
|
-
const result = await p.text({
|
|
657
|
-
message: formatFieldLabel(ctx),
|
|
658
|
-
placeholder: currentVal !== null ? String(currentVal) : void 0,
|
|
659
|
-
initialValue: currentVal !== null ? String(currentVal) : "",
|
|
660
|
-
validate: (value) => {
|
|
661
|
-
if (field.required && !value.trim()) return "This field is required";
|
|
662
|
-
if (!value.trim()) return;
|
|
663
|
-
const num = Number(value);
|
|
664
|
-
if (isNaN(num)) return "Please enter a valid number";
|
|
665
|
-
if (field.integer && !Number.isInteger(num)) return "Please enter a whole number";
|
|
666
|
-
if (field.min !== void 0 && num < field.min) return `Minimum value is ${field.min}`;
|
|
667
|
-
if (field.max !== void 0 && num > field.max) return `Maximum value is ${field.max}`;
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
if (p.isCancel(result)) return null;
|
|
671
|
-
if (!result && !field.required) return null;
|
|
672
|
-
return {
|
|
673
|
-
op: "set_number",
|
|
674
|
-
fieldId: field.id,
|
|
675
|
-
value: result ? Number(result) : null
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
/**
|
|
679
|
-
* Prompt for a string list field value.
|
|
680
|
-
*/
|
|
681
|
-
async function promptForStringList(ctx) {
|
|
682
|
-
const field = ctx.field;
|
|
683
|
-
const currentItems = ctx.currentValue?.kind === "string_list" ? ctx.currentValue.items : [];
|
|
684
|
-
const hint = ctx.description ? `${ctx.description.slice(0, 50)}... (one item per line)` : "Enter items, one per line. Press Enter twice when done.";
|
|
685
|
-
const result = await p.text({
|
|
686
|
-
message: formatFieldLabel(ctx),
|
|
687
|
-
placeholder: hint,
|
|
688
|
-
initialValue: currentItems.join("\n"),
|
|
689
|
-
validate: (value) => {
|
|
690
|
-
const items$1 = value.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
691
|
-
if (field.required && items$1.length === 0) return "At least one item is required";
|
|
692
|
-
if (field.minItems && items$1.length < field.minItems) return `Minimum ${field.minItems} items required`;
|
|
693
|
-
if (field.maxItems && items$1.length > field.maxItems) return `Maximum ${field.maxItems} items allowed`;
|
|
694
|
-
}
|
|
695
|
-
});
|
|
696
|
-
if (p.isCancel(result)) return null;
|
|
697
|
-
const items = result.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
698
|
-
if (items.length === 0 && !field.required) return null;
|
|
699
|
-
return {
|
|
700
|
-
op: "set_string_list",
|
|
701
|
-
fieldId: field.id,
|
|
702
|
-
items
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Prompt for a single-select field value.
|
|
707
|
-
*/
|
|
708
|
-
async function promptForSingleSelect(ctx) {
|
|
709
|
-
const field = ctx.field;
|
|
710
|
-
const currentSelected = ctx.currentValue?.kind === "single_select" ? ctx.currentValue.selected : null;
|
|
711
|
-
const options = field.options.map((opt) => ({
|
|
712
|
-
value: opt.id,
|
|
713
|
-
label: opt.label
|
|
714
|
-
}));
|
|
715
|
-
const result = await p.select({
|
|
716
|
-
message: formatFieldLabel(ctx),
|
|
717
|
-
options,
|
|
718
|
-
initialValue: currentSelected ?? void 0
|
|
719
|
-
});
|
|
720
|
-
if (p.isCancel(result)) return null;
|
|
721
|
-
return {
|
|
722
|
-
op: "set_single_select",
|
|
723
|
-
fieldId: field.id,
|
|
724
|
-
selected: result
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Prompt for a multi-select field value.
|
|
729
|
-
*/
|
|
730
|
-
async function promptForMultiSelect(ctx) {
|
|
731
|
-
const field = ctx.field;
|
|
732
|
-
const currentSelected = ctx.currentValue?.kind === "multi_select" ? ctx.currentValue.selected : [];
|
|
733
|
-
const options = field.options.map((opt) => ({
|
|
734
|
-
value: opt.id,
|
|
735
|
-
label: opt.label
|
|
736
|
-
}));
|
|
737
|
-
const result = await p.multiselect({
|
|
738
|
-
message: formatFieldLabel(ctx),
|
|
739
|
-
options,
|
|
740
|
-
initialValues: currentSelected,
|
|
741
|
-
required: field.required
|
|
742
|
-
});
|
|
743
|
-
if (p.isCancel(result)) return null;
|
|
744
|
-
return {
|
|
745
|
-
op: "set_multi_select",
|
|
746
|
-
fieldId: field.id,
|
|
747
|
-
selected: result
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
/**
|
|
751
|
-
* Prompt for a checkboxes field value.
|
|
752
|
-
*
|
|
753
|
-
* Behavior varies by checkboxMode:
|
|
754
|
-
* - simple: multiselect to pick items marked as done
|
|
755
|
-
* - multi: per-option select with 5 states
|
|
756
|
-
* - explicit: per-option yes/no/skip
|
|
757
|
-
*/
|
|
758
|
-
async function promptForCheckboxes(ctx) {
|
|
759
|
-
const field = ctx.field;
|
|
760
|
-
const currentValues = ctx.currentValue?.kind === "checkboxes" ? ctx.currentValue.values : {};
|
|
761
|
-
if (field.checkboxMode === "simple") {
|
|
762
|
-
const options = field.options.map((opt) => ({
|
|
763
|
-
value: opt.id,
|
|
764
|
-
label: opt.label
|
|
765
|
-
}));
|
|
766
|
-
const currentlyDone = field.options.filter((opt) => currentValues[opt.id] === "done").map((opt) => opt.id);
|
|
767
|
-
const result = await p.multiselect({
|
|
768
|
-
message: formatFieldLabel(ctx),
|
|
769
|
-
options,
|
|
770
|
-
initialValues: currentlyDone,
|
|
771
|
-
required: field.required && field.minDone !== void 0 && field.minDone > 0
|
|
772
|
-
});
|
|
773
|
-
if (p.isCancel(result)) return null;
|
|
774
|
-
const selected = result;
|
|
775
|
-
const values$1 = {};
|
|
776
|
-
for (const opt of field.options) values$1[opt.id] = selected.includes(opt.id) ? "done" : "todo";
|
|
777
|
-
return {
|
|
778
|
-
op: "set_checkboxes",
|
|
779
|
-
fieldId: field.id,
|
|
780
|
-
values: values$1
|
|
781
|
-
};
|
|
782
|
-
}
|
|
783
|
-
if (field.checkboxMode === "explicit") {
|
|
784
|
-
const values$1 = {};
|
|
785
|
-
for (const opt of field.options) {
|
|
786
|
-
const current = currentValues[opt.id];
|
|
787
|
-
const result = await p.select({
|
|
788
|
-
message: `${opt.label}`,
|
|
789
|
-
options: [
|
|
790
|
-
{
|
|
791
|
-
value: "yes",
|
|
792
|
-
label: "Yes"
|
|
793
|
-
},
|
|
794
|
-
{
|
|
795
|
-
value: "no",
|
|
796
|
-
label: "No"
|
|
797
|
-
},
|
|
798
|
-
{
|
|
799
|
-
value: "unfilled",
|
|
800
|
-
label: "Skip"
|
|
801
|
-
}
|
|
802
|
-
],
|
|
803
|
-
initialValue: current === "yes" || current === "no" ? current : "unfilled"
|
|
804
|
-
});
|
|
805
|
-
if (p.isCancel(result)) return null;
|
|
806
|
-
values$1[opt.id] = result;
|
|
807
|
-
}
|
|
808
|
-
return {
|
|
809
|
-
op: "set_checkboxes",
|
|
810
|
-
fieldId: field.id,
|
|
811
|
-
values: values$1
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
const values = {};
|
|
815
|
-
for (const opt of field.options) {
|
|
816
|
-
const current = currentValues[opt.id];
|
|
817
|
-
const result = await p.select({
|
|
818
|
-
message: `${opt.label}`,
|
|
819
|
-
options: [
|
|
820
|
-
{
|
|
821
|
-
value: "todo",
|
|
822
|
-
label: "To do"
|
|
823
|
-
},
|
|
824
|
-
{
|
|
825
|
-
value: "active",
|
|
826
|
-
label: "In progress"
|
|
827
|
-
},
|
|
828
|
-
{
|
|
829
|
-
value: "done",
|
|
830
|
-
label: "Done"
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
value: "incomplete",
|
|
834
|
-
label: "Incomplete"
|
|
835
|
-
},
|
|
836
|
-
{
|
|
837
|
-
value: "na",
|
|
838
|
-
label: "N/A"
|
|
839
|
-
}
|
|
840
|
-
],
|
|
841
|
-
initialValue: current ?? "todo"
|
|
842
|
-
});
|
|
843
|
-
if (p.isCancel(result)) return null;
|
|
844
|
-
values[opt.id] = result;
|
|
845
|
-
}
|
|
846
|
-
return {
|
|
847
|
-
op: "set_checkboxes",
|
|
848
|
-
fieldId: field.id,
|
|
849
|
-
values
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* Prompt user for a single field value based on field type.
|
|
854
|
-
* Returns a Patch to set the value, or null if skipped/cancelled.
|
|
855
|
-
*/
|
|
856
|
-
async function promptForField(ctx) {
|
|
857
|
-
if (ctx.description) p.note(ctx.description, pc.dim("Instructions"));
|
|
858
|
-
switch (ctx.field.kind) {
|
|
859
|
-
case "string": return promptForString(ctx);
|
|
860
|
-
case "number": return promptForNumber(ctx);
|
|
861
|
-
case "string_list": return promptForStringList(ctx);
|
|
862
|
-
case "single_select": return promptForSingleSelect(ctx);
|
|
863
|
-
case "multi_select": return promptForMultiSelect(ctx);
|
|
864
|
-
case "checkboxes": return promptForCheckboxes(ctx);
|
|
865
|
-
default: return null;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
/**
|
|
869
|
-
* Run an interactive fill session for a list of field issues.
|
|
870
|
-
* Returns patches for all filled fields.
|
|
871
|
-
*
|
|
872
|
-
* @param form - The parsed form
|
|
873
|
-
* @param issues - The issues indicating fields to fill
|
|
874
|
-
* @returns Array of patches to apply
|
|
875
|
-
*/
|
|
876
|
-
async function runInteractiveFill(form, issues) {
|
|
877
|
-
const fieldIssues = issues.filter((i) => i.scope === "field");
|
|
878
|
-
const seenFieldIds = /* @__PURE__ */ new Set();
|
|
879
|
-
const uniqueFieldIssues = fieldIssues.filter((issue) => {
|
|
880
|
-
if (seenFieldIds.has(issue.ref)) return false;
|
|
881
|
-
seenFieldIds.add(issue.ref);
|
|
882
|
-
return true;
|
|
883
|
-
});
|
|
884
|
-
if (uniqueFieldIssues.length === 0) {
|
|
885
|
-
p.note("No fields to fill for the selected role.", "Info");
|
|
886
|
-
return {
|
|
887
|
-
patches: [],
|
|
888
|
-
cancelled: false
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
const patches = [];
|
|
892
|
-
let index = 0;
|
|
893
|
-
for (const issue of uniqueFieldIssues) {
|
|
894
|
-
const field = getFieldById(form, issue.ref);
|
|
895
|
-
if (!field) continue;
|
|
896
|
-
index++;
|
|
897
|
-
const patch = await promptForField({
|
|
898
|
-
field,
|
|
899
|
-
currentValue: form.valuesByFieldId[field.id],
|
|
900
|
-
description: getFieldDescription(form, field.id),
|
|
901
|
-
index,
|
|
902
|
-
total: uniqueFieldIssues.length
|
|
903
|
-
});
|
|
904
|
-
if (patch === null && p.isCancel(patch)) {
|
|
905
|
-
const shouldContinue = await p.confirm({
|
|
906
|
-
message: "Cancel and discard changes?",
|
|
907
|
-
initialValue: false
|
|
908
|
-
});
|
|
909
|
-
if (p.isCancel(shouldContinue) || shouldContinue) return {
|
|
910
|
-
patches: [],
|
|
911
|
-
cancelled: true
|
|
912
|
-
};
|
|
913
|
-
index--;
|
|
914
|
-
continue;
|
|
915
|
-
}
|
|
916
|
-
if (patch) patches.push(patch);
|
|
917
|
-
}
|
|
918
|
-
return {
|
|
919
|
-
patches,
|
|
920
|
-
cancelled: false
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
/**
|
|
924
|
-
* Show intro message for interactive fill session.
|
|
925
|
-
*/
|
|
926
|
-
function showInteractiveIntro(formTitle, role, fieldCount) {
|
|
927
|
-
p.intro(pc.bgCyan(pc.black(" Markform Interactive Fill ")));
|
|
928
|
-
const lines = [
|
|
929
|
-
`${pc.bold("Form:")} ${formTitle}`,
|
|
930
|
-
`${pc.bold("Role:")} ${role}`,
|
|
931
|
-
`${pc.bold("Fields:")} ${fieldCount} to fill`
|
|
932
|
-
];
|
|
933
|
-
p.note(lines.join("\n"), "Session Info");
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Show outro message after interactive fill.
|
|
937
|
-
*/
|
|
938
|
-
function showInteractiveOutro(patchCount, outputPath, cancelled) {
|
|
939
|
-
if (cancelled) {
|
|
940
|
-
p.cancel("Interactive fill cancelled.");
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
if (patchCount === 0) {
|
|
944
|
-
p.outro(pc.yellow("No changes made."));
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
p.outro(`✓ ${patchCount} field(s) updated. Saved to ${formatPath(outputPath)}`);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
//#endregion
|
|
951
|
-
//#region src/cli/commands/examples.ts
|
|
952
|
-
/**
|
|
953
|
-
* Print non-interactive list of examples.
|
|
954
|
-
*/
|
|
955
|
-
function printExamplesList() {
|
|
956
|
-
console.log(pc.bold("Available examples:\n"));
|
|
957
|
-
for (const example of EXAMPLE_DEFINITIONS) {
|
|
958
|
-
console.log(` ${pc.cyan(example.id)}`);
|
|
959
|
-
console.log(` ${pc.bold(example.title)}`);
|
|
960
|
-
console.log(` ${pc.dim(example.description)}`);
|
|
961
|
-
console.log(` Default filename: ${example.filename}`);
|
|
962
|
-
console.log("");
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* Display API availability status at startup.
|
|
967
|
-
*/
|
|
968
|
-
function showApiStatus() {
|
|
969
|
-
console.log(pc.dim("API Status:"));
|
|
970
|
-
for (const [provider, _models] of Object.entries(SUGGESTED_LLMS)) {
|
|
971
|
-
const info = getProviderInfo(provider);
|
|
972
|
-
const hasKey = !!process.env[info.envVar];
|
|
973
|
-
const status = hasKey ? pc.green("✓") : pc.dim("○");
|
|
974
|
-
const envVar = hasKey ? pc.dim(info.envVar) : pc.yellow(info.envVar);
|
|
975
|
-
console.log(` ${status} ${provider} (${envVar})`);
|
|
976
|
-
}
|
|
977
|
-
console.log("");
|
|
978
|
-
}
|
|
979
|
-
/**
|
|
980
|
-
* Build model options for the select prompt.
|
|
981
|
-
*/
|
|
982
|
-
function buildModelOptions() {
|
|
983
|
-
const options = [];
|
|
984
|
-
for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
|
|
985
|
-
const info = getProviderInfo(provider);
|
|
986
|
-
const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : pc.dim("○");
|
|
987
|
-
for (const model of models.slice(0, 2)) options.push({
|
|
988
|
-
value: `${provider}/${model}`,
|
|
989
|
-
label: `${provider}/${model}`,
|
|
990
|
-
hint: `${keyStatus} ${info.envVar}`
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
options.push({
|
|
994
|
-
value: "custom",
|
|
995
|
-
label: "Enter custom model ID...",
|
|
996
|
-
hint: "provider/model-id format"
|
|
997
|
-
});
|
|
998
|
-
return options;
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* Prompt user to select a model for agent fill.
|
|
1002
|
-
*/
|
|
1003
|
-
async function promptForModel() {
|
|
1004
|
-
const modelOptions = buildModelOptions();
|
|
1005
|
-
const selection = await p.select({
|
|
1006
|
-
message: "Select LLM model:",
|
|
1007
|
-
options: modelOptions
|
|
1008
|
-
});
|
|
1009
|
-
if (p.isCancel(selection)) return null;
|
|
1010
|
-
if (selection === "custom") {
|
|
1011
|
-
const customModel = await p.text({
|
|
1012
|
-
message: "Model ID (provider/model-id):",
|
|
1013
|
-
placeholder: "anthropic/claude-sonnet-4-20250514",
|
|
1014
|
-
validate: (value) => {
|
|
1015
|
-
if (!value.includes("/")) return "Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)";
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1018
|
-
if (p.isCancel(customModel)) return null;
|
|
1019
|
-
return customModel;
|
|
1020
|
-
}
|
|
1021
|
-
return selection;
|
|
1022
|
-
}
|
|
1023
|
-
/**
|
|
1024
|
-
* Run the agent fill workflow.
|
|
1025
|
-
*/
|
|
1026
|
-
async function runAgentFill(form, modelId, _outputPath) {
|
|
1027
|
-
const spinner = p.spinner();
|
|
1028
|
-
try {
|
|
1029
|
-
spinner.start(`Resolving model: ${modelId}`);
|
|
1030
|
-
const { model } = await resolveModel(modelId);
|
|
1031
|
-
spinner.stop(`Model resolved: ${modelId}`);
|
|
1032
|
-
const harnessConfig = {
|
|
1033
|
-
maxTurns: DEFAULT_MAX_TURNS,
|
|
1034
|
-
maxPatchesPerTurn: DEFAULT_MAX_PATCHES_PER_TURN,
|
|
1035
|
-
maxIssues: DEFAULT_MAX_ISSUES,
|
|
1036
|
-
targetRoles: [AGENT_ROLE],
|
|
1037
|
-
fillMode: "continue"
|
|
1038
|
-
};
|
|
1039
|
-
const harness = createHarness(form, harnessConfig);
|
|
1040
|
-
const agent = createLiveAgent({
|
|
1041
|
-
model,
|
|
1042
|
-
targetRole: AGENT_ROLE
|
|
1043
|
-
});
|
|
1044
|
-
console.log("");
|
|
1045
|
-
p.log.step(pc.bold("Agent fill in progress..."));
|
|
1046
|
-
let stepResult = harness.step();
|
|
1047
|
-
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
1048
|
-
console.log(pc.dim(` Turn ${stepResult.turnNumber}: ${stepResult.issues.length} issue(s) to address`));
|
|
1049
|
-
const patches = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1050
|
-
for (const patch of patches) {
|
|
1051
|
-
const fieldId = patch.fieldId;
|
|
1052
|
-
console.log(pc.dim(` → ${patch.op} ${fieldId}`));
|
|
1053
|
-
}
|
|
1054
|
-
stepResult = harness.apply(patches, stepResult.issues);
|
|
1055
|
-
console.log(pc.dim(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining`));
|
|
1056
|
-
if (!stepResult.isComplete) stepResult = harness.step();
|
|
1057
|
-
}
|
|
1058
|
-
if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
|
|
1059
|
-
else p.log.warn(pc.yellow(`Max turns reached (${harnessConfig.maxTurns})`));
|
|
1060
|
-
Object.assign(form, harness.getForm());
|
|
1061
|
-
return {
|
|
1062
|
-
success: stepResult.isComplete,
|
|
1063
|
-
turnCount: harness.getTurnNumber()
|
|
1064
|
-
};
|
|
1065
|
-
} catch (error) {
|
|
1066
|
-
spinner.stop(pc.red("Agent fill failed"));
|
|
1067
|
-
throw error;
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Run the interactive example scaffolding and filling flow.
|
|
1072
|
-
*/
|
|
1073
|
-
async function runInteractiveFlow(preselectedId) {
|
|
1074
|
-
const startTime = Date.now();
|
|
1075
|
-
p.intro(pc.bgCyan(pc.black(" markform examples ")));
|
|
1076
|
-
showApiStatus();
|
|
1077
|
-
let selectedId = preselectedId;
|
|
1078
|
-
if (!selectedId) {
|
|
1079
|
-
const selection = await p.select({
|
|
1080
|
-
message: "Select an example form to scaffold:",
|
|
1081
|
-
options: EXAMPLE_DEFINITIONS.map((example$1) => ({
|
|
1082
|
-
value: example$1.id,
|
|
1083
|
-
label: example$1.title,
|
|
1084
|
-
hint: example$1.description
|
|
1085
|
-
}))
|
|
1086
|
-
});
|
|
1087
|
-
if (p.isCancel(selection)) {
|
|
1088
|
-
p.cancel("Cancelled.");
|
|
1089
|
-
process.exit(0);
|
|
1090
|
-
}
|
|
1091
|
-
selectedId = selection;
|
|
1092
|
-
}
|
|
1093
|
-
const example = getExampleById(selectedId);
|
|
1094
|
-
if (!example) {
|
|
1095
|
-
p.cancel(`Unknown example: ${selectedId}`);
|
|
1096
|
-
process.exit(1);
|
|
1097
|
-
}
|
|
1098
|
-
const defaultFilename = generateVersionedPath(example.filename);
|
|
1099
|
-
const filenameResult = await p.text({
|
|
1100
|
-
message: "Output filename:",
|
|
1101
|
-
initialValue: defaultFilename,
|
|
1102
|
-
validate: (value) => {
|
|
1103
|
-
if (!value.trim()) return "Filename is required";
|
|
1104
|
-
if (!value.endsWith(".form.md") && !value.endsWith(".md")) return "Filename should end with .form.md or .md";
|
|
1105
|
-
}
|
|
1106
|
-
});
|
|
1107
|
-
if (p.isCancel(filenameResult)) {
|
|
1108
|
-
p.cancel("Cancelled.");
|
|
1109
|
-
process.exit(0);
|
|
1110
|
-
}
|
|
1111
|
-
const filename = filenameResult;
|
|
1112
|
-
const outputPath = join(process.cwd(), filename);
|
|
1113
|
-
if (existsSync(outputPath)) {
|
|
1114
|
-
const overwrite = await p.confirm({
|
|
1115
|
-
message: `${filename} already exists. Overwrite?`,
|
|
1116
|
-
initialValue: false
|
|
1117
|
-
});
|
|
1118
|
-
if (p.isCancel(overwrite) || !overwrite) {
|
|
1119
|
-
p.cancel("Cancelled.");
|
|
1120
|
-
process.exit(0);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
let content;
|
|
1124
|
-
try {
|
|
1125
|
-
content = loadExampleContent(selectedId);
|
|
1126
|
-
writeFileSync(outputPath, content, "utf-8");
|
|
1127
|
-
} catch (error) {
|
|
1128
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1129
|
-
p.cancel(`Failed to write file: ${message}`);
|
|
1130
|
-
process.exit(1);
|
|
1131
|
-
}
|
|
1132
|
-
p.log.success(`Created ${formatPath(outputPath)}`);
|
|
1133
|
-
const form = parseForm(content);
|
|
1134
|
-
const targetRoles = [USER_ROLE];
|
|
1135
|
-
const inspectResult = inspect(form, { targetRoles });
|
|
1136
|
-
const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
|
|
1137
|
-
const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
|
|
1138
|
-
if (uniqueFieldIds.size === 0) {
|
|
1139
|
-
p.log.info("No user-role fields to fill in this example.");
|
|
1140
|
-
if (inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field").length === 0) {
|
|
1141
|
-
logTiming({
|
|
1142
|
-
verbose: false,
|
|
1143
|
-
format: "console",
|
|
1144
|
-
dryRun: false,
|
|
1145
|
-
quiet: false
|
|
1146
|
-
}, "Total time", Date.now() - startTime);
|
|
1147
|
-
p.outro(pc.dim("Form scaffolded with no fields to fill."));
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1150
|
-
} else {
|
|
1151
|
-
showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
|
|
1152
|
-
const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
|
|
1153
|
-
if (cancelled) {
|
|
1154
|
-
showInteractiveOutro(0, "", true);
|
|
1155
|
-
process.exit(1);
|
|
1156
|
-
}
|
|
1157
|
-
if (patches.length > 0) applyPatches(form, patches);
|
|
1158
|
-
const { formPath, rawPath, yamlPath } = exportMultiFormat(form, outputPath);
|
|
1159
|
-
showInteractiveOutro(patches.length, outputPath, false);
|
|
1160
|
-
console.log("");
|
|
1161
|
-
p.log.success("Outputs:");
|
|
1162
|
-
console.log(` ${formatPath(formPath)} ${pc.dim("(markform)")}`);
|
|
1163
|
-
console.log(` ${formatPath(rawPath)} ${pc.dim("(plain markdown)")}`);
|
|
1164
|
-
console.log(` ${formatPath(yamlPath)} ${pc.dim("(values as YAML)")}`);
|
|
1165
|
-
logTiming({
|
|
1166
|
-
verbose: false,
|
|
1167
|
-
format: "console",
|
|
1168
|
-
dryRun: false,
|
|
1169
|
-
quiet: false
|
|
1170
|
-
}, "Fill time", Date.now() - startTime);
|
|
1171
|
-
}
|
|
1172
|
-
const agentFieldIssues = inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field");
|
|
1173
|
-
if (agentFieldIssues.length > 0) {
|
|
1174
|
-
console.log("");
|
|
1175
|
-
p.log.info(`This form has ${agentFieldIssues.length} agent-role field(s) remaining.`);
|
|
1176
|
-
const runAgent = await p.confirm({
|
|
1177
|
-
message: "Run agent fill now?",
|
|
1178
|
-
initialValue: true
|
|
1179
|
-
});
|
|
1180
|
-
if (p.isCancel(runAgent) || !runAgent) {
|
|
1181
|
-
console.log("");
|
|
1182
|
-
console.log(pc.dim("You can run agent fill later with:"));
|
|
1183
|
-
console.log(pc.dim(` markform fill ${formatPath(outputPath)} --agent=live --model=<provider/model>`));
|
|
1184
|
-
p.outro(pc.dim("Happy form filling!"));
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1187
|
-
const modelId = await promptForModel();
|
|
1188
|
-
if (!modelId) {
|
|
1189
|
-
p.cancel("Cancelled.");
|
|
1190
|
-
process.exit(0);
|
|
1191
|
-
}
|
|
1192
|
-
const agentDefaultFilename = generateVersionedPath(outputPath);
|
|
1193
|
-
const agentFilenameResult = await p.text({
|
|
1194
|
-
message: "Agent output filename:",
|
|
1195
|
-
initialValue: basename(agentDefaultFilename),
|
|
1196
|
-
validate: (value) => {
|
|
1197
|
-
if (!value.trim()) return "Filename is required";
|
|
1198
|
-
}
|
|
1199
|
-
});
|
|
1200
|
-
if (p.isCancel(agentFilenameResult)) {
|
|
1201
|
-
p.cancel("Cancelled.");
|
|
1202
|
-
process.exit(0);
|
|
1203
|
-
}
|
|
1204
|
-
const agentOutputPath = join(process.cwd(), agentFilenameResult);
|
|
1205
|
-
const agentStartTime = Date.now();
|
|
1206
|
-
try {
|
|
1207
|
-
const { success, turnCount: _turnCount } = await runAgentFill(form, modelId, agentOutputPath);
|
|
1208
|
-
logTiming({
|
|
1209
|
-
verbose: false,
|
|
1210
|
-
format: "console",
|
|
1211
|
-
dryRun: false,
|
|
1212
|
-
quiet: false
|
|
1213
|
-
}, "Agent fill time", Date.now() - agentStartTime);
|
|
1214
|
-
const { formPath, rawPath, yamlPath } = exportMultiFormat(form, agentOutputPath);
|
|
1215
|
-
console.log("");
|
|
1216
|
-
p.log.success("Agent fill complete. Outputs:");
|
|
1217
|
-
console.log(` ${formatPath(formPath)} ${pc.dim("(markform)")}`);
|
|
1218
|
-
console.log(` ${formatPath(rawPath)} ${pc.dim("(plain markdown)")}`);
|
|
1219
|
-
console.log(` ${formatPath(yamlPath)} ${pc.dim("(values as YAML)")}`);
|
|
1220
|
-
if (!success) p.log.warn("Agent did not complete all fields. You may need to run it again.");
|
|
1221
|
-
} catch (error) {
|
|
1222
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1223
|
-
p.log.error(`Agent fill failed: ${message}`);
|
|
1224
|
-
console.log("");
|
|
1225
|
-
console.log(pc.dim("You can try again with:"));
|
|
1226
|
-
console.log(pc.dim(` markform fill ${formatPath(outputPath)} --agent=live --model=${modelId}`));
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
p.outro(pc.dim("Happy form filling!"));
|
|
1230
|
-
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Register the examples command.
|
|
1233
|
-
*/
|
|
1234
|
-
function registerExamplesCommand(program) {
|
|
1235
|
-
program.command("examples").description("Scaffold an example form and fill it interactively").option("--list", "List available examples without interactive selection").option("--name <example>", "Select example by ID (still prompts for filename)").action(async (options, cmd) => {
|
|
1236
|
-
getCommandContext(cmd);
|
|
1237
|
-
try {
|
|
1238
|
-
if (options.list) {
|
|
1239
|
-
printExamplesList();
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
if (options.name) {
|
|
1243
|
-
if (!getExampleById(options.name)) {
|
|
1244
|
-
logError(`Unknown example: ${options.name}`);
|
|
1245
|
-
console.log("\nAvailable examples:");
|
|
1246
|
-
for (const ex of EXAMPLE_DEFINITIONS) console.log(` ${ex.id}`);
|
|
1247
|
-
process.exit(1);
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
await runInteractiveFlow(options.name);
|
|
1251
|
-
} catch (error) {
|
|
1252
|
-
logError(error instanceof Error ? error.message : String(error));
|
|
1253
|
-
process.exit(1);
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
//#endregion
|
|
1259
|
-
//#region src/cli/commands/export.ts
|
|
1260
|
-
/**
|
|
1261
|
-
* Register the export command.
|
|
1262
|
-
*/
|
|
1263
|
-
function registerExportCommand(program) {
|
|
1264
|
-
program.command("export <file>").description("Export form as markform (default), markdown (readable), or json/yaml for structured data").option("--compact", "Output compact JSON (no formatting, only for JSON format)").action(async (file, options, cmd) => {
|
|
1265
|
-
const ctx = getCommandContext(cmd);
|
|
1266
|
-
let format = "markform";
|
|
1267
|
-
if (ctx.format === "json") format = "json";
|
|
1268
|
-
else if (ctx.format === "yaml") format = "yaml";
|
|
1269
|
-
else if (ctx.format === "markdown") format = "markdown";
|
|
1270
|
-
else if (ctx.format === "markform") format = "markform";
|
|
1271
|
-
try {
|
|
1272
|
-
logVerbose(ctx, `Reading file: ${file}`);
|
|
1273
|
-
const content = await readFile(file);
|
|
1274
|
-
logVerbose(ctx, "Parsing form...");
|
|
1275
|
-
const form = parseForm(content);
|
|
1276
|
-
if (format === "markform") {
|
|
1277
|
-
console.log(serialize(form));
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
if (format === "markdown") {
|
|
1281
|
-
console.log(serializeRawMarkdown(form));
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
const output = {
|
|
1285
|
-
schema: {
|
|
1286
|
-
id: form.schema.id,
|
|
1287
|
-
title: form.schema.title,
|
|
1288
|
-
groups: form.schema.groups.map((group) => ({
|
|
1289
|
-
id: group.id,
|
|
1290
|
-
title: group.title,
|
|
1291
|
-
children: group.children.map((field) => ({
|
|
1292
|
-
id: field.id,
|
|
1293
|
-
kind: field.kind,
|
|
1294
|
-
label: field.label,
|
|
1295
|
-
required: field.required,
|
|
1296
|
-
...field.kind === "single_select" || field.kind === "multi_select" || field.kind === "checkboxes" ? { options: field.options.map((opt) => ({
|
|
1297
|
-
id: opt.id,
|
|
1298
|
-
label: opt.label
|
|
1299
|
-
})) } : {}
|
|
1300
|
-
}))
|
|
1301
|
-
}))
|
|
1302
|
-
},
|
|
1303
|
-
values: form.valuesByFieldId,
|
|
1304
|
-
markdown: serialize(form)
|
|
1305
|
-
};
|
|
1306
|
-
if (format === "json") if (options.compact) console.log(JSON.stringify(output));
|
|
1307
|
-
else console.log(JSON.stringify(output, null, 2));
|
|
1308
|
-
else console.log(YAML.stringify(output));
|
|
1309
|
-
} catch (error) {
|
|
1310
|
-
logError(error instanceof Error ? error.message : String(error));
|
|
1311
|
-
process.exit(1);
|
|
1312
|
-
}
|
|
1313
|
-
});
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
//#endregion
|
|
1317
|
-
//#region src/cli/commands/fill.ts
|
|
1318
|
-
/** Supported agent types */
|
|
1319
|
-
const AGENT_TYPES = ["mock", "live"];
|
|
1320
|
-
/**
|
|
1321
|
-
* Format a patch value for display.
|
|
1322
|
-
*/
|
|
1323
|
-
function formatPatchValue(patch) {
|
|
1324
|
-
switch (patch.op) {
|
|
1325
|
-
case "set_string": return patch.value ? `"${patch.value}"` : "(empty)";
|
|
1326
|
-
case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
1327
|
-
case "set_string_list": return patch.items.length > 0 ? `[${patch.items.join(", ")}]` : "(empty)";
|
|
1328
|
-
case "set_single_select": return patch.selected ?? "(none)";
|
|
1329
|
-
case "set_multi_select": return patch.selected.length > 0 ? `[${patch.selected.join(", ")}]` : "(none)";
|
|
1330
|
-
case "set_checkboxes": return Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", ");
|
|
1331
|
-
case "clear_field": return "(cleared)";
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* Format session transcript for console output.
|
|
1336
|
-
*/
|
|
1337
|
-
function formatConsoleSession(transcript, useColors) {
|
|
1338
|
-
const lines = [];
|
|
1339
|
-
const bold = useColors ? pc.bold : (s) => s;
|
|
1340
|
-
const dim = useColors ? pc.dim : (s) => s;
|
|
1341
|
-
const cyan = useColors ? pc.cyan : (s) => s;
|
|
1342
|
-
const green = useColors ? pc.green : (s) => s;
|
|
1343
|
-
const yellow = useColors ? pc.yellow : (s) => s;
|
|
1344
|
-
lines.push(bold(cyan("Session Transcript")));
|
|
1345
|
-
lines.push("");
|
|
1346
|
-
lines.push(`${bold("Form:")} ${transcript.form.path}`);
|
|
1347
|
-
lines.push(`${bold("Mode:")} ${transcript.mode}`);
|
|
1348
|
-
lines.push(`${bold("Version:")} ${transcript.sessionVersion}`);
|
|
1349
|
-
lines.push("");
|
|
1350
|
-
lines.push(bold("Harness Config:"));
|
|
1351
|
-
lines.push(` Max turns: ${transcript.harness.maxTurns}`);
|
|
1352
|
-
lines.push(` Max patches/turn: ${transcript.harness.maxPatchesPerTurn}`);
|
|
1353
|
-
lines.push(` Max issues: ${transcript.harness.maxIssues}`);
|
|
1354
|
-
lines.push("");
|
|
1355
|
-
lines.push(bold(`Turns (${transcript.turns.length}):`));
|
|
1356
|
-
for (const turn of transcript.turns) {
|
|
1357
|
-
const issueCount = turn.inspect.issues.length;
|
|
1358
|
-
const patchCount = turn.apply.patches.length;
|
|
1359
|
-
const afterIssues = turn.after.requiredIssueCount;
|
|
1360
|
-
lines.push(` Turn ${turn.turn}: ${dim(`${issueCount} issues`)} → ${yellow(`${patchCount} patches`)} → ${afterIssues === 0 ? green("0 remaining") : dim(`${afterIssues} remaining`)}`);
|
|
1361
|
-
}
|
|
1362
|
-
lines.push("");
|
|
1363
|
-
const expectText = transcript.final.expectComplete ? green("✓ complete") : yellow("○ incomplete");
|
|
1364
|
-
lines.push(`${bold("Expected:")} ${expectText}`);
|
|
1365
|
-
lines.push(`${bold("Completed form:")} ${transcript.final.expectedCompletedForm}`);
|
|
1366
|
-
return lines.join("\n");
|
|
1367
|
-
}
|
|
1368
|
-
/**
|
|
1369
|
-
* Register the fill command.
|
|
1370
|
-
*/
|
|
1371
|
-
function registerFillCommand(program) {
|
|
1372
|
-
program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--agent <type>", `Agent type: ${AGENT_TYPES.join(", ")} (default: live)`, "live").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-4o)").option("--mock-source <file>", "Path to completed form for mock agent").option("--record <file>", "Record session transcript to file").option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_MAX_PATCHES_PER_TURN})`, String(DEFAULT_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues shown per turn (default: ${DEFAULT_MAX_ISSUES})`, String(DEFAULT_MAX_ISSUES)).option("--max-fields <n>", "Maximum unique fields per turn (applied before --max-issues)").option("--max-groups <n>", "Maximum unique groups per turn (applied before --max-issues)").option("--roles <roles>", "Target roles to fill (comma-separated, or '*' for all; default: 'agent' in agent mode, 'user' in --interactive mode)").option("--mode <mode>", "Fill mode: continue (skip filled fields) or overwrite (re-fill; default: continue)").option("-o, --output <file>", "Write final form to file").option("--prompt <file>", "Path to custom system prompt file (appends to default)").option("--instructions <text>", "Inline system prompt (appends to default; takes precedence over --prompt)").option("-i, --interactive", "Interactive mode: prompt user for field values (defaults to user role)").action(async (file, options, cmd) => {
|
|
1373
|
-
const ctx = getCommandContext(cmd);
|
|
1374
|
-
const filePath = resolve(file);
|
|
1375
|
-
try {
|
|
1376
|
-
const startTime = Date.now();
|
|
1377
|
-
let targetRoles;
|
|
1378
|
-
if (options.roles) try {
|
|
1379
|
-
targetRoles = parseRolesFlag(options.roles);
|
|
1380
|
-
} catch (error) {
|
|
1381
|
-
logError(`Invalid --roles: ${error instanceof Error ? error.message : String(error)}`);
|
|
1382
|
-
process.exit(1);
|
|
1383
|
-
}
|
|
1384
|
-
else targetRoles = options.interactive ? [USER_ROLE] : [AGENT_ROLE];
|
|
1385
|
-
let fillMode = "continue";
|
|
1386
|
-
if (options.mode) {
|
|
1387
|
-
if (options.mode !== "continue" && options.mode !== "overwrite") {
|
|
1388
|
-
logError(`Invalid --mode: ${options.mode}. Valid modes: continue, overwrite`);
|
|
1389
|
-
process.exit(1);
|
|
1390
|
-
}
|
|
1391
|
-
fillMode = options.mode;
|
|
1392
|
-
}
|
|
1393
|
-
logVerbose(ctx, `Reading form: ${filePath}`);
|
|
1394
|
-
const formContent = await readFile(filePath);
|
|
1395
|
-
logVerbose(ctx, "Parsing form...");
|
|
1396
|
-
const form = parseForm(formContent);
|
|
1397
|
-
if (options.interactive) {
|
|
1398
|
-
if (options.agent && options.agent !== "live") {
|
|
1399
|
-
logError("--interactive cannot be used with --agent");
|
|
1400
|
-
process.exit(1);
|
|
1401
|
-
}
|
|
1402
|
-
if (options.model) {
|
|
1403
|
-
logError("--interactive cannot be used with --model");
|
|
1404
|
-
process.exit(1);
|
|
1405
|
-
}
|
|
1406
|
-
if (options.mockSource) {
|
|
1407
|
-
logError("--interactive cannot be used with --mock-source");
|
|
1408
|
-
process.exit(1);
|
|
1409
|
-
}
|
|
1410
|
-
const inspectResult = inspect(form, { targetRoles });
|
|
1411
|
-
const formTitle = form.schema.title ?? form.schema.id;
|
|
1412
|
-
const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
|
|
1413
|
-
const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
|
|
1414
|
-
showInteractiveIntro(formTitle, targetRoles.join(", "), uniqueFieldIds.size);
|
|
1415
|
-
const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
|
|
1416
|
-
if (cancelled) {
|
|
1417
|
-
showInteractiveOutro(0, "", true);
|
|
1418
|
-
process.exit(1);
|
|
1419
|
-
}
|
|
1420
|
-
if (patches.length > 0) applyPatches(form, patches);
|
|
1421
|
-
const durationMs$1 = Date.now() - startTime;
|
|
1422
|
-
const outputPath$1 = options.output ? resolve(options.output) : generateVersionedPath(filePath);
|
|
1423
|
-
const formMarkdown$1 = serialize(form);
|
|
1424
|
-
if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
|
|
1425
|
-
else await writeFile(outputPath$1, formMarkdown$1);
|
|
1426
|
-
showInteractiveOutro(patches.length, outputPath$1, false);
|
|
1427
|
-
logTiming(ctx, "Fill time", durationMs$1);
|
|
1428
|
-
if (patches.length > 0) {
|
|
1429
|
-
console.log("");
|
|
1430
|
-
console.log(pc.dim("Next step: fill remaining fields with agent"));
|
|
1431
|
-
console.log(pc.dim(` markform fill ${outputPath$1} --agent=live --model=<provider/model>`));
|
|
1432
|
-
}
|
|
1433
|
-
process.exit(0);
|
|
1434
|
-
}
|
|
1435
|
-
const agentType = options.agent ?? "live";
|
|
1436
|
-
if (!AGENT_TYPES.includes(agentType)) {
|
|
1437
|
-
logError(`Invalid agent type '${options.agent}'. Valid types: ${AGENT_TYPES.join(", ")}`);
|
|
1438
|
-
process.exit(1);
|
|
1439
|
-
}
|
|
1440
|
-
if (agentType === "mock" && !options.mockSource) {
|
|
1441
|
-
logError("--agent=mock requires --mock-source <file>");
|
|
1442
|
-
process.exit(1);
|
|
1443
|
-
}
|
|
1444
|
-
if (agentType === "live" && !options.model) {
|
|
1445
|
-
logError("--agent=live requires --model <provider/model-id>");
|
|
1446
|
-
console.log("");
|
|
1447
|
-
console.log(formatSuggestedLlms());
|
|
1448
|
-
process.exit(1);
|
|
1449
|
-
}
|
|
1450
|
-
if (targetRoles.includes("*")) logWarn(ctx, "Warning: Filling all roles including user-designated fields");
|
|
1451
|
-
const harnessConfig = {
|
|
1452
|
-
maxTurns: parseInt(options.maxTurns ?? String(DEFAULT_MAX_TURNS), 10),
|
|
1453
|
-
maxPatchesPerTurn: parseInt(options.maxPatches ?? String(DEFAULT_MAX_PATCHES_PER_TURN), 10),
|
|
1454
|
-
maxIssues: parseInt(options.maxIssues ?? String(DEFAULT_MAX_ISSUES), 10),
|
|
1455
|
-
...options.maxFields && { maxFieldsPerTurn: parseInt(options.maxFields, 10) },
|
|
1456
|
-
...options.maxGroups && { maxGroupsPerTurn: parseInt(options.maxGroups, 10) },
|
|
1457
|
-
targetRoles,
|
|
1458
|
-
fillMode
|
|
1459
|
-
};
|
|
1460
|
-
const harness = createHarness(form, harnessConfig);
|
|
1461
|
-
let agent;
|
|
1462
|
-
let mockPath;
|
|
1463
|
-
if (agentType === "mock") {
|
|
1464
|
-
mockPath = resolve(options.mockSource);
|
|
1465
|
-
logVerbose(ctx, `Reading mock source: ${mockPath}`);
|
|
1466
|
-
agent = createMockAgent(parseForm(await readFile(mockPath)));
|
|
1467
|
-
} else {
|
|
1468
|
-
const modelId = options.model;
|
|
1469
|
-
logVerbose(ctx, `Resolving model: ${modelId}`);
|
|
1470
|
-
const { model } = await resolveModel(modelId);
|
|
1471
|
-
let systemPrompt;
|
|
1472
|
-
if (options.instructions) {
|
|
1473
|
-
systemPrompt = options.instructions;
|
|
1474
|
-
logVerbose(ctx, "Using inline system prompt from --instructions");
|
|
1475
|
-
} else if (options.prompt) {
|
|
1476
|
-
const promptPath = resolve(options.prompt);
|
|
1477
|
-
logVerbose(ctx, `Reading system prompt from: ${promptPath}`);
|
|
1478
|
-
systemPrompt = await readFile(promptPath);
|
|
1479
|
-
}
|
|
1480
|
-
const primaryRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0];
|
|
1481
|
-
agent = createLiveAgent({
|
|
1482
|
-
model,
|
|
1483
|
-
systemPromptAddition: systemPrompt,
|
|
1484
|
-
targetRole: primaryRole
|
|
1485
|
-
});
|
|
1486
|
-
logVerbose(ctx, `Using live agent with model: ${modelId}`);
|
|
1487
|
-
}
|
|
1488
|
-
logInfo(ctx, pc.cyan(`Filling form: ${filePath}`));
|
|
1489
|
-
logInfo(ctx, `Agent: ${agentType}${options.model ? ` (${options.model})` : ""}`);
|
|
1490
|
-
logVerbose(ctx, `Max turns: ${harnessConfig.maxTurns}`);
|
|
1491
|
-
logVerbose(ctx, `Max patches per turn: ${harnessConfig.maxPatchesPerTurn}`);
|
|
1492
|
-
logVerbose(ctx, `Max issues per step: ${harnessConfig.maxIssues}`);
|
|
1493
|
-
logVerbose(ctx, `Target roles: ${targetRoles.includes("*") ? "*" : targetRoles.join(", ")}`);
|
|
1494
|
-
logVerbose(ctx, `Fill mode: ${fillMode}`);
|
|
1495
|
-
let stepResult = harness.step();
|
|
1496
|
-
logInfo(ctx, `Turn ${pc.bold(String(stepResult.turnNumber))}: ${pc.yellow(String(stepResult.issues.length))} issues`);
|
|
1497
|
-
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
1498
|
-
const patches = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1499
|
-
logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches`);
|
|
1500
|
-
for (const patch of patches) {
|
|
1501
|
-
const value = formatPatchValue(patch);
|
|
1502
|
-
logVerbose(ctx, ` ${pc.cyan(patch.fieldId)} ${pc.dim("=")} ${pc.green(value)}`);
|
|
1503
|
-
}
|
|
1504
|
-
stepResult = harness.apply(patches, stepResult.issues);
|
|
1505
|
-
if (stepResult.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
|
|
1506
|
-
else {
|
|
1507
|
-
stepResult = harness.step();
|
|
1508
|
-
logInfo(ctx, `Turn ${pc.bold(String(stepResult.turnNumber))}: ${pc.yellow(String(stepResult.issues.length))} issues`);
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
const durationMs = Date.now() - startTime;
|
|
1512
|
-
if (stepResult.isComplete) logSuccess(ctx, `Form completed in ${harness.getTurnNumber()} turn(s)`);
|
|
1513
|
-
else if (harness.hasReachedMaxTurns()) logWarn(ctx, `Max turns reached (${harnessConfig.maxTurns})`);
|
|
1514
|
-
logTiming(ctx, "Fill time", durationMs);
|
|
1515
|
-
const outputPath = options.output ? resolve(options.output) : generateVersionedPath(filePath);
|
|
1516
|
-
const formMarkdown = serialize(harness.getForm());
|
|
1517
|
-
if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
|
|
1518
|
-
else {
|
|
1519
|
-
await writeFile(outputPath, formMarkdown);
|
|
1520
|
-
logSuccess(ctx, `Form written to: ${outputPath}`);
|
|
1521
|
-
}
|
|
1522
|
-
const transcript = buildSessionTranscript(filePath, agentType, mockPath, options.model, harnessConfig, harness.getTurns(), stepResult.isComplete, outputPath);
|
|
1523
|
-
if (options.record) {
|
|
1524
|
-
const recordPath = resolve(options.record);
|
|
1525
|
-
const yaml = serializeSession(transcript);
|
|
1526
|
-
if (ctx.dryRun) {
|
|
1527
|
-
logInfo(ctx, `[DRY RUN] Would write session to: ${recordPath}`);
|
|
1528
|
-
console.log(yaml);
|
|
1529
|
-
} else {
|
|
1530
|
-
await writeFile(recordPath, yaml);
|
|
1531
|
-
logSuccess(ctx, `Session recorded to: ${recordPath}`);
|
|
1532
|
-
}
|
|
1533
|
-
} else {
|
|
1534
|
-
const output = formatOutput(ctx, transcript, (data, useColors) => formatConsoleSession(data, useColors));
|
|
1535
|
-
console.log(output);
|
|
1536
|
-
}
|
|
1537
|
-
process.exit(stepResult.isComplete ? 0 : 1);
|
|
1538
|
-
} catch (error) {
|
|
1539
|
-
logError(error instanceof Error ? error.message : String(error));
|
|
1540
|
-
process.exit(1);
|
|
1541
|
-
}
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
/**
|
|
1545
|
-
* Build a session transcript from harness execution.
|
|
1546
|
-
*/
|
|
1547
|
-
function buildSessionTranscript(formPath, agentType, mockPath, modelId, harnessConfig, turns, expectComplete, outputPath) {
|
|
1548
|
-
const transcript = {
|
|
1549
|
-
sessionVersion: "0.1.0",
|
|
1550
|
-
mode: agentType,
|
|
1551
|
-
form: { path: formPath },
|
|
1552
|
-
harness: harnessConfig,
|
|
1553
|
-
turns,
|
|
1554
|
-
final: {
|
|
1555
|
-
expectComplete,
|
|
1556
|
-
expectedCompletedForm: agentType === "mock" ? mockPath ?? outputPath : outputPath
|
|
1557
|
-
}
|
|
1558
|
-
};
|
|
1559
|
-
if (agentType === "mock" && mockPath) transcript.mock = { completedMock: mockPath };
|
|
1560
|
-
else if (agentType === "live" && modelId) transcript.live = { modelId };
|
|
1561
|
-
return transcript;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
//#endregion
|
|
1565
|
-
//#region src/cli/commands/inspect.ts
|
|
1566
|
-
/**
|
|
1567
|
-
* Format state badge for console output.
|
|
1568
|
-
*/
|
|
1569
|
-
function formatState$1(state, useColors) {
|
|
1570
|
-
const [text, colorFn] = {
|
|
1571
|
-
complete: ["✓ complete", pc.green],
|
|
1572
|
-
incomplete: ["○ incomplete", pc.yellow],
|
|
1573
|
-
empty: ["◌ empty", pc.dim],
|
|
1574
|
-
invalid: ["✗ invalid", pc.red]
|
|
1575
|
-
}[state] ?? [state, (s) => s];
|
|
1576
|
-
return useColors ? colorFn(text) : text;
|
|
1577
|
-
}
|
|
1578
|
-
/**
|
|
1579
|
-
* Format priority badge for console output.
|
|
1580
|
-
*
|
|
1581
|
-
* Priority tiers and colors:
|
|
1582
|
-
* - P1: bold red (critical)
|
|
1583
|
-
* - P2: yellow (high)
|
|
1584
|
-
* - P3: cyan (medium)
|
|
1585
|
-
* - P4: blue (low)
|
|
1586
|
-
* - P5: dim/gray (minimal)
|
|
1587
|
-
*/
|
|
1588
|
-
function formatPriority$1(priority, useColors) {
|
|
1589
|
-
const label = `P${priority}`;
|
|
1590
|
-
if (!useColors) return label;
|
|
1591
|
-
switch (priority) {
|
|
1592
|
-
case 1: return pc.red(pc.bold(label));
|
|
1593
|
-
case 2: return pc.yellow(label);
|
|
1594
|
-
case 3: return pc.cyan(label);
|
|
1595
|
-
case 4: return pc.blue(label);
|
|
1596
|
-
case 5:
|
|
1597
|
-
default: return pc.dim(label);
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
/**
|
|
1601
|
-
* Format severity badge for console output.
|
|
1602
|
-
*/
|
|
1603
|
-
function formatSeverity$1(severity, useColors) {
|
|
1604
|
-
if (!useColors) return severity;
|
|
1605
|
-
return severity === "required" ? pc.red(severity) : pc.yellow(severity);
|
|
1606
|
-
}
|
|
1607
|
-
/**
|
|
1608
|
-
* Format a field value for console display.
|
|
1609
|
-
*/
|
|
1610
|
-
function formatFieldValue(value, useColors) {
|
|
1611
|
-
const dim = useColors ? pc.dim : (s) => s;
|
|
1612
|
-
const green = useColors ? pc.green : (s) => s;
|
|
1613
|
-
if (!value) return dim("(empty)");
|
|
1614
|
-
switch (value.kind) {
|
|
1615
|
-
case "string": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
1616
|
-
case "number": return value.value !== null ? green(String(value.value)) : dim("(empty)");
|
|
1617
|
-
case "string_list": return value.items.length > 0 ? green(`[${value.items.map((i) => `"${i}"`).join(", ")}]`) : dim("(empty)");
|
|
1618
|
-
case "single_select": return value.selected ? green(value.selected) : dim("(none selected)");
|
|
1619
|
-
case "multi_select": return value.selected.length > 0 ? green(`[${value.selected.join(", ")}]`) : dim("(none selected)");
|
|
1620
|
-
case "checkboxes": {
|
|
1621
|
-
const entries = Object.entries(value.values);
|
|
1622
|
-
if (entries.length === 0) return dim("(no entries)");
|
|
1623
|
-
return entries.map(([k, v]) => `${k}:${v}`).join(", ");
|
|
1624
|
-
}
|
|
1625
|
-
default: return dim("(unknown)");
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
/**
|
|
1629
|
-
* Format inspect report for console output.
|
|
1630
|
-
*/
|
|
1631
|
-
function formatConsoleReport$1(report, useColors) {
|
|
1632
|
-
const lines = [];
|
|
1633
|
-
const bold = useColors ? pc.bold : (s) => s;
|
|
1634
|
-
const dim = useColors ? pc.dim : (s) => s;
|
|
1635
|
-
const cyan = useColors ? pc.cyan : (s) => s;
|
|
1636
|
-
const yellow = useColors ? pc.yellow : (s) => s;
|
|
1637
|
-
lines.push(bold(cyan("Form Inspection Report")));
|
|
1638
|
-
if (report.title) lines.push(`${bold("Title:")} ${report.title}`);
|
|
1639
|
-
lines.push("");
|
|
1640
|
-
lines.push(`${bold("Form State:")} ${formatState$1(report.form_state, useColors)}`);
|
|
1641
|
-
lines.push("");
|
|
1642
|
-
const structure = report.structure;
|
|
1643
|
-
lines.push(bold("Structure:"));
|
|
1644
|
-
lines.push(` Groups: ${structure.groupCount}`);
|
|
1645
|
-
lines.push(` Fields: ${structure.fieldCount}`);
|
|
1646
|
-
lines.push(` Options: ${structure.optionCount}`);
|
|
1647
|
-
lines.push("");
|
|
1648
|
-
const progress = report.progress;
|
|
1649
|
-
lines.push(bold("Progress:"));
|
|
1650
|
-
lines.push(` Total fields: ${progress.counts.totalFields}`);
|
|
1651
|
-
lines.push(` Required: ${progress.counts.requiredFields}`);
|
|
1652
|
-
lines.push(` Submitted: ${progress.counts.submittedFields}`);
|
|
1653
|
-
lines.push(` Complete: ${progress.counts.completeFields}`);
|
|
1654
|
-
lines.push(` Incomplete: ${progress.counts.incompleteFields}`);
|
|
1655
|
-
lines.push(` Invalid: ${progress.counts.invalidFields}`);
|
|
1656
|
-
lines.push(` Empty (required): ${progress.counts.emptyRequiredFields}`);
|
|
1657
|
-
lines.push(` Empty (optional): ${progress.counts.emptyOptionalFields}`);
|
|
1658
|
-
lines.push("");
|
|
1659
|
-
lines.push(bold("Form Content:"));
|
|
1660
|
-
for (const group of report.groups) {
|
|
1661
|
-
lines.push(` ${bold(group.title ?? group.id)}`);
|
|
1662
|
-
for (const field of group.children) {
|
|
1663
|
-
const reqBadge = field.required ? yellow("[required]") : dim("[optional]");
|
|
1664
|
-
const roleBadge = field.role !== "agent" ? cyan(`[${field.role}]`) : "";
|
|
1665
|
-
const value = report.values[field.id];
|
|
1666
|
-
const valueStr = formatFieldValue(value, useColors);
|
|
1667
|
-
lines.push(` ${field.label} ${dim(`(${field.kind})`)} ${reqBadge} ${roleBadge}`.trim());
|
|
1668
|
-
lines.push(` ${dim("→")} ${valueStr}`);
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
lines.push("");
|
|
1672
|
-
if (report.issues.length > 0) {
|
|
1673
|
-
lines.push(bold(`Issues (${report.issues.length}):`));
|
|
1674
|
-
for (const issue of report.issues) {
|
|
1675
|
-
const priority = formatPriority$1(issue.priority, useColors);
|
|
1676
|
-
const severity = formatSeverity$1(issue.severity, useColors);
|
|
1677
|
-
const blockedInfo = issue.blockedBy ? ` ${dim(`(blocked by: ${issue.blockedBy})`)}` : "";
|
|
1678
|
-
lines.push(` ${priority} (${severity}) ${dim(`[${issue.scope}]`)} ${dim(issue.ref)}: ${issue.message}${blockedInfo}`);
|
|
1679
|
-
}
|
|
1680
|
-
} else lines.push(dim("No issues found."));
|
|
1681
|
-
return lines.join("\n");
|
|
1682
|
-
}
|
|
1683
|
-
/**
|
|
1684
|
-
* Register the inspect command.
|
|
1685
|
-
*/
|
|
1686
|
-
function registerInspectCommand(program) {
|
|
1687
|
-
program.command("inspect <file>").description("Inspect a form and display its structure, progress, and issues").option("--roles <roles>", "Filter issues by target roles (comma-separated, or '*' for all; default: all)").action(async (file, options, cmd) => {
|
|
1688
|
-
const ctx = getCommandContext(cmd);
|
|
1689
|
-
try {
|
|
1690
|
-
let targetRoles;
|
|
1691
|
-
if (options.roles) try {
|
|
1692
|
-
targetRoles = parseRolesFlag(options.roles);
|
|
1693
|
-
if (targetRoles.includes("*")) targetRoles = void 0;
|
|
1694
|
-
} catch (error) {
|
|
1695
|
-
logError(`Invalid --roles: ${error instanceof Error ? error.message : String(error)}`);
|
|
1696
|
-
process.exit(1);
|
|
1697
|
-
}
|
|
1698
|
-
logVerbose(ctx, `Reading file: ${file}`);
|
|
1699
|
-
const content = await readFile(file);
|
|
1700
|
-
logVerbose(ctx, "Parsing form...");
|
|
1701
|
-
const form = parseForm(content);
|
|
1702
|
-
logVerbose(ctx, "Running inspection...");
|
|
1703
|
-
const result = inspect(form, { targetRoles });
|
|
1704
|
-
const output = formatOutput(ctx, {
|
|
1705
|
-
title: form.schema.title,
|
|
1706
|
-
structure: result.structureSummary,
|
|
1707
|
-
progress: result.progressSummary,
|
|
1708
|
-
form_state: result.formState,
|
|
1709
|
-
groups: form.schema.groups.map((group) => ({
|
|
1710
|
-
id: group.id,
|
|
1711
|
-
title: group.title,
|
|
1712
|
-
children: group.children.map((field) => ({
|
|
1713
|
-
id: field.id,
|
|
1714
|
-
kind: field.kind,
|
|
1715
|
-
label: field.label,
|
|
1716
|
-
required: field.required,
|
|
1717
|
-
role: field.role
|
|
1718
|
-
}))
|
|
1719
|
-
})),
|
|
1720
|
-
values: form.valuesByFieldId,
|
|
1721
|
-
issues: result.issues.map((issue) => ({
|
|
1722
|
-
ref: issue.ref,
|
|
1723
|
-
scope: issue.scope,
|
|
1724
|
-
reason: issue.reason,
|
|
1725
|
-
message: issue.message,
|
|
1726
|
-
priority: issue.priority,
|
|
1727
|
-
severity: issue.severity,
|
|
1728
|
-
blockedBy: issue.blockedBy
|
|
1729
|
-
}))
|
|
1730
|
-
}, (data, useColors) => formatConsoleReport$1(data, useColors));
|
|
1731
|
-
console.log(output);
|
|
1732
|
-
} catch (error) {
|
|
1733
|
-
logError(error instanceof Error ? error.message : String(error));
|
|
1734
|
-
process.exit(1);
|
|
1735
|
-
}
|
|
1736
|
-
});
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
//#endregion
|
|
1740
|
-
//#region src/cli/commands/models.ts
|
|
1741
|
-
/**
|
|
1742
|
-
* Get model info for all providers or a specific one.
|
|
1743
|
-
*/
|
|
1744
|
-
function getModelInfo(providerFilter) {
|
|
1745
|
-
const providers = getProviderNames();
|
|
1746
|
-
if (providerFilter && !providers.includes(providerFilter)) throw new Error(`Unknown provider: "${providerFilter}". Available: ${providers.join(", ")}`);
|
|
1747
|
-
return (providerFilter ? [providerFilter] : providers).map((provider) => {
|
|
1748
|
-
return {
|
|
1749
|
-
provider,
|
|
1750
|
-
envVar: getProviderInfo(provider).envVar,
|
|
1751
|
-
models: SUGGESTED_LLMS[provider] ?? []
|
|
1752
|
-
};
|
|
1753
|
-
});
|
|
1754
|
-
}
|
|
1755
|
-
/**
|
|
1756
|
-
* Format model info for console output.
|
|
1757
|
-
*/
|
|
1758
|
-
function formatConsoleOutput(info, useColors) {
|
|
1759
|
-
const lines = [];
|
|
1760
|
-
const bold = useColors ? pc.bold : (s) => s;
|
|
1761
|
-
const cyan = useColors ? pc.cyan : (s) => s;
|
|
1762
|
-
const dim = useColors ? pc.dim : (s) => s;
|
|
1763
|
-
const green = useColors ? pc.green : (s) => s;
|
|
1764
|
-
for (const { provider, envVar, models } of info) {
|
|
1765
|
-
lines.push(bold(cyan(`${provider}/`)));
|
|
1766
|
-
lines.push(` ${dim("env:")} ${envVar}`);
|
|
1767
|
-
if (models.length > 0) {
|
|
1768
|
-
lines.push(` ${dim("models:")}`);
|
|
1769
|
-
for (const model of models) lines.push(` ${green(`${provider}/${model}`)}`);
|
|
1770
|
-
} else lines.push(` ${dim("(no suggested models)")}`);
|
|
1771
|
-
lines.push("");
|
|
1772
|
-
}
|
|
1773
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1774
|
-
return lines.join("\n");
|
|
1775
|
-
}
|
|
1776
|
-
/**
|
|
1777
|
-
* Register the models command.
|
|
1778
|
-
*/
|
|
1779
|
-
function registerModelsCommand(program) {
|
|
1780
|
-
program.command("models").description("List available AI providers and example models").option("-p, --provider <name>", "Filter by provider (anthropic, openai, google, xai, deepseek)").action((options, cmd) => {
|
|
1781
|
-
const ctx = getCommandContext(cmd);
|
|
1782
|
-
try {
|
|
1783
|
-
const output = formatOutput(ctx, getModelInfo(options.provider), (data, useColors) => formatConsoleOutput(data, useColors));
|
|
1784
|
-
console.log(output);
|
|
1785
|
-
} catch (error) {
|
|
1786
|
-
logError(error instanceof Error ? error.message : String(error));
|
|
1787
|
-
process.exit(1);
|
|
1788
|
-
}
|
|
1789
|
-
});
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
//#endregion
|
|
1793
|
-
//#region src/cli/commands/serve.ts
|
|
1794
|
-
/**
|
|
1795
|
-
* Open a URL in the default browser (cross-platform).
|
|
1796
|
-
*/
|
|
1797
|
-
function openBrowser(url) {
|
|
1798
|
-
const platform = process.platform;
|
|
1799
|
-
let command;
|
|
1800
|
-
if (platform === "darwin") command = `open "${url}"`;
|
|
1801
|
-
else if (platform === "win32") command = `start "" "${url}"`;
|
|
1802
|
-
else command = `xdg-open "${url}"`;
|
|
1803
|
-
exec(command, (error) => {
|
|
1804
|
-
if (error) {}
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
/**
|
|
1808
|
-
* Register the serve command.
|
|
1809
|
-
*/
|
|
1810
|
-
function registerServeCommand(program) {
|
|
1811
|
-
program.command("serve <file>").description("Serve a form as a web page for browsing").option("-p, --port <port>", "Port to serve on", String(DEFAULT_PORT)).option("--no-open", "Don't open browser automatically").action(async (file, options, cmd) => {
|
|
1812
|
-
const ctx = getCommandContext(cmd);
|
|
1813
|
-
const port = parseInt(options.port ?? String(DEFAULT_PORT), 10);
|
|
1814
|
-
const filePath = resolve(file);
|
|
1815
|
-
try {
|
|
1816
|
-
logVerbose(ctx, `Reading file: ${filePath}`);
|
|
1817
|
-
let form = parseForm(await readFile(filePath));
|
|
1818
|
-
const server = createServer((req, res) => {
|
|
1819
|
-
handleRequest(req, res, form, filePath, ctx, (updatedForm) => {
|
|
1820
|
-
form = updatedForm;
|
|
1821
|
-
}).catch((err) => {
|
|
1822
|
-
console.error("Request error:", err);
|
|
1823
|
-
res.writeHead(500);
|
|
1824
|
-
res.end("Internal Server Error");
|
|
1825
|
-
});
|
|
1826
|
-
});
|
|
1827
|
-
server.listen(port, () => {
|
|
1828
|
-
const url = `http://localhost:${port}`;
|
|
1829
|
-
logInfo(ctx, pc.green(`\n✓ Form server running at ${pc.bold(url)}\n`));
|
|
1830
|
-
logInfo(ctx, pc.dim("Press Ctrl+C to stop\n"));
|
|
1831
|
-
if (options.open !== false) openBrowser(url);
|
|
1832
|
-
});
|
|
1833
|
-
process.on("SIGINT", () => {
|
|
1834
|
-
logInfo(ctx, "\nShutting down server...");
|
|
1835
|
-
server.close();
|
|
1836
|
-
process.exit(0);
|
|
1837
|
-
});
|
|
1838
|
-
} catch (error) {
|
|
1839
|
-
logError(error instanceof Error ? error.message : String(error));
|
|
1840
|
-
process.exit(1);
|
|
1841
|
-
}
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
/**
|
|
1845
|
-
* Handle HTTP requests.
|
|
1846
|
-
*/
|
|
1847
|
-
async function handleRequest(req, res, form, filePath, ctx, updateForm) {
|
|
1848
|
-
const url = req.url ?? "/";
|
|
1849
|
-
if (req.method === "GET" && url === "/") {
|
|
1850
|
-
const html = renderFormHtml(form);
|
|
1851
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1852
|
-
res.end(html);
|
|
1853
|
-
} else if (req.method === "POST" && url === "/save") await handleSave(req, res, form, filePath, ctx, updateForm);
|
|
1854
|
-
else if (url === "/api/form") {
|
|
1855
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1856
|
-
res.end(JSON.stringify({ schema: form.schema }));
|
|
1857
|
-
} else {
|
|
1858
|
-
res.writeHead(404);
|
|
1859
|
-
res.end("Not Found");
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
/**
|
|
1863
|
-
* Parse form body data.
|
|
1864
|
-
*/
|
|
1865
|
-
function parseFormBody(body) {
|
|
1866
|
-
const result = {};
|
|
1867
|
-
const params = new URLSearchParams(body);
|
|
1868
|
-
for (const [key, value] of params) {
|
|
1869
|
-
const existing = result[key];
|
|
1870
|
-
if (existing !== void 0) if (Array.isArray(existing)) existing.push(value);
|
|
1871
|
-
else result[key] = [existing, value];
|
|
1872
|
-
else result[key] = value;
|
|
1873
|
-
}
|
|
1874
|
-
return result;
|
|
1875
|
-
}
|
|
1876
|
-
/**
|
|
1877
|
-
* Convert form data to patches.
|
|
1878
|
-
*/
|
|
1879
|
-
function formDataToPatches(formData, form) {
|
|
1880
|
-
const patches = [];
|
|
1881
|
-
const fields = form.schema.groups.flatMap((g) => g.children);
|
|
1882
|
-
for (const field of fields) {
|
|
1883
|
-
const fieldId = field.id;
|
|
1884
|
-
switch (field.kind) {
|
|
1885
|
-
case "string": {
|
|
1886
|
-
const value = formData[fieldId];
|
|
1887
|
-
if (typeof value === "string" && value.trim() !== "") patches.push({
|
|
1888
|
-
op: "set_string",
|
|
1889
|
-
fieldId,
|
|
1890
|
-
value: value.trim()
|
|
1891
|
-
});
|
|
1892
|
-
else if (!value || typeof value === "string" && value.trim() === "") patches.push({
|
|
1893
|
-
op: "clear_field",
|
|
1894
|
-
fieldId
|
|
1895
|
-
});
|
|
1896
|
-
break;
|
|
1897
|
-
}
|
|
1898
|
-
case "number": {
|
|
1899
|
-
const value = formData[fieldId];
|
|
1900
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
1901
|
-
const num = parseFloat(value);
|
|
1902
|
-
if (!isNaN(num)) patches.push({
|
|
1903
|
-
op: "set_number",
|
|
1904
|
-
fieldId,
|
|
1905
|
-
value: num
|
|
1906
|
-
});
|
|
1907
|
-
} else patches.push({
|
|
1908
|
-
op: "clear_field",
|
|
1909
|
-
fieldId
|
|
1910
|
-
});
|
|
1911
|
-
break;
|
|
1912
|
-
}
|
|
1913
|
-
case "string_list": {
|
|
1914
|
-
const value = formData[fieldId];
|
|
1915
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
1916
|
-
const items = value.split("\n").map((s) => s.trim()).filter((s) => s !== "");
|
|
1917
|
-
if (items.length > 0) patches.push({
|
|
1918
|
-
op: "set_string_list",
|
|
1919
|
-
fieldId,
|
|
1920
|
-
items
|
|
1921
|
-
});
|
|
1922
|
-
else patches.push({
|
|
1923
|
-
op: "clear_field",
|
|
1924
|
-
fieldId
|
|
1925
|
-
});
|
|
1926
|
-
} else patches.push({
|
|
1927
|
-
op: "clear_field",
|
|
1928
|
-
fieldId
|
|
1929
|
-
});
|
|
1930
|
-
break;
|
|
1931
|
-
}
|
|
1932
|
-
case "single_select": {
|
|
1933
|
-
const value = formData[fieldId];
|
|
1934
|
-
if (typeof value === "string" && value !== "") patches.push({
|
|
1935
|
-
op: "set_single_select",
|
|
1936
|
-
fieldId,
|
|
1937
|
-
selected: value
|
|
1938
|
-
});
|
|
1939
|
-
else patches.push({
|
|
1940
|
-
op: "clear_field",
|
|
1941
|
-
fieldId
|
|
1942
|
-
});
|
|
1943
|
-
break;
|
|
1944
|
-
}
|
|
1945
|
-
case "multi_select": {
|
|
1946
|
-
const value = formData[fieldId];
|
|
1947
|
-
const selected = Array.isArray(value) ? value : value ? [value] : [];
|
|
1948
|
-
if (selected.length > 0 && selected[0] !== "") patches.push({
|
|
1949
|
-
op: "set_multi_select",
|
|
1950
|
-
fieldId,
|
|
1951
|
-
selected
|
|
1952
|
-
});
|
|
1953
|
-
else patches.push({
|
|
1954
|
-
op: "clear_field",
|
|
1955
|
-
fieldId
|
|
1956
|
-
});
|
|
1957
|
-
break;
|
|
1958
|
-
}
|
|
1959
|
-
case "checkboxes":
|
|
1960
|
-
if ((field.checkboxMode ?? "multi") === "simple") {
|
|
1961
|
-
const value = formData[fieldId];
|
|
1962
|
-
const checked = Array.isArray(value) ? value : value ? [value] : [];
|
|
1963
|
-
const values = {};
|
|
1964
|
-
for (const opt of field.options) values[opt.id] = checked.includes(opt.id) ? "done" : "todo";
|
|
1965
|
-
patches.push({
|
|
1966
|
-
op: "set_checkboxes",
|
|
1967
|
-
fieldId,
|
|
1968
|
-
values
|
|
1969
|
-
});
|
|
1970
|
-
} else {
|
|
1971
|
-
const values = {};
|
|
1972
|
-
for (const opt of field.options) {
|
|
1973
|
-
const selectValue = formData[`${fieldId}.${opt.id}`];
|
|
1974
|
-
if (typeof selectValue === "string" && selectValue !== "") values[opt.id] = selectValue;
|
|
1975
|
-
}
|
|
1976
|
-
if (Object.keys(values).length > 0) patches.push({
|
|
1977
|
-
op: "set_checkboxes",
|
|
1978
|
-
fieldId,
|
|
1979
|
-
values
|
|
1980
|
-
});
|
|
1981
|
-
}
|
|
1982
|
-
break;
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
return patches;
|
|
1986
|
-
}
|
|
1987
|
-
/**
|
|
1988
|
-
* Handle form save request.
|
|
1989
|
-
*/
|
|
1990
|
-
async function handleSave(req, res, form, filePath, ctx, updateForm) {
|
|
1991
|
-
try {
|
|
1992
|
-
const chunks = [];
|
|
1993
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
1994
|
-
applyPatches(form, formDataToPatches(parseFormBody(Buffer.concat(chunks).toString("utf-8")), form));
|
|
1995
|
-
updateForm(form);
|
|
1996
|
-
const newPath = generateVersionedPath(filePath);
|
|
1997
|
-
const content = serialize(form);
|
|
1998
|
-
if (ctx.dryRun) {
|
|
1999
|
-
logInfo(ctx, `[DRY RUN] Would save to: ${newPath}`);
|
|
2000
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2001
|
-
res.end(JSON.stringify({
|
|
2002
|
-
success: true,
|
|
2003
|
-
path: newPath,
|
|
2004
|
-
dryRun: true
|
|
2005
|
-
}));
|
|
2006
|
-
return;
|
|
2007
|
-
}
|
|
2008
|
-
await writeFile(newPath, content);
|
|
2009
|
-
logInfo(ctx, pc.green(`Saved to: ${newPath}`));
|
|
2010
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2011
|
-
res.end(JSON.stringify({
|
|
2012
|
-
success: true,
|
|
2013
|
-
path: newPath
|
|
2014
|
-
}));
|
|
2015
|
-
} catch (error) {
|
|
2016
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2017
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2018
|
-
res.end(JSON.stringify({
|
|
2019
|
-
success: false,
|
|
2020
|
-
error: message
|
|
2021
|
-
}));
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
/**
|
|
2025
|
-
* Render the form as HTML.
|
|
2026
|
-
* @public Exported for testing.
|
|
2027
|
-
*/
|
|
2028
|
-
function renderFormHtml(form) {
|
|
2029
|
-
const { schema, valuesByFieldId } = form;
|
|
2030
|
-
const formTitle = schema.title ?? schema.id;
|
|
2031
|
-
const groupsHtml = schema.groups.map((group) => renderGroup(group, valuesByFieldId)).join("\n");
|
|
2032
|
-
return `<!DOCTYPE html>
|
|
2033
|
-
<html lang="en">
|
|
2034
|
-
<head>
|
|
2035
|
-
<meta charset="UTF-8">
|
|
2036
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2037
|
-
<title>${escapeHtml(formTitle)} - Markform</title>
|
|
2038
|
-
<style>
|
|
2039
|
-
* { box-sizing: border-box; }
|
|
2040
|
-
body {
|
|
2041
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2042
|
-
line-height: 1.6;
|
|
2043
|
-
max-width: 800px;
|
|
2044
|
-
margin: 0 auto;
|
|
2045
|
-
padding: 2rem;
|
|
2046
|
-
background: #f8f9fa;
|
|
2047
|
-
color: #212529;
|
|
2048
|
-
}
|
|
2049
|
-
h1 { color: #495057; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5rem; }
|
|
2050
|
-
h2 { color: #6c757d; margin-top: 2rem; font-size: 1.25rem; }
|
|
2051
|
-
.group {
|
|
2052
|
-
background: white;
|
|
2053
|
-
border-radius: 8px;
|
|
2054
|
-
padding: 1.5rem;
|
|
2055
|
-
margin-bottom: 1.5rem;
|
|
2056
|
-
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
2057
|
-
}
|
|
2058
|
-
.field {
|
|
2059
|
-
margin-bottom: 1.5rem;
|
|
2060
|
-
padding-bottom: 1rem;
|
|
2061
|
-
border-bottom: 1px solid #e9ecef;
|
|
2062
|
-
}
|
|
2063
|
-
.field:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
|
|
2064
|
-
.field-label {
|
|
2065
|
-
font-weight: 600;
|
|
2066
|
-
color: #495057;
|
|
2067
|
-
display: block;
|
|
2068
|
-
margin-bottom: 0.5rem;
|
|
2069
|
-
}
|
|
2070
|
-
.required { color: #dc3545; }
|
|
2071
|
-
.type-badge {
|
|
2072
|
-
font-size: 0.7rem;
|
|
2073
|
-
padding: 0.1rem 0.3rem;
|
|
2074
|
-
background: #e9ecef;
|
|
2075
|
-
border-radius: 3px;
|
|
2076
|
-
color: #6c757d;
|
|
2077
|
-
margin-left: 0.5rem;
|
|
2078
|
-
font-weight: normal;
|
|
2079
|
-
}
|
|
2080
|
-
input[type="text"],
|
|
2081
|
-
input[type="number"],
|
|
2082
|
-
textarea,
|
|
2083
|
-
select {
|
|
2084
|
-
width: 100%;
|
|
2085
|
-
padding: 0.5rem 0.75rem;
|
|
2086
|
-
font-size: 1rem;
|
|
2087
|
-
border: 1px solid #ced4da;
|
|
2088
|
-
border-radius: 4px;
|
|
2089
|
-
background: #fff;
|
|
2090
|
-
transition: border-color 0.15s ease-in-out;
|
|
2091
|
-
}
|
|
2092
|
-
input[type="text"]:focus,
|
|
2093
|
-
input[type="number"]:focus,
|
|
2094
|
-
textarea:focus,
|
|
2095
|
-
select:focus {
|
|
2096
|
-
outline: none;
|
|
2097
|
-
border-color: #80bdff;
|
|
2098
|
-
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
|
2099
|
-
}
|
|
2100
|
-
textarea {
|
|
2101
|
-
min-height: 100px;
|
|
2102
|
-
resize: vertical;
|
|
2103
|
-
}
|
|
2104
|
-
.checkbox-group {
|
|
2105
|
-
display: flex;
|
|
2106
|
-
flex-direction: column;
|
|
2107
|
-
gap: 0.5rem;
|
|
2108
|
-
}
|
|
2109
|
-
.checkbox-item {
|
|
2110
|
-
display: flex;
|
|
2111
|
-
align-items: center;
|
|
2112
|
-
gap: 0.5rem;
|
|
2113
|
-
}
|
|
2114
|
-
.checkbox-item input[type="checkbox"] {
|
|
2115
|
-
width: auto;
|
|
2116
|
-
margin: 0;
|
|
2117
|
-
}
|
|
2118
|
-
.checkbox-item label {
|
|
2119
|
-
margin: 0;
|
|
2120
|
-
font-weight: normal;
|
|
2121
|
-
cursor: pointer;
|
|
2122
|
-
}
|
|
2123
|
-
.checkbox-item select {
|
|
2124
|
-
width: auto;
|
|
2125
|
-
min-width: 120px;
|
|
2126
|
-
}
|
|
2127
|
-
.option-row {
|
|
2128
|
-
display: flex;
|
|
2129
|
-
align-items: center;
|
|
2130
|
-
gap: 0.75rem;
|
|
2131
|
-
margin-bottom: 0.5rem;
|
|
2132
|
-
}
|
|
2133
|
-
.option-row:last-child { margin-bottom: 0; }
|
|
2134
|
-
.option-label {
|
|
2135
|
-
flex: 1;
|
|
2136
|
-
}
|
|
2137
|
-
.toolbar {
|
|
2138
|
-
position: fixed;
|
|
2139
|
-
bottom: 2rem;
|
|
2140
|
-
right: 2rem;
|
|
2141
|
-
display: flex;
|
|
2142
|
-
gap: 0.5rem;
|
|
2143
|
-
}
|
|
2144
|
-
.btn {
|
|
2145
|
-
padding: 0.75rem 1.5rem;
|
|
2146
|
-
border: none;
|
|
2147
|
-
border-radius: 6px;
|
|
2148
|
-
font-size: 1rem;
|
|
2149
|
-
cursor: pointer;
|
|
2150
|
-
transition: all 0.2s;
|
|
2151
|
-
}
|
|
2152
|
-
.btn-primary {
|
|
2153
|
-
background: #0d6efd;
|
|
2154
|
-
color: white;
|
|
2155
|
-
}
|
|
2156
|
-
.btn-primary:hover { background: #0b5ed7; }
|
|
2157
|
-
.field-help {
|
|
2158
|
-
font-size: 0.85rem;
|
|
2159
|
-
color: #6c757d;
|
|
2160
|
-
margin-top: 0.25rem;
|
|
2161
|
-
}
|
|
2162
|
-
</style>
|
|
2163
|
-
</head>
|
|
2164
|
-
<body>
|
|
2165
|
-
<h1>${escapeHtml(formTitle)}</h1>
|
|
2166
|
-
<form method="POST" action="/save" id="markform">
|
|
2167
|
-
${groupsHtml}
|
|
2168
|
-
<div class="toolbar">
|
|
2169
|
-
<button type="submit" class="btn btn-primary">Save</button>
|
|
2170
|
-
</div>
|
|
2171
|
-
</form>
|
|
2172
|
-
<script>
|
|
2173
|
-
document.getElementById('markform').addEventListener('submit', async (e) => {
|
|
2174
|
-
e.preventDefault();
|
|
2175
|
-
const formData = new FormData(e.target);
|
|
2176
|
-
const params = new URLSearchParams();
|
|
2177
|
-
for (const [key, value] of formData) {
|
|
2178
|
-
params.append(key, value);
|
|
2179
|
-
}
|
|
2180
|
-
try {
|
|
2181
|
-
const res = await fetch('/save', {
|
|
2182
|
-
method: 'POST',
|
|
2183
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
2184
|
-
body: params.toString()
|
|
2185
|
-
});
|
|
2186
|
-
const data = await res.json();
|
|
2187
|
-
if (data.success) {
|
|
2188
|
-
alert('Saved to: ' + data.path);
|
|
2189
|
-
location.reload();
|
|
2190
|
-
} else {
|
|
2191
|
-
alert('Error: ' + data.error);
|
|
2192
|
-
}
|
|
2193
|
-
} catch (err) {
|
|
2194
|
-
alert('Save failed: ' + err.message);
|
|
2195
|
-
}
|
|
2196
|
-
});
|
|
2197
|
-
<\/script>
|
|
2198
|
-
</body>
|
|
2199
|
-
</html>`;
|
|
2200
|
-
}
|
|
2201
|
-
/**
|
|
2202
|
-
* Render a field group as HTML.
|
|
2203
|
-
*/
|
|
2204
|
-
function renderGroup(group, values) {
|
|
2205
|
-
const groupTitle = group.title ?? group.id;
|
|
2206
|
-
const fieldsHtml = group.children.map((field) => renderFieldHtml(field, values[field.id])).join("\n");
|
|
2207
|
-
return `
|
|
2208
|
-
<div class="group">
|
|
2209
|
-
<h2>${escapeHtml(groupTitle)}</h2>
|
|
2210
|
-
${fieldsHtml}
|
|
2211
|
-
</div>`;
|
|
2212
|
-
}
|
|
2213
|
-
/**
|
|
2214
|
-
* Render a field as HTML.
|
|
2215
|
-
* @public Exported for testing.
|
|
2216
|
-
*/
|
|
2217
|
-
function renderFieldHtml(field, value) {
|
|
2218
|
-
const requiredMark = field.required ? "<span class=\"required\">*</span>" : "";
|
|
2219
|
-
const typeLabel = `<span class="type-badge">${field.kind}</span>`;
|
|
2220
|
-
let inputHtml;
|
|
2221
|
-
switch (field.kind) {
|
|
2222
|
-
case "string":
|
|
2223
|
-
inputHtml = renderStringInput(field, value);
|
|
2224
|
-
break;
|
|
2225
|
-
case "number":
|
|
2226
|
-
inputHtml = renderNumberInput(field, value);
|
|
2227
|
-
break;
|
|
2228
|
-
case "string_list":
|
|
2229
|
-
inputHtml = renderStringListInput(field, value);
|
|
2230
|
-
break;
|
|
2231
|
-
case "single_select":
|
|
2232
|
-
inputHtml = renderSingleSelectInput(field, value);
|
|
2233
|
-
break;
|
|
2234
|
-
case "multi_select":
|
|
2235
|
-
inputHtml = renderMultiSelectInput(field, value);
|
|
2236
|
-
break;
|
|
2237
|
-
case "checkboxes":
|
|
2238
|
-
inputHtml = renderCheckboxesInput(field, value);
|
|
2239
|
-
break;
|
|
2240
|
-
default: inputHtml = "<div class=\"field-help\">(unknown field type)</div>";
|
|
2241
|
-
}
|
|
2242
|
-
return `
|
|
2243
|
-
<div class="field">
|
|
2244
|
-
<label class="field-label" for="field-${field.id}">
|
|
2245
|
-
${escapeHtml(field.label)} ${requiredMark} ${typeLabel}
|
|
2246
|
-
</label>
|
|
2247
|
-
${inputHtml}
|
|
2248
|
-
</div>`;
|
|
2249
|
-
}
|
|
2250
|
-
/**
|
|
2251
|
-
* Render a string field as text input.
|
|
2252
|
-
*/
|
|
2253
|
-
function renderStringInput(field, value) {
|
|
2254
|
-
const currentValue = value?.kind === "string" && value.value !== null ? value.value : "";
|
|
2255
|
-
const requiredAttr = field.required ? " required" : "";
|
|
2256
|
-
const minLengthAttr = field.minLength !== void 0 ? ` minlength="${field.minLength}"` : "";
|
|
2257
|
-
const maxLengthAttr = field.maxLength !== void 0 ? ` maxlength="${field.maxLength}"` : "";
|
|
2258
|
-
return `<input type="text" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minLengthAttr}${maxLengthAttr}>`;
|
|
2259
|
-
}
|
|
2260
|
-
/**
|
|
2261
|
-
* Render a number field as number input.
|
|
2262
|
-
*/
|
|
2263
|
-
function renderNumberInput(field, value) {
|
|
2264
|
-
const currentValue = value?.kind === "number" && value.value !== null ? String(value.value) : "";
|
|
2265
|
-
const requiredAttr = field.required ? " required" : "";
|
|
2266
|
-
const minAttr = field.min !== void 0 ? ` min="${field.min}"` : "";
|
|
2267
|
-
const maxAttr = field.max !== void 0 ? ` max="${field.max}"` : "";
|
|
2268
|
-
const stepAttr = field.integer ? " step=\"1\"" : "";
|
|
2269
|
-
return `<input type="number" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minAttr}${maxAttr}${stepAttr}>`;
|
|
2270
|
-
}
|
|
2271
|
-
/**
|
|
2272
|
-
* Render a string list field as textarea.
|
|
2273
|
-
*/
|
|
2274
|
-
function renderStringListInput(field, value) {
|
|
2275
|
-
const currentValue = (value?.kind === "string_list" ? value.items : []).join("\n");
|
|
2276
|
-
const requiredAttr = field.required ? " required" : "";
|
|
2277
|
-
return `<textarea id="field-${field.id}" name="${field.id}" placeholder="Enter one item per line"${requiredAttr}>${escapeHtml(currentValue)}</textarea>`;
|
|
2278
|
-
}
|
|
2279
|
-
/**
|
|
2280
|
-
* Render a single-select field as select element.
|
|
2281
|
-
*/
|
|
2282
|
-
function renderSingleSelectInput(field, value) {
|
|
2283
|
-
const selected = value?.selected ?? null;
|
|
2284
|
-
const requiredAttr = field.required ? " required" : "";
|
|
2285
|
-
const options = field.options.map((opt) => {
|
|
2286
|
-
const selectedAttr = selected === opt.id ? " selected" : "";
|
|
2287
|
-
return `<option value="${escapeHtml(opt.id)}"${selectedAttr}>${escapeHtml(opt.label)}</option>`;
|
|
2288
|
-
}).join("\n ");
|
|
2289
|
-
return `<select id="field-${field.id}" name="${field.id}"${requiredAttr}>
|
|
2290
|
-
<option value="">-- Select --</option>
|
|
2291
|
-
${options}
|
|
2292
|
-
</select>`;
|
|
2293
|
-
}
|
|
2294
|
-
/**
|
|
2295
|
-
* Render a multi-select field as checkboxes.
|
|
2296
|
-
*/
|
|
2297
|
-
function renderMultiSelectInput(field, value) {
|
|
2298
|
-
const selected = value?.selected ?? [];
|
|
2299
|
-
return `<div class="checkbox-group">
|
|
2300
|
-
${field.options.map((opt) => {
|
|
2301
|
-
const checkedAttr = selected.includes(opt.id) ? " checked" : "";
|
|
2302
|
-
const checkboxId = `field-${field.id}-${opt.id}`;
|
|
2303
|
-
return `<div class="checkbox-item">
|
|
2304
|
-
<input type="checkbox" id="${checkboxId}" name="${field.id}" value="${escapeHtml(opt.id)}"${checkedAttr}>
|
|
2305
|
-
<label for="${checkboxId}">${escapeHtml(opt.label)}</label>
|
|
2306
|
-
</div>`;
|
|
2307
|
-
}).join("\n ")}
|
|
2308
|
-
</div>`;
|
|
2309
|
-
}
|
|
2310
|
-
/**
|
|
2311
|
-
* Render checkboxes field based on mode.
|
|
2312
|
-
*/
|
|
2313
|
-
function renderCheckboxesInput(field, value) {
|
|
2314
|
-
const checkboxValues = value?.values ?? {};
|
|
2315
|
-
const mode = field.checkboxMode ?? "multi";
|
|
2316
|
-
if (mode === "simple") return `<div class="checkbox-group">
|
|
2317
|
-
${field.options.map((opt) => {
|
|
2318
|
-
const checkedAttr = checkboxValues[opt.id] === "done" ? " checked" : "";
|
|
2319
|
-
const checkboxId = `field-${field.id}-${opt.id}`;
|
|
2320
|
-
return `<div class="checkbox-item">
|
|
2321
|
-
<input type="checkbox" id="${checkboxId}" name="${field.id}" value="${escapeHtml(opt.id)}"${checkedAttr}>
|
|
2322
|
-
<label for="${checkboxId}">${escapeHtml(opt.label)}</label>
|
|
2323
|
-
</div>`;
|
|
2324
|
-
}).join("\n ")}
|
|
2325
|
-
</div>`;
|
|
2326
|
-
if (mode === "explicit") return `<div class="checkbox-group">
|
|
2327
|
-
${field.options.map((opt) => {
|
|
2328
|
-
const state = checkboxValues[opt.id] ?? "unfilled";
|
|
2329
|
-
const selectId = `field-${field.id}-${opt.id}`;
|
|
2330
|
-
const selectName = `${field.id}.${opt.id}`;
|
|
2331
|
-
return `<div class="option-row">
|
|
2332
|
-
<span class="option-label">${escapeHtml(opt.label)}</span>
|
|
2333
|
-
<select id="${selectId}" name="${selectName}">
|
|
2334
|
-
<option value="unfilled"${state === "unfilled" ? " selected" : ""}>-- Select --</option>
|
|
2335
|
-
<option value="yes"${state === "yes" ? " selected" : ""}>Yes</option>
|
|
2336
|
-
<option value="no"${state === "no" ? " selected" : ""}>No</option>
|
|
2337
|
-
</select>
|
|
2338
|
-
</div>`;
|
|
2339
|
-
}).join("\n ")}
|
|
2340
|
-
</div>`;
|
|
2341
|
-
return `<div class="checkbox-group">
|
|
2342
|
-
${field.options.map((opt) => {
|
|
2343
|
-
const state = checkboxValues[opt.id] ?? "todo";
|
|
2344
|
-
const selectId = `field-${field.id}-${opt.id}`;
|
|
2345
|
-
const selectName = `${field.id}.${opt.id}`;
|
|
2346
|
-
return `<div class="option-row">
|
|
2347
|
-
<span class="option-label">${escapeHtml(opt.label)}</span>
|
|
2348
|
-
<select id="${selectId}" name="${selectName}">
|
|
2349
|
-
<option value="todo"${state === "todo" ? " selected" : ""}>To Do</option>
|
|
2350
|
-
<option value="active"${state === "active" ? " selected" : ""}>Active</option>
|
|
2351
|
-
<option value="done"${state === "done" ? " selected" : ""}>Done</option>
|
|
2352
|
-
<option value="incomplete"${state === "incomplete" ? " selected" : ""}>Incomplete</option>
|
|
2353
|
-
<option value="na"${state === "na" ? " selected" : ""}>N/A</option>
|
|
2354
|
-
</select>
|
|
2355
|
-
</div>`;
|
|
2356
|
-
}).join("\n ")}
|
|
2357
|
-
</div>`;
|
|
2358
|
-
}
|
|
2359
|
-
/**
|
|
2360
|
-
* Escape HTML special characters.
|
|
2361
|
-
* @public Exported for testing.
|
|
2362
|
-
*/
|
|
2363
|
-
function escapeHtml(str) {
|
|
2364
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
//#endregion
|
|
2368
|
-
//#region src/cli/commands/render.ts
|
|
2369
|
-
/**
|
|
2370
|
-
* Generate default output path by replacing .form.md with .form.html.
|
|
2371
|
-
*/
|
|
2372
|
-
function getDefaultOutputPath(inputPath) {
|
|
2373
|
-
const dir = dirname(inputPath);
|
|
2374
|
-
const base = basename(inputPath);
|
|
2375
|
-
const newBase = base.replace(/\.form\.md$/i, ".form.html");
|
|
2376
|
-
if (newBase === base) return `${inputPath}.html`;
|
|
2377
|
-
return resolve(dir, newBase);
|
|
2378
|
-
}
|
|
2379
|
-
/**
|
|
2380
|
-
* Register the render command.
|
|
2381
|
-
*/
|
|
2382
|
-
function registerRenderCommand(program) {
|
|
2383
|
-
program.command("render <file>").description("Render a form as static HTML output").option("-o, --output <path>", "Output file path (default: same stem + .html)").action(async (file, options, cmd) => {
|
|
2384
|
-
const ctx = getCommandContext(cmd);
|
|
2385
|
-
const filePath = resolve(file);
|
|
2386
|
-
const outputPath = options.output ? resolve(options.output) : getDefaultOutputPath(filePath);
|
|
2387
|
-
try {
|
|
2388
|
-
logVerbose(ctx, `Reading file: ${filePath}`);
|
|
2389
|
-
const content = await readFile(filePath);
|
|
2390
|
-
logVerbose(ctx, "Parsing form...");
|
|
2391
|
-
const form = parseForm(content);
|
|
2392
|
-
logVerbose(ctx, "Rendering HTML...");
|
|
2393
|
-
const html = renderFormHtml(form);
|
|
2394
|
-
if (ctx.dryRun) {
|
|
2395
|
-
logDryRun(`Would write HTML to: ${outputPath}`);
|
|
2396
|
-
return;
|
|
2397
|
-
}
|
|
2398
|
-
await writeFile(outputPath, html);
|
|
2399
|
-
logSuccess(ctx, pc.green(`✓ Rendered to ${outputPath}`));
|
|
2400
|
-
} catch (error) {
|
|
2401
|
-
logError(error instanceof Error ? error.message : String(error));
|
|
2402
|
-
process.exit(1);
|
|
2403
|
-
}
|
|
2404
|
-
});
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
//#endregion
|
|
2408
|
-
//#region src/cli/commands/validate.ts
|
|
2409
|
-
/**
|
|
2410
|
-
* Format state badge for console output.
|
|
2411
|
-
*/
|
|
2412
|
-
function formatState(state, useColors) {
|
|
2413
|
-
const [text, colorFn] = {
|
|
2414
|
-
complete: ["✓ complete", pc.green],
|
|
2415
|
-
incomplete: ["○ incomplete", pc.yellow],
|
|
2416
|
-
empty: ["◌ empty", pc.dim],
|
|
2417
|
-
invalid: ["✗ invalid", pc.red]
|
|
2418
|
-
}[state] ?? [state, (s) => s];
|
|
2419
|
-
return useColors ? colorFn(text) : text;
|
|
2420
|
-
}
|
|
2421
|
-
/**
|
|
2422
|
-
* Format priority badge for console output.
|
|
2423
|
-
*/
|
|
2424
|
-
function formatPriority(priority, useColors) {
|
|
2425
|
-
const label = `P${priority}`;
|
|
2426
|
-
if (!useColors) return label;
|
|
2427
|
-
switch (priority) {
|
|
2428
|
-
case 1: return pc.red(pc.bold(label));
|
|
2429
|
-
case 2: return pc.yellow(label);
|
|
2430
|
-
case 3: return pc.cyan(label);
|
|
2431
|
-
case 4: return pc.blue(label);
|
|
2432
|
-
case 5:
|
|
2433
|
-
default: return pc.dim(label);
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
/**
|
|
2437
|
-
* Format severity badge for console output.
|
|
2438
|
-
*/
|
|
2439
|
-
function formatSeverity(severity, useColors) {
|
|
2440
|
-
if (!useColors) return severity;
|
|
2441
|
-
return severity === "required" ? pc.red(severity) : pc.yellow(severity);
|
|
2442
|
-
}
|
|
2443
|
-
/**
|
|
2444
|
-
* Format validate report for console output.
|
|
2445
|
-
*/
|
|
2446
|
-
function formatConsoleReport(report, useColors) {
|
|
2447
|
-
const lines = [];
|
|
2448
|
-
const bold = useColors ? pc.bold : (s) => s;
|
|
2449
|
-
const dim = useColors ? pc.dim : (s) => s;
|
|
2450
|
-
const cyan = useColors ? pc.cyan : (s) => s;
|
|
2451
|
-
lines.push(bold(cyan("Form Validation Report")));
|
|
2452
|
-
if (report.title) lines.push(`${bold("Title:")} ${report.title}`);
|
|
2453
|
-
lines.push("");
|
|
2454
|
-
lines.push(`${bold("Form State:")} ${formatState(report.form_state, useColors)}`);
|
|
2455
|
-
lines.push("");
|
|
2456
|
-
const structure = report.structure;
|
|
2457
|
-
lines.push(bold("Structure:"));
|
|
2458
|
-
lines.push(` Groups: ${structure.groupCount}`);
|
|
2459
|
-
lines.push(` Fields: ${structure.fieldCount}`);
|
|
2460
|
-
lines.push(` Options: ${structure.optionCount}`);
|
|
2461
|
-
lines.push("");
|
|
2462
|
-
const progress = report.progress;
|
|
2463
|
-
lines.push(bold("Progress:"));
|
|
2464
|
-
lines.push(` Total fields: ${progress.counts.totalFields}`);
|
|
2465
|
-
lines.push(` Required: ${progress.counts.requiredFields}`);
|
|
2466
|
-
lines.push(` Submitted: ${progress.counts.submittedFields}`);
|
|
2467
|
-
lines.push(` Complete: ${progress.counts.completeFields}`);
|
|
2468
|
-
lines.push(` Incomplete: ${progress.counts.incompleteFields}`);
|
|
2469
|
-
lines.push(` Invalid: ${progress.counts.invalidFields}`);
|
|
2470
|
-
lines.push(` Empty (required): ${progress.counts.emptyRequiredFields}`);
|
|
2471
|
-
lines.push(` Empty (optional): ${progress.counts.emptyOptionalFields}`);
|
|
2472
|
-
lines.push("");
|
|
2473
|
-
if (report.issues.length > 0) {
|
|
2474
|
-
lines.push(bold(`Issues (${report.issues.length}):`));
|
|
2475
|
-
for (const issue of report.issues) {
|
|
2476
|
-
const priority = formatPriority(issue.priority, useColors);
|
|
2477
|
-
const severity = formatSeverity(issue.severity, useColors);
|
|
2478
|
-
lines.push(` ${priority} (${severity}) ${dim(`[${issue.scope}]`)} ${dim(issue.ref)}: ${issue.message}`);
|
|
2479
|
-
}
|
|
2480
|
-
} else lines.push(dim("No issues found."));
|
|
2481
|
-
return lines.join("\n");
|
|
2482
|
-
}
|
|
2483
|
-
/**
|
|
2484
|
-
* Register the validate command.
|
|
2485
|
-
*/
|
|
2486
|
-
function registerValidateCommand(program) {
|
|
2487
|
-
program.command("validate <file>").description("Validate a form and display summary and issues (no form content)").action(async (file, _options, cmd) => {
|
|
2488
|
-
const ctx = getCommandContext(cmd);
|
|
2489
|
-
try {
|
|
2490
|
-
logVerbose(ctx, `Reading file: ${file}`);
|
|
2491
|
-
const content = await readFile(file);
|
|
2492
|
-
logVerbose(ctx, "Parsing form...");
|
|
2493
|
-
const form = parseForm(content);
|
|
2494
|
-
logVerbose(ctx, "Running validation...");
|
|
2495
|
-
const result = inspect(form);
|
|
2496
|
-
const output = formatOutput(ctx, {
|
|
2497
|
-
title: form.schema.title,
|
|
2498
|
-
structure: result.structureSummary,
|
|
2499
|
-
progress: result.progressSummary,
|
|
2500
|
-
form_state: result.formState,
|
|
2501
|
-
issues: result.issues.map((issue) => ({
|
|
2502
|
-
ref: issue.ref,
|
|
2503
|
-
scope: issue.scope,
|
|
2504
|
-
reason: issue.reason,
|
|
2505
|
-
message: issue.message,
|
|
2506
|
-
priority: issue.priority,
|
|
2507
|
-
severity: issue.severity
|
|
2508
|
-
}))
|
|
2509
|
-
}, (data, useColors) => formatConsoleReport(data, useColors));
|
|
2510
|
-
console.log(output);
|
|
2511
|
-
} catch (error) {
|
|
2512
|
-
logError(error instanceof Error ? error.message : String(error));
|
|
2513
|
-
process.exit(1);
|
|
2514
|
-
}
|
|
2515
|
-
});
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
//#endregion
|
|
2519
|
-
//#region src/cli/cli.ts
|
|
2520
|
-
/**
|
|
2521
|
-
* CLI implementation for markform.
|
|
2522
|
-
*
|
|
2523
|
-
* Provides commands for inspecting, applying patches, exporting,
|
|
2524
|
-
* serving, and running harness loops on .form.md files.
|
|
2525
|
-
*/
|
|
2526
|
-
/**
|
|
2527
|
-
* Configure Commander with colored help text and global options display.
|
|
2528
|
-
*/
|
|
2529
|
-
function withColoredHelp(cmd) {
|
|
2530
|
-
cmd.configureHelp({
|
|
2531
|
-
styleTitle: (str) => pc.bold(pc.cyan(str)),
|
|
2532
|
-
styleCommandText: (str) => pc.green(str),
|
|
2533
|
-
styleOptionText: (str) => pc.yellow(str),
|
|
2534
|
-
showGlobalOptions: true
|
|
2535
|
-
});
|
|
2536
|
-
return cmd;
|
|
2537
|
-
}
|
|
2538
|
-
/**
|
|
2539
|
-
* Create and configure the CLI program.
|
|
2540
|
-
*/
|
|
2541
|
-
function createProgram() {
|
|
2542
|
-
const program = withColoredHelp(new Command());
|
|
2543
|
-
program.name("markform").description("Agent-friendly, human-readable, editable forms").version(VERSION).option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--dry-run", "Show what would be done without making changes").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, "console");
|
|
2544
|
-
registerInspectCommand(program);
|
|
2545
|
-
registerValidateCommand(program);
|
|
2546
|
-
registerApplyCommand(program);
|
|
2547
|
-
registerExportCommand(program);
|
|
2548
|
-
registerDumpCommand(program);
|
|
2549
|
-
registerRenderCommand(program);
|
|
2550
|
-
registerServeCommand(program);
|
|
2551
|
-
registerFillCommand(program);
|
|
2552
|
-
registerModelsCommand(program);
|
|
2553
|
-
registerExamplesCommand(program);
|
|
2554
|
-
return program;
|
|
2555
|
-
}
|
|
2556
|
-
/**
|
|
2557
|
-
* Run the CLI.
|
|
2558
|
-
*/
|
|
2559
|
-
async function runCli() {
|
|
2560
|
-
await createProgram().parseAsync(process.argv);
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
//#endregion
|
|
2564
|
-
export { runCli as t };
|