@worca/app 1.3.0 → 1.4.0-rc.1

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 (187) hide show
  1. package/README.md +85 -6
  2. package/agents/clarify.meta.json +1 -0
  3. package/agents/memoryDefragmenter.meta.json +2 -1
  4. package/agents/reviewer.meta.json +60 -0
  5. package/agents/worca-cc-code-reviewer.md +33 -0
  6. package/agents/worca-cc-memory-defragmenter.md +5 -3
  7. package/agents/workspaceScanner.meta.json +1 -0
  8. package/package.json +14 -10
  9. package/scripts/git-diff.mjs +25 -0
  10. package/scripts/gitDiff.meta.json +18 -0
  11. package/scripts/js-inline.mjs +11 -0
  12. package/scripts/js.meta.json +22 -0
  13. package/scripts/py-inline.py +27 -0
  14. package/scripts/py.meta.json +22 -0
  15. package/scripts/shell.meta.json +24 -0
  16. package/skills/worca/SKILL.md +3 -2
  17. package/src/cli/models.mjs +247 -0
  18. package/src/cli/render.mjs +72 -4
  19. package/src/cli/schedule.mjs +494 -0
  20. package/src/cli/worca-cc.mjs +1001 -22
  21. package/src/core/agent-registry.mjs +75 -23
  22. package/src/core/agent-store.mjs +51 -2
  23. package/src/core/artifacts.mjs +73 -10
  24. package/src/core/ask/events.mjs +119 -1
  25. package/src/core/ask/limits.mjs +32 -4
  26. package/src/core/ask/mcp-stdio.mjs +12 -0
  27. package/src/core/ask/model-deps.mjs +126 -0
  28. package/src/core/ask/model-proposal.mjs +370 -0
  29. package/src/core/ask/models.mjs +12 -0
  30. package/src/core/ask/policy-deps.mjs +124 -0
  31. package/src/core/ask/policy-proposal.mjs +363 -0
  32. package/src/core/ask/prompt.mjs +74 -9
  33. package/src/core/ask/proposal.mjs +54 -5
  34. package/src/core/ask/schedule-deps.mjs +83 -0
  35. package/src/core/ask/schedule-spec.mjs +310 -0
  36. package/src/core/ask/script-deps.mjs +357 -0
  37. package/src/core/ask/source-deps.mjs +52 -0
  38. package/src/core/ask/source-spec.mjs +157 -0
  39. package/src/core/ask/spawn.mjs +1 -0
  40. package/src/core/ask/store.mjs +6 -3
  41. package/src/core/ask/tool-deps.mjs +4 -0
  42. package/src/core/ask/tools.mjs +657 -37
  43. package/src/core/ask/turn.mjs +109 -2
  44. package/src/core/ask-files.mjs +406 -0
  45. package/src/core/ask-forms.mjs +195 -0
  46. package/src/core/ask-projection.mjs +72 -0
  47. package/src/core/bridge/errors.mjs +84 -0
  48. package/src/core/bridge/provider-ops.mjs +281 -0
  49. package/src/core/bridge/providers/copilot.mjs +269 -0
  50. package/src/core/bridge/providers/endpoint.mjs +257 -0
  51. package/src/core/bridge/registry.mjs +88 -0
  52. package/src/core/bridge/semaphore.mjs +73 -0
  53. package/src/core/bridge/server.mjs +184 -0
  54. package/src/core/bridge/telemetry.mjs +53 -0
  55. package/src/core/bridge/translate/request.mjs +252 -0
  56. package/src/core/bridge/translate/response.mjs +82 -0
  57. package/src/core/bridge/translate/stream.mjs +242 -0
  58. package/src/core/bridge/upstream.mjs +209 -0
  59. package/src/core/chat/command-router.mjs +58 -4
  60. package/src/core/chat/notifier.mjs +14 -1
  61. package/src/core/chat/renderers.mjs +35 -0
  62. package/src/core/claude-runner.mjs +126 -19
  63. package/src/core/config.mjs +212 -31
  64. package/src/core/cost-budget.mjs +3 -2
  65. package/src/core/db.mjs +169 -15
  66. package/src/core/failure-policy.mjs +10 -0
  67. package/src/core/fs-browse.mjs +16 -4
  68. package/src/core/git-info.mjs +22 -0
  69. package/src/core/graph/builtin-workflows.mjs +3 -1
  70. package/src/core/graph/exec-io.mjs +71 -0
  71. package/src/core/graph/executor.mjs +139 -70
  72. package/src/core/graph/human-evidence.mjs +131 -0
  73. package/src/core/graph/python-probe.mjs +172 -0
  74. package/src/core/graph/registry-ports.mjs +10 -6
  75. package/src/core/graph/scheduler.mjs +39 -24
  76. package/src/core/graph/script-child.mjs +81 -0
  77. package/src/core/graph/script-runner.mjs +597 -0
  78. package/src/core/graph/worca_script.py +207 -0
  79. package/src/core/guardrail-store.mjs +16 -0
  80. package/src/core/human-backfill.mjs +108 -0
  81. package/src/core/human-rate.mjs +17 -0
  82. package/src/core/index-html.mjs +6 -2
  83. package/src/core/memory-defrag-model.mjs +112 -0
  84. package/src/core/memory-store.mjs +70 -18
  85. package/src/core/memory-sync.mjs +22 -11
  86. package/src/core/metrics/read.mjs +4 -1
  87. package/src/core/metrics/record.mjs +50 -2
  88. package/src/core/metrics/sync.mjs +6 -4
  89. package/src/core/model-env.mjs +149 -0
  90. package/src/core/model-test.mjs +14 -1
  91. package/src/core/notifications.mjs +128 -0
  92. package/src/core/onboarding.mjs +8 -2
  93. package/src/core/orchestrator.mjs +357 -26
  94. package/src/core/phases.mjs +95 -6
  95. package/src/core/plugin-api.mjs +24 -7
  96. package/src/core/plugin-manifest.mjs +184 -18
  97. package/src/core/plugin-models.mjs +1 -0
  98. package/src/core/plugin-script-cases.mjs +118 -0
  99. package/src/core/plugin-store.mjs +163 -17
  100. package/src/core/plugin-workflows.mjs +71 -17
  101. package/src/core/policy/cache.mjs +116 -0
  102. package/src/core/policy/effective.mjs +175 -0
  103. package/src/core/policy/gate.mjs +91 -0
  104. package/src/core/policy/local.mjs +145 -0
  105. package/src/core/policy/registry.mjs +330 -0
  106. package/src/core/policy/scope.mjs +61 -0
  107. package/src/core/policy/state.mjs +79 -0
  108. package/src/core/policy/sync.mjs +513 -0
  109. package/src/core/protocol.mjs +43 -0
  110. package/src/core/run-harness.mjs +421 -72
  111. package/src/core/scheduler.mjs +980 -0
  112. package/src/core/script-bench.mjs +628 -0
  113. package/src/core/script-registry.mjs +116 -0
  114. package/src/core/script-store.mjs +563 -0
  115. package/src/core/settings.mjs +489 -14
  116. package/src/core/stats.mjs +33 -2
  117. package/src/core/workflow-export.mjs +94 -3
  118. package/src/core/workflow-share.mjs +67 -18
  119. package/src/core/workflows.mjs +47 -11
  120. package/src/core/workspaces.mjs +18 -12
  121. package/src/shared/forms/answer.mjs +164 -0
  122. package/src/shared/forms/catalog.mjs +91 -0
  123. package/src/shared/forms/form-def.mjs +290 -0
  124. package/src/shared/forms/layout.mjs +67 -0
  125. package/src/shared/forms/paths.mjs +47 -0
  126. package/src/shared/forms/project.mjs +309 -0
  127. package/src/shared/forms/schema.mjs +205 -0
  128. package/src/shared/graph/agent-meta.mjs +55 -5
  129. package/src/shared/graph/constants.mjs +14 -2
  130. package/src/shared/graph/flow-layout.mjs +2 -1
  131. package/src/shared/graph/isomorphic.mjs +5 -3
  132. package/src/shared/graph/manifest.mjs +22 -13
  133. package/src/shared/graph/ports.mjs +45 -19
  134. package/src/shared/graph/script-cases.mjs +257 -0
  135. package/src/shared/graph/script-icons.mjs +46 -0
  136. package/src/shared/graph/script-infer.mjs +259 -0
  137. package/src/shared/graph/script-meta.mjs +408 -0
  138. package/src/shared/graph/script-templates.mjs +201 -0
  139. package/src/shared/graph/template.mjs +4 -4
  140. package/src/shared/graph/validate.mjs +89 -16
  141. package/src/shared/human-estimate.mjs +100 -0
  142. package/src/shared/schedule/recurrence.mjs +353 -0
  143. package/src/shared/team-metrics/aggregate.mjs +51 -11
  144. package/{scripts → tools}/install.mjs +3 -3
  145. package/ui/public/app.js +4390 -683
  146. package/ui/public/artifact-picker.mjs +189 -0
  147. package/ui/public/ask/dom.mjs +121 -0
  148. package/ui/public/ask/form-preview.mjs +55 -0
  149. package/ui/public/ask/form-renderer.mjs +250 -0
  150. package/ui/public/ask/registry.mjs +53 -0
  151. package/ui/public/ask/widgets-display.mjs +370 -0
  152. package/ui/public/ask/widgets-input.mjs +624 -0
  153. package/ui/public/ask/widgets-layout.mjs +90 -0
  154. package/ui/public/ask-panel.mjs +401 -27
  155. package/ui/public/ask-run-card.mjs +1 -1
  156. package/ui/public/bridge-view.mjs +694 -0
  157. package/ui/public/chat-settings-view.mjs +24 -0
  158. package/ui/public/code-editor.mjs +181 -0
  159. package/ui/public/getting-started.mjs +34 -7
  160. package/ui/public/graph/composer.mjs +138 -10
  161. package/ui/public/graph/inspector.mjs +61 -58
  162. package/ui/public/graph/palette.mjs +27 -9
  163. package/ui/public/graph/run-decor.mjs +34 -17
  164. package/ui/public/graph/run-hosts.mjs +7 -1
  165. package/ui/public/graph/save-dialog.mjs +3 -0
  166. package/ui/public/graph/view.mjs +23 -7
  167. package/ui/public/guardrails-view.mjs +15 -3
  168. package/ui/public/guide-spot.mjs +87 -9
  169. package/ui/public/index.html +480 -141
  170. package/ui/public/memory-view.mjs +22 -4
  171. package/ui/public/models-view.mjs +162 -17
  172. package/ui/public/node-tunables.mjs +33 -4
  173. package/ui/public/plugins-view.mjs +23 -1
  174. package/ui/public/results-view.mjs +4 -2
  175. package/ui/public/schedule-sheet.mjs +430 -0
  176. package/ui/public/schedules-view.mjs +432 -0
  177. package/ui/public/script-bench-view.mjs +1154 -0
  178. package/ui/public/script-forms.mjs +282 -0
  179. package/ui/public/script-wizard.mjs +529 -0
  180. package/ui/public/scripts-view.mjs +868 -0
  181. package/ui/public/stats-view.mjs +159 -52
  182. package/ui/public/style.css +1461 -44
  183. package/ui/public/team-metrics-surfaces.mjs +77 -16
  184. package/ui/public/team-metrics-view.mjs +68 -5
  185. package/ui/public/team-policy-view.mjs +1402 -0
  186. package/ui/public/ui-level.mjs +237 -0
  187. package/ui/server.mjs +1827 -73
