layoutbridge-light 0.0.0-stage → 1.0.0

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +26 -0
  3. package/README.de.md +338 -0
  4. package/README.md +325 -2
  5. package/THIRD-PARTY-NOTICES.md +2878 -0
  6. package/dist/src/mcp/catalog.js +126 -0
  7. package/dist/src/mcp/config.js +211 -0
  8. package/dist/src/mcp/dispatch.js +164 -0
  9. package/dist/src/mcp/epub-check.js +331 -0
  10. package/dist/src/mcp/epub-orientation.js +132 -0
  11. package/dist/src/mcp/license/free-tools.js +16 -0
  12. package/dist/src/mcp/license/gate.js +210 -0
  13. package/dist/src/mcp/license/index.js +107 -0
  14. package/dist/src/mcp/license/instance-name.js +57 -0
  15. package/dist/src/mcp/license/key-source.js +59 -0
  16. package/dist/src/mcp/license/lemonsqueezy-verifier.js +207 -0
  17. package/dist/src/mcp/license/placeholder-verifier.js +10 -0
  18. package/dist/src/mcp/license/polar-ids.js +100 -0
  19. package/dist/src/mcp/license/polar-verifier.js +285 -0
  20. package/dist/src/mcp/license/product-ids.js +73 -0
  21. package/dist/src/mcp/license/state-store.js +91 -0
  22. package/dist/src/mcp/license/types.js +11 -0
  23. package/dist/src/mcp/logging.js +271 -0
  24. package/dist/src/mcp/product-info.js +54 -0
  25. package/dist/src/mcp/product.js +56 -0
  26. package/dist/src/mcp/server.js +253 -0
  27. package/dist/src/mcp/setup.js +680 -0
  28. package/dist/src/mcp/start.cjs +117 -0
  29. package/dist/src/mcp/tool-definition.js +2 -0
  30. package/dist/src/mcp/tools.js +2425 -0
  31. package/dist/src/mcp/typography-rules.js +70 -0
  32. package/dist/src/transport/com.js +250 -0
  33. package/dist/src/transport/osascript.js +171 -0
  34. package/dist/src/transport/queue.js +19 -0
  35. package/dist/src/transport/script-bundle.js +92 -0
  36. package/dist/src/transport/transport.js +30 -0
  37. package/indesign/commands/10-app.js +6 -0
  38. package/indesign/commands/20-documents.js +517 -0
  39. package/indesign/commands/22-document-setup.js +204 -0
  40. package/indesign/commands/23-save-package.js +258 -0
  41. package/indesign/commands/25-export.js +448 -0
  42. package/indesign/commands/26-export-image.js +202 -0
  43. package/indesign/commands/27-textdefaults.js +151 -0
  44. package/indesign/commands/28-selection.js +174 -0
  45. package/indesign/commands/30-pages.js +1032 -0
  46. package/indesign/commands/31-view.js +210 -0
  47. package/indesign/commands/32-sections.js +203 -0
  48. package/indesign/commands/35-parents.js +428 -0
  49. package/indesign/commands/37-layout.js +837 -0
  50. package/indesign/commands/40-frames.js +851 -0
  51. package/indesign/commands/41-shapes.js +505 -0
  52. package/indesign/commands/42-groups.js +117 -0
  53. package/indesign/commands/43-layers.js +310 -0
  54. package/indesign/commands/44-effects.js +274 -0
  55. package/indesign/commands/45-frame-options.js +247 -0
  56. package/indesign/commands/47-anchored.js +331 -0
  57. package/indesign/commands/50-modify.js +781 -0
  58. package/indesign/commands/52-align.js +139 -0
  59. package/indesign/commands/55-fit.js +106 -0
  60. package/indesign/commands/60-images.js +1128 -0
  61. package/indesign/commands/62-embed.js +120 -0
  62. package/indesign/commands/69-placeholders.js +127 -0
  63. package/indesign/commands/70-text.js +675 -0
  64. package/indesign/commands/71-outlines.js +92 -0
  65. package/indesign/commands/72-place-text.js +476 -0
  66. package/indesign/commands/73-story-lock.js +157 -0
  67. package/indesign/commands/74-stories.js +121 -0
  68. package/indesign/commands/75-tables.js +676 -0
  69. package/indesign/commands/76-text-variables.js +260 -0
  70. package/indesign/commands/77-story-options.js +86 -0
  71. package/indesign/commands/78-frame-lines.js +452 -0
  72. package/indesign/commands/79-check-typography.js +341 -0
  73. package/indesign/commands/80-styles.js +1865 -0
  74. package/indesign/commands/81-composition.js +355 -0
  75. package/indesign/commands/82-object-styles.js +263 -0
  76. package/indesign/commands/83-drop-caps.js +87 -0
  77. package/indesign/commands/83-indents-tabs.js +264 -0
  78. package/indesign/commands/84-import-styles.js +142 -0
  79. package/indesign/commands/85-swatches.js +112 -0
  80. package/indesign/commands/86-gradients.js +185 -0
  81. package/indesign/commands/87-replace-swatch.js +180 -0
  82. package/indesign/commands/88-replace-font.js +212 -0
  83. package/indesign/commands/89-toc-index.js +945 -0
  84. package/indesign/commands/90-print-on-demand.js +495 -0
  85. package/indesign/commands/91-epub.js +546 -0
  86. package/indesign/interpreter.js +779 -0
  87. package/package.json +41 -4
  88. package/rules/typography-rules.json +31 -0
