@opencraw/studio 0.0.3 → 0.0.5
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/bin/opencraw-studio.mjs +0 -0
- package/dist/index.esm.js +333 -4
- package/dist/src/index.d.ts +2 -2
- package/dist/src/scope-outline/card-sentence.mapper.d.ts +18 -0
- package/dist/src/scope-outline/index.d.ts +8 -0
- package/dist/src/scope-outline/outline-to-recipe.mapper.d.ts +17 -0
- package/dist/src/scope-outline/outline.model.d.ts +57 -0
- package/dist/src/scope-outline/recipe-to-outline.mapper.d.ts +14 -0
- package/dist/src/scope-outline/scope-at-path.algorithm.d.ts +19 -0
- package/dist/src/studio-api/command.contract.d.ts +20 -0
- package/dist/src/studio-api/index.d.ts +4 -2
- package/dist/src/studio-api/outline-view.contract.d.ts +37 -0
- package/dist/src/studio-api/workspace-view.contract.d.ts +4 -2
- package/dist/src/studio-server/save-outline.handler.d.ts +18 -0
- package/dist/ui/assets/index-DYb-tdmr.js +12 -0
- package/dist/ui/assets/index-x1XGuNl0.css +1 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/index.html +16 -0
- package/package.json +5 -19
package/bin/opencraw-studio.mjs
CHANGED
|
File without changes
|
package/dist/index.esm.js
CHANGED
|
@@ -7,6 +7,34 @@ import { BrowserClient, HttpClient, readRecipeSource, parseInputRecipe, parseOut
|
|
|
7
7
|
import { writeFile } from 'node:fs/promises';
|
|
8
8
|
import { existsSync, createReadStream } from 'node:fs';
|
|
9
9
|
|
|
10
|
+
const sentencePartSchema = z.object({
|
|
11
|
+
kind: z.enum(['word', 'pill', 'code']),
|
|
12
|
+
text: z.string()
|
|
13
|
+
});
|
|
14
|
+
const outlineNodeBase = {
|
|
15
|
+
path: z.string(),
|
|
16
|
+
stepType: z.string(),
|
|
17
|
+
sentence: z.array(sentencePartSchema),
|
|
18
|
+
step: z.record(z.string(), z.unknown())
|
|
19
|
+
};
|
|
20
|
+
const outlineCardSchema = z.object({
|
|
21
|
+
...outlineNodeBase,
|
|
22
|
+
kind: z.literal('card'),
|
|
23
|
+
custom: z.boolean()
|
|
24
|
+
});
|
|
25
|
+
const outlineBracketSchema = z.lazy(() => z.object({
|
|
26
|
+
...outlineNodeBase,
|
|
27
|
+
kind: z.literal('bracket'),
|
|
28
|
+
stepType: z.enum(['forEach', 'paginate', 'if']),
|
|
29
|
+
children: z.array(outlineNodeSchema),
|
|
30
|
+
elseChildren: z.array(outlineNodeSchema).optional()
|
|
31
|
+
}));
|
|
32
|
+
const outlineNodeSchema = z.union([outlineCardSchema, outlineBracketSchema]);
|
|
33
|
+
const outlineViewSchema = z.object({
|
|
34
|
+
recipe: z.record(z.string(), z.unknown()),
|
|
35
|
+
steps: z.array(outlineNodeSchema)
|
|
36
|
+
});
|
|
37
|
+
|
|
10
38
|
/** A budget for a sample run; never authored into a recipe, only asked for by the caller. */
|
|
11
39
|
const sampleBudgetSchema = z.object({
|
|
12
40
|
maxRecords: z.number().int().positive().optional(),
|
|
@@ -39,7 +67,22 @@ const fetchStartPageCommandSchema = z.object({
|
|
|
39
67
|
type: z.literal('fetch-start-page'),
|
|
40
68
|
recipeId: z.string().min(1)
|
|
41
69
|
});
|
|
42
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Writes a recipe file from an edited outline: the server converts it back
|
|
72
|
+
* to the recipe's JSON (`scope-outline`'s `outlineToRecipe`) and saves it
|
|
73
|
+
* exactly as `save-recipe` would. Kept as its own command, rather than
|
|
74
|
+
* asking the Steps tab to run the outline↔recipe mappers itself, so that
|
|
75
|
+
* conversion stays server-side code (it already is: `@opencraw/studio`'s
|
|
76
|
+
* root export also carries Node-only slices, so the UI never imports
|
|
77
|
+
* runtime code from it, only types — see `apps/studio-ui`'s `steps-outline`
|
|
78
|
+
* for where this is called from).
|
|
79
|
+
*/
|
|
80
|
+
const saveOutlineCommandSchema = z.object({
|
|
81
|
+
type: z.literal('save-outline'),
|
|
82
|
+
path: z.string().min(1),
|
|
83
|
+
outline: outlineViewSchema
|
|
84
|
+
});
|
|
85
|
+
const studioCommandSchema = z.discriminatedUnion('type', [openWorkspaceCommandSchema, runSampleCommandSchema, stopRunCommandSchema, saveRecipeCommandSchema, fetchStartPageCommandSchema, saveOutlineCommandSchema]);
|
|
43
86
|
|
|
44
87
|
/** Why a sample run stopped before finishing on its own; mirrors `@opencraw/core`'s `RecipeReport.stoppedBy`. */
|
|
45
88
|
const stoppedBySchema = z.enum(['sample-maxRecords', 'sample-maxPages', 'sample-maxMs']);
|
|
@@ -85,7 +128,9 @@ const recipeListingSchema = z.object({
|
|
|
85
128
|
id: z.string().optional(),
|
|
86
129
|
issues: z.array(recipeIssueSchema),
|
|
87
130
|
/** The file's JSON, pretty-printed: the JSON tab's starting text (see `save-recipe` for how an edit is written back). */
|
|
88
|
-
text: z.string()
|
|
131
|
+
text: z.string(),
|
|
132
|
+
/** The Steps tab's view of this file's `steps`, from `@opencraw/studio`'s `scope-outline` slice. `input` recipes only; `undefined` for an `output` or `unknown` file, which has no steps to show. */
|
|
133
|
+
outline: outlineViewSchema.optional()
|
|
89
134
|
});
|
|
90
135
|
/** What `open-workspace` answers with: every recipe file directly inside the folder. */
|
|
91
136
|
const workspaceViewSchema = z.object({
|
|
@@ -326,6 +371,261 @@ async function handleFetchStartPage(state, command) {
|
|
|
326
371
|
};
|
|
327
372
|
}
|
|
328
373
|
|
|
374
|
+
/**
|
|
375
|
+
* The outline model: an `InputRecipe`'s `steps` (and only `steps`; the rest
|
|
376
|
+
* of the recipe passes through untouched, see `recipe-to-outline.mapper.ts`)
|
|
377
|
+
* read as a tree of cards and brackets instead of raw JSON.
|
|
378
|
+
*
|
|
379
|
+
* A **card** is one step that reads as a sentence (`sentence`, built by
|
|
380
|
+
* `card-sentence.mapper.ts`). A **bracket** is a container step (`forEach`,
|
|
381
|
+
* `paginate`, `if`) whose body is itself a list of cards/brackets: the
|
|
382
|
+
* indentation the plan doc describes is this nesting, not a separate layout
|
|
383
|
+
* concept. Both kinds carry `step`, the step's own raw JSON exactly as
|
|
384
|
+
* parsed (every key, in its original order): `outline-to-recipe.mapper.ts`
|
|
385
|
+
* rebuilds the recipe from it, substituting only the (possibly edited)
|
|
386
|
+
* `children`/`elseChildren` for whatever `steps`/`else` value `step` itself
|
|
387
|
+
* holds, so an edit to one field never disturbs another's position or a
|
|
388
|
+
* step type this outline cannot make sense of.
|
|
389
|
+
*/
|
|
390
|
+
/** The step types the outline renders as a bracket: their body is nested steps, not a single value. */
|
|
391
|
+
const BRACKET_STEP_TYPES = ['forEach', 'paginate', 'if'];
|
|
392
|
+
function isBracket(node) {
|
|
393
|
+
return node.kind === 'bracket';
|
|
394
|
+
}
|
|
395
|
+
function isBracketStepType(type) {
|
|
396
|
+
return BRACKET_STEP_TYPES.includes(type);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Step types the plan doc names as always going to a "custom" card: nothing here renders their meaning as a sentence. */
|
|
400
|
+
const ALWAYS_CUSTOM = new Set(['evaluate', 'hook', 'captcha']);
|
|
401
|
+
/**
|
|
402
|
+
* Turns one step's raw JSON into its sentence parts (word/pill/code), the
|
|
403
|
+
* way the plan doc's examples read: *Go to…*, *Read… from…*, *For each… in…
|
|
404
|
+
* → one record*, and so on. Never throws: a step type this cannot read
|
|
405
|
+
* becomes a short, honest fallback with `custom: true`, so the outline never
|
|
406
|
+
* drops a step for looking unfamiliar.
|
|
407
|
+
*
|
|
408
|
+
* @param step - A step's raw JSON (its `type` and whatever fields it has).
|
|
409
|
+
* @returns The sentence parts, and whether they are a real reading or a fallback.
|
|
410
|
+
*/
|
|
411
|
+
function cardSentence(step) {
|
|
412
|
+
const type = typeof step.type === 'string' ? step.type : undefined;
|
|
413
|
+
if (type === undefined || ALWAYS_CUSTOM.has(type)) return fallback(type);
|
|
414
|
+
const build = BUILDERS[type];
|
|
415
|
+
if (build === undefined) return fallback(type);
|
|
416
|
+
try {
|
|
417
|
+
return {
|
|
418
|
+
parts: build(step),
|
|
419
|
+
custom: false
|
|
420
|
+
};
|
|
421
|
+
} catch {
|
|
422
|
+
return fallback(type);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function fallback(type) {
|
|
426
|
+
return {
|
|
427
|
+
parts: [word(type ?? 'custom step')],
|
|
428
|
+
custom: true
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function word(text) {
|
|
432
|
+
return {
|
|
433
|
+
kind: 'word',
|
|
434
|
+
text
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function pill(text) {
|
|
438
|
+
return {
|
|
439
|
+
kind: 'pill',
|
|
440
|
+
text
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function code(text) {
|
|
444
|
+
return {
|
|
445
|
+
kind: 'code',
|
|
446
|
+
text
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/** A field that names an id: shown as a pill, `fallbackText` when it is absent. */
|
|
450
|
+
function pillField(step, key, fallbackText) {
|
|
451
|
+
const value = step[key];
|
|
452
|
+
return pill(typeof value === 'string' && value.length > 0 ? value : fallbackText);
|
|
453
|
+
}
|
|
454
|
+
/** A field shown as a literal (a selector, a template, a value): stringified when it is not already a string. */
|
|
455
|
+
function codeField(step, key, fallbackText = '') {
|
|
456
|
+
const value = step[key];
|
|
457
|
+
if (typeof value === 'string') return code(value);
|
|
458
|
+
if (value === undefined) return code(fallbackText);
|
|
459
|
+
return code(safeJson(value));
|
|
460
|
+
}
|
|
461
|
+
function safeJson(value) {
|
|
462
|
+
try {
|
|
463
|
+
return JSON.stringify(value) ?? String(value);
|
|
464
|
+
} catch {
|
|
465
|
+
return String(value);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/** `selector` or `target`, whichever the step gives (`step.contract.ts`'s `TargetFields`: exactly one is present). */
|
|
469
|
+
function targetField(step) {
|
|
470
|
+
if (typeof step.target === 'string') return code(step.target);
|
|
471
|
+
return codeField(step, 'selector');
|
|
472
|
+
}
|
|
473
|
+
const BUILDERS = {
|
|
474
|
+
goto: step => [word('Go to'), codeField(step, 'url')],
|
|
475
|
+
click: step => [word('Click'), targetField(step)],
|
|
476
|
+
fill: step => [word('Fill'), targetField(step), word('with'), codeField(step, 'value')],
|
|
477
|
+
press: step => [word('Press'), codeField(step, 'key'), word('in'), targetField(step)],
|
|
478
|
+
select: step => [word('Choose'), selectChoice(step), word('in'), targetField(step)],
|
|
479
|
+
scroll: step => [word('Scroll to'), codeField(step, 'to')],
|
|
480
|
+
wait: step => [word('Wait for'), waitFor(step)],
|
|
481
|
+
screenshot: step => [word('Screenshot'), codeField(step, 'path')],
|
|
482
|
+
request: step => [word('Request'), code(`${typeof step.method === 'string' ? step.method : 'GET'} `), codeField(step, 'url')],
|
|
483
|
+
extract: step => [word('Read'), pillField(step, 'id', '(unnamed)'), word('from'), pillField(step, 'from', 'page'), word('←'), codeField(step, 'selector')],
|
|
484
|
+
set: step => [word('Set'), pillField(step, 'id', '(unnamed)'), word('to'), codeField(step, 'value')],
|
|
485
|
+
collect: step => [word('Collect'), codeField(step, 'value'), word('into'), pillField(step, 'into', '(unnamed)')],
|
|
486
|
+
forEach: step => forEachSentence(step),
|
|
487
|
+
paginate: step => paginateSentence(step),
|
|
488
|
+
if: step => [word('If'), codeField(step, 'test')],
|
|
489
|
+
emit: step => emitSentence(step)
|
|
490
|
+
};
|
|
491
|
+
function selectChoice(step) {
|
|
492
|
+
if (typeof step.value === 'string') return code(step.value);
|
|
493
|
+
if (typeof step.label === 'string') return code(step.label);
|
|
494
|
+
if (typeof step.index === 'number') return code(String(step.index));
|
|
495
|
+
if (Array.isArray(step.values)) return code(step.values.join(', '));
|
|
496
|
+
return code('');
|
|
497
|
+
}
|
|
498
|
+
function waitFor(step) {
|
|
499
|
+
if (typeof step.selector === 'string') return code(step.selector);
|
|
500
|
+
if (typeof step.ms === 'number') return code(`${step.ms}ms`);
|
|
501
|
+
if (typeof step.state === 'string') return code(step.state);
|
|
502
|
+
return code('');
|
|
503
|
+
}
|
|
504
|
+
function forEachSentence(step) {
|
|
505
|
+
const parts = [word('For each'), pillField(step, 'as', '(item)'), word('in'), typeof step.over === 'string' ? pill(step.over) : targetField(step)];
|
|
506
|
+
if (step.emit !== undefined) parts.push(word('→'), word('one record'));
|
|
507
|
+
return parts;
|
|
508
|
+
}
|
|
509
|
+
function paginateSentence(step) {
|
|
510
|
+
const parts = [word('For every page')];
|
|
511
|
+
const next = step.next;
|
|
512
|
+
if (next !== null && typeof next === 'object') {
|
|
513
|
+
const record = next;
|
|
514
|
+
if (typeof record.selector === 'string') parts.push(word('via'), code(record.selector));else if (typeof record.url === 'string') parts.push(word('via'), code(record.url));else if (typeof record.jsonpath === 'string') parts.push(word('via'), code(record.jsonpath));
|
|
515
|
+
}
|
|
516
|
+
if (typeof step.until === 'string') parts.push(word('until'), code(step.until));
|
|
517
|
+
return parts;
|
|
518
|
+
}
|
|
519
|
+
function emitSentence(step) {
|
|
520
|
+
if (typeof step.output === 'string') return [word('Emit'), word('to'), pill(step.output)];
|
|
521
|
+
return [word('Emit')];
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Reads a recipe's `steps` as an outline. Works on whatever JSON parsed
|
|
526
|
+
* (not a validated `InputRecipe`): a recipe mid-edit in the JSON tab may be
|
|
527
|
+
* missing fields or have the wrong shape, and the outline still has to
|
|
528
|
+
* render something rather than crash the tab. Anything that is not a
|
|
529
|
+
* well-formed step becomes a custom card holding what was there; anything
|
|
530
|
+
* that is not an object at the top becomes an outline with no steps.
|
|
531
|
+
*
|
|
532
|
+
* @param content - The recipe's parsed JSON (the JSON tab's own `JSON.parse` of its text).
|
|
533
|
+
* @returns The outline: the untouched top-level record plus the step tree.
|
|
534
|
+
*/
|
|
535
|
+
function recipeToOutline(content) {
|
|
536
|
+
const recipe = isRecord(content) ? content : {};
|
|
537
|
+
const steps = Array.isArray(recipe.steps) ? recipe.steps : [];
|
|
538
|
+
return {
|
|
539
|
+
recipe,
|
|
540
|
+
steps: stepsToNodes(steps, 'steps')
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function stepsToNodes(steps, pathPrefix) {
|
|
544
|
+
return steps.map((step, index) => stepToNode(step, `${pathPrefix}.${index}`));
|
|
545
|
+
}
|
|
546
|
+
function stepToNode(step, path) {
|
|
547
|
+
if (!isRecord(step)) {
|
|
548
|
+
return {
|
|
549
|
+
kind: 'card',
|
|
550
|
+
path,
|
|
551
|
+
stepType: 'unknown',
|
|
552
|
+
sentence: [{
|
|
553
|
+
kind: 'word',
|
|
554
|
+
text: 'custom step'
|
|
555
|
+
}],
|
|
556
|
+
step: {},
|
|
557
|
+
custom: true
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const stepType = typeof step.type === 'string' ? step.type : 'unknown';
|
|
561
|
+
const {
|
|
562
|
+
parts,
|
|
563
|
+
custom
|
|
564
|
+
} = cardSentence(step);
|
|
565
|
+
if (!isBracketStepType(stepType)) return {
|
|
566
|
+
kind: 'card',
|
|
567
|
+
path,
|
|
568
|
+
stepType,
|
|
569
|
+
sentence: parts,
|
|
570
|
+
step,
|
|
571
|
+
custom
|
|
572
|
+
};
|
|
573
|
+
const children = Array.isArray(step.steps) ? stepsToNodes(step.steps, `${path}.steps`) : [];
|
|
574
|
+
const elseChildren = stepType === 'if' && Array.isArray(step.else) ? stepsToNodes(step.else, `${path}.else`) : undefined;
|
|
575
|
+
return {
|
|
576
|
+
kind: 'bracket',
|
|
577
|
+
path,
|
|
578
|
+
stepType,
|
|
579
|
+
sentence: parts,
|
|
580
|
+
step,
|
|
581
|
+
children,
|
|
582
|
+
...(elseChildren !== undefined && {
|
|
583
|
+
elseChildren
|
|
584
|
+
})
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function isRecord(value) {
|
|
588
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Rebuilds a recipe from its outline: `recipe`'s keys, in their original
|
|
593
|
+
* order, with `steps` (when the recipe had one) replaced by the outline's
|
|
594
|
+
* (possibly edited) step tree. A card's own raw `step` is used as-is; a
|
|
595
|
+
* bracket's `step` supplies every field but `steps`/`else`, which come from
|
|
596
|
+
* `children`/`elseChildren` instead, put back at the position `step` itself
|
|
597
|
+
* held that key at (or appended, for a bracket a `+` menu just added, whose
|
|
598
|
+
* `step` never had one) — this, together with `recipe-to-outline.mapper.ts`
|
|
599
|
+
* always keeping the step's whole original object, is what makes
|
|
600
|
+
* `recipe → outline → recipe` preserve every key and its order.
|
|
601
|
+
*
|
|
602
|
+
* @param outline - An outline, from `recipe-to-outline.mapper.ts` or edited by the UI.
|
|
603
|
+
* @returns The recipe's plain JSON, ready for `save-recipe`.
|
|
604
|
+
*/
|
|
605
|
+
function outlineToRecipe(outline) {
|
|
606
|
+
if (!('steps' in outline.recipe) && outline.steps.length === 0) return {
|
|
607
|
+
...outline.recipe
|
|
608
|
+
};
|
|
609
|
+
return {
|
|
610
|
+
...outline.recipe,
|
|
611
|
+
steps: nodesToSteps(outline.steps)
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function nodesToSteps(nodes) {
|
|
615
|
+
return nodes.map(node => nodeToStep(node));
|
|
616
|
+
}
|
|
617
|
+
function nodeToStep(node) {
|
|
618
|
+
if (!isBracket(node)) return node.step;
|
|
619
|
+
const keys = Object.keys(node.step);
|
|
620
|
+
const step = {};
|
|
621
|
+
for (const key of keys) {
|
|
622
|
+
if (key === 'steps') step.steps = nodesToSteps(node.children);else if (key === 'else' && node.stepType === 'if') step.else = nodesToSteps(node.elseChildren ?? []);else step[key] = node.step[key];
|
|
623
|
+
}
|
|
624
|
+
if (!keys.includes('steps')) step.steps = nodesToSteps(node.children);
|
|
625
|
+
if (node.stepType === 'if' && node.elseChildren !== undefined && !keys.includes('else')) step.else = nodesToSteps(node.elseChildren);
|
|
626
|
+
return step;
|
|
627
|
+
}
|
|
628
|
+
|
|
329
629
|
/**
|
|
330
630
|
* Opens a recipe folder: every `.json`/`.jsonl` recipe directly inside it
|
|
331
631
|
* (core's own directory reading, so relative `file:` URLs resolve the same
|
|
@@ -366,7 +666,10 @@ async function openWorkspace(folder) {
|
|
|
366
666
|
kind,
|
|
367
667
|
id,
|
|
368
668
|
issues,
|
|
369
|
-
text: `${JSON.stringify(content, null, 2)}\n
|
|
669
|
+
text: `${JSON.stringify(content, null, 2)}\n`,
|
|
670
|
+
// The Steps tab needs steps to render: an output/unknown file has none. recipeToOutline never
|
|
671
|
+
// throws, so this covers a file that fails validation too (its outline just has "custom" cards).
|
|
672
|
+
outline: kind === 'input' ? recipeToOutline(content) : undefined
|
|
370
673
|
}))
|
|
371
674
|
};
|
|
372
675
|
}
|
|
@@ -559,6 +862,28 @@ async function handleStopRun(state, _command) {
|
|
|
559
862
|
};
|
|
560
863
|
}
|
|
561
864
|
|
|
865
|
+
/**
|
|
866
|
+
* Handles `save-outline`: the Steps tab's edits, as the outline it holds
|
|
867
|
+
* locally, converted back to the recipe's JSON (`scope-outline`'s
|
|
868
|
+
* `outlineToRecipe`, so the conversion runs here rather than in the
|
|
869
|
+
* browser) and written the same way `save-recipe` writes the JSON tab's
|
|
870
|
+
* edits. Every connected client, including the sender, is told the
|
|
871
|
+
* workspace changed, so both tabs re-fetch it and stay in sync.
|
|
872
|
+
*
|
|
873
|
+
* @param state - The server state holding the connected sockets.
|
|
874
|
+
* @param command - The command: the file path and the edited outline.
|
|
875
|
+
* @returns `{ saved: true }` once the write has completed.
|
|
876
|
+
*/
|
|
877
|
+
async function handleSaveOutline(state, command) {
|
|
878
|
+
await saveRecipe(command.path, outlineToRecipe(command.outline));
|
|
879
|
+
broadcast(state, {
|
|
880
|
+
type: 'workspace-changed'
|
|
881
|
+
});
|
|
882
|
+
return {
|
|
883
|
+
saved: true
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
562
887
|
/**
|
|
563
888
|
* Handles `save-recipe`: writes the file (see `recipe-workspace/save-recipe.use-case.ts`
|
|
564
889
|
* for the pretty-printing and its round-trip guarantee), then tells every
|
|
@@ -826,6 +1151,10 @@ function dispatch(command, state) {
|
|
|
826
1151
|
{
|
|
827
1152
|
return handleFetchStartPage(state, command);
|
|
828
1153
|
}
|
|
1154
|
+
case 'save-outline':
|
|
1155
|
+
{
|
|
1156
|
+
return handleSaveOutline(state, command);
|
|
1157
|
+
}
|
|
829
1158
|
}
|
|
830
1159
|
}
|
|
831
1160
|
function respondJson(response, status, body) {
|
|
@@ -882,5 +1211,5 @@ async function main(argv) {
|
|
|
882
1211
|
process.on('SIGTERM', shutdown);
|
|
883
1212
|
}
|
|
884
1213
|
|
|
885
|
-
export { fetchStartPage, fetchStartPageCommandSchema, loadRecipePair, main, openWorkspace, openWorkspaceCommandSchema, recipeIssueSchema, recipeListingSchema, recordEventSchema, runFinishedEventSchema, runSample, runSampleCommandSchema, sampleBudgetSchema, sampleRunRecordSchema, sampleRunViewSchema, saveRecipe, saveRecipeCommandSchema, startPageViewSchema, startStudioServer, stopRunCommandSchema, stoppedBySchema, studioCommandSchema, studioEventSchema, traceLineEventSchema, workspaceChangedEventSchema, workspaceViewSchema };
|
|
1214
|
+
export { fetchStartPage, fetchStartPageCommandSchema, loadRecipePair, main, openWorkspace, openWorkspaceCommandSchema, outlineBracketSchema, outlineCardSchema, outlineNodeSchema, outlineViewSchema, recipeIssueSchema, recipeListingSchema, recordEventSchema, runFinishedEventSchema, runSample, runSampleCommandSchema, sampleBudgetSchema, sampleRunRecordSchema, sampleRunViewSchema, saveOutlineCommandSchema, saveRecipe, saveRecipeCommandSchema, sentencePartSchema, startPageViewSchema, startStudioServer, stopRunCommandSchema, stoppedBySchema, studioCommandSchema, studioEventSchema, traceLineEventSchema, workspaceChangedEventSchema, workspaceViewSchema };
|
|
886
1215
|
//# sourceMappingURL=index.esm.js.map
|
package/dist/src/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@ export { openWorkspace, saveRecipe } from './recipe-workspace/index.js';
|
|
|
5
5
|
export { runSample, loadRecipePair } from './sample-run/index.js';
|
|
6
6
|
export type { RecipePair, SampleRunCallbacks, SampleRunHandle, SampleRunRecord, SampleRunResult, SampleStoppedBy } from './sample-run/index.js';
|
|
7
7
|
export { fetchStartPage } from './page-snapshot/index.js';
|
|
8
|
-
export { sampleBudgetSchema, openWorkspaceCommandSchema, runSampleCommandSchema, stopRunCommandSchema, saveRecipeCommandSchema, fetchStartPageCommandSchema, studioCommandSchema, stoppedBySchema, traceLineEventSchema, recordEventSchema, runFinishedEventSchema, workspaceChangedEventSchema, studioEventSchema, recipeIssueSchema, recipeListingSchema, workspaceViewSchema, sampleRunRecordSchema, sampleRunViewSchema, startPageViewSchema, } from './studio-api/index.js';
|
|
9
|
-
export type { SampleBudget, OpenWorkspaceCommand, RunSampleCommand, StopRunCommand, SaveRecipeCommand, FetchStartPageCommand, StudioCommand, StoppedBy, TraceLineEvent, RecordEvent, RunFinishedEvent, WorkspaceChangedEvent, StudioEvent, RecipeIssue, RecipeListing, WorkspaceView, SampleRunView, StartPageView, } from './studio-api/index.js';
|
|
8
|
+
export { sampleBudgetSchema, openWorkspaceCommandSchema, runSampleCommandSchema, stopRunCommandSchema, saveRecipeCommandSchema, fetchStartPageCommandSchema, saveOutlineCommandSchema, studioCommandSchema, stoppedBySchema, traceLineEventSchema, recordEventSchema, runFinishedEventSchema, workspaceChangedEventSchema, studioEventSchema, recipeIssueSchema, recipeListingSchema, workspaceViewSchema, sampleRunRecordSchema, sampleRunViewSchema, startPageViewSchema, sentencePartSchema, outlineCardSchema, outlineBracketSchema, outlineNodeSchema, outlineViewSchema, } from './studio-api/index.js';
|
|
9
|
+
export type { SampleBudget, OpenWorkspaceCommand, RunSampleCommand, StopRunCommand, SaveRecipeCommand, FetchStartPageCommand, SaveOutlineCommand, StudioCommand, StoppedBy, TraceLineEvent, RecordEvent, RunFinishedEvent, WorkspaceChangedEvent, StudioEvent, RecipeIssue, RecipeListing, WorkspaceView, SampleRunView, StartPageView, SentencePart, OutlineCard, OutlineBracket, OutlineNode, OutlineView, } from './studio-api/index.js';
|
|
10
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SentencePart } from './outline.model.js';
|
|
2
|
+
export interface StepSentence {
|
|
3
|
+
parts: SentencePart[];
|
|
4
|
+
/** `true` when this is a fallback (an unrecognised type, or one of `ALWAYS_CUSTOM`): the UI shows `step`'s raw JSON instead of trusting `parts` alone. */
|
|
5
|
+
custom: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Turns one step's raw JSON into its sentence parts (word/pill/code), the
|
|
9
|
+
* way the plan doc's examples read: *Go to…*, *Read… from…*, *For each… in…
|
|
10
|
+
* → one record*, and so on. Never throws: a step type this cannot read
|
|
11
|
+
* becomes a short, honest fallback with `custom: true`, so the outline never
|
|
12
|
+
* drops a step for looking unfamiliar.
|
|
13
|
+
*
|
|
14
|
+
* @param step - A step's raw JSON (its `type` and whatever fields it has).
|
|
15
|
+
* @returns The sentence parts, and whether they are a real reading or a fallback.
|
|
16
|
+
*/
|
|
17
|
+
export declare function cardSentence(step: Record<string, unknown>): StepSentence;
|
|
18
|
+
//# sourceMappingURL=card-sentence.mapper.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { BRACKET_STEP_TYPES, isBracket, isBracketStepType } from './outline.model.js';
|
|
2
|
+
export type { OutlineBracket, OutlineCard, OutlineNode, RecipeOutline, SentencePart, BracketStepType } from './outline.model.js';
|
|
3
|
+
export { cardSentence } from './card-sentence.mapper.js';
|
|
4
|
+
export type { StepSentence } from './card-sentence.mapper.js';
|
|
5
|
+
export { recipeToOutline } from './recipe-to-outline.mapper.js';
|
|
6
|
+
export { outlineToRecipe } from './outline-to-recipe.mapper.js';
|
|
7
|
+
export { scopeAtPath } from './scope-at-path.algorithm.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RecipeOutline } from './outline.model.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rebuilds a recipe from its outline: `recipe`'s keys, in their original
|
|
4
|
+
* order, with `steps` (when the recipe had one) replaced by the outline's
|
|
5
|
+
* (possibly edited) step tree. A card's own raw `step` is used as-is; a
|
|
6
|
+
* bracket's `step` supplies every field but `steps`/`else`, which come from
|
|
7
|
+
* `children`/`elseChildren` instead, put back at the position `step` itself
|
|
8
|
+
* held that key at (or appended, for a bracket a `+` menu just added, whose
|
|
9
|
+
* `step` never had one) — this, together with `recipe-to-outline.mapper.ts`
|
|
10
|
+
* always keeping the step's whole original object, is what makes
|
|
11
|
+
* `recipe → outline → recipe` preserve every key and its order.
|
|
12
|
+
*
|
|
13
|
+
* @param outline - An outline, from `recipe-to-outline.mapper.ts` or edited by the UI.
|
|
14
|
+
* @returns The recipe's plain JSON, ready for `save-recipe`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function outlineToRecipe(outline: RecipeOutline): Record<string, unknown>;
|
|
17
|
+
//# sourceMappingURL=outline-to-recipe.mapper.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outline model: an `InputRecipe`'s `steps` (and only `steps`; the rest
|
|
3
|
+
* of the recipe passes through untouched, see `recipe-to-outline.mapper.ts`)
|
|
4
|
+
* read as a tree of cards and brackets instead of raw JSON.
|
|
5
|
+
*
|
|
6
|
+
* A **card** is one step that reads as a sentence (`sentence`, built by
|
|
7
|
+
* `card-sentence.mapper.ts`). A **bracket** is a container step (`forEach`,
|
|
8
|
+
* `paginate`, `if`) whose body is itself a list of cards/brackets: the
|
|
9
|
+
* indentation the plan doc describes is this nesting, not a separate layout
|
|
10
|
+
* concept. Both kinds carry `step`, the step's own raw JSON exactly as
|
|
11
|
+
* parsed (every key, in its original order): `outline-to-recipe.mapper.ts`
|
|
12
|
+
* rebuilds the recipe from it, substituting only the (possibly edited)
|
|
13
|
+
* `children`/`elseChildren` for whatever `steps`/`else` value `step` itself
|
|
14
|
+
* holds, so an edit to one field never disturbs another's position or a
|
|
15
|
+
* step type this outline cannot make sense of.
|
|
16
|
+
*/
|
|
17
|
+
/** One piece of a card's sentence: plain wording, a bound id (a "pill"), or a literal (a selector, a template, a value) shown as code. */
|
|
18
|
+
export interface SentencePart {
|
|
19
|
+
kind: 'word' | 'pill' | 'code';
|
|
20
|
+
text: string;
|
|
21
|
+
}
|
|
22
|
+
/** The step types the outline renders as a bracket: their body is nested steps, not a single value. */
|
|
23
|
+
export declare const BRACKET_STEP_TYPES: readonly ["forEach", "paginate", "if"];
|
|
24
|
+
export type BracketStepType = typeof BRACKET_STEP_TYPES[number];
|
|
25
|
+
interface OutlineNodeBase {
|
|
26
|
+
/** The step's JSON path in the recipe, e.g. `steps.3.steps.1`. */
|
|
27
|
+
path: string;
|
|
28
|
+
/** The step's own `type`, or `'unknown'` when `step` has none. */
|
|
29
|
+
stepType: string;
|
|
30
|
+
sentence: SentencePart[];
|
|
31
|
+
/** The step's raw JSON, exactly as parsed: never dropped, always editable as JSON. */
|
|
32
|
+
step: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
/** One step the outline could not, or chose not to, read as a bracket: a leaf card. */
|
|
35
|
+
export interface OutlineCard extends OutlineNodeBase {
|
|
36
|
+
kind: 'card';
|
|
37
|
+
/** `true` when `card-sentence.mapper.ts` could not build a real sentence: the UI shows `step` as raw, editable JSON instead. */
|
|
38
|
+
custom: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** A container step (`forEach`, `paginate`, `if`): its body nested one level in. */
|
|
41
|
+
export interface OutlineBracket extends OutlineNodeBase {
|
|
42
|
+
kind: 'bracket';
|
|
43
|
+
stepType: BracketStepType;
|
|
44
|
+
children: OutlineNode[];
|
|
45
|
+
/** `if` only: the `else` branch, when the step has one. */
|
|
46
|
+
elseChildren?: OutlineNode[];
|
|
47
|
+
}
|
|
48
|
+
export type OutlineNode = OutlineCard | OutlineBracket;
|
|
49
|
+
/** A recipe read as an outline: `recipe` is everything but the derived view of `steps`, kept so the mapper back can restore it untouched. */
|
|
50
|
+
export interface RecipeOutline {
|
|
51
|
+
recipe: Record<string, unknown>;
|
|
52
|
+
steps: OutlineNode[];
|
|
53
|
+
}
|
|
54
|
+
export declare function isBracket(node: OutlineNode): node is OutlineBracket;
|
|
55
|
+
export declare function isBracketStepType(type: string): type is BracketStepType;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=outline.model.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RecipeOutline } from './outline.model.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads a recipe's `steps` as an outline. Works on whatever JSON parsed
|
|
4
|
+
* (not a validated `InputRecipe`): a recipe mid-edit in the JSON tab may be
|
|
5
|
+
* missing fields or have the wrong shape, and the outline still has to
|
|
6
|
+
* render something rather than crash the tab. Anything that is not a
|
|
7
|
+
* well-formed step becomes a custom card holding what was there; anything
|
|
8
|
+
* that is not an object at the top becomes an outline with no steps.
|
|
9
|
+
*
|
|
10
|
+
* @param content - The recipe's parsed JSON (the JSON tab's own `JSON.parse` of its text).
|
|
11
|
+
* @returns The outline: the untouched top-level record plus the step tree.
|
|
12
|
+
*/
|
|
13
|
+
export declare function recipeToOutline(content: unknown): RecipeOutline;
|
|
14
|
+
//# sourceMappingURL=recipe-to-outline.mapper.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ids visible at a step path: the built-ins (`page`, `start`, `vars`),
|
|
3
|
+
* every id bound above it on the same path, and the `as`/`next.as` name of
|
|
4
|
+
* every loop it sits inside. This is core's own binding rule
|
|
5
|
+
* (`bindingsAt`, `recipe-loading/recipe-binding.validator.ts`): the walk
|
|
6
|
+
* that decides what a `{{ }}` template, `from` or `over` can name is the one
|
|
7
|
+
* `opencraw validate` already runs, not a copy of it.
|
|
8
|
+
*
|
|
9
|
+
* `content` is the recipe's raw parsed JSON, the same loosely-typed value
|
|
10
|
+
* `recipe-to-outline.mapper.ts` reads: a recipe mid-edit may not parse as a
|
|
11
|
+
* valid `InputRecipe` (a missing `mode`, a step with the wrong shape), and
|
|
12
|
+
* this never throws for that — it falls back to just the built-ins.
|
|
13
|
+
*
|
|
14
|
+
* @param content - The recipe's parsed JSON.
|
|
15
|
+
* @param path - A step's JSON path, e.g. `steps.3.steps.1`.
|
|
16
|
+
* @returns The ids in scope at that path, `page`/`start`/`vars` first.
|
|
17
|
+
*/
|
|
18
|
+
export declare function scopeAtPath(content: unknown, path: string): string[];
|
|
19
|
+
//# sourceMappingURL=scope-at-path.algorithm.d.ts.map
|
|
@@ -35,6 +35,21 @@ export declare const fetchStartPageCommandSchema: z.ZodObject<{
|
|
|
35
35
|
type: z.ZodLiteral<"fetch-start-page">;
|
|
36
36
|
recipeId: z.ZodString;
|
|
37
37
|
}, z.core.$strip>;
|
|
38
|
+
/**
|
|
39
|
+
* Writes a recipe file from an edited outline: the server converts it back
|
|
40
|
+
* to the recipe's JSON (`scope-outline`'s `outlineToRecipe`) and saves it
|
|
41
|
+
* exactly as `save-recipe` would. Kept as its own command, rather than
|
|
42
|
+
* asking the Steps tab to run the outline↔recipe mappers itself, so that
|
|
43
|
+
* conversion stays server-side code (it already is: `@opencraw/studio`'s
|
|
44
|
+
* root export also carries Node-only slices, so the UI never imports
|
|
45
|
+
* runtime code from it, only types — see `apps/studio-ui`'s `steps-outline`
|
|
46
|
+
* for where this is called from).
|
|
47
|
+
*/
|
|
48
|
+
export declare const saveOutlineCommandSchema: z.ZodObject<{
|
|
49
|
+
type: z.ZodLiteral<"save-outline">;
|
|
50
|
+
path: z.ZodString;
|
|
51
|
+
outline: z.ZodType<import("./outline-view.contract").OutlineView, unknown, z.core.$ZodTypeInternals<import("./outline-view.contract").OutlineView, unknown>>;
|
|
52
|
+
}, z.core.$strip>;
|
|
38
53
|
export declare const studioCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
39
54
|
type: z.ZodLiteral<"open-workspace">;
|
|
40
55
|
folder: z.ZodString;
|
|
@@ -55,6 +70,10 @@ export declare const studioCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
55
70
|
}, z.core.$strip>, z.ZodObject<{
|
|
56
71
|
type: z.ZodLiteral<"fetch-start-page">;
|
|
57
72
|
recipeId: z.ZodString;
|
|
73
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
74
|
+
type: z.ZodLiteral<"save-outline">;
|
|
75
|
+
path: z.ZodString;
|
|
76
|
+
outline: z.ZodType<import("./outline-view.contract").OutlineView, unknown, z.core.$ZodTypeInternals<import("./outline-view.contract").OutlineView, unknown>>;
|
|
58
77
|
}, z.core.$strip>], "type">;
|
|
59
78
|
export type SampleBudget = z.infer<typeof sampleBudgetSchema>;
|
|
60
79
|
export type OpenWorkspaceCommand = z.infer<typeof openWorkspaceCommandSchema>;
|
|
@@ -62,6 +81,7 @@ export type RunSampleCommand = z.infer<typeof runSampleCommandSchema>;
|
|
|
62
81
|
export type StopRunCommand = z.infer<typeof stopRunCommandSchema>;
|
|
63
82
|
export type SaveRecipeCommand = z.infer<typeof saveRecipeCommandSchema>;
|
|
64
83
|
export type FetchStartPageCommand = z.infer<typeof fetchStartPageCommandSchema>;
|
|
84
|
+
export type SaveOutlineCommand = z.infer<typeof saveOutlineCommandSchema>;
|
|
65
85
|
/** Every command the UI can send the server, over HTTP POST. */
|
|
66
86
|
export type StudioCommand = z.infer<typeof studioCommandSchema>;
|
|
67
87
|
//# sourceMappingURL=command.contract.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { sampleBudgetSchema, openWorkspaceCommandSchema, runSampleCommandSchema, stopRunCommandSchema, saveRecipeCommandSchema, fetchStartPageCommandSchema, studioCommandSchema } from './command.contract.js';
|
|
2
|
-
export type { SampleBudget, OpenWorkspaceCommand, RunSampleCommand, StopRunCommand, SaveRecipeCommand, FetchStartPageCommand, StudioCommand } from './command.contract.js';
|
|
1
|
+
export { sampleBudgetSchema, openWorkspaceCommandSchema, runSampleCommandSchema, stopRunCommandSchema, saveRecipeCommandSchema, fetchStartPageCommandSchema, saveOutlineCommandSchema, studioCommandSchema } from './command.contract.js';
|
|
2
|
+
export type { SampleBudget, OpenWorkspaceCommand, RunSampleCommand, StopRunCommand, SaveRecipeCommand, FetchStartPageCommand, SaveOutlineCommand, StudioCommand } from './command.contract.js';
|
|
3
3
|
export { stoppedBySchema, traceLineEventSchema, recordEventSchema, runFinishedEventSchema, workspaceChangedEventSchema, studioEventSchema } from './event.contract.js';
|
|
4
4
|
export type { StoppedBy, TraceLineEvent, RecordEvent, RunFinishedEvent, WorkspaceChangedEvent, StudioEvent } from './event.contract.js';
|
|
5
5
|
export { recipeIssueSchema, recipeListingSchema, workspaceViewSchema } from './workspace-view.contract.js';
|
|
@@ -8,4 +8,6 @@ export { sampleRunRecordSchema, sampleRunViewSchema } from './sample-run-view.co
|
|
|
8
8
|
export type { SampleRunRecord, SampleRunView } from './sample-run-view.contract.js';
|
|
9
9
|
export { startPageViewSchema } from './start-page-view.contract.js';
|
|
10
10
|
export type { StartPageView } from './start-page-view.contract.js';
|
|
11
|
+
export { sentencePartSchema, outlineCardSchema, outlineBracketSchema, outlineNodeSchema, outlineViewSchema } from './outline-view.contract.js';
|
|
12
|
+
export type { SentencePart, OutlineCard, OutlineBracket, OutlineNode, OutlineView } from './outline-view.contract.js';
|
|
11
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** One piece of a card's sentence: plain wording, a bound id (a "pill"), or a literal shown as code. Mirrors `scope-outline/outline.model.ts`'s `SentencePart`: the wire shape for the UI. */
|
|
3
|
+
export interface SentencePart {
|
|
4
|
+
kind: 'word' | 'pill' | 'code';
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
interface OutlineNodeBase {
|
|
8
|
+
path: string;
|
|
9
|
+
stepType: string;
|
|
10
|
+
sentence: SentencePart[];
|
|
11
|
+
step: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/** A leaf step, read as a sentence (or, when `custom`, not read as one: the UI shows `step`'s raw JSON instead). */
|
|
14
|
+
export interface OutlineCard extends OutlineNodeBase {
|
|
15
|
+
kind: 'card';
|
|
16
|
+
custom: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** A container step (`forEach`, `paginate`, `if`), its body nested one level in. */
|
|
19
|
+
export interface OutlineBracket extends OutlineNodeBase {
|
|
20
|
+
kind: 'bracket';
|
|
21
|
+
stepType: 'forEach' | 'paginate' | 'if';
|
|
22
|
+
children: OutlineNode[];
|
|
23
|
+
elseChildren?: OutlineNode[];
|
|
24
|
+
}
|
|
25
|
+
export type OutlineNode = OutlineCard | OutlineBracket;
|
|
26
|
+
/** The outline the UI renders and edits: the recipe's other keys untouched, plus its steps as a tree. */
|
|
27
|
+
export interface OutlineView {
|
|
28
|
+
recipe: Record<string, unknown>;
|
|
29
|
+
steps: OutlineNode[];
|
|
30
|
+
}
|
|
31
|
+
export declare const sentencePartSchema: z.ZodType<SentencePart>;
|
|
32
|
+
export declare const outlineCardSchema: z.ZodType<OutlineCard>;
|
|
33
|
+
export declare const outlineBracketSchema: z.ZodType<OutlineBracket>;
|
|
34
|
+
export declare const outlineNodeSchema: z.ZodType<OutlineNode>;
|
|
35
|
+
export declare const outlineViewSchema: z.ZodType<OutlineView>;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=outline-view.contract.d.ts.map
|