@@ -0,0 +1,529 @@
1
+ // ui/public/script-wizard.mjs
2
+ // The two steps of the script wizard (script-wizard plan S1, S9, S11, S16, S20):
3
+ // the runtime picker and the split workspace — identity, the interface read
4
+ // from the code, the advanced disclosure, the editor and the bench mount. Pure
5
+ // renderers returning DETACHED DOM (the script-forms.mjs posture); the page
6
+ // controller in scripts-view.mjs binds the listeners and repaints the
7
+ // Interface panel in place on every debounce. Every string is textContent;
8
+ // the ONE innerHTML sink is svgIcon, fed by the fixed set, the ƒ glyph, or a
9
+ // saved fragment that passed sanitizeIcon.
10
+ import { h, text, textarea, number } from './script-forms.mjs';
11
+ import { createCodeEditor, escapeHtml } from './code-editor.mjs';
12
+ import { sanitizeIcon } from '../../src/shared/graph/manifest.mjs';
13
+ import { SCRIPT_ICONS, SCRIPT_GLYPH, iconNameOf, iconSvgOf } from '../../src/shared/graph/script-icons.mjs';
14
+ import { SCRIPT_COLORS, SCRIPT_RUNTIMES, SCRIPT_KEY_RE } from '../../src/shared/graph/script-meta.mjs';
15
+ import { RUNTIME_DEFAULTS } from '../../src/shared/graph/script-templates.mjs';
16
+ import { WHEN_LABEL, CHIP_PARAM_TYPES, interfaceToMeta } from '../../src/shared/graph/script-infer.mjs';
17
+
18
+ export const EDITOR_LANGUAGE = Object.freeze({ node: 'javascript', shell: 'bash', python: 'python' });
19
+ export const RUNTIME_LABEL = Object.freeze({ node: 'Node.js', python: 'Python', shell: 'Shell' });
20
+ const RUNTIME_LINE = Object.freeze({
21
+ node: 'A JavaScript module. Runs everywhere worca runs, nothing to install.',
22
+ python: 'A Python function. Same inputs and outputs, handed in as one object.',
23
+ shell: 'A command or script. Exit 0 passes, exit 1 fails. Ports arrive as env vars.',
24
+ });
25
+ const RUNTIME_SIG = Object.freeze({ node: 'export default async function (api)', python: 'def main(api)', shell: '$WORCA_IN_PLAN → $WORCA_OUT_LOG' });
26
+ const EXT = Object.freeze({ node: '.mjs', python: '.py', shell: '.sh' });
27
+ /** The mono hint an empty zone shows: how THIS runtime declares a port (S19). */
28
+ export const IFACE_HINTS = Object.freeze({
29
+ node: Object.freeze({ in: 'none yet · read inputs.<name>.path', out: 'none yet · write outputs.<name>.path', param: 'none yet · read params.<name>' }),
30
+ python: Object.freeze({ in: 'none yet · read api.inputs.<name>.path', out: 'none yet · write api.outputs.<name>.path', param: 'none yet · read api.params.<name>' }),
31
+ shell: Object.freeze({ in: 'none yet · read $WORCA_IN_<NAME>', out: 'none yet · write $WORCA_OUT_<NAME>', param: 'none yet · read $WORCA_PARAM_<NAME>' }),
32
+ });
33
+
34
+ /** 'builtin' -> 'built-in', 'plugin:tools' -> 'tools'. (Moved from scripts-view.mjs.) */
35
+ export function originLabel(origin) {
36
+ const o = String(origin || '');
37
+ if (o.startsWith('plugin:')) return o.slice('plugin:'.length);
38
+ return o === 'user' ? 'user' : 'built-in';
39
+ }
40
+
41
+ // A sidecar's `command` is a string or a per-platform map { default, win32, … }.
42
+ // The editor holds the DEFAULT entry; the other entries ride a hidden field so a
43
+ // save never flattens the map. (Moved from scripts-view.mjs.)
44
+ export const commandText = (c) => (typeof c === 'string' ? c : (c && typeof c.default === 'string' ? c.default : ''));
45
+ export const commandExtra = (c) => {
46
+ if (!c || typeof c !== 'object') return '';
47
+ const { default: _d, ...rest } = c;
48
+ return Object.keys(rest).length ? JSON.stringify(rest) : '';
49
+ };
50
+ export function joinCommand(textValue, extraJson) {
51
+ let extra = {};
52
+ try { extra = extraJson ? JSON.parse(extraJson) : {}; } catch { extra = {}; }
53
+ return extra && typeof extra === 'object' && Object.keys(extra).length ? { default: textValue, ...extra } : textValue;
54
+ }
55
+ /** A shell script edits a FILE when its meta names one, a command otherwise. */
56
+ export const shellMode = (meta) => (meta.runtime === 'shell' && !meta.file ? 'command' : 'file');
57
+ const csv = (list) => (Array.isArray(list) ? list.join(', ') : '');
58
+ // A BLANK entry is no exit code (`Number('')` is 0, which would list 0 as both clean and blocking).
59
+ const ints = (s) => String(s || '').split(',').map((x) => x.trim()).filter((x) => x !== '').map(Number).filter((n) => Number.isInteger(n));
60
+
61
+ const btn = (doc, cls, label, kind = 'btn-ghost') => { const b = h(doc, 'button', `btn ${kind} btn-mini ${cls}`, label); b.type = 'button'; return b; };
62
+ const chip = (doc, cls, label) => h(doc, 'span', `chip ${cls}`, label);
63
+ const hidden = (doc, field, value) => { const i = doc.createElement('input'); i.type = 'hidden'; i.dataset.field = field; i.value = value == null ? '' : String(value); return i; };
64
+
65
+ /** A 24×24 stroke svg around a TRUSTED fragment (the set's, the glyph, or one that passed sanitizeIcon).
66
+ * `name` rides as data-icon-name: the set's name, 'glyph' or 'custom' — what a test can read back
67
+ * (jsdom re-serializes a self-closed <path/> as <path></path>, so innerHTML is not comparable). */
68
+ function svgIcon(doc, fragment, size, name = 'custom') {
69
+ const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
70
+ svg.dataset.iconName = name;
71
+ svg.setAttribute('viewBox', '0 0 24 24');
72
+ svg.setAttribute('width', String(size));
73
+ svg.setAttribute('height', String(size));
74
+ svg.setAttribute('fill', 'none');
75
+ svg.setAttribute('stroke', 'currentColor');
76
+ svg.setAttribute('stroke-width', '1.8');
77
+ svg.setAttribute('stroke-linecap', 'round');
78
+ svg.setAttribute('stroke-linejoin', 'round');
79
+ svg.setAttribute('aria-hidden', 'true');
80
+ svg.innerHTML = fragment; // the ONE innerHTML in this module; every caller passes a trusted fragment
81
+ return svg;
82
+ }
83
+ /** What a stored icon renders as: the set's fragment, else the saved one through the allowlist, else the ƒ. */
84
+ function safeIcon(icon) {
85
+ const named = iconNameOf(icon);
86
+ if (named) return { svg: iconSvgOf(named), name: named };
87
+ const clean = sanitizeIcon(String(icon || ''));
88
+ return clean ? { svg: clean, name: 'custom' } : { svg: SCRIPT_GLYPH, name: 'glyph' };
89
+ }
90
+ const safeColor = (c) => (SCRIPT_COLORS.includes(c) ? c : 'amber');
91
+
92
+ function probeChip(doc, rt, probe) {
93
+ // process.version arrives as `v22.13.0`: one `v`, not `Node v22`.
94
+ if (rt === 'node') return chip(doc, 'chip-ok rt-probe', probe && probe.version ? `Node ${String(probe.version).replace(/^v/, '')} · ready` : 'Node · ready');
95
+ if (rt === 'python') {
96
+ if (probe && probe.ok === false) return chip(doc, 'chip-warn rt-probe', 'python not found');
97
+ return chip(doc, 'chip-ok rt-probe', probe && probe.version ? `Python ${probe.version} found` : 'Python found');
98
+ }
99
+ return chip(doc, 'chip-muted rt-probe', 'sh · cmd.exe on Windows');
100
+ }
101
+
102
+ /** The `1 Runtime › 2 Build & test` track. Step 1's pill is a BUTTON on step 2 (back to the picker). */
103
+ function stepTrack(doc, { step, runtime = '' }) {
104
+ const track = h(doc, 'div', 'wz-track');
105
+ const one = h(doc, 'button', `wz-step-pill ${step === 1 ? 'on' : 'done'}`);
106
+ one.type = 'button'; one.dataset.step = '1'; one.disabled = step === 1;
107
+ one.append(h(doc, 'span', 'wz-step-n', step === 1 ? '1' : '✓'), doc.createTextNode('Runtime'));
108
+ if (step === 2 && runtime) one.appendChild(chip(doc, `chip-rt chip-${runtime}`, RUNTIME_LABEL[runtime] || runtime));
109
+ track.appendChild(one);
110
+ track.appendChild(h(doc, 'span', 'wz-step-sep', '›'));
111
+ const two = h(doc, 'span', `wz-step-pill${step === 2 ? ' on' : ''}`);
112
+ two.append(h(doc, 'span', 'wz-step-n', '2'), doc.createTextNode('Build & test'));
113
+ track.appendChild(two);
114
+ return track;
115
+ }
116
+
117
+ /** Step 1 (S16): three cards from the runtimes probe, Cancel, Continue. */
118
+ export function renderRuntimeStep({ doc = globalThis.document, runtimes = {}, picked = 'node' } = {}) {
119
+ const root = h(doc, 'div', 'wz wz-step-1');
120
+ root.dataset.step = '1';
121
+ const bar = h(doc, 'div', 'wz-bar');
122
+ bar.append(stepTrack(doc, { step: 1 }), h(doc, 'span', 'sp'), btn(doc, 'wz-cancel', 'Cancel'));
123
+ root.appendChild(bar);
124
+ const pick = h(doc, 'div', 'wz-pick');
125
+ const title = h(doc, 'div', 'wz-pick-title');
126
+ title.append(h(doc, 'h1', '', 'New script'), h(doc, 'small', 'wz-pick-line', 'Pick what runs it. Everything else is read from the code.'));
127
+ pick.appendChild(title);
128
+ const cards = h(doc, 'div', 'wz-rt-cards');
129
+ for (const rt of SCRIPT_RUNTIMES) {
130
+ const probe = runtimes[rt] || {};
131
+ const on = rt === picked;
132
+ const card = h(doc, 'button', `rt${on ? ' sel' : ''}`);
133
+ card.type = 'button'; card.dataset.runtime = rt; card.setAttribute('aria-pressed', on ? 'true' : 'false');
134
+ if (probe.ok === false) { card.disabled = true; card.title = probe.reason || 'not available on this machine'; }
135
+ const head = h(doc, 'span', 'rt-head');
136
+ const tile = h(doc, 'span', `tile tile-${RUNTIME_DEFAULTS[rt].color}`);
137
+ tile.appendChild(svgIcon(doc, iconSvgOf(RUNTIME_DEFAULTS[rt].icon), 24, RUNTIME_DEFAULTS[rt].icon));
138
+ head.appendChild(tile);
139
+ if (on) head.appendChild(h(doc, 'span', 'rt-check', '✓'));
140
+ card.append(head, h(doc, 'span', 'rt-name', RUNTIME_LABEL[rt]), h(doc, 'small', 'rt-line', RUNTIME_LINE[rt]),
141
+ h(doc, 'code', 'rt-sig mono', RUNTIME_SIG[rt]), probeChip(doc, rt, probe));
142
+ cards.appendChild(card);
143
+ }
144
+ pick.appendChild(cards);
145
+ const foot = h(doc, 'div', 'wz-pick-foot');
146
+ const go = btn(doc, 'wz-continue', 'Continue', 'btn-primary');
147
+ go.disabled = Boolean(runtimes[picked] && runtimes[picked].ok === false);
148
+ foot.appendChild(go);
149
+ pick.appendChild(foot);
150
+ root.appendChild(pick);
151
+ return root;
152
+ }
153
+
154
+ /** The card exactly as the palette and the canvas will show it (S9). */
155
+ export function renderTile({ doc = globalThis.document, name = '', key = '', runtime = 'node', color = 'amber', icon = '' } = {}) {
156
+ const tile = h(doc, 'div', 'panel wz-tile');
157
+ const box = h(doc, 'div', `tile tile-${safeColor(color)}`);
158
+ const safe = safeIcon(icon);
159
+ box.appendChild(svgIcon(doc, safe.svg, 22, safe.name));
160
+ tile.appendChild(box);
161
+ const txt = h(doc, 'div', 'wz-tile-text');
162
+ txt.appendChild(h(doc, 'div', `pv-name${name ? '' : ' muted'}`, name || 'Untitled script'));
163
+ const line = h(doc, 'div', 'wz-tile-line');
164
+ line.append(h(doc, 'span', 'pv-key mono', key || 'key'), chip(doc, `chip-rt chip-${runtime}`, RUNTIME_LABEL[runtime] || runtime), chip(doc, 'chip-muted', '$0 per run'));
165
+ txt.appendChild(line);
166
+ tile.appendChild(txt);
167
+ return tile;
168
+ }
169
+
170
+ function identityPanel(doc, meta, { readOnly, isNew }) {
171
+ const panel = h(doc, 'div', 'panel wz-identity');
172
+ const row = h(doc, 'div', 'wz-row');
173
+ const name = text(doc, 'wz-f', 'meta:displayName', 'Name', meta.displayName);
174
+ name.querySelector('input').placeholder = 'Run tests';
175
+ const key = text(doc, 'wz-f', 'meta:key', 'Key', meta.key);
176
+ const keyInput = key.querySelector('input');
177
+ keyInput.classList.add('mono'); keyInput.placeholder = 'runTests'; keyInput.disabled = !isNew;
178
+ row.append(name, key);
179
+ panel.appendChild(row);
180
+ const desc = textarea(doc, 'wz-f', 'meta:description', 'Description', meta.description, 2);
181
+ const ta = desc.querySelector('textarea');
182
+ ta.classList.remove('mono'); ta.placeholder = 'What it does, in one line.';
183
+ panel.appendChild(desc);
184
+ const colors = h(doc, 'div', 'wz-f');
185
+ colors.appendChild(h(doc, 'span', 'ins-label', 'Color'));
186
+ const sw = h(doc, 'div', 'wz-swatches');
187
+ const current = safeColor(meta.color);
188
+ for (const c of SCRIPT_COLORS) {
189
+ const b = h(doc, 'button', `sw sw-${c}${c === current ? ' sel' : ''}`);
190
+ b.type = 'button'; b.dataset.swatch = c; b.setAttribute('aria-label', c); b.setAttribute('aria-pressed', c === current ? 'true' : 'false');
191
+ sw.appendChild(b);
192
+ }
193
+ colors.appendChild(sw);
194
+ panel.appendChild(colors);
195
+ const icons = h(doc, 'div', 'wz-f');
196
+ icons.appendChild(h(doc, 'span', 'ins-label', 'Icon'));
197
+ const grid = h(doc, 'div', 'wz-icons');
198
+ const picked = iconNameOf(meta.icon);
199
+ for (const { name: n, svg } of SCRIPT_ICONS) {
200
+ const b = h(doc, 'button', `ico${n === picked ? ' sel' : ''}`);
201
+ b.type = 'button'; b.dataset.icon = n; b.setAttribute('aria-label', n); b.setAttribute('aria-pressed', n === picked ? 'true' : 'false');
202
+ b.appendChild(svgIcon(doc, svg, 18, n));
203
+ grid.appendChild(b);
204
+ }
205
+ icons.appendChild(grid);
206
+ panel.appendChild(icons);
207
+ panel.append(hidden(doc, 'meta:color', current), hidden(doc, 'meta:icon', meta.icon || ''), hidden(doc, 'meta:runtime', meta.runtime || 'node'));
208
+ if (readOnly) for (const c of panel.querySelectorAll('input:not([type="hidden"]),textarea,button')) c.disabled = true;
209
+ return panel;
210
+ }
211
+
212
+ /** A ports:"config" script's defaultPorts as inert rows (S8). */
213
+ function configRows(defaultPorts) {
214
+ const dp = defaultPorts || { inputs: [], outputs: [] };
215
+ return {
216
+ inputs: (dp.inputs || []).filter((p) => p && p.id).map((p) => ({ id: p.id, type: p.type || 'md', mode: p.loop ? 'loop' : (p.required === false ? 'optional' : 'required'), inCode: true })),
217
+ outputs: (dp.outputs || []).filter((p) => p && p.id).map((p) => ({ id: p.id, type: p.type || 'md', when: p.when || 'always', filename: p.filename || '', inCode: true })),
218
+ };
219
+ }
220
+
221
+ /**
222
+ * The Interface panel (S3–S8, S20). Repainted IN PLACE by the controller on every
223
+ * debounce and every chip click; `rows` is Task 1's Rows. `verdictFilename` is
224
+ * the sidecar's, handed in explicitly so a re-render never loses it.
225
+ */
226
+ export function renderInterfacePanel({
227
+ doc = globalThis.document, runtime = 'node', key = '', rows = null, verdict = false, routing = false, readOnly = false,
228
+ configPorts = false, defaultPorts = null, verdictFilename = '',
229
+ } = {}) {
230
+ const panel = h(doc, 'div', 'panel wz-iface');
231
+ const head = h(doc, 'div', 'wz-iface-head');
232
+ head.append(h(doc, 'span', 'wz-panel-title', 'Interface'), chip(doc, 'chip-muted', configPorts ? 'ports per card' : 'read from the code'));
233
+ panel.appendChild(head);
234
+ const base = rows || { inputs: [], outputs: [], params: [] };
235
+ // A config script's params are the sidecar's (the runner reads them, not the program): never `not in code`.
236
+ const r = configPorts ? { ...configRows(defaultPorts), params: (base.params || []).map((p) => ({ ...p, inCode: true })) } : base;
237
+ const portInert = readOnly || configPorts;
238
+ const hints = IFACE_HINTS[runtime] || IFACE_HINTS.node;
239
+ const hasVerdict = runtime === 'shell' ? (routing || verdict) : verdict;
240
+ const side3 = (side) => (side === 'inputs' ? 'in' : side === 'outputs' ? 'out' : 'param');
241
+ const tchip = (dataChip, label, cls, inert) => { const b = h(doc, 'button', `tchip${cls ? ` ${cls}` : ''}`, label); b.type = 'button'; b.dataset.chip = dataChip; b.disabled = inert; return b; };
242
+ const prow = (side, row) => {
243
+ const el = h(doc, 'div', `wz-prow${row.inCode === false ? ' stale' : ''}`);
244
+ el.dataset.side = side; el.dataset.id = row.id;
245
+ el.appendChild(h(doc, 'span', 'pid mono', row.id));
246
+ return el;
247
+ };
248
+ const trailer = (el, side, row, inert) => {
249
+ el.appendChild(h(doc, 'span', 'sp'));
250
+ if (row.inCode === false) {
251
+ el.appendChild(chip(doc, 'chip-warn chip-tiny', 'not in code'));
252
+ if (!inert) { const x = h(doc, 'button', 'wz-prm', '×'); x.type = 'button'; x.dataset.remove = `${side}:${row.id}`; x.title = 'Remove'; el.appendChild(x); }
253
+ }
254
+ el.appendChild(hidden(doc, `iface:${side3(side)}:${row.id}:inCode`, row.inCode === false ? '' : '1'));
255
+ };
256
+ const zone = (label, extra = null) => { const z = h(doc, 'div', 'wz-zone-head'); z.appendChild(h(doc, 'span', 'zone', label)); if (extra) z.appendChild(extra); panel.appendChild(z); };
257
+
258
+ zone('Inputs');
259
+ if (!r.inputs.length) panel.appendChild(h(doc, 'div', 'empty mono', hints.in));
260
+ for (const row of r.inputs) {
261
+ const el = prow('inputs', row);
262
+ el.append(tchip(`in:${row.id}:type`, row.type, row.type === 'void' ? 'void' : '', portInert),
263
+ tchip(`in:${row.id}:mode`, row.mode || 'optional', 'mode', portInert),
264
+ hidden(doc, `iface:in:${row.id}:type`, row.type), hidden(doc, `iface:in:${row.id}:mode`, row.mode || 'optional'));
265
+ trailer(el, 'inputs', row, portInert);
266
+ panel.appendChild(el);
267
+ }
268
+
269
+ const vchip = hasVerdict
270
+ ? chip(doc, 'chip-ok chip-tiny wz-verdict-chip', `routes on pass / fail · ${runtime === 'shell' && !verdict ? 'from the exit code' : 'returned by the script'}`)
271
+ : null;
272
+ zone('Outputs', vchip);
273
+ if (!r.outputs.length) panel.appendChild(h(doc, 'div', 'empty mono', hints.out));
274
+ for (const row of r.outputs) {
275
+ const el = prow('outputs', row);
276
+ el.appendChild(tchip(`out:${row.id}:type`, row.type, row.type === 'void' ? 'void' : '', portInert));
277
+ if (hasVerdict) el.appendChild(tchip(`out:${row.id}:when`, WHEN_LABEL[row.when] || 'always', 'when', portInert));
278
+ el.append(hidden(doc, `iface:out:${row.id}:type`, row.type), hidden(doc, `iface:out:${row.id}:when`, row.when || 'always'),
279
+ hidden(doc, `iface:out:${row.id}:filename`, row.filename || ''));
280
+ trailer(el, 'outputs', row, portInert);
281
+ panel.appendChild(el);
282
+ }
283
+ if (runtime === 'shell' && !configPorts) {
284
+ const tog = h(doc, 'div', 'wz-routing');
285
+ const b = h(doc, 'button', `tog${routing ? ' on' : ''}`);
286
+ b.type = 'button'; b.dataset.routing = ''; b.setAttribute('role', 'switch');
287
+ b.setAttribute('aria-checked', routing ? 'true' : 'false'); b.setAttribute('aria-label', 'Route by exit code'); b.disabled = readOnly;
288
+ tog.append(b, h(doc, 'span', 'wz-routing-line', 'exit 0 → pass · exit 1 → fail'));
289
+ panel.appendChild(tog);
290
+ }
291
+
292
+ zone('Params');
293
+ if (!r.params.length) panel.appendChild(h(doc, 'div', 'empty mono', hints.param));
294
+ for (const row of r.params) {
295
+ const el = prow('params', row);
296
+ const fixed = row.fixed === true || !CHIP_PARAM_TYPES.includes(row.type);
297
+ el.appendChild(tchip(`param:${row.id}:type`, row.type, fixed ? 'fixed' : '', readOnly || fixed));
298
+ const dflt = doc.createElement('input');
299
+ dflt.type = 'text'; dflt.className = 'wz-pdflt mono'; dflt.dataset.field = `iface:param:${row.id}:default`;
300
+ dflt.value = row.default === undefined || row.default === null ? '' : String(row.default);
301
+ dflt.placeholder = 'default'; dflt.setAttribute('aria-label', `Default for ${row.id}`); dflt.disabled = readOnly;
302
+ el.appendChild(dflt);
303
+ const extra = {};
304
+ for (const f of ['required', 'label', 'options', 'language', 'description']) if (row[f] !== undefined) extra[f] = row[f];
305
+ extra.fixed = fixed;
306
+ el.append(hidden(doc, `iface:param:${row.id}:type`, row.type), hidden(doc, `iface:param:${row.id}:extra`, JSON.stringify(extra)));
307
+ trailer(el, 'params', row, readOnly);
308
+ panel.appendChild(el);
309
+ }
310
+
311
+ panel.append(hidden(doc, 'iface:verdict', verdict ? '1' : ''), hidden(doc, 'iface:routing', routing ? '1' : ''),
312
+ hidden(doc, 'iface:verdictFilename', verdictFilename || ''),
313
+ hidden(doc, 'iface:config', configPorts ? JSON.stringify(defaultPorts || { inputs: [], outputs: [] }) : ''));
314
+ return panel;
315
+ }
316
+
317
+ /** The Advanced disclosure's one-line summary (S11): `10 min timeout · general · order 50`. Exported so the
318
+ * controller can keep it in step with the three fields as they are typed. */
319
+ export function advSummary(meta) {
320
+ const timeoutSec = Math.round((meta.timeoutMs || 600000) / 1000);
321
+ const order = Number.isFinite(meta.order) ? meta.order : 50;
322
+ return `${Math.max(1, Math.round(timeoutSec / 60))} min timeout · ${meta.domain || 'general'} · order ${order}`;
323
+ }
324
+
325
+ function advancedPanel(doc, meta, { readOnly, advOpen, verdictFile }) {
326
+ const panel = h(doc, 'div', 'panel wz-adv');
327
+ const toggle = h(doc, 'button', 'wz-adv-toggle');
328
+ toggle.type = 'button'; toggle.setAttribute('aria-expanded', advOpen ? 'true' : 'false');
329
+ const timeoutSec = Math.round((meta.timeoutMs || 600000) / 1000);
330
+ const order = Number.isFinite(meta.order) ? meta.order : 50;
331
+ toggle.append(h(doc, 'span', `chev${advOpen ? ' open' : ''}`, '›'), h(doc, 'span', 'wz-panel-title', 'Advanced'), h(doc, 'span', 'wz-adv-sum', advSummary(meta)));
332
+ panel.appendChild(toggle);
333
+ const body = h(doc, 'div', 'wz-adv-body');
334
+ body.hidden = !advOpen;
335
+ body.append(number(doc, 'wz-f', 'meta:timeoutSec', 'Timeout (s)', timeoutSec, 1), text(doc, 'wz-f', 'meta:domain', 'Domain', meta.domain || ''),
336
+ number(doc, 'wz-f', 'meta:order', 'Order in the palette', order, 0));
337
+ const vf = h(doc, 'div', 'ins-f wz-f');
338
+ vf.append(h(doc, 'span', 'ins-label', 'Verdict file'), h(doc, 'code', 'wz-verdict-file mono', verdictFile || '—'));
339
+ body.appendChild(vf);
340
+ if (meta.runtime === 'shell') {
341
+ const codes = meta.exitCodes || {};
342
+ body.append(text(doc, 'wz-f', 'meta:exitCodesClean', 'Clean exit codes', csv(codes.clean || [0])),
343
+ text(doc, 'wz-f', 'meta:exitCodesBlocking', 'Blocking exit codes', csv(codes.blocking || [1])));
344
+ }
345
+ if (readOnly) for (const c of body.querySelectorAll('input')) c.disabled = true;
346
+ panel.appendChild(body);
347
+ return panel;
348
+ }
349
+
350
+ /**
351
+ * The editor panel alone (S15): the file name, the runtime chip, a shell script's
352
+ * Command | File and sh | win32 controls, Load example, the code editor and the
353
+ * hidden mirrors of the halves that are not on screen. Exported so the controller
354
+ * can swap ONLY this panel on a mode / tab hop — the bench under it (a run in
355
+ * flight, its result, the typed inputs) stays mounted.
356
+ */
357
+ export function renderEditorPanel(doc, data, { readOnly, highlight, srcMode, srcTab, editors, onSourceInput, key }) {
358
+ const meta = data.meta;
359
+ const rt = meta.runtime || 'node';
360
+ const panel = h(doc, 'div', 'panel wz-editor script-source'); // .script-source: the controller reads dataset.srcMode off it
361
+ panel.dataset.srcMode = srcMode; panel.dataset.srcTab = srcTab;
362
+ const head = h(doc, 'div', 'wz-ed-head');
363
+ const command = rt === 'shell' && srcMode === 'command';
364
+ const win32 = srcMode === 'file' && srcTab === 'win32';
365
+ head.append(h(doc, 'span', 'wz-file mono', command ? 'command' : `${key || 'script'}${win32 ? '.cmd' : EXT[rt]}`), chip(doc, `chip-rt chip-${rt}`, RUNTIME_LABEL[rt]));
366
+ if (rt === 'shell') {
367
+ const seg = h(doc, 'div', 'seg script-src-mode');
368
+ for (const [mode, label] of [['command', 'Command'], ['file', 'File']]) {
369
+ const b = h(doc, 'button', mode === srcMode ? 'on' : '', label);
370
+ b.type = 'button'; b.dataset.srcMode = mode; b.disabled = readOnly; b.setAttribute('aria-pressed', mode === srcMode ? 'true' : 'false');
371
+ seg.appendChild(b);
372
+ }
373
+ head.appendChild(seg);
374
+ if (srcMode === 'file') {
375
+ const plat = h(doc, 'div', 'seg script-src-plat');
376
+ for (const [id, label] of [['default', 'sh'], ['win32', 'win32']]) {
377
+ const b = h(doc, 'button', id === srcTab ? 'on' : '', label);
378
+ b.type = 'button'; b.dataset.srcTab = id; b.disabled = readOnly; b.setAttribute('aria-pressed', id === srcTab ? 'true' : 'false');
379
+ plat.appendChild(b);
380
+ }
381
+ head.appendChild(plat);
382
+ }
383
+ }
384
+ head.appendChild(h(doc, 'span', 'sp'));
385
+ if (!readOnly) head.appendChild(btn(doc, 'wz-example', 'Load example'));
386
+ panel.appendChild(head);
387
+ const editor = createCodeEditor({
388
+ doc,
389
+ value: command ? commandText(meta.command) : (win32 ? data.sourceWin32 : data.source),
390
+ language: command ? 'bash' : (EDITOR_LANGUAGE[rt] || 'javascript'),
391
+ readOnly,
392
+ rows: command ? 4 : 18,
393
+ name: command ? 'meta:command' : (win32 ? 'script:sourceWin32' : 'script:source'),
394
+ highlight,
395
+ onInput: typeof onSourceInput === 'function' ? onSourceInput : null,
396
+ });
397
+ editors.push(editor);
398
+ const mount = h(doc, 'div', 'script-editor-mount');
399
+ mount.appendChild(editor.el);
400
+ panel.appendChild(mount);
401
+ // The half that is not on screen still reaches the server on Save (the hidden-mirror idiom).
402
+ for (const [name, value] of [['script:source', data.source], ['script:sourceWin32', data.sourceWin32],
403
+ ['meta:command', commandText(meta.command)], ['meta:commandExtra', commandExtra(meta.command)]]) {
404
+ if (panel.querySelector(`[data-field="${name}"]`)) continue;
405
+ panel.appendChild(hidden(doc, name, value || ''));
406
+ }
407
+ return panel;
408
+ }
409
+
410
+ function wsHead(doc, meta, { readOnly, isNew, runtimes, sourcePath }) {
411
+ const bar = h(doc, 'div', 'wz-bar wz-head');
412
+ bar.appendChild(btn(doc, 'script-back', 'Scripts'));
413
+ const track = stepTrack(doc, { step: 2, runtime: meta.runtime || 'node' });
414
+ if (readOnly) track.querySelector('.wz-step-pill[data-step="1"]').disabled = true;
415
+ bar.appendChild(track);
416
+ if (meta.origin && meta.origin !== 'user') bar.appendChild(h(doc, 'span', 'badge script-origin', originLabel(meta.origin)));
417
+ const probe = runtimes && runtimes[meta.runtime];
418
+ if (meta.runtime === 'python' && probe && probe.ok === false) bar.appendChild(chip(doc, 'script-warn', 'python not found'));
419
+ bar.appendChild(h(doc, 'span', 'sp'));
420
+ const dirty = chip(doc, 'script-dirty', 'unsaved');
421
+ dirty.hidden = true;
422
+ bar.appendChild(dirty);
423
+ if (readOnly) {
424
+ const row = h(doc, 'span', 'script-path-row');
425
+ row.append(h(doc, 'code', 'script-path mono', sourcePath || meta.scriptPath || meta.commandResolved || ''), btn(doc, 'script-copy', 'Copy'));
426
+ bar.appendChild(row);
427
+ }
428
+ if (!isNew) bar.appendChild(btn(doc, 'script-duplicate', 'Duplicate'));
429
+ if (!isNew && meta.origin === 'user') bar.appendChild(btn(doc, 'script-delete', 'Delete', 'btn-danger'));
430
+ if (!readOnly) {
431
+ const save = btn(doc, 'script-save', isNew ? 'Save script' : 'Save', 'btn-primary');
432
+ save.disabled = !String(meta.displayName || '').trim() || !SCRIPT_KEY_RE.test(String(meta.key || ''));
433
+ bar.appendChild(save);
434
+ }
435
+ return bar;
436
+ }
437
+
438
+ /**
439
+ * Step 2 (S1): the whole workspace. `rows` / `verdict` / `routing` are the
440
+ * controller's interface state; `onSourceInput(text)` is the editor's live hook.
441
+ * `root._editors` carries the code editors so the caller can tear them down.
442
+ */
443
+ export function renderWorkspace(data, {
444
+ doc = globalThis.document, runtimes = {}, readOnly = false, isNew = false, highlight = async (t) => escapeHtml(t),
445
+ srcMode = '', srcTab = 'default', rows = null, verdict = false, routing = false, advOpen = false, onSourceInput = null,
446
+ verdictFilename = null, // the SIDECAR's verdict filename; null = read it off data.meta
447
+ } = {}) {
448
+ const meta = data.meta;
449
+ const editors = [];
450
+ const root = h(doc, 'div', 'script-detail wz wz-step-2');
451
+ root.dataset.scriptKey = meta.key || ''; root.dataset.step = '2'; root._editors = editors;
452
+ root.appendChild(wsHead(doc, meta, { readOnly, isNew, runtimes, sourcePath: data.sourcePath || '' }));
453
+ const body = h(doc, 'div', 'wz-body');
454
+ const left = h(doc, 'div', 'wz-left');
455
+ left.appendChild(renderTile({ doc, name: meta.displayName, key: meta.key, runtime: meta.runtime, color: meta.color, icon: meta.icon }));
456
+ left.appendChild(identityPanel(doc, meta, { readOnly, isNew }));
457
+ const configPorts = meta.ports === 'config';
458
+ const hasVerdict = meta.runtime === 'shell' ? (routing || verdict) : verdict;
459
+ const savedVerdict = verdictFilename !== null ? String(verdictFilename || '') : ((meta.verdict && meta.verdict.filename) || '');
460
+ left.appendChild(renderInterfacePanel({ doc, runtime: meta.runtime, key: meta.key, rows, verdict, routing, readOnly, configPorts,
461
+ defaultPorts: configPorts ? meta.defaultPorts : null, verdictFilename: savedVerdict }));
462
+ left.appendChild(advancedPanel(doc, meta, { readOnly, advOpen, verdictFile: hasVerdict || (configPorts && savedVerdict) ? (savedVerdict || `${meta.key || 'script'}-cycle{cycle}.json`) : '' }));
463
+ const right = h(doc, 'div', 'wz-right');
464
+ right.appendChild(renderEditorPanel(doc, data, { readOnly, highlight, srcMode: srcMode || shellMode(meta), srcTab, editors, onSourceInput, key: meta.key }));
465
+ right.appendChild(h(doc, 'div', 'script-test-mount'));
466
+ body.append(left, right);
467
+ root.appendChild(body);
468
+ return root;
469
+ }
470
+
471
+ /** Read the workspace back as the wire body `POST /api/scripts` / `PUT` take (S17, S20). Pure over `root`. */
472
+ export function collectScriptDraft(root) {
473
+ const val = (name, scope = root) => { const n = scope.querySelector(`[data-field="${name}"]`); return n ? String(n.value) : ''; };
474
+ // A CLEARED box is '' and Number('') is 0: blank = the default, never a 1 s timeout or order 0.
475
+ const num = (name, fallback) => { const raw = val(name).trim(); const n = Number(raw); return raw !== '' && Number.isFinite(n) ? n : fallback; };
476
+ const runtime = val('meta:runtime') || 'node';
477
+ const key = (val('meta:key') || root.dataset.scriptKey || '').trim();
478
+ const meta = {
479
+ key, metaVersion: 2, displayName: val('meta:displayName'), description: val('meta:description'), domain: val('meta:domain'),
480
+ color: val('meta:color') || 'amber', icon: val('meta:icon'), order: num('meta:order', 50), runtime,
481
+ timeoutMs: Math.max(1000, Math.round(num('meta:timeoutSec', 600) * 1000)),
482
+ };
483
+ const iface = root.querySelector('.wz-iface') || root;
484
+ const rows = { inputs: [], outputs: [], params: [] };
485
+ for (const el of iface.querySelectorAll('.wz-prow')) {
486
+ const { side, id } = el.dataset;
487
+ const pre = side === 'inputs' ? 'in' : side === 'outputs' ? 'out' : 'param';
488
+ const f = (name) => val(`iface:${pre}:${id}:${name}`, el);
489
+ if (side === 'inputs') rows.inputs.push({ id, type: f('type') || 'md', mode: f('mode') || 'optional' });
490
+ else if (side === 'outputs') rows.outputs.push({ id, type: f('type') || 'md', when: f('when') || 'always', filename: f('filename') });
491
+ else {
492
+ let extra = {};
493
+ try { extra = JSON.parse(f('extra') || '{}'); } catch { extra = {}; }
494
+ rows.params.push({ id, type: f('type') || 'string', default: f('default'), ...extra });
495
+ }
496
+ }
497
+ const verdictFilename = val('iface:verdictFilename', iface);
498
+ const built = interfaceToMeta(rows, { key, runtime, verdict: val('iface:verdict', iface) === '1', routing: val('iface:routing', iface) === '1', verdictFilename });
499
+ const config = val('iface:config', iface);
500
+ // A key this form can CLEAR is always sent, `null` when cleared: the store merges the sent meta
501
+ // over the stored one and reads null as "remove" (script-store.mjs stripNullKeys).
502
+ if (config) {
503
+ let dp = null;
504
+ try { dp = JSON.parse(config); } catch { dp = null; }
505
+ meta.ports = 'config'; meta.defaultPorts = dp || { inputs: [], outputs: [] }; meta.inputs = null; meta.outputs = null;
506
+ meta.verdict = verdictFilename ? { filename: verdictFilename } : null; // the sidecar's, untouched (S8)
507
+ } else {
508
+ meta.inputs = built.inputs; meta.outputs = built.outputs; meta.ports = null; meta.defaultPorts = null;
509
+ meta.verdict = built.verdict;
510
+ }
511
+ meta.params = built.params;
512
+ meta.exitCodes = null;
513
+ meta.command = null;
514
+ const box = root.querySelector('.script-source');
515
+ const mode = box ? box.dataset.srcMode : shellMode(meta);
516
+ if (runtime === 'shell') {
517
+ const clean = ints(val('meta:exitCodesClean'));
518
+ const blocking = ints(val('meta:exitCodesBlocking'));
519
+ if (clean.length || blocking.length) meta.exitCodes = { clean, blocking };
520
+ const command = val('meta:command');
521
+ if (mode === 'command' && command.trim()) meta.command = joinCommand(command, val('meta:commandExtra'));
522
+ }
523
+ const file = !(runtime === 'shell' && mode === 'command');
524
+ return {
525
+ meta,
526
+ source: file ? val('script:source') : '',
527
+ sourceWin32: runtime === 'shell' && mode === 'file' ? val('script:sourceWin32') : '',
528
+ };
529
+ }