@@ -0,0 +1,126 @@
1
+ import { PRODUCT } from './product.js';
2
+ import { TOOLS } from './tools.js';
3
+ /** Die 77 Werkzeuge von LayoutBridge Light (Wahl Sascha 30.10.2026, a), in der Reihenfolge des Katalogs. */
4
+ export const LIGHT_TOOLS = [
5
+ 'get_app_version', 'list_documents', 'open_document', 'close_document', 'create_document', 'save_document',
6
+ 'export_pdf', 'preflight_document', 'document_text_defaults', 'set_document_bleed',
7
+ 'list_pages', 'get_page_state', 'add_page', 'delete_page', 'list_parent_pages', 'create_parent_page',
8
+ 'apply_parent_to_page', 'insert_page_number_marker',
9
+ 'get_layout_grid', 'set_layout_grid',
10
+ 'create_text_frame', 'create_image_frame', 'create_rectangle', 'delete_frame',
11
+ 'set_frame_bounds', 'set_frame_appearance', 'set_frame_columns', 'set_frame_inset', 'set_text_wrap', 'thread_text_frames',
12
+ 'place_image', 'list_links', 'update_links',
13
+ 'get_frame_text', 'set_text', 'set_text_in_range', 'find_replace',
14
+ 'list_paragraph_styles', 'define_paragraph_style', 'define_character_style', 'apply_paragraph_style_to_range', 'apply_character_style_to_range', 'apply_character_format_to_range',
15
+ 'set_paragraph_options', 'get_font_info_at_index', 'find_font_usage', 'debug_inspect_run', 'create_swatch',
16
+ 'get_frame_options', 'list_swatches', 'export_page_image', 'place_text_file', 'fit_frame',
17
+ 'list_stories', 'anchor_frame_in_text', 'list_sections', 'set_section',
18
+ 'set_document_setup', 'list_text_variables', 'create_text_variable', 'insert_text_variable',
19
+ 'save_document_as', 'set_story_options', 'get_frame_lines', 'check_typography',
20
+ 'position_image_in_frame', 'list_toc_styles', 'define_toc_style', 'create_toc', 'add_index_entry', 'list_index', 'generate_index',
21
+ 'check_print_on_demand', 'create_book_cover', 'export_epub', 'check_epub', 'autoflow_story',
22
+ ];
23
+ /** Die 46 Werkzeuge, die Light ausdruecklich nicht hat. */
24
+ export const LIGHT_EXCLUDED = [
25
+ /* Gestaltung */
26
+ 'create_ellipse', 'create_polygon', 'set_corner_options', 'create_gradient_swatch', 'set_frame_effects',
27
+ 'convert_text_to_outlines',
28
+ /* Fremddokumente und Verknuepfungen */
29
+ 'replace_font', 'replace_swatch', 'embed_link', 'unembed_link', 'delete_link', 'list_pasteboard_items',
30
+ /* Ebenen, Gruppen, Objektformate */
31
+ 'list_layers', 'set_frame_layer', 'set_layer_visibility', 'group_frames', 'ungroup_frame',
32
+ 'list_object_styles', 'apply_object_style',
33
+ /* Heftlayout */
34
+ 'move_pages', 'move_pages_to_new_spread', 'move_frame_to_page', 'set_frame_rotation', 'set_frame_stacking',
35
+ 'align_objects', 'list_guides', 'delete_guides', 'clear_frame_content', 'get_selection', 'set_selection',
36
+ /* Tabellen */
37
+ 'create_table', 'set_table_cells', 'set_table_style', 'get_table',
38
+ /* Im Buchsatz mit Umweg erledigt (Wahl Sascha 30.10.2026, a) */
39
+ 'duplicate_parent_page', 'override_parent_item_on_page', 'show_page', 'duplicate_frame', 'create_line',
40
+ 'unanchor_frame', 'import_styles', 'package_document', 'relink_links', 'get_image_link_info',
41
+ 'scale_image_in_frame', 'apply_paragraph_style',
42
+ ];
43
+ /**
44
+ * Gezielte Ersetzungen fuer Light: Halbsaetze, die auf ein fehlendes
45
+ * Werkzeug verweisen, werden gekuerzt, der Rest des Satzes bleibt. Jede
46
+ * Ersetzung muss im Katalog genau ihren Text finden (Waechtertest) – aendert
47
+ * sich eine Beschreibung des Vollprodukts, faellt das auf.
48
+ */
49
+ export const LIGHT_TEXT_REPLACEMENTS = [
50
+ [' (Ebene; see list_layers for locked or hidden layers)', ' (Ebene)'],
51
+ ['; repair with move_pages_to_new_spread)', ')'],
52
+ [' (see list_swatches, create_gradient_swatch)', ' (see list_swatches)'],
53
+ ['Existing layer to create the object on (see list_layers).', 'Existing layer to create the object on.'],
54
+ [' Per-corner settings: set_corner_options.', ''],
55
+ [' – to put it on another page use move_frame_to_page', ''],
56
+ [' (their graphics would stay behind) – use move_frame_to_page.', ' (their graphics would stay behind).'],
57
+ [' in its rotated space; set set_frame_rotation 0 first.', ' in its rotated space.'],
58
+ ["(print: the document's CMYK swatches, see list_swatches, create_gradient_swatch)", "(print: the document's CMYK swatches, see list_swatches)"],
59
+ [' – delete_link removes it, list_pasteboard_items lists all pasteboard objects.', '.'],
60
+ ['exact name from list_layers.', 'exact name of an existing layer.'],
61
+ [', or call show_page.', '.'],
62
+ [' to stay on the master, or call show_page with one of the page_ids to work on it visibly.', ' to stay on the master.'],
63
+ [' to switch it off, or build borders from four create_line calls.', ' to switch it off.'],
64
+ [' – use scale_image_in_frame or fit_frame when', ' – use fit_frame when'],
65
+ [' – to merely swap the file use relink_links)', ')'],
66
+ ['check get_image_link_info before repeating.', 'check get_page_state before repeating.'],
67
+ ['To change the size use scale_image_in_frame or fit_frame.', 'To change the size use fit_frame.'],
68
+ ['(create_line, create_rectangle, set_frame_appearance,', '(create_rectangle, set_frame_appearance,'],
69
+ ];
70
+ /** Einen Text in Saetze teilen (Satzende . ! ? gefolgt von Leerraum). */
71
+ function sentences(text) {
72
+ return text.split(/(?<=[.!?])\s+/);
73
+ }
74
+ export function mentionsAny(text, names) {
75
+ return names.some((name) => new RegExp('(^|[^A-Za-z0-9_])' + name + '([^A-Za-z0-9_]|$)').test(text));
76
+ }
77
+ /**
78
+ * Verweise auf fehlende Werkzeuge entfernen: zuerst die gezielten
79
+ * Ersetzungen, danach als Rueckfall ganze Saetze, die noch eines nennen.
80
+ * Bleibt nichts uebrig, gilt der Originaltext (dann schlaegt der
81
+ * Waechtertest an).
82
+ */
83
+ export function withoutMentions(text, names) {
84
+ if (!mentionsAny(text, names))
85
+ return text;
86
+ let result = text;
87
+ for (const [from, to] of LIGHT_TEXT_REPLACEMENTS)
88
+ result = result.split(from).join(to);
89
+ if (!mentionsAny(result, names))
90
+ return result;
91
+ const kept = sentences(result).filter((sentence) => !mentionsAny(sentence, names));
92
+ return kept.length === 0 ? text : kept.join(' ');
93
+ }
94
+ /** Ein Parameterschema mit neuer Beschreibung; das Original bleibt unberuehrt. */
95
+ function describedAs(schema, description) {
96
+ return schema.describe(description);
97
+ }
98
+ /** Eine Kopie der Definition, deren Texte keines der fehlenden Werkzeuge nennen. */
99
+ export function lightDefinition(definition, missing = LIGHT_EXCLUDED) {
100
+ let shapeChanged = false;
101
+ const shape = {};
102
+ for (const [key, schema] of Object.entries(definition.inputShape)) {
103
+ const current = schema.description;
104
+ if (typeof current === 'string' && mentionsAny(current, missing)) {
105
+ shape[key] = describedAs(schema, withoutMentions(current, missing));
106
+ shapeChanged = true;
107
+ }
108
+ else {
109
+ shape[key] = schema;
110
+ }
111
+ }
112
+ const description = withoutMentions(definition.description, missing);
113
+ if (!shapeChanged && description === definition.description)
114
+ return definition;
115
+ return { ...definition, description, inputShape: shapeChanged ? shape : definition.inputShape };
116
+ }
117
+ /** Der Katalog eines Produkts. */
118
+ export function catalogFor(product = PRODUCT, tools = TOOLS) {
119
+ if (product.id !== 'light')
120
+ return [...tools];
121
+ const allowed = new Set(LIGHT_TOOLS);
122
+ return tools.filter((tool) => allowed.has(tool.name)).map((tool) => lightDefinition(tool));
123
+ }
124
+ /** Der Katalog dieses Pakets. */
125
+ export const CATALOG = catalogFor();
126
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Serverkonfiguration (E-54): Hauswerte, die nicht in die Werkzeug-
3
+ * beschreibungen gehoeren – Toleranzen, Preset-Namen, Schwellen. Eine
4
+ * JSON-Datei, einmal beim Start gelesen. Ein Parameter im Aufruf geht
5
+ * immer vor einem konfigurierten Wert.
6
+ *
7
+ * Ort der Datei: Umgebungsvariable LAYOUTBRIDGE_SPM_CONFIG (voller Pfad),
8
+ * sonst macOS ~/Library/Application Support/layoutbridge-spm/config.json,
9
+ * sonst ~/.layoutbridge-spm/config.json. Fehlt die Datei, gilt keine
10
+ * Konfiguration – der Server laeuft mit den neutralen Vorgaben.
11
+ */
12
+ import { readFileSync } from 'node:fs';
13
+ import { homedir, platform } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { envName, PRODUCT } from './product.js';
16
+ const KNOWN_KEYS = ['tracking_warn_beyond', 'min_ppi', 'export_pdf', 'print_on_demand', 'epub'];
17
+ const POD_NUMBER_KEYS = [
18
+ ['min_page_count', 1], ['max_page_count', 1], ['page_count_multiple', 1], ['min_outside_margin_mm', 0], ['bleed_mm', 0],
19
+ ['min_font_size_pt', 0], ['spine_per_page_mm', 0], ['safety_margin_mm', 0], ['spine_safety_mm', 0],
20
+ ];
21
+ const KNOWN_POD_KEYS = [...POD_NUMBER_KEYS.map(([key]) => key), 'inside_margin_tiers', 'barcode_area'];
22
+ const BARCODE_KEYS = ['width_mm', 'height_mm', 'offset_x_mm', 'offset_y_mm'];
23
+ const KNOWN_PDF_KEYS = ['preset', 'fallback_preset', 'fallback_over_bytes'];
24
+ export function defaultConfigPath(env = process.env, os = platform(), home = homedir(), product = PRODUCT) {
25
+ const fromEnv = env[envName('CONFIG', product)];
26
+ if (fromEnv !== undefined && fromEnv.length > 0)
27
+ return fromEnv;
28
+ if (os === 'darwin') {
29
+ return join(home, 'Library', 'Application Support', product.dataDirName, 'config.json');
30
+ }
31
+ return join(home, '.' + product.dataDirName, 'config.json');
32
+ }
33
+ function isRecord(value) {
34
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
35
+ }
36
+ function takeNumber(source, key, min, problems, prefix) {
37
+ const value = source[key];
38
+ if (value === undefined)
39
+ return undefined;
40
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < min) {
41
+ problems.push(`${prefix}${key}: expected a number of ${min} or more, got ${JSON.stringify(value)}`);
42
+ return undefined;
43
+ }
44
+ return value;
45
+ }
46
+ function takeString(source, key, problems, prefix) {
47
+ const value = source[key];
48
+ if (value === undefined)
49
+ return undefined;
50
+ if (typeof value !== 'string') {
51
+ problems.push(`${prefix}${key}: expected a string, got ${JSON.stringify(value)}`);
52
+ return undefined;
53
+ }
54
+ return value;
55
+ }
56
+ /** Text einer Konfigurationsdatei pruefen. Unbekannte Schluessel werden gemeldet und ignoriert. */
57
+ export function parseServerConfig(text) {
58
+ const problems = [];
59
+ let raw;
60
+ try {
61
+ raw = JSON.parse(text);
62
+ }
63
+ catch (error) {
64
+ return { config: {}, problems: ['not valid JSON: ' + String(error)] };
65
+ }
66
+ if (!isRecord(raw)) {
67
+ return { config: {}, problems: ['the top level must be a JSON object'] };
68
+ }
69
+ const config = {};
70
+ for (const key of Object.keys(raw)) {
71
+ if (!KNOWN_KEYS.includes(key))
72
+ problems.push(`unknown key "${key}" ignored`);
73
+ }
74
+ const tracking = takeNumber(raw, 'tracking_warn_beyond', 0, problems, '');
75
+ if (tracking !== undefined)
76
+ config.tracking_warn_beyond = tracking;
77
+ const minPpi = takeNumber(raw, 'min_ppi', 1, problems, '');
78
+ if (minPpi !== undefined)
79
+ config.min_ppi = minPpi;
80
+ if (raw['export_pdf'] !== undefined) {
81
+ const pdf = raw['export_pdf'];
82
+ if (!isRecord(pdf)) {
83
+ problems.push('export_pdf: expected an object');
84
+ }
85
+ else {
86
+ for (const key of Object.keys(pdf)) {
87
+ if (!KNOWN_PDF_KEYS.includes(key))
88
+ problems.push(`unknown key "export_pdf.${key}" ignored`);
89
+ }
90
+ const out = {};
91
+ const preset = takeString(pdf, 'preset', problems, 'export_pdf.');
92
+ if (preset !== undefined)
93
+ out.preset = preset;
94
+ const fallback = takeString(pdf, 'fallback_preset', problems, 'export_pdf.');
95
+ if (fallback !== undefined)
96
+ out.fallback_preset = fallback;
97
+ const bytes = takeNumber(pdf, 'fallback_over_bytes', 0, problems, 'export_pdf.');
98
+ if (bytes !== undefined)
99
+ out.fallback_over_bytes = bytes;
100
+ config.export_pdf = out;
101
+ }
102
+ }
103
+ if (raw['print_on_demand'] !== undefined) {
104
+ const pod = parsePrintOnDemand(raw['print_on_demand'], problems);
105
+ if (pod !== undefined)
106
+ config.print_on_demand = pod;
107
+ }
108
+ if (raw['epub'] !== undefined) {
109
+ const epub = raw['epub'];
110
+ if (!isRecord(epub)) {
111
+ problems.push('epub: expected an object');
112
+ }
113
+ else {
114
+ for (const key of Object.keys(epub)) {
115
+ if (key !== 'min_ppi')
116
+ problems.push(`unknown key "epub.${key}" ignored`);
117
+ }
118
+ const minPpi = takeNumber(epub, 'min_ppi', 1, problems, 'epub.');
119
+ config.epub = minPpi === undefined ? {} : { min_ppi: minPpi };
120
+ }
121
+ }
122
+ return { config, problems };
123
+ }
124
+ function parsePrintOnDemand(value, problems) {
125
+ const prefix = 'print_on_demand.';
126
+ if (!isRecord(value)) {
127
+ problems.push('print_on_demand: expected an object');
128
+ return undefined;
129
+ }
130
+ for (const key of Object.keys(value)) {
131
+ if (!KNOWN_POD_KEYS.includes(key))
132
+ problems.push(`unknown key "${prefix}${key}" ignored`);
133
+ }
134
+ const out = {};
135
+ for (const [key, min] of POD_NUMBER_KEYS) {
136
+ const n = takeNumber(value, key, min, problems, prefix);
137
+ if (n !== undefined)
138
+ out[key] = n;
139
+ }
140
+ if (value['inside_margin_tiers'] !== undefined) {
141
+ const tiers = value['inside_margin_tiers'];
142
+ const parsed = [];
143
+ if (!Array.isArray(tiers)) {
144
+ problems.push(`${prefix}inside_margin_tiers: expected an array of {max_pages, min_mm}`);
145
+ }
146
+ else {
147
+ tiers.forEach((tier, index) => {
148
+ const at = `${prefix}inside_margin_tiers[${index}].`;
149
+ if (!isRecord(tier)) {
150
+ problems.push(`${at.slice(0, -1)}: expected an object {max_pages, min_mm}`);
151
+ return;
152
+ }
153
+ const maxPages = takeNumber(tier, 'max_pages', 1, problems, at);
154
+ const minMm = takeNumber(tier, 'min_mm', 0, problems, at);
155
+ if (maxPages !== undefined && minMm !== undefined)
156
+ parsed.push({ max_pages: maxPages, min_mm: minMm });
157
+ else if (tier['max_pages'] === undefined || tier['min_mm'] === undefined)
158
+ problems.push(`${at}: max_pages and min_mm are both required`);
159
+ });
160
+ if (parsed.length > 0)
161
+ out.inside_margin_tiers = parsed;
162
+ }
163
+ }
164
+ if (value['barcode_area'] !== undefined) {
165
+ const area = value['barcode_area'];
166
+ if (!isRecord(area)) {
167
+ problems.push(`${prefix}barcode_area: expected an object {width_mm, height_mm, offset_x_mm, offset_y_mm}`);
168
+ }
169
+ else {
170
+ const parsed = {};
171
+ for (const key of BARCODE_KEYS) {
172
+ const n = takeNumber(area, key, 0, problems, `${prefix}barcode_area.`);
173
+ if (n !== undefined)
174
+ parsed[key] = n;
175
+ }
176
+ if (BARCODE_KEYS.every((key) => parsed[key] !== undefined))
177
+ out.barcode_area = parsed;
178
+ else
179
+ problems.push(`${prefix}barcode_area: width_mm, height_mm, offset_x_mm and offset_y_mm are all required`);
180
+ }
181
+ }
182
+ return out;
183
+ }
184
+ /** Konfigurationsdatei lesen. Keine Datei = leere Konfiguration, kein Fehler. */
185
+ export function loadServerConfig(path = defaultConfigPath()) {
186
+ let text;
187
+ try {
188
+ text = readFileSync(path, 'utf8');
189
+ }
190
+ catch (error) {
191
+ const code = error.code;
192
+ if (code === 'ENOENT')
193
+ return { config: {}, path: null, problems: [] };
194
+ return { config: {}, path, problems: ['could not read ' + path + ': ' + String(error)] };
195
+ }
196
+ const parsed = parseServerConfig(text);
197
+ return { config: parsed.config, path, problems: parsed.problems };
198
+ }
199
+ /**
200
+ * Konfigurierte Vorgaben in die Befehlsargumente legen – nur dort, wo der
201
+ * Aufruf selbst nichts gesagt hat. Felder ohne Wert werden nicht gesetzt.
202
+ */
203
+ export function applyConfigDefaults(args, defaults) {
204
+ const out = { ...args };
205
+ for (const key of Object.keys(defaults)) {
206
+ if (out[key] === undefined && defaults[key] !== undefined)
207
+ out[key] = defaults[key];
208
+ }
209
+ return out;
210
+ }
211
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Bindeglied: geprueften Parametersatz in einen Befehl uebersetzen, ueber den
3
+ * Transport schicken, protokollieren, Ergebnisumschlag zurueckgeben.
4
+ * Kennt InDesign nicht, kennt den Transport nur als Schnittstelle.
5
+ */
6
+ import { homedir } from 'node:os';
7
+ import { z } from 'zod';
8
+ import { applyConfigDefaults } from './config.js';
9
+ import { maskHome } from './logging.js';
10
+ import { transportError, } from '../transport/transport.js';
11
+ /** Entfernt Felder ohne Wert, damit der Befehl keine leeren Schluessel traegt. */
12
+ export function dropUndefined(input) {
13
+ const out = {};
14
+ for (const key of Object.keys(input)) {
15
+ if (input[key] !== undefined)
16
+ out[key] = input[key];
17
+ }
18
+ return out;
19
+ }
20
+ /** Parameter pruefen und daraus den Befehl bauen – ohne InDesign. */
21
+ export function buildCommand(definition, rawInput, config = {}) {
22
+ /*
23
+ * strictObject: unbekannte Parameter sind ein Fehler, kein stilles Weglassen.
24
+ * Sonst wuerde z. B. add_page mit "position" statt "at" die Seite still ans
25
+ * Ende haengen (Spezifikation nennt die verwechselbaren Namen ausdruecklich).
26
+ */
27
+ const schema = z.strictObject(definition.inputShape);
28
+ const parsed = schema.safeParse(rawInput ?? {});
29
+ if (!parsed.success) {
30
+ const issues = parsed.error.issues.map((issue) => (issue.path.length > 0 ? issue.path.join('.') + ': ' : '') + issue.message);
31
+ return {
32
+ error: transportError('invalid_argument', issues.join('; '), { tool: definition.name, issues }),
33
+ };
34
+ }
35
+ const values = dropUndefined(parsed.data);
36
+ let args = definition.buildArgs ? definition.buildArgs(values) : values;
37
+ /* Konfigurierte Vorgaben erst nach der Pruefung: der Aufruf geht vor (E-54). */
38
+ if (definition.configArgs)
39
+ args = applyConfigDefaults(args, definition.configArgs(config));
40
+ return { command: { cmd: definition.command, args: dropUndefined(args) } };
41
+ }
42
+ /**
43
+ * Ersetzt das Benutzerverzeichnis in der Fehlerantwort durch "~" – in
44
+ * `error.message` und in `error.details` (Auftrag A-8, Punkt 4; Entscheidung
45
+ * des Vermarktungsprojekts vom 14.09.2026, Vermerk E-68). Der Pfad bleibt
46
+ * lesbar, allein der Benutzername faellt weg. Aufbau und Felder der
47
+ * Fehlerantwort bleiben unveraendert (A-5).
48
+ */
49
+ export function maskErrorPaths(result, home = homedir()) {
50
+ if (result.error === undefined)
51
+ return result;
52
+ return {
53
+ ...result,
54
+ error: {
55
+ kind: result.error.kind,
56
+ message: maskHome(result.error.message, home),
57
+ details: maskDeep(result.error.details, home, 0),
58
+ },
59
+ };
60
+ }
61
+ /** Wie maskErrorPaths, aber fuer beliebig verschachtelte Details. */
62
+ function maskDeep(value, home, depth) {
63
+ if (typeof value === 'string')
64
+ return maskHome(value, home);
65
+ if (depth >= 6)
66
+ return value;
67
+ if (Array.isArray(value))
68
+ return value.map((item) => maskDeep(item, home, depth + 1));
69
+ if (value !== null && typeof value === 'object') {
70
+ const out = {};
71
+ for (const key of Object.keys(value)) {
72
+ out[key] = maskDeep(value[key], home, depth + 1);
73
+ }
74
+ return out;
75
+ }
76
+ return value;
77
+ }
78
+ /**
79
+ * Sitzung 38 (A-22): Pfade der Redaktionsdateien (`managed_file`, in
80
+ * list_stories, get_frame_text, find_replace.skipped_locked[]) mit "~" statt
81
+ * des Benutzerverzeichnisses – wie die Fehlerantworten (E-68). Nur dieses
82
+ * Feld; andere Felder (Text, Pfade anderer Werkzeuge) bleiben unveraendert.
83
+ */
84
+ export function maskManagedFiles(result, home = homedir()) {
85
+ return maskManagedDeep(result, home, 0);
86
+ }
87
+ function maskManagedDeep(value, home, depth) {
88
+ if (depth >= 6)
89
+ return value;
90
+ if (Array.isArray(value))
91
+ return value.map((item) => maskManagedDeep(item, home, depth + 1));
92
+ if (value !== null && typeof value === 'object') {
93
+ const out = {};
94
+ for (const key of Object.keys(value)) {
95
+ const entry = value[key];
96
+ out[key] = key === 'managed_file' && typeof entry === 'string' ? maskHome(entry, home) : maskManagedDeep(entry, home, depth + 1);
97
+ }
98
+ return out;
99
+ }
100
+ return value;
101
+ }
102
+ /** Ein Werkzeugaufruf von Anfang bis Ende. */
103
+ export async function runTool(definition, rawInput, transport, logger, options = {}) {
104
+ const built = buildCommand(definition, rawInput, options.config ?? {});
105
+ if ('error' in built) {
106
+ const masked = maskErrorPaths(built.error);
107
+ logger.log({
108
+ tool: definition.name,
109
+ command: definition.command,
110
+ args: {},
111
+ duration_ms: 0,
112
+ ok: false,
113
+ error_kind: masked.error?.kind ?? 'invalid_argument',
114
+ error_message: masked.error?.message ?? '',
115
+ warnings: [],
116
+ result_keys: [],
117
+ });
118
+ return masked;
119
+ }
120
+ const startedAt = Date.now();
121
+ let result;
122
+ try {
123
+ /* Sitzung 60 (E-102): Werkzeuge mit lokalem Teil antworten ohne InDesign, wenn local ein Ergebnis liefert. */
124
+ const local = definition.local ? definition.local(built.command.args) : undefined;
125
+ if (local !== undefined) {
126
+ result = local;
127
+ }
128
+ else {
129
+ const sendOptions = options.timeoutMs === undefined ? undefined : { timeoutMs: options.timeoutMs };
130
+ result = await transport.send(built.command, sendOptions);
131
+ }
132
+ }
133
+ catch (error) {
134
+ result = transportError('script_error', 'Transport failed: ' + String(error), null);
135
+ }
136
+ const duration = Date.now() - startedAt;
137
+ if (result.ok && definition.after) {
138
+ try {
139
+ result = definition.after(built.command.args, result);
140
+ }
141
+ catch (error) {
142
+ result = { ...result, warnings: [...(Array.isArray(result.warnings) ? result.warnings : []), 'Post-processing failed: ' + String(error)] };
143
+ }
144
+ }
145
+ result = maskErrorPaths(result);
146
+ if (result.ok)
147
+ result = maskManagedFiles(result);
148
+ if (result.ok && definition.mapResult) {
149
+ result = { ...result, ...definition.mapResult(result) };
150
+ }
151
+ logger.log({
152
+ tool: definition.name,
153
+ command: built.command.cmd,
154
+ args: built.command.args,
155
+ duration_ms: duration,
156
+ ok: result.ok === true,
157
+ error_kind: result.error?.kind ?? null,
158
+ error_message: result.error?.message ?? null,
159
+ warnings: Array.isArray(result.warnings) ? result.warnings : [],
160
+ result_keys: Object.keys(result).filter((key) => key !== 'ok' && key !== 'warnings'),
161
+ });
162
+ return result;
163
+ }
164
+ //# sourceMappingURL=dispatch.js.map