diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b6347d..3d89577 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,6 +69,14 @@ Issues and pull requests are welcome. Before opening one: screenshots go to `/tmp/t3-access-ui-audit` (`T3_ACCESS_UI_OUTPUT` overrides it). All Access API writes are intercepted and never reach real devices. +For shared MCP changes, run `node scripts/mcp-native-smoke.mjs PROVIDER +/absolute/path/to/executable` for Claude, Codex, OpenCode (both V1 and V2) and +Cursor. It uses disposable homes and fixture servers without model requests. +`node scripts/mcp-ui-audit.js` checks the MCP tab's apply/unlink controls, +revisions, JSON values and failed/late reads in both themes at phone, desktop +and embedded sizes. It uses response fixtures, with screenshots under +`/tmp/t3-mcp-ui-audit`. The standard screenshot matrix includes `instructions/mcp`. + For OpenCode credential changes, run `node scripts/opencode-auth-smoke.mjs /absolute/path/to/opencode` against both 1.x and 2.x (including 2.0.18, the supported V2 floor). It uses an isolated diff --git a/README.md b/README.md index 8877da4..0e3f743 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ instead. Details: [Reaching the server from a phone](docs/remote-access.md). | [Source control](docs/source-control.md) | gh, glab, fj, tea and az sign-in | | [Publishing a port](docs/ports.md) | Public URLs for dev servers | | [Giving agents a browser](docs/agent-browser.md) | T3's server browser and the browser MCP servers | -| [Context management](docs/context-management.md) | Global `AGENTS.md` and skills | +| [Context management](docs/context-management.md) | Global `AGENTS.md`, skills and shared MCP servers | | [Storage management](docs/storage.md) | Disk usage, cache cleanup, version retention | | [Configuration](docs/configuration.md) | Every environment variable, volumes, building | | [Troubleshooting](docs/troubleshooting.md) | Pairing, providers, terminals, permissions | diff --git a/docker/bin/t3-browser-mcp b/docker/bin/t3-browser-mcp index bd6cf85..172cdf5 100644 --- a/docker/bin/t3-browser-mcp +++ b/docker/bin/t3-browser-mcp @@ -50,8 +50,8 @@ Notes: * Chromium is sandboxed by the container already, so --no-sandbox is passed. * chrome-devtools-mcp officially supports Google Chrome rather than Debian's Chromium; it does work here, but playwright is the tested default. - * Each harness is registered through its managed executable. Install the - harness (setup page or `t3-harness install`) before registering MCP with it. + * Install the managed harness (setup page or `t3-harness install`) first. + Claude uses its native CLI; Codex/OpenCode use the setup config adapters. USAGE } @@ -105,8 +105,6 @@ harness_exec() { # id fi } -json_args="$(printf '%s\n' "${args[@]}" | jq -R . | jq -s .)" - register_claude() { local bin bin="$(harness_exec claude)" @@ -124,60 +122,33 @@ register_claude() { fi } +# Use the console's adapters so both entry points agree on JSONC precedence, +# OpenCode V2 nesting, TOML sections, backups and file symlinks. +write_browser_entry() { + "$T3_INFRA_NODE" --input-type=module - "$1" "$name" "$action" "$cmd" "${args[@]}" <<'NODE' +import * as io from "node:fs/promises"; +const { writeProviderMcpEntry } = await import(process.env.T3_MCP_ADAPTERS_MODULE || "file:///opt/t3-setup/mcp/adapters.mjs"); +const [provider, name, action, command, ...args] = process.argv.slice(2); +const entry = action === "remove" ? null : provider === "codex" + ? { command, args } : { type: "local", command: [command, ...args], enabled: true }; +await writeProviderMcpEntry(io, provider, { home: process.env.HOME, env: process.env }, name, entry); +NODE +} + register_codex() { local bin bin="$(harness_exec codex)" [ -n "$bin" ] || { echo " codex: not installed"; return; } - local config="${CODEX_HOME:-$HOME/.codex}/config.toml" - mkdir -p "$(dirname "$config")" - touch "$config" - - # Drop any block we previously wrote, then re-add if needed. - local tmp; tmp="$(mktemp)" - awk -v marker="# >>> t3-browser-mcp:${name} >>>" \ - -v endmarker="# <<< t3-browser-mcp:${name} <<<" ' - $0 == marker { skip = 1 } - skip != 1 { print } - $0 == endmarker { skip = 0 } - ' "$config" > "$tmp" - mv "$tmp" "$config" - - if [ "$action" = "remove" ]; then - echo " codex: removed $name" - return - fi - - { - printf '\n# >>> t3-browser-mcp:%s >>>\n' "$name" - printf '[mcp_servers.%s]\n' "$name" - printf 'command = "%s"\n' "$cmd" - printf 'args = %s\n' "$(printf '%s' "$json_args" | jq -c .)" - printf '# <<< t3-browser-mcp:%s <<<\n' "$name" - } >> "$config" - echo " codex: registered $name in $config (via $bin)" + write_browser_entry codex + echo " codex: $action $name (via $bin)" } register_opencode() { local bin bin="$(harness_exec opencode)" [ -n "$bin" ] || { echo " opencode: not installed"; return; } - local config="${XDG_CONFIG_HOME:-$HOME/.config}/opencode/opencode.json" - mkdir -p "$(dirname "$config")" - [ -s "$config" ] || echo '{"$schema":"https://opencode.ai/config.json"}' > "$config" - - local tmp; tmp="$(mktemp)" - if [ "$action" = "remove" ]; then - jq --arg n "$name" 'del(.mcp[$n])' "$config" > "$tmp" - mv "$tmp" "$config" - echo " opencode: removed $name" - return - fi - - jq --arg n "$name" --arg c "$cmd" --argjson a "$json_args" \ - '.mcp[$n] = {type: "local", command: ([$c] + $a), enabled: true}' \ - "$config" > "$tmp" - mv "$tmp" "$config" - echo " opencode: registered $name in $config (via $bin)" + write_browser_entry opencode + echo " opencode: $action $name (via $bin)" } printf '%s %s (%s)\n' \ diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 7f79347..56ab57a 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -268,9 +268,9 @@ register_projects # and az keep their sign-ins in these. AGENT_DIRS=".claude .codex .cursor .grok .config/opencode .local/share/opencode .config/muse .local/share/muse .agents/skills .local/state/skills .gemini/config/skills .gemini/antigravity-cli/skills" AGENT_DIRS="$AGENT_DIRS .config/gh .config/glab-cli .config/tea .local/share/forgejo-cli .azure" -# And git's own settings: who you commit as, and the credential helpers gh and -# tea set up so an HTTPS push uses their sign-in. git writes through the link. -AGENT_FILES=".gitconfig .agents/.skill-lock.json" +# And git's own settings, plus Claude's global account/MCP config outside its +# config directory. Git and Claude's native writer preserve these file links. +AGENT_FILES=".gitconfig .agents/.skill-lock.json .claude.json" persist_agent_credentials() { [ "$T3_PERSIST_AGENT_CREDENTIALS" = "1" ] || return 0 @@ -299,7 +299,10 @@ persist_agent_credentials() { # The skills CLI owns its JSON lock format. An absent lock stays absent; # its future write follows the link onto the volume without an empty or # invented JSON document. This does not move the rest of .agents. - if [ "$rel" != ".agents/.skill-lock.json" ]; then + if [ "$rel" = ".claude.json" ]; then + [ -e "$dst" ] || printf '{}\n' > "$dst" + chmod 600 "$dst" + elif [ "$rel" != ".agents/.skill-lock.json" ]; then [ -e "$dst" ] || : > "$dst" fi ;; diff --git a/docker/setup/app.js b/docker/setup/app.js index cb29296..127d30a 100644 --- a/docker/setup/app.js +++ b/docker/setup/app.js @@ -136,6 +136,7 @@ const storageUI = T3StorageUI.create({ api, getStatus: () => state.status, reload: () => loadStatus(), render: () => render() }); const skillsUI = T3SkillsUI.create({ api, getStatus: () => state.status, render: () => render(), readMarkdown: (file) => readMarkdownFile(file, 64 * 1024), downloadMarkdown: (...args) => downloadMarkdown(...args) }); + const mcpUI = T3McpUI.create({ api, getStatus: () => state.status, render: () => render() }); /** Whether this browser came in on its T3 Code session rather than the key, so Lock would end nothing. */ const viaT3 = (s) => Boolean(s && s.viewer && s.viewer.via === 't3'); @@ -212,6 +213,7 @@ tellAttention(); checkHere(); if (state.route === 'instructions' && state.contextTab === 'skills') skillsUI.activate(); + if (state.route === 'instructions' && state.contextTab === 'mcp') mcpUI.activate(); if (state.route === 'instructions' && state.contextTab === 'instructions') void loadInstructionsPreview(); } else if (!locked) { // Name the URL and the reason. "Could not read status" sent someone @@ -1615,8 +1617,8 @@ `; }; - const CONTEXT_TABS = [['instructions', 'Instructions'], ['skills', 'Skills']]; - /** Instructions and skills: two tabs on one page, each at an address of its own. */ + const CONTEXT_TABS = [['instructions', 'Instructions'], ['skills', 'Skills'], ['mcp', 'MCP servers']]; + /** Instructions, skills and MCP servers: three tabs on one page, each at an address of its own. */ const instructionsPage = (s) => { const tab = state.contextTab; const rows = M.contextProviderRows(s); @@ -1624,6 +1626,8 @@ const previewLines = instructionsPreview.content.split('\n', 19); const panel = tab === 'skills' ? skillsUI.content() + : tab === 'mcp' + ? mcpUI.content() : html` ${view.error ? notice('danger', 'circle-alert', 'Couldn’t read the instructions', view.error) : ''}

Shared instructions

${M.contextAgentsMdRow(s).readers.length ? 'Read by ' + M.listOf(M.contextAgentsMdRow(s).readers) : 'No agents set to Shared'}
@@ -1638,7 +1642,7 @@ return html`
${icon('chevron-right', 'tc-back-icon')}Setup

Instructions & skills

${EMBED ? html`` : html`${icon('info')}`}
${pageHead('instructions')} -
${CONTEXT_TABS.map(([value, label]) => html``)}
+
${CONTEXT_TABS.map(([value, label]) => html``)}
${pageNotices('instructions')}
${panel}
`; }; @@ -1850,7 +1854,7 @@ const [name, sub] = location.hash.replace(/^#\/?/, '').split('?')[0].split('/'); const route = T3Routes.resolve(name); if (route && route !== name) history.replaceState(null, '', '#' + route + (sub ? '/' + sub : '')); - if (route === 'instructions') state.contextTab = sub === 'skills' ? 'skills' : 'instructions'; + if (route === 'instructions') state.contextTab = sub === 'skills' ? 'skills' : sub === 'mcp' ? 'mcp' : 'instructions'; return route && PAGES[route] ? route : 'overview'; }; const go = (route) => { @@ -1880,6 +1884,7 @@ if (route === 'ports' && !state.ports) loadPorts(); if (route === 'instructions' && state.contextTab === 'skills' && state.status) skillsUI.activate(true); if (route === 'instructions' && state.contextTab === 'instructions' && state.status) void loadInstructionsPreview(true); + if (route === 'instructions' && state.contextTab === 'mcp' && state.status) mcpUI.activate(); }; const navigateFromHash = async focus => { const version = ++navigationVersion; @@ -1888,7 +1893,8 @@ // keeps the old route, tab and draft together. if (!navigationTask) navigationTask = (async () => { if (contextEditor.layer && !await contextEditor.requestClose()) return false; - return skillsUI.beforeNavigate(); + if (!await skillsUI.beforeNavigate()) return false; + return mcpUI.beforeNavigate(); })(); const allowed = await navigationTask; if (version !== navigationVersion) return; @@ -3509,6 +3515,11 @@ 'skills.group': (a) => skillsUI.group(a.id), 'skills.copy': (a) => skillsUI.copy(a.id, a.provider), 'skills.copy-group': (a) => skillsUI.copyGroup(a.id), + 'mcp.refresh': () => mcpUI.refresh(), + 'mcp.view': (a) => mcpUI.view(a.id), + 'mcp.add': () => mcpUI.add(), + 'mcp.link': (a) => mcpUI.toggle(a.provider, a.id), + 'mcp.apply': (a) => mcpUI.apply(a.provider, a.id), 'storage.goto': () => go('storage'), refresh: () => { loadStatus(); loadPorts(); }, goto: (a) => go(a.route), @@ -3737,7 +3748,7 @@ 'link.revoke': (a) => confirmRevoke('pairing', a.id), 'context.tab': (a) => { - const hash = '#instructions' + (a.value === 'skills' ? '/skills' : ''); + const hash = '#instructions' + (a.value === 'skills' ? '/skills' : a.value === 'mcp' ? '/mcp' : ''); if (location.hash !== hash) location.hash = hash; }, 'context.preview.retry': () => loadInstructionsPreview(true), @@ -3868,6 +3879,7 @@ render(); }); document.addEventListener('change', (e) => { + if (e.target.id === 'mcp-provider') mcpUI.select(e.target.value); if (e.target.hasAttribute('data-skill-policy')) skillsUI.setState(e.target.getAttribute('data-skill-policy'), e.target.getAttribute('data-skill-provider'), e.target.value); }); diff --git a/docker/setup/client/mcp.js b/docker/setup/client/mcp.js new file mode 100644 index 0000000..1b7910d --- /dev/null +++ b/docker/setup/client/mcp.js @@ -0,0 +1,237 @@ +// Shared MCP servers: one canonical list, linked per provider. Status polls +// carry redacted summaries; secrets load only in the editor. +const T3McpUI = (() => { + 'use strict'; + const { html, raw, icon } = Kit; + const M = T3Model; + const providers = [['claude', 'Claude Code'], ['codex', 'Codex'], ['opencode', 'OpenCode'], ['cursor', 'Cursor'], ['grok', 'Grok Build'], ['muse', 'Muse Code'], ['antigravity', 'Antigravity']]; + const providerName = (id) => providers.find(([key]) => key === id)?.[1] || id; + + const create = ({ api, getStatus, render }) => { + let provider = null, shared = null, loading = false, error = null, generation = 0, viewer = null, linking = null; + const snapshot = () => ({ ...getStatus(), context: { ...getStatus()?.context, mcp: shared || getStatus()?.context?.mcp } }); + + const chooseDefault = () => { + const installed = (getStatus()?.harnesses || []).filter((row) => row.installed).map((row) => row.id); + return providers.find(([id]) => installed.includes(id))?.[0] || 'claude'; + }; + + const load = async (refresh = false) => { + if (loading) return; + provider ||= chooseDefault(); + const current = ++generation; + loading = true; error = null; render(); + const response = await api('/context/mcp' + (refresh ? '?refresh=1' : '')); + if (current !== generation) return; + loading = false; + if (response.ok) shared = response.data; + else error = response.error; + render(); + }; + + const parseJson = (text, array) => { + let out; + try { out = JSON.parse(text); } catch { throw new Error(array ? 'Enter the command as a JSON array of strings.' : 'Enter values as a JSON object of strings.'); } + if (!out || typeof out !== 'object' || Array.isArray(out) !== array || !Object.values(out).every((value) => typeof value === 'string')) throw new Error(array ? 'Enter a JSON array of strings.' : 'Enter a JSON object of strings.'); + return out; + }; + + const serverRow = (server, selected) => { + const differs = selected?.diverged?.includes(server.name); + const linked = differs || selected?.linked?.includes(server.name); + const managed = selected?.supported && !selected.error; + const canApply = server.enabled !== false || ['codex', 'opencode'].includes(provider); + const busy = loading || Boolean(linking); + const detail = server.type === 'local' ? (server.command || []).join(' ') : server.url; + return html`
+ + +
+ ${managed ? html`${differs && canApply ? html`` : ''} + ` : ''} +
+
`; + }; + + const content = () => { + const view = M.mcpView(snapshot()); + const selected = shared?.providers?.find((row) => row.provider === (provider || chooseDefault())); + const servers = shared?.shared?.servers || []; + const unavailable = selected?.supported && selected.error; + return html`
+
${shared && !loading ? M.plural(servers.length, 'shared server') : ''}
+ +
+
+ ${loading ? html`

Reading MCP servers…

` : ''} + ${error ? html`` : ''} + ${view.error && !loading ? html`

${icon('triangle-alert')}${view.error}

` : ''} + ${!loading && !error && shared ? html` + ${selected && (!selected.supported || selected.error) ? html`

${icon(unavailable ? 'triangle-alert' : 'info')}${unavailable ? 'Couldn’t read ' + providerName(provider) + ' settings: ' : ''}${selected.error || 'MCP settings for this agent are not managed here.'} You can still edit shared definitions.

` : ''} + ${selected?.warning ? html`

${icon('triangle-alert')}${selected.warning}

` : ''} + ${servers.length ? html`
${servers.map((server) => serverRow(server, selected))}
` : html`
No shared servers yetAdd a server, then choose which agents should use it.
`} + ${selected?.path ? html`
${providerName(provider)} settings${selected.ownCount ? ' · ' + M.plural(selected.ownCount, 'other server') : ''}${selected.path}

${selected.ownCount ? 'Other servers in this file: ' + (selected.own?.join(', ') || selected.ownCount) + '. They stay unchanged.' : 'Only same-name entries are changed when you link, apply or unlink.'}

` : ''} +
How sharing works

Definitions are shared across agents. Saving leaves existing agent copies unchanged; choose Apply to update a copy marked Differs. Switches show saved entries, including disabled ones. They do not show live connection status.

This page manages global settings. Project settings and custom T3 agent profiles can differ. Codex and OpenCode support disabled definitions here; unlink for Claude and Cursor. Start a new session to load changes.

Remote servers use Streamable HTTP. Sign in through your agent when required: claude mcp login, codex mcp login, opencode mcp auth or cursor-agent mcp login.

+ ` : ''} +
+
${icon('info', 'tc-icon--sm')}Choose an agent, then link the shared servers it should use. After editing a definition, Apply updates that agent’s copy.
+
`; + }; + + const open = async (name = null) => { + if (viewer && !await viewer.close('replace')) return; + const creating = !name; + const current = { creating, name: name || '', loading: !creating, data: null, revision: null, error: null, layer: null, saving: false, + draft: { name: name || '', type: 'local', command: '', url: '', env: '{}', headers: '{}', enabled: true }, original: '' }; + current.original = JSON.stringify(current.draft); + const dirty = () => JSON.stringify(current.draft) !== current.original; + const cannotSave = () => current.saving || current.loading || (!creating && !current.data) || !current.draft.name.trim() || (!creating && !dirty()); + const beforeUnload = event => { if (current.saving || dirty()) { event.preventDefault(); event.returnValue = ''; } }; + const requestClose = async (reason = 'close') => { + if (current.saving || current.closing) return false; + if (dirty()) { + current.closing = true; + const yes = await Kit.confirm({ title: 'Discard changes?', body: 'Your edits to this shared server have not been saved.', confirm: 'Discard changes', tone: 'danger' }); + current.closing = false; + if (!yes || viewer !== current || current.saving) return false; + } + current.layer.close(reason); return true; + }; + current.close = requestClose; + const title = creating ? 'Add shared MCP server' : 'Edit ' + name; + current.layer = Kit.open({ kind: 'center', + panel: { tag: 'div', class: 'tc-sheet tc-context-sheet tc-mcp-editor ' + (Kit.isPhone() ? 'tc-sheet--bottom' : 'tc-sheet--inset'), role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'mcp-title' }, + backdrop: 'ignore', onEscape: () => { void requestClose('escape'); }, + onClose: () => { window.removeEventListener('beforeunload', beforeUnload); if (viewer === current) viewer = null; }, + render: () => html`

${title}

Shared definition for all agents
+
+ ${current.error ? html`` : ''} + ${current.loading ? html`

Reading server…

` : creating || current.data ? html` + + +

Use lowercase letters, numbers and single hyphens. To rename a server, remove it and add it again.

+ + + ${current.draft.type === 'local' ? html`` : html``} + + + +

For Claude and Cursor, unlink to disable. Saving does not change existing agent copies.

+

Put secrets in environment values or headers; commands are visible in the list. Variable references follow each agent’s own rules.

+ ` : ''} +
+ ${!creating ? html`` : ''} +
`, + }); + viewer = current; + window.addEventListener('beforeunload', beforeUnload); + const panel = current.layer.panel; + const sync = () => current.layer.render(); + const syncSave = () => { panel.querySelector('[data-mcp=save]').disabled = cannotSave(); }; + panel.addEventListener('keydown', (event) => { + if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') { + event.preventDefault(); panel.querySelector('[data-mcp=save]').click(); + } + }); + panel.addEventListener('input', (event) => { + if (event.target.id === 'mcp-name') current.draft.name = event.target.value; + if (event.target.id === 'mcp-url') current.draft.url = event.target.value; + if (event.target.id === 'mcp-command') current.draft.command = event.target.value; + if (event.target.id === 'mcp-pairs') current.draft[current.draft.type === 'local' ? 'env' : 'headers'] = event.target.value; + syncSave(); + }); + panel.addEventListener('change', (event) => { + if (event.target.id === 'mcp-type') { current.draft.type = event.target.value; sync(); } + }); + panel.addEventListener('click', async (event) => { + const button = event.target.closest('button'); + if (!button || button.disabled) return; + if (button.hasAttribute('data-mcp') && button.getAttribute('data-mcp') === 'close') { await requestClose(); return; } + if (button.id === 'mcp-enabled') { current.draft.enabled = !current.draft.enabled; button.setAttribute('aria-checked', String(current.draft.enabled)); syncSave(); return; } + if (button.getAttribute('data-mcp') === 'remove') { + const yes = await Kit.confirm({ title: 'Remove ' + current.name + '?', body: 'Unlink this server from every agent first. Removing it here deletes the shared definition. Existing sessions and compatibility imports can still use their own copies.', confirm: 'Remove server', tone: 'danger' }); + if (!yes || viewer !== current || current.saving) return; + current.saving = true; current.error = null; current.layer.render(); + const response = await api('/context/mcp/remove', { body: { name: current.name, revision: current.revision } }); + current.saving = false; + if (!response.ok) { if (viewer === current) { current.error = response.error; current.layer.render(); } return; } + if (viewer === current) current.layer.close('saved'); + Kit.toast('Removed shared MCP server'); + generation++; loading = false; + await load(true); + return; + } + if (button.getAttribute('data-mcp') === 'save') { + try { + const pairs = parseJson(current.draft[current.draft.type === 'local' ? 'env' : 'headers'], false); + const server = current.draft.type === 'local' + ? { name: (current.draft.name || '').trim(), type: 'local', enabled: current.draft.enabled, + command: parseJson(current.draft.command, true), env: pairs } + : { name: (current.draft.name || '').trim(), type: 'remote', enabled: current.draft.enabled, + url: (current.draft.url || '').trim(), headers: pairs }; + current.saving = true; current.error = null; current.layer.render(); + const response = creating + ? await api('/context/mcp/create', { body: { server } }) + : await api('/context/mcp/save', { body: { name: current.name, revision: current.revision, server } }); + current.saving = false; + if (!response.ok) { if (viewer === current) { current.error = response.error; current.layer.render(); } return; } + if (viewer === current) current.layer.close('saved'); + Kit.toast(creating ? 'Added shared MCP server' : 'Saved shared MCP server'); + generation++; loading = false; + await load(true); + } catch (err) { + current.saving = false; current.error = err.message; current.layer.render(); + } + } + }); + if (creating) { current.loading = false; current.layer.render(); return; } + const response = await api('/context/mcp/read?name=' + encodeURIComponent(name)); + if (viewer !== current) return; + current.loading = false; + if (!response.ok) { current.error = response.error; current.layer.render(); return; } + const server = response.data.server; + current.revision = response.data.revision; + current.data = server; + current.draft = { name: server.name, type: server.type, enabled: server.enabled !== false, + command: JSON.stringify(server.command || [], null, 2), url: server.url || '', + env: JSON.stringify(server.env || {}, null, 2), headers: JSON.stringify(server.headers || {}, null, 2) }; + current.original = JSON.stringify(current.draft); + current.layer.render(); + }; + + const setLink = async (provider, name, apply = false) => { + if (linking) return; + if (loading || error) return; + const current = shared?.providers?.find((row) => row.provider === provider); + const sharedRevision = shared?.shared?.revision; + if (!current?.revision) return; + const differs = (current.diverged || []).includes(name); + const linked = apply || ![...(current.linked || []), ...(current.diverged || [])].includes(name); + if (differs || !linked) { + const yes = await Kit.confirm({ title: linked ? 'Apply shared ' + name + '?' : 'Unlink ' + name + '?', body: linked ? 'Replaces the complete ' + providerName(provider) + ' entry, including its agent-specific options. A private backup is kept beside the settings file.' : 'Removes this entry from ' + providerName(provider) + ' global settings. A private backup is kept. Existing sessions, project settings and compatibility imports can still use the server.', confirm: linked ? 'Apply shared' : 'Unlink', tone: linked ? 'primary' : 'danger' }); + if (!yes || linking) return; + } + linking = provider; render(); + const response = await api('/context/mcp/link', { body: { provider, name, linked, sharedRevision, providerRevision: current.revision } }); + linking = null; + if (!response.ok) Kit.toast('Could not change MCP link', { tone: 'danger', detail: response.error }); + else Kit.toast(linked ? 'Applied ' + name + ' to ' + providerName(provider) : 'Removed ' + name + ' from ' + providerName(provider) + ' global config'); + generation++; loading = false; + await load(true); + }; + + return { content, load, beforeNavigate: () => viewer ? viewer.close('route') : true, refresh: () => load(true), add: () => open(null), view: (id) => open(id), + activate: () => { if (!shared && !loading && !error) load(); }, + toggle: (provider, name) => setLink(provider, name), + apply: (provider, name) => setLink(provider, name, true), + select: (id) => { if (!linking && providers.some(([key]) => key === id)) { provider = id; render(); } }, + }; + }; + return { create }; +})(); diff --git a/docker/setup/client/model.js b/docker/setup/client/model.js index 59e5b4a..980e00b 100644 --- a/docker/setup/client/model.js +++ b/docker/setup/client/model.js @@ -1891,6 +1891,15 @@ const T3Model = (() => { }); }; + // Shared MCP servers: redacted summaries in /status, secrets only in the + // editor. Provider support is uneven; unsupported providers stay visible + // with their reason instead of failing the whole section. + const mcpView = (status) => { + const mcp = status?.context?.mcp || {}; + const shared = mcp.shared || {}; + return { present: Boolean(shared.present), count: shared.count || (shared.servers || []).length, revision: shared.revision || null, + bytes: shared.bytes || 0, error: shared.error || null, ok: mcp.ok !== false }; + }; // Automatic updates: the policy and last run from /status, as the Updates // page's switches say them. Pure, so the words are testable without a browser. const autoUpdateView = (status, now) => { @@ -1942,6 +1951,7 @@ const T3Model = (() => { readiness, readySummary, needsYou, containerItems, activity, setupBanner, setupFailures, publicUrlSource, hereOffer, connectView, contextView, contextAgentsMdRow, contextProviderRows, + mcpView, navBadges, serverWarnings, diskWarning, attentionCount, summaries, updatesView, runtimeView, autoUpdateView, paletteItems, searchPalette, redactedDiagnostics, storageView, diff --git a/docker/setup/client/routes.js b/docker/setup/client/routes.js index fa6e045..dc5ef2c 100644 --- a/docker/setup/client/routes.js +++ b/docker/setup/client/routes.js @@ -22,7 +22,7 @@ const T3Routes = (() => { { route: 'agents', title: 'Agents', icon: 'bot', key: 'a', group: 'agents', summary: 'Install, sign in and update your coding agents.', doc: 'agents.md' }, { route: 'instructions', title: 'Instructions & skills', icon: 'book-open', key: 'i', group: 'agents', - summary: 'What your agents read at the start of every session.', doc: 'context-management.md' }, + summary: 'Global instructions, skills and MCP servers for your agents.', doc: 'context-management.md' }, { route: 'toolchains', title: 'Toolchains', icon: 'wrench', key: 't', group: 'tools', summary: 'Languages and tools on every agent’s path, at exact versions.', doc: 'toolchains.md' }, { route: 'sourcecontrol', title: 'Source control', icon: 'git-pull-request', key: 's', group: 'tools', diff --git a/docker/setup/console.css b/docker/setup/console.css index c58c221..35aa838 100644 --- a/docker/setup/console.css +++ b/docker/setup/console.css @@ -647,6 +647,8 @@ html.tc-locked { overflow: hidden; } .tc-context-sheet .tc-sheet-head, .tc-context-sheet .tc-sheet-foot { flex-shrink: 0; } .tc-context-sheet .tc-sheet-foot { flex-wrap: wrap; } .tc-context-sheet .tc-input--area { box-sizing: border-box; width: 100%; min-height: min(42dvh, 420px); resize: vertical; font-family: var(--font-mono); line-height: 1.6; } +.tc-mcp-editor .tc-sheet-body { display: flex; flex-direction: column; } +.tc-mcp-editor textarea.tc-input { flex: none; width: 100%; min-height: 88px; height: auto; resize: vertical; line-height: 1.5; } .tc-context-path { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); } .tc-editor-toolbar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-2) var(--space-3); } .tc-editor-toolbar .tc-label { margin: 0; } @@ -655,6 +657,33 @@ html.tc-locked { overflow: hidden; } .tc-editor-meta { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 2px var(--space-4); font-size: 12px; line-height: 18px; color: var(--fg-muted); } .tc-editor-meta > span:first-child { font-variant-numeric: tabular-nums; white-space: nowrap; } +/* Shared MCP server list, independent of the Skills library layout. */ +.tc-mcp-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3); padding: var(--space-4); } +.tc-mcp-toolbar .tc-label { margin: 0; } +.tc-mcp-toolbar select { width: auto; min-width: 140px; max-width: 100%; } +.tc-mcp-count { white-space: nowrap; } +.tc-mcp-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); } +@media (max-width: 639.98px) { + .tc-mcp-actions { width: 100%; } + .tc-mcp-actions > .tc-btn:not(.tc-btn--icon) { flex: 1 1 auto; } +} +.tc-mcp-message { display: flex; gap: var(--space-2); margin: 0; padding: var(--space-3) var(--space-4); overflow-wrap: anywhere; } +.tc-mcp-message > .tc-icon { flex: none; margin-top: 1px; } +/* Server details open the shared editor; the trailing control links this agent. */ +.tc-mcp-open { appearance: none; border: 0; background: none; padding: 0; margin: 0; font: inherit; color: inherit; text-align: left; cursor: pointer; } +.tc-mcp-open:hover .tc-row-name { text-decoration: underline; text-decoration-color: var(--border-strong); text-underline-offset: 3px; } +.tc-mcp-open:focus-visible { outline: 2px solid var(--focus); outline-offset: 4px; border-radius: var(--radius-sm); } +.tc-mcp-open .tc-row-name { overflow-wrap: anywhere; } +.tc-mcp-description { display: block; min-width: 0; font-size: 12px; line-height: 18px; color: var(--fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +@media (max-width: 639.98px) { + .tc-mcp-description { white-space: normal; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; } +} +.tc-mcp-locations { padding: var(--space-3) var(--space-4); font-size: 12px; color: var(--fg-muted); } +.tc-mcp-locations summary { width: fit-content; cursor: pointer; } +.tc-mcp-locations summary:hover { color: var(--fg); } +.tc-mcp-locations code { display: block; margin-top: var(--space-2); overflow-wrap: anywhere; } +.tc-mcp .tc-warning { color: var(--warn-text); } + /* Tabs under a page's head: the page in two halves, each at its own address. */ .tc-tabs { display: flex; gap: var(--space-6); padding: 0 var(--space-4); margin-top: calc(var(--space-3) - var(--_gap)); border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent); } diff --git a/docker/setup/design/ui.js b/docker/setup/design/ui.js index 15482ab..3a24be7 100644 --- a/docker/setup/design/ui.js +++ b/docker/setup/design/ui.js @@ -3,6 +3,7 @@ shipped console (docker/setup/app.js). Markup carries the state; this file only wires icons and the few interactions every surface shares. */ (function () { + // Lucide plug, used by the shared MCP list and editor. var ICONS = {"layout-dashboard": " ", "smartphone": " ", "bot": " ", "wrench": "", "git-pull-request": " ", "ethernet-port": " ", "settings-2": " ", "search": " ", "command": "", "copy": " ", "check": "", "x": " ", "external-link": " ", "qr-code": " ", "ellipsis": " ", "circle-arrow-up": " ", "download": " ", "trash-2": " ", "key-round": " ", "log-in": " ", "plus": " ", "refresh-cw": " ", "circle-alert": " ", "triangle-alert": " ", "info": " ", "circle-check": " ", "loader-circle": "", "sun": " ", "moon": "", "monitor": " ", "chevron-right": "", "chevron-down": "", "lock": " ", "link": " ", "globe": " ", "circle-stop": " ", "activity": "", "terminal": " ", "server": " ", "shield-check": " ", "eye": " ", "eye-off": " ", "laptop": "", "log-out": " ", "arrow-right": " ", "history": " "}; var SAMPLE = { qrPair: "\n", qrPort: "\n" }; @@ -41,6 +42,7 @@ ICONS.sparkles = ''; ICONS['file-text'] = ''; + ICONS.plug = ''; function agentMark(name, cls) { var mark = AGENT_MARKS[name]; if (!mark) return ''; diff --git a/docker/setup/mcp.mjs b/docker/setup/mcp.mjs new file mode 100644 index 0000000..f914aa4 --- /dev/null +++ b/docker/setup/mcp.mjs @@ -0,0 +1,149 @@ +// Shared MCP servers: canonical store plus per-provider link state derived +// by comparison (no new manifest format). No MCP execution here. +import path from "node:path"; +import { mcpStorePathFor, readMcpStore, createMcpServer, saveMcpServer, removeMcpServer, redactMcpServer, validateMcpServer, MCP_PROVIDERS, UNSUPPORTED_MCP_PROVIDERS, withStore } from "./mcp/store.mjs"; +import { readProviderMcp, writeProviderMcpEntry, toClaudeEntry, toCodexEntry, toOpenCodeEntry, toCursorEntry, isSupportedMcpProvider } from "./mcp/adapters.mjs"; + +const problem = (code, message) => Object.assign(new Error(message), { code }); +const summaryError = (error) => String(error?.message || error).slice(0, 240); + +export function mcpProviders() { + return [...Object.entries(MCP_PROVIDERS).map(([id, name]) => ({ id, name })), ...Object.entries(UNSUPPORTED_MCP_PROVIDERS).map(([id, reason]) => ({ id, name: { grok: "Grok Build", muse: "Muse Code", antigravity: "Antigravity" }[id], unsupported: reason }))]; +} + +const toEntryFor = (provider, server) => + provider === "claude" ? toClaudeEntry(server) + : provider === "codex" ? toCodexEntry(server) + : provider === "opencode" ? toOpenCodeEntry(server) + : toCursorEntry(server); + +const normalize = (value) => { + if (Array.isArray(value)) return value.map(normalize); + if (value && typeof value === "object") { + return Object.fromEntries(Object.keys(value).sort().map((key) => [key, normalize(value[key])])); + } + return value; +}; +const canonicalEqual = (a, b) => JSON.stringify(normalize(a)) === JSON.stringify(normalize(b)); +// Apply disabled definitions only to Codex/OpenCode. A surviving old copy +// elsewhere remains visible as differing until explicitly unlinked. +const linkable = (provider, server) => server.enabled !== false || provider === "codex" || provider === "opencode"; + +export function createMcpService({ home, env = process.env, io, baseDir, ttlMs = 15000 } = {}) { + if (!io) throw new Error("mcp service needs an fs implementation"); + home = path.resolve(home || env.HOME || "/home/t3"); + baseDir = baseDir || process.env.T3CODE_HOME || path.join(home, ".t3"); + const statusCache = { at: 0, promise: null }; + + const linkState = async (provider, shared) => { + if (UNSUPPORTED_MCP_PROVIDERS[provider]) return { provider, supported: false, error: UNSUPPORTED_MCP_PROVIDERS[provider], linked: [], unlinked: shared.map((s) => s.name), diverged: [], own: [], ownCount: 0 }; + if (!isSupportedMcpProvider(provider)) throw problem("invalid", "Unknown MCP provider."); + try { + const read = await readProviderMcp(io, provider, { home, env }); + const byName = new Map(read.servers.map((row) => [row.name, row])); + const linked = [], unlinked = [], diverged = []; + for (const server of shared) { + const row = byName.get(server.name); + if (!row) { unlinked.push(server.name); continue; } + if (row.canonical && !row.extra && canonicalEqual(row.canonical, server)) { linked.push(server.name); continue; } + // Content differs: a provider-side edit, or a shared edit not yet applied. + diverged.push(server.name); + } + const sharedNames = new Set(shared.map((server) => server.name)); + const own = read.servers.filter((row) => !sharedNames.has(row.name)).map((row) => row.name); + return { provider, supported: true, path: read.path, present: read.present, revision: read.revision, linked, unlinked, diverged, + own, ownCount: own.length, error: null, + warning: read.servers.some((row) => !row.canonical) ? "One or more provider entries use an unsupported shape and are left in place." : null }; + } catch (error) { + return { provider, supported: true, path: null, linked: [], unlinked: shared.map((s) => s.name), own: [], ownCount: 0, error: summaryError(error), code: error?.code }; + } + }; + + const status = async ({ refresh = false } = {}) => { + if (!refresh && statusCache.promise && Date.now() - statusCache.at < ttlMs) return statusCache.promise; + const promise = (async () => { + const store = await readMcpStore(io, baseDir); + const providers = []; + for (const provider of ["claude", "codex", "opencode", "cursor", "antigravity", "grok", "muse"]) { + providers.push(await linkState(provider, store.servers)); + } + return { ok: !store.error, shared: { present: store.present, count: store.servers.length, revision: store.revision, bytes: store.bytes, error: store.error ?? null, + servers: store.servers.map(redactMcpServer) }, providers, scannedAt: Date.now() }; + })(); + statusCache.promise = promise; + statusCache.at = Date.now(); + promise.then(() => {}, () => { statusCache.promise = null; }); + return promise; + }; + const invalidate = () => { statusCache.promise = null; statusCache.at = 0; }; + + const list = async (provider) => { + if (UNSUPPORTED_MCP_PROVIDERS[provider]) throw problem("unsupported", UNSUPPORTED_MCP_PROVIDERS[provider]); + if (!isSupportedMcpProvider(provider)) throw problem("invalid", "Unknown MCP provider."); + const store = await readMcpStore(io, baseDir); + const state = await linkState(provider, store.servers); + if (state.error) throw problem(state.code || "unavailable", state.error); + let detail = null; + try { detail = await readProviderMcp(io, provider, { home, env }); } + catch (error) { throw problem(error?.code || "unavailable", summaryError(error)); } + return { ok: true, provider, path: detail.path, revision: detail.revision, sharedRevision: store.revision, + shared: store.servers.map((server) => { + const row = detail.servers.find((entry) => entry.name === server.name); + const linked = Boolean(row?.canonical && !row.extra && canonicalEqual(row.canonical, server)); + return { ...redactMcpServer(server), + linked, + diverged: Boolean(row && !linked), + linkable: linkable(provider, server), + providerEntry: row && !linked ? "(differs from shared)" : row ? "(matches shared)" : "(not present)", + }; + }), + own: detail.servers.filter((row) => !store.servers.some((server) => server.name === row.name)).map((row) => ({ name: row.name, recognized: Boolean(row.canonical) })), + unsupported: detail.servers.filter((row) => !row.canonical).map((row) => row.name), + }; + }; + + const read = async (name) => { + const store = await readMcpStore(io, baseDir); + if (store.error) throw problem(store.code || "unavailable", store.error); + const server = store.servers.find((entry) => entry.name === name); + if (!server) throw problem("missing", "This shared MCP server no longer exists. Refresh the list."); + return { ok: true, server, revision: store.revision }; + }; + + const setLink = async (provider, name, linked, sharedRevision, providerRevision) => withStore(mcpStorePathFor(baseDir), async () => { + if (UNSUPPORTED_MCP_PROVIDERS[provider]) throw problem("unsupported", UNSUPPORTED_MCP_PROVIDERS[provider]); + if (!isSupportedMcpProvider(provider)) throw problem("invalid", "Unknown MCP provider."); + if (typeof linked !== "boolean") throw problem("invalid", "Expected a boolean linked value."); + const base = baseDir; + const store = await readMcpStore(io, base); + if (store.error) throw problem(store.code || "unavailable", store.error); + if (store.revision !== sharedRevision) throw problem("conflict", "Shared MCP servers changed. Refresh the list before changing links."); + const server = store.servers.find((entry) => entry.name === name); + if (!server) throw problem("missing", "This shared MCP server no longer exists. Refresh the list."); + if (linked && !linkable(provider, server)) { + throw problem("unsupported", "Disabled shared definitions can only be applied to Codex and OpenCode. Unlink this agent’s copy instead."); + } + const result = await writeProviderMcpEntry(io, provider, { home, env }, name, linked ? toEntryFor(provider, server) : null, { revision: providerRevision }); + invalidate(); + return { ...result, ok: true, provider, name, linked }; + }); + + return { + status, list, read, setLink, providers: mcpProviders, + create: async (...args) => { const result = await createMcpServer(io, baseDir, ...args); invalidate(); if (!result.ok && !result.code) throw problem("invalid", result.error); if (!result.ok) throw problem(result.code || "invalid", result.error); return { ok: true, ...result }; }, + save: async (...args) => { const result = await saveMcpServer(io, baseDir, ...args); invalidate(); if (!result.ok && !result.code) throw problem("invalid", result.error); if (!result.ok) throw problem(result.code || "invalid", result.error); return { ok: true, ...result }; }, + remove: async (name, revision) => { + const check = async () => { + for (const provider of Object.keys(MCP_PROVIDERS)) { + const detail = await readProviderMcp(io, provider, { home, env }); + if (detail.servers.some((row) => row.name === name)) throw problem("conflict", "Unlink this server from every provider before removing its shared definition."); + } + }; + const result = await removeMcpServer(io, baseDir, name, revision, check); invalidate(); + if (!result.ok) throw problem(result.code || "invalid", result.error); return { ok: true, ...result }; + }, + validate: validateMcpServer, + }; +} + +export { mcpStorePathFor }; diff --git a/docker/setup/mcp/adapters.mjs b/docker/setup/mcp/adapters.mjs new file mode 100644 index 0000000..22ab25a --- /dev/null +++ b/docker/setup/mcp/adapters.mjs @@ -0,0 +1,156 @@ +// Provider-native config translation. Inventory is read-only; explicit apply +// and unlink use revision-checked, symlink-preserving native-settings writes. +import path from "node:path"; +import { parse, parseTree, modify, applyEdits } from "../vendor/jsonc.mjs"; +import { parseMcpToml, applyMcpToml } from "./toml.mjs"; +import { readConfig, changeConfig } from "./files.mjs"; +import { validateMcpServer } from "./store.mjs"; + +const problem = (code, message) => Object.assign(new Error(message), { code }); +const object = (value) => value && typeof value === "object" && !Array.isArray(value); +const homeOf = (home, env) => path.resolve(home || env.HOME || "/home/t3"); +export const claudeMcpPath = (home, env) => env.CLAUDE_CONFIG_DIR?.trim() + ? path.join(path.resolve(homeOf(home, env), env.CLAUDE_CONFIG_DIR), ".claude.json") : path.join(homeOf(home, env), ".claude.json"); +export const codexMcpPath = (home, env) => path.join(path.resolve(homeOf(home, env), env.CODEX_HOME || ".codex"), "config.toml"); +export const cursorMcpPath = (home, env) => path.join(homeOf(home, env), ".cursor", "mcp.json"); +export const openCodeConfigPath = async (io, home, env) => { + const root = path.resolve(homeOf(home, env), env.OPENCODE_CONFIG_DIR || path.join(env.XDG_CONFIG_HOME || ".config", "opencode")); + const jsonc = path.join(root, "opencode.jsonc"); + try { await io.lstat(jsonc); return { file: jsonc, jsonc: true }; } + catch (e) { if (e.code !== "ENOENT") throw e; return { file: path.join(root, "opencode.json"), jsonc: false }; } +}; + +export function toClaudeEntry(s) { + return s.type === "local" ? { command: s.command[0], args: s.command.slice(1), ...(Object.keys(s.env).length ? { env: { ...s.env } } : {}) } + : { type: "http", url: s.url, ...(Object.keys(s.headers).length ? { headers: { ...s.headers } } : {}) }; +} +export function fromClaudeEntry(name, entry) { + if (!object(entry)) return null; + if (entry.type && !["http", "streamable-http", "stdio"].includes(entry.type)) return null; + if (["http", "streamable-http"].includes(entry.type) && typeof entry.url === "string" && /^https?:\/\//.test(entry.url)) return { name, type: "remote", url: entry.url, headers: { ...(entry.headers ?? {}) }, enabled: true }; + if (typeof entry.command === "string" && entry.command) return { name, type: "local", command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])], env: { ...(entry.env ?? {}) }, enabled: true }; + return null; +} +export function toCodexEntry(s) { + const entry = s.type === "local" ? { command: s.command[0], args: s.command.slice(1), ...(Object.keys(s.env).length ? { env: { ...s.env } } : {}) } + : { url: s.url, ...(Object.keys(s.headers).length ? { http_headers: { ...s.headers } } : {}) }; + if (s.enabled === false) entry.enabled = false; + return entry; +} +export function fromCodexEntry(name, entry) { + if (!object(entry)) return null; + if (typeof entry.url === "string" && entry.url) return { name, type: "remote", url: entry.url, headers: { ...(entry.http_headers ?? {}) }, enabled: entry.enabled !== false }; + if (typeof entry.command === "string" && entry.command) return { name, type: "local", command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])], env: { ...(entry.env ?? {}) }, enabled: entry.enabled !== false }; + return null; +} +export function toOpenCodeEntry(s, v2 = false) { + const state = v2 ? { disabled: s.enabled === false } : { enabled: s.enabled !== false }; + return s.type === "local" ? { type: "local", command: [...s.command], ...(Object.keys(s.env).length ? { environment: { ...s.env } } : {}), ...state } + : { type: "remote", url: s.url, ...(Object.keys(s.headers).length ? { headers: { ...s.headers } } : {}), ...state }; +} +export function fromOpenCodeEntry(name, entry, v2 = false) { + if (!object(entry)) return null; + // The legacy loader can discard the other layout's state flag. It must + // not make inventory claim that a shared disabled definition was applied. + const enabled = v2 ? entry.disabled !== true : entry.enabled !== false; + if (entry.type === "remote" && typeof entry.url === "string") return { name, type: "remote", url: entry.url, headers: { ...(entry.headers ?? {}) }, enabled }; + if (entry.type === "local" && Array.isArray(entry.command)) return { name, type: "local", command: [...entry.command], env: { ...(entry.environment ?? {}) }, enabled }; + return null; +} +export const toCursorEntry = toClaudeEntry; +// Cursor accepts a remote URL without an explicit transport selector. +export function fromCursorEntry(name, entry) { + if (!object(entry) || (entry.type !== undefined && !["http", "stdio"].includes(entry.type))) return null; + return fromClaudeEntry(name, entry.type === undefined && typeof entry.url === "string" ? { ...entry, type: "http" } : entry); +} +export const isSupportedMcpProvider = (provider) => ["claude", "codex", "opencode", "cursor"].includes(provider); + +function jsonDocument(raw, strict) { + const errors = []; + const tree = parseTree(raw || "{}", errors, { allowTrailingComma: !strict, disallowComments: strict }); + const check = (node) => { + if (node.type === "object") { + const keys = node.children.map((p) => p.children[0].value); + if (new Set(keys).size !== keys.length) throw problem("unsupported", "Duplicate keys in the MCP configuration. Repair them before changing servers."); + } + for (const child of node.children ?? []) check(child); + }; + if (errors.length || tree?.type !== "object") throw problem("unsupported", "The MCP configuration is not a valid JSON object. Repair it before changing servers."); + check(tree); + return parse(raw || "{}", [], { allowTrailingComma: !strict, disallowComments: strict }); +} + +function containerFor(provider, value) { + const key = provider === "opencode" ? "mcp" : "mcpServers"; + const root = value[key]; + if (root !== undefined && !object(root)) throw problem("unsupported", "The MCP configuration has an unsupported server section."); + // V1 may contain a real server named 'servers'. Its transport distinguishes + // that entry from V2's server dictionary. Never rewrite V2 back to V1. + const v2 = provider === "opencode" && ["servers", "timeout", "codemode", "protocol"].some((key) => + Object.hasOwn(root ?? {}, key) && !["local", "remote"].includes(root[key]?.type)); + if (v2 && root.servers !== undefined && !object(root.servers)) throw problem("unsupported", "The OpenCode V2 server dictionary must be an object."); + if (v2 && Object.keys(root).some((k) => !["servers", "timeout", "codemode", "protocol"].includes(k))) { + throw problem("unsupported", "OpenCode mixes V1 and V2 MCP entries. Repair its MCP section before changing servers."); + } + return { v2, keys: v2 ? [key, "servers"] : [key], entries: v2 ? root.servers ?? {} : root ?? {} }; +} + +async function fileFor(io, provider, home, env) { + if (provider === "opencode") return (await openCodeConfigPath(io, home, env)).file; + if (provider === "codex") return codexMcpPath(home, env); + if (provider === "claude") return claudeMcpPath(home, env); + if (provider === "cursor") return cursorMcpPath(home, env); + throw problem("unsupported", "MCP management is not supported for this provider."); +} + +// Unknown provider options must be shown as differing, rather than implying +// that cwd/auth/tool-policy differences are represented by the shared entry. +export function hasExtraMcpOptions(provider, entry, v2 = false) { + const remote = typeof entry?.url === "string"; + const keys = provider === "codex" ? [...(remote ? ["url", "http_headers"] : ["command", "args", "env"]), "enabled"] + : provider === "opencode" ? [...(remote ? ["url", "headers"] : ["command", "environment"]), "type", v2 ? "disabled" : "enabled"] + : [...(remote ? ["url", "headers"] : ["command", "args", "env"]), "type"]; + return !object(entry) || Object.keys(entry).some((k) => !keys.includes(k)); +} + +export async function readProviderMcp(io, provider, { home, env = {} }) { + const file = await fileFor(io, provider, home, env); + const snapshot = await readConfig(io, file); + let entries, v2 = false; + if (provider === "codex") entries = [...parseMcpToml(snapshot.content)]; + else { + const container = containerFor(provider, jsonDocument(snapshot.content, provider !== "opencode")); + entries = Object.entries(container.entries); v2 = container.v2; + } + const from = provider === "codex" ? fromCodexEntry : provider === "opencode" ? (name, entry) => fromOpenCodeEntry(name, entry, v2) : provider === "cursor" ? fromCursorEntry : fromClaudeEntry; + return { provider, path: file, present: snapshot.exists, revision: snapshot.revision, + servers: entries.map(([name, entry]) => { + const canonical = from(name, entry); + const malformed = !object(entry) + || ['enabled', 'disabled'].some(k => entry[k] !== undefined && typeof entry[k] !== 'boolean') + || (entry.args !== undefined && (!Array.isArray(entry.args) || !entry.args.every(a => typeof a === 'string'))) + || ['env', 'environment', 'headers', 'http_headers'].some(k => entry[k] !== undefined && + (!object(entry[k]) || ![null, Object.prototype].includes(Object.getPrototypeOf(entry[k])) || !Object.values(entry[k]).every(v => typeof v === 'string'))) + || (['claude', 'cursor'].includes(provider) && entry.type !== undefined && + !(canonical?.type === 'remote' ? ['http', 'streamable-http'] : ['stdio']).includes(entry.type)); + return { name, entry, canonical: !malformed && canonical && validateMcpServer(canonical).ok ? canonical : null, extra: hasExtraMcpOptions(provider, entry, v2) }; + }) }; +} + +export async function writeProviderMcpEntry(io, provider, { home, env = {} }, name, entry, { revision } = {}) { + if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) throw problem("invalid", "Invalid MCP server name."); + const file = await fileFor(io, provider, home, env); + return changeConfig(io, file, revision, (raw) => { + if (provider === "codex") return applyMcpToml(raw, name, entry); + const value = jsonDocument(raw, provider !== "opencode"); + const container = containerFor(provider, value); + let nextEntry = entry; + if (entry && container.v2) { + const { enabled, ...rest } = entry; + nextEntry = { ...rest, disabled: enabled === false }; + } + if (!entry && !Object.hasOwn(container.entries, name)) return raw; + return applyEdits(raw || "{}\n", modify(raw || "{}\n", [...container.keys, name], nextEntry ?? undefined, + { formattingOptions: { insertSpaces: true, tabSize: 2, eol: raw.includes("\r\n") ? "\r\n" : "\n" } })); + }); +} diff --git a/docker/setup/mcp/files.mjs b/docker/setup/mcp/files.mjs new file mode 100644 index 0000000..5a1b625 --- /dev/null +++ b/docker/setup/mcp/files.mjs @@ -0,0 +1,48 @@ +// Share the native-settings queue with skill policy writers. Snapshots cover +// content, resolved target, permissions and file identity, with bounded reads. +import path from "node:path"; +import { randomBytes } from "node:crypto"; +import { configSnapshot, serializeState } from "../skills/state.mjs"; + +const conflict = () => Object.assign(new Error("Provider settings changed. Refresh MCP servers before applying this change."), { code: "conflict" }); +export async function readConfig(io, file) { + const value = await configSnapshot(file, io, true); + if (!value.exists) { + let ancestor = path.dirname(file), suffix = path.basename(file); + for (;;) { + try { value.resolvedPath = path.join(await io.realpath(ancestor), suffix); break; } + catch (e) { if (e.code !== "ENOENT") throw e; suffix = path.join(path.basename(ancestor), suffix); ancestor = path.dirname(ancestor); } + } + } + return { ...value, content: value.exists ? value.content : "" }; +} + +export async function changeConfig(io, file, revision, change) { + await io.mkdir(path.dirname(file), { recursive: true }); + const initial = await readConfig(io, file); + return serializeState(initial.resolvedPath, async () => { + const before = await readConfig(io, file); + if (initial.resolvedPath !== before.resolvedPath || (revision !== undefined && revision !== before.revision)) throw conflict(); + const content = change(before.content); + if (content === before.content) return { ok: true, path: file, revision: before.revision }; + if (Buffer.byteLength(content, "utf8") > 1024 * 1024) throw Object.assign(new Error("The MCP configuration would exceed the 1 MiB limit."), { code: "too_large" }); + const suffix = `${process.pid}.${randomBytes(8).toString("hex")}`; + const tmp = `${before.resolvedPath}.tmp.${suffix}`; + const backupPath = before.exists ? `${before.resolvedPath}.t3-mcp-backup.${suffix}` : null; + try { + const mode = before.mode & 0o600; + await io.writeFile(tmp, content, { encoding: "utf8", mode, flag: "wx" }); + await io.chmod(tmp, mode); + if ((await readConfig(io, file)).revision !== before.revision) throw conflict(); + if (backupPath) await io.writeFile(backupPath, before.content, { encoding: "utf8", mode: 0o600, flag: "wx" }); + if (before.exists) { + if ((await readConfig(io, file)).revision !== before.revision) throw conflict(); + await io.rename(tmp, before.resolvedPath); + } else { + try { await io.link(tmp, before.resolvedPath); } + catch (e) { if (e.code === "EEXIST") throw conflict(); throw e; } + } + return { ok: true, path: file, backupPath, revision: (await readConfig(io, file)).revision }; + } finally { await io.rm(tmp, { force: true }); } + }); +} diff --git a/docker/setup/mcp/store.mjs b/docker/setup/mcp/store.mjs new file mode 100644 index 0000000..23db7d2 --- /dev/null +++ b/docker/setup/mcp/store.mjs @@ -0,0 +1,212 @@ +// Shared MCP server store. Canonical JSON on the state volume; provider +// files are only touched by explicit link/unlink saves. +import path from "node:path"; +import { createHash, randomBytes } from "node:crypto"; + +export const MCP_PROVIDERS = Object.freeze({ + claude: "Claude Code", + codex: "Codex", + opencode: "OpenCode", + cursor: "Cursor", +}); +export const SUPPORTED_MCP_PROVIDERS = Object.freeze(Object.keys(MCP_PROVIDERS)); +// Keep native formats without a verified integration explicitly unavailable. +export const UNSUPPORTED_MCP_PROVIDERS = Object.freeze({ + antigravity: "Antigravity uses isolated T3 profiles; shared MCP settings are not integrated yet.", + grok: "Grok MCP management is not implemented. Grok may also load Claude and Cursor MCP settings through its native compatibility imports.", + muse: "Muse MCP management is not implemented yet.", +}); + +const MAX_STORE_BYTES = 256 * 1024; +const MAX_SERVERS = 128; +export const mcpDirFor = (baseDir) => path.join(baseDir, "setup", "mcp"); +export const mcpStorePathFor = (baseDir) => path.join(mcpDirFor(baseDir), "servers.json"); + +export const MCP_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/; +const problem = (code, message) => Object.assign(new Error(message), { code }); + +export function validateMcpServer(input) { + const value = input && typeof input === "object" ? input : null; + if (!value) return { ok: false, error: "Expected an MCP server object." }; + const name = String(value.name ?? "").trim(); + if (!MCP_NAME_RE.test(name) || name.length > 64) return { ok: false, error: "Use a server name of 1–64 lowercase letters, numbers and single hyphens." }; + const type = value.type === "remote" ? "remote" : value.type === "local" ? "local" : null; + if (!type) return { ok: false, error: "Choose a local or remote server type." }; + const enabled = value.enabled === undefined ? true : value.enabled; + if (typeof enabled !== "boolean") return { ok: false, error: "Expected enabled to be a boolean." }; + const out = { name, type, enabled }; + if (type === "local") { + const command = Array.isArray(value.command) ? value.command : null; + if (!command || command.length < 1 || command.length > 32 || !command[0] || !command.every((part) => typeof part === "string" && part.length <= 1024)) { + return { ok: false, error: "A local server needs an executable and at most 31 arguments, each up to 1024 characters." }; + } + if (command.some((part) => part.includes("\0"))) return { ok: false, error: "Command entries must not contain NUL bytes." }; + out.command = [...command]; + const env = value.env === undefined ? {} : value.env; + if (!env || typeof env !== "object" || Array.isArray(env)) return { ok: false, error: "Expected env to be an object." }; + const keys = Object.keys(env); + if (keys.length > 64) return { ok: false, error: "A server holds at most 64 environment entries." }; + for (const key of keys) { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || key.length > 128) return { ok: false, error: `Invalid environment name: ${key}` }; + if (typeof env[key] !== "string" || env[key].length > 8192 || env[key].includes("\0")) return { ok: false, error: `Invalid environment value for ${key}.` }; + } + out.env = Object.fromEntries(keys.sort().map((key) => [key, String(env[key])])); + } else { + const url = String(value.url ?? "").trim(); + try { + const parsed = new URL(url); + if (!["http:", "https:"].includes(parsed.protocol) || !parsed.hostname || url.length > 2048 || /[\r\n\0]/.test(url)) throw new Error(); + if (parsed.username || parsed.password) return { ok: false, error: "Put remote credentials in Headers, not in the URL." }; + } catch { return { ok: false, error: "A remote server needs a valid http(s) URL of at most 2048 characters." }; } + out.url = url; + const headers = value.headers === undefined ? {} : value.headers; + if (!headers || typeof headers !== "object" || Array.isArray(headers)) return { ok: false, error: "Expected headers to be an object." }; + const keys = Object.keys(headers); + if (new Set(keys.map((key) => key.toLowerCase())).size !== keys.length) return { ok: false, error: "Header names must be unique regardless of letter case." }; + if (keys.length > 64) return { ok: false, error: "A server holds at most 64 header entries." }; + for (const key of keys) { + if (!/^[A-Za-z0-9-]+$/.test(key) || key.length > 128) return { ok: false, error: `Invalid header name: ${key}` }; + if (typeof headers[key] !== "string" || headers[key].length > 8192 || /[\r\n\0]/.test(headers[key])) return { ok: false, error: `Invalid header value for ${key}.` }; + } + out.headers = Object.fromEntries(keys.sort().map((key) => [key, String(headers[key])])); + } + return { ok: true, server: out }; +} + +const canonicalServer = (server) => JSON.stringify(server); +export const mcpRevision = (servers) => createHash("sha256").update(JSON.stringify([...servers].sort((a, b) => a.name.localeCompare(b.name)))).digest("hex"); + +async function readStoreFile(fs, file) { + let stat; + try { stat = await fs.lstat(file); } + catch (error) { if (error?.code === "ENOENT") return { present: false }; throw error; } + if (!stat.isFile()) throw problem("invalid", "The shared MCP store must be a regular file."); + if (stat.size > MAX_STORE_BYTES) throw problem("too_large", "The shared MCP store exceeds the 256 KiB limit. Reduce it directly on disk."); + const handle = await fs.open(file, "r"); + let raw; + try { + const buf = Buffer.alloc(MAX_STORE_BYTES + 1); let bytes = 0; + while (bytes < buf.length) { const r = await handle.read(buf, bytes, buf.length - bytes, bytes); if (!r.bytesRead) break; bytes += r.bytesRead; } + if (bytes > MAX_STORE_BYTES) throw problem("too_large", "The shared MCP store exceeds the 256 KiB limit."); + raw = buf.toString("utf8", 0, bytes); + } finally { await handle.close(); } + let parsed; + try { parsed = JSON.parse(raw); } + catch { throw problem("invalid", "The shared MCP store is not valid JSON. Repair it directly on disk."); } + const list = Array.isArray(parsed) ? parsed : parsed?.servers; + if (!Array.isArray(list)) throw problem("invalid", "The shared MCP store must hold a server list."); + if (list.length > MAX_SERVERS) throw problem("too_large", "The shared MCP store holds too many servers."); + const servers = []; + for (const entry of list) { + const checked = validateMcpServer(entry); + if (!checked.ok) throw problem("invalid", "The shared MCP store holds an invalid server: " + checked.error); + servers.push(checked.server); + } + const names = servers.map((server) => server.name); + if (new Set(names).size !== names.length) throw problem("invalid", "The shared MCP store holds duplicate server names."); + return { present: true, servers, bytes: Buffer.byteLength(raw, "utf8") }; +} + +async function writeFileAtomic(fs, file, text) { + if (Buffer.byteLength(text, "utf8") > MAX_STORE_BYTES) throw problem("too_large", "The shared MCP store would exceed the 256 KiB limit. Reduce the server definition."); + await fs.mkdir(path.dirname(file), { recursive: true }); + const tmp = `${file}.tmp.${process.pid}.${randomBytes(6).toString("hex")}`; + try { + await fs.writeFile(tmp, text, { encoding: "utf8", mode: 0o600 }); + await fs.chmod(tmp, 0o600); + await fs.rename(tmp, file); + } finally { + await fs.rm(tmp, { force: true }); + } +} + +const storeQueues = new Map(); +export async function withStore(file, work) { + const key = path.resolve(file); + const previous = storeQueues.get(key) ?? Promise.resolve(); + const current = previous.catch(() => {}).then(work); + storeQueues.set(key, current); + try { return await current; } + finally { if (storeQueues.get(key) === current) storeQueues.delete(key); } +} + +export async function readMcpStore(fs, baseDir) { + const file = mcpStorePathFor(baseDir); + try { + const read = await readStoreFile(fs, file); + if (!read.present) return { present: false, path: file, servers: [], revision: mcpRevision([]), bytes: 0, error: null }; + return { present: read.servers.length > 0, path: file, servers: read.servers, revision: mcpRevision(read.servers), bytes: read.bytes, error: null }; + } catch (error) { + return { present: error?.code === "too_large", path: file, servers: [], revision: null, bytes: 0, + code: error?.code, error: String(error?.message ?? error) }; + } +} + +const changedStore = () => ({ ok: false, code: "conflict", error: "Shared MCP servers changed since you opened them. Reopen the editor to load the current list." }); + +export async function createMcpServer(fs, baseDir, input) { + const checked = validateMcpServer(input); + if (!checked.ok) return checked; + const file = mcpStorePathFor(baseDir); + return withStore(file, async () => { + const read = await readMcpStore(fs, baseDir); + if (read.error) return { ok: false, error: read.error }; + if (read.servers.some((server) => server.name === checked.server.name)) { + return { ok: false, code: "conflict", error: "A shared MCP server with that name already exists." }; + } + const servers = [...read.servers, checked.server]; + if (servers.length > MAX_SERVERS) throw problem("too_large", "The shared MCP store holds at most 128 servers."); + await writeFileAtomic(fs, file, JSON.stringify({ version: 1, servers }, null, 2) + "\n"); + return { ok: true, path: file, created: !read.present && read.servers.length === 0, revision: mcpRevision(servers), server: checked.server }; + }); +} + +export async function saveMcpServer(fs, baseDir, name, revision, input) { + const checked = validateMcpServer({ ...(input ?? {}), name }); + if (!checked.ok) return checked; + if (checked.server.name !== String(name ?? "").trim()) return { ok: false, error: "The server name cannot be changed. Remove it and add a new one." }; + const file = mcpStorePathFor(baseDir); + return withStore(file, async () => { + const read = await readMcpStore(fs, baseDir); + if (read.error) return { ok: false, error: read.error }; + if (read.revision !== revision) return changedStore(); + const at = read.servers.findIndex((server) => server.name === checked.server.name); + if (at < 0) return { ok: false, code: "missing", error: "This shared MCP server no longer exists. Refresh the list." }; + const servers = read.servers.map((server, index) => index === at ? checked.server : server); + await writeFileAtomic(fs, file, JSON.stringify({ version: 1, servers }, null, 2) + "\n"); + return { ok: true, path: file, revision: mcpRevision(servers), server: checked.server }; + }); +} + +export async function removeMcpServer(fs, baseDir, name, revision, beforeRemove = async () => {}) { + const file = mcpStorePathFor(baseDir); + return withStore(file, async () => { + const read = await readMcpStore(fs, baseDir); + if (read.error) return { ok: false, error: read.error }; + if (read.revision !== revision) return changedStore(); + if (!read.servers.some((server) => server.name === name)) return { ok: true, path: file, deleted: false, revision: read.revision }; + await beforeRemove(); + const servers = read.servers.filter((server) => server.name !== name); + if (servers.length === 0) await fs.rm(file, { force: true }); + else await writeFileAtomic(fs, file, JSON.stringify({ version: 1, servers }, null, 2) + "\n"); + return { ok: true, path: file, deleted: true, removed: name, revision: mcpRevision(servers) }; + }); +} + +export function redactMcpServer(server) { + const { env, headers, ...rest } = server; + // Paths and query strings may contain signed credentials. Only the origin + // belongs in polling/list summaries; the full URL loads with the editor. + if (rest.url) { try { rest.url = new URL(rest.url).origin; } catch { rest.url = "(invalid URL)"; } } + return { ...rest, + envKeys: Object.keys(env ?? {}), + envCount: Object.keys(env ?? {}).length, + headerKeys: Object.keys(headers ?? {}), + headerCount: Object.keys(headers ?? {}).length, + secretCount: Object.keys(env ?? {}).length + Object.keys(headers ?? {}).length, + }; +} + +// Fingerprint of the translated provider entry, so link state is derived by +// comparison instead of a new manifest format. +export const mcpFingerprint = (value) => createHash("sha256").update(canonicalServer(value)).digest("hex"); diff --git a/docker/setup/mcp/toml.mjs b/docker/setup/mcp/toml.mjs new file mode 100644 index 0000000..510dc23 --- /dev/null +++ b/docker/setup/mcp/toml.mjs @@ -0,0 +1,77 @@ +// Parse and patch the complete TOML syntax tree. The vendor keeps untouched +// comments/formatting; never scan table boundaries with regular expressions. +import { parse, patch, stringify, TomlDocument } from "../vendor/toml-patch.mjs"; +import { isDeepStrictEqual } from "node:util"; + +const problem = () => Object.assign(new Error("Codex config.toml cannot be safely edited. Check its TOML syntax before changing MCP servers."), { code: "unsupported" }); +const object = (v) => v && typeof v === "object" && !Array.isArray(v); +const plain = (v) => Array.isArray(v) ? v.map(plain) + : v && Object.getPrototypeOf(v) === null ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, plain(x)])) : v; +function document(text) { + try { + const value = parse(String(text ?? "")); + if (value.mcp_servers !== undefined && !object(value.mcp_servers)) throw problem(); + return plain(value); + } catch { throw problem(); } // Parser errors can contain credential values. +} + +export function parseMcpToml(text) { + return new Map(Object.entries(document(text).mcp_servers ?? {})); +} + +export function applyMcpToml(text, name, entry) { + const value = document(text); + if (entry === null && !Object.hasOwn(value.mcp_servers ?? {}, name)) return text; + const servers = { ...(value.mcp_servers ?? {}) }; + if (entry === null) delete servers[name]; + else servers[name] = entry; + const desired = { ...value, mcp_servers: servers }; + if (!Object.keys(servers).length) delete desired.mcp_servers; + try { + const tree = new TomlDocument(text).cst; + const starts = [0]; + for (let at = text.indexOf("\n"); at !== -1; at = text.indexOf("\n", at + 1)) starts.push(at + 1); + const lineSpan = (node) => { + const start = starts[node.loc.start.line - 1]; + const end = starts[node.loc.end.line] ?? text.length; + return [start, end]; + }; + const owns = (keys) => keys[0] === "mcp_servers" && keys[1] === name; + const edits = []; + let inlineRoot = false; + for (const node of tree) { + if (node.type === "KeyValue") { + if (owns(node.key.value)) edits.push([...lineSpan(node), ""]); + else if (node.key.value.length === 1 && node.key.value[0] === "mcp_servers") { + // Patch an inline root locally, never the surrounding document. + const [start, end] = lineSpan(node); + const updated = desired.mcp_servers ? { mcp_servers: desired.mcp_servers } : {}; + edits.push([start, end, patch(text.slice(start, end), updated)]); + inlineRoot = true; + } + } else if (node.type === "Table" || node.type === "TableArray") { + const keys = node.key.item.value; + if (owns(keys)) { + // The parser attaches comments before the next table to this one. + // End at its last value, preserving those unrelated trailing notes. + const rows = node.items.filter((item) => item.type === "KeyValue"); + edits.push([lineSpan(node.key)[0], lineSpan(rows.at(-1) ?? node.key)[1], ""]); + } else if (keys.length === 1 && keys[0] === "mcp_servers") { + for (const row of node.items) if (row.type === "KeyValue" && row.key.value[0] === name) edits.push([...lineSpan(row), ""]); + } + } + } + let next = text; + for (const [start, end, replacement] of edits.sort((a, b) => b[0] - a[0])) next = next.slice(0, start) + replacement + next.slice(end); + if (entry !== null && !inlineRoot) { + const eol = text.includes("\r\n") ? "\r\n" : "\n"; + // Remove only the known, generated parent marker, not user source. + const generated = stringify({ mcp_servers: { [name]: entry } }, { inlineTableStart: 3, newLine: eol }).replace(/^\[mcp_servers\]\r?\n(?:\r?\n)?/, ""); + next += (next.endsWith("\n") || !next ? "" : eol) + eol + generated; + } + const actual = document(next); + if (actual.mcp_servers && !Object.keys(actual.mcp_servers).length) delete actual.mcp_servers; + if (!isDeepStrictEqual(actual, desired)) throw problem(); + return next; + } catch { throw problem(); } +} diff --git a/docker/setup/page.mjs b/docker/setup/page.mjs index 3ed9ce0..96b2c4e 100644 --- a/docker/setup/page.mjs +++ b/docker/setup/page.mjs @@ -33,6 +33,7 @@ export const CONSOLE_SCRIPTS = [ "client/skill-inspector.js", "client/skill-import.js", "client/skills.js", + "client/mcp.js", "app.js", ]; export const UNLOCK_SCRIPTS = ["client/base.js", "design/ui.js", "client/unlock.js"]; diff --git a/docker/setup/server.mjs b/docker/setup/server.mjs index 724b4b8..5bfcc38 100644 --- a/docker/setup/server.mjs +++ b/docker/setup/server.mjs @@ -33,6 +33,7 @@ import { createDeviceLabels } from "./device-labels.mjs"; import * as Context from "./context.mjs"; import { createOpenCodeAuth } from "./opencode-auth.mjs"; import { createSkillsService } from "./skills.mjs"; +import { createMcpService } from "./mcp.mjs"; import { clientAddress } from "./client-address.mjs"; import { newKey, readKeyFile, writeKeyFile } from "./setup-key.mjs"; import { connectState, jsonFrom, parseLinkOutput } from "./connect.mjs"; @@ -519,6 +520,9 @@ const contextFs = async () => (await import("node:fs/promises")); const skillsService = createSkillsService({ home: CONTEXT_HOME, env: process.env, getHarness: async (provider) => (await harnessCache.snapshotCheap()).harnesses?.find((row) => row.id === provider), }); +const mcpService = createMcpService({ home: CONTEXT_HOME, env: process.env, baseDir: STATE_DIR, + io: await contextFs(), +}); // The browser image's agent browser, as built: the versions are build args the // Dockerfile bakes into the environment. Absent on the core image. @@ -912,7 +916,14 @@ const status = async () => { // Every other tool in the global mise config, added here or with `mise use -g`. packages: withLatest("package", packageRows), // Global instruction metadata; the editor fetches contents on demand. - context: await attempt("context", async () => Context.contextStatus(await contextFs(), { baseDir: STATE_DIR, home: CONTEXT_HOME, env: process.env }), { + // Shared MCP metadata rides alongside; server entries stay server-side. + context: await attempt("context", async () => { + const [instructions, mcp] = await Promise.all([ + Context.contextStatus(await contextFs(), { baseDir: STATE_DIR, home: CONTEXT_HOME, env: process.env }), + mcpService.status().catch((error) => ({ ok: false, error: String(error?.message ?? error) })), + ]); + return { ...instructions, mcp }; + }, { agentsMd: { present: false, bytes: 0, error: "unavailable" }, }), // The background install of everything T3_PREINSTALL names, on a first @@ -2153,7 +2164,8 @@ const ROUTES = ["/login", "/logout", "/hello", "/status", "/pair", "/revoke", "/ "/providers", "/updates/check", "/packages/install", "/packages/update", "/packages/uninstall", "/packages/cancel", "/packages/registry", "/packages/versions", "/packages/info", - "/context/agents-md", "/context/skills/reach", "/context/skills/read", "/context/skills/create", "/context/skills/save", "/context/skills/remove", "/context/skills/state", "/context/skills/source", "/context/skills/plan", "/context/skills/install", "/context/skills/discard", "/context/skills"]; + "/context/agents-md", "/context/skills/reach", "/context/skills/read", "/context/skills/create", "/context/skills/save", "/context/skills/remove", "/context/skills/state", "/context/skills/source", "/context/skills/plan", "/context/skills/install", "/context/skills/discard", "/context/skills", + "/context/mcp", "/context/mcp/read", "/context/mcp/create", "/context/mcp/save", "/context/mcp/remove", "/context/mcp/link"]; // One background pass for the opt-in auto-updater. Checks are cheap and // always safe; installs run only when enabled, an update is known, and @@ -2680,6 +2692,43 @@ const server = createServer(async (req, res) => { } } + // --- shared MCP servers --------------------------------------------- + // Inventory per provider is on demand, never part of /status polling; + // /status carries only redacted shared metadata and link states. + if (req.method === "GET" && (route === "/context/mcp" || route === "/context/mcp/read")) { + try { + const params = raw.searchParams; + if (route === "/context/mcp/read") { + if (!params.get("name")) return sendJson(res, 400, { ok: false, code: "invalid", error: "Expected an MCP server name." }); + return sendJson(res, 200, await mcpService.read(params.get("name"))); + } + if (params.get("provider")) return sendJson(res, 200, await mcpService.list(params.get("provider"))); + return sendJson(res, 200, await mcpService.status({ refresh: params.get("refresh") === "1" })); + } catch (error) { + const status = { invalid: 400, missing: 404, ENOENT: 404, conflict: 409, too_large: 413, unsupported: 403, EACCES: 403, EPERM: 403 }[error?.code] || 500; + return sendJson(res, status, { ok: false, code: error?.code || "unavailable", error: String(error?.message || "MCP discovery is unavailable.").slice(0, 300) }); + } + } + + if (req.method === "POST" && ["/context/mcp/create", "/context/mcp/save", "/context/mcp/remove", "/context/mcp/link"].includes(route)) { + try { + const input = JSON.parse((await readBody(req, 6 * 64 * 1024 + 4096)) || "{}"); + if (!input || typeof input !== "object" || Array.isArray(input)) return sendJson(res, 400, { ok: false, error: "Expected an MCP operation object." }); + if (route === "/context/mcp/link" && typeof input.providerRevision !== "string") return sendJson(res, 400, { ok: false, code: "invalid", error: "Refresh MCP servers before changing provider settings." }); + const result = route === "/context/mcp/link" ? await mcpService.setLink(input.provider, input.name, input.linked, input.sharedRevision, input.providerRevision) + : route === "/context/mcp/create" ? await mcpService.create(input.server) + : route === "/context/mcp/save" ? await mcpService.save(input.name, input.revision, input.server) + : await mcpService.remove(input.name, input.revision); + recordEvent("context.saved", route === "/context/mcp/link" + ? `${input.provider} ${input.linked ? "uses" : "stopped using"} shared MCP server ${input.name}` + : route === "/context/mcp/remove" ? "Removed a shared MCP server" : "Saved a shared MCP server"); + return sendJson(res, 200, result); + } catch (error) { + const status = { invalid: 400, missing: 404, ENOENT: 404, conflict: 409, EEXIST: 409, too_large: 413, unsupported: 403, EACCES: 403, EPERM: 403 }[error?.code] || (error instanceof SyntaxError ? 400 : 500); + return sendJson(res, status, { ok: false, code: error?.code || "unavailable", error: String(error?.message || "The MCP operation failed.").slice(0, 500) }); + } + } + // --- global instructions ---------------------------------------------- if (req.method === "GET" && route === "/context/agents-md") { try { diff --git a/docker/setup/vendor/README.md b/docker/setup/vendor/README.md index 966b78e..8fb394e 100644 --- a/docker/setup/vendor/README.md +++ b/docker/setup/vendor/README.md @@ -41,3 +41,28 @@ npm install --prefix /tmp/jsonc-build --no-audit --no-fund jsonc-parser@3.3.1 es Bundle SHA-256: `bd0f3ce191c835fbd222afb0e0e416852d0ea1c917406bdd0a609a0b8655f5ef`. Inputs are bounded to 1 MiB; invalid syntax, duplicate keys and unsupported policy shapes disable controls rather than normalizing away existing settings. + +## TOML syntax tree + +`toml-patch.mjs` is the unmodified ESM distribution of +`@decimalturn/toml-patch@3.1.2` (MIT; see `toml-patch.LICENSE`). +Upstream: https://github.com/DecimalTurn/toml-patch + +```sh +npm pack @decimalturn/toml-patch@3.1.2 +tar -xzf decimalturn-toml-patch-3.1.2.tgz +cp package/dist/toml-patch.js toml-patch.mjs +cp package/LICENSE toml-patch.LICENSE +``` + +Archive SHA-256: `aed417934ced4cd4d19da54eb286c16750ba9058ca2e47be4162e3c0e5449514`. +Distribution SHA-256: `8872b6d74fd090a2e358b727ce68d055af802c9efa86d05a8a1d05cdba6ce869`. + +MCP editing validates the complete document and uses this pinned parser's CST +locations to change only the selected server. Unrelated source, including +comments attached to the end of a preceding table, stays in place. Inline-root +edits are patched only inside that root assignment. The result is parsed again +and checked against the intended data before publication. Nested tables, +arrays of tables and multiline strings are covered by regression tests. Update +the distribution, license, checksums and CST compatibility checks together. +There is no runtime package installation and the bundle never reaches browsers. diff --git a/docker/setup/vendor/toml-patch.LICENSE b/docker/setup/vendor/toml-patch.LICENSE new file mode 100644 index 0000000..a8c55ea --- /dev/null +++ b/docker/setup/vendor/toml-patch.LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2019–2025 Tim Hall +Copyright (c) 2025 Martin Leduc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docker/setup/vendor/toml-patch.mjs b/docker/setup/vendor/toml-patch.mjs new file mode 100644 index 0000000..4cbaab6 --- /dev/null +++ b/docker/setup/vendor/toml-patch.mjs @@ -0,0 +1,56 @@ +//! @decimalturn/toml-patch v3.1.2 - https://github.com/DecimalTurn/toml-patch - @license: MIT +function e(e){return e.type===`Document`}function t(e){return e.type===`Table`}function n(e){return e.type===`TableKey`}function r(e){return e.type===`TableArray`}function i(e){return e.type===`TableArrayKey`}function a(e){return e.type===`KeyValue`}function o(e){return e.type===`String`}function s(e){return e.type===`Float`}function c(e){return e.type===`DateTime`}function l(e){return e.type===`InlineArray`}function u(e){return e.type===`InlineItem`}function d(e){return e.type===`InlineTable`}function f(e){return e.type===`Comment`}function p(n){return e(n)||t(n)||r(n)||d(n)||l(n)}function m(e){return n(e)||i(e)||u(e)}function h(e){return a(e)||t(e)||r(e)||f(e)}let g,_;function v(e){if(g===e&&_!==void 0)return _;let t=C(e);return g=e,_=t,t}function y(){g=void 0,_=void 0}function b(e){return{lines:e.end.line-e.start.line+1,columns:e.end.column-e.start.column}}function x(e,t){let n=Array.isArray(e)?e:C(e);if(n.length===0)return{line:1,column:t};let r=0,i=n.length-1;for(;r>>1;n[e]({start:x(a,e),end:x(a,t)});for(;r=n||e.charCodeAt(r+1)!==10))throw new D(e,x(a,r),`Standalone CR; must be CRLF or LF`);if(t!==32&&t!==9&&t!==13){if(t===10)s(r);else if(t===91||t===93)yield{type:`Bracket`,raw:e[r],loc:o(r,r+1)};else if(t===123||t===125)yield{type:`Curly`,raw:e[r],loc:o(r,r+1)};else if(t===61)yield{type:`Equal`,raw:`=`,loc:o(r,r+1)};else if(t===44)yield{type:`Comma`,raw:`,`,loc:o(r,r+1)};else if(t===46)yield{type:`Dot`,raw:`.`,loc:o(r,r+1)};else if(t===35)yield c();else{let t=ne(e,r,`'`)||ne(e,r,`"`);t?yield l(t):yield u()}}r++}function s(t){a.push(t);let n=t>0&&e.charCodeAt(t-1)===13?`\r +`:` +`;i.reference===null?i.reference=n:i.reference!==n&&(i.mixed=!0)}function c(){let t=r;for(;r+1=6)throw new D(e,x(a,r),`Invalid multiline string: ${n} consecutive ${t} characters`);if(n===3){r+=2;break}r+=n-3,r+=2;break}let i=e.charCodeAt(r);if(i===13&&(r+1>=n||e.charCodeAt(r+1)!==10))throw new D(e,x(a,r),`Standalone CR in multiline string; must be CRLF or LF`);if((i<=31||i===127)&&i!==9&&i!==10&&i!==13){let n=t===`"`?`multiline basic strings`:`multiline literal strings`,o=`0x${i.toString(16).toUpperCase().padStart(2,`0`)}`,s=``;i===0?s=`Null`:i===127&&(s=`DEL`);let c=s?`${s} (${o}) not allowed in ${n}`:`Control char ${o} not allowed in ${n}`;throw new D(e,x(a,r),c)}i===10&&s(r),r++}if(r>=n){if(t===`"`){let t=0,i=n-1;for(;i>=0&&e[i]===`\\`;)t++,i--;if(t>0&&t%2!=0)throw new D(e,x(a,r),`Unterminated multiline ${c} (possible escape issue)`)}throw new D(e,x(a,r),`Unterminated multiline ${c}`)}return{type:`Literal`,raw:e.slice(i,r+1),loc:o(i,r+1)}}function u(){let t=e[r];if(!ee.test(t))throw new D(e,x(a,r),`Unexpected char "${t}"`);let i=r,s=t===`"`,c=t===`'`;for(;r=n);){let t=e.charCodeAt(r+1);if(!(s||c)&&(t===32||t===9||t===10||t===13||t===44||t===46||t===93||t===125||t===61||t===35))break;if(r++,!(s||c)){let t=e.charCodeAt(r);if(t<=31||t===127)throw new D(e,x(a,r),`Control char 0x${t.toString(16).toUpperCase().padStart(2,`0`)} not allowed in unquoted value`)}if(s||c){let t=e.charCodeAt(r);if((t<=31||t===127)&&t!==9){let n=s?`basic strings`:`literal strings`,i=`0x${t.toString(16).toUpperCase().padStart(2,`0`)}`,o=``;t===10?o=`Newline`:t===13?o=`Carriage return`:t===0?o=`Null`:t===127&&(o=`DEL`);let c=o?`${o} (${i}) not allowed in ${n}`:`Control char ${i} not allowed in ${n}`;throw new D(e,x(a,r),c)}}let i=e[r];if(i===`"`&&!c&&(s=!s),i===`'`&&!s&&(c=!c),r+1>=n)break;if(s&&i===`\\`){let t=e[r+1];(t===`"`||t===`\\`)&&r++}}if(s||c)throw new D(e,x(a,i),`Expected close of string with ${s?`"`:`'`}`);return{type:`Literal`,raw:e.slice(i,r+1),loc:o(i,r+1)}}}function ne(e,t,n){if(!n||e[t]!==n||e[t+1]!==n||e[t+2]!==n)return!1;if(n===`'`)return n;let r=0,i=t-1;for(;i>=0&&e[i]===`\\`;)r++,i--;return(r===0||r%2==0)&&n}const j=new Int8Array(128);j.fill(-1);for(let e=0;e<10;e++)j[48+e]=e;for(let e=0;e<6;e++)j[65+e]=10+e,j[97+e]=10+e;function M(e){let t=e.charCodeAt(0);return t<128&&j[t]!==-1}function re(e,t){return j[e.charCodeAt(t)]<<12|j[e.charCodeAt(t+1)]<<8|j[e.charCodeAt(t+2)]<<4|j[e.charCodeAt(t+3)]}function ie(e){return e.startsWith(`'''`)?oe(e.slice(3,e.length-3)):e.startsWith(`'`)?e.slice(1,e.length-1):e.startsWith(`"""`)?ae(oe(e.slice(3,e.length-3)),!0):e.startsWith(`"`)?ae(e.slice(1,e.length-1),!1):e}function ae(e,t){let n=e.length;if(!t&&e.indexOf(`\\`)===-1)return e;let r=[],i=0;for(let a=0;ai&&r.push(e.slice(i,a)),a++,a>=n)throw Error(`Trailing backslash`);let o=e[a];switch(o){case`b`:r.push(`\b`);break;case`t`:r.push(` `);break;case`n`:r.push(` +`);break;case`f`:r.push(`\f`);break;case`r`:r.push(`\r`);break;case`"`:r.push(`"`);break;case`\\`:r.push(`\\`);break;case`e`:r.push(`\x1B`);break;case`u`:{if(a+4>n||!M(e[a+1])||!M(e[a+2])||!M(e[a+3])||!M(e[a+4]))throw Error(`Invalid Unicode escape: \\u${e.slice(a+1,a+5)}`);let t=re(e,a+1);if(t>=55296&&t<=57343)throw Error(`Invalid \\u${e.slice(a+1,a+5)}: surrogates not allowed`);r.push(String.fromCharCode(t)),a+=4;break}case`U`:{if(a+8>n)throw Error(`Invalid Unicode escape: \\U${e.slice(a+1,a+9)}`);for(let t=1;t<=8;t++)if(!M(e[a+t]))throw Error(`Invalid Unicode escape: \\U${e.slice(a+1,a+9)}`);let t=parseInt(e.slice(a+1,a+9),16);if(t>=55296&&t<=57343)throw Error(`Invalid \\U${e.slice(a+1,a+9)}: surrogates not allowed`);r.push(String.fromCodePoint(t)),a+=8;break}case`x`:{if(a+2>n||!M(e[a+1])||!M(e[a+2]))throw Error(`Invalid hex escape: \\x${e.slice(a+1,a+3)}`);let t=j[e.charCodeAt(a+1)]<<4|j[e.charCodeAt(a+2)];r.push(String.fromCharCode(t)),a+=2;break}default:if(t&&(o===` +`||o===`\r`||o===` `||o===` `)){let t=o===` +`||o===`\r`,r=a;for(;r0?`.`+String(e).padStart(3,`0`).replace(/0+$/,``):``}function me(e){if(e===`Z`)return 0;let t=e[0]===`+`?1:-1,[n,r]=e.slice(1).split(`:`);return t*(parseInt(n)*60+parseInt(r))}var he=class e{static IS_DATE_ONLY=/^\d{4}-\d{2}-\d{2}$/;static IS_TIME_ONLY=/^\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;static IS_LOCAL_DATETIME_T=/^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;static IS_LOCAL_DATETIME_SPACE=/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;static IS_OFFSET_DATETIME_T=/^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;static IS_OFFSET_DATETIME_SPACE=/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;static IS_FULL_DATE=/(\d{4})-(\d+)-(\d+)/;static IS_FULL_TIME=/(\d+):(\d+)(?::(\d+))?/;static createDateWithOriginalFormat(t,n){if(e.IS_DATE_ONLY.test(n)){if(t.getUTCHours()!==0||t.getUTCMinutes()!==0||t.getUTCSeconds()!==0||t.getUTCMilliseconds()!==0){if(t instanceof ye)return t;let e=t.toISOString().replace(`Z`,``);return e=e.replace(/\.000$/,``),new ve(e,!1)}let e=t.toISOString().split(`T`)[0];return new ge(e)}if(e.IS_TIME_ONLY.test(n)){if(t instanceof _e)return t;{let e=N(t),r=pe(e.ms,n);return new _e(`${e.hours}:${e.minutes}:${e.seconds}${r}`,n)}}if(e.IS_LOCAL_DATETIME_T.test(n)){let e=N(t),r=pe(e.ms,n);return new ve(`${e.year}-${e.month}-${e.day}T${e.hours}:${e.minutes}:${e.seconds}${r}`,!1,n)}if(e.IS_LOCAL_DATETIME_SPACE.test(n)){let e=N(t),r=pe(e.ms,n);return new ve(`${e.year}-${e.month}-${e.day} ${e.hours}:${e.minutes}:${e.seconds}${r}`,!0,n)}if(e.IS_OFFSET_DATETIME_T.test(n)||e.IS_OFFSET_DATETIME_SPACE.test(n)){let r=n.match(/([+-]\d{2}:\d{2}|[Zz])$/),i=r?r[1]===`z`?`Z`:r[1]:`Z`,a=e.IS_OFFSET_DATETIME_SPACE.test(n),o=N(new Date(t.getTime()+me(i)*6e4)),s=a?` `:`T`,c=pe(o.ms,n,/\.(\d+)(?:[Zz]|[+-]\d{2}:\d{2})\s*$/);return new ye(`${o.year}-${o.month}-${o.day}${s}${o.hours}:${o.minutes}:${o.seconds}${c}${i}`,a)}return t}},ge=class extends Date{constructor(e){super(e)}toISOString(){let e=N(this);return`${e.year}-${e.month}-${e.day}`}},_e=class extends Date{originalFormat;constructor(e,t){let n=e;/:\d{2}:\d{2}/.test(e)||(n=e+`:00`),super(`0000-01-01T${n}Z`),this.originalFormat=t}toISOString(){let e=N(this);return`${e.hours}:${e.minutes}:${e.seconds}${pe(e.ms,this.originalFormat)}`}},ve=class extends Date{useSpaceSeparator=!1;originalFormat;constructor(e,t=!1,n){let r=e;/\d{2}:\d{2}:\d{2}/.test(e)||(r=e.replace(/(\d{2}:\d{2})([\s\-+TZ]|$)/,`$1:00$2`)),super(r.replace(` `,`T`)+`Z`),this.useSpaceSeparator=t,this.originalFormat=n||e}toISOString(){let e=N(this),t=this.useSpaceSeparator?` `:`T`;return`${e.year}-${e.month}-${e.day}${t}${e.hours}:${e.minutes}:${e.seconds}${pe(e.ms,this.originalFormat)}`}},ye=class extends Date{useSpaceSeparator=!1;originalOffset;originalFormat;constructor(e,t=!1){let n=e;/\d{2}:\d{2}:\d{2}/.test(e)||(n=e.replace(/(\d{2}:\d{2})([\s\-+TZ]|$)/,`$1:00$2`)),super(n.replace(` `,`T`)),this.useSpaceSeparator=t,this.originalFormat=e;let r=e.match(/([+-]\d{2}:\d{2}|[Zz])$/);r&&(this.originalOffset=r[1]===`z`?`Z`:r[1])}toISOString(){if(this.originalOffset){let e=N(new Date(this.getTime()+me(this.originalOffset)*6e4)),t=this.useSpaceSeparator?` `:`T`,n=pe(e.ms,this.originalFormat,/\.(\d+)(?:[Zz]|[+-]\d{2}:\d{2})\s*$/);return`${e.year}-${e.month}-${e.day}${t}${e.hours}:${e.minutes}:${e.seconds}${n}${this.originalOffset}`}let e=super.toISOString();return this.useSpaceSeparator?e.replace(`T`,` `):e}};const be=he,xe=`true`,Se=/e/i,Ce=/_/g,we=/^[+-]?inf$/,Te=/^[+-]?nan$/,Ee=/^[+-]?0x/i;function De(e){return e>=65&&e<=90||e>=97&&e<=122||e>=48&&e<=57||e===95||e===45}const Oe=/^[+-]?0o/i,ke=/^[+-]?0b/i;function Ae(e,t,n){for(let r=0;r23)throw new D(i,a,`"${r}": hour must be 00-23`)}if(t!==void 0){let e=parseInt(t,10);if(e<0||e>59)throw new D(i,a,`"${r}": minute must be 00-59`)}if(n!==void 0){let e=parseInt(n,10);if(e<0||e>60)throw new D(i,a,`"${r}": second must be 00-60`)}}function Re(e,t,n){if(Ee.test(e)||Oe.test(e)||ke.test(e))return;let r=e.replace(/^[+-]/,``).replace(/_/g,``);if(!/^[0-9]+$/.test(r))throw new D(t,n,`Invalid integer "${e}"`)}function ze(e,t,n,r,i,a,o){let s=r[0]+r[1].toUpperCase();if(RegExp(`^[+\\-]?`+s.replace(/([[\]])/g,`\\$1`)).test(e))throw new D(t,n,`${a} prefix must be lowercase "${r}"`);if(RegExp(`^[+\\-]?`+r+`_`,`i`).test(e))throw new D(t,n,`Underscore must be between digits`);let c=RegExp(`^[+\\-]?`+r,`i`),l=e.replace(c,``);if(!l||l===`_`||l.startsWith(`_`))throw new D(t,n,`Incomplete ${a.toLowerCase()} number`);let u=l.replace(/_/g,``);if(!i.test(u))throw new D(t,n,o??`Invalid ${a.toLowerCase()} digits`);if(/^[+-]/.test(e))throw new D(t,n,`${a} numbers cannot have a sign prefix`)}function*Be(e,t){let n=new se(te(e,t));try{for(;!n.next().done;){let t=$e(n,e);for(let e of t)yield e}}finally{y()}}function*Ve(e,t,n){for(let t of e)yield t;let r=new se(te(t));try{for(;!r.next().done;){let e=$e(r,t);for(let t of e)n&&Ue(t,n),yield t}}finally{y()}}function He(e,t){return{line:e.line+t.line-1,column:e.line===1?e.column+t.column:e.column}}function Ue(e,t){let n=[e];for(;n.length>0;){let e=n.pop();if(e.loc.start=He(e.loc.start,t),e.loc.end=He(e.loc.end,t),e.type===`KeyValue`){let t=e;n.push(t.value,t.key)}else if(e.type===`TableKey`||e.type===`TableArrayKey`||e.type===`InlineItem`)n.push(e.item);else if(e.type===`Table`||e.type===`TableArray`){let t=e;n.push(t.key);for(let e=t.items.length-1;e>=0;e--)n.push(t.items[e])}else if(e.type===`InlineTable`||e.type===`InlineArray`){let t=e.items;for(let e=t.length-1;e>=0;e--)n.push(t[e])}}}function We(e){return{type:`Comment`,loc:e.value.loc,raw:e.value.raw}}function Ge(e,t){let n=!e.peek().done&&e.peek().value.type===`Bracket`?`TableArray`:`Table`,r=n===`Table`;if(r&&e.value.raw!==`[`)throw new D(t,e.value.loc.start,`Expected table opening "[", found ${e.value.raw}`);if(!r){let n=e.peek();if(n.done)throw new D(t,e.value.loc.start,`Expected "[[" for Array of Tables, found end of input`);if(e.value.raw!==`[`||n.value.raw!==`[`)throw new D(t,e.value.loc.start,`Expected "[[", found ${e.value.raw+n.value.raw}`);let r=e.value,i=n.value;if(r.loc.end.line!==i.loc.start.line||r.loc.end.column!==i.loc.start.column)throw new D(t,r.loc.start,`"[[" brackets must be adjacent (no whitespace)`)}let i=r?{type:`TableKey`,loc:e.value.loc}:{type:`TableArrayKey`,loc:e.value.loc};if(e.next(),n===`TableArray`&&e.next(),e.done)throw new D(t,i.loc.start,`Expected table key, reached end of file`);if(e.value.type===`Bracket`&&e.value.raw===`]`)throw new D(t,e.value.loc.start,n===`TableArray`?`Array of Tables header [[]] requires a table name`:`Table header [] requires a table name`);let a=e.value.raw;je(a,t,e.value.loc.start),a.startsWith(`"`)||a.startsWith(`'`)||Ae(a,t,e.value.loc.start);let o;for(o=[Me(e.value.raw,t,e.value.loc.start)],i.item={type:`Key`,loc:e.value.loc,raw:e.value.raw,value:o};!e.peek().done&&e.peek().value.type===`Dot`;){e.next();let n=e.value;e.next();let r=e.value.raw;r.startsWith(`"`)||r.startsWith(`'`)||Ae(r,t,e.value.loc.start);let a=` `.repeat(n.loc.start.column-i.item.loc.end.column),o=` `.repeat(e.value.loc.start.column-n.loc.end.column);i.item.loc.end=e.value.loc.end,i.item.raw+=`${a}.${o}${e.value.raw}`;try{i.item.value.push(ie(e.value.raw))}catch(n){let r=n;throw new D(t,e.value.loc.start,r.message)}}if(e.next(),!e.done){let n=i.loc.start.line;if(e.value.loc.start.line!==n)throw new D(t,e.value.loc.start,r?`Table header must be single-line`:`Array of Tables header must be single-line`)}if(r&&(e.done||e.value.raw!==`]`))throw new D(t,e.done?i.item.loc.end:e.value.loc.start,`Expected table closing "]", found ${e.done?`end of file`:e.value.raw}`);if(!r&&(e.done||e.peek().done||e.value.raw!==`]`||e.peek().value.raw!==`]`))throw new D(t,e.done||e.peek().done?i.item.loc.end:e.value.loc.start,`Expected "]]" closing, found ${e.done||e.peek().done?`end of file`:e.value.raw+e.peek().value.raw}`);if(!r){let n=e.value,r=e.peek().value;if(n.loc.end.line!==r.loc.start.line||n.loc.end.column!==r.loc.start.column)throw new D(t,n.loc.start,`"]]" brackets must be adjacent (no whitespace)`)}if(r||e.next(),i.loc.end=e.value.loc.end,!e.peek().done){let n=e.peek().value;if(n.loc.start.line===i.loc.end.line&&n.type!==`Comment`)throw new D(t,n.loc.start,`Extra content after ${r?`table`:`Array of Tables`} header`)}let s=[];for(;!e.peek().done&&e.peek().value.type!==`Bracket`;){e.next();let n=$e(e,t);for(let e=0;e23)throw new D(t,n,`Offset "${e}": hour must be 00-23`);let c=parseInt(o,10);if(c<0||c>59)throw new D(t,n,`Offset "${e}": minute must be 00-59`)}if(!/^\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})?)?$/.test(e)&&!/^\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/.test(e))throw/^\d{4}-\d{2}-\d{2}T$/.test(e)?new D(t,n,`"${e}": 'T' requires time component`):/^\d{4}-\d{2}-\d{2}[a-su-zA-SU-Z]/.test(e)?new D(t,n,`Invalid date "${e}": unexpected character after date`):/^\d{4}-\d{2}-\d{2}\d{2}:\d{2}/.test(e)?new D(t,n,`"${e}": missing 'T' or space separator`):new D(t,n,`Invalid datetime "${e}"`);let i=e.match(/^(\d+)-/);if(i&&i[1].length!==4)throw new D(t,n,`"${e}": year must be 4 digits`);let a=e.match(/^(\d+)-(\d+)-(\d+)/);if(a){let[,,r,i]=a;if(r.length!==2)throw new D(t,n,`"${e}": month must be 2 digits`);if(i.length!==2)throw new D(t,n,`"${e}": day must be 2 digits`)}let o=e.match(/[T ](\d+):(\d+)(?::(\d+))?/);if(o){let[,r,i,a]=o;Ie(r,i,a,e,t,n)}let s=e.match(/^(\d+):(\d+)(?::(\d+))?/);if(s&&!a){let[,r,i,a]=s;Ie(r,i,a,e,t,n)}let c=e.match(/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?/),l=e.match(/^(\d{2}):(\d{2})(?::(\d{2}))?/);if(c){let[,r,i,a,o,s,l]=c,u=parseInt(i,10);if(u<1||u>12)throw new D(t,n,`"${e}": month must be 01-12`);let d=parseInt(a,10);if(d<1||d>31)throw new D(t,n,`"${e}": day must be 01-31`);if(d>Ye(parseInt(r,10),u))throw new D(t,n,`"${e}": day ${a} invalid for ${r}-${i}`);Le(o,s,l,e,t,n)}else if(l){let[,r,i,a]=l;Le(r,i,a,e,t,n)}}function Ze(e,t){let n=e.value.loc,r=e.value.raw,i;if(we.test(r))i=r.startsWith(`-`)?-1/0:1/0;else if(Te.test(r)){let e=new Float64Array(1),t=new DataView(e.buffer);r.startsWith(`-`)?(t.setUint32(0,0,!0),t.setUint32(4,4294443008,!0)):(t.setUint32(0,0,!0),t.setUint32(4,2146959360,!0)),i=e[0];let a=r.startsWith(`+`)?`+`:r.startsWith(`-`)?`-`:void 0;return{type:`Float`,loc:n,raw:r,value:i,nanSign:a}}else if(!e.peek().done&&e.peek().value.type===`Dot`){let a=n.start;{if(Se.test(r)&&!Ee.test(r))throw new D(t,n.start,`Float "${r}": dot after exponent`);let e=r;Pe(e,t,n.start,`the integer part of a float`);let i=e.replace(/^[+-]/,``);if(i===``||i===`_`)throw new D(t,n.start,`Float: digit required before dot`);Ne(e,t,n.start)}if(e.next(),e.peek().done||e.peek().value.type!==`Literal`)throw new D(t,e.value.loc.end,`Expected fraction value for Float`);e.next(),r+=`.${e.value.raw}`,n={start:a,end:e.value.loc.end};{let n=e.value.raw;if(!/^\d/.test(n))throw new D(t,e.value.loc.start,`Float: fraction must start with digit, found "${n}"`);Ne(n,t,e.value.loc.start,!1),Fe(n,r,t,e.value.loc.start)}i=Number(r.replace(Ce,``))}else{if(Ne(r,t,n.start),Fe(r,r,t,n.start),!e.peek().done&&e.peek().value.type===`Dot`)throw new D(t,e.peek().value.loc.start,`Float "${r}.": dot after exponent`);Pe(r,t,n.start,`the integer part of a float`),i=Number(r.replace(Ce,``))}if(Number.isNaN(i)&&!Te.test(r))throw new D(t,n.start,`Invalid float "${r}"`);return{type:`Float`,loc:n,raw:r,value:i}}function Qe(e,t){let n=e.value.raw,r=e.value.loc;if(/^\d{1,}-\d{1,}/.test(n)||/^\d{1,}:\d{1,}/.test(n)||/^\d{6}-\d{2}$/.test(n))throw new D(t,r.start,`Invalid integer "${n}"`);if(n===`-0`||n===`+0`)return{type:`Integer`,loc:r,raw:n,value:0};if(/^[+-]{2,}/.test(n))throw new D(t,r.start,`Double sign is not allowed in integers`);Pe(n,t,r.start,`decimal integers`),Ne(n,t,r.start),Re(n,t,r.start);let i=10;Ee.test(n)?(i=16,ze(n,t,r.start,`0x`,/^[0-9a-fA-F]+$/,`Hexadecimal`)):Oe.test(n)?(i=8,ze(n,t,r.start,`0o`,/^[0-7]+$/,`Octal`,`Invalid octal digits (must be 0-7)`)):ke.test(n)&&(i=2,ze(n,t,r.start,`0b`,/^[01]+$/,`Binary`,`Invalid binary digits (must be 0 or 1)`));let a=parseInt(n.replace(Ce,``).replace(Oe,``).replace(ke,``),i);if(Number.isNaN(a))throw new D(t,r.start,`Invalid integer "${n}"`);return{type:`Integer`,loc:r,raw:n,value:a}}function $e(e,t){if(e.value.type===`Comment`)return[We(e)];if(e.value.type===`Bracket`)return[Ge(e,t)];if(e.value.type===`Literal`)return et(e,t);throw e.value.type===`Equal`?new D(t,e.value.loc.start,`Missing key before '='`):new D(t,e.value.loc.start,`Unexpected token "${e.value.type}"`)}function et(e,t){let n=e.value.raw;if(n.endsWith(`:`))throw new D(t,{line:e.value.loc.start.line,column:e.value.loc.start.column+[...n].length-1},`Use '=' to separate keys and values, not ':'`);je(n,t,e.value.loc.start),n.startsWith(`"`)||n.startsWith(`'`)||Ae(n,t,e.value.loc.start);let r;r=[Me(e.value.raw,t,e.value.loc.start)];let i={type:`Key`,loc:e.value.loc,raw:e.value.raw,value:r};for(;!e.peek().done&&e.peek().value.type===`Dot`;){e.next();let n=e.value;e.next();let r=e.value.raw;je(r,t,e.value.loc.start),r.startsWith(`"`)||r.startsWith(`'`)||Ae(r,t,e.value.loc.start);let a=S(t,i.loc.end),o=S(t,n.loc.end),s=a.slice(i.loc.end.column,n.loc.start.column),c=o.slice(n.loc.end.column,e.value.loc.start.column);i.loc.end=e.value.loc.end,i.raw+=`${s}.${c}${e.value.raw}`,i.value.push(Me(e.value.raw,t,e.value.loc.start))}if(e.next(),!e.done&&e.value.loc.start.line!==i.loc.end.line)throw new D(t,e.value.loc.start,`"=" must be on same line as key`);if(e.done||e.value.type!==`Equal`)throw!e.done&&e.value.raw===`:`?new D(t,e.value.loc.start,`Use '=' to separate keys and values, not ':'`):new D(t,e.done?i.loc.end:e.value.loc.start,`Expected "=" for key-value`);let a=e.value.loc.start.column,o=e.value.loc.start.line;if(e.next(),e.done)throw new D(t,i.loc.start,`Expected value, reached EOF`);if(e.value.loc.start.line!==o)throw new D(t,e.value.loc.start,`Value must be on same line as '='`);if(e.done)throw new D(t,i.loc.start,`Expected value for key-value`);let s=tt(e,t),c=s[0];if(!e.peek().done){let n=e.peek().value;if(n.type===`Dot`&&n.loc.start.line===c.loc.end.line&&(c.type===`Float`||c.type===`Integer`))throw new D(t,n.loc.start,`Invalid number: multiple decimal points not allowed`);if((n.type===`Literal`||n.type===`Bracket`)&&n.loc.start.line===c.loc.end.line)throw new D(t,n.loc.start,`Key/value pairs must be separated by a newline`)}return s[0]={type:`KeyValue`,key:i,value:c,loc:{start:w(i.loc.start),end:w(c.loc.end)},equals:a},s}function tt(e,t){if(e.value.type===`Literal`){let n=e.value.raw;return n[0]===`"`||n[0]===`'`?[Ke(e,t)]:n===xe||n===`false`?[qe(e)]:/^\d/.test(n)&&(/^\d{1,}-\d{1,}/.test(n)||/^\d{1,}:\d{1,}/.test(n))?[Je(e,t)]:!e.peek().done&&e.peek().value.type===`Dot`||we.test(n)||Te.test(n)||Se.test(n)&&!Ee.test(n)?[Ze(e,t)]:[Qe(e,t)]}if(e.value.type===`Curly`){let[n,r]=rt(e,t);return[n,...r]}if(e.value.type===`Bracket`){let[n,r]=it(e,t);return[n,...r]}throw e.value.type===`Dot`?new D(t,e.value.loc.start,`Number cannot start with a dot`):new D(t,e.value.loc.start,`Unrecognized token type`)}function nt(e){for(let t=1;t=1)return e}}}function rt(e,t){if(e.value.raw!==`{`)throw new D(t,e.value.loc.start,`Expected "{" for inline table`);let n={type:`InlineTable`,loc:e.value.loc,items:[]},r=[];for(e.next();!e.done&&(e.value.type!==`Curly`||e.value.raw!==`}`);){if(e.value.type===`Comment`){r.push(We(e)),e.next();continue}if(e.value.type===`Comma`){let r=n.items[n.items.length-1];if(!r)throw new D(t,e.value.loc.start,`Leading comma in inline table`);if(r.comma)throw new D(t,e.value.loc.start,`Consecutive commas in inline table`);r.comma=!0,r.loc.end=e.value.loc.start,e.next();continue}let i=n.items[n.items.length-1];if(i&&!i.comma)throw new D(t,e.value.loc.start,`Missing comma between inline table items`);let a=$e(e,t),o=a[0];if(o.type===`KeyValue`){n.items.push({type:`InlineItem`,loc:T(o.loc),item:o,comma:!1});for(let e=1;e=-(2**53-1)&&e<=2**53-1}function ct(e){return typeof e==`bigint`}function lt(e){return typeof e==`number`&&(!st(e)||!isFinite(e)||Object.is(e,-0))}function ut(e){return typeof e==`boolean`}function dt(e){return Object.prototype.toString.call(e)===`[object Date]`}const ft=new Set([`Temporal.PlainDate`,`Temporal.PlainTime`,`Temporal.PlainDateTime`,`Temporal.ZonedDateTime`,`PlainDate`,`PlainTime`,`PlainDateTime`,`ZonedDateTime`]);function pt(e){var t;return typeof e==`object`&&!!e&&ft.has((t=e.constructor)==null?void 0:t.name)&&typeof e.equals==`function`}function mt(e){var t;let n=((t=e.constructor)==null?void 0:t.name)??``;if(n===`Temporal.ZonedDateTime`||n===`ZonedDateTime`){let t=e.toString(),n=t.match(/\[(.+)\]$/);if(n&&!/^[+-]\d{2}:\d{2}$/.test(n[1]))throw Error(`ZonedDateTime with IANA timezone "${t}" cannot be represented in TOML. TOML only supports offset-based timezones (+05:30, Z).`);return t.replace(/\[.*\]$/,``).replace(/(\+00:00)$/,`Z`)}let r=e.toString();if(/\[.*\]/.test(r))throw Error(`Temporal value contains unsupported annotation: "${r}". TOML only supports ISO 8601 calendar.`);return r}function F(e){return e&&typeof e==`object`&&!dt(e)&&!pt(e)&&!Array.isArray(e)}function ht(e){return e!=null&&typeof e[Symbol.iterator]==`function`}function gt(e){return e.startsWith(`"`)&&!e.startsWith(`"""`)}function _t(e){return e.startsWith(`"""`)}function vt(e){return e.startsWith(`'`)&&!e.startsWith(`'''`)}function yt(e){return e.startsWith(`'''`)}function bt(e,t){return t in e}function I(e,t){if(e.length!==t.length)return!1;for(let n=0;n0;){let e=r.pop();if(`text`in e){t.push(e.text),`leave`in e&&n.delete(e.leave);continue}let i=e.value;if(F(i)){if(n.has(i))throw TypeError(`Cannot stableStringify a circular structure`);n.add(i);let e=Object.keys(i).sort();t.push(`{`),r.push({leave:i,text:`}`});for(let t=e.length-1;t>=0;t--){let n=e[t];t=0;e--)e57343)){if(n<=56319){let n=e.charCodeAt(t+1);if(n>=56320&&n<=57343){t++;continue}}return{index:t,code:n}}}}function Et(e,t){if(wt!==void 0&&wt.call(e))return;let n=Tt(e);if(!n)return;let r=n.code.toString(16).toUpperCase().padStart(4,`0`);throw Error(`${t} contains a lone surrogate (U+${r}) at index ${n.index}. Unpaired surrogates are not Unicode scalar values and cannot be encoded as TOML.`)}function Dt(e,t){ht(e)?n(e,null):r(e,null);function n(e,t){for(let n of e)r(n,t)}function r(e,i){let a=t[e.type];switch(a&&typeof a==`function`&&a(e,i),a&&a.enter&&a.enter(e,i),e.type){case`Document`:n(e.items,e);break;case`Table`:r(e.key,e),n(e.items,e);break;case`TableKey`:r(e.item,e);break;case`TableArray`:r(e.key,e),n(e.items,e);break;case`TableArrayKey`:r(e.item,e);break;case`KeyValue`:r(e.key,e),r(e.value,e);break;case`InlineArray`:n(e.items,e);break;case`InlineItem`:r(e.item,e);break;case`InlineTable`:n(e.items,e);break;case`Key`:case`String`:case`Integer`:case`Float`:case`Boolean`:case`DateTime`:case`Comment`:break;default:throw Error(`Unrecognized node type "${e.type}"`)}a&&a.exit&&a.exit(e,i)}}function Ot(e){return new TextDecoder(`utf-8`,{fatal:!0,ignoreBOM:!0}).decode(e)}function kt(e){return e.charCodeAt(0)===65279}function At(e){return kt(e)?e.slice(1):e}const jt=`auto`,Mt=`auto`;function Nt(e){for(let t of e){let e=Lt(t);if(e!==null)return e}return!1}function Pt(e,t){for(let n of t){let t=Ft(n,e);if(t!==null)return t}return!0}function Ft(e,t){if(!e||typeof e!=`object`)return null;if((e.type===`InlineArray`||e.type===`InlineTable`)&&e.loc){let n=It(e.loc,t);if(n!==null)return n}if(e.items&&Array.isArray(e.items))for(let n of e.items){let e=Ft(n,t);if(e!==null)return e;if(n.item){let e=Ft(n.item,t);if(e!==null)return e}}for(let n of[`value`,`key`,`item`])if(e[n]){let r=Ft(e[n],t);if(r!==null)return r}return null}function It(e,t){if(!e||!e.start||!e.end)return null;let n=t.split(/\r?\n/),r=e.start.line-1,i=e.end.line-1,a=e.start.column,o=e.end.column,s=``;if(r===i){var c;s=((c=n[r])==null?void 0:c.substring(a,o+1))||``}else{n[r]&&(s+=n[r].substring(a));for(let e=r+1;e0:u?u[1].length>0:null}function Lt(e){if(!e||typeof e!=`object`)return null;if(e.type===`InlineArray`&&e.items&&Array.isArray(e.items)||e.type===`InlineTable`&&e.items&&Array.isArray(e.items))return Rt(e.items);if(e.type===`KeyValue`&&e.value)return Lt(e.value);if(e.items&&Array.isArray(e.items))for(let t of e.items){let e=Lt(t);if(e!==null)return e}return null}function Rt(e){if(e.length===0)return null;let t=e[e.length-1];return t&&typeof t==`object`&&`comma`in t?t.comma===!0:!1}function zt(e){let t=e.indexOf(` +`);return t>0&&e[t-1]===`\r`?`\r +`:` +`}function Bt(e){let t=0,n=e.length;for(;n>0&&e[n-1]===` +`;)t++,n--,n>0&&e[n-1]===`\r`&&n--;return t}function Vt(e){let t=e.split(/\r?\n/),n=0,r=0;for(let e of t)if(e.length!==0&&(e[0]===` `?n++:e[0]===` `&&r++,n+r>=5))break;return n>0&&n>=r}function Ht(e){let t=0;for(;t{var s,c,l;if(!e||typeof e!=`object`)return;if(t!==void 0&&a.has(e.type)&&((s=e.loc)==null||(s=s.start)==null?void 0:s.line)>t){let t=Ht(n[e.loc.start.line-1]??``);t>0&&r.push(t)}let u=i.has(e.type)&&((c=e.loc)==null||(c=c.end)==null?void 0:c.line)>((l=e.loc)==null||(l=l.start)==null?void 0:l.line)?e.loc.start.line:t;if(Array.isArray(e.items))for(let t of e.items)o(t,u);e.item&&o(e.item,u),e.value&&o(e.value,u)},s=Array.isArray(t)?t:Array.from(t??[]);for(let e of s)o(e);if(r.length>0)return Math.min(...r);let c=n.filter(e=>e.trim().length>0&&!e.trimStart().startsWith(`#`)).map(Ht).filter(e=>e>0);return c.length>0?Math.min(...c):2}const Wt=new Map([[` +`,` +`],[`\r +`,`\r +`],[`\\n`,` +`],[`\\r\\n`,`\r +`],[`LF`,` +`],[`CRLF`,`\r +`],[`UNIX`,` +`],[`DOS`,`\r +`]]);function Gt(e){let t=Wt.get(e)??Wt.get(e.toUpperCase());if(t===void 0)throw TypeError(`Invalid newLine value: expected LF or CRLF`);return t}function Kt(e){if(!e||typeof e!=`object`)return{};let t=e=>typeof e==`boolean`?null:`expected boolean, got ${typeof e}`,n=e=>e==null||typeof e==`boolean`||e===`auto`||e===`parent`||typeof e==`number`&&Number.isInteger(e)&&e>=0?null:`expected boolean, non-negative integer, "auto" or "parent", got ${typeof e}`,r={newLine:e=>typeof e==`string`?null:`expected string, got ${typeof e}`,trailingNewline:e=>typeof e==`boolean`||typeof e==`number`?null:`expected boolean or number, got ${typeof e}`,trailingComma:t,bracketSpacing:t,leadingBom:t,inlineTableStart:e=>e==null||typeof e==`number`&&Number.isInteger(e)&&e>=0?null:`expected non-negative integer or undefined, got ${typeof e}`,truncateZeroTimeInDates:t,useTabsForIndentation:t,indentWidth:e=>e==null||typeof e==`number`&&Number.isInteger(e)&&e>0?null:`expected positive integer or undefined, got ${typeof e}`,minimumDecimals:e=>e==null||typeof e==`number`&&Number.isInteger(e)&&e>=0?null:`expected non-negative integer or undefined, got ${typeof e}`,updateOrder:t,multilineTable:n,multilineArray:n},i={},a=[],o=[];for(let t in e){let n=Object.prototype.hasOwnProperty.call(r,t)?r[t]:void 0;if(n){let r=e[t],a=n(r);a?o.push(`${t} (${a})`):i[t]=r}else Object.prototype.hasOwnProperty.call(e,t)&&a.push(t)}if(a.length>0&&console.warn(`toml-patch: Ignoring unsupported format properties: ${a.join(`, `)}. Supported properties are: ${Object.keys(r).join(`, `)}`),o.length>0)throw TypeError(`Invalid types for format properties: ${o.join(`, `)}`);return`newLine`in i&&(i.newLine=Gt(i.newLine)),i}function L(e,t){if(e){if(e instanceof R)return e;{let n=Kt(e);return new R(n.newLine??t.newLine,n.trailingNewline??t.trailingNewline,n.trailingComma??t.trailingComma,n.bracketSpacing??t.bracketSpacing,`inlineTableStart`in n?n.inlineTableStart:t.inlineTableStart,n.truncateZeroTimeInDates??t.truncateZeroTimeInDates,n.useTabsForIndentation??t.useTabsForIndentation,n.minimumDecimals??t.minimumDecimals,n.leadingBom??t.leadingBom,n.updateOrder??t.updateOrder,n.indentWidth??t.indentWidth,n.multilineTable??t.multilineTable,n.multilineArray??t.multilineArray)}}return t}var R=class e{newLine;trailingNewline;trailingComma;bracketSpacing;leadingBom;inlineTableStart;truncateZeroTimeInDates;useTabsForIndentation;indentWidth;minimumDecimals;updateOrder;multilineTable;multilineArray;constructor(e,t,n,r,i,a,o,s,c,l,u,d,f){this.newLine=e==null?` +`:Gt(e),this.trailingNewline=t??1,this.trailingComma=n??!1,this.bracketSpacing=r??!0,this.inlineTableStart=i??1,this.truncateZeroTimeInDates=a??!1,this.useTabsForIndentation=o??!1,this.indentWidth=this.useTabsForIndentation?1:u??2,this.minimumDecimals=s??0,this.leadingBom=c??!1,this.updateOrder=l??!1,this.multilineTable=d??`auto`,this.multilineArray=f??`auto`}static default(){return new e(` +`,1,!1,!0,1,!1,!1,0,!1,!1,2,jt,Mt)}static autoDetectFormat(t){return e.autoDetectFormatWithCst(t)}static autoDetectFormatWithCst(t,n){let r=e.default();r.leadingBom=kt(t);let i=At(t),a=[];r.newLine=zt(i),r.trailingNewline=Bt(i);try{a=Array.isArray(n)?n:Array.from(n??Be(i)),r.trailingComma=Nt(a),r.bracketSpacing=Pt(i,a)}catch{r.trailingComma=!1,r.bracketSpacing=!0}return r.useTabsForIndentation=Vt(i),r.indentWidth=r.useTabsForIndentation?1:Ut(i,a),r.inlineTableStart=1,r.truncateZeroTimeInDates=!1,r.minimumDecimals=0,r.updateOrder=!1,r.multilineTable=jt,r.multilineArray=Mt,r}static autoDetectFormatWithAst(t,n){return e.autoDetectFormatWithCst(t,n)}};const qt=new WeakMap,Jt=new WeakMap,Yt=new WeakSet,Xt=new WeakMap;function Zt(e,t){return(e[t.line-1]??e[e.length-1])+t.column}function Qt(e,t){let n=$t(t);for(let t of e)n(t)}function $t(e){let n=[0];for(let t=0;t{let o=[i];for(;o.length>0;){let i=o.pop(),s=[Zt(n,i.loc.start),Zt(n,i.loc.end)];if(Object.defineProperty(i,"range",{configurable:!0,enumerable:!1,value:s}),qt.set(i,e),Xt.set(i,tn(i).slice()),a(i))o.push(i.value),o.push(i.key);else if(u(i))o.push(i.item);else if(t(i)||r(i)){o.push(i.key);let e=i.items;for(let t=e.length-1;t>=0;t--)o.push(e[t])}else if(p(i)){let e=i.items;for(let t=e.length-1;t>=0;t--)o.push(e[t])}}}}function z(e){return qt.get(e)}function en(e){let t=Xt.get(e);if(!t)return!1;let n=tn(e);return t.length===n.length&&t.every((e,t)=>e===n[t])}function tn(e){return a(e)?[e.key,e.value]:u(e)?[e.item]:t(e)||r(e)?[e.key,...e.items]:p(e)?e.items:[]}function nn(e){if(Yt.has(e))return;let t=[e];for(;t.length>0;){let e=t.pop();for(let n of tn(e))Jt.set(n,e),t.push(n)}Yt.add(e)}function rn(e){let t=[e];for(;t.length>0;){let e=t.pop();on(e);for(let n of tn(e))t.push(n)}}function an(e){rn(e)}function on(e){Object.defineProperty(e,"dirty",{configurable:!0,enumerable:!1,value:!0,writable:!0})}function sn(e,t,n){nn(e),n&&(Jt.set(n,t),rn(n));let r=t;for(;r;)on(r),r=Jt.get(r)}function cn(e){let t=Xt.get(e);return!!(t!=null&&t[0])&&t[0].loc.start.line>e.loc.start.line}const ln=new WeakMap,un=new WeakSet,dn=new WeakSet,fn=new WeakSet,pn=new WeakSet;function mn(e,t,n,r){let i=e===`array`?r.multilineArray:r.multilineTable;return typeof i==`boolean`?i:typeof i==`number`?t>=i:i===`parent`&&n}function hn(e,t){ln.set(e,t),t&&e.items.length===0&&(e.loc.end.line=e.loc.start.line+1,e.loc.end.column=e.loc.start.column+1)}function gn(e){return ln.get(e)}function _n(e){un.add(e)}function vn(e){return un.has(e)}function yn(e){dn.add(e)}function bn(e){return dn.has(e)}function xn(e){fn.add(e)}function Sn(e){return fn.has(e)}function Cn(e){pn.add(e)}function wn(e){return pn.has(e)}function Tn(e){return e.items.length?e.items.some((t,n)=>{let r=e.items[n-1];return t.loc.start.line>((r==null?void 0:r.loc.end.line)??e.loc.start.line)}):e.loc.end.line>e.loc.start.line}function En(e,t){let n=(e,r)=>{if(e===t)return r;if(a(e))return n(e.value,r);if(u(e))return n(e.item,r);if(l(e)||d(e)){for(let t of e.items){let e=n(t,r+1);if(e!==void 0)return e}return}if(p(e))for(let t of e.items){let e=n(t,r);if(e!==void 0)return e}};return n(e,0)}function Dn(e,t){let n=(e,r)=>{if(e===t)return r;if(a(e))return n(e.value,r);if(u(e))return n(e.item,r);if(l(e)||d(e)){for(let t of e.items){let r=n(t,e);if(r)return r}return}if(p(e))for(let t of e.items){let e=n(t,r);if(e)return e}};return n(e,void 0)}const On=new WeakSet,kn=new WeakSet,An=new WeakMap,jn=new WeakMap;function Mn(e,t){An.set(e,t)}function Nn(e,t){jn.set(e,t)}function Pn(e){kn.add(e)}const Fn=new WeakMap,In=new WeakSet,Ln=new WeakSet;function Rn(e){return Ln.has(e)}function zn(e){Ln.delete(e)}function Bn(e,t,n){rr(n,B(e),t)}function Vn(e){an(e),On.add(e)}const Hn=new WeakMap,Un=e=>(Hn.has(e)||Hn.set(e,new WeakMap),Hn.get(e));function Wn(e){return Hn.has(e)||Hn.set(e,new WeakMap),Hn.get(e)}const Gn=new WeakMap;function B(e){return Gn.has(e)||Gn.set(e,new WeakMap),Gn.get(e)}function Kn(e,t,n,r){if(p(t)){let e=t.items.indexOf(n);if(e<0)throw Error(`Item not found in parent for replace`);t.items.splice(e,1,r)}else if(a(t)&&d(t.value)&&!d(n)){let e=t.value.items.indexOf(n);if(e<0)throw Error(`Item not found in parent for replace`);t.value.items.splice(e,1,r)}else if(m(t))t.item=r;else if(a(t))t.key===n?t.key=r:t.value=r;else throw Error(`Unsupported parent type "${t.type}" for replace`);W(r,{lines:n.loc.start.line-r.loc.start.line,columns:n.loc.start.column-r.loc.start.column});let i=b(n.loc),o=b(r.loc);rr({lines:o.lines-i.lines,columns:o.columns-i.columns},B(e),r,n),kn.has(e)||sn(e,t,r),On.add(e)}function V(t,n,r,i,a,o,s,c=!1){if(!p(n))throw Error(`Unsupported parent type "${n.type}" for insert`);i=i!=null&&typeof i==`number`?i:n.items.length;let u,m;l(n)||d(n)?{shift:u,offset:m}=Zn(n,r,i,An.get(t)??2):a&&e(n)?{shift:u,offset:m}=Qn(n,r,i):{shift:u,offset:m}=Jn(t,n,r,i,s),W(r,u,{first_line_only:c});let h=n.items[i-1],g=h&&B(t).get(h);if(g&&(m.lines+=g.lines,m.columns+=g.columns,B(t).delete(h)),(d(n)||l(n))&&m.lines!==0&&(o||p(t))&&t!==n){let e=r.loc.start.line,i=o??t.items,a=qn(n,i);for(let t=0;ta.loc.end.line||r{t===e&&(r=!0)};if(Dt(t,{InlineArray:i,InlineTable:i}),r)return t}return e}function Jn(n,i,a,o,s){if(!h(a))throw Error(`Incompatible child type "${a.type}"`);let c=i.items[o-1],l=e(i)&&!i.items.length,u=e(i)&&!l&&c===void 0;i.items.splice(o,0,a);let d;if(c!==void 0){if(kn.has(n))d=c;else{let e=!1;for(let t=0;te||a.line===e&&a.column>t)&&(e=a.line,t=a.column,d=r)}}}}let p=d?{line:d.loc.end.line,column:f(d)?i.loc.start.column:d.loc.start.column}:w(i.loc.start),m=t(a)||r(a),g=0;s===void 0?l||u||(g=m?2:1):g=s,p.line+=g;let _={lines:p.line-a.loc.start.line,columns:p.column-a.loc.start.column},v=b(a.loc),y=!kn.has(n)&&c===void 0&&(t(i)||r(i)||e(i))&&Fn.has(i),x=y&&!In.has(i);y&&(Fn.delete(i),In.delete(i));let S=y&&!x,C=0;if(S){let e=t(i)||r(i)?B(n).get(i.key):Un(n).get(i);C=e?-e.lines:1}let T=x||S?g-2:g-1,E={lines:u?v.lines+1:v.lines+T,columns:v.columns};return{shift:{lines:_.lines+C,columns:_.columns},offset:E}}function Yn(e,t,n,r={}){let{useNewLine:i=!1,skipCommaSpace:a=2,skipBracketSpace:o=1,hasCommaHandling:s=!1,isLastElement:c=!1,hasSeparatingCommaBefore:u=!1,hasSeparatingCommaAfter:p=!1,hasTrailingComma:m=!1,indentWidth:h=2}=r,g=n>0?e.items[n-1]:void 0,_=g?{line:g.loc.end.line,column:i?f(g)?e.loc.start.column:g.loc.start.column:g.loc.end.column}:w(e.loc.start);if(!g&&i){let t=e.items.find((e,t)=>t>n&&!f(e));t?_.column=t.loc.start.column:e.loc.end.line>e.loc.start.line&&(_.column=(l(e)||d(e)?jn.get(e):void 0)??e.loc.end.column-1+h)}let v=0;if(i)v=1;else{let e=u||!s&&!!g;e&&s?_.column+=a:(e||s&&!g)&&(_.column+=o)}_.line+=v;let y={lines:_.line-t.loc.start.line,columns:_.column-t.loc.start.column},x=b(t.loc);if(!s)return{shift:y,offset:{lines:x.lines+(v-1),columns:x.columns}};let S=u&&m&&!p&&c,C=0;S&&(C=-1);let T=x.columns+(u||p?a:0)+(m?1+C:0);return i&&c&&g&&e.loc.end.line===g.loc.end.line&&(T=x.columns-(g.loc.end.column-g.loc.start.column)),{shift:y,offset:{lines:x.lines+(v-1),columns:T}}}function Xn(e){return fe(e)??2}function Zn(e,t,n,r){if(!u(t))throw Error(`Incompatible child type "${t.type}"`);let i=Xn(e),a=n==null?P(e.items):e.items[n-1],o=n==null||n===e.items.length,s=n==null?void 0:e.items[n];e.items.splice(n,0,t);let c=!!a,l=!o;return c&&(a.comma=!0),l&&(t.comma=!0),Yn(e,t,n,{useNewLine:nr(e,t)&&!(a&&s&&s.loc.start.line===a.loc.end.line&&s.loc.start.column>=a.loc.end.column)&&!(gn(e)!==!0&&d(e)&&!s&&a&&a.loc.end.line===e.loc.end.line),skipCommaSpace:i,hasCommaHandling:!0,isLastElement:o,hasSeparatingCommaBefore:c,hasSeparatingCommaAfter:l,hasTrailingComma:o&&t.comma===!0,indentWidth:r})}function Qn(e,t,n){let r=Yn(e,t,n,{useNewLine:!1,hasCommaHandling:!1});return e.items.splice(n,0,t),r}function H(n,i,a,o){if(!p(i))throw Error(`Unsupported parent type "${i.type}" for remove`);kn.has(n)||sn(n,i);let s=i.items.indexOf(a);if(s<0){if(s=i.items.findIndex(e=>m(e)&&e.item===a),s<0)throw Error(`Node not found in parent for removal`);a=i.items[s]}let c=i.items[s-1],h=i.items[s+1];i.items.splice(s,1);let g=b(a.loc);h&&f(h)&&h.loc.start.line===a.loc.end.line&&(g=b({start:a.loc.start,end:h.loc.end}),h=i.items[s+1],i.items.splice(s,1));let _=c&&u(c)||h&&u(h),v=c?B(n).get(c):void 0,y=c&&c.loc.end.line===a.loc.start.line+((v==null?void 0:v.lines)??0),x=h&&h.loc.start.line===a.loc.end.line,S=_&&(y||x),C={lines:-(g.lines-+!!S),columns:S?-g.columns:0};if((e(i)||t(i)||r(i))&&h&&!S&&h.loc.start.line>a.loc.end.line){let e=t(a)||r(a),o=t(h)||r(h),l=-1;for(let e=0;el&&(l=t)}let u=c?B(n).get(c):void 0,d=B(n).get(a),f=c?e?a.loc.start.line+((u==null?void 0:u.lines)??0)-l-1:0:o?h.loc.start.line+((d==null?void 0:d.lines)??0)-a.loc.end.line-1:0;f>0&&(C.lines-=f)}let w=(d(i)||l(i))&&i.loc.end.line>i.loc.start.line,T=w&&a.loc.start.line===i.loc.start.line;w&&!T&&c===void 0&&h===void 0&&a.loc.start.line>i.loc.start.line&&i.loc.end.line>a.loc.end.line&&Cn(i),c===void 0&&h===void 0&&(!w||T)&&(C.lines=0,C.columns=0,(d(i)||l(i))&&Ln.add(i),(t(i)||r(i)||e(i))&&Fn.set(i,!0)),(c!==void 0||h!==void 0)&&(t(i)||r(i)||e(i))&&In.add(i);let E=l(i)||d(i)?Xn(i):2;if(_&&y&&(C.columns-=E),_&&!c&&h&&(nr(i)||(C.columns-=E)),_&&c&&!h&&(c.comma=!!a.comma,y)){var D;let e=c?((D=B(n).get(c))==null?void 0:D.columns)??0:0;C.columns=c.loc.end.column-a.loc.end.column-e}let O,k;c?(O=c,k=B(n)):(t(i)||r(i))&&`key`in i?(O=i.key,k=B(n)):(O=i,k=Un(n));let ee=B(n),A=k.get(O);A&&(C.lines+=A.lines,C.columns+=A.columns);let te=ee.get(a);if(te&&(C.lines+=te.lines,S&&(C.columns+=te.columns)),k.set(O,C),On.add(n),w&&(o||p(n))&&n!==i){let e=a.loc.start.line,t=o??n.items,r=qn(i,t),s=[];for(let n=0;nr.loc.end.line||(o===e&&(o=0;e--)t.splice(s[e],1)}}function $n(e){let t=e.loc.start.line,n=e.loc.start.column;if(`key`in e){let r=e.key.loc.end;(r.line>t||r.line===t&&r.column>n)&&(t=r.line,n=r.column)}for(let r=0;rt||i.line===t&&i.column>n)&&(t=i.line,n=i.column)}e.loc.end={line:t,column:n}}function er(e,t,n=!0){if(!n||!t.items.length)return;rr({lines:0,columns:1},Un(e),t);let r=P(t.items);rr({lines:0,columns:1},B(e),r),On.add(e)}function tr(e,t,n=!1){if(!n||!t.items.length)return;let r=P(t.items);r.comma=!0,rr({lines:0,columns:1},B(e),r),On.add(e)}function U(e){if(!On.has(e))return;let t=Un(e),n=B(e),r=0,i={};function a(e){switch(e.type){case`Document`:{let t=e;o(t);for(let e=0;e{if(!o||e.loc.start.line===c){e.loc.start.column+=i;let t=(!o||s)&&e.loc.end.line!==e.loc.start.line&&((l(e)||d(e))&&gn(e)!==void 0&&!vn(e)||u(e)&&((l(e.item)||d(e.item))&&gn(e.item)!==void 0&&!vn(e.item)||a(e.item)&&(l(e.item.value)||d(e.item.value))&&gn(e.item.value)!==void 0&&!vn(e.item.value))||a(e)&&(l(e.value)||d(e.value))&&gn(e.value)!==void 0);(e.loc.end.line===e.loc.start.line||t)&&(e.loc.end.column+=i)}e.loc.start.line+=r,e.loc.end.line+=r};return Dt(e,{Table:p,TableKey:p,TableArray:p,TableArrayKey:p,KeyValue(e){let t=!o||e.loc.start.line===c;p(e),t&&(e.equals+=i)},Key:p,String:p,Integer:p,Float:p,Boolean:p,DateTime:p,InlineArray:p,InlineItem:p,InlineTable:p,Comment:p}),e}function nr(e,t){let n=gn(e);if(n===!0)return!0;if(n!==void 0||!e.items.length)return!1;let r=t?e.items.filter(e=>e!==t):e.items;return r.length?e.loc.end.line>e.loc.start.line&&r.every((e,t)=>t===0||e.loc.start.line>r[t-1].loc.start.line):e.loc.end.line>e.loc.start.line}function rr(e,t,n,r){let i=t.get(r||n);i&&(e.lines+=i.lines,e.columns+=i.columns),t.set(n,e)}function ir(e){let t=e.match(/\r\n|\n/);return(t==null?void 0:t[0])===`\r +`?`\r +`:` +`}function ar(e,t){var n;let r=ir(e);e=e.replace(/\r\n/g,` +`).replace(/\n/g,r);let i,a;e.startsWith(`"""\\${r}`)?(i=4+r.length,a=`"""\\${r}`):e.startsWith(`"""${r}`)?(i=3+r.length,a=`"""${r}`):(i=3,a=`"""`);let o=e.length-3,s=e.slice(i,o).split(r),c=s[s.length-1],l=s.length>1&&/^[\t ]*$/.test(c),u=l?c:``,d=(l?s.slice(0,-1):s).map(e=>{var t;if(/^[\t ]*$/.test(e))return{indent:e,content:``,trailingWs:``,hasBackslash:!1,isBlank:!0};let n=e,r=0;for(let e=n.length-1;e>=0&&n[e]===`\\`;e--)r++;let i=r%2==1;i&&(n=n.slice(0,-1));let a=((t=n.match(/^[\t ]*/))==null?void 0:t[0])??``,o=n.slice(a.length),s=o.match(/([\t ]+)$/),c=s?s[1]:``;return{indent:a,content:o.slice(0,o.length-c.length),trailingWs:c,hasBackslash:i,isBlank:!1}}),f=d.filter(e=>!e.isBlank),p=Math.max(...f.map(e=>e.content.length+e.trailingWs.length),1),m=f.filter(e=>e.hasBackslash),h=m[m.length-1]??f[f.length-1]??{indent:``,trailingWs:``,hasBackslash:!1,content:``,isBlank:!1},g=f[f.length-1]??h,_=[];{let e=[],t=0;for(let n of d)n.isBlank?e.push(n.indent):(t>0&&_.push(e),e=[],t++)}let v=f.some((e,t)=>!e.hasBackslash&&t0?((n=t.match(/^[\t ]*/))==null?void 0:n[0])??``:``,x=e=>{let t=e.match(/\S+| +/g)??[],n=[],r=0;for(;r0&&n[0]===` `)return null;let i=e[e.length-1];if(i.length>0&&i.length-i.trimEnd().length!==g.trailingWs.length)return null;for(let t=0;t0&&E.set(o,i);for(let e of a)S.push(e);t0&&(e[0]===` `||e.length-e.trimEnd().length!==g.trailingWs.length))return null;if(!v){let t=0,n=0;for(let r=0;r=t;r--){let t=f[r],a=t.content+t.trailingWs;if(a.length===0)break;let s=e.length-o-a.length;if(s>=n&&e.slice(s,s+a.length)===a)i++,o+=a.length;else break}if(i>0){let s=e.slice(n,e.length-o);if(s.length===0||s[0]!==` `){for(let e=0;e0)for(let t of _[e])C.push(t)}if(s.length>0){let e=x(s),n=e.join(``),r=/(?0&&!r){let e=t-1;if(e<_.length&&_[e].length>0)for(let t of _[e])C.push(t)}for(let n=0;n0&&/\s$/.test(i)?``:s.endsWith(` `)?` `:``;if(C.push(`${c}${i}${l}\\`),n0)for(let t of _[e])C.push(t)}}let a=f.length-i-1;if(!r&&a>=0&&a<_.length&&_[a].length>0)for(let e of _[a])C.push(e)}else if(t>0){let e=t-1;if(e<_.length&&_[e].length>0)for(let t of _[e])C.push(t)}let e=f.length-i;for(let t=e;te){let e=t-1;if(e<_.length&&_[e].length>0)for(let t of _[e])C.push(t)}C.push(r?`${n.indent}${n.content}${n.trailingWs}${n.hasBackslash&&n.content!==``?`\\`:``}`:`${n.indent}${n.content}${n.trailingWs}\\`)}return l&&!(g.hasBackslash&&g.content===``)?`${a}${C.join(r)}${r}${u}"""`:`${a}${C.join(r)}"""`}}let s=e.slice(n);if(t>0&&(n===e.length||s.length>0&&s[0]!==` `)){for(let e=0;e0)for(let t of _[e])C.push(t)}if(n===e.length){let e=f[t-1],n=g.hasBackslash&&e.content!==``?`\\`:``;C[C.length-1]=`${e.indent}${e.content}${g.trailingWs}${n}`}else{let e=x(s),n=e.join(``),r=/(?0)for(let e of _[i])C.push(e);for(let n=0;n0&&/\s$/.test(a)?``:` `,u=`\\`),C.push(`${c}${a}${l}${u}`),!i){let e=t+n;if(!r&&e<_.length&&_[e].length>0)for(let t of _[e])C.push(t)}}}return l&&!(g.hasBackslash&&g.content===``)?`${a}${C.join(r)}${r}${u}"""`:`${a}${C.join(r)}"""`}}let n=x(e);for(let e of n)S.push(e)}for(let e=0;e0?E.get(e)??``:i?y&&e===0?b:i.indent:r?g.indent:h.indent,o=S[e],s,c;if(r&&t?(s=g.trailingWs,c=g.hasBackslash&&g.content!==``?`\\`:``):r?(s=g.trailingWs,c=``):(s=o.length>0&&/\s$/.test(o)?``:` `,c=`\\`),C.push(`${a}${o}${s}${c}`),!t&&!n&&!D){let t=S.join(``);if(!/(?0)for(let t of _[e])C.push(t)}}return l&&!(g.hasBackslash&&g.content===``)?`${a}${C.join(r)}${r}${u}"""`:`${a}${C.join(r)}"""`}function or(e){switch(e){case`b`:return`\b`;case`t`:return` `;case`n`:return` +`;case`f`:return`\f`;case`r`:return`\r`;case`"`:return`"`;case`\\`:return`\\`;default:return null}}function sr(e,t){return t===`singleline-basic`?e<=31||e===127:e>=0&&e<=7||e===11||e>=14&&e<=31||e===127}function cr(e,t){switch(e){case`\\`:return`\\\\`;case`\b`:return`\\b`;case` `:return`\\t`;case`\f`:return`\\f`;case`"`:return t===`singleline-basic`?`\\"`:`"`;case` +`:return t===`singleline-basic`?`\\n`:` +`;case`\r`:return t===`singleline-basic`?`\\r`:`\r`;default:{let n=e.charCodeAt(0);return sr(n,t)?`\\u${n.toString(16).padStart(4,`0`).toUpperCase()}`:e}}}function lr(e){let t=new Map;for(let n=0;n(Et(e,`Key ${JSON.stringify(e)}`),k.test(e)?e:_r(e))).join(`.`)}function yr(e){let t=vr(e);return{type:`Key`,loc:{start:E(),end:{line:1,column:t.length}},raw:t,value:e}}function br(e,t){return Et(e,`String value`),t?xr(e,t):Sr(e)}function xr(e,t){if(gt(t))return Sr(e,t);if(vt(t))return e.includes(`'`)?e.includes(`'''`)?Sr(e):Tr(e,`'''${t.slice(1,-1)}'''`):Cr(e);if(yt(t))return e.includes(`'''`)?wr(e,`"""${t.slice(3,-3)}"""`):Tr(e,t);if(_t(t))return wr(e,t);throw Error(`Existing raw string value is not valid: ${t}`)}function Sr(e,t){let n=``;return n=t?`"${dr(e,t,`singleline-basic`)}"`:_r(e),{type:`String`,loc:{start:E(),end:{line:1,column:n.length}},raw:n,value:e}}function Cr(e){let t=`'${e}'`,n;if(t.includes(` +`)){let e=t.split(/\r\n|\n/),r=e[e.length-1];n={line:e.length,column:r.length}}else n={line:1,column:t.length};return{type:`String`,loc:{start:E(),end:n},raw:t,value:e}}function wr(e,t){let n=dr(e,t,`multiline-basic`),r=`"""`+(t.startsWith(`"""\r +`)?`\r +`:t.startsWith(`""" +`)?` +`:``)+n+`"""`,i=t.slice(3,-3);if(/(^|[^\\])(\\\\)*\\$/m.test(i)){let e=ar(t,n);e!==null&&(r=e)}let a=Er(r);return{type:`String`,loc:{start:E(),end:a},raw:r,value:e}}function Tr(e,t){let n=`'''`+(t.startsWith(`'''\r +`)?`\r +`:t.startsWith(`''' +`)?` +`:``)+e+`'''`,r=Er(n);return{type:`String`,loc:{start:E(),end:r},raw:n,value:e}}function Er(e){if(e.includes(` +`)){let t=e.split(/\r\n|\n/),n=t[t.length-1];return{line:t.length,column:n.length}}return{line:1,column:e.length}}function Dr(e){let t=e.toString();return{type:`Integer`,loc:{start:E(),end:{line:1,column:t.length}},raw:t,value:e}}function Or(e,t=1){let n,r;if(e===1/0)n=`inf`;else if(e===-1/0)n=`-inf`;else if(Number.isNaN(e)){let t=new Float64Array([e]),i=!!(new DataView(t.buffer).getUint32(4,!0)&2147483648);r=i?`-`:void 0,n=i?`-nan`:`nan`}else if(Object.is(e,-0))n=`-0.`+`0`.repeat(Math.max(t,1));else{n=e.toString();let r=n.search(/[eE]/);if(r!==-1){let e=n.slice(0,r),i=n.slice(r),a=e.indexOf(`.`);if(a===-1)n=`${e}.${`0`.repeat(Math.max(t,1))}${i}`;else{let r=e.length-a-1;n=r{if(!a(e))return!1;let n=d(e.value),r=l(e.value)&&e.value.items.length>0&&e.value.items.every(e=>d(e.item));if(n||r){let n=Gr(e.key.value);return t.inlineTableStart===void 0||n{H(e,e,n),d(n.value)?n.value.items.length===0?V(e,e,G(n.key.value)):V(e,e,Lr(n,t.bracketSpacing)):Rr(n,t.bracketSpacing).forEach(t=>{V(e,e,t)})}),U(e),e)}function Lr(e,t){let n=G(e.key.value);Pn(n);for(let t of e.value.items)V(n,n,t.item,void 0,void 0,void 0,void 0,!0);return U(n),Hr(n,1,t),n}function Rr(e,n){let i=fr();Pn(i);for(let t of e.value.items){let n=mr(e.key.value);V(i,i,n);for(let e of t.item.items)V(i,n,e.item,void 0,void 0,void 0,void 0,!0)}U(i);for(let e of i.items)(t(e)||r(e))&&Hr(e,1,n);return i.items}function zr(e){let t=gn(e);return t===void 0?Tn(e):t}function Br(e){return Math.max(e,2)}function Vr(e,t,n,r=!0){let i=zr(e),o=!1;if(i){let i=t+n;for(let t of e.items)W(t,{lines:0,columns:i-t.loc.start.column}),u(t)&&(l(t.item)||d(t.item)?Vr(t.item,t.item.loc.start.column,Br(n),r)&&(t.loc.end={...t.item.loc.end},o=!0):a(t.item)&&(l(t.item.value)||d(t.item.value))&&Vr(t.item.value,t.item.loc.start.column,n,r)&&(t.item.loc.end={...t.item.value.loc.end},t.loc.end={...t.item.loc.end},o=!0));e.loc.end.column=t+1,o=!0}for(let t of e.items)if(u(t)){if(l(t.item)||d(t.item))Vr(t.item,t.item.loc.start.column,Br(n),r)&&(t.loc.end={...t.item.loc.end},o=!0);else if(a(t.item)&&(l(t.item.value)||d(t.item.value))){let e=t.item.value,i=t.item.loc.start.column,a=zr(e);Vr(e,i,n,r)&&(a&&(e.loc.end.column=i+1),t.item.loc.end={...e.loc.end},t.loc.end={...t.item.loc.end},o=!0)}}let s=fe(e)??2;for(let t=1;t0){let t=e.items[e.items.length-1];e.loc.end={line:t.loc.end.line,column:t.loc.end.column+ +!!t.comma+(r?2:1)}}return o}function Hr(e,t,n=!0){let r=a(e)?[e]:e.items;for(let e of r)a(e)&&(l(e.value)||d(e.value))&&Vr(e.value,e.loc.start.column,t,n)}function Ur(e,t){let n=(e,r)=>{if(r&&(delete e.range,gn(e)===!0||Tn(e))){let n=e.loc.start.column+t;for(let t of e.items)W(t,{lines:0,columns:n-t.loc.start.column});e.loc.end.column=e.loc.start.column+1}for(let t of e.items)u(t)&&(l(t.item)||d(t.item)?n(t.item,l(e)&&d(t.item)):a(t.item)&&(l(t.item.value)||d(t.item.value))&&n(t.item.value,!1))};n(e,!1)}function Wr(e){if(e.items.length>0){let t=e.items[e.items.length-1];e.loc.end.line=t.loc.end.line,e.loc.end.column=t.loc.end.column}else e.loc.end.line=e.key.loc.end.line,e.loc.end.column=e.key.loc.end.column}function Gr(e){return Math.max(0,e.length-1)}function Kr(e,t){if(t.inlineTableStart===void 0||t.inlineTableStart===0)return e;let n=[];for(let i of e.items)if(a(i)&&d(i.value)){if(Gr(i.key.value)0&&U(e)}function Jr(e){let t=0,n=0;for(let r of e.items)n===0&&r.loc.start.line>1?t=1-r.loc.start.line:r.loc.start.line+t>n+2&&(t+=n+2-(r.loc.start.line+t)),W(r,{lines:t,columns:0}),n=r.loc.end.line;return e}function Yr(e){return e.items.length>0&&e.loc.end.line-e.loc.start.line+1>e.items.length}function Xr(e){return l(e)||d(e)}function Zr(e,t,n){if(!u(t)||!Xr(t.item)||!Yr(e))return;let r=e.items.find(e=>Xr(e.item)&&e.item.type===t.item.type&&Yr(e.item));if(!r||!Xr(r.item))return;let i=t.item,a=r.item,o=a.items.length>0?a.items[0].loc.start.column-a.loc.start.column:n,s=a.items.length>0?a.items[0].loc.start.line-a.loc.start.line:1,c=a.items.length>0?a.loc.end.line-a.items[a.items.length-1].loc.end.line:1,l=i.loc.start.line,d=i.loc.start.column,f=l+s;for(let e of i.items)W(e,{lines:f-e.loc.start.line,columns:d+o-e.loc.start.column}),f=e.loc.end.line+1;i.loc.end={line:f-1+c,column:a.loc.end.column},_n(i),t.loc={start:w(i.loc.start),end:w(i.loc.end)}}function Qr(e,t){let n=e=>{let r=!1;for(let i of e.items){if(!u(i)||!Xr(i.item))continue;let a=i.item;if(d(a)&&Tn(a)){delete i.range,delete a.range;let n=a.loc.start.column+Math.max(t,2);for(let e of a.items)W(e,{lines:0,columns:n-e.loc.start.column});a.loc.end.column=a.loc.start.column+1,yn(a),_n(a),r=!0,xn(e),i.comma=!0,i.loc.end={line:a.loc.end.line,column:a.loc.end.column},e.loc.end={line:a.loc.end.line,column:a.loc.end.column+(i.comma?2:1)}}l(a)&&n(a)&&(delete a.range,delete i.range,i.loc.end={line:a.loc.end.line,column:a.loc.end.column},r=!0,xn(e))}if(r){for(let t=1;t1&&Kr(i,t),t.multilineTable!==`auto`||t.multilineArray!==`auto`)for(let e of i.items)(e.type===`Table`||e.type===`TableArray`||a(e)&&(l(e.value)||d(e.value)))&&Hr(e,t.indentWidth,t.bracketSpacing);return Jr(i)}function*$r(e,t,n=0,r=!1){for(let i of Object.keys(e)){let a=e[i];if(a===void 0)continue;let o=ri(a);o!==void 0&&(yield gr([i],ei(o,t,n,r),n>0&&!r))}}function ei(e,t,n,r){let i=t.minimumDecimals??0;if(e===null)throw Error(`"null" values are not supported`);if(e===void 0)throw Error(`"undefined" values are not supported inside arrays`);return ot(e)?br(e):ct(e)?Dr(e):st(e)?i>0?Or(e,i):Dr(e):lt(e)?Or(e,Math.max(i,1)):ut(e)?kr(e):pt(e)?jr(e,t.truncateZeroTimeInDates):dt(e)?Ar(e,t.truncateZeroTimeInDates):Array.isArray(e)?ti(e,t,n,r):ni(e,t,n,r)}function ti(e,t,n,r){let i=Mr(),a=!(e.length===0&&n>0)&&mn(`array`,n,r,t);hn(i,a),Mn(i,t.indentWidth),Pn(i);for(let r of e){let e=ei(r,t,n+1,a),o=K(e);!a&&(e.type===`InlineArray`||e.type===`InlineTable`)&&gn(e)===!0&&_n(e),Zr(i,o,t.indentWidth),V(i,i,o)}return er(i,i,t.bracketSpacing),tr(i,i,t.trailingComma),U(i),i}function ni(e,t,n,r){if(e=ri(e),!F(e))return ei(e,t,n,r);let i=Nr(),a=!(Object.keys(e).length===0&&n>0)&&mn(`table`,n,r,t);hn(i,a),Mn(i,t.indentWidth),Pn(i);for(let r of $r(e,t,n+1,a))V(i,i,K(r));return er(i,i,t.bracketSpacing),tr(i,i,t.trailingComma),U(i),i}function ri(e){return!e||dt(e)||pt(e)?e:typeof e.toJSON==`function`?e.toJSON():e}const ii=/(\r\n|\n)/g;function ai(e,t){let n=[],r=e=>{let t={line:e.loc.end.line,column:e.loc.end.column-1},n=e.items[e.items.length-1];return n&&(n.loc.end.line>t.line||n.loc.end.line===t.line&&n.loc.end.column>t.column)&&(t.line=n.loc.end.line,t.column=n.loc.end.column+ +!!n.comma),t};function i(e){switch(e.type){case`Document`:for(let t=0;t{if(e.type===`String`&&e.loc.end.line>e.loc.start.line)for(let n=e.loc.start.line+1;n<=e.loc.end.line;n++)t.add(n);if(p(e))for(let t of e.items)r(t);a(e)&&r(e.value),u(e)&&r(e.item)};if(ht(e))for(let t of e)r(t);else r(e);for(let e=0;e{if(e.type===`Comment`&&c.push(e),a(e))_(e.value);else if(u(e))_(e.item);else if(e.type===`Table`||e.type===`TableArray`)for(let t of e.items)_(t);else if(p(e))for(let t of e.items)_(t)};for(let e of i)_(e);let v=new WeakSet,y=e=>{for(let n of e)if(a(n)){let e=n.value;(d(e)||l(e))&&v.add(e)}else(t(n)||r(n))&&y(n.items)};y(i);let b=e=>(n.useTabsForIndentation?` `:` `).repeat(e),x=e=>{if(e.length===0)return;o.push(e);let t=e.split(/\r\n|\n/);t.length===1?g+=e.length:(h+=t.length-1,g=t[t.length-1].length)},S=(e,t)=>{e>h?(x(n.newLine.repeat(e-h)),t>0&&x(b(t))):e===h&&t>g?x(g===0?b(t):` `.repeat(t-g)):o.length>0&&(e0&&x(b(t)))},C=e=>{switch(e.type){case`Key`:return e.raw;case`String`:return e.raw;case`Integer`:return e.raw;case`Float`:return e.raw;case`Boolean`:return e.value.toString();case`DateTime`:return e.raw;case`Comment`:return e.raw;default:return}},w=e=>{if(a(e))return[e.key,e.value];if(u(e)||m(e))return[e.item];if(e.type===`Table`||e.type===`TableArray`){let t=e;return[t.key,...t.items]}return p(e)?e.items:[]},T=e=>{let t=[e];for(;t.length>0;){let e=t.pop();if(e.dirty)return!1;let n=C(e);if(n!==void 0){let t=z(e);if(!t||!e.range||t.slice(e.range[0],e.range[1])!==n)return!1}else if(!en(e))return!1;t.push(...w(e))}return!0},E=e=>e.type!==`Document`&&e.type!==`InlineItem`&&e.type!==`TableKey`&&e.type!==`TableArrayKey`&&(C(e)!==void 0||en(e)),D=(e,t,r=!1)=>{t.line>e.line?(x(n.newLine.repeat(t.line-e.line)),t.column>0&&x(r?` `.repeat(t.column):b(t.column))):t.line===e.line&&t.column>e.column&&x(` `.repeat(t.column-e.column))},O=(e,t,n)=>{var r,i;let a=z(e);if(!a||!e.range||l(e)&&Sn(e)&&(!n||t&&!t.range||n&&!n.range)||t&&z(t)!==a||n&&z(n)!==a)return;let o=(t==null||(r=t.range)==null?void 0:r[1])??e.range[0]+1,s=(n==null||(i=n.range)==null?void 0:i[0])??e.range[1]-1;if(o===void 0||s===void 0||s{let n=z(t);return n!==void 0&&z(e)===n&&t.range!==void 0&&e.range!==void 0&&e.range[0]>=t.range[0]&&e.range[1]<=t.range[1]},ee=e=>{let t=d(e)&&bn(e),r;for(let n of e.items){let i=r?{line:r.loc.end.line,column:r.loc.end.column+ +!!r.comma}:{line:e.loc.start.line,column:e.loc.start.column+1},a=c.filter(t=>!f.has(t)&&k(t,e)&&t.loc.start.line>=i.line&&t.loc.end.line<=n.loc.start.line&&(t.loc.start.line>i.line||t.loc.start.column>=i.column)&&(t.loc.end.line=e.loc.start.line&&t.loc.end.line<=e.loc.end.line).sort((e,t)=>e.loc.start.line-t.loc.start.line||e.loc.start.column-t.loc.start.column);if(a.length>0){for(let e of a)D(i,e.loc.start),x(e.raw),f.add(e),i=e.loc.end;D(i,n.loc.start)}else{let a=O(e,r,n);if(a!==void 0)x(a);else{let a=z(n)!==z(e);if(r&&a&&n.loc.start.line===r.loc.end.line){let t=fe(e)??2,n=Math.max(0,t-1);n>0&&x(` `.repeat(n))}else D(i,n.loc.start,t)}}A(n,!1);for(let t of c){if(f.has(t)||!k(t,e)||t.loc.start.line!==n.loc.end.line||t.loc.start.columne.loc.end.line)continue;let r=n.loc.end.column+ +!!n.comma;t.loc.start.column>r&&x(` `.repeat(t.loc.start.column-r)),x(t.raw),f.add(t)}r=n}let i=r?{line:r.loc.end.line,column:r.loc.end.column+ +!!r.comma}:{line:e.loc.start.line,column:e.loc.start.column+1},a=c.filter(t=>!f.has(t)&&k(t,e)&&t.loc.start.line>=i.line&&t.loc.end.line<=e.loc.end.line).sort((e,t)=>e.loc.start.line-t.loc.start.line||e.loc.start.column-t.loc.start.column);if(a.length>0){for(let e of a)D(i,e.loc.start),x(e.raw),f.add(e),i=e.loc.end;D(i,{line:e.loc.end.line,column:e.loc.end.column-1})}else{let a=O(e,r,void 0);if(a!==void 0)x(a);else if(r)t&&r.comma&&x(` `),D(i,{line:e.loc.end.line,column:e.loc.end.column-1},t);else{let t=z(e);if(t&&e.range){let r=t.slice(e.range[0],e.range[1]),i=Math.max(r.lastIndexOf(` +`),r.lastIndexOf(`\r`)),a=v.has(e)||wn(e);if(i!==-1&&a&&cn(e)){var o;let e=((o=r.slice(i+1,-1).match(/^[\t ]*/))==null?void 0:o[0])??``;x(n.newLine+e)}}}}},A=(e,t=!0)=>{if(e.type===`Comment`&&f.has(e))return;let n=z(e);if(!(n!==void 0&&e.range&&s.some(t=>t.source===n&&t.start<=e.range[0]&&t.end>=e.range[1]))){if(E(e)&&T(e)&&e.range&&n!==void 0){let r=n.slice(e.range[0],e.range[1]),i=C(e);if(i===void 0||i===r){t&&S(e.loc.start.line,e.loc.start.column),x(r),s.push({source:n,start:e.range[0],end:e.range[1]});return}}switch(e.type){case`Document`:for(let t of e.items)A(t);break;case`Table`:{let n=e;A(n.key,t);for(let e of n.items)A(e);break}case`TableKey`:{let n=e;t&&S(n.loc.start.line,n.loc.start.column),x(`[${n.item.raw}]`);break}case`TableArray`:{let n=e;A(n.key,t);for(let e of n.items)A(e);break}case`TableArrayKey`:{let n=e;t&&S(n.loc.start.line,n.loc.start.column),x(`[[${n.item.raw}]]`);break}case`KeyValue`:{let n=e;t&&S(n.loc.start.line,n.loc.start.column),A(n.key,!1),D(n.key.loc.end,{line:n.loc.start.line,column:n.equals}),x(`=`),D({line:n.loc.start.line,column:n.equals+1},n.value.loc.start),A(n.value,!1);break}case`Key`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;case`String`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;case`Integer`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;case`Float`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;case`Boolean`:t&&S(e.loc.start.line,e.loc.start.column),x(e.value.toString());break;case`DateTime`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;case`InlineArray`:{let n=e;t&&S(n.loc.start.line,n.loc.start.column),x(`[`),ee(n),x(`]`);break}case`InlineTable`:{let n=e;t&&S(n.loc.start.line,n.loc.start.column),x(`{`),ee(n),x(`}`);break}case`InlineItem`:{let t=e;if(A(t.item,!1),t.comma){let e=z(t);if(e&&t.range&&t.item.range&&z(t.item)===e){let n=e.slice(t.item.range[1],t.range[1]);/^[\t ]*$/.test(n)&&x(n)}x(`,`)}break}case`Comment`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;default:throw Error(`toTOMLCursor: Unrecognized node type: ${String(e.type)}`)}}};for(let e of i)A(e);return o.join(``).replace(/\r\n|\n/g,n.newLine)+n.newLine.repeat(n.trailingNewline)}function si(e,t){let n=Array.isArray(e)?e:ht(e)?Array.from(e):[e],r=[],i=1,a=0,o=e=>(t.useTabsForIndentation?` `:` `).repeat(e),s=e=>{if(e.length===0)return;r.push(e);let t=0,n=-1,o=e.indexOf(` +`);for(;o!==-1;)t++,n=o,o=e.indexOf(` +`,o+1);if(t===0){a+=e.length;return}i+=t,a=e.length-n-1},c=(e,n)=>{e>i?(s(t.newLine.repeat(e-i)),n>0&&s(o(n))):e===i&&n>a?s(a===0?o(n):` `.repeat(n-a)):r.length>0&&(e0&&s(o(n)))},l=e=>{switch(e.type){case`Key`:return e.raw;case`String`:return e.raw;case`Integer`:return e.raw;case`Float`:return e.raw;case`Boolean`:return e.value.toString();case`DateTime`:return e.raw;case`Comment`:return e.raw;default:return``}},u=[],d=(e,t)=>{switch(e.type){case`Document`:{let t=e.items;for(let e=t.length-1;e>=0;e--)u.push([0,t[e],!0]);break}case`Table`:{let n=e,r=n.items;for(let e=r.length-1;e>=0;e--)u.push([0,r[e],!0]);u.push([0,n.key,t]);break}case`TableKey`:{let n=e;u.push([1,`[${n.item.raw}]`]),t&&u.push([3,n.loc.start.line,n.loc.start.column]);break}case`TableArray`:{let n=e,r=n.items;for(let e=r.length-1;e>=0;e--)u.push([0,r[e],!0]);u.push([0,n.key,t]);break}case`TableArrayKey`:{let n=e;u.push([1,`[[${n.item.raw}]]`]),t&&u.push([3,n.loc.start.line,n.loc.start.column]);break}case`KeyValue`:{let n=e;u.push([0,n.value,!1]),u.push([2,n.loc.start.line,n.equals+1,n.value.loc.start.line,n.value.loc.start.column,!1]),u.push([1,`=`]),u.push([2,n.key.loc.end.line,n.key.loc.end.column,n.loc.start.line,n.equals,!1]),u.push([0,n.key,!1]),t&&u.push([3,n.loc.start.line,n.loc.start.column]);break}case`Key`:case`String`:case`Integer`:case`Float`:case`Boolean`:case`DateTime`:case`Comment`:u.push([1,l(e)]),t&&u.push([3,e.loc.start.line,e.loc.start.column]);break;case`InlineArray`:{let n=e,r=n.items;if(u.push([1,`]`]),r.length>0){let e=r[r.length-1];u.push([2,e.loc.end.line,e.loc.end.column+ +!!e.comma,n.loc.end.line,n.loc.end.column-1,!1])}for(let e=r.length-1;e>=0;e--){let t=r[e],i=e>0?r[e-1]:void 0;u.push([0,t,!1]),u.push(i?[2,i.loc.end.line,i.loc.end.column+ +!!i.comma,t.loc.start.line,t.loc.start.column,!1]:[2,n.loc.start.line,n.loc.start.column+1,t.loc.start.line,t.loc.start.column,!1])}u.push([1,`[`]),t&&u.push([3,n.loc.start.line,n.loc.start.column]);break}case`InlineTable`:{let n=e,r=n.items,i=bn(n);if(u.push([1,`}`]),r.length>0){let e=r[r.length-1];i&&e.comma&&u.push([1,` `]),u.push([2,e.loc.end.line,e.loc.end.column+ +!!e.comma,n.loc.end.line,n.loc.end.column-1,i])}for(let e=r.length-1;e>=0;e--){let t=r[e],a=e>0?r[e-1]:void 0;u.push([0,t,!1]),u.push(a?[2,a.loc.end.line,a.loc.end.column+ +!!a.comma,t.loc.start.line,t.loc.start.column,i]:[2,n.loc.start.line,n.loc.start.column+1,t.loc.start.line,t.loc.start.column,i])}u.push([1,`{`]),t&&u.push([3,n.loc.start.line,n.loc.start.column]);break}case`InlineItem`:{let t=e;t.comma&&u.push([1,`,`]),u.push([0,t.item,!1]);break}default:throw Error(`toTOMLSequential: Unrecognized node type: ${String(e.type)}`)}};for(let e=n.length-1;e>=0;e--)u.push([0,n[e],!0]);for(;u.length>0;){let e=u.pop();switch(e[0]){case 0:d(e[1],e[2]);break;case 1:s(e[1]);break;case 2:{let n=e[1],r=e[2],i=e[3],a=e[4],c=e[5];i>n?(s(t.newLine.repeat(i-n)),a>0&&s(c?` `.repeat(a):o(a))):i===n&&a>r&&s(` `.repeat(a-r));break}case 3:c(e[1],e[2])}}return r.join(``).replace(/\r\n|\n/g,t.newLine)+t.newLine.repeat(t.trailingNewline)}function ci(e,t,n){if(t.start.line===t.end.line){let r=ui(e,t.start.line),i=r.substring(0,t.start.column),a=i.lengthe!==` +`&&e!==`\r +`),i=t.end.line-t.start.line+1;if(r.length!==i)throw Error(`Mismatch between location and raw string, expected ${i} lines for "${n}"`);for(let n=t.start.line;n<=t.end.line;n++){let i=ui(e,n),a=n===t.start.line,o=n===t.end.line,s=``;if(a){let e=i.substring(0,t.start.column);s=e.length=BigInt(-(2**53-1))&&i<=BigInt(2**53-1)?Number(i):i}function fi(e,t,n){for(let r of e.items){let e=r.item;if(e.value.type===`InlineTable`){let r=t.concat(e.key.value);n.add(xi(r)),fi(e.value,r,n)}}}function pi(e){var t,n,r,i,a,o;if((t=e.constructor)!=null&&(t=t.name)!=null&&(n=t.startsWith)!=null&&n.call(t,`Temporal.`)||((r=e.constructor)==null?void 0:r.name)===`PlainDate`||((i=e.constructor)==null?void 0:i.name)===`PlainTime`||((a=e.constructor)==null?void 0:a.name)===`PlainDateTime`||((o=e.constructor)==null?void 0:o.name)===`ZonedDateTime`)return e;let s=globalThis.Temporal;if(e instanceof ge)return s.PlainDate.from(e.toISOString());if(e instanceof _e)return s.PlainTime.from(e.toISOString());if(e instanceof ve)return s.PlainDateTime.from(e.toISOString().replace(` `,`T`));if(e instanceof ye){let t=e.toISOString().replace(` `,`T`),n=t.match(/([+-]\d{2}:\d{2}|Z)$/),r=n?n[1]:`Z`,i=t.replace(/([+-]\d{2}:\d{2}|Z)$/,``),a=r===`Z`?`+00:00`:r;return s.ZonedDateTime.from(`${i}${r}[${a}]`)}return s.PlainDateTime.from(e.toISOString().replace(`Z`,``))}function J(e,t=``,n={}){let{integersAsBigInt:r=`asNeeded`,temporal:i=!1}=n,a=at(),o=new Set,s=new Set,c=new Set,l=new Set,u=new Set,d=a,f=[],p={tables:o,table_arrays:s,defined:c,implicit_tables:l,inline_tables:u};for(let t of e)switch(t.type){case`Table`:m(t);for(let e of t.items)e.type===`KeyValue`&&g(e);break;case`TableArray`:h(t);for(let e of t.items)e.type===`KeyValue`&&g(e);break;case`KeyValue`:g(t)}return a;function m(e){let n=e.key.item.value;try{hi(a,[],n,e.type,p)}catch(n){let r=n;throw new D(t,e.key.loc.start,r.message)}let r=xi(n);o.add(r),c.add(r),d=gi(a,n),f=n}function h(e){let n=e.key.item.value;try{hi(a,[],n,e.type,p)}catch(n){let r=n;throw new D(t,e.key.loc.start,r.message)}let r=xi(n);s.add(r),c.add(r),d=_i(a,n),f=n}function g(e){let n=e.key.value;try{hi(d,f,n,e.type,p)}catch(n){let r=n;throw new D(t,e.key.loc.start,r.message)}if(n.length>1)for(let e=1;e1?gi(d,n.slice(0,-1)):d;o[P(n)]=a,c.add(xi(f.concat(n)))}}function mi(e,t={}){let{integersAsBigInt:n=`asNeeded`,temporal:r=!1}=t;switch(e.type){case`InlineTable`:let t=at(),i=new Set,a=new Map;return e.items.forEach(({item:e})=>{let o=e.key.value,s=mi(e.value,{integersAsBigInt:n,temporal:r}),c=bi(o),l=xi(o);if(i.has(l))throw Error(`Duplicate key "${c}" in inline table`);for(let e=1;e1?gi(t,o.slice(0,-1)):t;u[P(o)]=s}),t;case`InlineArray`:return e.items.map(e=>mi(e.item,{integersAsBigInt:n,temporal:r}));case`DateTime`:if(r){if(globalThis.Temporal===void 0)throw Error(`Temporal API is not available in this runtime. Set temporal: false or use a runtime with Temporal support.`);return pi(e.value)}return e.value;case`String`:return e.value;case`Integer`:return di(e.raw,n);case`Float`:case`Boolean`:return e.value;default:throw Error(`Unrecognized value type "${e.type}"`)}}function hi(e,t,n,r,i){let a=t.length>0?[...t]:[],o=Array(n.length),s=Array(n.length);for(let e=0;e1)for(let e=1;e1)for(let e=1;e1)for(let e=1;e<=n.length;e++){let t=o[e-1];if(i.tables.has(s[e-1]))throw Error(`Cannot extend explicit table ${t} with dotted keys`)}let u=0;for(let t of n){if(!bt(e,t))return;if(yi(e[t]))throw Error(`Value already defined for ${o[u]}`);let r=o[u];if(Array.isArray(e[t])&&!i.table_arrays.has(s[u]))throw Error(`Cannot add to static array at ${r}`);let a=u++(e[t]||(e[t]=at()),Array.isArray(e[t])?P(e[t]):e[t]),e)}function yi(e){return typeof e!=`object`&&!dt(e)}function bi(e){return e.join(`.`)}function xi(e){return JSON.stringify(e)}function Si(e){return e.type===`Add`}function Ci(e){return e.type===`Edit`}function Y(e){return e.type===`Remove`}function wi(e){return e.type===`Move`}function Ti(e){return e.type===`Rename`}function Ei(e,t,n=[],r={}){if(e===t||xt(e,t))return[];if(typeof e==`number`&&typeof t==`number`&&Number.isNaN(e)&&Number.isNaN(t)){let n=new Float64Array([e]),r=new Float64Array([t]),i=new DataView(n.buffer),a=new DataView(r.buffer);if((i.getUint32(4,!0)&2147483648)==(a.getUint32(4,!0)&2147483648))return[]}return Array.isArray(e)&&Array.isArray(t)?ji(e,t,n,r):F(e)&&F(t)?Di(e,t,n,r):[{type:`Edit`,path:n}]}function Di(e,t,n=[],r={}){let i=[],a=Object.keys(e),o=a.map(t=>St(e[t])),s=Object.keys(t),c=s.map(e=>St(t[e])),l=new Map(s.map((e,t)=>[e,c[t]])),u=new Set(a),d=new Set(s),f=(e,t,n)=>{let r=new Map;return e.forEach((e,i)=>{if(n.has(e))return;let a=r.get(t[i]);a?a.push(e):r.set(t[i],[e])}),r},p=f(a,o,d),m=f(s,c,u),h=new Map,g=new Set,_=new Map;for(let[e,t]of p){let n=m.get(e);if(!n)continue;let r=Math.min(t.length,n.length);for(let e=0;e{let c=n.concat(a);if(d.has(a)){let n=e[a],u=t[a],d=typeof n==`number`&&!Number.isFinite(n)||typeof u==`number`&&!Number.isFinite(u);(r.updateOrder||d||o[s]!==l.get(a))&&Ct(i,Ei(e[a],t[a],c,r))}else h.has(a)?i.push({type:`Rename`,path:n,from:a,to:h.get(a)}):i.push({type:`Remove`,path:c})}),s.forEach(e=>{if(!u.has(e)&&!g.has(e)){let t={type:`Add`,path:n.concat(e)};if(r.orderSource!==void 0){let i=r.orderSource;for(let e of n)i=i==null?void 0:i[e];if(F(i)){let n=Object.keys(i),r=n.indexOf(e);for(let e=r+1;et.has(e))}}let y=i.filter(e=>Si(e)&&e.path.length===n.length+1||Ti(e)&&e.path.length===n.length);if(r.orderSource!==void 0&&y.length>1){let e=[...y].sort((e,t)=>{let n=Si(e)?e.path[e.path.length-1]:Ti(e)?e.to:``,r=Si(t)?t.path[t.path.length-1]:Ti(t)?t.to:``;return v.indexOf(n)-v.indexOf(r)}),t=0;i=i.map(n=>y.includes(n)?e[t++]:n)}if(r.updateOrder){let e=[];for(let t of a)if(d.has(t))e.includes(t)||e.push(t);else if(h.has(t)){let n=h.get(t);e.includes(n)||e.push(n)}for(let t of s)e.includes(t)||e.push(t);s.forEach((t,r)=>{if(e[r]===t)return;let a=e.indexOf(t);i.push({type:`Move`,path:n,from:a,to:r,key:t}),e.splice(a,1),e.splice(r,0,t)})}return i}function Oi(e){if(typeof e==`string`)return e.includes(` +`);if(Array.isArray(e))return e.length>0;if(typeof e==`object`&&e&&!(e instanceof Date)){let t=Object.values(e);return t.some(e=>typeof e==`object`&&!!e)||t.some(Oi)}return!1}function ki(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&!(e instanceof Date)}function Ai(e){return e.length>4&&e.some(e=>typeof e==`string`)&&e.some(ki)}function ji(e,t,n=[],r={}){let i=[],a=e.map(St),o=t.map(St),s=e.some(Oi)||t.some(Oi)||Ai(e)||Ai(t),c=e.slice(),l=0,u=!1,d=e=>{let t={type:`Remove`,path:n.concat(e)};return(!u||l>0)&&Object.defineProperty(t,"coordinate",{value:`source`}),t};for(let e=0;e=a.length;if(!p&&a[e]===f)continue;let m=p?-1:a.indexOf(f,e+1);if(m>-1){let r=a[e],p=0;for(let t=e;th;if(s&&(o.indexOf(a[e])===-1||g)){i.push(d(e+l)),a.splice(e,1),c.splice(e,1),l++,e--;continue}if(l>0&&g){i.push(d(e+l)),a.splice(e,1),c.splice(e,1),l++,e--;continue}if(s&&l>0){i.push({type:`Add`,path:n.concat(e)}),u=!0,a.splice(e,0,f),c.splice(e,0,t[e]),l--;continue}i.push({type:`Move`,path:n,from:m,to:e});let _=a.splice(m,1);a.splice(e,0,..._);let v=c.splice(m,1);c.splice(e,0,...v);continue}let h=!1;if(!p){let t=a[e],n=0;for(let r=e;rr}if(!p&&h){Ct(i,Ei(c[e],t[e],n.concat(e),r)),a[e]=f,c[e]=t[e];continue}i.push({type:`Add`,path:n.concat(e)}),u=!0,a.splice(e,0,f),c.splice(e,0,t[e]),l>0&&l--}for(let e=o.length;e{try{let f=[];if(a(l))f=l.key.value;else if(t(l))f=l.key.item.value;else if(r(l)){f=l.key.item.value;let e=St(f);o[e]||(o[e]=0);let t=o[e]++;f=f.concat(t)}else u(l)&&a(l.item)?f=l.item.key.value:u(l)&&(f=[d]);if(f.length&&I(f,i.slice(0,f.length))){let t=i.slice(f.length);if(e(n)&&r(l)&&t.length>0){let e=l.key.item.value,i=n,a=i.items.findIndex((t,n)=>n>d&&r(t)&&I(t.key.item.value,e)),o=a===-1?i.items.length:a;s=Mi(l,t,e,i.items.slice(d+1,o))}else s=u(l)&&a(l.item)?i.length===f.length?l:X(l.item.value,t):u(l)&&i.length>f.length?X(l.item,t):X(l,t);return!0}return(a(l)||u(l)&&a(l.item))&&f.length>i.length&&I(f.slice(0,i.length),i)&&(c||=l),!1}catch{return!1}}),!s&&c&&(s=c),!s)throw Error(`Node not found at ${i.join(`.`)}`);return s}function Mi(e,n,i,a){try{return X(e,n)}catch{}let o={};for(let e of a){let a=[];if(t(e)){let t=e.key.item.value;t.length>i.length&&I(t.slice(0,i.length),i)&&(a=t.slice(i.length))}else if(r(e)){let t=e.key.item.value;if(t.length>i.length&&I(t.slice(0,i.length),i)){let e=t.slice(i.length),n=St(e);o[n]||(o[n]=0);let r=o[n]++;a=e.concat(r)}}if(a.length&&I(a,n.slice(0,a.length)))try{return X(e,n.slice(a.length))}catch{}}throw Error(`Node not found at (aot scope) ${n.join(`.`)}`)}function Z(e,t){try{return X(e,t)}catch{}}function Ni(e,t){let n=t,r;for(;n.length&&!r;)n=n.slice(0,-1),r=Z(e,n);if(!r)throw Error(`Parent not found for ${t.join(`.`)}`);return r}const Pi=String.raw`(?:[\w-]+|"[^"]*"|'[^']*')`,Fi=new RegExp(String.raw`^#\s*${Pi}(?:\s*\.\s*${Pi})*\s*=`),Ii=String.raw`(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|"""[\s\S]*?"""|'''[\s\S]*?'''|[^\s#]+)`,Li=new RegExp(String.raw`^#\s*${Pi}(?:\s*\.\s*${Pi})*\s*=\s*${Ii}\s*$`),Ri=new RegExp(String.raw`^#\s*\[\[?\s*${Pi}(?:\s*\.\s*${Pi})*\s*\]\]?\s*$`);function zi(e){return Li.test(e.raw)||Ri.test(e.raw)}function Bi(e){let t=e.raw.indexOf(`=`);if(t<0)return!1;let n=!1,r=!1;for(let i=t+1;i=0&&(n=n.substring(0,r)),(n.startsWith(`"`)&&n.endsWith(`"`)||n.startsWith(`'`)&&n.endsWith(`'`))&&(n=n.slice(1,-1)),n||void 0}function Ui(e){return(d(e)||l(e))&&e.loc.end.line>e.loc.start.line}function Wi(e){if(a(e))return e.key.value[0];if(t(e)||r(e))return e.key.item.value[0];if(u(e)&&a(e.item))return e.item.key.value[0]}function Gi(e,t,n,r=Wi){let i=[],a=t,o,s=[],c=()=>{s.length&&(i.push({kind:`pinned`,items:s,startLine:s[0].loc.start.line,endLine:P(s).loc.end.line}),s=[])};for(let t of e){if(f(t)){if(t.loc.start.line<=a){o?(o.items.push(t),o.endLine=Math.max(o.endLine,t.loc.end.line)):i.push({kind:`pinned`,items:[t],startLine:t.loc.start.line,endLine:t.loc.end.line});continue}let e=s.length?P(s).loc.end.line:void 0;s.length&&t.loc.start.line===e+1||c(),s.push(t);continue}let e=[];if(s.length){if(P(s).loc.end.line+1===t.loc.start.line&&n(t)){let n=r(t);if(n!==void 0){let e=-1;for(let t=s.length-1;t>=0;t--){let r=Vi(s[t])?Hi(s[t]):void 0;if(r!==void 0&&r!==n){e=t;break}}if(e>=0){let t=s.splice(0,e+1);i.push({kind:`pinned`,items:t,startLine:t[0].loc.start.line,endLine:P(t).loc.end.line})}}e=s,s=[]}else c()}let l=[...e,t],u={kind:`member`,member:t,key:r(t),items:l,startLine:l[0].loc.start.line,endLine:t.loc.end.line};i.push(u),o=u,a=t.loc.end.line}return c(),i}function Ki(t,n=()=>!0,r=Wi){let i=e(t)?0:t.key.loc.end.line;return Gi(t.items,i,n,r)}function qi(e,t){let n=t.filter(t=>f(t)&&(t.loc.start.line>e.loc.start.line||t.loc.start.line===e.loc.start.line&&t.loc.start.column>=e.loc.start.column)&&(t.loc.end.linee.loc.start.line-t.loc.start.line||e.loc.start.column-t.loc.start.column),e.loc.start.line,()=>!0)}function Ji(e,t){function n(e){if(a(e))return n(e.value);if(p(e)){for(let n of e.items)if(u(n)&&n.item===t)return n;for(let t of e.items){let e=n(u(t)?t.item:t);if(e)return e}}}return n(e)}function Yi(e,n){function i(e,t){if(e===n)return t;if(a(e))return i(e.value,t);if(d(e)||l(e))for(let r of e.items){if(r===n)return t;let e=i(r.item,t);if(e)return e}}function o(e){for(let s of e.items){if(s===n)return e;if(a(s)){let t=i(s.value,e);if(t)return t}else if(t(s)||r(s)){let e=o(s);if(e)return e}}}return o(e)}function Xi(e){for(let n=0;n0?i.items[e-1]:void 0;if(o&&(t(o)||r(o))){let e=Zi(o,a);if(e)for(let t of e)H(n,o,t)}}if(e(i)||t(i)||r(i)){let e=Ki(i).find(e=>e.member===a);if(e){let t=e.items.indexOf(a);for(let r=0;r<=t;r++){let t=e.items[r];i.items.includes(t)&&H(n,i,t)}for(let n=t+1;ne.member===a);if(t){let r=t.items.filter(e=>e!==a);for(let t of r){let n=e.items.indexOf(t);n<0||e.items.splice(n,1)}r.length&&(a.loc.start=t.items[0].loc.start,a.loc.end=P(t.items).loc.end),H(n,i,a,e.items),U(n);return}}}H(n,i,a)}function Qi(t,n,r,i){let s=!1;if(Ui(n)&&e(t)){let e=Yi(t,n);if(e){let c=qi(n,e.items);{let e=n.items,r=1;for(let n=1;n=3){s=!0;break}}else r=1}let f=[],m,h,g,_=[],v=(e,t)=>{if(!p(e))return;let n=e.items;for(let e=0;et;if(i&&_.push({node:r,start:w(r.loc.start),end:w(r.loc.end)}),r.loc.end.line>t&&r.loc.end.line>r.loc.start.line&&_.push({node:r,start:w(r.loc.start),end:w(r.loc.end),endOnly:!0}),u(r)){let e=r.item;if(i){let t=a(e)?e:void 0;_.push({node:e,start:w(e.loc.start),end:w(e.loc.end),equals:t?t.equals:void 0}),t&&(_.push({node:t.key,start:w(t.key.loc.start),end:w(t.key.loc.end)}),_.push({node:t.value,start:w(t.value.loc.start),end:w(t.value.loc.end)}))}e.loc.end.line>t&&e.loc.end.line>e.loc.start.line&&_.push({node:e,start:w(e.loc.start),end:w(e.loc.end),endOnly:!0}),v(e,t)}else p(r)&&v(r,t)}};for(let t of c){if(t.kind!==`member`||!t.member)continue;let n=t.items.filter(e=>e!==t.member);if(t.member===r&&(m=w(r.loc.start),h=w(r.loc.end),u(r)&&(g=w(r.item.loc.end)),v(u(r)?r.item:r,m.line),n.length&&(r.loc.start=w(t.items[0].loc.start),r.loc.end=w(P(t.items).loc.end))),n.length){f.push({owner:t.member,ownerOriginalStart:w(t.member.loc.start),comments:n});for(let t of n){let n=e.items.indexOf(t);n>=0&&e.items.splice(n,1)}}}let y=n.items.indexOf(r),b=y===n.items.length-1,x=y>0?n.items[y-1]:void 0,S=i===0&&y>0&&r.loc.end.line>r.loc.start.line&&x!==void 0&&x.loc.start.line===r.loc.start.line;H(t,n,r,e.items);let C=!1;if(b&&x!==void 0&&r.loc.end.line>r.loc.start.line&&!nr(n)&&B(t).get(x)&&(B(t).delete(x),C=!0),U(t),V(t,n,r,i,void 0,e.items),C){let e=B(t).get(r);e&&(e.lines=0,e.columns=0)}U(t),m&&h&&r.loc.end.line>r.loc.start.line&&(r.loc.end.column=h.column,u(r)&&g&&r.item.loc.end.line>r.item.loc.start.line&&(r.item.loc.end.column=g.column));for(let{node:e,start:t,end:n,endOnly:r,equals:i}of _){if(r){e.loc.end.column=n.column;continue}e.loc.start.column=t.column,e.loc.end.line===e.loc.start.line&&(e.loc.end.column=n.column),i!==void 0&&a(e)&&(e.equals=i)}for(let{owner:t,ownerOriginalStart:n,comments:i}of f){let a;t===r?(a={lines:r.loc.start.line-n.line,columns:r.loc.start.column-n.column},r.loc.start={line:m.line+a.lines,column:m.column+a.columns},r.loc.end={line:h.line+a.lines,column:h.column+a.columns}):a={lines:t.loc.start.line-n.line,columns:t.loc.start.column-n.column};for(let t of i){W(t,a);let n=e.items.findIndex(e=>e.loc.start.line>t.loc.start.line);n===-1?e.items.push(t):e.items.splice(n,0,t)}}let T=n;if(s||S){let e=(t,n)=>{let r;for(let n of t.items){if(!r){r={line:n.loc.end.line,column:n.loc.end.column+2};continue}let t={lines:r.line-n.loc.start.line,columns:r.column-n.loc.start.column};if(t.lines!==0||t.columns!==0){W(n,t),n.loc.end.line>n.loc.start.line&&(n.loc.end.column-=t.columns,u(n)&&n.item.loc.end.line>n.item.loc.start.line&&(n.item.loc.end.column-=t.columns));let r=u(n)?a(n.item)?n.item.value:n.item:n;(d(r)||l(r))&&r.loc.end.line>r.loc.start.line&&e(r,n)}r={line:n.loc.end.line,column:n.loc.end.column+2}}r&&(t.loc.end={line:r.line,column:r.column-1},n&&n.loc.end.line===t.loc.end.line&&(n.loc.end.column=t.loc.end.column))},n;for(let t of T.items){if(t===r){n={line:r.loc.end.line,column:r.loc.end.column+2};continue}if(!n)continue;let i={lines:n.line-t.loc.start.line,columns:n.column-t.loc.start.column};if(i.lines!==0||i.columns!==0){W(t,i);let n=u(t)?a(t.item)?t.item.value:t.item:t;(d(n)||l(n))&&n.loc.end.line>n.loc.start.line&&e(n,u(t)?t:void 0)}let s=t.loc.end.line,c=u(t)?t.item:t;o(c)&&c.loc.end.line>c.loc.start.line&&(s=c.loc.start.line+c.raw.split(` +`).length-1,c.loc.end.line=s,t.loc.end.line=s),n={line:s,column:t.loc.end.column+2}}if(n){let e=w(T.loc.end);T.loc.end={line:n.line,column:n.column-1};let r=Ji(t,T);r&&(r.loc.end={line:T.loc.end.line,column:T.loc.end.column});let i={lines:T.loc.end.line-e.line,columns:T.loc.end.column-e.column};(i.lines!==0||i.columns!==0)&&Bn(t,r??T,i)}}return}}let c=n,f=c.items,m=f[0],h=f[f.length-1],g=m.loc.start.column-c.loc.start.column-1,_=c.loc.end.column-1-h.loc.end.column;H(t,n,r),V(t,n,r,i),U(t);let v=c.items,y=v[0],b=y.loc.start.column-c.loc.start.column-1;b!==g&&W(y,{lines:0,columns:g-b});let x=v[v.length-1];c.loc.end.column-1-x.loc.end.column!==_&&(c.loc.end.column=x.loc.end.column+1+_)}function $i(e){let n=[];for(let i of e){if(i.kind===`pinned`){n.push({kind:`pinned`,isSection:!1,movable:!1,groups:[i],startLine:i.startLine,endLine:i.endLine,items:i.items});continue}let e=t(i.member)||r(i.member),a=n[n.length-1];if(a&&a.kind===`member`&&i.key!==void 0&&a.key===i.key){a.groups.push(i),a.endLine=i.endLine,a.items=a.items.concat(i.items);continue}n.push({kind:`member`,key:i.key,isSection:e,movable:i.key!==void 0,groups:[i],startLine:i.startLine,endLine:i.endLine,items:[...i.items]})}let i=new Map;for(let e of n)e.kind===`member`&&e.key!==void 0&&i.set(e.key,(i.get(e.key)??0)+1);for(let e of n)e.kind===`member`&&e.key!==void 0&&i.get(e.key)>1&&(e.movable=!1);return n}function ea(e,t){let n=[...e];for(let e of t){if(e.key===void 0)continue;let t=n.indexOf(e.key);if(t===-1)continue;let r=Math.max(0,Math.min(e.to,n.length-1));t!==r&&(n.splice(t,1),n.splice(r,0,e.key))}return n}function ta(e){return[...e.path,e.key].join(`.`)}function na(e,n,i,o){if(n.some(e=>typeof e!=`string`))return;let s=[...n,i],c=[{container:e,prefix:[]}];for(let n of e.items)(t(n)||r(n))&&c.push({container:n,prefix:n.key.item.value});for(let e of c)for(let t of e.container.items){if(!a(t)||t.key.value.length<2||o.has(t))continue;let n=[...e.prefix,...t.key.value];if(n.length===s.length&&n.every((e,t)=>e===s[t]))return{container:e.container,dottedPrefix:t.key.value.slice(0,-1)}}}function ra(n,i,o,s){if(i.length===0)return{container:n};let c;try{c=Z(n,i)}catch{return}if(!c)return;if(a(c)&&(c=c.value),c&&m(c)&&(c=c.item),c&&(e(c)||t(c)||r(c)))return{container:c};if(o===void 0)return;let l=na(n,i,o,s);if(l)return l;let u=-1;for(let e=i.length-1;e>=0;e--)if(typeof i[e]==`number`){u=e;break}if(u<0)return;let d=i.slice(u+1);if(d.length===0||d.some(e=>typeof e!=`string`))return;let f=Z(n,i.slice(0,u+1));if(!f||!r(f))return;let p=[...d,o];return f.items.some(e=>a(e)&&e.key.value.length===p.length&&e.key.value.every((e,t)=>e===p[t]))?{container:f,dottedPrefix:d}:void 0}function ia(e,t,n){if(t.key===void 0)return!1;let r;try{r=Z(e,t.path.concat(t.key))}catch{return!1}return r!==void 0&&u(r)&&!n.has(r)}function aa(t,n,r,i,o){let s=$i(Ki(t,e=>r.has(e),o?e=>{if(!a(e))return;let t=e.key.value;if(t.length===o.length+1){for(let e=0;ee.kind===`member`&&e.key!==void 0).map(e=>e.key));for(let e of n)e.key!==void 0&&l.has(e.key)&&!c.has(e.key)&&i.push(`"${ta(e)}" -- its entries are not contiguous in the document (e.g. the TOML spec's own "valid but discouraged" out-of-order table pattern), so reordering was skipped for it`);if(c.size===0)return;let u=o?s.filter(e=>e.kind===`member`&&e.movable).map(e=>e.key):s.filter(e=>e.kind===`member`).map((e,t)=>e.movable?e.key:`\u0000anchor:${t}`),d=n.filter(e=>e.key!==void 0&&c.has(e.key));if(d.length===0)return;let f=ea(u,d).filter(e=>c.has(e)),p=f.filter(e=>!c.get(e).isSection),m=f.filter(e=>c.get(e).isSection),h=!1;for(let e of f)if(c.get(e).isSection)h=!0;else if(h){i.push(`the requested order asked for a root key-value to be positioned after a section header, which TOML cannot represent -- each group (root key-values, section blocks) was reordered to match its own requested relative order instead`);break}let g=0,_=0,v=s.map(e=>{if(e.kind!==`member`||!e.movable)return e;let t=e.isSection?m[_++]:p[g++];return c.get(t)});if(v.every((e,t)=>e===s[t]))return;let y=e(t)?1:t.key.loc.end.line,b=s.map((e,t)=>t===0?e.startLine-y:e.startLine-s[t-1].endLine-1),x=y;for(let e=0;ee.items),C=t.items;C.splice(0,C.length,...S),$n(t)}function oa(e,t,n){if(t.length===0)return;Xi(e);let r=[],i=new Map;for(let o of t){var a;if(o.key===void 0)continue;let t=ra(e,o.path,o.key,n);if(!t){if(ia(e,o,n))continue;r.push(`"${ta(o)}" -- unsupported location for reordering (e.g. a dotted-key implicit table, or an interior of an inline table/array-of-tables entry)`);continue}let s=((a=t.dottedPrefix)==null?void 0:a.join(`\0`))??``,c=i.get(t.container);c||(c=new Map,i.set(t.container,c));let l=c.get(s);l?l.moves.push(o):c.set(s,{...t,moves:[o]})}for(let e of i.values())for(let t of e.values())aa(t.container,t.moves,n,r,t.dottedPrefix);if(r.length>0){let e=[...new Set(r)];console.warn(`toml-patch: updateOrder could not honor the requested position for ${e.length===1?`1 entry`:`${e.length} entries`}, left unchanged:\n`+e.map(e=>` - ${e}`).join(` +`))}$n(e)}const sa=new WeakMap,ca=new WeakMap;function la(e,t){let n=e[t].loc.end.line;for(let r=t+1;rn)return}}function ua(e,t,n){for(let r=t+n;r>=0&&r0,a=n{(e>n||e===n&&t>i)&&(n=e,i=t)};(t(e)||r(e))&&a(e.key.loc.end.line,e.key.loc.end.column);for(let t of e.items)a(t.loc.end.line,t.loc.end.column);e.loc.end.line=n,e.loc.end.column=i}function ba(e,t,n){let r=n.newLine,i=t.match(/(?:\r\n|\n)+$/u),a=i?i[0]:``,o=a?t.slice(0,-a.length):t,s=o.length?o.split(r):[],c=va(e),l=(e,t)=>{let n=e.lastIndexOf(t.raw);return n>=0?n:t.loc.start.column},u=_a(e).filter(e=>{let t=s[e.loc.start.line-1]||``,n=l(t,e),r=t.slice(0,n);return r.trim().length>0&&/\s{2,}$/u.test(r)}).sort((e,t)=>e.loc.start.line-t.loc.start.line),d=[],f=[];for(let e of u){let t=f[f.length-1];!t||e.loc.start.line===t.loc.start.line+1?f.push(e):(f.length>0&&d.push(f),f=[e])}f.length>0&&d.push(f);for(let e of d){if(e.length<2)continue;let t=ca.get(e[0])??e[0].loc.start.column,n=new Map;for(let t of e){let e=ca.get(t)??t.loc.start.column;n.set(e,(n.get(e)||0)+1)}let r=1;for(let[e,i]of n)i>r&&(r=i,t=e);let i=Math.max(...e.map(e=>{let t=s[e.loc.start.line-1]||``,n=l(t,e);return t.slice(0,n).trimEnd().length})),a=Math.min(...e.map(e=>{let t=s[e.loc.start.line-1]||``,n=l(t,e),r=t.slice(0,n).trimEnd();return Math.max(1,n-r.length)})),o=e.filter(e=>sa.has(e)),u=e.some(e=>!c.has(e.loc.start.line));if(o.length>0&&u){let t=Math.min(...e.map(e=>{if(sa.has(e))return sa.get(e)||1;let t=s[e.loc.start.line-1]||``,n=l(t,e),r=t.slice(0,n).trimEnd();return Math.max(1,n-r.length)}));a=Math.min(a,t)}if(to.leadingBom?`${e}`:e)(Da(a,t,o,n instanceof R||n!=null&&Object.prototype.hasOwnProperty.call(n,`indentWidth`),n instanceof R||n!=null&&Object.prototype.hasOwnProperty.call(n,`trailingComma`)).tomlString)}function Sa(e,t,n){let r=e.slice(n),i=0,a=Math.min(r.length,t.length);for(let e=1;e<=a;e++)I(r.slice(r.length-e),t.slice(0,e))&&(i=e);return i}function Ca(e,t,n){if(n===void 0)return;let r=t.key.value;if(r.length<2)return;let i=r.slice(0,-1);for(let t=0;t{t.add(e)};return Dt(e,{Document:n,Table:n,TableKey:n,TableArray:n,TableArrayKey:n,KeyValue:n,Key:n,String:n,Integer:n,Float:n,Boolean:n,DateTime:n,InlineArray:n,InlineItem:n,InlineTable:n,Comment:n}),t}function Ta(e,t,n,r){if(!F(t))return t;let i=J(q(e,L({...n,inlineTableStart:0},n)).items,``,{temporal:r});if(!F(i))return t;let a={};for(let e of Object.keys(i))Object.prototype.hasOwnProperty.call(t,e)&&(a[e]=t[e]);for(let e of Object.keys(t))Object.prototype.hasOwnProperty.call(a,e)||(a[e]=t[e]);return a}function Ea(e){let n=e.items;for(let e=0;e0&&f(a[o-1]);)o--;if(os||t.line===s&&t.column>c)&&(s=t.line,c=t.column)}let l=J(a,``,{temporal:o}),u={type:`Document`,loc:{start:{line:1,column:0},end:{line:s,column:c}},items:a};Mn(u,n.indentWidth);let d=n.inlineTableStart!=null&&n.inlineTableStart>1?1:n.inlineTableStart,f=L({...n,inlineTableStart:d},n),p=q(t,f),m=J(p.items,``,{temporal:o}),h=n.updateOrder?Ta(t,m,f,o):m;if(!n.updateOrder&&Oa(l,h))return{tomlString:ai(a,n),document:u};let g=ja(Ma(u,h,Ei(l,h,[],{updateOrder:n.updateOrder,orderSource:t})));if(g.length===0)return{tomlString:ai(a,n),document:u};let _=za(u,p,g,n,o,wa(u),t,r,i);return{tomlString:ba(_,oi(_.items,n),n),document:_}}function Oa(e,t){let n=[[e,t]];for(;n.length>0;){let[e,t]=n.pop();if(typeof e==`number`&&typeof t==`number`&&(!Number.isFinite(e)||!Number.isFinite(t))||typeof e==`number`&&typeof t==`number`&&Number.isNaN(e)&&Number.isNaN(t))return!1;if(Object.is(e,t))continue;if(e instanceof Date&&t instanceof Date){if(e.getTime()!==t.getTime())return!1;continue}if(!e||!t||typeof e!=`object`||typeof t!=`object`||Array.isArray(e)!==Array.isArray(t))return!1;if(!Array.isArray(e)){let n=Object.getPrototypeOf(e),r=Object.getPrototypeOf(t);if(n!==Object.prototype&&n!==null||r!==Object.prototype&&r!==null)return!1}let r=Object.keys(e),i=Object.keys(t);if(r.length!==i.length)return!1;for(let i of r){if(!Object.prototype.hasOwnProperty.call(t,i))return!1;n.push([e[i],t[i]])}}return!0}function ka(e,t=new WeakSet){return typeof e!=`object`||!e?!1:pt(e)?!0:!t.has(e)&&(t.add(e),Object.values(e).some(e=>ka(e,t)))}function Aa(e){if(Array.isArray(e)){let t=!1,n=[];for(let r=0;rr){e.splice(a,1),e.splice(t,0,o),t=-1;break}a++}}}return e}function Ma(e,t,n){let i=new Set,o=[],s=new Map;for(let e of n){if(!Y(e)&&!Si(e))continue;let t=e.path.slice(0,-1);if(t.length===0)continue;let n=JSON.stringify(t),r=s.get(n);r||(r={path:t,removes:[],adds:[]},s.set(n,r)),(Y(e)?r.removes:r.adds).push(e)}for(let t of s.values())t.removes.length!==0&&t.adds.length!==0&&(Z(e,t.path)||(t.removes.forEach(e=>i.add(e)),t.adds.forEach(e=>i.add(e)),o.push({type:`Edit`,path:t.path})));for(let t of n){if(i.has(t)||!Y(t)&&!Si(t))continue;let r=t.path.slice(0,-1);if(r.length===0||typeof P(r)!=`number`)continue;let s=Z(e,r);if(!s||!u(s)||!d(s.item))continue;let c=n.filter(e=>!i.has(e)&&(Y(e)||Si(e))&&I(e.path.slice(0,-1),r));if(c.length===0||c.some(e=>e.path.length!==r.length+1))continue;let l=new Set(s.item.items.filter(u).map(e=>{let t=a(e.item)?e.item:void 0;return(t==null?void 0:t.key.value.length)===1?t.key.value[0]:void 0}).filter(e=>e!==void 0)),f=new Set(c.filter(Y).map(e=>P(e.path)));if(c.some(Y)&&c.some(Si)&&l.size===f.size&&![...l].some(e=>!f.has(e))){for(let e of c)i.add(e);o.push({type:`Edit`,path:r})}}let c=new Map;for(let t of n){if(i.has(t)||!Ci(t)&&!Si(t)&&!Y(t)||typeof P(t.path)!=`number`)continue;let n=t.path.slice(0,-1);if(n.length===0)continue;let a=Z(e,n.concat(0));a&&r(a)&&c.set(JSON.stringify(n),n)}for(let e of c.values()){let r=t;for(let t of e)r=r==null?void 0:r[t];if(!Array.isArray(r)||r.every(e=>F(e)))continue;let a=n.filter(t=>!i.has(t)&&(Ci(t)||Si(t)||Y(t))&&t.path.length>e.length&&I(t.path.slice(0,e.length),e));if(!(a.length<2)){for(let e of a)i.add(e);o.push({type:`Edit`,path:e})}}if(i.size===0)return n;let l=n.filter(e=>!i.has(e));return l.push(...o),l}function Na(e,t){return t.map(t=>k.test(t)?t:`"${dr(t,e,`singleline-basic`)}"`).join(`.`)}function Pa(e){let t=[],n,r=!1;for(let i=0;i0&&(e[o-1]===` `||e[o-1]===` `);)o--;let s=i+1;for(;sr===0?t:`${e}${n[Math.min(r-1,n.length-1)]}${t}`,``),e.loc.end.column=e.loc.start.column+e.raw.length)}function La(e,t){let n;return Dt(e,{KeyValue:e=>{!n&&e.key.value.length>t.length&&I(e.key.value.slice(0,t.length),t)&&Pa(e.key.raw).length>0&&(n=e)}}),n}function Ra(e,t){if(o(e)&&o(t)){let n=br(t.value,e.raw);t.raw=n.raw,t.loc=n.loc}if(c(e)&&c(t)){let n=e.raw,r=t.value;if(pt(r)){let e=mt(r);if(n.includes(` `)&&e.includes(`T`)&&(e=e.replace(`T`,` `)),/(?:\+00:00|-00:00)/.test(n)&&e.endsWith(`Z`)){let t=n.match(/([+-]00:00)/)[1];e=e.replace(/Z$/,t)}t.raw=e,t.loc.end.column=t.loc.start.column+t.raw.length}else if(!(he.IS_TIME_ONLY.test(n)&&r instanceof Date&&r.getUTCFullYear()>1)){let e=he.createDateWithOriginalFormat(r,n);t.value=e,t.raw=e.toISOString(),t.loc.end.column=t.loc.start.column+t.raw.length}}if(s(e)&&s(t)&&Number.isNaN(e.value)&&Number.isNaN(t.value)){let n=e,r=t;n.nanSign&&(r.nanSign=`+`,r.raw=`+nan`)}if(l(e)&&l(t)){let n=Pr(e);if(t.items.length>0){let e=t.items[t.items.length-1];e.comma!==n&&(e.comma=n,n&&(t.loc.end.column+=1))}}if(d(e)&&d(t)){let n=Fr(e);if(t.items.length>0){let e=t.items[t.items.length-1];e.comma!==n&&(e.comma=n,n&&(t.loc.end.column+=1))}}}function za(n,i,s,c,h=!1,g=new WeakSet,_=void 0,v=!1,y=!0){let x=new Map,S=new Set;function C(e){let t=n.items,r=t.indexOf(e);if(r>0){let n=t[r-1];f(n)&&n.loc.end.line+1===e.loc.start.line&&S.add(e)}}let w=new WeakMap;function T(e){let i=w.get(e);if(i!==void 0)return i;let o={};function s(n,i){if(n===e)return i;if(a(n))return s(n.value,i);if(u(n))return s(n.item,i);if(p(n))for(let e=0;et.length||!I(n.key.value,t.slice(t.length-r))}function D(e,i){let o=e.key.item.value.concat(i);return e.items.some(e=>a(e)&&e.key.value.length>i.length&&I(e.key.value.slice(0,i.length),i))?!0:n.items.some(e=>{if(!t(e)&&!r(e))return!1;let n=e.key.item.value;return n.length>o.length&&I(n.slice(0,o.length),o)})}function O(e,t){let n=[],r=!1;for(let i of e.items){if(!a(i)||!d(i.value)||!D(t,i.key.value)){n.push(i);continue}let e=[];for(let t of i.value.items){if(!u(t)||!a(t.item))continue;let n=J([t.item],``,{temporal:h});for(let e of t.item.key.value)n=n==null?void 0:n[e];let r=eo(n,c);r!==void 0&&e.push(gr(i.key.value.concat(t.item.key.value),r))}e.length>0?(n.push(...e),r=!0):n.push(i)}if(!r)return e;let i=mr(e.key.item.value);for(let e of n)V(i,i,e);return U(i),i}function ee(e,t){function n(e){if(a(e))return n(e.value);if(u(e))return n(e.item);if(!p(e))return;let r=e.items;for(let n of r)if(n===t||u(n)&&n.item===t)return e;for(let e of r){let t=n(u(e)?e.item:e);if(t)return t}}return n(e)}function A(e,t,r,i){let a=t.concat(e.key.value),o=e.key.raw;e.key.value=a,e.key.raw=a.map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`);let s=e.key.raw.length-o.length;e.key.loc.end.column=e.key.loc.start.column+e.key.raw.length,e.equals+=s,W(e.value,{lines:0,columns:s},{first_line_only:!0}),e.loc.end.line===e.loc.start.line&&(e.loc.end.column+=s),Kn(n,i,r,e),Ua(n,e)}function te(e,t){let n=e.key.item,r=n.raw,i=t.concat(n.value),a=i.map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`),o=a.length-r.length;n.value=i,n.raw=a,n.loc.end.column=n.loc.start.column+a.length,e.key.loc.end.line===e.key.loc.start.line&&(e.key.loc.end.column+=o)}function ne(e,t,r,i){te(e,t),Kn(n,i,r,e)}function j(e,t,r){let i=Z(n,e);if(i&&!E(i,e))return null;let a=e.length;for(;a>0;){let t=Z(n,e.slice(0,a));if(t&&!E(t,e.slice(0,a)))break;a--}let o=e.slice(a);if(o.length===0||!o.every(e=>typeof e==`string`))return null;if(_!==void 0){let e=_;for(let t of r)e=e==null?void 0:e[t];if(e!==void 0){let n=eo(e,c);if(n!==void 0)return gr([...o,...t.key.value],n)}}let s=t.key,l=[...o,...s.value].map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`),u=l.length-s.raw.length;return s.raw=l,s.value=[...o,...s.value],s.loc.end.column=s.loc.start.column+l.length,t.equals+=u,W(t.value,{lines:0,columns:u},{first_line_only:!0}),t.loc.end.line===t.loc.start.line&&(t.loc.end.column+=u),t}let M=new Set,re=new Set,ie=new WeakMap;function ae(e,t){return ie.get(e)??t}let oe=[],se=new Set,ce=[];function le(e,t,r){if(!l(e)&&!d(e)||!u(r)||d(e)&&!a(r.item))return r;let i=_;for(let e of t)i=i==null?void 0:i[e];if(!Array.isArray(i)&&!F(i))return r;let o=En(n,e)??0,s=nr(e)||e.items.length===0&&e.loc.end.line>e.loc.start.line,f=v?c.indentWidth:l(e)&&e.items.length===0?Math.max(c.indentWidth,4):c.indentWidth;l(e)&&e.items.length===0&&!c.useTabsForIndentation&&Nn(e,Math.max(f,4));let p=Array.isArray(i)?`array`:`table`,m=e.items.map(t=>d(e)&&a(t.item)?t.item.value:t.item).find(e=>p===`array`?l(e):d(e)),h=m&&p===`array`?Pr(m):m&&p===`table`?Fr(m):!1,g=m,b=g&&g.items.length>0?g.items[0].loc.start.column-g.loc.start.column>1:void 0,x=mn(p,o+1,s,c),S=p===`array`?`multilineArray`:`multilineTable`,C=L({...c,inlineTableStart:0,[S]:x,trailingComma:y?c.trailingComma:h,bracketSpacing:b??c.bracketSpacing,indentWidth:f},c),w=q({tmp:i},C,o+1,s).items[0];if(!w||!a(w))return r;let T=w.value;if(x&&(l(T)||d(T))){let e=T.loc.start.column;T.loc.start.column=0,T.loc.end.line===T.loc.start.line?T.loc.end.column-=e:mn(l(T)?`array`:`table`,o+1,s,C)&&(T.loc.end.column=1)}if(d(e)){let e=K(gr(r.item.key.value,T));return e.comma=r.comma,e}let E=K(T);return e.items.length===0&&l(e)&&d(T)&&(E.comma=!0),E}function ue(e,t,r){let i=u(t)?t.item:t;if(!l(i)&&!d(i))return t;let o=_;for(let e of r)o=o==null?void 0:o[e];if(!Array.isArray(o)&&!F(o))return t;let s=Dn(n,e),f=s?En(n,s)??0:-1,p=s?nr(s)||s.items.length===0&&s.loc.end.line>s.loc.start.line:!1,m=Array.isArray(o)?`array`:`table`,h=m===`array`?c.multilineArray:c.multilineTable,g=mn(m,f+1,p,c),v=u(e)?e.item:e,b=l(v)||d(v)?v:void 0,x=l(v)?`array`:d(v)?`table`:void 0,S=b!==void 0&&x!==m&&Tn(b)&&h===`auto`;if(!(g||S))return t;let C=m===`array`?`multilineArray`:`multilineTable`,w=L({...c,inlineTableStart:0,[C]:!0,trailingComma:y?c.trailingComma:!1},c),T=q({tmp:o},w,f+1,p).items[0];if(!T||!a(T))return t;let E=T.value;if(l(E)||d(E)){let t=u(e)?e.item.loc.start.column:e.loc.start.column;E.loc.end.line===E.loc.start.line?E.loc.end.column+=t-E.loc.start.column:E.loc.end.column=1,E.loc.start.column=t}if(u(t)){let e=K(E);return e.comma=t.comma,e}return E}s.forEach(s=>{if(Si(s)){var v;let o=X(i,s.path),m=s.path.slice(0,-1),g=P(s.path),_=r(o);if(st(g)&&!_&&!st(P(m))){let e=Z(n,m.concat(0));e&&r(e)&&(_=!0)}if(_&&st(g)&&!st(P(m))){let e=Z(n,m),t=e&&a(e)?e.value:e;if(t&&l(t)){let e=J(i.items,``,{temporal:h});for(let t of s.path)e=e==null?void 0:e[t];let r=L({...c,inlineTableStart:0},c),l=e===void 0?void 0:q({tmp:e},r),f=l==null?void 0:l.items[0];if(f&&a(f)&&(o=K(f.value),_=!1,u(o)&&d(o.item)&&o.item.items.length>0)){let e=o.item;if(!(e.items[0].loc.start.column-e.loc.start.column>1)){let r=t.items.map(e=>e.item).find(e=>d(e)&&e.items.length>0);r&&r.items[0].loc.start.column-r.loc.start.column>1&&er(n,e,!0)}}}}if(_&&!r(o)){let e=m.filter(e=>typeof e==`string`),n=J(i.items,``,{temporal:h});for(let e of s.path)n=n==null?void 0:n[e];if(n!==void 0){let i=mr(e),a=q(n,c);for(let e of a.items)V(i,i,e,void 0);let s=n=>{if(p(n))for(let i of n.items){if(!t(i)&&!r(i))continue;let n=i.key,a=n.item,o=e.concat(a.value);a.value=o,a.raw=yr(o).raw,a.loc.start.column=n.loc.start.column+1,a.loc.end.column=a.loc.start.column+a.raw.length,n.loc.end.column=a.loc.start.column+a.raw.length+1,s(i)}};s(i),U(i),o=i}}let y;if(t(o))y=n;else if(_){y=n;let e=n,i=Z(e,m.concat(g-1)),a=Z(e,m.concat(g));if(a)g=e.items.indexOf(a);else if(i){if(g=e.items.indexOf(i)+1,r(i)){let n=i.key.item.value,a=g-1;for(let i=g;in.length&&I(s.slice(0,n.length),n)&&(a=i)}g=a+1,r(o)&&(o=O(o,i))}}else g=e.items.length}else{y=n;for(let e=s.path.length-1;e>=0;e--){let t=Z(n,s.path.slice(0,e));if(t&&!E(t,s.path.slice(0,e))){y=t;break}}a(y)?y=y.value:u(y)&&a(y.item)?y=y.item.value:(u(y)&&d(y.item)||u(y)&&l(y.item))&&(y=y.item)}if((l(y)||d(y))&&(o=le(y,s.path,o),Zr(y,o,c.indentWidth)),l(y)){let e=Z(n,m),t=Z(n,m.slice(0,-1));if(e&&a(e)&&t){let n=ga(y,o,g);n!==0&&ma(t,e,n)}}let b=(l(y)||d(y))&&y.loc.end.line>y.loc.start.line;b&&re.has(y)&&U(n);let x=l(y)||d(y)?(v=Yi(n,y))==null?void 0:v.items:void 0;if(r(y)||l(y)||e(y)){if(l(y)){let e=ae(y,Pr(y));if(u(o)){let t=y.items.length===0&&d(o.item)&&nr(o.item);o.comma=t||e;let n=o,r=d(n.item)?n.item:void 0,l=r&&r.items.length>0?y.items.map(e=>e.item).find(e=>e!==n.item&&d(e)):void 0;if(r&&l){let e=Fr(l);if(e!==Fr(r)){let t=J(i.items,``,{temporal:h});for(let e of s.path)t=t==null?void 0:t[e];let r=L({...c,inlineTableStart:0,trailingComma:e},c),l=t===void 0?void 0:q({tmp:t},r).items[0];if(l&&a(l)){let e=K(l.value);e.comma=n.comma,o=e}}}}}if(c.inlineTableStart!==void 0&&c.inlineTableStart>0&&e(y)&&t(o)){let e=Qa(o,n,c);V(n,y,o,g);for(let t of e)V(n,n,t,void 0)}else{let i=o;u(o)&&(r(y)||e(y))&&(i=o.item);let c=g;if(e(y)&&a(i)){let e=y.items.findIndex(e=>t(e)||r(e));e!==-1&&(c=e)}let d=!1;if((r(y)||e(y))&&a(i)){let e=j(m,i,s.path);e&&(M.has(y)&&U(n),i=e,d=!0)}if(l(y)&&y.items.length===0&&U(n),c===0&&l(y)&&Wn(n).has(y)&&U(n),c===0&&(e(y)||t(y)||r(y))&&Wn(n).has(y)&&U(n),a(i)&&i.key.value.length>1){let e=La(n,i.key.value.slice(0,-1));if(e){let t=i.equals;Ia(i.key,e.key.raw),i.equals=i.key.loc.end.column+e.equals-e.key.loc.end.column;let n=i.equals-t;W(i.value,{lines:0,columns:n},{first_line_only:!0}),i.loc.end.line===i.loc.start.line&&(i.loc.end.column+=n)}}V(n,y,i,c,void 0,x),d&&M.add(y)}}else if(d(y)){y.items.length===0&&(U(n),Rn(y)&&y.loc.end.line>y.loc.start.line&&(y.loc.end.line=y.loc.start.line));let e=ae(y,Fr(y));if(a(o)){let t=K(o);t.comma=e;let r=Ca(y,o,s.before);V(n,y,t,r,void 0,x)}else if(u(o)&&a(o.item)){let t=o.item,r=j(m,t,s.path);r&&(M.has(y)&&U(n),t=r);let i=K(t);i.comma=e;let a=Ca(y,t,s.before);V(n,y,i,a,void 0,x),r&&M.add(y)}else V(n,y,o,void 0,void 0,x)}else if(c.inlineTableStart!==void 0&&c.inlineTableStart>0&&a(o)&&d(o.value)&&t(y))Gr([...y.key.item.value,...o.key.value])0&&y.items.every(f)?2:void 0;V(n,y,i,void 0,void 0,void 0,l),c&&M.add(y)}b&&re.add(y)}else if(Ci(s)){let f=Z(n,s.path),m;try{m=X(i,s.path)}catch{m=void 0}if(!f||!m){Ga(n,i,s,c,h,g,S);return}if(a(f)&&t(m)){let e=m.key.item.value;if($(n,e).some(e=>e!==f)){Ga(n,i,s,c,h,g,S);return}}if(u(f)&&(t(m)||r(m))){let e=Z(n,s.path.slice(0,-1)),t=e&&a(e)?e.value:e;if(t&&l(t)){let e=_;for(let t of s.path)e=e==null?void 0:e[t];if(e!==void 0){let t=L({...c,inlineTableStart:0},c),n=q({tmp:e},t),r=n==null?void 0:n.items[0];r&&a(r)&&(m=K(r.value))}}}let v,y=Z(n,s.path.slice(0,-1));y&&a(y)&&E(y,s.path.slice(0,-1))&&(y=ee(n,y));let x=to(n,s.path);if(a(f)&&a(m)){let i=!1;if(f.key.value.length>1){let o=T(f),c=Sa(s.path,f.key.value,o?o.length-f.key.value.length:0);if(c>0&&c=0;i--){let o=e[i];if(o===f)continue;let s=a(o)?o.key.value:t(o)||r(o)?o.key.item.value:void 0;s&&s.length>=u.length&&I(s.slice(0,u.length),u)&&Q(n,y,o)}}}}if(!i&&a(m)&&y&&(t(y)||e(y)||r(y))&&ro(n,y,f.key.value,f),Ra(f.value,m.value),y&&pa(y,f,f.value,m.value),v=f,f=f.value,m=m.value,i&&_!==void 0){let e=_;for(let t of s.path)e=e==null?void 0:e[t];if(e!==void 0){let t=eo(e,c);t!==void 0&&(m=t,Ra(f,m))}}}else if(a(f)&&u(m)&&a(m.item)){let i=!1;if(f.key.value.length>1){let o=T(f),c=Sa(s.path,f.key.value,o?o.length-f.key.value.length:0);if(c>0&&c=0;i--){let o=e[i];if(o===f)continue;let s=a(o)?o.key.value:t(o)||r(o)?o.key.item.value:void 0;s&&s.length>=u.length&&I(s.slice(0,u.length),u)&&Q(n,y,o)}}}}if(!i&&a(m.item)&&y&&(t(y)||e(y)||r(y))&&ro(n,y,f.key.value,f),v=f,f=f.value,m=m.item.value,i&&_!==void 0){let e=_;for(let t of s.path)e=e==null?void 0:e[t];if(e!==void 0){let t=eo(e,c);t!==void 0&&(m=t)}}}else if(u(f)&&a(f.item)&&a(m)){let e=f.item,i=!1;if(e.key.value.length>1){let o=T(e),c=Sa(s.path,e.key.value,o?o.length-e.key.value.length:0);if(c>0&&c=0;i--){let o=e[i];if(o===f)continue;let s=a(o)?o.key.value:u(o)&&a(o.item)?o.item.key.value:t(o)||r(o)?o.key.item.value:void 0;s&&s.length>=d.length&&I(s.slice(0,d.length),d)&&Q(n,m,o)}}}}if(Ra(e.value,m.value),v=e,f=e.value,m=m.value,i&&_!==void 0){let e=_;for(let t of s.path)e=e==null?void 0:e[t];if(e!==void 0){let t=eo(e,c);t!==void 0&&(m=t,Ra(f,m))}}}else if(u(f)&&u(m)&&a(f.item)&&a(m.item)){let e=!1,i=f.item;if(i.key.value.length>1){let o=T(i),c=Sa(s.path,i.key.value,o?o.length-i.key.value.length:0);if(c>0&&c=0;i--){let o=e[i];if(o===f)continue;let s=a(o)?o.key.value:u(o)&&a(o.item)?o.item.key.value:t(o)||r(o)?o.key.item.value:void 0;s&&s.length>=d.length&&I(s.slice(0,d.length),d)&&Q(n,m,o)}}}}if(Ra(i.value,m.item.value),v=i,f=i.value,m=m.item.value,e&&_!==void 0){let e=_;for(let t of s.path)e=e==null?void 0:e[t];if(e!==void 0){let t=eo(e,c);t!==void 0&&(m=t,Ra(f,m))}}}else if(t(f)){let o=J(i.items,``,{temporal:h});for(let e of s.path)o=o==null?void 0:o[e];if(o!==void 0){let i=f.key.item.value,l=i.slice(-1),u=i.slice(0,-1),d=ee(n,f)??Ni(n,s.path);if(e(d)){let e=$(n,i).filter(e=>e!==f&&(t(e)||r(e)));for(let t of e)Q(n,d,t)}let p=q({[l[0]]:o},c),m=p.items[0];if(u.length>0){if(t(m)||r(m))ne(m,u,f,d),g.add(m);else if($(n,u).some(a))A(m,u,f,d),g.add(m);else{let e=n.items.find(e=>e!==f&&(t(e)||r(e))&&I(e.key.item.value,u));if(e)V(n,e,m,void 0),Q(n,d,f),g.add(m);else{let e=G(u);S.add(e),V(n,e,m,0),Kn(n,d,f,e),g.add(e)}}}else Kn(n,d,f,m),g.add(m),Ua(n,m);if(p.items.length>1)for(let e=1;e0&&te(t,u);let r=e===1?m:p.items[e-1];V(n,d,t,d.items.indexOf(r)+1),g.add(t)}return}v=Ni(n,s.path)}else if(r(f)){let o=J(i.items,``,{temporal:h}),l=f.key.item.value,u=o,d=typeof s.path[s.path.length-1]==`number`?s.path.slice(0,-1):s.path;for(let e of d)u=u==null?void 0:u[e];if(u!==void 0){let i=l.slice(-1),o=l.slice(0,-1),d=ee(n,f)??Ni(n,s.path),p=q({[i[0]]:u},c).items[0];if(Array.isArray(u)&&!u.every(e=>F(e))){let e=$(n,l).filter(e=>e!==f&&(t(e)||r(e)));for(let t of e)Q(n,n,t)}if(o.length>0){if(t(p)||r(p))ne(p,o,f,d),g.add(p);else{let r=e(d)?d.items.find(e=>t(e)&&I(e.key.item.value,o)):void 0;if(r){let e=r.items.find(e=>a(e)&&I(e.key.value,p.key.value));e?Kn(n,r,e,p):V(n,r,p,void 0),Q(n,d,f),g.add(r);return}if($(n,o).some(a))A(p,o,f,d),g.add(p);else{let e=G(o);S.add(e),V(n,e,p,0),Kn(n,d,f,e),g.add(e)}}}else Kn(n,d,f,p),g.add(p),Ua(n,p);return}v=Ni(n,s.path)}else if(v=Ni(n,s.path),a(v)){let e=X(n,s.path.slice(0,-1));a(e)&&l(e.value)&&(v=e.value)}else u(v)&&a(v.item)?v=v.item.value:(u(v)&&d(v.item)||u(v)&&l(v.item))&&(v=v.item);if(u(f)&&u(m)&&(o(f.item)&&o(m.item)&&(Ra(f.item,m.item),m.loc={start:{...m.item.loc.start},end:{...m.item.loc.end}}),m.comma=f.comma),x){let e=b(f.loc),t=b(m.loc).columns-e.columns;t!==0&&ha(x.container,x.row,t)}m=ue(f,m,s.path),Kn(n,v,f,m),l(m)&&se.add(m),(l(m)||d(m))&&(U(n),Ur(m,c.indentWidth),ce.push({owner:v,container:m})),e(v)&&a(f)&&(t(m)||r(m))&&Wa(n,m)}else if(Y(s)){let o=Z(n,s.path);if(o){let f=Ni(n,s.path);if(f===o&&(f=Ni(n,s.path.slice(0,-1))),f===o){let e=Yi(n,o);e&&(f=e)}a(f)&&(f=f.value),u(f)&&a(f.item)&&(f=f.item.value),u(f)&&d(f.item)&&(f=f.item),u(f)&&l(f.item)&&(f=f.item),(!p(f)||!f.items.includes(o))&&(f=ee(n,o)??(p(f)?n:Yi(n,o)??n));let h=!1;if(s.path.length>1&&t(o)&&_!==void 0){let e=s.path.slice(0,-1),t=o.key.item.value.slice(0,-1);if($(n,t).filter(e=>e!==o).length===0){let r=_;for(let t of e)r=r==null?void 0:r[t];if(F(r)&&Object.keys(r).length===0){let e=o;for(;e.items.length>0;)H(n,e,P(e.items));U(n);let r=e.key,i=m(r)?r.item:r;i.value=t,i.raw=Na(i.raw,i.value),i.loc.end.column=i.loc.start.column+i.raw.length,r.loc.end.column=r.loc.start.column+i.raw.length+2,e.loc.end.line=r.loc.end.line,e.loc.end.column=r.loc.end.column,C(e),h=!0}}}let g=e(f)&&p(f)?f.items.indexOf(o):-1,v=p(f)&&!e(f)?f.items.indexOf(o):-1,y=u(o)?o.comma:void 0;u(o)&&(l(f)||d(f))&&Nn(f,o.loc.start.column),u(o)&&(l(f)||d(f))&&v===f.items.length-1&&y!==void 0&&ie.set(f,y);let b=u(o)&&(l(f)||d(f))&&o.loc.end.line===f.loc.end.line?f.loc.end.column-o.loc.end.column:void 0,w=T(u(o)&&a(o.item)?o.item:o),E=a(o)?o:u(o)&&a(o.item)?o.item:void 0;if(E&&w&&p(f)){let e=w.length-E.key.value.length,i=s.path.slice(e);if(i.length>0&&i.lengthi.length&&I(e.slice(0,i.length),i)&&s.push(n)}for(let e of s)Q(n,f,e)}}if(E&&w&&p(f)){let e=E.key.value,i=f.items,s=[];for(let n of i){if(n===o)continue;let i=a(n)?n.key.value:u(n)&&a(n.item)?n.item.key.value:t(n)||r(n)?n.key.item.value:void 0;i&&i.length>e.length&&I(i.slice(0,e.length),e)&&s.push(n)}for(let e of s)Q(n,f,e)}if((t(o)||r(o))&&p(f)){let e=o.key.item.value,i=f.items,s=[];for(let n of i){if(n===o)continue;let i=a(n)?n.key.value:t(n)||r(n)?n.key.item.value:void 0;i&&i.length>e.length&&I(i.slice(0,e.length),e)&&s.push(n)}for(let e of s)Q(n,f,e)}if(h||Q(n,f,o),u(o)&&(l(f)||d(f))&&o.loc.end.line===f.loc.end.line){let e=f.items,t=f.loc.end.line===f.loc.start.line?e.every(e=>e.loc.end.line===f.loc.start.line):e.length>0&&P(e).loc.end.line===f.loc.end.line;if(e.length>0&&v===e.length&&t){let t=b??f.loc.end.column-o.loc.end.column;U(n);let r=P(e).loc.end.column+t;f.loc.end.column!==r&&(f.loc.end.column=r);let i,s=e=>{if(a(e))return e.value===f?e:s(e.value);if(u(e))return s(e.item);if(p(e))for(let t of e.items){let e=s(u(t)?t.item:t);if(e)return e}};if(i=s(n),i&&i.loc.end.line===f.loc.end.line){i.loc.end.column=f.loc.end.column;let e=t=>{if(a(t))return e(t.value);if(u(t))return t.item===i?t:e(t.item);if(p(t))for(let n of t.items){let t=e(n);if(t)return t}},t=e(n);if(t){let e=f.loc.end.column-t.loc.end.column;t.loc.end.column=f.loc.end.column,e!==0&&(Bn(n,t,{lines:0,columns:e}),Vn(n),U(n))}}}}if(!h&&s.path.length>1&&(t(o)||r(o))&&_!==void 0){let e=s.path.slice(0,-1),t=o.key.item.value.slice(0,-1);if($(n,t).length===0){let r=_;for(let t of e)r=r==null?void 0:r[t];if(F(r)&&Object.keys(r).length===0){let e=G(t);S.add(e);let r=g>=0?Math.min(g,n.items.length):n.items.length;U(n),V(n,n,e,r)}}}if(!h&&s.path.length>1&&_!==void 0){let i=a(o)?o:u(o)&&a(o.item)?o.item:void 0;if(i&&i.key.value.length>1){let o=s.path.slice(0,-1),l=e(f)||t(f)||r(f)||d(f)?f:n;if(p(l)){let s;if(t(l)||r(l)){let e=l.key.item.value,t=0,n=0;for(let r=0;r=o.length||o[t]!==e[r])break;n++,t++}for(;t=o.length||o[t]!==e[n])break;n++,t++}for(;ttypeof e==`string`);let f=l.items.filter(e=>{let t=a(e)?e.key.value:u(e)&&a(e.item)?e.item.key.value:void 0;return t?I(t.slice(0,s.length),s):!1}),p=r(l),m=[];p&&f.length===0&&(m=$(n,l.key.item.value.concat(s)));let h=s.length===0&&(t(l)||r(l)||d(l));if(f.length===0&&m.length===0&&!h){let r=_;for(let e of o)r=r==null?void 0:r[e];if(F(r)&&Object.keys(r).length===0){if(d(l)){let e=s.map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`),t=Array.from(Be(`${e} = {}`))[0];if(a(t)){let e=K(t);e.comma=y??!1,U(n),Rn(l)&&l.loc.end.line>l.loc.start.line&&(l.loc.end.line=l.loc.start.line),V(n,l,e,v>=0?v:void 0),c.bracketSpacing&&W(e,{lines:0,columns:1});let r=l.items[v+1],i=B(n).get(e);r&&i&&r.loc.start.line===e.loc.end.line&&(i.columns=e.loc.end.column+2-r.loc.start.column)}return}let r=G(p||t(l)?l.key.item.value.concat(s):s);S.add(r);let i=g>=0?g:n.items.length;if(p){let e=n.items.indexOf(l);i=e>=0?e+1:n.items.length}else if(t(l)){let e=n.items.indexOf(l);i=e>=0?e+1:n.items.length}else if(e(l)){let e=Va(n);i=e>=0?e:n.items.length}U(n),V(n,n,r,i)}}}}}if(r(o)){let e=o.key.item.value,t=s.path.slice(0,-1);!Z(n,t.concat(0))&&Z(i,t)&&x.set(e.join(`.`),e)}}else{let e=()=>Z(n,s.path.concat(0))||$(n,s.path).filter(r)[0],a=e();if(a){let o=n.items.indexOf(a),c=!1;if(s.path.length>1&&_!==void 0){let e=s.path.slice(0,-1);if($(n,e).filter(e=>!(r(e)&&I(e.key.item.value,s.path))).length===0){let t=_;for(let n of e)t=t==null?void 0:t[n];if(F(t)&&Object.keys(t).length===0){let t=a,r=t.key;for(;t.items.length>0;)Q(n,t,P(t.items));U(n),t.type=`Table`,r.type=`TableKey`;let i=m(r)?r.item:r;i.value=e,i.raw=Na(i.raw,i.value),i.loc.end.column=i.loc.start.column+i.raw.length,r.loc.end.column=r.loc.start.column+i.raw.length+2,t.loc.end.line=r.loc.end.line,t.loc.end.column=r.loc.end.column,C(t),c=!0}}}let l;for(;l=e();)Q(n,n,l);for(let e of $(n,s.path).filter(t))Q(n,n,e);if(Z(i,s.path)){let e=s.path;x.set(e.join(`.`),e)}if(!c&&s.path.length>1&&_!==void 0){let e=s.path.slice(0,-1);if($(n,e).length===0){let t=_;for(let n of e)t=t==null?void 0:t[n];if(F(t)&&Object.keys(t).length===0){let t=G(e);S.add(t);let r=o>=0?Math.min(o,n.items.length):n.items.length;U(n),V(n,n,t,r)}}}}else{let e=no(n,s.path),t=$(n,e);if(t.length>0){let r=n.items.indexOf(t[0]);for(let e of t)Q(n,n,e);if(e.length>1&&_!==void 0){let t=e.slice(0,-1);if($(n,t).length===0){let e=_;for(let t of s.path.slice(0,-1))e=e==null?void 0:e[t];if(F(e)&&Object.keys(e).length===0){let e=G(t);S.add(e);let i=r>=0?Math.min(r,n.items.length):n.items.length;U(n),V(n,n,e,i)}}}}else X(n,s.path)}}}else if(wi(s)){if(s.key!==void 0){oe.push(s);return}let e=Z(n,s.path);if(e){if(m(e)&&(e=e.item),a(e)&&(e=e.value),s.from>=e.items.length)return;let t=e.items[s.from];Qi(n,e,t,s.to)}else{Ea(n);let e=X(n,s.path.concat(s.from)),i=Ki(n).find(t=>t.member===e),a=i?[...i.items]:[e],o=a.map(e=>({startLine:e.loc.start.line,endLine:e.loc.end.line})),c=n.items.indexOf(i?i.items[0]:e),l=o[o.length-1].endLine,u;for(let e=0;el){u=t.loc.start.line-l;break}}if(u===void 0&&c>0){let e=n.items[c-1];u=o[0].startLine-e.loc.end.line}let d=i?i.items.indexOf(e):0;for(let e=0;e<=d;e++)n.items.includes(a[e])&&H(n,n,a[e]);for(let e=d+1;e=0&&n.items.splice(t,1)}let p=Z(n,s.path.concat(s.to)),m,h=!1,g=i&&i.items[0]!==e&&f(i.items[0]);if(p){let e=Ki(n).find(e=>e.member===p);h=!!(e&&e.items[0]!==p&&f(e.items[0])),m=h&&g?n.items.indexOf(e.items[0]):n.items.indexOf(p)}else m=n.items.length;let _=m>0?n.items[m-1].loc.end.line:0,v=m0&&m0){let n=e===0?_:o[e-1].endLine,a=e===0&&v!==void 0?v-_:o[e].startLine-n;a!==(t(i)||r(i)?2:1)&&(s=a)}V(n,n,i,b,void 0,void 0,s),b++}if(u!==void 0){U(n);let e=a[a.length-1],t=n.items.indexOf(e)+1;if(t0&&(o.equals+=t,W(o.value,{lines:0,columns:t},{first_line_only:!0}),Bn(n,o.value,{lines:0,columns:t}));return}throw Error(`Cannot rename "${d.raw}" to "${f.raw}": the replacement key has ${f.value.length} segment(s) where the existing key has ${d.value.length}, so one cannot be substituted for the other.`)}f.raw=Na(d.raw,f.value),Ia(f,d.raw),f.loc.end.column=f.loc.start.column+f.raw.length,Kn(n,m(l)?l:o,d,f)}}),U(n);for(let e=1;e0&&W(i,{lines:-e,columns:0})}}let de=n.items;for(let e=0;e1){let n=1-t;for(let t=e+1;t{Rn(e)&&(fe=!0)},InlineArray:e=>{Rn(e)&&(fe=!0)}}),fe&&U(n);let N=!1,pe=[];if(Dt(n,{InlineTable:e=>{Rn(e)&&(pe.push(e),Ka(e),zn(e),er(n,e,c.bracketSpacing),N=!0)},InlineArray:e=>{Rn(e)&&(Ka(e),zn(e),er(n,e,c.bracketSpacing),N=!0)}}),N){U(n);for(let e of pe)qa(n,e);Ja(n)}if(Xa(n),Za(n,x,c),oa(n,oe,g),oe.length>0&&Vn(n),se.size>0){U(n);for(let e of se)Qr(e,c.indentWidth)}if(ce.length>0){U(n);for(let{owner:e,container:t}of ce)Ba(t),Vr(t,e.loc.start.column,c.indentWidth,c.bracketSpacing)}return n}function Ba(e){if(delete e.range,p(e))for(let t of e.items)Ba(t);m(e)&&Ba(e.item),a(e)&&(Ba(e.key),Ba(e.value))}function Va(e){return e.items.findIndex(e=>t(e)||r(e))}function Ha(e){let t=Va(e);return t===-1?void 0:t}function Ua(e,t){let n=e.items.indexOf(t);if(n<0)return;let r=Va(e);r===-1||r>n||(H(e,e,t),U(e),V(e,e,t,r))}function Wa(e,n){let i=e.items.indexOf(n);if(i<0)return;let o=e.items.findIndex((e,n)=>n>i&&(t(e)||r(e)));if(!e.items.some((e,t)=>t>i&&a(e)&&(o===-1||ttypeof e==`number`);if(n>0&&i.path.slice(n+1).every(e=>typeof e==`string`)){let s=Z(e,i.path.slice(0,n+1));if(s&&r(s)){let r=s.key.item.value,c=i.path.slice(n+1),l=$(e,r.concat(c));for(let t of l)Q(e,e,t);let u=s.items.filter(e=>{let t=a(e)?e.key.value:void 0;return t&&t.length>c.length&&I(t.slice(0,c.length),c)});for(let t of u)Q(e,s,t);let d=f;for(let e=i.path.length-1;e>n;e--)d={[i.path[e]]:d};let p=ai(q(d,o).items,o),m=Array.from(Be(p)),h=t=>{let n=s.items.find(e=>a(e)&&I(e.key.value,t.key.value));n?Kn(e,s,n,t):V(e,s,t,void 0)};for(let e of m){if(a(e)){h(e);continue}if(t(e)){let t=e.key.item.value;for(let n of e.items){if(!a(n))continue;let e=n.key.raw,r=t.concat(n.key.value);n.key.value=r,n.key.raw=yr(r).raw;let i=n.key.raw.length-e.length;n.key.loc.end.column=n.key.loc.start.column+n.key.raw.length,n.equals+=i,W(n.value,{lines:0,columns:i},{first_line_only:!0}),n.loc.end.line===n.loc.start.line&&(n.loc.end.column+=i),h(n)}}}return}}}let p=f;for(let e=i.path.length-1;e>=0;e--)p={[i.path[e]]:p};let m=$(e,i.path);for(let t of m)H(e,e,t);let h=ai(q(p,o).items,o),g=Array.from(Be(h)),_=Ha(e);_!==void 0&&U(e);let v;for(let n=0;nt(e)&&I(e.key.item.value,i.key.item.value)):void 0;if(o){for(let t of i.items)V(e,o,t,void 0);v||=o;continue}if(a(i)&&d(i.value)){let o=i.key.value,s=e.items.find(e=>t(e)&&I(e.key.item.value,o));if(s){for(let t of i.value.items)V(e,s,u(t)?t.item:t,void 0);v||=s;continue}if($(e,o).filter(e=>a(e)||t(e)||r(e)).length>0){for(let t of i.value.items){let r=u(t)?t.item:t;if(a(r)){let e=r.key.raw,t=o.concat(r.key.value);r.key.value=t,r.key.raw=t.map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`);let n=r.key.raw.length-e.length;r.key.loc.end.column=r.key.loc.start.column+r.key.raw.length,r.equals+=n,W(r.value,{lines:0,columns:n},{first_line_only:!0}),r.loc.end.line===r.loc.start.line&&(r.loc.end.column+=n)}V(e,e,r,n===0?_:void 0),v||=r}continue}}if(t(i)){let t=i.key.item.value;if($(e,t).filter(e=>a(e)).length>0){for(let r of i.items){if(a(r)){let e=r.key.raw,n=t.concat(r.key.value);r.key.value=n,r.key.raw=n.map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`);let i=r.key.raw.length-e.length;r.key.loc.end.column=r.key.loc.start.column+r.key.raw.length,r.equals+=i,W(r.value,{lines:0,columns:i},{first_line_only:!0}),r.loc.end.line===r.loc.start.line&&(r.loc.end.column+=i)}V(e,e,r,n===0?_:void 0),v||=r}continue}}n>0&&v&&t(v)&&a(i)?V(e,v,i,void 0):V(e,e,i,n===0?_:void 0),v||=i}let y=v;t(y)&&l.add(y),c.add(y)}function Ka(e){if(e.items.length>0){let t=e.items[e.items.length-1];e.loc.end.column=t.loc.end.column+1}else e.loc.end.line=e.loc.start.line,e.loc.end.column=e.loc.start.column+2}function qa(e,t){let n=[],r=e=>{if(n.push(e),e===t)return!0;if(a(e)){if(r(e.value))return!0}else if(u(e)){if(r(e.item))return!0}else if(p(e)){for(let t of e.items)if(r(t))return!0}return n.pop(),!1};if(!r(e))return;let i=n.findLast(u);if(!i)return;let o=t.loc.end.line-i.loc.end.line;if(o===0)return;let s=n.findLastIndex((e,t)=>t=0?c.items[m+1]:void 0;if(m<0||!h)return;let g=h.loc.start.column-f.loc.end.column,_=Ya(f)+g-h.loc.start.column;for(let e=m+1;et.loc.end.line&&(e.loc.end.line+=o);for(let e=n.length-1;e>=0;e--){let r=n[e];a(r)?(r.loc.end={...r.value.loc.end},r.value!==t&&d(r.value)&&(r.loc.end.column+=_)):u(r)&&(r.loc.end={...r.item.loc.end})}}function Ja(e){Dt(e,{InlineTable:{exit:e=>{if(e.items.length===0||e.loc.end.line!==e.loc.start.line)return;let t=e.items[e.items.length-1],n=Ya(t),r=e.loc.end.column,i=n+(r-t.loc.end.column);i!==r&&(e.loc.end.column=i)}},InlineArray:{exit:e=>{if(e.items.length===0||e.loc.end.line!==e.loc.start.line)return;let t=e.items[e.items.length-1],n=Ya(t),r=e.loc.end.column,i=n+(r-t.loc.end.column);i!==r&&(e.loc.end.column=i)}}})}function Ya(e){if(!u(e))return e.loc.end.column;if(a(e.item)){let t=e.item.value;if(d(t)||l(t))return t.loc.end.column}return d(e.item)||l(e.item)?e.item.loc.end.column:e.loc.end.column}function Xa(e){Dt(e,{KeyValue:t=>{let n=t.value;if(!l(n)||n.items.length>0)return;let r=Ni(e,[t.key.value[0]]);if(!r||!p(r))return;let i=t.loc.start.line,a=t.loc.end.line,o=r.items;for(let e=o.length-1;e>=0;e--){let t=o[e];if(!f(t))continue;let n=t.loc.start.line;n>=i&&n<=a&&o.splice(e,1)}}})}function Za(e,n,i){if(n.size!==0){for(let a of n.values()){if(a.length>1){let n=Z(e,a.slice(0,-1).concat([0]));if(n&&r(n)){let t=a[a.length-1],r=q({[t]:[]},i),o=gr([t],r.items[0].value);U(e),V(e,n,o);continue}let o;for(let n=a.length-1;n>=1;n--){let r=Z(e,a.slice(0,n));if(r&&t(r)){o=r;break}}if(o){let t=a.slice(o.key.item.value.length),n=gr(t,q({[t[t.length-1]]:[]},i).items[0].value);U(e),V(e,o,n);continue}}let n=gr(a,q({[a[a.length-1]]:[]},i).items[0].value);U(e),V(e,e,n,Ha(e))}U(e)}}function Qa(e,t,n){let r=[],i=(e,r)=>{for(let o=e.items.length-1;o>=0;o--){let s=e.items[o];if(a(s)&&d(s.value)){let c=[...e.key.item.value,...s.key.value];if(Gr(c)<(n.inlineTableStart??1)&&n.inlineTableStart!==0){let n=G(c);for(let e of s.value.items)u(e)&&a(e.item)&&V(t,n,e.item,void 0);e.items.splice(o,1),Wr(e),r.push(n),i(n,r)}}}};return i(e,r),r}function $a(e,t,n,r){let i=G([...t.key.item.value,...e.key.value]);if(d(e.value))for(let t of e.value.items)u(t)&&a(t.item)&&V(n,i,t.item,void 0);V(n,n,i,void 0),Wr(t);let o=Qa(i,n,r);for(let e of o)V(n,n,e,void 0)}function eo(e,n){let r=L({...n,inlineTableStart:0},n),i=ai(q({__tmp__:e},r).items,r),o=Array.from(Be(i))[0];if(a(o))return o.value;if(t(o)&&o.items.length>0&&a(o.items[0]))return o.items[0].value}function to(e,t){for(let n=t.length-1;n>0;n--){let r=Z(e,t.slice(0,n));if(!r||!a(r)||!d(r.value))continue;let i=Z(e,t.slice(0,n-1));if(i&&p(i))return{container:i,row:r}}}function $(e,n){let i=[];for(let o of e.items){let e;a(o)?e=o.key.value:(t(o)||r(o))&&(e=o.key.item.value),e&&I(e.slice(0,n.length),n)&&i.push(o)}return i}function no(e,t){let n=[];for(let i of t)typeof i==`number`&&$(e,n).some(r)||n.push(i);return n}function ro(e,n,i,o){let s=[...n.items];for(let c=s.length-1;c>=0;c--){let l=s[c];if(l===o)continue;let u=a(l)?l.key.value:t(l)||r(l)?l.key.item.value:void 0;u&&u.length>i.length&&I(u.slice(0,i.length),i)&&Q(e,n,l)}}function io(e,t){return e.line===t.line?e.column-t.column:e.line-t.line}function ao(e,t,n){let r={line:t,column:n},i=[],a=null;for(let t of e){let e=io(t.loc.end,r)<0,n=io(t.loc.start,r)<0;if(e)i.push(t),a=t.loc.end;else if(n&&!e)break;else break}return{truncatedCst:i,lastEndPosition:a}}var oo=class{_cst;_currentTomlString;_format;_integersAsBigInt;_temporal;constructor(e,t){let n=typeof e==`string`?e:Ot(e),r=At(n);this._currentTomlString=r,this._cst=Array.from(Be(r)),this._integersAsBigInt=(t==null?void 0:t.integersAsBigInt)??`asNeeded`,this._temporal=(t==null?void 0:t.temporal)??!1,this._format=R.autoDetectFormatWithCst(n,this._cst)}get toTomlString(){return this._format.leadingBom?`${this._currentTomlString}`:this._currentTomlString}get toJsObject(){let e=J(this._cst,this._currentTomlString,{integersAsBigInt:this._integersAsBigInt,temporal:this._temporal});return this._temporal?e:lo(e)}get cst(){return this._cst}patch(e,t){let n=L(t,this._format),r=this._currentTomlString;if(!this._temporal&&so(e)){let t=J(this._cst,r,{integersAsBigInt:this._integersAsBigInt,temporal:!1});e=co(e,t)}let i=t instanceof R||t!=null&&Object.prototype.hasOwnProperty.call(t,`indentWidth`),a=t instanceof R||t!=null&&Object.prototype.hasOwnProperty.call(t,`trailingComma`);Qt(this._cst,r);let o=Da(this._cst,e,n,i,a);this._cst=Array.from(Be(o.tomlString)),this._format=n,this._currentTomlString=o.tomlString}update(e){let t=At(e);if(t===this._currentTomlString){this._format.leadingBom=kt(e);return}let n=this._currentTomlString.split(this._format.newLine),r=zt(t),i=t.split(r),a=0;for(;a0&&d>0&&(f[0]=f[0].substring(d));let p=f.join(this._format.newLine);this._cst=Array.from(Ve(c,p,{line:u,column:d})),this._currentTomlString=t,this._format=R.autoDetectFormatWithCst(e,this._cst)}overwrite(e){let t=At(e);if(t===this._currentTomlString){this._format.leadingBom=kt(e);return}this._cst=Array.from(Be(t)),this._currentTomlString=t,this._format=R.autoDetectFormatWithCst(e,this._cst)}};function so(e,t=new WeakSet){return e instanceof Date?e.constructor===Date:!e||typeof e!=`object`||t.has(e)?!1:(t.add(e),Array.isArray(e)?e.some(e=>so(e,t)):Object.keys(e).some(n=>so(e[n],t)))}function co(e,t,n=new WeakSet){if(e instanceof Date)return e.constructor===Date&&t instanceof Date&&t.constructor!==Date&&t.getTime()===e.getTime()?t:e;if(!e||typeof e!=`object`||n.has(e))return e;if(n.add(e),Array.isArray(e)){if(!Array.isArray(t))return e;let r=!1,i=e.map((e,i)=>{let a=co(e,t[i],n);return a!==e&&(r=!0),a});return r?i:e}if(!F(t))return e;let r=!1,i={};for(let a of Object.keys(e)){let o=e[a],s=co(o,t[a],n);i[a]=s,s!==o&&(r=!0)}return r?i:e}function lo(e){if(e instanceof Date)return new Date(e.getTime());if(Array.isArray(e))return e.map(lo);if(e&&typeof e==`object`){let t={};for(let[n,r]of Object.entries(e))t[n]=lo(r);return t}return e}const uo=Object.freeze(R.default());function fo(e){let t=uo;return e.newLine===t.newLine&&e.trailingNewline===t.trailingNewline&&e.trailingComma===t.trailingComma&&e.bracketSpacing===t.bracketSpacing&&e.inlineTableStart===t.inlineTableStart&&e.truncateZeroTimeInDates===t.truncateZeroTimeInDates&&e.useTabsForIndentation===t.useTabsForIndentation&&e.indentWidth===t.indentWidth&&e.minimumDecimals===t.minimumDecimals&&e.leadingBom===t.leadingBom&&e.updateOrder===t.updateOrder&&e.multilineTable===t.multilineTable&&e.multilineArray===t.multilineArray}function po(e,t){let n=At(typeof e==`string`?e:Ot(e));return J(Be(n),n,{integersAsBigInt:(t==null?void 0:t.integersAsBigInt)??`asNeeded`,temporal:(t==null?void 0:t.temporal)??!1})}function mo(e,t){let n=t==null?uo:L(t,R.default()),r=q(e,n),i=fo(n)?si(r.items,n):ai(r.items,n);return n.leadingBom?`${i}`:i}function ho(e,t){return new oo(e,t)}export{ge as LocalDate,ve as LocalDateTime,_e as LocalTime,ye as OffsetDateTime,oo as TomlDocument,R as TomlFormat,po as parse,ho as parseDocument,xa as patch,mo as stringify}; \ No newline at end of file diff --git a/docker/t3-client/setup-bridge.js b/docker/t3-client/setup-bridge.js index c1b4b69..72e8154 100644 --- a/docker/t3-client/setup-bridge.js +++ b/docker/t3-client/setup-bridge.js @@ -69,7 +69,8 @@ const PALETTE_ITEMS = [ { id: 'setup', route: null, title: 'Open setup', meta: 'Agents, skills, toolchains, source control, ports, devices and updates on this server', terms: ['setup console'] }, { id: 'agents', route: 'agents', title: 'Agents', meta: 'Setup · sign in, install and update', terms: [] }, - { id: 'context', route: 'instructions', title: 'Instructions & skills', meta: 'Setup · global AGENTS.md and skills', terms: ['agents.md global instructions', 'context'] }, + { id: 'context', route: 'instructions', title: 'Instructions & skills', meta: 'Setup · global AGENTS.md, skills and MCP servers', terms: ['agents.md global instructions', 'context', 'mcp servers tools'] }, + { id: 'mcp', route: 'instructions/mcp', title: 'MCP servers', meta: 'Setup · shared definitions and provider configuration', terms: ['mcp tools servers'] }, { id: 'pair', route: 'access', title: 'Pair a device', meta: 'Setup · Access', terms: ['devices', 'pairing'] }, { id: 'toolchains', route: 'toolchains', title: 'Toolchains', meta: 'Setup · Go, Rust, Bun, Deno, uv and any mise tool', terms: [] }, { id: 'sourcecontrol', route: 'sourcecontrol', title: 'Source control', meta: 'Setup · sign in to GitHub, GitLab, Forgejo, Gitea and Azure DevOps', terms: ['codeberg', 'gh glab fj tea az'] }, diff --git a/docs/agent-browser.md b/docs/agent-browser.md index 3282302..5a94fe0 100644 --- a/docs/agent-browser.md +++ b/docs/agent-browser.md @@ -31,7 +31,10 @@ docker compose exec t3code t3-browser-mcp --remove This registers headless Chromium as an MCP server with the installed Claude Code, Codex and OpenCode, so agents can navigate, take screenshots, click and -read the browser console, whichever client you use. Run it after the first +read the browser console, whichever client you use. The console's +**Instructions & skills → MCP servers** tab can define these commands and +apply them per provider, with the same restart rule. `t3-browser-mcp` writes +provider entries directly without creating shared definitions. Run either after the first start has installed those agents (the setup page shows when), then start a new thread, because agents read their MCP configuration when a session starts. diff --git a/docs/context-management.md b/docs/context-management.md index f4b6734..fc3010a 100644 --- a/docs/context-management.md +++ b/docs/context-management.md @@ -2,7 +2,8 @@ **Setup → Instructions & skills** edits the shared AGENTS.md and each agent's own instruction file, on its **Instructions** tab; skills are on its **Skills** -tab (`#instructions/skills`). A two-column overview shows a safe text preview of +tab (`#instructions/skills`) and shared MCP servers on its **MCP servers** tab +(`#instructions/mcp`). A two-column overview shows a safe text preview of AGENTS.md beside the per-agent source choices. The instruction editor is centered on desktop and opens as a bottom sheet on phones. Uploading a Markdown file replaces the draft; only **Save** applies it. Downloads include unsaved edits. @@ -429,6 +430,99 @@ settings. A failed verification reports that the policy may already be saved; it does not roll back over a possible external change. External applications can still race the final check. Start a new conversation to load policy changes. +### Shared MCP servers + +Open **Instructions & skills → MCP servers**, then choose an agent, just as +on the Skills tab. Each row opens the shared definition; its switch links or +unlinks the selected agent’s copy. A **Differs** badge and **Apply** button +appear when that copy needs updating. Expand the agent’s settings to see its +config path and other servers, which stay unchanged. + +The tab holds local command and Streamable HTTP definitions at +`$T3CODE_HOME/setup/mcp/servers.json` (0600, at most 128 entries and 256 KiB). +Add/edit the command as a JSON array and environment/headers as JSON objects, +so empty arguments, whitespace and multiline values survive editing. Local and +remote drafts keep separate environment and header values when switching type. +Closing, changing tabs or leaving the page confirms discarding unsaved changes. +Reload warns while a draft or save is pending; navigation waits for a save to finish. +Values are literal; variable interpolation follows each provider's native rules. + +**Save updates the shared definition. Apply updates a provider's copy.** +Existing copies stay unchanged after a shared edit and show **Differs** until +explicitly applied or unlinked. Switches show whether a same-name entry exists, +including disabled or differing entries; they do not establish a live connection. +**Apply** replaces that complete entry, including provider-only options, after +confirmation. Unlink removes it. Both preserve a private config backup next to +the resolved target (`.t3-mcp-backup.`); these files contain secrets +and can be removed manually once no longer needed. Unlink from every supported +provider before removing the shared definition. Other provider entries remain +in place. Start a new provider process/session after applying changes. + +The tab manages default global config files, honoring the server environment's +Claude/Codex/XDG/OpenCode directory overrides. Custom T3 provider-instance homes, +project overrides, enterprise policy, approvals, plugin servers and effective +runtime availability are outside its inventory. An unreadable provider config +blocks removal rather than silently leaving an unknown copy behind. + +| Provider | Native global config | Support in this release | +| --- | --- | --- | +| Claude Code | `~/.claude.json`, or `$CLAUDE_CONFIG_DIR/.claude.json` | Local command/args/env and HTTP URL/headers | +| Codex | `$CODEX_HOME/config.toml` (default `~/.codex/config.toml`) | Local and HTTP; native `enabled`; full TOML parsing with edits limited to the selected server | +| OpenCode 1 | `opencode.json[c]`, `.mcp[name]` | Local and HTTP; native `enabled` | +| OpenCode 2 | `opencode.json[c]`, `.mcp.servers[name]` | Local and HTTP; native `disabled`; existing native V2 files stay V2; new files use the V1-compatible format also accepted by V2 | +| Cursor | `~/.cursor/mcp.json` | Local and HTTP; native approval/login still applies | +| Antigravity | T3's isolated provider profile | Not integrated; Gemini CLI settings are not an Antigravity integration | +| Grok Build | `~/.grok/config.toml` | Not managed; Grok may also load Claude/Cursor MCP entries through native compatibility imports | +| Muse Code | `$XDG_CONFIG_HOME/muse/settings.json` | Not managed in this release | + +Claude HTTP entries require an explicit transport type; a URL alone is +reported as differing. The documented `streamable-http` alias is recognized. +OpenCode JSONC wins if both files exist. V2 global options are preserved even +when the config has no server dictionary yet. Entries using the other layout's +state flag (`enabled` in V2 or `disabled` in V1) show **Differs** until applied +with the correct flag. Codex uses a pinned vendored TOML +syntax tree; nested tables, arrays of tables and unrelated comments are preserved. +All provider writes share the native-settings serialization queue with skills, +follow existing file/directory symlinks, reject stale revisions/retargeting and +publish atomically with owner-only permissions. Other applications can still +race between the final check and rename; this is not a cross-process transaction. +With credential persistence enabled, Claude's default `.claude.json` also lives +on the state volume. Custom roots need their own persistent mounts. + +Only Codex and OpenCode have a disabled state managed by this tab. Claude also +has native per-project MCP toggles, which this page leaves unchanged. For +Claude and Cursor, unlink to remove the global copy. Changing the definition’s +enabled flag does not stop old copies or existing sessions. A disabled definition with an +old copy remains visible as **Differs** and can always be unlinked. + +Environment/header values load only in the authenticated editor. Polling and +lists contain key names/counts, and remote URLs show only their origin (paths +and queries can contain credentials). Commands are displayed, so put secrets in +Environment or Headers. Credentials in URL userinfo are rejected. OAuth is not +shared: sign in through `claude mcp login`, `codex mcp login`, `opencode mcp auth` +or `cursor-agent mcp login` where required. Legacy SSE-only endpoints are not a +portable substitute for Streamable HTTP. + +Native checks on 2026-10-19 passed local and HTTP handshakes, environment/header +translation and unlink for Claude 2.1.303, Codex 0.163.0, OpenCode 1.14.19, +OpenCode 2.0.18/2.0.32 and Cursor 2026.10.18-ea085cb. Codex/OpenCode disabled +entries are checked not to start/connect. Checks use disposable homes and +fixture servers, without a model prompt or real credentials. This verifies +native configuration and MCP negotiation, not model-selected tool calls or +OAuth. Reproduce with `node scripts/mcp-native-smoke.mjs PROVIDER /absolute/path`. + +Native contracts: [Claude](https://code.claude.com/docs/en/mcp), +[Codex](https://learn.chatgpt.com/docs/extend/mcp?surface=cli), +[OpenCode V1](https://opencode.ai/docs/mcp-servers/), +[OpenCode V2](https://dev.opencode.ai/v2/docs/mcp-servers/), +[Cursor](https://cursor.com/docs/cli/mcp), +[Antigravity](https://antigravity.google/docs/cli/gcli-migration/), +[Grok](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/07-mcp-servers.md), +and [Muse](https://meta-models.github.io/muse-code-sdk/next/guides/extend/mcp-servers/). + +`t3-browser-mcp` remains a separate quick CLI path for the bundled browser +servers; it writes provider entries directly and does not create shared definitions. + ## Validation `node --test tests/*.test.mjs` checks file writes, existing-file discovery and @@ -472,6 +566,15 @@ confirmation, native state failures, busy controls and late provider responses. also cover catalog selection, raw Markdown previews, provider selection, replacement consent/conflicts, failed backup moves/restoration, supporting-file edits with restored mtimes, shared-root retargeting and partial results. +Unit/HTTP checks for shared MCP servers (`tests/mcp.test.mjs`, +`tests/mcp-api.test.mjs`, `tests/mcp-regressions.test.mjs`) cover store validation and revisions, TOML section +preservation, provider translation round-trips, link/diverged detection, +comment-preserving JSONC writes, unsupported providers and the authenticated +boundary including secret redaction in polling responses. `scripts/mcp-ui-audit.js` +checks agent selection, immediate refresh, editor revisions, lossless JSON +values, separate transport drafts, save/discard guards, apply/unlink, failed/late +reads and mobile/desktop/embedded layouts in both themes. Its API +responses are fixtures; no real provider configuration is modified. The adapters were also checked with Codex 0.163.0 in a disposable home. Native enable/disable, stale-request rejection, settings persistence across fresh diff --git a/docs/media/mcp/desktop-dark.png b/docs/media/mcp/desktop-dark.png new file mode 100644 index 0000000..790605e Binary files /dev/null and b/docs/media/mcp/desktop-dark.png differ diff --git a/docs/media/mcp/desktop-light.png b/docs/media/mcp/desktop-light.png new file mode 100644 index 0000000..2e42d82 Binary files /dev/null and b/docs/media/mcp/desktop-light.png differ diff --git a/docs/media/mcp/phone-dark.png b/docs/media/mcp/phone-dark.png new file mode 100644 index 0000000..7ab6742 Binary files /dev/null and b/docs/media/mcp/phone-dark.png differ diff --git a/docs/media/mcp/phone-light.png b/docs/media/mcp/phone-light.png new file mode 100644 index 0000000..2bf57cd Binary files /dev/null and b/docs/media/mcp/phone-light.png differ diff --git a/docs/setup-page.md b/docs/setup-page.md index 14f2328..735e003 100644 --- a/docs/setup-page.md +++ b/docs/setup-page.md @@ -132,4 +132,10 @@ allow/ask/deny user policies and reports the Build agent's effective rule. Unmanaged shared skills can be added, edited and removed from Codex/OpenCode. Other providers show **Found** with a discovery-only explanation. Phone layouts use grouped lists and bottom sheets. Status polling does not rescan skills or reset drafts. +The **MCP servers** tab holds one shared list of local and remote servers and +applies each entry to Claude Code, Codex, OpenCode or Cursor. Saving a shared +definition leaves existing provider copies unchanged until explicitly applied +or unlinked. Antigravity, Grok Build and Muse Code are not managed here. +Provider-only servers stay in place; switches indicate config presence rather +than runtime connectivity. See [Context management](context-management.md) for paths and verification. diff --git a/scripts/console-screens.js b/scripts/console-screens.js index 560cc8f..6d8ca35 100644 --- a/scripts/console-screens.js +++ b/scripts/console-screens.js @@ -46,7 +46,7 @@ const CHROME = process.env.CHROME_PATH || "/usr/bin/chromium"; const { routesTable, statusFor, portsFor, routeConsole, framed, unlock, visit } = require("./lib/console-fixtures.cjs"); const T3Routes = routesTable(); // Pages, and the second tab of the one that has tabs. -const ROUTES = list("routes", [...T3Routes.PAGES.map((p) => p.route), "instructions/skills", "more"]); +const ROUTES = list("routes", [...T3Routes.PAGES.map((p) => p.route), "instructions/skills", "instructions/mcp", "more"]); const SIZES = { phone: { width: 390, height: 844, scale: 2 }, tablet: { width: 820, height: 1180, scale: 1 }, @@ -62,6 +62,12 @@ const withSkillOverlays = args.includes("--skill-overlays"); // The overlays worth a picture: how to reach each from a loaded console. Each // returns false when the scenario has nothing to open. const OVERLAYS = [ + ['mcp-editor', async (t) => { + await t.evaluate(() => { location.hash = 'instructions/mcp'; }); + const edit = await t.waitForSelector('[data-cmd="mcp.view"]', { timeout: 5000 }).catch(() => null); + if (!edit) return false; + await edit.click(); await t.waitForSelector('.tc-mcp-editor'); return true; + }], ["palette", async (t, page) => { await t.evaluate(() => { location.hash = "overview"; }); await page.keyboard.press("Control+k"); await t.waitForSelector(".tc-layer .tc-palette"); return true; }], ["device-menu", async (t) => { await t.evaluate(() => { location.hash = "access"; }); diff --git a/scripts/fixtures/console/mcp.json b/scripts/fixtures/console/mcp.json new file mode 100644 index 0000000..fc2095f --- /dev/null +++ b/scripts/fixtures/console/mcp.json @@ -0,0 +1,23 @@ +{ + "ok": true, + "shared": { + "present": true, + "count": 3, + "revision": "mcp-fixture", + "error": null, + "servers": [ + {"name":"playwright","type":"local","enabled":true,"command":["playwright-mcp","--headless","--isolated"],"secretCount":0}, + {"name":"docs","type":"remote","enabled":true,"url":"https://docs.example.test","headerKeys":["Authorization"],"secretCount":1}, + {"name":"database","type":"local","enabled":false,"command":["database-mcp"],"envKeys":["DATABASE_URL"],"secretCount":1} + ] + }, + "providers": [ + {"provider":"claude","supported":true,"path":"/home/t3/.claude.json","revision":"claude-fixture","linked":["playwright"],"diverged":["docs"],"unlinked":["database"],"ownCount":1,"own":["github"]}, + {"provider":"codex","supported":true,"path":"/home/t3/.codex/config.toml","revision":"codex-fixture","linked":["playwright","docs","database"],"diverged":[],"unlinked":[],"ownCount":0}, + {"provider":"opencode","supported":true,"path":"/home/t3/.config/opencode/opencode.jsonc","revision":"opencode-fixture","linked":["docs"],"diverged":[],"unlinked":["playwright","database"],"ownCount":2,"own":["github","filesystem"]}, + {"provider":"cursor","supported":true,"path":"/home/t3/.cursor/mcp.json","revision":"cursor-fixture","linked":[],"diverged":[],"unlinked":["playwright","docs","database"],"ownCount":0}, + {"provider":"antigravity","supported":false,"error":"Antigravity uses isolated T3 profiles; shared MCP settings are not integrated yet.","linked":[],"diverged":[],"unlinked":[],"ownCount":0}, + {"provider":"grok","supported":false,"error":"Grok MCP management is not implemented. Grok may inherit Claude and Cursor servers through compatibility imports.","linked":[],"diverged":[],"unlinked":[],"ownCount":0}, + {"provider":"muse","supported":false,"error":"Muse MCP management is not implemented yet.","linked":[],"diverged":[],"unlinked":[],"ownCount":0} + ] +} diff --git a/scripts/lib/console-fixtures.cjs b/scripts/lib/console-fixtures.cjs index 672b338..fc6ca74 100644 --- a/scripts/lib/console-fixtures.cjs +++ b/scripts/lib/console-fixtures.cjs @@ -90,6 +90,13 @@ const statusFor = (scenario, change) => { const base = fixture("status.json"); const now = Date.now(); const status = SCENARIOS[scenario](shiftTimes(base, now - base.storage.at), now); + const mcp = fixture('mcp.json'); + if (scenario === 'setup') { + Object.assign(mcp.shared, { present: false, count: 0, servers: [] }); + for (const row of mcp.providers) Object.assign(row, { linked: [], unlinked: [], diverged: [], ownCount: 0 }); + } + if (scenario === 'attention') Object.assign(mcp.providers.find(p => p.provider === 'codex'), { error: 'Codex config.toml cannot be safely edited. Check its TOML syntax before changing MCP servers.', linked: [], unlinked: ['playwright', 'docs', 'database'] }); + status.context = { ...status.context, mcp }; return change ? change(status, now) || status : status; }; const portsFor = (scenario, change) => { @@ -113,6 +120,14 @@ const routeConsole = async (page, { status, ports, skillOptions }) => { if (at("/status")) return route.fulfill({ json: status() }); if (at("/ports")) return route.fulfill({ json: ports() }); if (at("/providers")) return route.fulfill({ json: fixture("providers.json") }); + if (at('/context/mcp')) return route.fulfill({ json: status().context.mcp }); + if (at('/context/mcp/read')) { + const name = url.searchParams.get('name'); + const summary = status().context.mcp.shared.servers.find(s => s.name === name); + const server = summary?.type === 'remote' ? { name, type: 'remote', enabled: true, url: 'https://docs.example.test/mcp', headers: { Authorization: 'Bearer sample-token' } } + : { name, type: 'local', enabled: summary?.enabled !== false, command: summary?.command || ['server'], env: name === 'database' ? { DATABASE_URL: 'sample-database-url' } : {} }; + return route.fulfill({ json: { ok: true, server, revision: status().context.mcp.shared.revision } }); + } return route.continue(); }); }; diff --git a/scripts/mcp-native-smoke.mjs b/scripts/mcp-native-smoke.mjs new file mode 100644 index 0000000..c54ec29 --- /dev/null +++ b/scripts/mcp-native-smoke.mjs @@ -0,0 +1,124 @@ +#!/usr/bin/env node +// Configuration -> native MCP discovery, without authentication or inference. +// node scripts/mcp-native-smoke.mjs codex /absolute/path/to/codex +import assert from "node:assert/strict"; +import * as fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; +import { createServer } from "node:http"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +const setup = process.env.T3_SETUP_MODULE || new URL("../docker/setup/mcp.mjs", import.meta.url).href; +const { createMcpService } = await import(setup); +const { withNativeProcess } = await import(process.env.T3_NATIVE_PROCESS_MODULE || new URL("../docker/harness/native-process.mjs", import.meta.url).href); +const [provider, executable] = process.argv.slice(2); +assert.ok(["claude", "codex", "opencode", "cursor"].includes(provider) && path.isAbsolute(executable || ""), "Pass a provider and its absolute executable path."); +const root = await fs.mkdtemp(path.join(os.tmpdir(), "t3-mcp-native-")); +const requests = []; +const answer = (request) => request.method === "initialize" + ? { protocolVersion: request.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "setup-fixture", version: "1.0.0" } } + : request.method === "tools/list" ? { tools: [{ name: "fixture", description: "Disposable test", inputSchema: { type: "object", properties: {} } }] } + : request.method === "resources/list" ? { resources: [] } + : request.method === "resources/templates/list" ? { resourceTemplates: [] } + : request.method === "prompts/list" ? { prompts: [] } : {}; +const http = createServer(async (request, response) => { + if (request.method !== "POST") { response.writeHead(405).end(); return; } + let body = ""; for await (const chunk of request) body += chunk; + const message = JSON.parse(body); + requests.push({ method: message.method, auth: request.headers.authorization, custom: request.headers["x-fixture"] }); + if (message.id === undefined) { response.writeHead(202).end(); return; } + response.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ jsonrpc: "2.0", id: message.id, result: answer(message) })); +}); +await new Promise(resolve => http.listen(0, "127.0.0.1", resolve)); +const mock = path.join(root, "server.cjs"); +await fs.writeFile(mock, `const fs = require('node:fs'); const answer = ${answer.toString()}; +require('node:readline').createInterface({input:process.stdin}).on('line', line => { + const request = JSON.parse(line); + fs.appendFileSync(process.argv[2], JSON.stringify({method:request.method,env:process.env.SETUP_FIXTURE_VALUE})+'\\n'); + if(request.id!==undefined) process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request.id,result:answer(request)})+'\\n'); +});`); +const nativeCommand = async (env, home, args) => (await promisify(execFile)(executable, args, { env, cwd: home, timeout: 20000, maxBuffer: 1024 * 1024 })).stdout; + +async function inventory(env, home, major) { + if (provider === "codex") return withNativeProcess({ executable, env, home, timeoutMs: 20000 }, ["app-server", "--stdio"], async ({ child, nextLine }) => { + const request = async (id, method, params) => { + child.stdin.write(JSON.stringify({ id, method, params }) + "\n"); + for (;;) { const message = JSON.parse(await nextLine()); if (message.id === id) { assert.ok(!message.error); return message.result; } } + }; + await request(1, "initialize", { clientInfo: { name: "setup-native-smoke", version: "1" }, capabilities: { experimentalApi: true } }); + child.stdin.write('{"method":"initialized","params":{}}\n'); + return request(2, "mcpServerStatus/list", {}); + }); + if (provider === "opencode" && major === 2) return withNativeProcess({ executable, home, env: { ...env, OPENCODE_USERNAME: "opencode", OPENCODE_PASSWORD: "fixture" }, timeoutMs: 20000 }, ["serve", "--stdio", "--hostname", "127.0.0.1", "--port", "0"], async ({ nextLine, signal }) => { + const ready = JSON.parse(await nextLine()), base = new URL(ready.url); + assert.ok(base.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(base.hostname)); + let result; + for (let i = 0; i < 120; i++) { + const response = await fetch(new URL("/api/mcp?directory=" + encodeURIComponent(home), base), { signal, headers: { authorization: "Basic " + Buffer.from("opencode:fixture").toString("base64") } }); + assert.equal(response.status, 200); result = await response.json(); + if (result.data?.some(row => row.name === "fixture" && ["connected", "disabled", "failed"].includes(row.status?.status))) break; + await new Promise(resolve => setTimeout(resolve, 100)); + } + return result; + }); + if (provider === "cursor") { + await nativeCommand(env, home, ["mcp", "enable", "fixture"]); + return nativeCommand(env, home, ["mcp", "list-tools", "fixture"]); + } + return nativeCommand(env, home, ["mcp", provider === "claude" ? "get" : "list", ...(provider === "claude" ? ["fixture"] : [])]); +} + +try { + for (const transport of ["local", "remote"]) { + const home = path.join(root, transport); await fs.mkdir(home); + const env = { ...process.env }; + for (const key of Object.keys(env)) if (/TOKEN|API_KEY|PASSWORD|^OPENCODE|^CLAUDE|^ANTHROPIC|^GEMINI|^T3_/.test(key)) delete env[key]; + Object.assign(env, { HOME: home, CODEX_HOME: path.join(home, ".codex"), CLAUDE_CONFIG_DIR: path.join(home, ".claude"), + XDG_CONFIG_HOME: path.join(home, ".config"), XDG_DATA_HOME: path.join(home, ".data"), XDG_STATE_HOME: path.join(home, ".state"), XDG_CACHE_HOME: path.join(home, ".cache"), + OPENCODE_CONFIG_DIR: path.join(home, ".config/opencode"), OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_CONFIG_PROJECT_DISABLE: "1", OPENCODE_DISABLE_PROJECT_CONFIG: "1", DISABLE_AUTOUPDATER: "1", CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" }); + const version = (await nativeCommand(env, home, ["--version"])).trim(), major = Number(/(?:^|\s|v)(\d+)\./.exec(version)?.[1]); + if (provider === 'claude' && transport === 'local') { + delete env.CLAUDE_CONFIG_DIR; + const target = path.join(root, 'persisted-claude.json'); + await fs.writeFile(target, '{}\n', {mode:0o600}); + await fs.symlink(target, path.join(home,'.claude.json')); + await nativeCommand(env,home,['mcp','add','--scope','user','writer-fixture','--','/usr/bin/false']); + assert.equal((await fs.lstat(path.join(home,'.claude.json'))).isSymbolicLink(),true,'native Claude writes must preserve state-volume persistence'); + assert.ok(JSON.parse(await fs.readFile(target,'utf8')).mcpServers['writer-fixture']); + } + if (provider === "opencode" && major === 2) { + await fs.mkdir(env.OPENCODE_CONFIG_DIR, { recursive: true }); + await fs.writeFile(path.join(env.OPENCODE_CONFIG_DIR, "opencode.json"), '{"mcp":{"servers":{}}}\n'); + } + const marker = path.join(home, "requests.jsonl"); + const svc = createMcpService({ home, env, io: fs, baseDir: path.join(home, ".t3") }); + const input = transport === "local" + ? { name: "fixture", type: "local", command: [process.execPath, mock, marker], env: { SETUP_FIXTURE_VALUE: "received" } } + : { name: "fixture", type: "remote", url: `http://127.0.0.1:${http.address().port}/mcp`, headers: { Authorization: "Bearer fixture", "X-Fixture": "received" } }; + const created = await svc.create(input); + await svc.setLink(provider, "fixture", true, created.revision, (await svc.list(provider)).revision); + const before = requests.length; + const result = await inventory(env, home, major); + const traffic = transport === "local" ? (await fs.readFile(marker, "utf8")).trim().split("\n").map(JSON.parse) : requests.slice(before); + assert.ok(traffic.some(row => row.method === "initialize"), "native runtime did not initialize MCP"); + if (provider === "codex" || provider === "opencode" && major === 2 || provider === "cursor") assert.ok(traffic.some(row => row.method === "tools/list"), "native runtime did not discover tools"); + if (transport === "local") assert.ok(traffic.every(row => row.env === "received")); + else assert.ok(traffic.every(row => row.auth === "Bearer fixture" && row.custom === "received")); + if (provider === "codex") assert.ok(result.data.some(row => row.tools?.fixture)); + if (provider === "opencode" && major === 2) assert.equal(result.data.find(row => row.name === "fixture").status.status, "connected"); + let currentRevision = created.revision; + if (["codex", "opencode"].includes(provider)) { + currentRevision = (await svc.save('fixture', currentRevision, { ...input, enabled: false })).revision; + await svc.setLink(provider, 'fixture', true, currentRevision, (await svc.list(provider)).revision); + const count = transport === 'local' ? (await fs.readFile(marker, 'utf8')).length : requests.length; + await inventory(env, home, major); + assert.equal(transport === 'local' ? (await fs.readFile(marker, 'utf8')).length : requests.length, count, 'a disabled server must not start or connect'); + } + await svc.setLink(provider, "fixture", false, currentRevision, (await svc.list(provider)).revision); + assert.equal((await svc.list(provider)).shared[0].linked, false); + console.log(`${provider} ${version}: ${transport} MCP initialize, credentials, unlink${['codex','opencode'].includes(provider) ? ' and native disable' : ''} passed.`); + } +} finally { + http.closeAllConnections(); await new Promise(resolve => http.close(resolve)); + await fs.rm(root, { recursive: true, force: true, maxRetries: 3 }); +} diff --git a/scripts/mcp-ui-audit.js b/scripts/mcp-ui-audit.js new file mode 100644 index 0000000..0923da2 --- /dev/null +++ b/scripts/mcp-ui-audit.js @@ -0,0 +1,166 @@ +#!/usr/bin/env node +// MCP tab behavior with deterministic, isolated API responses. No real config writes. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +let chromium; +try { ({chromium} = require('playwright')); } catch { ({chromium} = require('playwright-core')); } +const {statusFor, portsFor, routeConsole, framed, unlock, visit} = require('./lib/console-fixtures.cjs'); +const base = process.argv[2] || 'http://127.0.0.1:23780/__setup/'; +const key = process.argv[3] || 'dev'; +const out = process.env.T3_MCP_UI_OUTPUT || '/tmp/t3-mcp-ui-audit'; +fs.mkdirSync(out, {recursive:true}); +(async () => { + const browser = await chromium.launch({executablePath:process.env.CHROME_PATH || '/usr/bin/chromium',args:['--no-sandbox']}); + try { + for (const theme of ['light','dark']) for (const variant of ['phone','desktop','embedded']) { + const viewport = variant === 'phone' ? {width:375,height:667} : variant === 'embedded' ? {width:1198,height:878} : {width:1440,height:900}; + const context = await browser.newContext({viewport,colorScheme:theme,reducedMotion:'reduce'}); + const page = await context.newPage(); + const errors = []; page.on('pageerror',e=>errors.push(e.message)); + let revision = 'shared-1', nativeRevision = 1, configured = false, differs = false, claudePresent = false, readError = false, pendingRead, pendingSave; + let definition = {name:'probe',type:'local',enabled:true,command:['server'],env:{}}; + const calls = []; + const snapshot = () => ({ok:true,shared:{present:true,count:1,revision,servers:[{name:'probe',type:definition.type,enabled:definition.enabled,command:definition.command,secretCount:Object.keys(definition.env).length}]},providers:[ + {provider:'codex',supported:true,path:'/home/t3/.codex/config.toml',revision:'native-'+nativeRevision,linked:configured&&!differs?['probe']:[],diverged:configured&&differs?['probe']:[],unlinked:configured?[]:['probe']}, + {provider:'claude',supported:true,path:'/home/t3/.claude.json',revision:'native-'+nativeRevision,linked:[],diverged:claudePresent?['probe']:[],unlinked:claudePresent?[]:['probe']} + ]}); + const status = statusFor('skills'); status.context.mcp = snapshot(); + await routeConsole(page,{status:()=>status,ports:()=>portsFor('ready')}); + await page.route('**/*',async route=>{ + const req = route.request(), url = new URL(req.url()); + if (!url.pathname.includes('/context/mcp')) return route.fallback(); + if (req.method() === 'GET') { + if (url.pathname.endsWith('/read')) { + const value = {ok:true,server:structuredClone(definition),revision}; + if (pendingRead) await pendingRead.promise; + return route.fulfill({status:readError?500:200,json:readError?{ok:false,error:'Fixture read failure'}:value}); + } + return route.fulfill({json:snapshot()}); + } + if (url.pathname.endsWith('/save') && pendingSave) await pendingSave; + const body = req.postDataJSON(); calls.push({route:url.pathname,body}); + if (url.pathname.endsWith('/link')) { + assert.equal(body.providerRevision,'native-'+nativeRevision); + assert.equal(body.sharedRevision,revision); + if(body.provider==='codex') {configured=body.linked;differs=false;} else claudePresent=body.linked; + nativeRevision++; + } else if (url.pathname.endsWith('/save')) { + assert.equal(body.revision,revision,'Save must use the revision loaded with the editor'); + definition=body.server; revision += '-saved'; differs=configured; + } + return route.fulfill({json:{ok:true}}); + }); + const url = new URL(base); url.searchParams.set('theme',theme); + if(variant==='embedded')url.searchParams.set('embed','t3'); + const target = variant==='embedded'?await framed(page,url.href):page; + if(variant!=='embedded')await page.goto(url.href); + await unlock(target,key); await visit(target,page,'instructions/mcp'); + await target.locator('#mcp-provider').selectOption('codex'); + const toggle=target.getByRole('switch',{name:'Codex configured for probe'}); + await toggle.click(); await toggle.waitFor({state:'visible'}); + await target.waitForFunction(()=>document.querySelector('[aria-label="Codex configured for probe"]')?.getAttribute('aria-checked')==='true'); + await toggle.click(); await target.getByRole('button',{name:'Unlink',exact:true}).click(); + await target.waitForFunction(()=>document.querySelector('[aria-label="Codex configured for probe"]')?.getAttribute('aria-checked')==='false'); + assert.deepEqual(calls.filter(c=>c.route.endsWith('/link')).map(c=>c.body.linked),[true,false]); + // The list and global /status remain stale; the editor must use its read response. + revision='externally-updated'; definition={...definition,command:['server',' spaced ',''],env:{MULTILINE:'line one\nline two',SPACES:' value '}}; + await target.getByRole('button',{name:'Edit probe',exact:true}).click(); + await target.locator('#mcp-command').waitFor(); + await target.waitForFunction(()=>document.querySelector('#mcp-command')?.value.includes('spaced')); + assert.equal(await target.getByRole('button',{name:'Save',exact:true}).isDisabled(),true); + // Each transport keeps its own values, including when the DOM is morphed. + const originalEnv=await target.locator('#mcp-pairs').inputValue(); + await target.locator('#mcp-type').selectOption('remote'); + assert.equal(await target.locator('#mcp-pairs').inputValue(),'{}'); + await target.locator('#mcp-pairs').fill('{"Authorization":"fixture"}'); + await target.locator('#mcp-type').selectOption('local'); + assert.equal(await target.locator('#mcp-pairs').inputValue(),originalEnv); + await target.locator('#mcp-command').fill('["server", " spaced ", ""]'); + let finishSave; pendingSave=new Promise(resolve=>{finishSave=resolve;}); + await target.getByRole('button',{name:'Save',exact:true}).click(); + await target.getByRole('button',{name:'Saving…',exact:true}).waitFor(); + assert.equal(await target.locator('.tc-mcp-editor [aria-label=Close]').isDisabled(),true); + await page.keyboard.press('Escape'); + assert.equal(await target.locator('.tc-mcp-editor').count(),1); + await target.evaluate(() => { location.hash = 'instructions/skills'; }); + await target.waitForFunction(() => location.hash === '#instructions/mcp'); + assert.equal(await target.locator('#cx-tab-mcp').getAttribute('aria-selected'), 'true', 'navigation waits for a save'); + finishSave(); pendingSave=null; + await target.locator('.tc-mcp-editor').waitFor({state:'detached'}); + assert.deepEqual(definition.command,['server',' spaced ','']); + assert.deepEqual(definition.env,{MULTILINE:'line one\nline two',SPACES:' value '}); + configured=true; differs=true; + await target.getByRole('button',{name:'Refresh MCP servers'}).click(); + await target.getByRole('button',{name:'Apply',exact:true}).click(); + await target.getByRole('button',{name:'Apply shared',exact:true}).click(); + await target.getByRole('button',{name:'Apply',exact:true}).waitFor({state:'detached'}); + assert.equal(differs,false);assert.equal(configured,true); + // A disabled definition with a surviving Claude entry must remain removable. + definition.enabled=false;claudePresent=true; + await target.getByRole('button',{name:'Refresh MCP servers'}).click(); + await target.locator('#mcp-provider').selectOption('claude'); + const claude=target.getByRole('switch',{name:'Claude Code configured for probe'}); + await target.waitForFunction(()=>document.querySelector('[aria-label="Claude Code configured for probe"]')?.getAttribute('aria-checked')==='true'); + await claude.click();await target.getByRole('button',{name:'Unlink',exact:true}).click(); + await target.waitForFunction(()=>document.querySelector('[aria-label="Claude Code configured for probe"]')?.getAttribute('aria-checked')==='false'); + assert.equal(claudePresent,false); + await target.locator('#mcp-provider').selectOption('codex'); + assert.equal(await toggle.getAttribute('aria-checked'),'true'); + // A dismissed read must never populate a newly opened Add dialog. + let release;pendingRead={promise:new Promise(resolve=>{release=resolve;})}; + await target.getByRole('button',{name:'Edit probe',exact:true}).click(); + await target.locator('.tc-mcp-editor [data-mcp=close][aria-label=Close]').click(); + await target.getByRole('button',{name:'Add server',exact:true}).click(); + release();pendingRead=null; + await target.locator('#mcp-name').waitFor(); + assert.equal(await target.locator('#mcp-name').inputValue(),''); + await target.locator('#mcp-name').fill('new-server'); + await target.locator('#mcp-command').fill('not JSON'); + const count=calls.length; + await target.getByRole('button',{name:'Add server',exact:true}).last().click(); + await target.getByRole('alert').filter({hasText:'JSON array'}).waitFor(); + assert.equal(calls.length,count,'invalid JSON must not submit'); + assert.equal(await target.locator('#mcp-command').inputValue(),'not JSON'); + await page.keyboard.press('Escape'); + await target.getByRole('alertdialog').getByRole('button',{name:'Cancel',exact:true}).click(); + assert.equal(await target.locator('#mcp-command').inputValue(),'not JSON'); + await target.locator('.tc-mcp-editor [data-mcp=close][aria-label=Close]').click(); + await target.getByRole('button',{name:'Discard changes',exact:true}).click(); + // Page/history navigation shares the new library's draft protections. + await target.getByRole('button',{name:'Edit probe',exact:true}).click(); + await target.locator('#mcp-command').waitFor(); + await target.locator('#mcp-command').fill('["unsaved-server"]'); + assert.equal(await target.evaluate(() => { + const event = new Event('beforeunload', { cancelable: true }); + window.dispatchEvent(event); return event.defaultPrevented; + }), true, 'reload warns while an MCP draft is dirty'); + await target.evaluate(() => { location.hash = 'instructions/skills'; }); + await target.getByRole('alertdialog').getByRole('button',{name:'Cancel',exact:true}).click(); + await target.waitForFunction(() => location.hash === '#instructions/mcp'); + assert.equal(await target.locator('#cx-tab-mcp').getAttribute('aria-selected'), 'true'); + assert.equal(await target.locator('#mcp-command').inputValue(), '["unsaved-server"]'); + await target.evaluate(() => { location.hash = 'instructions/skills'; }); + await target.getByRole('button',{name:'Discard changes',exact:true}).click(); + await target.locator('.tc-skills-library').waitFor(); + assert.equal(await target.locator('.tc-mcp-editor').count(), 0); + await target.waitForFunction(() => document.querySelector('#cx-tab-skills')?.textContent.trim() === 'Skills 22'); + assert.equal(await target.locator('#cx-tab-skills').innerText(), 'Skills 22'); + await target.evaluate(() => { location.hash = 'instructions'; }); + await target.locator('.tc-doc-preview .tc-code').waitFor(); + await target.evaluate(() => { location.hash = 'instructions/mcp'; }); + await target.locator('#mcp-provider').waitFor(); + readError=true; + await target.getByRole('button',{name:'Edit probe',exact:true}).click(); + await target.getByRole('alert').filter({hasText:'Fixture read failure'}).waitFor(); + assert.equal(await target.getByRole('button',{name:'Save',exact:true}).isDisabled(),true); + assert.equal(await target.getByRole('button',{name:'Remove…',exact:true}).isDisabled(),true); + await page.screenshot({path:path.join(out,`${variant}-${theme}-read-error.png`)}); + await target.locator('.tc-mcp-editor [data-mcp=close][aria-label=Close]').click(); + assert.equal(await target.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false); + assert.deepEqual(errors,[]); + await context.close(); + console.log(`${variant} ${theme}: agent selection, links, editor revisions, separate transport drafts, guarded saves/discard, apply/unlink, failed and late reads passed.`); + } + } finally {await browser.close();} +})().catch(error=>{console.error(error);process.exitCode=1;}); diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 2462b13..410b6be 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -672,6 +672,33 @@ context_instructions_work() { } check "Context authenticates saves and reports provider links" context_instructions_work +mcp_servers_list_without_secrets() { + docker exec "$NAME" curl -fsS --max-time 25 -H "x-t3-setup-key: $SETUP_KEY" \ + http://127.0.0.1:3774/__setup/context/mcp | python3 -c " +import json, sys +body = json.load(sys.stdin) +shared = body['shared'] +assert isinstance(shared.get('servers'), list), shared +for server in shared['servers']: + assert 'env' not in server and 'headers' not in server, server +providers = {row['provider'] for row in body['providers']} +assert {'claude', 'codex', 'opencode'} <= providers, providers +" +} +check "shared MCP servers list without secrets" mcp_servers_list_without_secrets + +mcp_editor_works() { + [ "$HAS_BROWSER" -eq 1 ] || return 0 + docker exec "$NAME" mkdir -p /tmp/t3-mcp-audit/lib /tmp/t3-mcp-audit/fixtures || return 1 + docker cp scripts/mcp-ui-audit.js "$NAME:/tmp/t3-mcp-audit/mcp-ui-audit.js" >/dev/null 2>&1 || return 1 + docker cp scripts/lib/console-fixtures.cjs "$NAME:/tmp/t3-mcp-audit/lib/console-fixtures.cjs" >/dev/null 2>&1 || return 1 + docker cp scripts/lib/skills-fixtures.cjs "$NAME:/tmp/t3-mcp-audit/lib/skills-fixtures.cjs" >/dev/null 2>&1 || return 1 + docker cp scripts/fixtures/console "$NAME:/tmp/t3-mcp-audit/fixtures/console" >/dev/null 2>&1 || return 1 + docker exec -e CHROME_PATH=/usr/bin/chromium -e NODE_PATH=/opt/t3/node_modules \ + "$NAME" node /tmp/t3-mcp-audit/mcp-ui-audit.js "http://127.0.0.1:3774/" "$SETUP_KEY" >>"$UI_AUDIT_LOG" 2>&1 +} +check "MCP controls refresh immediately and preserve editor revisions and values" mcp_editor_works + hello_says_only_what_it_may() { local stranger member stranger="$(docker exec "$NAME" curl -fsS --max-time 25 http://127.0.0.1:3774/__setup/hello)" || return 1 @@ -812,6 +839,19 @@ native_skill_controls_work() { } check "native skill controls work with the installed Claude, Codex and OpenCode" "native_skill_controls_work >>\"$UI_AUDIT_LOG\" 2>&1" +native_mcp_controls_work() { + local id executable + docker cp scripts/mcp-native-smoke.mjs "$NAME:/tmp/mcp-native-smoke.mjs" >/dev/null 2>&1 || return 1 + for id in claude codex opencode cursor; do + executable="$(status_json | jq -r --arg id "$id" '.harnesses[] | select(.id == $id) | .executable')" + [ -n "$executable" ] && [ "$executable" != null ] || return 1 + docker exec -u t3 -e T3_SETUP_MODULE=/opt/t3-setup/mcp.mjs \ + -e T3_NATIVE_PROCESS_MODULE=/opt/t3-harness/native-process.mjs \ + "$NAME" node /tmp/mcp-native-smoke.mjs "$id" "$executable" || return 1 + done +} +check "native MCP local and HTTP settings work in Claude, Codex, OpenCode and Cursor" "native_mcp_controls_work >>\"$UI_AUDIT_LOG\" 2>&1" + # T3 learns about each agent through its settings file, as soon as the agent # lands. Read it the way T3 does: each points at the agent's launcher, which # runs the managed executable (and hands T3's own Update to the manager). diff --git a/scripts/ui-audit.js b/scripts/ui-audit.js index cf0e670..25736a3 100755 --- a/scripts/ui-audit.js +++ b/scripts/ui-audit.js @@ -350,6 +350,10 @@ const audit = () => { await page.click(".tc-layer .tc-menu-item:has-text('Rename')"); }; const STATES = [ + ['MCP servers', async (page) => { + await page.evaluate(() => { location.hash = 'instructions/mcp'; }); + await page.waitForSelector('.tc-mcp:not([hidden])'); + }], ...ROUTES.map((route) => [route, async (page) => { await page.evaluate((r) => { location.hash = r; }, route); await page.waitForSelector(`#page-${route}:not([hidden]) .tc-section, #page-${route}:not([hidden]) .tc-group`, { timeout: 60000 }); diff --git a/tests/context-api.test.mjs b/tests/context-api.test.mjs index 659b49f..f2206cd 100644 --- a/tests/context-api.test.mjs +++ b/tests/context-api.test.mjs @@ -25,12 +25,16 @@ test("global instruction API discovers existing files and changes them only on e MISE_CONFIG_DIR: path.join(config, "mise"), T3_SETUP_PORT: String(port), T3_SETUP_KEY: "fixture-key", T3_SETUP_KEY_SOURCE: "env", T3_INFRA_LAUNCHER: "/bin/false", T3CODE_PORT: "1", }, stdio: ["ignore", "pipe", "pipe"] }); + const closed = once(child, "close"); let logs = ""; child.stdout.on("data", (chunk) => { logs += chunk; }); child.stderr.on("data", (chunk) => { logs += chunk; }); t.after(async () => { - if (child.exitCode === null) { const exit = once(child, "exit"); child.kill(); await exit; } - await fs.rm(root, { recursive: true, force: true }); + if (child.exitCode === null && child.signalCode === null) child.kill(); + await closed; + // Native status probes can finish filesystem work during shutdown. Drain + // their stdio and allow bounded cleanup retries, as other API fixtures do. + await fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); }); const base = `http://127.0.0.1:${port}`; let ready = false; diff --git a/tests/mcp-api.test.mjs b/tests/mcp-api.test.mjs new file mode 100644 index 0000000..1cb726f --- /dev/null +++ b/tests/mcp-api.test.mjs @@ -0,0 +1,87 @@ +// Check the authenticated MCP HTTP boundary in a disposable agent home. +import assert from "node:assert/strict"; +import test from "node:test"; +import path from "node:path"; +import os from "node:os"; +import * as fs from "node:fs/promises"; +import { spawn } from "node:child_process"; +import { createServer } from "node:net"; +import { once } from "node:events"; +import { fileURLToPath } from "node:url"; + +test("shared MCP API creates, links and removes servers without leaking secrets", { timeout: 20000 }, async (t) => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "t3-mcp-api-")); + const home = path.join(root, "home"); + await fs.mkdir(home, { recursive: true }); + const reservation = createServer().listen(0, "127.0.0.1"); + await once(reservation, "listening"); + const port = reservation.address().port; + await new Promise((resolve) => reservation.close(resolve)); + const child = spawn(process.execPath, [fileURLToPath(new URL("../docker/setup/server.mjs", import.meta.url))], { env: { + ...process.env, HOME: home, T3CODE_HOME: path.join(home, ".t3"), T3_WORKSPACE: path.join(root, "workspace"), + CODEX_HOME: path.join(home, ".codex"), CLAUDE_CONFIG_DIR: path.join(home, ".claude"), XDG_CONFIG_HOME: path.join(home, ".config"), + MISE_DATA_DIR: path.join(home, ".local/share/mise"), MISE_STATE_DIR: path.join(home, ".local/state/mise"), + MISE_CONFIG_DIR: path.join(home, ".config/mise"), T3_SETUP_PORT: String(port), T3_SETUP_KEY: "mcp-fixture-key", + T3_SETUP_KEY_SOURCE: "env", T3_INFRA_LAUNCHER: "/bin/false", T3CODE_PORT: "1", + }, stdio: ["ignore", "pipe", "pipe"] }); + const closed = once(child, "close"); + let logs = ""; + child.stdout.on("data", (chunk) => { logs += chunk; }); + child.stderr.on("data", (chunk) => { logs += chunk; }); + t.after(async () => { + if (child.exitCode === null && child.signalCode === null) child.kill(); + await closed; + // Match the other API fixtures: native probes may be draining work after + // the setup process exits, so cleanup retries are bounded. + await fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); + }); + const base = `http://127.0.0.1:${port}/__setup`; + let ready = false; + for (let i = 0; i < 200; i++) { + if (child.exitCode !== null) throw new Error(logs); + try { if ((await fetch(base + "/hello")).ok) { ready = true; break; } } catch {} + await new Promise((resolve) => setTimeout(resolve, 20)); + } + assert.ok(ready, logs); + const get = (route) => fetch(base + route, { headers: { "x-t3-setup-key": "mcp-fixture-key" } }); + const post = (route, body, headers = {}) => fetch(base + route, { method: "POST", headers: { "x-t3-setup-key": "mcp-fixture-key", "content-type": "application/json", ...headers }, body: JSON.stringify(body) }); + assert.equal((await fetch(base + "/context/mcp")).status, 401); + assert.equal((await post("/context/mcp/create", { server: { name: "x", type: "local", command: ["x"] } }, { "sec-fetch-site": "cross-site" })).status, 403); + assert.equal((await post("/context/mcp/create", { server: { name: "Bad Name", type: "local", command: ["x"] } })).status, 400); + assert.equal((await post("/context/mcp/create", null)).status, 400); + + const created = await (await post("/context/mcp/create", { server: { name: "playwright", type: "local", command: ["playwright-mcp", "--headless"], env: { TOKEN: "secret" } } })).json(); + assert.equal(created.ok, true); + const status = await (await get("/status")).json(); + assert.equal(status.context.mcp.shared.count, 1); + assert.equal(JSON.stringify(status).includes('"secret"'), false); + assert.deepEqual(status.context.mcp.providers.find((row) => row.provider === "codex").linked, []); + assert.match(status.context.mcp.providers.find((row) => row.provider === "grok").error, /not implemented/); + + const read = await (await get("/context/mcp/read?name=playwright")).json(); + assert.equal(read.server.env.TOKEN, "secret"); + assert.equal((await get("/context/mcp/read")).status, 400); + assert.equal((await get("/context/mcp/read?name=missing")).status, 404); + assert.equal((await get("/context/mcp?provider=nope")).status, 400); + assert.equal((await get("/context/mcp?provider=grok")).status, 403); + + const providerRevision = (provider) => get('/context/mcp?provider=' + provider).then(r => r.json()).then(r => r.revision); + const link = await post("/context/mcp/link", { provider: "codex", name: "playwright", linked: true, sharedRevision: created.revision, providerRevision: await providerRevision('codex') }); + assert.equal(link.status, 200); + assert.match(await fs.readFile(path.join(home, ".codex", "config.toml"), "utf8"), /playwright-mcp/); + const codex = await (await get("/context/mcp?provider=codex")).json(); + assert.equal(codex.shared[0].linked, true); + assert.equal((await post("/context/mcp/link", { provider: "codex", name: "playwright", linked: true, sharedRevision: "stale", providerRevision: await providerRevision('codex') })).status, 409); + assert.equal((await post("/context/mcp/link", { provider: "codex", name: "playwright", linked: false, sharedRevision: created.revision })).status, 400); + const unlink = await post("/context/mcp/link", { provider: "claude", name: "playwright", linked: false, sharedRevision: created.revision, providerRevision: await providerRevision('claude') }); + assert.equal(unlink.status, 200); + assert.equal((await post("/context/mcp/link", { provider: "grok", name: "playwright", linked: true, sharedRevision: created.revision, providerRevision: 'unused' })).status, 403); + + const saved = await post("/context/mcp/save", { name: "playwright", revision: "stale", server: { name: "playwright", type: "local", command: ["other"], env: {} } }); + assert.equal(saved.status, 409); + assert.equal((await post("/context/mcp/remove", { name: "playwright", revision: created.revision })).status, 409); + assert.equal((await post("/context/mcp/link", { provider: "codex", name: "playwright", linked: false, sharedRevision: created.revision, providerRevision: await providerRevision('codex') })).status, 200); + const removed = await post("/context/mcp/remove", { name: "playwright", revision: created.revision }); + assert.equal(removed.status, 200); + assert.equal((await get("/context/mcp/read?name=playwright")).status, 404); +}); diff --git a/tests/mcp-regressions.test.mjs b/tests/mcp-regressions.test.mjs new file mode 100644 index 0000000..cdd3fcd --- /dev/null +++ b/tests/mcp-regressions.test.mjs @@ -0,0 +1,249 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import * as fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; +import { createMcpService } from "../docker/setup/mcp.mjs"; +import { applyMcpToml, parseMcpToml } from "../docker/setup/mcp/toml.mjs"; +import { readProviderMcp, writeProviderMcpEntry } from "../docker/setup/mcp/adapters.mjs"; +import { mcpStorePathFor, validateMcpServer } from "../docker/setup/mcp/store.mjs"; +const server = (name, extra = {}) => ({ name, type: "local", command: ["server"], env: {}, ...extra }); +async function fixture(t, extraEnv = {}) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "t3-mcp-regression-")); + t.after(() => fs.rm(root, { recursive: true, force: true })); + const home = path.join(root, "home"), baseDir = path.join(root, "state"); + const env = { HOME: home, ...extraEnv }; + return { root, home, baseDir, env, svc: createMcpService({ home, baseDir, env, io: fs }) }; +} + +test("Codex edits preserve nested servers, multiline strings and unrelated array tables", () => { + const unrelated = '# skill policy\n[[skills.config]]\npath = "/tmp/skill"\nenabled = false\n'; + const raw = '# model\nmodel = "test"\n\n[mcp_servers.probe]\ncommand = "node"\nargs = [\n "a,b",\n "\\\\u0041",\n]\n[mcp_servers.probe.env]\nTOKEN = """first\n[mcp_servers.fake]\nlast"""\n\n' + unrelated; + const parsed = parseMcpToml(raw); + assert.equal(parsed.size, 1); + assert.deepEqual(parsed.get("probe").args, ["a,b", "\\u0041"]); + const removed = applyMcpToml(raw, "probe", null); + assert.ok(removed.includes(unrelated)); + assert.equal(parseMcpToml(removed).size, 0); + assert.ok(removed.startsWith('# model\nmodel = "test"')); + const updated = applyMcpToml(raw, "probe", { command: "other", env: { TOKEN: "a,b\nsecond", EMPTY: "" } }); + assert.deepEqual(parseMcpToml(updated).get("probe"), { command: "other", env: { TOKEN: "a,b\nsecond", EMPTY: "" } }); + assert.ok(updated.includes(unrelated)); +}); + +test("Codex rejects malformed documents before writing and hides parser values", async (t) => { + const f = await fixture(t), file = path.join(f.home, ".codex/config.toml"); + await fs.mkdir(path.dirname(file), { recursive: true }); + const raw = '[mcp_servers.probe]\ncommand="secret-credential\n'; + await fs.writeFile(file, raw); + await assert.rejects(writeProviderMcpEntry(fs, "codex", f, "probe", null), (e) => e.code === "unsupported" && !e.message.includes("secret-credential")); + assert.equal(await fs.readFile(file, "utf8"), raw); +}); + +test("native OpenCode 2 dictionaries are inventoried, updated and unlinked in place", async (t) => { + const f = await fixture(t), file = path.join(f.home, ".config/opencode/opencode.jsonc"); + await fs.mkdir(path.dirname(file), { recursive: true }); + await fs.writeFile(file, '{\n// retained\n"mcp":{"timeout":10000,"servers":{"probe":{"type":"local","command":["server"],"disabled":true},"own":{"type":"remote","url":"https://example.test/mcp"}}}\n}\n'); + const created = await f.svc.create(server("probe", { enabled: false })); + assert.equal((await f.svc.list("opencode")).shared[0].linked, true); + const saved = await f.svc.save("probe", created.revision, server("probe")); + await f.svc.setLink("opencode", "probe", true, saved.revision, (await f.svc.list("opencode")).revision); + assert.equal((await f.svc.list("opencode")).shared[0].linked, true); + assert.match(await fs.readFile(file, "utf8"), /"disabled": false/); + await f.svc.setLink("opencode", "probe", false, saved.revision, (await f.svc.list("opencode")).revision); + const read = await readProviderMcp(fs, "opencode", f); + assert.deepEqual(read.servers.map((r) => r.name), ["own"]); + const raw = await fs.readFile(file, "utf8"); + assert.match(raw, /retained/); assert.match(raw, /"timeout":\s*10000/); +}); + +test("concurrent links retain both entries, also through a provider-file symlink", async (t) => { + for (const provider of ["claude", "codex", "opencode", "cursor"]) { + const f = await fixture(t); + const file = provider === "claude" ? path.join(f.home, ".claude.json") : provider === "codex" ? path.join(f.home, ".codex/config.toml") : provider === "cursor" ? path.join(f.home, ".cursor/mcp.json") : path.join(f.home, ".config/opencode/opencode.json"); + await fs.mkdir(path.dirname(file), { recursive: true }); + const target = path.join(f.root, "persistent-config"); + await fs.writeFile(target, provider === "codex" ? '# retained\nmodel="test"\n' : '{"retained":true}\n', { mode: 0o600 }); + await fs.symlink(target, file); + await f.svc.create(server("first")); const second = await f.svc.create(server("second")); + await Promise.all([f.svc.setLink(provider, "first", true, second.revision), f.svc.setLink(provider, "second", true, second.revision)]); + assert.equal((await fs.lstat(file)).isSymbolicLink(), true, provider); + const read = await readProviderMcp(fs, provider, f); + assert.deepEqual(read.servers.map((r) => r.name).sort(), ["first", "second"], provider); + assert.match(await fs.readFile(target, "utf8"), /retained/); + const backups = (await fs.readdir(f.root)).filter(n => n.includes('.t3-mcp-backup.')); + assert.equal(backups.length, 2); + for (const backup of backups) assert.equal((await fs.stat(path.join(f.root, backup))).mode & 0o777, 0o600); + } +}); + +test("provider revisions refuse external edits and retargeted symlinks", async (t) => { + const f = await fixture(t), created = await f.svc.create(server("probe")); + const read = await f.svc.list("claude"); + await fs.mkdir(f.home, { recursive: true }); + const file = path.join(f.home, ".claude.json"); + await fs.writeFile(file, '{"retained":true}'); + await assert.rejects(f.svc.setLink("claude", "probe", true, created.revision, read.revision), { code: "conflict" }); + const a = path.join(f.root, 'a'), b = path.join(f.root, 'b'); + await fs.writeFile(a, '{}'); await fs.writeFile(b, '{}'); await fs.rm(file); await fs.symlink(a, file); + const revision = (await f.svc.list("claude")).revision; + await fs.rm(file); await fs.symlink(b, file); + await assert.rejects(f.svc.setLink("claude", "probe", true, created.revision, revision), { code: "conflict" }); + assert.equal(await fs.readFile(b, 'utf8'), '{}'); +}); + +test("shared disable stays visibly divergent until unlink and removal requires unlink", async (t) => { + const f = await fixture(t), created = await f.svc.create(server("probe")); + await f.svc.setLink("claude", "probe", true, created.revision); + const saved = await f.svc.save("probe", created.revision, server("probe", { enabled: false })); + const state = (await f.svc.status({ refresh: true })).providers.find(p => p.provider === 'claude'); + assert.deepEqual(state.diverged, ['probe']); assert.deepEqual(state.unlinked, []); + await assert.rejects(f.svc.remove("probe", saved.revision), { code: 'conflict' }); + await f.svc.setLink("claude", "probe", false, saved.revision, state.revision); + assert.equal((await f.svc.remove("probe", saved.revision)).ok, true); +}); + +test("store limits reject writes and leave a usable previous store", async (t) => { + const f = await fixture(t), original = await f.svc.create(server("first")); + const large = Object.fromEntries(Array.from({length:33},(_,i)=>['KEY_'+i,'x'.repeat(8192)])); + await assert.rejects(f.svc.create(server('large', { env: large })), { code: 'too_large' }); + await assert.rejects(f.svc.save('first', original.revision, server('first', { env: large })), { code: 'too_large' }); + assert.equal((await f.svc.read('first')).revision, original.revision); + const entries = Array.from({length:128},(_,i)=>server('s'+i)); + await fs.writeFile(mcpStorePathFor(f.baseDir), JSON.stringify({servers:entries})); + await assert.rejects(f.svc.create(server('extra')), { code: 'too_large' }); + assert.equal((await f.svc.status({refresh:true})).shared.count, 128); +}); + +test("URLs, headers, unsupported providers and extra provider options are honest", async (t) => { + const f = await fixture(t); + for (const url of ['https://user:password@example.test', 'https://', 'https://host\n.test', 'https://[broken']) assert.equal(validateMcpServer({name:'remote',type:'remote',url}).ok, false); + assert.equal(validateMcpServer({name:'remote',type:'remote',url:'https://example.test',headers:{Authorization:'a',authorization:'b'}}).ok, false); + await f.svc.create({name:'remote',type:'remote',url:'https://example.test/private-secret-path?token=secret',headers:{Authorization:'secret'}}); + const c = await f.svc.create(server('probe')); + await f.svc.setLink('codex','probe',true,c.revision); + const file = path.join(f.home,'.codex/config.toml'); + await fs.writeFile(file, '[mcp_servers.probe]\ncommand="server"\ncwd="/different"\n'); + const status = await f.svc.status({refresh:true}); + assert.equal(status.shared.servers.find(s=>s.name==='remote').url, 'https://example.test'); + assert.equal(JSON.stringify(status).includes('private-secret-path'), false); + assert.equal(JSON.stringify(status).includes('token=secret'), false); + assert.deepEqual(status.providers.find(p=>p.provider==='codex').diverged,['probe']); + await assert.rejects(f.svc.setLink('antigravity','probe',true,c.revision), {code:'unsupported'}); +}); + +test("Claude override roots are honored and duplicate JSON keys cannot be changed", async (t) => { + const f = await fixture(t, {CLAUDE_CONFIG_DIR:'custom-claude'}), c = await f.svc.create(server('probe')); + await f.svc.setLink('claude','probe',true,c.revision); + const file=path.join(f.home,'custom-claude/.claude.json'); + assert.equal(JSON.parse(await fs.readFile(file,'utf8')).mcpServers.probe.command,'server'); + const raw='{"mcpServers":{},"mcpServers":{"probe":{"command":"existing"}}}'; + await fs.writeFile(file,raw); + await assert.rejects(f.svc.setLink('claude','probe',false,c.revision),{code:'unsupported'}); + assert.equal(await fs.readFile(file,'utf8'),raw); +}); + +test("mixed transport fields differ and malformed native maps are not recognized", async (t) => { + for (const provider of ['claude', 'codex', 'opencode', 'cursor']) { + const f = await fixture(t); + const created = await f.svc.create({name:'probe',type:'remote',url:'https://example.test/mcp',headers:{}}); + const mixed = provider === 'codex' ? {url:'https://example.test/mcp',command:'extra'} + : provider === 'opencode' ? {type:'remote',url:'https://example.test/mcp',command:['extra']} + : {type:'http',url:'https://example.test/mcp',command:'extra'}; + await writeProviderMcpEntry(fs,provider,f,'probe',mixed); + assert.equal((await f.svc.list(provider)).shared[0].diverged,true,provider); + await f.svc.setLink(provider,'probe',true,created.revision,(await f.svc.list(provider)).revision); + assert.equal((await f.svc.list(provider)).shared[0].linked,true,provider); + const headerKey = provider === 'codex' ? 'http_headers' : 'headers'; + await writeProviderMcpEntry(fs,provider,f,'probe',{...mixed,[headerKey]:'not-an-object'}); + assert.equal((await readProviderMcp(fs,provider,f)).servers[0].canonical,null,provider); + const local = provider === 'opencode' ? {type:'local',command:['server'],environment:0} + : {command:'server',env:0}; + await writeProviderMcpEntry(fs,provider,f,'probe',local); + assert.equal((await readProviderMcp(fs,provider,f)).servers[0].canonical,null,provider); + if (['claude','cursor'].includes(provider)) { + await writeProviderMcpEntry(fs,provider,f,'probe',{command:'server',type:'http'}); + assert.equal((await readProviderMcp(fs,provider,f)).servers[0].canonical,null,provider); + } + } +}); + +test("the browser CLI and console can alternate without duplicate TOML or ignored JSONC/V2 entries", async (t) => { + const f = await fixture(t), bin = path.join(f.root, 'bin'); + await fs.mkdir(bin); + for (const name of ['playwright-mcp', 'managed']) await fs.writeFile(path.join(bin, name), '#!/bin/sh\nexit 0\n', {mode:0o755}); + const resolver = path.join(f.root, 'resolve.mjs'); + await fs.writeFile(resolver, `console.log(${JSON.stringify(path.join(bin, 'managed'))});`); + const adapters = new URL('../docker/setup/mcp/adapters.mjs', import.meta.url).href; + const command = fileURLToPath(new URL('../docker/bin/t3-browser-mcp', import.meta.url)); + const env = {...process.env,...f.env,PATH:bin+path.delimiter+process.env.PATH,T3_INFRA_NODE:process.execPath,T3_PROVIDER_CLI:resolver,T3_MCP_ADAPTERS_MODULE:adapters,CODEX_HOME:path.join(f.home,'.codex'),XDG_CONFIG_HOME:path.join(f.home,'.config')}; + delete env.CLAUDE_CONFIG_DIR; delete env.OPENCODE_CONFIG_DIR; + const ocFile=path.join(f.home,'.config/opencode/opencode.jsonc'); + await fs.mkdir(path.dirname(ocFile),{recursive:true});await fs.writeFile(ocFile,'{\n// keep\n"mcp":{"servers":{}}\n}\n'); + const svc=createMcpService({home:f.home,baseDir:f.baseDir,env,io:fs}); + const created=await svc.create(server('playwright')); + for(const provider of ['codex','opencode']) { + await svc.setLink(provider,'playwright',true,created.revision); + for(let i=0;i<2;i++) await promisify(execFile)('bash',[command,'--harness',provider],{env,timeout:15000}); + assert.equal((await readProviderMcp(fs,provider,{home:f.home,env})).servers.filter(r=>r.name==='playwright').length,1); + await svc.setLink(provider,'playwright',true,created.revision); + await promisify(execFile)('bash',[command,'--harness',provider,'--remove'],{env,timeout:15000}); + assert.equal((await svc.list(provider)).shared[0].linked,false); + } + assert.match(await fs.readFile(ocFile,'utf8'),/keep/); +}); + + +test("Claude HTTP needs an explicit transport while Cursor accepts its URL default", async (t) => { + const f = await fixture(t); + const url = 'https://example.test/mcp'; + await f.svc.create({name:'probe',type:'remote',url,headers:{}}); + await writeProviderMcpEntry(fs,'claude',f,'probe',{url}); + assert.equal((await f.svc.list('claude')).shared[0].diverged,true); + assert.equal((await readProviderMcp(fs,'claude',f)).servers[0].canonical,null); + await writeProviderMcpEntry(fs,'cursor',f,'probe',{url}); + assert.equal((await f.svc.list('cursor')).shared[0].linked,true); + await writeProviderMcpEntry(fs,'claude',f,'probe',{type:'streamable-http',url}); + assert.equal((await f.svc.list('claude')).shared[0].linked,true); +}); + +test("OpenCode V2 global options without servers retain their shape when linking", async (t) => { + for (const options of [{codemode:false},{protocol:'legacy'},{timeout:{startup:30000}}]) { + const f = await fixture(t), file = path.join(f.home,'.config/opencode/opencode.jsonc'); + await fs.mkdir(path.dirname(file),{recursive:true}); + await fs.writeFile(file,JSON.stringify({mcp:options})); + const created = await f.svc.create(server('probe')); + assert.deepEqual((await readProviderMcp(fs,'opencode',f)).servers,[]); + await f.svc.setLink('opencode','probe',true,created.revision); + assert.deepEqual(JSON.parse(await fs.readFile(file,'utf8')).mcp,{...options,servers:{probe:{type:'local',command:['server'],disabled:false}}}); + assert.equal((await f.svc.list('opencode')).shared[0].linked,true); + } + // V1 servers can use these names; transport-bearing objects are entries. + const f = await fixture(t); + for (const name of ['servers','timeout','codemode','protocol']) await writeProviderMcpEntry(fs,'opencode',f,name,{type:'local',command:['server']}); + assert.deepEqual((await readProviderMcp(fs,'opencode',f)).servers.map(s=>s.name),['servers','timeout','codemode','protocol']); +}); + +test("OpenCode inventory distinguishes V1 enabled and V2 disabled flags", async (t) => { + for (const v2 of [false, true]) for (const enabled of [false, true]) { + const f = await fixture(t), file = path.join(f.home,'.config/opencode/opencode.json'); + await fs.mkdir(path.dirname(file),{recursive:true}); + const wrongFlag = v2 ? {enabled} : {disabled:!enabled}; + const entry = {type:'local',command:['server'],...wrongFlag}; + await fs.writeFile(file,JSON.stringify({mcp:v2 ? {servers:{probe:entry}} : {probe:entry}})); + const created = await f.svc.create(server('probe',{enabled})); + assert.equal((await f.svc.list('opencode')).shared[0].linked,false); + assert.equal((await f.svc.list('opencode')).shared[0].diverged,true); + const native = await readProviderMcp(fs,'opencode',f); + assert.equal(native.servers[0].extra,true); + assert.equal(native.servers[0].canonical.enabled,true,'the other format’s flag must not change native state'); + await f.svc.setLink('opencode','probe',true,created.revision,native.revision); + const repaired = await readProviderMcp(fs,'opencode',f); + assert.equal((await f.svc.list('opencode')).shared[0].linked,true); + assert.equal(repaired.servers[0].canonical.enabled,enabled); + assert.deepEqual(repaired.servers[0].entry,{type:'local',command:['server'],...(v2 ? {disabled:!enabled} : {enabled})}); + } +}); diff --git a/tests/mcp.test.mjs b/tests/mcp.test.mjs new file mode 100644 index 0000000..838b789 --- /dev/null +++ b/tests/mcp.test.mjs @@ -0,0 +1,174 @@ +// Shared MCP servers: store validation, TOML preservation, provider +// translation and link-state derivation. +import assert from "node:assert/strict"; +import * as fs from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { readFileSync } from "node:fs"; +import vm from "node:vm"; +import { validateMcpServer, readMcpStore, createMcpServer, saveMcpServer, removeMcpServer, redactMcpServer, mcpRevision } from "../docker/setup/mcp/store.mjs"; +import { parseMcpToml, applyMcpToml } from "../docker/setup/mcp/toml.mjs"; +import { toClaudeEntry, fromClaudeEntry, toCodexEntry, fromCodexEntry, toOpenCodeEntry, fromOpenCodeEntry, toCursorEntry, fromCursorEntry, readProviderMcp, writeProviderMcpEntry } from "../docker/setup/mcp/adapters.mjs"; +import { createMcpService } from "../docker/setup/mcp.mjs"; + +async function fixture(t) { + const root = await fs.mkdtemp(path.join(tmpdir(), "t3-mcp-")); + t.after(() => fs.rm(root, { recursive: true, force: true })); + return { root, home: path.join(root, "home"), baseDir: path.join(root, ".t3") }; +} +const svcFor = (f, extraEnv = {}) => createMcpService({ home: f.home, env: { HOME: f.home, ...extraEnv }, io: fs, baseDir: f.baseDir }); + +test("validation rejects bad names, commands, urls and secrets", () => { + assert.equal(validateMcpServer(null).ok, false); + assert.equal(validateMcpServer({ name: "Bad Name", type: "local", command: ["x"] }).ok, false); + assert.equal(validateMcpServer({ name: "ok", type: "local", command: [] }).ok, false); + assert.equal(validateMcpServer({ name: "ok", type: "local", command: ["x"], env: { "bad-key": "v" } }).ok, false); + assert.equal(validateMcpServer({ name: "ok", type: "remote", url: "ftp://x" }).ok, false); + assert.equal(validateMcpServer({ name: "ok", type: "remote", url: "https://x", headers: { bad: "a\nb" } }).ok, false); + const local = validateMcpServer({ name: "playwright", type: "local", command: ["playwright-mcp", "--headless"], env: { A: "1" } }); + assert.equal(local.ok, true); + assert.deepEqual(local.server, { name: "playwright", type: "local", enabled: true, command: ["playwright-mcp", "--headless"], env: { A: "1" } }); + const remote = validateMcpServer({ name: "notion", type: "remote", url: "https://mcp.notion.com/mcp", enabled: false }); + assert.equal(remote.ok, true); + assert.deepEqual(remote.server.headers, {}); +}); + +test("the shared store round-trips with revisions and conflicts", async (t) => { + const f = await fixture(t); + const svc = svcFor(f); + assert.equal((await svc.status()).shared.present, false); + const created = await svc.create({ name: "playwright", type: "local", command: ["playwright-mcp"], env: {} }); + assert.equal(created.ok, true); + await assert.rejects(svc.create({ name: "playwright", type: "local", command: ["other"], env: {} }), { code: "conflict" }); + const read = await svc.read("playwright"); + assert.deepEqual(read.server.command, ["playwright-mcp"]); + const saved = await svc.save("playwright", read.revision, { name: "playwright", type: "local", command: ["playwright-mcp", "--headless"], env: {} }); + assert.equal(saved.ok, true); + await assert.rejects(svc.save("playwright", read.revision, { name: "playwright", type: "local", command: ["stale"], env: {} }), { code: "conflict" }); + await assert.rejects(svc.remove("playwright", "stale"), { code: "conflict" }); + assert.equal((await svc.remove("playwright", saved.revision)).ok, true); + assert.equal((await svc.status({ refresh: true })).shared.present, false); + await assert.rejects(svc.read("playwright"), { code: "missing" }); +}); + +test("status never carries secrets", async (t) => { + const f = await fixture(t); + const svc = svcFor(f); + await svc.create({ name: "db", type: "remote", url: "https://db.example/mcp", headers: { Authorization: "Bearer secret" } }); + const status = await svc.status({ refresh: true }); + assert.equal(JSON.stringify(status).includes("Bearer secret"), false); + assert.equal(status.shared.servers[0].secretCount, 1); + assert.deepEqual(status.shared.servers[0].headerKeys, ["Authorization"]); + const redacted = redactMcpServer({ name: "db", type: "remote", enabled: true, url: "https://db.example/mcp", headers: { Authorization: "Bearer secret" } }); + assert.equal(redacted.headers, undefined); + assert.equal(redacted.headerCount, 1); +}); + +test("codex TOML edits preserve unrelated sections", () => { + const original = '# comment\nmodel = "o4-mini"\n\n[mcp_servers.playwright]\ncommand = "playwright-mcp"\nargs = ["--headless"]\n\n[other]\nkey = 1\n'; + const parsed = parseMcpToml(original); + assert.deepEqual(parsed.get("playwright"), { command: "playwright-mcp", args: ["--headless"] }); + const updated = applyMcpToml(original, "notion", { url: "https://mcp.notion.com/mcp", enabled: false }); + assert.match(updated, /model = "o4-mini"/); + assert.match(updated, /\[mcp_servers\.playwright\]/); + assert.deepEqual(parseMcpToml(updated).get("notion"), { url: "https://mcp.notion.com/mcp", enabled: false }); + assert.match(updated, /\[other\]/); + const removed = applyMcpToml(updated, "notion", null); + assert.doesNotMatch(removed, /notion/); + assert.match(removed, /playwright-mcp/); + assert.throws(() => parseMcpToml("[mcp_servers.a]\ncommand = 1\n[mcp_servers.a]\ncommand = 2\n"), /safely edited/); +}); + +test("every supported provider translates a local and a remote server", () => { + const local = { name: "playwright", type: "local", enabled: true, command: ["playwright-mcp", "--headless"], env: { FOO: "bar" } }; + const remote = { name: "notion", type: "remote", enabled: true, url: "https://mcp.notion.com/mcp", headers: { Authorization: "Bearer x" } }; + assert.deepEqual(toClaudeEntry(local), { command: "playwright-mcp", args: ["--headless"], env: { FOO: "bar" } }); + assert.deepEqual(fromClaudeEntry("playwright", toClaudeEntry(local)), local); + assert.deepEqual(fromClaudeEntry("notion", toClaudeEntry(remote)), remote); + assert.deepEqual(toCodexEntry(remote), { url: "https://mcp.notion.com/mcp", http_headers: { Authorization: "Bearer x" } }); + assert.deepEqual(fromCodexEntry("notion", toCodexEntry(remote)), remote); + assert.deepEqual(toOpenCodeEntry(local), { type: "local", command: ["playwright-mcp", "--headless"], environment: { FOO: "bar" }, enabled: true }); + assert.deepEqual(fromOpenCodeEntry("playwright", toOpenCodeEntry(local)), local); + assert.deepEqual(fromOpenCodeEntry("notion", toOpenCodeEntry(remote)), remote); + assert.deepEqual(toCursorEntry(local), toClaudeEntry(local)); + assert.deepEqual(fromCursorEntry("playwright", toCursorEntry(local)), local); + assert.equal(fromClaudeEntry("x", { nonsense: true }), null); +}); + +test("links write provider files and derive state by comparison", async (t) => { + const f = await fixture(t); + const svc = svcFor(f, { CODEX_HOME: path.join(f.home, ".codex"), XDG_CONFIG_HOME: path.join(f.home, ".config") }); + const created = await svc.create({ name: "playwright", type: "local", command: ["playwright-mcp", "--headless"], env: {} }); + const revision = created.revision; + for (const provider of ["claude", "codex", "opencode", "cursor"]) { + await svc.setLink(provider, "playwright", true, revision); + } + const status = await svc.status({ refresh: true }); + for (const provider of ["claude", "codex", "opencode", "cursor"]) { + const row = status.providers.find((entry) => entry.provider === provider); + assert.deepEqual(row.linked, ["playwright"], provider); + assert.deepEqual(row.diverged, [], provider); + } + // A provider-side edit shows as diverged, not linked. + await svc.setLink("claude", "playwright", false, revision); + const unlinked = (await svc.status({ refresh: true })).providers.find((entry) => entry.provider === "claude"); + assert.deepEqual(unlinked.linked, []); + assert.deepEqual(unlinked.unlinked, ["playwright"]); + const grok = (await svc.status()).providers.find((entry) => entry.provider === "grok"); + assert.equal(grok.supported, false); + assert.match(grok.error, /not implemented/); +}); + +test("disabled shared servers link only where the provider has an enabled flag", async (t) => { + const f = await fixture(t); + const svc = svcFor(f); + const created = await svc.create({ name: "quiet", type: "local", enabled: false, command: ["cmd"], env: {} }); + await svc.setLink("codex", "quiet", true, created.revision); + await svc.setLink("opencode", "quiet", true, created.revision); + assert.equal((await svc.list("codex")).shared[0].linked, true); + await assert.rejects(svc.setLink("claude", "quiet", true, created.revision), { code: "unsupported" }); + await assert.rejects(svc.setLink("grok", "quiet", true, created.revision), /not implemented/); +}); + +test("unsupported and unknown MCP shapes stay in place with a warning", async (t) => { + const f = await fixture(t); + const svc = svcFor(f); + await svc.create({ name: "playwright", type: "local", command: ["playwright-mcp"], env: {} }); + await fs.mkdir(path.join(f.home, ".codex"), { recursive: true }); + await fs.writeFile(path.join(f.home, ".codex", "config.toml"), '[mcp_servers.weird]\nstartup_timeout_ms = 5\n'); + const status = await svc.status({ refresh: true }); + const codex = status.providers.find((entry) => entry.provider === "codex"); + assert.match(codex.warning ?? "", /unsupported shape/); + assert.deepEqual(codex.own, ["weird"]); + const list = await svc.list("codex"); + assert.deepEqual(list.unsupported, ["weird"]); + await assert.rejects(svc.list("nope"), { code: "invalid" }); + await assert.rejects(writeProviderMcpEntry(fs, "codex", { home: f.home, env: {} }, "Bad Name", {}), /Invalid MCP server name/); +}); + +test("provider files preserve unrelated keys and survive JSONC", async (t) => { + const f = await fixture(t); + const env = { HOME: f.home, XDG_CONFIG_HOME: path.join(f.home, ".config"), OPENCODE_CONFIG_DIR: path.join(f.home, ".config", "opencode") }; + await fs.mkdir(path.join(f.home, ".config", "opencode"), { recursive: true }); + await fs.writeFile(path.join(f.home, ".config", "opencode", "opencode.jsonc"), '{\n// comment\n"theme": "dark"\n}\n'); + const svc = createMcpService({ home: f.home, env, io: fs, baseDir: f.baseDir }); + const created = await svc.create({ name: "playwright", type: "local", command: ["playwright-mcp"], env: {} }); + await svc.setLink("opencode", "playwright", true, created.revision); + const raw = await fs.readFile(path.join(f.home, ".config", "opencode", "opencode.jsonc"), "utf8"); + assert.match(raw, /comment/); + assert.match(raw, /"theme": "dark"/); + assert.match(raw, /playwright/); + const read = await readProviderMcp(fs, "opencode", { home: f.home, env }); + assert.equal(read.servers.find((row) => row.name === "playwright").canonical.command[0], "playwright-mcp"); +}); + +const ctx = vm.createContext({ URL }); +vm.runInContext(`${readFileSync(new URL("../docker/setup/client/model.js", import.meta.url), "utf8")}\nthis.M = T3Model;`, ctx); +const M = ctx.M; + +test("MCP view model reports shared counts and read errors", () => { + assert.equal(M.mcpView({context:{mcp:{shared:{servers:[{},{}]}}}}).count, 2); + assert.equal(M.mcpView({context:{mcp:{ok:false,shared:{error:"Cannot read store"}}}}).error, "Cannot read store"); + assert.equal(M.mcpView({}).count, 0); +}); diff --git a/tests/persist-credentials.test.sh b/tests/persist-credentials.test.sh index 2b7e2af..388697f 100644 --- a/tests/persist-credentials.test.sh +++ b/tests/persist-credentials.test.sh @@ -52,6 +52,7 @@ printf '%s' 'saved Muse session' > "$T3_HOME/.local/share/muse/session-test" mkdir -p "$T3_HOME/.local/share/opencode" printf '%s' 'OpenCode credential database' > "$T3_HOME/.local/share/opencode/opencode.db" printf '%s' 'OpenCode pending journal' > "$T3_HOME/.local/share/opencode/opencode.db-wal" +printf '%s' '{"mcpServers":{"probe":{"command":"server"}}}' > "$T3_HOME/.claude.json" mkdir -p "$T3_HOME/.agents/skills/local/references" printf '%s' 'Shared skill instructions' > "$T3_HOME/.agents/skills/local/SKILL.md" printf '%s' 'Supporting reference' > "$T3_HOME/.agents/skills/local/references/guide.md" @@ -102,6 +103,7 @@ is "Muse credentials survive a recreate" '{"providers":{"meta":{"api_key":"muse- is "Muse sessions survive a recreate" 'saved Muse session' "$(cat "$T3_HOME/.local/share/muse/session-test")" is "OpenCode credential database survives a recreate" 'OpenCode credential database' "$(cat "$T3_HOME/.local/share/opencode/opencode.db")" is "OpenCode database sidecars survive a recreate" 'OpenCode pending journal' "$(cat "$T3_HOME/.local/share/opencode/opencode.db-wal")" +is "Claude global MCP config survives a recreate" '{"mcpServers":{"probe":{"command":"server"}}}' "$(cat "$T3_HOME/.claude.json")" is "Antigravity CLI skills survive a recreate" 'Antigravity CLI skill' "$(cat "$T3_HOME/.gemini/antigravity-cli/skills/cli-check/SKILL.md")" is "Antigravity skills survive a recreate" 'Antigravity skill' "$(cat "$T3_HOME/.gemini/config/skills/review/SKILL.md")" is "shared skills survive a recreate" 'Shared skill instructions' "$(cat "$T3_HOME/.agents/skills/local/SKILL.md")" @@ -125,6 +127,14 @@ persist_agent_credentials [ ! -L "$T3_HOME/.agents/skills" ] && ok "a mounted .agents parent is respected" || no "a mounted .agents parent is respected" is "mounted shared files remain intact" 'Host skill' "$(cat "$T3_HOME/.agents/skills/mounted/SKILL.md")" +# An absent Claude config must not become a broken persistence link. +rm -rf "$T3_HOME" "$T3CODE_HOME" +mkdir -p "$T3_HOME" "$T3CODE_HOME" +MOUNTED="" +persist_agent_credentials +is "a fresh Claude config starts as valid JSON" '{}' "$(cat "$T3_HOME/.claude.json")" +is "a fresh Claude config is private" '600' "$(stat -c '%a' "$STORE/.claude.json")" + # Turned off: nothing moves. rm -rf "$T3_HOME" "$T3CODE_HOME" mkdir -p "$T3_HOME" "$T3CODE_HOME" diff --git a/tests/t3-client.test.mjs b/tests/t3-client.test.mjs index 570b88a..05ac6ae 100644 --- a/tests/t3-client.test.mjs +++ b/tests/t3-client.test.mjs @@ -100,7 +100,8 @@ test("Setup's commands are found in T3's palette the way T3 finds its own", () = assert.equal(found(">open setup"), "setup"); // Nothing typed, or nothing that answers: none. for (const q of ["", " ", ">", "settings", "new thread", "setup ports zzz", "theme"]) assert.equal(found(q), "", q); - for (const item of B.PALETTE_ITEMS) assert.ok(item.route === null || /^[a-z]+$/.test(item.route), item.id); + assert.equal(found('mcp').split(',')[0], 'mcp'); + for (const item of B.PALETTE_ITEMS) assert.ok(item.route === null || /^[a-z]+(?:\/mcp)?$/.test(item.route), item.id); }); test("arrow keys walk Setup's rows and T3's as one list that wraps", () => {