@henols/c64-re-tools 2.1.2 → 2.1.3
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/package.json +1 -1
- package/skills/c64-disk/scripts/lib/containers.ts +2 -1
- package/skills/c64-emulator/SKILL.md +2 -0
- package/skills/c64-knowledge/SKILL.md +6 -2
- package/skills/c64-knowledge/scripts/knowledge.ts +6 -12
- package/skills/c64-knowledge/scripts/lib/listing.ts +8 -1
- package/skills/c64-memory-map/SKILL.md +2 -0
- package/skills/c64-memory-map/scripts/memory-map.ts +22 -3
- package/skills/c64-provenance/SKILL.md +4 -3
- package/skills/c64-provenance/scripts/lib/library.ts +28 -5
- package/skills/c64-provenance/scripts/lib/separate.ts +34 -6
- package/skills/c64-provenance/scripts/provenance.ts +13 -4
- package/skills/c64-source/SKILL.md +1 -1
- package/skills/c64-source/scripts/lib/source.ts +18 -4
- package/skills/c64-source/scripts/source.ts +9 -13
- package/skills/c64-static-analysis/SKILL.md +4 -3
- package/skills/c64-static-analysis/scripts/lib/findings.ts +23 -8
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +38 -25
- package/skills/c64-static-analysis/scripts/static-analysis.ts +17 -14
- package/skills/c64-testing/SKILL.md +6 -3
- package/skills/c64-testing/references/scenario-format.md +5 -3
- package/skills/c64-testing/scripts/lib/diff-files.ts +27 -8
- package/skills/c64-testing/scripts/lib/runs.ts +23 -7
- package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
- package/skills/c64-testing/scripts/testing.ts +5 -2
- package/skills/c64-unpacker/SKILL.md +2 -2
- package/skills/c64-unpacker/scripts/lib/loads.ts +4 -1
- package/skills/c64-unpacker/scripts/lib/machine.ts +31 -0
- package/skills/c64-unpacker/scripts/unpacker.ts +4 -1
- package/src/host/vice/session.ts +98 -11
- package/src/host-client/image.ts +22 -8
- package/src/host-client/steps.ts +2 -2
- package/src/host-client/tools.ts +4 -3
- package/src/host-client/transfer.ts +8 -3
- package/src/knowledge/write.ts +64 -28
- package/src/mcp/output.ts +1 -1
- package/src/mcp/server.ts +7 -0
- package/src/mcp/tools/debug.ts +2 -1
- package/src/mcp/tools/media.ts +5 -1
- package/src/media.ts +15 -0
- package/src/opcodes.ts +31 -1
- package/src/platform.ts +14 -1
- package/src/project.ts +8 -1
- package/src/script.ts +63 -2
package/src/knowledge/write.ts
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
type SuppressionRow,
|
|
63
63
|
type SymbolRow,
|
|
64
64
|
} from "./read.ts";
|
|
65
|
-
import { CODE_REGION_TYPES, COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, MACRO_NAME, MACRO_PARAM, SWITCH_NAME, NO_ELEMENT_TYPES, OPERAND_SELECTS, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type EvidenceKind, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
65
|
+
import { CODE_REGION_TYPES, COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, isSemantic, MACRO_NAME, MACRO_PARAM, SWITCH_NAME, NO_ELEMENT_TYPES, OPERAND_SELECTS, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type EvidenceKind, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
66
66
|
import { newRevisionId, rowId, sqlName } from "./text.ts";
|
|
67
67
|
|
|
68
68
|
/** What a change rests on: a coverage run, a test or another project file with its SHA-256, or a note. */
|
|
@@ -353,6 +353,27 @@ const result = <T, P>(written: { revision: string | null; current: Step<T, P> })
|
|
|
353
353
|
// Each write has a check, which refuses bad input before anything opens, and a
|
|
354
354
|
// step, which runs inside a change. A single write and a batch share both.
|
|
355
355
|
|
|
356
|
+
/**
|
|
357
|
+
* True when a write of `origin` over a current row with the same content
|
|
358
|
+
* records a decision, so it replaces the row: over a row of an analyzer (the
|
|
359
|
+
* next run of the analyzer can retire its own rows, never a decision), and the
|
|
360
|
+
* user over the LLM. The LLM over the user changes nothing.
|
|
361
|
+
*/
|
|
362
|
+
function confirms(existing: Origin, origin: Origin): boolean {
|
|
363
|
+
return existing !== origin && (!isSemantic(existing) || origin === "user");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The bytes of the program of a space in the knowledge, by address: undefined
|
|
368
|
+
* outside it. A module and a comment are checked against them.
|
|
369
|
+
*/
|
|
370
|
+
function programBytes(db: DatabaseSync, space: Space): (address: number) => number | undefined {
|
|
371
|
+
const rows = db.prepare("SELECT address, bytes FROM current_memory WHERE space = ?").all(space) as Array<{ address: number; bytes: string }>;
|
|
372
|
+
const memory = new Map<number, number>();
|
|
373
|
+
for (const row of rows) Buffer.from(row.bytes, "hex").forEach((byte, offset) => memory.set(row.address + offset, byte));
|
|
374
|
+
return (address) => memory.get(address);
|
|
375
|
+
}
|
|
376
|
+
|
|
356
377
|
/** Every request names its space; without one it is the C64. */
|
|
357
378
|
type InSpace = { space?: Space };
|
|
358
379
|
type SymbolRequest = InSpace & { address: number; name: string; kind?: SymbolKind };
|
|
@@ -381,7 +402,7 @@ function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: S
|
|
|
381
402
|
const space = spaceOf(request);
|
|
382
403
|
const existing = symbolAt(db, request.address, space);
|
|
383
404
|
const kind = request.kind ?? existing?.kind ?? "label";
|
|
384
|
-
if (existing !== undefined && existing.name === request.name && existing.kind === kind) return { current: existing };
|
|
405
|
+
if (existing !== undefined && existing.name === request.name && existing.kind === kind && !confirms(existing.origin, origin)) return { current: existing };
|
|
385
406
|
const owner = symbolNamed(db, request.name, space);
|
|
386
407
|
if (owner !== undefined && owner.address !== request.address) {
|
|
387
408
|
throw new Refusal("conflict", `The name ${request.name} already belongs to ${formatC64Address(owner.address)}. Rename or remove that symbol first.`);
|
|
@@ -459,7 +480,7 @@ function classifyStep(db: DatabaseSync, change: Change, origin: Origin, request:
|
|
|
459
480
|
const overlapping = regionsOverlapping(db, request.start, request.end, space);
|
|
460
481
|
const [only] = overlapping;
|
|
461
482
|
const same = overlapping.length === 1 && only!.start === request.start && only!.end === request.end && only!.type === request.type && only!.element === request.element;
|
|
462
|
-
if (same) return { current: overlapping };
|
|
483
|
+
if (same && !confirms(only!.origin, origin)) return { current: overlapping };
|
|
463
484
|
clearRange(db, change, request.start, request.end, space);
|
|
464
485
|
change.insertRegion({ space, start: request.start, end: request.end, type: request.type, ...(request.element === undefined ? {} : { element: request.element }), origin });
|
|
465
486
|
liftRegionSuppressions(db, change, request.start, request.end, space);
|
|
@@ -490,14 +511,17 @@ function checkComment(request: CommentRequest): string {
|
|
|
490
511
|
return text;
|
|
491
512
|
}
|
|
492
513
|
|
|
493
|
-
function commentStep(db: DatabaseSync, change: Change, origin: Origin, request: CommentRequest): Step<CommentRow> {
|
|
514
|
+
function commentStep(db: DatabaseSync, change: Change, origin: Origin, request: CommentRequest): Step<CommentRow, CommentRow> {
|
|
494
515
|
const space = spaceOf(request);
|
|
495
516
|
const text = request.text.trim();
|
|
496
517
|
const existing = commentsAt(db, request.address, space).find((comment) => comment.placement === request.placement);
|
|
497
|
-
if (existing?.text === text) return { current: existing };
|
|
518
|
+
if (existing?.text === text && !confirms(existing.origin, origin)) return { current: existing };
|
|
519
|
+
// A comment belongs to an instruction: the listing and the source show it at the start of the instruction only.
|
|
520
|
+
checkInstructionStart(db, space, programBytes(db, space), request.address, "comment");
|
|
498
521
|
if (existing !== undefined) change.close("comments", "space = ? AND address = ? AND placement = ?", space, request.address, request.placement);
|
|
499
522
|
change.insertComment({ space, address: request.address, placement: request.placement, text, origin });
|
|
500
|
-
|
|
523
|
+
const current = commentsAt(db, request.address, space).find((comment) => comment.placement === request.placement)!;
|
|
524
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
501
525
|
}
|
|
502
526
|
|
|
503
527
|
function checkPlacement(request: PlacementRequest): void {
|
|
@@ -523,14 +547,16 @@ function checkReference(request: ReferenceRequest): void {
|
|
|
523
547
|
if (request.kind === POINTER_KIND) throw new Refusal("invalid-input", "Only an operand expression writes a pointer reference: use operand and unoperand.");
|
|
524
548
|
}
|
|
525
549
|
|
|
526
|
-
function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<ReferenceRow> {
|
|
550
|
+
function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<ReferenceRow, ReferenceRow> {
|
|
527
551
|
const space = spaceOf(request);
|
|
528
552
|
const find = () => referencesFrom(db, request.from, space).find((reference) => reference.to === request.to && reference.kind === request.kind);
|
|
529
553
|
const existing = find();
|
|
530
|
-
if (existing !== undefined) return { current: existing };
|
|
554
|
+
if (existing !== undefined && !confirms(existing.origin, origin)) return { current: existing };
|
|
555
|
+
// A confirmation of a reference of an analyzer makes it a reference of the writer: the next run of the analyzer keeps it.
|
|
556
|
+
if (existing !== undefined) change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ?", space, request.from, request.to, request.kind);
|
|
531
557
|
change.insertReference({ space, from: request.from, to: request.to, kind: request.kind, origin });
|
|
532
558
|
change.close("suppressions", "space = ? AND entity = 'reference' AND start_address = ? AND to_address = ? AND kind = ?", space, request.from, request.to, request.kind);
|
|
533
|
-
return { current: find()! };
|
|
559
|
+
return existing !== undefined ? { current: find()!, previous: existing } : { current: find()! };
|
|
534
560
|
}
|
|
535
561
|
|
|
536
562
|
function unreferenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<null> {
|
|
@@ -581,26 +607,32 @@ const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${
|
|
|
581
607
|
/**
|
|
582
608
|
* Refuses an address that is not the start of an instruction. The code
|
|
583
609
|
* regions of the knowledge give the instructions: they are decoded from the
|
|
584
|
-
* start of the region, as the listing and the source decode them.
|
|
610
|
+
* start of the region, as the listing and the source decode them. An operand
|
|
611
|
+
* expression needs an instruction, so it is refused also outside the code
|
|
612
|
+
* regions and where the program has no byte. A module and a comment are
|
|
613
|
+
* refused only at an operand byte of a known instruction: they can start data.
|
|
585
614
|
*/
|
|
586
|
-
function checkInstructionStart(db: DatabaseSync, space: Space,
|
|
615
|
+
function checkInstructionStart(db: DatabaseSync, space: Space, byteAt: (address: number) => number | undefined, address: number, write: "operand" | "module" | "comment"): void {
|
|
587
616
|
const where = formatC64Address(address);
|
|
588
617
|
const code = regionsOverlapping(db, address, address, space).find((region) => CODE_REGION_TYPES.includes(region.type));
|
|
589
618
|
if (code === undefined) {
|
|
619
|
+
if (write !== "operand") return;
|
|
590
620
|
throw new Refusal("invalid-input", `${where} is in no code region, so the knowledge does not know an instruction that starts there. Classify the code that holds the instruction first (classify <range> code), then write the operand.`);
|
|
591
621
|
}
|
|
592
622
|
let start = code.start;
|
|
593
623
|
let next = code.start;
|
|
594
624
|
while (next < address) {
|
|
595
|
-
const opcode =
|
|
625
|
+
const opcode = byteAt(next);
|
|
596
626
|
if (opcode === undefined) {
|
|
627
|
+
if (write !== "operand") return;
|
|
597
628
|
throw new Refusal("invalid-input", `The code region ${formatC64Address(code.start)}-${formatC64Address(code.end)} has no program byte at ${formatC64Address(next)}, so its instructions up to ${where} are not known. Correct the region (classify), then write the operand.`);
|
|
598
629
|
}
|
|
599
630
|
start = next;
|
|
600
631
|
next += instructionLength(opcode);
|
|
601
632
|
}
|
|
602
633
|
if (next !== address) {
|
|
603
|
-
|
|
634
|
+
const what = write === "operand" ? "" : ` A ${write} is at the start of an instruction, as the listing and the source show it.`;
|
|
635
|
+
throw new Refusal("invalid-input", `${where} is not the start of an instruction. It is an operand byte of the instruction at ${formatC64Address(start)}, decoded from the start of the code region at ${formatC64Address(code.start)}.${what} Give ${formatC64Address(start)}.`);
|
|
604
636
|
}
|
|
605
637
|
}
|
|
606
638
|
|
|
@@ -613,7 +645,7 @@ function checkOperandValue(db: DatabaseSync, space: Space, image: OperandImage,
|
|
|
613
645
|
const where = formatC64Address(request.address);
|
|
614
646
|
const opcode = image.byteAt(request.address);
|
|
615
647
|
if (opcode === undefined) throw new Refusal("invalid-input", `${where} is outside the image.`);
|
|
616
|
-
checkInstructionStart(db, space, image, request.address);
|
|
648
|
+
checkInstructionStart(db, space, (at) => image.byteAt(at), request.address, "operand");
|
|
617
649
|
const length = instructionLength(opcode);
|
|
618
650
|
if (length === 1) throw new Refusal("invalid-input", `The instruction at ${where} (opcode ${shown(opcode, 0x100)}) has no operand.`);
|
|
619
651
|
if ((opcode & 0x1f) === 0x10) throw new Refusal("invalid-input", `The instruction at ${where} is a branch: its operand is a distance, not a value.`);
|
|
@@ -646,10 +678,10 @@ function operandStep(db: DatabaseSync, change: Change, context: ChangeContext &
|
|
|
646
678
|
if (request.expression !== undefined) wanted.expression = request.expression.trim();
|
|
647
679
|
const targets = new Set(checkOperandValue(db, space, context.image, request, operandText(wanted)));
|
|
648
680
|
const existing = operandAt(db, request.address, space);
|
|
649
|
-
|
|
681
|
+
const origin = context.origin as OperandRow["origin"];
|
|
682
|
+
if (existing !== undefined && existing.select === wanted.select && existing.symbol === wanted.symbol && existing.offset === wanted.offset && existing.expression === wanted.expression && !confirms(existing.origin, origin)) {
|
|
650
683
|
return { current: existing };
|
|
651
684
|
}
|
|
652
|
-
const origin = context.origin as OperandRow["origin"];
|
|
653
685
|
if (existing !== undefined) change.close("operands", "space = ? AND address = ?", space, request.address);
|
|
654
686
|
change.insertOperand({ space, address: request.address, ...wanted, origin });
|
|
655
687
|
const pointers = referencesFrom(db, request.address, space).filter((reference) => reference.kind === POINTER_KIND);
|
|
@@ -683,19 +715,22 @@ function checkModule(request: ModuleRequest): void {
|
|
|
683
715
|
if (request.title !== undefined && (request.title.trim().length === 0 || request.title.length > MAX_COMMENT)) throw new Refusal("invalid-input", `A module title has 1 to ${MAX_COMMENT} characters.`);
|
|
684
716
|
}
|
|
685
717
|
|
|
686
|
-
function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: ModuleRequest): Step<ModuleRow> {
|
|
718
|
+
function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: ModuleRequest): Step<ModuleRow, ModuleRow> {
|
|
687
719
|
const space = spaceOf(request);
|
|
688
720
|
const existing = moduleAt(db, request.start, space);
|
|
689
721
|
// A module write without a title keeps the title it has.
|
|
690
722
|
const title = request.title === undefined ? existing?.title : request.title.trim();
|
|
691
|
-
if (existing !== undefined && existing.file === request.file && existing.title === title) return { current: existing };
|
|
723
|
+
if (existing !== undefined && existing.file === request.file && existing.title === title && !confirms(existing.origin, origin)) return { current: existing };
|
|
724
|
+
// A file of the source starts at an instruction: a start inside one splits it into bytes.
|
|
725
|
+
checkInstructionStart(db, space, programBytes(db, space), request.start, "module");
|
|
692
726
|
const owner = moduleWithFile(db, request.file, space);
|
|
693
727
|
if (owner !== undefined && owner.start !== request.start) {
|
|
694
728
|
throw new Refusal("conflict", `The file ${request.file} already belongs to the module at ${formatC64Address(owner.start)}. Give this module another file, or remove that module first.`);
|
|
695
729
|
}
|
|
696
730
|
if (existing !== undefined) change.close("modules", "space = ? AND start_address = ?", space, request.start);
|
|
697
731
|
change.insertModule({ space, start: request.start, file: request.file, ...(title === undefined ? {} : { title }), origin: origin as ModuleRow["origin"] });
|
|
698
|
-
|
|
732
|
+
const current = moduleAt(db, request.start, space)!;
|
|
733
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
699
734
|
}
|
|
700
735
|
|
|
701
736
|
function unmoduleStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
|
|
@@ -760,9 +795,10 @@ function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & {
|
|
|
760
795
|
const symbol = symbolNamed(db, request.switch, space);
|
|
761
796
|
if (symbol !== undefined) throw new Refusal("conflict", `The switch name ${request.switch} is the name of the symbol at ${formatC64Address(symbol.address)}. Give the switch another name.`);
|
|
762
797
|
if (space === DEFAULT_SPACE && context.image.platform.has(request.switch)) throw new Refusal("conflict", `The switch name ${request.switch} is a C64 platform name. Give the switch another name.`);
|
|
763
|
-
|
|
798
|
+
// A write at the start of a patch of the same switch changes that patch; every other patch that it overlaps is a conflict.
|
|
799
|
+
const other = patchesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start || row.switch !== request.switch);
|
|
764
800
|
if (other !== undefined) {
|
|
765
|
-
throw new Refusal("conflict", `The patch ${range} overlaps the patch of ${other.switch} at ${formatC64Address(other.start)}-${formatC64Address(other.end)}. Change or remove that patch first (unpatch).`);
|
|
801
|
+
throw new Refusal("conflict", `The patch ${range} of ${request.switch} overlaps the patch of ${other.switch} at ${formatC64Address(other.start)}-${formatC64Address(other.end)}. A patch cannot overlap another patch, and only a patch of the same switch at the same start replaces it. Change or remove that patch first (unpatch).`);
|
|
766
802
|
}
|
|
767
803
|
const [use] = macroUsesOverlapping(db, request.start, request.end, space);
|
|
768
804
|
if (use !== undefined) {
|
|
@@ -772,8 +808,8 @@ function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & {
|
|
|
772
808
|
const code = request.code.trimEnd();
|
|
773
809
|
// A patch write without a comment keeps the comment it has, as a module keeps its title.
|
|
774
810
|
const comment = request.comment === undefined ? existing?.comment : request.comment.trim();
|
|
775
|
-
if (existing !== undefined && existing.end === request.end && existing.switch === request.switch && existing.code === code && existing.comment === comment) return { current: existing };
|
|
776
811
|
const origin = context.origin as PatchRow["origin"];
|
|
812
|
+
if (existing !== undefined && existing.end === request.end && existing.switch === request.switch && existing.code === code && existing.comment === comment && !confirms(existing.origin, origin)) return { current: existing };
|
|
777
813
|
if (existing !== undefined) change.close("patches", "space = ? AND start_address = ?", space, request.start);
|
|
778
814
|
change.insertPatch({ space, start: request.start, end: request.end, switch: request.switch, code, ...(comment === undefined ? {} : { comment }), origin });
|
|
779
815
|
const current = patchesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start)!;
|
|
@@ -815,7 +851,7 @@ function macroStep(db: DatabaseSync, change: Change, origin: Origin, request: Ma
|
|
|
815
851
|
const params = request.params ?? [];
|
|
816
852
|
const body = request.body.trimEnd();
|
|
817
853
|
const comment = request.comment === undefined ? existing?.comment : request.comment.trim();
|
|
818
|
-
if (existing !== undefined && listText(existing.params) === listText(params) && existing.body === body && existing.comment === comment) return { current: existing };
|
|
854
|
+
if (existing !== undefined && listText(existing.params) === listText(params) && existing.body === body && existing.comment === comment && !confirms(existing.origin, origin)) return { current: existing };
|
|
819
855
|
if (existing !== undefined && existing.params.length !== params.length) {
|
|
820
856
|
const uses = listMacroUses(db, undefined, request.name, undefined, space);
|
|
821
857
|
if (uses.length > 0) {
|
|
@@ -901,8 +937,8 @@ function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { im
|
|
|
901
937
|
throw new Refusal("conflict", `The macro use ${range} overlaps the use of ${other.macro} at ${formatC64Address(other.start)}-${formatC64Address(other.end)}. Remove that use first (unuse).`);
|
|
902
938
|
}
|
|
903
939
|
const existing = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
|
|
904
|
-
if (existing !== undefined && existing.end === request.end && existing.macro === request.macro && listText(existing.args) === listText(args)) return { current: existing };
|
|
905
940
|
const origin = context.origin as MacroUseRow["origin"];
|
|
941
|
+
if (existing !== undefined && existing.end === request.end && existing.macro === request.macro && listText(existing.args) === listText(args) && !confirms(existing.origin, origin)) return { current: existing };
|
|
906
942
|
if (existing !== undefined) change.close("macro_uses", "space = ? AND start_address = ?", space, request.start);
|
|
907
943
|
change.insertMacroUse({ space, start: request.start, end: request.end, macro: request.macro, args, origin });
|
|
908
944
|
const current = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start)!;
|
|
@@ -940,7 +976,7 @@ export function unclassifyRegion(db: DatabaseSync, context: WriteContext, reques
|
|
|
940
976
|
return result(write(db, context, "unclassify-region", (change) => unclassifyStep(db, change, context.origin, request)));
|
|
941
977
|
}
|
|
942
978
|
|
|
943
|
-
export function setComment(db: DatabaseSync, context: WriteContext, request: CommentRequest): WriteResult<CommentRow> {
|
|
979
|
+
export function setComment(db: DatabaseSync, context: WriteContext, request: CommentRequest): WriteResult<CommentRow, CommentRow> {
|
|
944
980
|
checkComment(request);
|
|
945
981
|
return result(write(db, context, "set-comment", (change) => commentStep(db, change, context.origin, request)));
|
|
946
982
|
}
|
|
@@ -950,7 +986,7 @@ export function removeComment(db: DatabaseSync, context: WriteContext, request:
|
|
|
950
986
|
return result(write(db, context, "remove-comment", (change) => uncommentStep(db, change, context.origin, request)));
|
|
951
987
|
}
|
|
952
988
|
|
|
953
|
-
export function addReference(db: DatabaseSync, context: WriteContext, request: ReferenceRequest): WriteResult<ReferenceRow> {
|
|
989
|
+
export function addReference(db: DatabaseSync, context: WriteContext, request: ReferenceRequest): WriteResult<ReferenceRow, ReferenceRow> {
|
|
954
990
|
checkReference(request);
|
|
955
991
|
return result(write(db, context, "add-reference", (change) => referenceStep(db, change, context.origin, request)));
|
|
956
992
|
}
|
|
@@ -961,7 +997,7 @@ export function removeReference(db: DatabaseSync, context: WriteContext, request
|
|
|
961
997
|
}
|
|
962
998
|
|
|
963
999
|
/** Starts a file of the generated source at an address; one module per address and per file. */
|
|
964
|
-
export function setModule(db: DatabaseSync, context: WriteContext, request: ModuleRequest): WriteResult<ModuleRow> {
|
|
1000
|
+
export function setModule(db: DatabaseSync, context: WriteContext, request: ModuleRequest): WriteResult<ModuleRow, ModuleRow> {
|
|
965
1001
|
checkModule(request);
|
|
966
1002
|
return result(write(db, context, "set-module", (change) => moduleStep(db, change, context.origin, request)));
|
|
967
1003
|
}
|
|
@@ -1053,7 +1089,7 @@ export const MAX_BATCH = 10_000;
|
|
|
1053
1089
|
/** The result of each write of a batch, in order. */
|
|
1054
1090
|
export type BatchStep = Step<
|
|
1055
1091
|
SymbolRow | RegionRow[] | CommentRow | ReferenceRow | ModuleRow | OperandRow | PatchRow | MacroRow | MacroUseRow | null,
|
|
1056
|
-
SymbolRow | RegionRow[] | OperandRow | PatchRow | MacroRow | MacroUseRow
|
|
1092
|
+
SymbolRow | RegionRow[] | CommentRow | ReferenceRow | ModuleRow | OperandRow | PatchRow | MacroRow | MacroUseRow
|
|
1057
1093
|
>;
|
|
1058
1094
|
|
|
1059
1095
|
/**
|
package/src/mcp/output.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
|
|
8
8
|
import { projectFolder, writeReported, type Written } from "../host-client/transfer.ts";
|
|
9
9
|
|
|
10
|
-
export const OutInput = z.string().min(1).describe("write the
|
|
10
|
+
export const OutInput = z.string().min(1).describe("write the data to this file, relative to the project folder. An existing file is overwritten. The tool description tells what the result then gives");
|
|
11
11
|
|
|
12
12
|
/** The project of a tool with a path. */
|
|
13
13
|
export const ProjectInput = z
|
package/src/mcp/server.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import { formatC64Address } from "../c64.ts";
|
|
11
11
|
import { Byte, TOOL_INPUTS } from "./inputs.ts";
|
|
12
12
|
import { LostBeforeAnswer, type ViceSessionClient } from "../host-client/vice-session.ts";
|
|
13
|
+
import { checkOut } from "../host-client/transfer.ts";
|
|
13
14
|
import { isRefusal } from "../errors.ts";
|
|
14
15
|
import { shownMessage, WireFailure, type WireError } from "../protocol/messages.ts";
|
|
15
16
|
import { COMPARISONS, CONDITION_REGISTERS, MEMORY_VIEWS, SPACES, type Condition } from "../protocol/vice.ts";
|
|
@@ -179,7 +180,10 @@ function errorResult(error: WireError): CallToolResult {
|
|
|
179
180
|
* An unknown field of the tool input also gets the fields that the tool takes.
|
|
180
181
|
*/
|
|
181
182
|
function describeIssues(error: z.ZodError, input?: z.ZodObject): string {
|
|
183
|
+
// An unknown field also breaks the rules that look at the whole object: name only the field.
|
|
184
|
+
const unknown = error.issues.some((issue) => issue.code === "unrecognized_keys");
|
|
182
185
|
return error.issues
|
|
186
|
+
.filter((issue) => !unknown || issue.code !== "custom")
|
|
183
187
|
.map((issue) => {
|
|
184
188
|
const where = issue.path.length === 0 ? "input" : issue.path.join(".");
|
|
185
189
|
const names = input === undefined ? [] : Object.keys(input.shape);
|
|
@@ -229,6 +233,9 @@ async function runCall(
|
|
|
229
233
|
const parsed = tool.inputSchema.safeParse(args ?? {});
|
|
230
234
|
if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error, tool.inputSchema) });
|
|
231
235
|
try {
|
|
236
|
+
// An out path is checked before the tool runs: a refused path must not lose the work or leave its side effects.
|
|
237
|
+
const fields = parsed.data as { out?: unknown; project?: unknown };
|
|
238
|
+
if (typeof fields.out === "string") checkOut(fields.out, typeof fields.project === "string" ? fields.project : undefined);
|
|
232
239
|
let output: unknown;
|
|
233
240
|
try {
|
|
234
241
|
output = await tool.run(parsed.data, await session());
|
package/src/mcp/tools/debug.ts
CHANGED
|
@@ -186,7 +186,8 @@ export const c64Trace = defineTool({
|
|
|
186
186
|
"Each hit gives the instruction (pc, bytes, text) and the registers a, x, y and sp before it. " +
|
|
187
187
|
"point in a hit is the number of its point in points, 0 for the first. " +
|
|
188
188
|
"It also gives the frame from 0, and the raster line and cycle where the instruction started. " +
|
|
189
|
-
"A read or write hit also gives the address that the instruction used, and the byte
|
|
189
|
+
"A read or write hit also gives the address that the instruction used, and value, the byte that it read or wrote. " +
|
|
190
|
+
"At $D000-$DFFF only a load or a store gives value: a read after the access can give another byte. " +
|
|
190
191
|
`The trace ends after frames frames (1 to ${MAX_TRACE_FRAMES}), at maxHits hits (at most ${MAX_TRACE_HITS}), or at a breakpoint, a watchpoint or a JAM. ` +
|
|
191
192
|
`The result lists the first ${TRACE_LISTED} hits. With out, it writes all hits as JSON lines to that project file. ` +
|
|
192
193
|
"Each hit stops the machine for a short time. Thus a point with many hits in each frame makes the trace slow. Use a small range and a maxHits. " +
|
package/src/mcp/tools/media.ts
CHANGED
|
@@ -44,6 +44,10 @@ export const c64Autostart = defineTool({
|
|
|
44
44
|
}),
|
|
45
45
|
readOnly: false,
|
|
46
46
|
async run(input, session) {
|
|
47
|
+
const type = extname(input.path).slice(1).toLowerCase();
|
|
48
|
+
if (input.index !== undefined && !(DISK_TYPES as readonly string[]).includes(type)) {
|
|
49
|
+
throw new WireFailure("invalid-input", `index selects an entry of a disk image (${list(DISK_TYPES)}). A .${type} file has no directory: leave out index.`);
|
|
50
|
+
}
|
|
47
51
|
// VICE numbers the directory entries from 1, and its 0 loads "*".
|
|
48
52
|
return session.autostart({ path: input.path, index: input.index === undefined ? 0 : input.index + 1, run: input.run, root: projectFolder(input.project) });
|
|
49
53
|
},
|
|
@@ -131,7 +135,7 @@ export const c64Snapshot = defineTool({
|
|
|
131
135
|
}
|
|
132
136
|
if (action === "restore" && fields.file !== undefined) {
|
|
133
137
|
requireFields(action, fields, ["file", "project"], ["file"]);
|
|
134
|
-
return session.snapshotRestore(readProjectFile(fields.file, { root: projectFolder(fields.project) }).bytes);
|
|
138
|
+
return session.snapshotRestore(readProjectFile(fields.file, { root: projectFolder(fields.project), what: "file" }).bytes);
|
|
135
139
|
}
|
|
136
140
|
if (action === "save") {
|
|
137
141
|
requireOut(fields);
|
package/src/media.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { Refusal } from "./errors.ts";
|
|
6
6
|
|
|
7
|
+
/** The signature at the start of every VICE snapshot file (.vsf). */
|
|
8
|
+
export const VSF_SIGNATURE = "VICE Snapshot File\x1a";
|
|
9
|
+
|
|
7
10
|
/** The signature at the start of each media type that has one. */
|
|
8
11
|
const SIGNATURES: Readonly<Record<string, readonly string[]>> = {
|
|
9
12
|
crt: ["C64 CARTRIDGE "],
|
|
@@ -11,6 +14,7 @@ const SIGNATURES: Readonly<Record<string, readonly string[]>> = {
|
|
|
11
14
|
tap: ["C64-TAPE-RAW"],
|
|
12
15
|
g64: ["GCR-1541"],
|
|
13
16
|
p00: ["C64File"],
|
|
17
|
+
vsf: [VSF_SIGNATURE],
|
|
14
18
|
};
|
|
15
19
|
|
|
16
20
|
/** True when `bytes` start with a signature of `type`, or `type` has none. */
|
|
@@ -30,11 +34,22 @@ const SIZES: Readonly<Record<string, readonly number[]>> = {
|
|
|
30
34
|
/** The x64 header: 64 bytes that start with these four, then a d64 image. */
|
|
31
35
|
const X64_MAGIC = Buffer.from([0x43, 0x15, 0x41, 0x64]);
|
|
32
36
|
|
|
37
|
+
/** The refusal of a file that is no VICE snapshot file (.vsf): its signature is missing. `path` names the file. */
|
|
38
|
+
export function checkSnapshotFile(path: string, bytes: Uint8Array): void {
|
|
39
|
+
if (!hasSignature("vsf", bytes)) throw new Refusal("invalid-input", `${path} is not a VICE snapshot file (.vsf): it does not start with "VICE Snapshot File". Give a file that c64_snapshot save or a save step of a run wrote.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
/** Refuses a file whose bytes are not of `type` (its extension), with media-error. */
|
|
34
43
|
export function checkMedia(type: string, bytes: Buffer): void {
|
|
35
44
|
const refuse = (why: string) => {
|
|
36
45
|
throw new Refusal("media-error", `The file is not a valid .${type} file: ${why}. Give a file of that type.`);
|
|
37
46
|
};
|
|
47
|
+
if (type === "crt" && hasSignature(type, bytes)) {
|
|
48
|
+
// The header gives its own length; a CHIP packet with the ROM data follows it.
|
|
49
|
+
const header = bytes.length >= 0x40 ? bytes.readUInt32BE(0x10) : 0;
|
|
50
|
+
if (bytes.length < 0x40) refuse("the header is shorter than 64 bytes");
|
|
51
|
+
if (bytes.toString("latin1", header, header + 4) !== "CHIP") refuse("it has no CHIP packet with ROM data after the header");
|
|
52
|
+
}
|
|
38
53
|
if (!hasSignature(type, bytes)) refuse(`it does not start with ${SIGNATURES[type]!.map((signature) => `"${signature.trimEnd()}"`).join(" or ")}`);
|
|
39
54
|
const sizes = SIZES[type];
|
|
40
55
|
if (sizes !== undefined && !sizes.includes(bytes.length)) refuse(`it has ${bytes.length} bytes, and a .${type} image has ${sizes.join(", ")}`);
|
package/src/opcodes.ts
CHANGED
|
@@ -76,6 +76,34 @@ export type UseKind = (typeof USE_KINDS)[number];
|
|
|
76
76
|
const WRITES = new Set(["sta", "stx", "sty", "sax", "sha", "shx", "shy", "tas"]);
|
|
77
77
|
const MODIFIES = new Set(["inc", "dec", "asl", "lsr", "rol", "ror", "slo", "rla", "sre", "rra", "dcp", "isc"]);
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The byte that an instruction wrote to or read from its operand address:
|
|
81
|
+
* a store gives its register, a load the register after it. For another
|
|
82
|
+
* access the byte at the address now (`memoryNow`) is the byte, when the
|
|
83
|
+
* caller can read it back. Undefined when it is not known.
|
|
84
|
+
*/
|
|
85
|
+
export function accessedValue(opcode: number, before: { a: number; x: number; y: number }, after: { a: number; x: number; y: number }, memoryNow: number | undefined): number | undefined {
|
|
86
|
+
switch (OPCODES[opcode]!.mnemonic) {
|
|
87
|
+
case "sta":
|
|
88
|
+
return before.a;
|
|
89
|
+
case "stx":
|
|
90
|
+
return before.x;
|
|
91
|
+
case "sty":
|
|
92
|
+
return before.y;
|
|
93
|
+
case "sax":
|
|
94
|
+
return before.a & before.x;
|
|
95
|
+
case "lda":
|
|
96
|
+
case "lax":
|
|
97
|
+
return after.a;
|
|
98
|
+
case "ldx":
|
|
99
|
+
return after.x;
|
|
100
|
+
case "ldy":
|
|
101
|
+
return after.y;
|
|
102
|
+
default:
|
|
103
|
+
return memoryNow;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
79
107
|
/** The use of the operand address of an instruction that takes an address. Through (zp),y and (zp,x) the address is a pointer. */
|
|
80
108
|
export function useKind(opcode: number): UseKind {
|
|
81
109
|
const { mnemonic, mode } = OPCODES[opcode]!;
|
|
@@ -130,7 +158,9 @@ export function operandNumber(bytes: readonly number[], pc: number): string {
|
|
|
130
158
|
/** The text of the instruction in `bytes` at `pc` as the monitor shows it, for example "STA $d011" or "BNE $c002". */
|
|
131
159
|
export function instructionText(bytes: readonly number[], pc: number): string {
|
|
132
160
|
const { mnemonic } = OPCODES[bytes[0]!]!;
|
|
133
|
-
|
|
161
|
+
const operand = instructionWith(bytes[0]!, operandNumber(bytes, pc)).slice(mnemonic.length);
|
|
162
|
+
// The monitor writes the index register in capitals: LDA ($c1),Y.
|
|
163
|
+
return `${mnemonic.toUpperCase()}${operand.replace(/,([xy])\b/g, (_, index: string) => `,${index.toUpperCase()}`)}`;
|
|
134
164
|
}
|
|
135
165
|
|
|
136
166
|
/**
|
package/src/platform.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Names follow "Mapping the Commodore 64". Only platform facts live here:
|
|
4
4
|
// an address without one belongs to the application.
|
|
5
5
|
|
|
6
|
-
import { d018Offsets, formatC64Address, vicBankBase } from "./c64.ts";
|
|
6
|
+
import { COVERAGE, d018Offsets, formatC64Address, vicBankBase } from "./c64.ts";
|
|
7
7
|
import { Refusal } from "./errors.ts";
|
|
8
8
|
|
|
9
9
|
export interface Entry {
|
|
@@ -420,6 +420,19 @@ export function romUseOfPort(port: number): RomUse {
|
|
|
420
420
|
return { basic: (port & 0x03) === 0x03, kernal: (port & 0x02) !== 0 };
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
/**
|
|
424
|
+
* The ROMs that a program uses, and what tells it: the coverage when the
|
|
425
|
+
* knowledge has it, else $01 of the entry state, else both ROMs. c64-source and
|
|
426
|
+
* c64-static-analysis give the platform names of platformNamesFor of it, so
|
|
427
|
+
* they use the same names.
|
|
428
|
+
*/
|
|
429
|
+
export function romUseOfProgram(coverage: Uint8Array | undefined, state: Record<string, unknown> | undefined): RomUse & { from: "coverage" | "entry state" | "none" } {
|
|
430
|
+
if (coverage !== undefined) return { ...romUseOf(coverage, COVERAGE.romExecute), from: "coverage" };
|
|
431
|
+
const port = (state?.cpuPort as Record<string, unknown> | undefined)?.$01;
|
|
432
|
+
if (typeof port === "number") return { ...romUseOfPort(port), from: "entry state" };
|
|
433
|
+
return { basic: true, kernal: true, from: "none" };
|
|
434
|
+
}
|
|
435
|
+
|
|
423
436
|
export type Decoded = Record<string, unknown>;
|
|
424
437
|
|
|
425
438
|
const bits = (value: number) => [0, 1, 2, 3, 4, 5, 6, 7].filter((bit) => (value & (1 << bit)) !== 0);
|
package/src/project.ts
CHANGED
|
@@ -37,8 +37,13 @@ let chosen: string | undefined;
|
|
|
37
37
|
/** The project that the project lookup found from a working directory, kept for the next call from the same directory. */
|
|
38
38
|
let found: { cwd: string; root: string } | undefined;
|
|
39
39
|
|
|
40
|
+
/** True when `folder` holds .c64-re-tools/. A folder that cannot be read (no permission) is not a project. */
|
|
40
41
|
function isProject(folder: string): boolean {
|
|
41
|
-
|
|
42
|
+
try {
|
|
43
|
+
return statSync(join(folder, PROJECT_DIRECTORY), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
/** `start` and each folder above it, nearest first, below the home folder when `start` is under it. */
|
|
@@ -277,6 +282,8 @@ export function checkOutputPath(path: string, kind: OutputKind, options: { root?
|
|
|
277
282
|
const stat = statSync(real);
|
|
278
283
|
if (existing === target && kind === "file") {
|
|
279
284
|
if (stat.isDirectory()) throw problem({ code: "EISDIR" });
|
|
285
|
+
} else if (existing === target && !stat.isDirectory()) {
|
|
286
|
+
throw new Refusal("invalid-input", `${what === undefined ? "" : `${what}: `}${path} is a file, but ${what ?? "the path"} needs a folder. Give the path of a folder, or a new path for the folder.`);
|
|
280
287
|
} else if (!stat.isDirectory()) {
|
|
281
288
|
throw problem({ code: "ENOTDIR" });
|
|
282
289
|
}
|
package/src/script.ts
CHANGED
|
@@ -428,6 +428,65 @@ function refuseSplitRange(command: CommandSpec, options: Readonly<Record<string,
|
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
/** The options of a command for a message: --a, --b and --c, or a word that it has none. */
|
|
432
|
+
function optionList(options: Readonly<Record<string, OptionSpec>>): string {
|
|
433
|
+
const names = Object.keys(options).map((name) => `--${name}`);
|
|
434
|
+
if (names.length === 0) return "It takes no options";
|
|
435
|
+
return `Its options are ${names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`}`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** True for a word that the command line reads as an option: - and one or more characters. A lone - is a subject (standard input). */
|
|
439
|
+
const isOptionWord = (word: string) => word.length > 1 && word.startsWith("-");
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Refuses the words of a command line that parseArgs refuses, with a message
|
|
443
|
+
* that names the option and tells what to give: an option that the command
|
|
444
|
+
* does not have, a flag with a value, an option without its value, a value
|
|
445
|
+
* that starts with -, and a range option in two parts.
|
|
446
|
+
*/
|
|
447
|
+
function refuseOptionWords(command: CommandSpec, options: Readonly<Record<string, OptionSpec>>, argv: readonly string[]): void {
|
|
448
|
+
const end = argv.indexOf("--");
|
|
449
|
+
const words = end < 0 ? argv : argv.slice(0, end);
|
|
450
|
+
for (let index = 0; index < words.length; index++) {
|
|
451
|
+
const word = words[index]!;
|
|
452
|
+
if (!isOptionWord(word)) continue;
|
|
453
|
+
if (!word.startsWith("--")) {
|
|
454
|
+
throw new UsageError(`${word} is not an option of ${command.name}: an option starts with --. ${optionList(options)}. Give a subject that starts with - after --, for example -- ${word}`);
|
|
455
|
+
}
|
|
456
|
+
const equals = word.indexOf("=");
|
|
457
|
+
const name = equals < 0 ? word.slice(2) : word.slice(2, equals);
|
|
458
|
+
const spec = options[name];
|
|
459
|
+
if (spec === undefined) throw new UsageError(`${command.name} has no option --${name}. ${optionList(options)}`);
|
|
460
|
+
if (spec.kind === "flag") {
|
|
461
|
+
if (equals >= 0) throw new UsageError(`--${name} is a flag: give --${name} without a value`);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
let value: string;
|
|
465
|
+
let valueIndex = index;
|
|
466
|
+
if (equals >= 0) {
|
|
467
|
+
value = word.slice(equals + 1);
|
|
468
|
+
} else {
|
|
469
|
+
valueIndex = index + 1;
|
|
470
|
+
const next = words[valueIndex];
|
|
471
|
+
if (next === undefined) throw new UsageError(`--${name} needs a value: give --${name} ${spec.value ?? "<value>"}`);
|
|
472
|
+
if (isOptionWord(next)) {
|
|
473
|
+
if ((spec.kind === "count" || spec.kind === "number") && /^-\d/.test(next)) {
|
|
474
|
+
const range = spec.kind === "count" ? `a whole number from ${spec.min ?? 0} to ${spec.max ?? Number.MAX_SAFE_INTEGER}` : `a number from ${spec.min ?? 0} to ${spec.max ?? 0xffff}`;
|
|
475
|
+
throw new UsageError(`--${name} must be ${range}, not ${JSON.stringify(next)}`);
|
|
476
|
+
}
|
|
477
|
+
throw new UsageError(`--${name} needs a value: give --${name} ${spec.value ?? "<value>"}, not ${next}. Give a value that starts with - as --${name}=${next}`);
|
|
478
|
+
}
|
|
479
|
+
value = next;
|
|
480
|
+
index++;
|
|
481
|
+
}
|
|
482
|
+
// A range option in two parts: --range '$1bc2' '$1c00'.
|
|
483
|
+
const second = words[valueIndex + 1];
|
|
484
|
+
if (spec.kind === "range" && rangeParts(value) === undefined && isAddressText(value) && second !== undefined && c64AddressOf(second) !== undefined) {
|
|
485
|
+
throw new UsageError(`--${name} is one token <start>-<end>: give --${name} '${value}-${second}', not '${value}' '${second}'`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
431
490
|
/**
|
|
432
491
|
* Refuses an output path before the command does any work: a path that
|
|
433
492
|
* leaves the project, a path in the knowledge, a folder where a file must be,
|
|
@@ -442,6 +501,7 @@ function checkOutput(text: string, what: string, kind: OutputKind): void {
|
|
|
442
501
|
export function parseCall(script: ScriptSpec, command: CommandSpec, argv: readonly string[]): Call {
|
|
443
502
|
const options = commandOptions(command);
|
|
444
503
|
refuseSplitRange(command, options, argv);
|
|
504
|
+
refuseOptionWords(command, options, argv);
|
|
445
505
|
let parsed: ReturnType<typeof parseArgs>;
|
|
446
506
|
try {
|
|
447
507
|
parsed = parseArgs({
|
|
@@ -450,8 +510,9 @@ export function parseCall(script: ScriptSpec, command: CommandSpec, argv: readon
|
|
|
450
510
|
allowPositionals: true,
|
|
451
511
|
options: Object.fromEntries(Object.entries(options).map(([name, spec]) => [name, { type: spec.kind === "flag" ? "boolean" : "string", ...(spec.multiple ? { multiple: true } : {}) }])),
|
|
452
512
|
});
|
|
453
|
-
} catch
|
|
454
|
-
|
|
513
|
+
} catch {
|
|
514
|
+
// refuseOptionWords gives a message of its own for each word that parseArgs refuses; this is the last guard.
|
|
515
|
+
throw new UsageError(`the command line of ${command.name} is not correct. Give each option as --name <value>, each flag as --name, and a subject that starts with - after --`);
|
|
455
516
|
}
|
|
456
517
|
const values = parsed.values as Record<string, string | boolean | string[] | undefined>;
|
|
457
518
|
const subjects = parsed.positionals;
|