@samyx/preview-stacks-ui 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{ActionButton.vue_vue_type_script_setup_true_lang-DBfqkgMV.js → ActionButton.vue_vue_type_script_setup_true_lang-pLJQfFvY.js} +2 -2
- package/dist/assets/{ActionButton.vue_vue_type_script_setup_true_lang-DBfqkgMV.js.map → ActionButton.vue_vue_type_script_setup_true_lang-pLJQfFvY.js.map} +1 -1
- package/dist/assets/{AxesTab-Bzm6UBFL.js → AxesTab-Bb8UjmaR.js} +2 -2
- package/dist/assets/{AxesTab-Bzm6UBFL.js.map → AxesTab-Bb8UjmaR.js.map} +1 -1
- package/dist/assets/{ConfigTab-B5Mkn_Xr.js → ConfigTab-DLjvLYzj.js} +2 -2
- package/dist/assets/{ConfigTab-B5Mkn_Xr.js.map → ConfigTab-DLjvLYzj.js.map} +1 -1
- package/dist/assets/{ConfigView-BMINdC1p.js → ConfigView-BO_yZHja.js} +2 -2
- package/dist/assets/{ConfigView-BMINdC1p.js.map → ConfigView-BO_yZHja.js.map} +1 -1
- package/dist/assets/{ConflictNote.vue_vue_type_script_setup_true_lang-DcogOjwN.js → ConflictNote.vue_vue_type_script_setup_true_lang-Bjozh4Jr.js} +2 -2
- package/dist/assets/{ConflictNote.vue_vue_type_script_setup_true_lang-DcogOjwN.js.map → ConflictNote.vue_vue_type_script_setup_true_lang-Bjozh4Jr.js.map} +1 -1
- package/dist/assets/ControlView-D-akBm65.js +2 -0
- package/dist/assets/ControlView-D-akBm65.js.map +1 -0
- package/dist/assets/{DangerTab-CLAU7ElD.js → DangerTab-DFtbEcU-.js} +2 -2
- package/dist/assets/{DangerTab-CLAU7ElD.js.map → DangerTab-DFtbEcU-.js.map} +1 -1
- package/dist/assets/{DashboardView-C7Ofmm_R.js → DashboardView-BWa1V1EI.js} +2 -2
- package/dist/assets/{DashboardView-C7Ofmm_R.js.map → DashboardView-BWa1V1EI.js.map} +1 -1
- package/dist/assets/{DeploymentDetailView-BnEVV3jQ.js → DeploymentDetailView-Cs1sMvRs.js} +2 -2
- package/dist/assets/{DeploymentDetailView-BnEVV3jQ.js.map → DeploymentDetailView-Cs1sMvRs.js.map} +1 -1
- package/dist/assets/{DeploymentsView-BQUywd2O.js → DeploymentsView-CD84qmqw.js} +2 -2
- package/dist/assets/{DeploymentsView-BQUywd2O.js.map → DeploymentsView-CD84qmqw.js.map} +1 -1
- package/dist/assets/{DialogTrigger-K-5IAbpM.js → DialogTrigger-BKNFZDUU.js} +2 -2
- package/dist/assets/{DialogTrigger-K-5IAbpM.js.map → DialogTrigger-BKNFZDUU.js.map} +1 -1
- package/dist/assets/{EquivalentCommand-Bs9vCxV-.js → EquivalentCommand-DSNG3Ig1.js} +2 -2
- package/dist/assets/{EquivalentCommand-Bs9vCxV-.js.map → EquivalentCommand-DSNG3Ig1.js.map} +1 -1
- package/dist/assets/{ErrorNote.vue_vue_type_script_setup_true_lang-DrinGQ_J.js → ErrorNote.vue_vue_type_script_setup_true_lang-BaTXNX-q.js} +2 -2
- package/dist/assets/{ErrorNote.vue_vue_type_script_setup_true_lang-DrinGQ_J.js.map → ErrorNote.vue_vue_type_script_setup_true_lang-BaTXNX-q.js.map} +1 -1
- package/dist/assets/{HelpModal.vue_vue_type_script_setup_true_lang-Cj0ZWsrz.js → HelpModal.vue_vue_type_script_setup_true_lang-BhdwErsB.js} +2 -2
- package/dist/assets/{HelpModal.vue_vue_type_script_setup_true_lang-Cj0ZWsrz.js.map → HelpModal.vue_vue_type_script_setup_true_lang-BhdwErsB.js.map} +1 -1
- package/dist/assets/{JobDetailView-ClTF4h1t.js → JobDetailView-DeRVlajk.js} +2 -2
- package/dist/assets/{JobDetailView-ClTF4h1t.js.map → JobDetailView-DeRVlajk.js.map} +1 -1
- package/dist/assets/{JobsView-WFqrBzR6.js → JobsView-BMWq45ys.js} +2 -2
- package/dist/assets/{JobsView-WFqrBzR6.js.map → JobsView-BMWq45ys.js.map} +1 -1
- package/dist/assets/{LogViewer.vue_vue_type_script_setup_true_lang-Btl7zhUv.js → LogViewer.vue_vue_type_script_setup_true_lang-Bj66MDSl.js} +2 -2
- package/dist/assets/{LogViewer.vue_vue_type_script_setup_true_lang-Btl7zhUv.js.map → LogViewer.vue_vue_type_script_setup_true_lang-Bj66MDSl.js.map} +1 -1
- package/dist/assets/{LoginView-B3SRuKRK.js → LoginView-Docai9d6.js} +2 -2
- package/dist/assets/{LoginView-B3SRuKRK.js.map → LoginView-Docai9d6.js.map} +1 -1
- package/dist/assets/{LogsTab-DtcfNtY8.js → LogsTab-Cfauurch.js} +2 -2
- package/dist/assets/{LogsTab-DtcfNtY8.js.map → LogsTab-Cfauurch.js.map} +1 -1
- package/dist/assets/{NotFoundView-1RdXOkoy.js → NotFoundView-D7OvnAVF.js} +2 -2
- package/dist/assets/{NotFoundView-1RdXOkoy.js.map → NotFoundView-D7OvnAVF.js.map} +1 -1
- package/dist/assets/{NotifiersView-diZ_EMYq.js → NotifiersView-DPXRy0Ml.js} +2 -2
- package/dist/assets/{NotifiersView-diZ_EMYq.js.map → NotifiersView-DPXRy0Ml.js.map} +1 -1
- package/dist/assets/{OverviewTab--YuThIvA.js → OverviewTab-BKYsu11o.js} +2 -2
- package/dist/assets/{OverviewTab--YuThIvA.js.map → OverviewTab-BKYsu11o.js.map} +1 -1
- package/dist/assets/{PublicLogsView-gChlTDrp.js → PublicLogsView-DWtP1KLG.js} +2 -2
- package/dist/assets/{PublicLogsView-gChlTDrp.js.map → PublicLogsView-DWtP1KLG.js.map} +1 -1
- package/dist/assets/{RefreshButton.vue_vue_type_script_setup_true_lang-D8LrAn-h.js → RefreshButton.vue_vue_type_script_setup_true_lang-zOMuwEGt.js} +2 -2
- package/dist/assets/{RefreshButton.vue_vue_type_script_setup_true_lang-D8LrAn-h.js.map → RefreshButton.vue_vue_type_script_setup_true_lang-zOMuwEGt.js.map} +1 -1
- package/dist/assets/{RegistriesView-Cich0dgu.js → RegistriesView-CXFrbpKU.js} +2 -2
- package/dist/assets/{RegistriesView-Cich0dgu.js.map → RegistriesView-CXFrbpKU.js.map} +1 -1
- package/dist/assets/{RelativeTime.vue_vue_type_script_setup_true_lang-surhYF0H.js → RelativeTime.vue_vue_type_script_setup_true_lang-DmifGQCq.js} +2 -2
- package/dist/assets/{RelativeTime.vue_vue_type_script_setup_true_lang-surhYF0H.js.map → RelativeTime.vue_vue_type_script_setup_true_lang-DmifGQCq.js.map} +1 -1
- package/dist/assets/{RequiresTab-CxGBOMkm.js → RequiresTab-DzIjJaLx.js} +2 -2
- package/dist/assets/{RequiresTab-CxGBOMkm.js.map → RequiresTab-DzIjJaLx.js.map} +1 -1
- package/dist/assets/{RouteTarget.vue_vue_type_script_setup_true_lang-C3qkKTNl.js → RouteTarget.vue_vue_type_script_setup_true_lang-mCKpGvoV.js} +2 -2
- package/dist/assets/{RouteTarget.vue_vue_type_script_setup_true_lang-C3qkKTNl.js.map → RouteTarget.vue_vue_type_script_setup_true_lang-mCKpGvoV.js.map} +1 -1
- package/dist/assets/{RoutingView-BXOGdZU4.js → RoutingView-BOH2ypa8.js} +2 -2
- package/dist/assets/{RoutingView-BXOGdZU4.js.map → RoutingView-BOH2ypa8.js.map} +1 -1
- package/dist/assets/RuntimeTab-DZ3MZE_K.js +3 -0
- package/dist/assets/RuntimeTab-DZ3MZE_K.js.map +1 -0
- package/dist/assets/{RuntimeTab-CXX0BGYC.css → RuntimeTab-Y2qBJykM.css} +1 -1
- package/dist/assets/{SelectMenu.vue_vue_type_script_setup_true_lang-C7bhFOYU.js → SelectMenu.vue_vue_type_script_setup_true_lang-CEOgxUqO.js} +2 -2
- package/dist/assets/{SelectMenu.vue_vue_type_script_setup_true_lang-C7bhFOYU.js.map → SelectMenu.vue_vue_type_script_setup_true_lang-CEOgxUqO.js.map} +1 -1
- package/dist/assets/{SettingsView-BvscpTdq.js → SettingsView-B0DeldbW.js} +2 -2
- package/dist/assets/{SettingsView-BvscpTdq.js.map → SettingsView-B0DeldbW.js.map} +1 -1
- package/dist/assets/{SkeletonList.vue_vue_type_script_setup_true_lang-CUZB1sxf.js → SkeletonList.vue_vue_type_script_setup_true_lang-Veni2uNM.js} +2 -2
- package/dist/assets/{SkeletonList.vue_vue_type_script_setup_true_lang-CUZB1sxf.js.map → SkeletonList.vue_vue_type_script_setup_true_lang-Veni2uNM.js.map} +1 -1
- package/dist/assets/{SpecDetailView-CAPlEmSg.js → SpecDetailView-CYTsg3dA.js} +2 -2
- package/dist/assets/{SpecDetailView-CAPlEmSg.js.map → SpecDetailView-CYTsg3dA.js.map} +1 -1
- package/dist/assets/{SpecsView-BO0QMGrI.js → SpecsView-DioO-zqq.js} +2 -2
- package/dist/assets/{SpecsView-BO0QMGrI.js.map → SpecsView-DioO-zqq.js.map} +1 -1
- package/dist/assets/{SsoMark.vue_vue_type_script_setup_true_lang-EdIcAiak.js → SsoMark.vue_vue_type_script_setup_true_lang-DYn0Yyze.js} +2 -2
- package/dist/assets/{SsoMark.vue_vue_type_script_setup_true_lang-EdIcAiak.js.map → SsoMark.vue_vue_type_script_setup_true_lang-DYn0Yyze.js.map} +1 -1
- package/dist/assets/{SsoView-2q_nGP-Q.js → SsoView-B_gXfZwB.js} +2 -2
- package/dist/assets/{SsoView-2q_nGP-Q.js.map → SsoView-B_gXfZwB.js.map} +1 -1
- package/dist/assets/{StateBadge.vue_vue_type_script_setup_true_lang-CwCTE4yP.js → StateBadge.vue_vue_type_script_setup_true_lang-DABjKwrU.js} +2 -2
- package/dist/assets/{StateBadge.vue_vue_type_script_setup_true_lang-CwCTE4yP.js.map → StateBadge.vue_vue_type_script_setup_true_lang-DABjKwrU.js.map} +1 -1
- package/dist/assets/{SubmitView-Dxre5OJU.js → SubmitView-D3dRZ8OV.js} +2 -2
- package/dist/assets/{SubmitView-Dxre5OJU.js.map → SubmitView-D3dRZ8OV.js.map} +1 -1
- package/dist/assets/{SwarmView-DtwrF08A.js → SwarmView-_n42sP13.js} +2 -2
- package/dist/assets/{SwarmView-DtwrF08A.js.map → SwarmView-_n42sP13.js.map} +1 -1
- package/dist/assets/{Teleport-bdITH--T.js → Teleport-Cy-A7RhK.js} +2 -2
- package/dist/assets/{Teleport-bdITH--T.js.map → Teleport-Cy-A7RhK.js.map} +1 -1
- package/dist/assets/{TerminalTab-CfcGPa8t.js → TerminalTab-DdbwvvIy.js} +2 -2
- package/dist/assets/{TerminalTab-CfcGPa8t.js.map → TerminalTab-DdbwvvIy.js.map} +1 -1
- package/dist/assets/{UsersView-nIlVItsK.js → UsersView-Dn9cLJCI.js} +2 -2
- package/dist/assets/{UsersView-nIlVItsK.js.map → UsersView-Dn9cLJCI.js.map} +1 -1
- package/dist/assets/{VarEditor.vue_vue_type_script_setup_true_lang-Be64GoXx.js → VarEditor.vue_vue_type_script_setup_true_lang-Dyb8Lmxc.js} +2 -2
- package/dist/assets/{VarEditor.vue_vue_type_script_setup_true_lang-Be64GoXx.js.map → VarEditor.vue_vue_type_script_setup_true_lang-Dyb8Lmxc.js.map} +1 -1
- package/dist/assets/{VarIO-BcNlaWXO.js → VarIO-D7bwBO4U.js} +2 -2
- package/dist/assets/{VarIO-BcNlaWXO.js.map → VarIO-D7bwBO4U.js.map} +1 -1
- package/dist/assets/{VariablesView-CzeCc-0m.js → VariablesView-CH3Ktrp1.js} +2 -2
- package/dist/assets/{VariablesView-CzeCc-0m.js.map → VariablesView-CH3Ktrp1.js.map} +1 -1
- package/dist/assets/{index-IvJmpI3b.js → index-ChQCta5r.js} +34 -29
- package/dist/assets/index-ChQCta5r.js.map +1 -0
- package/dist/assets/{useDeployment-rA-pgUHZ.js → useDeployment-DiChk3VG.js} +2 -2
- package/dist/assets/{useDeployment-rA-pgUHZ.js.map → useDeployment-DiChk3VG.js.map} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/assets/RuntimeTab-_QvQc5F2.js +0 -3
- package/dist/assets/RuntimeTab-_QvQc5F2.js.map +0 -1
- package/dist/assets/index-IvJmpI3b.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VarIO-BcNlaWXO.js","sources":["../../src/composables/useVarFormats.ts","../../src/components/VarIO.vue"],"sourcesContent":["/**\n * `.env`, CSV and TSV for variable lists — the one implementation both variable surfaces share.\n *\n * WHY PURE FUNCTIONS IN A `.ts`. There is no component test harness in this app, so anything that\n * can be wrong in an interesting way has to live somewhere `bun test` can reach it. Everything here\n * is a string in and a value out; the components own the chrome and the policy, and nothing else.\n *\n * THE ROUND-TRIP IS THE CONTRACT. `parseVars(formatVars(pairs, f), f)` returns `pairs` for every\n * value the editors can hold — newlines, quotes, `#`, leading and trailing spaces, backslashes,\n * `=`, the empty string. That is what makes \"copy them out, paste them back\" safe, and it is what\n * `test/useVarFormats.test.ts` spends most of its lines on. Two deliberate exceptions, both tested:\n *\n * - A lone carriage return survives `.env` (escaped as `\\r`) but not CSV/TSV, where CRLF and CR\n * are normalised to LF before parsing — a row terminator cannot also be data without a\n * dialect flag, and no `<input type=\"text\">` can produce one anyway.\n * - `parseVars` collapses a repeated name to its last value, as every dotenv reader does. The\n * import preview shows the result, so the collapse is visible before anything is replaced.\n *\n * WHY CSV AND TSV ALWAYS EXPORT A `name,value` HEADER. A header row is ambiguous with data — a\n * variable really can be called `name` — and guessing wrong either eats a variable or imports a\n * label. Emitting the header makes our own output unambiguous: the header we wrote is the row the\n * detector eats, so `{ k: 'name', v: 'value' }` round-trips. Foreign input whose first row happens\n * to read `name,value` loses that row, which is why the importer previews what it parsed.\n *\n * A NAME MUST BE ONE A SPEC CAN REFERENCE. `VarEditor` only trims what is typed, so `foo-bar` can\n * exist in the editor and be exported — but `${foo-bar}` is not a thing `internal/spec` can\n * interpolate, so importing it would add a row that can never do anything. It comes back as a\n * problem instead, named, where the operator can see it.\n *\n * A MALFORMED LINE IS NEVER DROPPED SILENTLY. Every line that does not become a pair comes back in\n * `problems` with its line number, its text and why — a paste that half-worked without saying so\n * is how a deployment ends up missing the one variable its stack name is built from.\n */\n\nimport type { VarPair } from '../api/types';\n\nexport type VarFormat = 'env' | 'csv' | 'tsv';\n\n/** A line that did not become a pair. Reported, never swallowed. */\nexport type VarProblem = { line: number; text: string; reason: string };\n\nexport type VarParse = { format: VarFormat; pairs: VarPair[]; problems: VarProblem[] };\n\nexport const VAR_FORMATS: VarFormat[] = ['env', 'csv', 'tsv'];\nexport const FORMAT_LABEL: Record<VarFormat, string> = { env: '.env', csv: 'CSV', tsv: 'TSV' };\n\n/** The charset a spec can reference — `internal/spec`'s `${NAME}` regex, not a UI opinion. */\nconst NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n/** Values needing no quotes: bytes that dotenv, compose and a shell all read back identically. */\nconst PLAIN = /^[A-Za-z0-9_@%+=:,./-]+$/;\nconst DELIM: Record<'csv' | 'tsv', string> = { csv: ',', tsv: '\\t' };\nconst HEADER_NAME = new Set(['name', 'key', 'variable', 'var']);\nconst HEADER_VALUE = new Set(['value', 'val']);\n\n// ── export ────────────────────────────────────────────────────────────────────────────────────────\n\n/**\n * Render pairs. `note` becomes `#` comment lines and is only expressible in `.env` — the surface\n * that passes one (host secrets, whose values are withheld) also says so on the page, because CSV\n * has nowhere to put it.\n */\nexport function formatVars(pairs: VarPair[], format: VarFormat, note?: string): string {\n const rows = pairs.filter((e) => e.k);\n if (format === 'env') {\n const head = note ? note.split('\\n').map((l) => `# ${l}`) : [];\n return [...head, ...rows.map((e) => `${e.k}=${envQuote(e.v)}`)].join('\\n') + '\\n';\n }\n const d = DELIM[format];\n return [['name', 'value'], ...rows.map((e) => [e.k, e.v])]\n .map((f) => f.map((v) => delimQuote(v ?? '', d)).join(d))\n .join('\\n') + '\\n';\n}\n\nfunction envQuote(v: string): string {\n // An empty value stays bare: `KEY=` parses back to `''` and reads as \"fill this in\", which is\n // exactly what a withheld secret is.\n if (v === '' || PLAIN.test(v)) return v;\n return `\"${v.replace(/[\\\\\"]/g, '\\\\$&').replace(/\\n/g, '\\\\n').replace(/\\r/g, '\\\\r')}\"`;\n}\n\nfunction delimQuote(v: string, d: string): string {\n const safe =\n v === '' || (!v.includes(d) && !v.includes('\"') && !/[\\n\\r]/.test(v) && v === v.trim());\n return safe ? v : `\"${v.replace(/\"/g, '\"\"')}\"`;\n}\n\n// ── import ────────────────────────────────────────────────────────────────────────────────────────\n\n/** Parse in `format`, or in whatever `detectFormat` reads the text as. */\nexport function parseVars(text: string, format?: VarFormat): VarParse {\n const f = format ?? detectFormat(text);\n return f === 'env' ? parseEnv(text) : parseDelimited(text, f);\n}\n\n/**\n * Which format is this? The first meaningful line decides `.env`; otherwise both delimiters are\n * tried with the real splitter and scored, because `indexOf('\\t')` calls a CSV whose value contains\n * a tab a TSV. A field that still holds a `\"` after splitting is the tell that the row was cut on\n * the wrong delimiter, so it counts against that reading.\n */\nexport function detectFormat(text: string): VarFormat {\n const first = text.split(/\\r\\n|\\n|\\r/).find((l) => l.trim() && !l.trim().startsWith('#')) ?? '';\n if (/^(export\\s+)?[A-Za-z_][A-Za-z0-9_]*\\s*=/.test(first.trim())) return 'env';\n const tsv = score(text, '\\t');\n const csv = score(text, ',');\n if (tsv <= 0 && csv <= 0) return 'env';\n return tsv > csv ? 'tsv' : 'csv';\n}\n\nfunction score(text: string, d: string): number {\n let s = 0;\n for (const r of splitRows(text, d)) {\n const f = padOrTrim(r.fields);\n if (!f.length) continue;\n s += f.length === 2 ? 1 : -1;\n for (const v of f) if (v.includes('\"')) s -= 1;\n }\n return s;\n}\n\nfunction parseEnv(text: string): VarParse {\n const lines = text.split(/\\r\\n|\\n|\\r/);\n const pairs: VarPair[] = [];\n const problems: VarProblem[] = [];\n\n for (let i = 0; i < lines.length; i++) {\n const raw = lines[i] ?? '';\n const at = i + 1;\n if (!raw.trim() || raw.trim().startsWith('#')) continue;\n\n // Only the LEFT is trimmed: a quoted value that spans lines keeps the spaces before its first\n // newline, which `raw.trim()` would have eaten.\n const s = raw.trimStart().replace(/^export\\s+/, '');\n const eq = s.indexOf('='); // the FIRST `=`; the rest of the line is value, `=` signs and all\n if (eq < 0) {\n problems.push({ line: at, text: raw, reason: 'no \"=\" — a line reads NAME=value' });\n continue;\n }\n const k = s.slice(0, eq).trim();\n if (!NAME.test(k)) {\n problems.push({ line: at, text: raw, reason: nameReason(k) });\n continue;\n }\n\n const rest = s.slice(eq + 1).replace(/^[ \\t]+/, '');\n const q = rest[0];\n if (q === '\"' || q === \"'\") {\n const start = i;\n let body = rest.slice(1);\n let end = closingQuote(body, q);\n while (end < 0 && i + 1 < lines.length) {\n i++;\n body += '\\n' + (lines[i] ?? '');\n end = closingQuote(body, q);\n }\n if (end < 0) {\n // Rewind. The lines swallowed hunting for a closing quote are other people's lines, and\n // eating the rest of the file behind one stray `\"` is the silent drop this reports instead.\n i = start;\n problems.push({ line: at, text: raw, reason: 'the quote is never closed' });\n continue;\n }\n const inner = body.slice(0, end);\n // Single quotes are literal, double quotes carry escapes — dotenv's rule, and the one our\n // own exporter writes against. Anything after the closing quote is a trailing comment.\n pairs.push({ k, v: q === '\"' ? unescape(inner) : inner });\n continue;\n }\n // Unquoted: a ` #` starts a comment, trailing whitespace is not value.\n pairs.push({ k, v: rest.replace(/\\s+#.*$/, '').trimEnd() });\n }\n return { format: 'env', pairs: lastWins(pairs), problems };\n}\n\nconst ESCAPES: Record<string, string> = {\n n: '\\n',\n r: '\\r',\n t: '\\t',\n '\\\\': '\\\\',\n '\"': '\"',\n \"'\": \"'\",\n $: '$',\n '`': '`',\n};\n\nfunction unescape(s: string): string {\n let out = '';\n for (let i = 0; i < s.length; i++) {\n const c = s[i] ?? '';\n if (c !== '\\\\' || i === s.length - 1) {\n out += c;\n continue;\n }\n const n = s[++i] ?? '';\n // An unknown escape keeps both characters: `C:\\path` typed inside quotes stays `C:\\path`\n // rather than losing the backslash to a rule nobody wrote down.\n out += ESCAPES[n] ?? '\\\\' + n;\n }\n return out;\n}\n\nfunction closingQuote(s: string, q: string): number {\n for (let i = 0; i < s.length; i++) {\n const c = s[i];\n if (q === '\"' && c === '\\\\') {\n i++;\n continue;\n }\n if (c === q) return i;\n }\n return -1;\n}\n\nfunction parseDelimited(text: string, format: 'csv' | 'tsv'): VarParse {\n const rows = splitRows(text, DELIM[format]);\n const pairs: VarPair[] = [];\n const problems: VarProblem[] = [];\n let first = true;\n\n for (const r of rows) {\n const fields = padOrTrim(r.fields);\n if (!fields.length) continue; // a blank line is not malformed\n if (first) {\n first = false;\n const a = (fields[0] ?? '').toLowerCase();\n const b = (fields[1] ?? '').toLowerCase();\n if (fields.length === 2 && HEADER_NAME.has(a) && HEADER_VALUE.has(b)) continue;\n }\n if (fields.length !== 2) {\n const n = fields.length;\n problems.push({\n line: r.line,\n text: r.raw,\n reason: `${n} column${n === 1 ? '' : 's'} — expected two, a name and a value`,\n });\n continue;\n }\n const k = (fields[0] ?? '').trim();\n if (!NAME.test(k)) {\n problems.push({ line: r.line, text: r.raw, reason: nameReason(k) });\n continue;\n }\n pairs.push({ k, v: fields[1] ?? '' });\n }\n return { format, pairs: lastWins(pairs), problems };\n}\n\ntype Row = { fields: string[]; line: number; raw: string };\n\n/**\n * RFC 4180 with one delimiter parameter: `\"\"` is a literal quote, a quoted field may hold the\n * delimiter and newlines, an unquoted field is trimmed. CRLF and lone CR are normalised to LF\n * first — a row terminator cannot also be data.\n */\nfunction splitRows(text: string, d: string): Row[] {\n const s = text.replace(/\\r\\n?/g, '\\n');\n const rows: Row[] = [];\n let fields: string[] = [];\n let field = '';\n let quoted = false;\n let inQuote = false;\n let line = 1;\n let rowLine = 1;\n let rowStart = 0;\n\n const endField = (): void => {\n fields.push(quoted ? field : field.trim());\n field = '';\n quoted = false;\n };\n\n for (let i = 0; i < s.length; i++) {\n const c = s[i] ?? '';\n if (inQuote) {\n if (c === '\"') {\n if (s[i + 1] === '\"') {\n field += '\"';\n i++;\n } else inQuote = false;\n } else {\n if (c === '\\n') line++;\n field += c;\n }\n continue;\n }\n // A quote opens a field only at its start — spaces before it are formatting, a quote after\n // real characters is data.\n if (c === '\"' && !quoted && field.trim() === '') {\n field = '';\n inQuote = true;\n quoted = true;\n continue;\n }\n if (c === d) {\n endField();\n continue;\n }\n if (c === '\\n') {\n endField();\n rows.push({ fields, line: rowLine, raw: s.slice(rowStart, i) });\n fields = [];\n line++;\n rowLine = line;\n rowStart = i + 1;\n continue;\n }\n field += c;\n }\n if (field !== '' || quoted || fields.length) {\n endField();\n rows.push({ fields, line: rowLine, raw: s.slice(rowStart) });\n }\n return rows;\n}\n\n/**\n * Drop trailing empty columns a spreadsheet padded the row with — but never below two, or\n * `SECRET,` (a name whose value was withheld) would read as a one-column row instead of an empty\n * value. An all-empty row is a blank line.\n */\nfunction padOrTrim(fields: string[]): string[] {\n let n = fields.length;\n while (n > 0 && (fields[n - 1] ?? '') === '') n--;\n return n === 0 ? [] : fields.slice(0, Math.max(n, Math.min(2, fields.length)));\n}\n\nfunction nameReason(k: string): string {\n return k\n ? `\"${k}\" is not a usable variable name — letters, digits and _, not starting with a digit`\n : 'no name before the value';\n}\n\n/** dotenv semantics: a repeated name keeps its last value, at the position it first appeared. */\nfunction lastWins(pairs: VarPair[]): VarPair[] {\n const m = new Map<string, string>();\n for (const e of pairs) m.set(e.k, e.v);\n return [...m].map(([k, v]) => ({ k, v }));\n}\n\n/** Incoming overrides by name, everything else keeps its place. Blank editor rows are dropped. */\nexport function mergeVars(existing: VarPair[], incoming: VarPair[]): VarPair[] {\n const next = existing.filter((e) => e.k).map((e) => ({ ...e }));\n for (const e of incoming) {\n const hit = next.find((n) => n.k === e.k);\n if (hit) hit.v = e.v;\n else next.push({ ...e });\n }\n return next;\n}\n","<script setup lang=\"ts\">\n/**\n * Copy a variable list out as `.env`, CSV or TSV; paste one back in. The chrome only — every rule\n * about what those formats mean lives in `composables/useVarFormats.ts`, which is tested directly\n * because this component cannot be.\n *\n * NOTHING IS REPLACED BEFORE IT IS SHOWN. The paste box parses as you type and renders what it\n * read, including the lines it could not read, and only then offers the buttons that change\n * anything. A paste that half-worked in silence is how a deployment loses the one variable its\n * stack name is built from — which is the leak this project exists to prevent, arriving through a\n * text box.\n *\n * MERGE AND REPLACE ARE BOTH REAL ANSWERS, so the operator picks. `replace` is offered only where\n * this component's owner can actually replace the whole list: on the host-variable page a\n * \"replace\" would have to DELETE the rows the paste omits, and a delete that happens because\n * something was missing from a paste is not a thing to offer behind a two-word button.\n *\n * THE CLIPBOARD IS NOT ALWAYS THERE. `navigator.clipboard` is undefined on a plain-http origin —\n * exactly how this app is reached through a loopback tunnel — and a secure origin can still refuse\n * the write. A failed copy therefore reveals a selected textarea instead of only apologising in a\n * toast: the text exists either way, only the shortcut is missing.\n */\nimport { computed, ref } from 'vue';\nimport type { VarPair } from '../api/types';\nimport {\n FORMAT_LABEL,\n VAR_FORMATS,\n formatVars,\n parseVars,\n type VarFormat,\n} from '../composables/useVarFormats';\nimport { toast } from '../composables/useToasts';\nimport SelectMenu from './SelectMenu.vue';\n\nconst props = defineProps<{\n /** What \"copy\" exports. Blank rows are dropped by the formatter. */\n pairs: VarPair[];\n /** `#` comment lines on the `.env` export — the withheld-secrets surface passes one. */\n note?: string;\n /** Offer \"Replace all\". See the header: not every owner can honestly replace. */\n replace?: boolean;\n /** Skip parsed entries with no value, and say so. For a store that refuses an empty value. */\n skipEmpty?: boolean;\n /** An import is in flight upstream. */\n busy?: boolean;\n}>();\n\nconst emit = defineEmits<{ apply: [pairs: VarPair[], mode: 'replace' | 'merge'] }>();\n\nconst open = ref(false);\nconst text = ref('');\nconst fmt = ref('auto');\n/** The export text, revealed only when the clipboard refused it. */\nconst fallback = ref('');\n\nconst PREVIEW = 12;\n\nconst exportable = computed(() => props.pairs.filter((e) => e.k).length);\n\nconst parsed = computed(() =>\n text.value.trim()\n ? parseVars(text.value, fmt.value === 'auto' ? undefined : (fmt.value as VarFormat))\n : null,\n);\nconst skipped = computed(() =>\n props.skipEmpty && parsed.value ? parsed.value.pairs.filter((e) => !e.v) : [],\n);\nconst usable = computed(() =>\n parsed.value ? (props.skipEmpty ? parsed.value.pairs.filter((e) => e.v) : parsed.value.pairs) : [],\n);\n\nconst formatOptions = [\n { value: 'auto', label: 'Detect the format', hint: 'read the paste and decide' },\n ...VAR_FORMATS.map((f) => ({ value: f, label: `Read as ${FORMAT_LABEL[f]}` })),\n];\n\nasync function copy(f: VarFormat): Promise<void> {\n const out = formatVars(props.pairs, f, props.note);\n try {\n await navigator.clipboard.writeText(out);\n fallback.value = '';\n toast('ok', `Copied ${exportable.value} as ${FORMAT_LABEL[f]}.`);\n } catch {\n // Not an error worth a red banner: the text is right here, it just could not be put on the\n // clipboard for you.\n fallback.value = out;\n toast('error', 'Could not reach the clipboard — select the text below and copy it.');\n }\n}\n\nfunction apply(mode: 'replace' | 'merge'): void {\n emit('apply', usable.value, mode);\n text.value = '';\n open.value = false;\n}\n\nfunction selectAll(e: FocusEvent): void {\n (e.target as HTMLTextAreaElement).select();\n}\n</script>\n\n<template>\n <div class=\"vario\">\n <div class=\"row\">\n <span class=\"mute vario-lbl\">Copy as</span>\n <button\n v-for=\"f in VAR_FORMATS\"\n :key=\"f\"\n type=\"button\"\n class=\"sm ghost\"\n :disabled=\"!exportable\"\n :title=\"exportable ? `Copy all ${exportable} as ${FORMAT_LABEL[f]}` : 'There is nothing to copy yet'\"\n @click=\"copy(f)\"\n >\n {{ FORMAT_LABEL[f] }}\n </button>\n <span class=\"sep\" aria-hidden=\"true\" />\n <button type=\"button\" class=\"sm\" :aria-expanded=\"open\" @click=\"open = !open\">\n {{ open ? 'Close' : 'Paste to fill' }}\n </button>\n </div>\n\n <div v-if=\"fallback\" class=\"vario-box\">\n <p class=\"hint\">\n The clipboard could not be reached — a browser grants it only on a secure origin, and not\n always then. Select this and copy it by hand.\n </p>\n <textarea\n :value=\"fallback\"\n readonly\n rows=\"6\"\n spellcheck=\"false\"\n aria-label=\"the exported variables\"\n @focus=\"selectAll\"\n />\n <button type=\"button\" class=\"sm ghost\" @click=\"fallback = ''\">Hide</button>\n </div>\n\n <div v-if=\"open\" class=\"vario-box\">\n <slot />\n\n <textarea\n v-model=\"text\"\n rows=\"7\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n aria-label=\"paste variables\"\n placeholder=\"PR=7 REGION=eu-central …or two columns of CSV or TSV\"\n />\n\n <div class=\"row\" style=\"margin-top: var(--s2)\">\n <SelectMenu v-model=\"fmt\" label=\"how to read the paste\" :options=\"formatOptions\" />\n <span v-if=\"parsed\" class=\"mute\" style=\"font-size: var(--t-sm)\">\n read as {{ FORMAT_LABEL[parsed.format] }}\n </span>\n </div>\n\n <template v-if=\"parsed\">\n <p class=\"hint\">\n <b>{{ usable.length }}</b> variable{{ usable.length === 1 ? '' : 's' }} read.\n <template v-if=\"skipped.length\">\n {{ skipped.length }} ({{ skipped.map((e) => e.k).join(', ') }}) had no value and will be\n skipped — a value stored here is never blank.\n </template>\n </p>\n\n <ul v-if=\"usable.length\" class=\"kvlist vario-preview\">\n <li v-for=\"e in usable.slice(0, PREVIEW)\" :key=\"e.k\">\n <span class=\"k\"><code>{{ e.k }}</code></span>\n <span class=\"v mono\">{{ e.v }}</span>\n </li>\n <li v-if=\"usable.length > PREVIEW\" class=\"mute\">\n and {{ usable.length - PREVIEW }} more\n </li>\n </ul>\n\n <!--\n Never a silent drop. A line that did not become a variable is shown with its number and\n its text, so the operator can see whether the paste was misread or the source was wrong.\n -->\n <div v-if=\"parsed.problems.length\" class=\"banner warn\">\n <b>\n {{ parsed.problems.length }} line{{ parsed.problems.length === 1 ? '' : 's' }} could not\n be read\n </b>\n <ul class=\"vario-problems\">\n <li v-for=\"p in parsed.problems\" :key=\"p.line\">\n <span class=\"mute\">line {{ p.line }}</span>\n <code>{{ p.text }}</code>\n <span class=\"dim\">{{ p.reason }}</span>\n </li>\n </ul>\n <p>\n They are not imported. Fix them and paste again, or apply what parsed and add the rest\n by hand.\n </p>\n </div>\n\n <div class=\"row\" style=\"margin-top: var(--s3)\">\n <button\n v-if=\"replace\"\n type=\"button\"\n class=\"primary\"\n :disabled=\"!usable.length || busy\"\n :title=\"usable.length ? 'Discard the current list and use exactly what was pasted' : 'Nothing parsed to apply'\"\n @click=\"apply('replace')\"\n >\n Replace all\n </button>\n <button\n type=\"button\"\n :class=\"replace ? '' : 'primary'\"\n :disabled=\"!usable.length || busy\"\n :title=\"usable.length ? 'Overwrite matching names, keep the rest, add what is new' : 'Nothing parsed to apply'\"\n @click=\"apply('merge')\"\n >\n Merge\n </button>\n <button type=\"button\" class=\"ghost\" @click=\"open = false\">Cancel</button>\n </div>\n </template>\n </div>\n </div>\n</template>\n\n<style scoped>\n.vario {\n margin-top: var(--s3);\n}\n.vario-lbl {\n font-size: var(--t-xs);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n/* Separates two groups of controls that do opposite things; the gap alone read as one strip. */\n.sep {\n width: 1px;\n height: 18px;\n background: var(--line);\n margin-inline: var(--s1);\n}\n.vario-box {\n margin-top: var(--s3);\n padding: var(--s3);\n border: 1px solid var(--line);\n border-radius: var(--r2);\n max-width: 640px;\n}\n.vario-box > textarea {\n width: 100%;\n}\n/* A pasted value can hold newlines; collapsing them here would hide half of what is being applied. */\n.vario-preview {\n margin-top: var(--s2);\n}\n.vario-preview .v {\n white-space: pre-wrap;\n}\n.vario-problems {\n margin: var(--s2) 0 0;\n padding: 0;\n list-style: none;\n display: grid;\n gap: var(--s1);\n font-size: var(--t-sm);\n}\n/*\n * Two columns, not a wrapping row: a long reason wrapping under `line 5` read as a new paragraph of\n * the banner rather than as that line's explanation.\n */\n.vario-problems > li {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n gap: 2px var(--s2);\n align-items: baseline;\n}\n.vario-problems > li > code {\n justify-self: start;\n}\n.vario-problems > li > .dim {\n grid-column: 2;\n}\n</style>\n"],"names":["VAR_FORMATS","FORMAT_LABEL","NAME","PLAIN","DELIM","HEADER_NAME","HEADER_VALUE","formatVars","pairs","format","note","rows","e","l","envQuote","d","f","v","delimQuote","parseVars","text","detectFormat","parseEnv","parseDelimited","first","tsv","score","csv","s","r","splitRows","padOrTrim","lines","problems","i","raw","at","eq","k","nameReason","rest","q","start","body","end","closingQuote","inner","unescape","lastWins","ESCAPES","out","c","fields","a","b","n","field","quoted","inQuote","line","rowLine","rowStart","endField","m","mergeVars","existing","incoming","next","hit","PREVIEW","props","__props","emit","__emit","open","ref","fmt","fallback","exportable","computed","parsed","skipped","usable","formatOptions","copy","toast","apply","mode","selectAll","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_cache","_Fragment","_renderList","_unref","$event","_toDisplayString","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_7","_renderSlot","_ctx","_hoisted_8","_createVNode","SelectMenu","_hoisted_9","_hoisted_10","_createTextVNode","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","p","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VarIO-D7bwBO4U.js","sources":["../../src/composables/useVarFormats.ts","../../src/components/VarIO.vue"],"sourcesContent":["/**\n * `.env`, CSV and TSV for variable lists — the one implementation both variable surfaces share.\n *\n * WHY PURE FUNCTIONS IN A `.ts`. There is no component test harness in this app, so anything that\n * can be wrong in an interesting way has to live somewhere `bun test` can reach it. Everything here\n * is a string in and a value out; the components own the chrome and the policy, and nothing else.\n *\n * THE ROUND-TRIP IS THE CONTRACT. `parseVars(formatVars(pairs, f), f)` returns `pairs` for every\n * value the editors can hold — newlines, quotes, `#`, leading and trailing spaces, backslashes,\n * `=`, the empty string. That is what makes \"copy them out, paste them back\" safe, and it is what\n * `test/useVarFormats.test.ts` spends most of its lines on. Two deliberate exceptions, both tested:\n *\n * - A lone carriage return survives `.env` (escaped as `\\r`) but not CSV/TSV, where CRLF and CR\n * are normalised to LF before parsing — a row terminator cannot also be data without a\n * dialect flag, and no `<input type=\"text\">` can produce one anyway.\n * - `parseVars` collapses a repeated name to its last value, as every dotenv reader does. The\n * import preview shows the result, so the collapse is visible before anything is replaced.\n *\n * WHY CSV AND TSV ALWAYS EXPORT A `name,value` HEADER. A header row is ambiguous with data — a\n * variable really can be called `name` — and guessing wrong either eats a variable or imports a\n * label. Emitting the header makes our own output unambiguous: the header we wrote is the row the\n * detector eats, so `{ k: 'name', v: 'value' }` round-trips. Foreign input whose first row happens\n * to read `name,value` loses that row, which is why the importer previews what it parsed.\n *\n * A NAME MUST BE ONE A SPEC CAN REFERENCE. `VarEditor` only trims what is typed, so `foo-bar` can\n * exist in the editor and be exported — but `${foo-bar}` is not a thing `internal/spec` can\n * interpolate, so importing it would add a row that can never do anything. It comes back as a\n * problem instead, named, where the operator can see it.\n *\n * A MALFORMED LINE IS NEVER DROPPED SILENTLY. Every line that does not become a pair comes back in\n * `problems` with its line number, its text and why — a paste that half-worked without saying so\n * is how a deployment ends up missing the one variable its stack name is built from.\n */\n\nimport type { VarPair } from '../api/types';\n\nexport type VarFormat = 'env' | 'csv' | 'tsv';\n\n/** A line that did not become a pair. Reported, never swallowed. */\nexport type VarProblem = { line: number; text: string; reason: string };\n\nexport type VarParse = { format: VarFormat; pairs: VarPair[]; problems: VarProblem[] };\n\nexport const VAR_FORMATS: VarFormat[] = ['env', 'csv', 'tsv'];\nexport const FORMAT_LABEL: Record<VarFormat, string> = { env: '.env', csv: 'CSV', tsv: 'TSV' };\n\n/** The charset a spec can reference — `internal/spec`'s `${NAME}` regex, not a UI opinion. */\nconst NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n/** Values needing no quotes: bytes that dotenv, compose and a shell all read back identically. */\nconst PLAIN = /^[A-Za-z0-9_@%+=:,./-]+$/;\nconst DELIM: Record<'csv' | 'tsv', string> = { csv: ',', tsv: '\\t' };\nconst HEADER_NAME = new Set(['name', 'key', 'variable', 'var']);\nconst HEADER_VALUE = new Set(['value', 'val']);\n\n// ── export ────────────────────────────────────────────────────────────────────────────────────────\n\n/**\n * Render pairs. `note` becomes `#` comment lines and is only expressible in `.env` — the surface\n * that passes one (host secrets, whose values are withheld) also says so on the page, because CSV\n * has nowhere to put it.\n */\nexport function formatVars(pairs: VarPair[], format: VarFormat, note?: string): string {\n const rows = pairs.filter((e) => e.k);\n if (format === 'env') {\n const head = note ? note.split('\\n').map((l) => `# ${l}`) : [];\n return [...head, ...rows.map((e) => `${e.k}=${envQuote(e.v)}`)].join('\\n') + '\\n';\n }\n const d = DELIM[format];\n return [['name', 'value'], ...rows.map((e) => [e.k, e.v])]\n .map((f) => f.map((v) => delimQuote(v ?? '', d)).join(d))\n .join('\\n') + '\\n';\n}\n\nfunction envQuote(v: string): string {\n // An empty value stays bare: `KEY=` parses back to `''` and reads as \"fill this in\", which is\n // exactly what a withheld secret is.\n if (v === '' || PLAIN.test(v)) return v;\n return `\"${v.replace(/[\\\\\"]/g, '\\\\$&').replace(/\\n/g, '\\\\n').replace(/\\r/g, '\\\\r')}\"`;\n}\n\nfunction delimQuote(v: string, d: string): string {\n const safe =\n v === '' || (!v.includes(d) && !v.includes('\"') && !/[\\n\\r]/.test(v) && v === v.trim());\n return safe ? v : `\"${v.replace(/\"/g, '\"\"')}\"`;\n}\n\n// ── import ────────────────────────────────────────────────────────────────────────────────────────\n\n/** Parse in `format`, or in whatever `detectFormat` reads the text as. */\nexport function parseVars(text: string, format?: VarFormat): VarParse {\n const f = format ?? detectFormat(text);\n return f === 'env' ? parseEnv(text) : parseDelimited(text, f);\n}\n\n/**\n * Which format is this? The first meaningful line decides `.env`; otherwise both delimiters are\n * tried with the real splitter and scored, because `indexOf('\\t')` calls a CSV whose value contains\n * a tab a TSV. A field that still holds a `\"` after splitting is the tell that the row was cut on\n * the wrong delimiter, so it counts against that reading.\n */\nexport function detectFormat(text: string): VarFormat {\n const first = text.split(/\\r\\n|\\n|\\r/).find((l) => l.trim() && !l.trim().startsWith('#')) ?? '';\n if (/^(export\\s+)?[A-Za-z_][A-Za-z0-9_]*\\s*=/.test(first.trim())) return 'env';\n const tsv = score(text, '\\t');\n const csv = score(text, ',');\n if (tsv <= 0 && csv <= 0) return 'env';\n return tsv > csv ? 'tsv' : 'csv';\n}\n\nfunction score(text: string, d: string): number {\n let s = 0;\n for (const r of splitRows(text, d)) {\n const f = padOrTrim(r.fields);\n if (!f.length) continue;\n s += f.length === 2 ? 1 : -1;\n for (const v of f) if (v.includes('\"')) s -= 1;\n }\n return s;\n}\n\nfunction parseEnv(text: string): VarParse {\n const lines = text.split(/\\r\\n|\\n|\\r/);\n const pairs: VarPair[] = [];\n const problems: VarProblem[] = [];\n\n for (let i = 0; i < lines.length; i++) {\n const raw = lines[i] ?? '';\n const at = i + 1;\n if (!raw.trim() || raw.trim().startsWith('#')) continue;\n\n // Only the LEFT is trimmed: a quoted value that spans lines keeps the spaces before its first\n // newline, which `raw.trim()` would have eaten.\n const s = raw.trimStart().replace(/^export\\s+/, '');\n const eq = s.indexOf('='); // the FIRST `=`; the rest of the line is value, `=` signs and all\n if (eq < 0) {\n problems.push({ line: at, text: raw, reason: 'no \"=\" — a line reads NAME=value' });\n continue;\n }\n const k = s.slice(0, eq).trim();\n if (!NAME.test(k)) {\n problems.push({ line: at, text: raw, reason: nameReason(k) });\n continue;\n }\n\n const rest = s.slice(eq + 1).replace(/^[ \\t]+/, '');\n const q = rest[0];\n if (q === '\"' || q === \"'\") {\n const start = i;\n let body = rest.slice(1);\n let end = closingQuote(body, q);\n while (end < 0 && i + 1 < lines.length) {\n i++;\n body += '\\n' + (lines[i] ?? '');\n end = closingQuote(body, q);\n }\n if (end < 0) {\n // Rewind. The lines swallowed hunting for a closing quote are other people's lines, and\n // eating the rest of the file behind one stray `\"` is the silent drop this reports instead.\n i = start;\n problems.push({ line: at, text: raw, reason: 'the quote is never closed' });\n continue;\n }\n const inner = body.slice(0, end);\n // Single quotes are literal, double quotes carry escapes — dotenv's rule, and the one our\n // own exporter writes against. Anything after the closing quote is a trailing comment.\n pairs.push({ k, v: q === '\"' ? unescape(inner) : inner });\n continue;\n }\n // Unquoted: a ` #` starts a comment, trailing whitespace is not value.\n pairs.push({ k, v: rest.replace(/\\s+#.*$/, '').trimEnd() });\n }\n return { format: 'env', pairs: lastWins(pairs), problems };\n}\n\nconst ESCAPES: Record<string, string> = {\n n: '\\n',\n r: '\\r',\n t: '\\t',\n '\\\\': '\\\\',\n '\"': '\"',\n \"'\": \"'\",\n $: '$',\n '`': '`',\n};\n\nfunction unescape(s: string): string {\n let out = '';\n for (let i = 0; i < s.length; i++) {\n const c = s[i] ?? '';\n if (c !== '\\\\' || i === s.length - 1) {\n out += c;\n continue;\n }\n const n = s[++i] ?? '';\n // An unknown escape keeps both characters: `C:\\path` typed inside quotes stays `C:\\path`\n // rather than losing the backslash to a rule nobody wrote down.\n out += ESCAPES[n] ?? '\\\\' + n;\n }\n return out;\n}\n\nfunction closingQuote(s: string, q: string): number {\n for (let i = 0; i < s.length; i++) {\n const c = s[i];\n if (q === '\"' && c === '\\\\') {\n i++;\n continue;\n }\n if (c === q) return i;\n }\n return -1;\n}\n\nfunction parseDelimited(text: string, format: 'csv' | 'tsv'): VarParse {\n const rows = splitRows(text, DELIM[format]);\n const pairs: VarPair[] = [];\n const problems: VarProblem[] = [];\n let first = true;\n\n for (const r of rows) {\n const fields = padOrTrim(r.fields);\n if (!fields.length) continue; // a blank line is not malformed\n if (first) {\n first = false;\n const a = (fields[0] ?? '').toLowerCase();\n const b = (fields[1] ?? '').toLowerCase();\n if (fields.length === 2 && HEADER_NAME.has(a) && HEADER_VALUE.has(b)) continue;\n }\n if (fields.length !== 2) {\n const n = fields.length;\n problems.push({\n line: r.line,\n text: r.raw,\n reason: `${n} column${n === 1 ? '' : 's'} — expected two, a name and a value`,\n });\n continue;\n }\n const k = (fields[0] ?? '').trim();\n if (!NAME.test(k)) {\n problems.push({ line: r.line, text: r.raw, reason: nameReason(k) });\n continue;\n }\n pairs.push({ k, v: fields[1] ?? '' });\n }\n return { format, pairs: lastWins(pairs), problems };\n}\n\ntype Row = { fields: string[]; line: number; raw: string };\n\n/**\n * RFC 4180 with one delimiter parameter: `\"\"` is a literal quote, a quoted field may hold the\n * delimiter and newlines, an unquoted field is trimmed. CRLF and lone CR are normalised to LF\n * first — a row terminator cannot also be data.\n */\nfunction splitRows(text: string, d: string): Row[] {\n const s = text.replace(/\\r\\n?/g, '\\n');\n const rows: Row[] = [];\n let fields: string[] = [];\n let field = '';\n let quoted = false;\n let inQuote = false;\n let line = 1;\n let rowLine = 1;\n let rowStart = 0;\n\n const endField = (): void => {\n fields.push(quoted ? field : field.trim());\n field = '';\n quoted = false;\n };\n\n for (let i = 0; i < s.length; i++) {\n const c = s[i] ?? '';\n if (inQuote) {\n if (c === '\"') {\n if (s[i + 1] === '\"') {\n field += '\"';\n i++;\n } else inQuote = false;\n } else {\n if (c === '\\n') line++;\n field += c;\n }\n continue;\n }\n // A quote opens a field only at its start — spaces before it are formatting, a quote after\n // real characters is data.\n if (c === '\"' && !quoted && field.trim() === '') {\n field = '';\n inQuote = true;\n quoted = true;\n continue;\n }\n if (c === d) {\n endField();\n continue;\n }\n if (c === '\\n') {\n endField();\n rows.push({ fields, line: rowLine, raw: s.slice(rowStart, i) });\n fields = [];\n line++;\n rowLine = line;\n rowStart = i + 1;\n continue;\n }\n field += c;\n }\n if (field !== '' || quoted || fields.length) {\n endField();\n rows.push({ fields, line: rowLine, raw: s.slice(rowStart) });\n }\n return rows;\n}\n\n/**\n * Drop trailing empty columns a spreadsheet padded the row with — but never below two, or\n * `SECRET,` (a name whose value was withheld) would read as a one-column row instead of an empty\n * value. An all-empty row is a blank line.\n */\nfunction padOrTrim(fields: string[]): string[] {\n let n = fields.length;\n while (n > 0 && (fields[n - 1] ?? '') === '') n--;\n return n === 0 ? [] : fields.slice(0, Math.max(n, Math.min(2, fields.length)));\n}\n\nfunction nameReason(k: string): string {\n return k\n ? `\"${k}\" is not a usable variable name — letters, digits and _, not starting with a digit`\n : 'no name before the value';\n}\n\n/** dotenv semantics: a repeated name keeps its last value, at the position it first appeared. */\nfunction lastWins(pairs: VarPair[]): VarPair[] {\n const m = new Map<string, string>();\n for (const e of pairs) m.set(e.k, e.v);\n return [...m].map(([k, v]) => ({ k, v }));\n}\n\n/** Incoming overrides by name, everything else keeps its place. Blank editor rows are dropped. */\nexport function mergeVars(existing: VarPair[], incoming: VarPair[]): VarPair[] {\n const next = existing.filter((e) => e.k).map((e) => ({ ...e }));\n for (const e of incoming) {\n const hit = next.find((n) => n.k === e.k);\n if (hit) hit.v = e.v;\n else next.push({ ...e });\n }\n return next;\n}\n","<script setup lang=\"ts\">\n/**\n * Copy a variable list out as `.env`, CSV or TSV; paste one back in. The chrome only — every rule\n * about what those formats mean lives in `composables/useVarFormats.ts`, which is tested directly\n * because this component cannot be.\n *\n * NOTHING IS REPLACED BEFORE IT IS SHOWN. The paste box parses as you type and renders what it\n * read, including the lines it could not read, and only then offers the buttons that change\n * anything. A paste that half-worked in silence is how a deployment loses the one variable its\n * stack name is built from — which is the leak this project exists to prevent, arriving through a\n * text box.\n *\n * MERGE AND REPLACE ARE BOTH REAL ANSWERS, so the operator picks. `replace` is offered only where\n * this component's owner can actually replace the whole list: on the host-variable page a\n * \"replace\" would have to DELETE the rows the paste omits, and a delete that happens because\n * something was missing from a paste is not a thing to offer behind a two-word button.\n *\n * THE CLIPBOARD IS NOT ALWAYS THERE. `navigator.clipboard` is undefined on a plain-http origin —\n * exactly how this app is reached through a loopback tunnel — and a secure origin can still refuse\n * the write. A failed copy therefore reveals a selected textarea instead of only apologising in a\n * toast: the text exists either way, only the shortcut is missing.\n */\nimport { computed, ref } from 'vue';\nimport type { VarPair } from '../api/types';\nimport {\n FORMAT_LABEL,\n VAR_FORMATS,\n formatVars,\n parseVars,\n type VarFormat,\n} from '../composables/useVarFormats';\nimport { toast } from '../composables/useToasts';\nimport SelectMenu from './SelectMenu.vue';\n\nconst props = defineProps<{\n /** What \"copy\" exports. Blank rows are dropped by the formatter. */\n pairs: VarPair[];\n /** `#` comment lines on the `.env` export — the withheld-secrets surface passes one. */\n note?: string;\n /** Offer \"Replace all\". See the header: not every owner can honestly replace. */\n replace?: boolean;\n /** Skip parsed entries with no value, and say so. For a store that refuses an empty value. */\n skipEmpty?: boolean;\n /** An import is in flight upstream. */\n busy?: boolean;\n}>();\n\nconst emit = defineEmits<{ apply: [pairs: VarPair[], mode: 'replace' | 'merge'] }>();\n\nconst open = ref(false);\nconst text = ref('');\nconst fmt = ref('auto');\n/** The export text, revealed only when the clipboard refused it. */\nconst fallback = ref('');\n\nconst PREVIEW = 12;\n\nconst exportable = computed(() => props.pairs.filter((e) => e.k).length);\n\nconst parsed = computed(() =>\n text.value.trim()\n ? parseVars(text.value, fmt.value === 'auto' ? undefined : (fmt.value as VarFormat))\n : null,\n);\nconst skipped = computed(() =>\n props.skipEmpty && parsed.value ? parsed.value.pairs.filter((e) => !e.v) : [],\n);\nconst usable = computed(() =>\n parsed.value ? (props.skipEmpty ? parsed.value.pairs.filter((e) => e.v) : parsed.value.pairs) : [],\n);\n\nconst formatOptions = [\n { value: 'auto', label: 'Detect the format', hint: 'read the paste and decide' },\n ...VAR_FORMATS.map((f) => ({ value: f, label: `Read as ${FORMAT_LABEL[f]}` })),\n];\n\nasync function copy(f: VarFormat): Promise<void> {\n const out = formatVars(props.pairs, f, props.note);\n try {\n await navigator.clipboard.writeText(out);\n fallback.value = '';\n toast('ok', `Copied ${exportable.value} as ${FORMAT_LABEL[f]}.`);\n } catch {\n // Not an error worth a red banner: the text is right here, it just could not be put on the\n // clipboard for you.\n fallback.value = out;\n toast('error', 'Could not reach the clipboard — select the text below and copy it.');\n }\n}\n\nfunction apply(mode: 'replace' | 'merge'): void {\n emit('apply', usable.value, mode);\n text.value = '';\n open.value = false;\n}\n\nfunction selectAll(e: FocusEvent): void {\n (e.target as HTMLTextAreaElement).select();\n}\n</script>\n\n<template>\n <div class=\"vario\">\n <div class=\"row\">\n <span class=\"mute vario-lbl\">Copy as</span>\n <button\n v-for=\"f in VAR_FORMATS\"\n :key=\"f\"\n type=\"button\"\n class=\"sm ghost\"\n :disabled=\"!exportable\"\n :title=\"exportable ? `Copy all ${exportable} as ${FORMAT_LABEL[f]}` : 'There is nothing to copy yet'\"\n @click=\"copy(f)\"\n >\n {{ FORMAT_LABEL[f] }}\n </button>\n <span class=\"sep\" aria-hidden=\"true\" />\n <button type=\"button\" class=\"sm\" :aria-expanded=\"open\" @click=\"open = !open\">\n {{ open ? 'Close' : 'Paste to fill' }}\n </button>\n </div>\n\n <div v-if=\"fallback\" class=\"vario-box\">\n <p class=\"hint\">\n The clipboard could not be reached — a browser grants it only on a secure origin, and not\n always then. Select this and copy it by hand.\n </p>\n <textarea\n :value=\"fallback\"\n readonly\n rows=\"6\"\n spellcheck=\"false\"\n aria-label=\"the exported variables\"\n @focus=\"selectAll\"\n />\n <button type=\"button\" class=\"sm ghost\" @click=\"fallback = ''\">Hide</button>\n </div>\n\n <div v-if=\"open\" class=\"vario-box\">\n <slot />\n\n <textarea\n v-model=\"text\"\n rows=\"7\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n aria-label=\"paste variables\"\n placeholder=\"PR=7 REGION=eu-central …or two columns of CSV or TSV\"\n />\n\n <div class=\"row\" style=\"margin-top: var(--s2)\">\n <SelectMenu v-model=\"fmt\" label=\"how to read the paste\" :options=\"formatOptions\" />\n <span v-if=\"parsed\" class=\"mute\" style=\"font-size: var(--t-sm)\">\n read as {{ FORMAT_LABEL[parsed.format] }}\n </span>\n </div>\n\n <template v-if=\"parsed\">\n <p class=\"hint\">\n <b>{{ usable.length }}</b> variable{{ usable.length === 1 ? '' : 's' }} read.\n <template v-if=\"skipped.length\">\n {{ skipped.length }} ({{ skipped.map((e) => e.k).join(', ') }}) had no value and will be\n skipped — a value stored here is never blank.\n </template>\n </p>\n\n <ul v-if=\"usable.length\" class=\"kvlist vario-preview\">\n <li v-for=\"e in usable.slice(0, PREVIEW)\" :key=\"e.k\">\n <span class=\"k\"><code>{{ e.k }}</code></span>\n <span class=\"v mono\">{{ e.v }}</span>\n </li>\n <li v-if=\"usable.length > PREVIEW\" class=\"mute\">\n and {{ usable.length - PREVIEW }} more\n </li>\n </ul>\n\n <!--\n Never a silent drop. A line that did not become a variable is shown with its number and\n its text, so the operator can see whether the paste was misread or the source was wrong.\n -->\n <div v-if=\"parsed.problems.length\" class=\"banner warn\">\n <b>\n {{ parsed.problems.length }} line{{ parsed.problems.length === 1 ? '' : 's' }} could not\n be read\n </b>\n <ul class=\"vario-problems\">\n <li v-for=\"p in parsed.problems\" :key=\"p.line\">\n <span class=\"mute\">line {{ p.line }}</span>\n <code>{{ p.text }}</code>\n <span class=\"dim\">{{ p.reason }}</span>\n </li>\n </ul>\n <p>\n They are not imported. Fix them and paste again, or apply what parsed and add the rest\n by hand.\n </p>\n </div>\n\n <div class=\"row\" style=\"margin-top: var(--s3)\">\n <button\n v-if=\"replace\"\n type=\"button\"\n class=\"primary\"\n :disabled=\"!usable.length || busy\"\n :title=\"usable.length ? 'Discard the current list and use exactly what was pasted' : 'Nothing parsed to apply'\"\n @click=\"apply('replace')\"\n >\n Replace all\n </button>\n <button\n type=\"button\"\n :class=\"replace ? '' : 'primary'\"\n :disabled=\"!usable.length || busy\"\n :title=\"usable.length ? 'Overwrite matching names, keep the rest, add what is new' : 'Nothing parsed to apply'\"\n @click=\"apply('merge')\"\n >\n Merge\n </button>\n <button type=\"button\" class=\"ghost\" @click=\"open = false\">Cancel</button>\n </div>\n </template>\n </div>\n </div>\n</template>\n\n<style scoped>\n.vario {\n margin-top: var(--s3);\n}\n.vario-lbl {\n font-size: var(--t-xs);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n/* Separates two groups of controls that do opposite things; the gap alone read as one strip. */\n.sep {\n width: 1px;\n height: 18px;\n background: var(--line);\n margin-inline: var(--s1);\n}\n.vario-box {\n margin-top: var(--s3);\n padding: var(--s3);\n border: 1px solid var(--line);\n border-radius: var(--r2);\n max-width: 640px;\n}\n.vario-box > textarea {\n width: 100%;\n}\n/* A pasted value can hold newlines; collapsing them here would hide half of what is being applied. */\n.vario-preview {\n margin-top: var(--s2);\n}\n.vario-preview .v {\n white-space: pre-wrap;\n}\n.vario-problems {\n margin: var(--s2) 0 0;\n padding: 0;\n list-style: none;\n display: grid;\n gap: var(--s1);\n font-size: var(--t-sm);\n}\n/*\n * Two columns, not a wrapping row: a long reason wrapping under `line 5` read as a new paragraph of\n * the banner rather than as that line's explanation.\n */\n.vario-problems > li {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n gap: 2px var(--s2);\n align-items: baseline;\n}\n.vario-problems > li > code {\n justify-self: start;\n}\n.vario-problems > li > .dim {\n grid-column: 2;\n}\n</style>\n"],"names":["VAR_FORMATS","FORMAT_LABEL","NAME","PLAIN","DELIM","HEADER_NAME","HEADER_VALUE","formatVars","pairs","format","note","rows","e","l","envQuote","d","f","v","delimQuote","parseVars","text","detectFormat","parseEnv","parseDelimited","first","tsv","score","csv","s","r","splitRows","padOrTrim","lines","problems","i","raw","at","eq","k","nameReason","rest","q","start","body","end","closingQuote","inner","unescape","lastWins","ESCAPES","out","c","fields","a","b","n","field","quoted","inQuote","line","rowLine","rowStart","endField","m","mergeVars","existing","incoming","next","hit","PREVIEW","props","__props","emit","__emit","open","ref","fmt","fallback","exportable","computed","parsed","skipped","usable","formatOptions","copy","toast","apply","mode","selectAll","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_cache","_Fragment","_renderList","_unref","$event","_toDisplayString","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_7","_renderSlot","_ctx","_hoisted_8","_createVNode","SelectMenu","_hoisted_9","_hoisted_10","_createTextVNode","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","p","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{d as Q,c as r,a as e,f as n,e as u,w as m,_ as S,z as C,g as y,F as D,t as d,r as L,h as X,i as M,v as Y,ac as ee,L as te,j as p,k as _,p as A,C as R,o,U}from"./index-
|
|
1
|
+
import{d as Q,c as r,a as e,f as n,e as u,w as m,_ as S,z as C,g as y,F as D,t as d,r as L,h as X,i as M,v as Y,ac as ee,L as te,j as p,k as _,p as A,C as R,o,U}from"./index-ChQCta5r.js";import{_ as j}from"./ActionButton.vue_vue_type_script_setup_true_lang-pLJQfFvY.js";import{E as ae}from"./EquivalentCommand-DSNG3Ig1.js";import{_ as se}from"./ErrorNote.vue_vue_type_script_setup_true_lang-BaTXNX-q.js";import{_ as H}from"./RelativeTime.vue_vue_type_script_setup_true_lang-DmifGQCq.js";import{_ as W}from"./SkeletonList.vue_vue_type_script_setup_true_lang-Veni2uNM.js";import{_ as le}from"./RefreshButton.vue_vue_type_script_setup_true_lang-zOMuwEGt.js";import{V as q}from"./VarIO-D7bwBO4U.js";import{_ as ne}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./SelectMenu.vue_vue_type_script_setup_true_lang-CEOgxUqO.js";import"./Teleport-Cy-A7RhK.js";const oe={class:"page-head"},re={class:"sub"},ie={key:1,class:"panel"},ue={class:"grid-2"},de={class:"panel"},ce={class:"phead"},ve={class:"mute",style:{"font-size":"var(--t-xs)"}},pe={key:1,class:"cards"},me={class:"name","data-label":"name"},fe={class:"break","data-label":"value"},he={class:"dim nowrap","data-label":"updated"},be={class:"right nowrap","data-label":""},ye=["onClick"],_e={key:2,class:"hint"},ge={class:"panel"},ke={class:"phead"},we={class:"mute",style:{"font-size":"var(--t-xs)"}},$e={key:1,class:"cards"},Ae={class:"name","data-label":"name"},Ee={class:"dim nowrap","data-label":"updated"},Ve={class:"right nowrap","data-label":""},xe=["onClick"],Ne={key:2,class:"hint"},Se={class:"panel"},Ce={class:"section",style:{"margin-bottom":"var(--s3)"}},De={class:"row wrap two-up"},Me={class:"field grow"},Re=["disabled"],Ue={class:"field grow"},Te=["type","placeholder"],ze={key:0,class:"s-failed",style:{"margin-top":"var(--s2)"}},Ie={class:"row",style:{"margin-top":"var(--s3)"}},Pe={class:"check"},Be=["disabled"],Le={key:1,class:"hint"},je={class:"row",style:{"margin-top":"var(--s4)"}},He=["disabled"],We={key:1,class:"mute",style:{"font-size":"var(--t-sm)"}},qe=`Secret VALUES are not included: this server has no route that returns one.
|
|
2
2
|
Each name is listed with an empty value for you to fill in.`,Fe=Q({__name:"VariablesView",setup(Oe){const E=p([]),V=p(!1),v=p(""),T=p(!1),g=_(()=>E.value.filter(a=>!a.secret)),k=_(()=>E.value.filter(a=>a.secret)),F=_(()=>g.value.map(a=>({k:a.name,v:a.value??""}))),O=_(()=>k.value.map(a=>({k:a.name,v:""}))),s=p({name:"",value:"",secret:!1}),x=p(!1),w=p(!1),i=p(null),z=/^[A-Za-z_][A-Za-z0-9_]*$/,Z=_(()=>!x.value&&z.test(s.value.name)&&s.value.value.length>0);async function h(){const a=await A.get("/api/host-vars");if(V.value=!0,a.status===404){T.value=!0;return}if(!a.ok){v.value=R(a,"load the variables");return}v.value="",E.value=a.body.entries??[]}h();function I(a){i.value=a,s.value={name:a.name,value:a.value??"",secret:a.secret}}function N(){i.value=null,s.value={name:"",value:"",secret:!1}}async function G(){x.value=!0;const a=await A.put(`/api/host-vars/${s.value.name}`,{value:s.value.value,secret:s.value.secret});if(x.value=!1,!a.ok){v.value=R(a,`store ${s.value.name}`);return}v.value="",U("ok",`Stored ${s.value.name}.`),N(),h()}async function P(a,t){w.value=!0;const c=[];for(const l of t)(await A.put(`/api/host-vars/${encodeURIComponent(l.k)}`,{value:l.v,secret:a})).ok||c.push(l.k);w.value=!1;const b=a?"secret":"variable",f=t.length-c.length;c.length?v.value=`Stored ${f} of ${t.length} ${b}s — ${c.join(", ")} could not be stored.`:(v.value="",U("ok",`Stored ${f} ${b}${f===1?"":"s"}.`)),h()}const J=a=>P(!1,a),K=a=>P(!0,a);async function B(a){var c;const t=await A.del(`/api/host-vars/${a.name}`);if(!t.ok){v.value=R(t,`delete ${a.name}`);return}U("ok",`Deleted ${a.name}. Specs referencing it will fail to resolve until it exists again.`),((c=i.value)==null?void 0:c.name)===a.name&&N(),h()}return(a,t)=>{var c,b,f;return o(),r("div",null,[e("div",oe,[e("div",null,[t[5]||(t[5]=e("h1",null,"Variables & secrets",-1)),e("div",re,[t[4]||(t[4]=n(" Host-level values every spec can reference ",-1)),u(S,{label:"how a spec uses these"},{default:m(()=>[...t[3]||(t[3]=[n(" Write ",-1),e("code",null,"${vars.NAME}",-1),n(" or ",-1),e("code",null,"${secrets.NAME}",-1),n(" anywhere a spec interpolates — the ",-1),e("code",null,"env:",-1),n(" block, hooks, compose settings. The spelling is deliberate: plain ",-1),e("code",null,"${NAME}",-1),n(' keeps meaning "whatever the request supplied", so the two can never collide. ',-1)])]),_:1})])]),t[6]||(t[6]=e("span",{class:"grow"},null,-1)),u(le,{run:h})]),v.value?(o(),C(se,{key:0,text:v.value,title:"Something went wrong."},null,8,["text"])):y("",!0),T.value?(o(),r("section",ie,[...t[7]||(t[7]=[e("div",{class:"banner plain"},[e("b",null,"This server has no variable store."),e("p",null,"It is an older build of pstack. Upgrade the host to use host-level variables.")],-1)])])):(o(),r(D,{key:2},[e("div",ue,[e("section",de,[e("div",ce,[t[9]||(t[9]=e("h2",{class:"section"},"Variables",-1)),u(S,{label:"what belongs here"},{default:m(()=>[...t[8]||(t[8]=[n(" Configuration that is fine to read back: a region, an image tag, the log level a preview should run at. Referenced as ",-1),e("code",null,"${vars.NAME}",-1),n(". ",-1)])]),_:1}),t[10]||(t[10]=e("span",{class:"grow"},null,-1)),e("span",ve,d(g.value.length),1)]),V.value?g.value.length?(o(),r("table",pe,[t[12]||(t[12]=e("thead",null,[e("tr",null,[e("th",null,"Name"),e("th",null,"Value"),e("th",null,"Updated"),e("th",{"aria-label":"Actions"})])],-1)),e("tbody",null,[(o(!0),r(D,null,L(g.value,l=>(o(),r("tr",{key:l.name},[e("td",me,[e("code",null,d(l.name),1)]),e("td",fe,d(l.value),1),e("td",he,[u(H,{at:l.updatedAt},null,8,["at"])]),e("td",be,[e("button",{class:"ghost sm",onClick:$=>I(l)},"Edit",8,ye),u(j,{class:"danger sm",confirm:`Delete ${l.name}?`,onRun:$=>B(l)},{default:m(()=>[...t[11]||(t[11]=[n(" Delete ",-1)])]),_:1},8,["confirm","onRun"])])]))),128))])])):(o(),r("p",_e,"No variables yet.")):(o(),C(W,{key:0,rows:2})),u(q,{pairs:F.value,busy:w.value,"skip-empty":"",onApply:J},{default:m(()=>[...t[13]||(t[13]=[e("p",{class:"hint",style:{"margin-top":"0"}}," Each name is stored on this host as a variable. A name already here is overwritten; names this paste does not mention are left alone, and nothing is deleted. ",-1)])]),_:1},8,["pairs","busy"])]),e("section",ge,[e("div",ke,[t[15]||(t[15]=e("h2",{class:"section"},"Secrets",-1)),u(S,{label:"what makes a secret different"},{default:m(()=>[...t[14]||(t[14]=[n(" The value is write-only: no page and no API route ever returns it, and it is scrubbed from job and container logs by content. Referenced as ",-1),e("code",null,"${secrets.NAME}",-1),n(" — refused in ",-1),e("code",null,"stack:",-1),n(", because a stack name appears in container names and hostnames that cannot be redacted. ",-1)])]),_:1}),t[16]||(t[16]=e("span",{class:"grow"},null,-1)),e("span",we,d(k.value.length),1)]),V.value?k.value.length?(o(),r("table",$e,[t[18]||(t[18]=e("thead",null,[e("tr",null,[e("th",null,"Name"),e("th",null,"Updated"),e("th",{"aria-label":"Actions"})])],-1)),e("tbody",null,[(o(!0),r(D,null,L(k.value,l=>(o(),r("tr",{key:l.name},[e("td",Ae,[e("code",null,d(l.name),1)]),e("td",Ee,[u(H,{at:l.updatedAt},null,8,["at"])]),e("td",Ve,[e("button",{class:"ghost sm",onClick:$=>I(l)},"Replace",8,xe),u(j,{class:"danger sm",confirm:`Delete ${l.name}?`,onRun:$=>B(l)},{default:m(()=>[...t[17]||(t[17]=[n(" Delete ",-1)])]),_:1},8,["confirm","onRun"])])]))),128))])])):(o(),r("p",Ne,"No secrets yet.")):(o(),C(W,{key:0,rows:2})),t[20]||(t[20]=e("p",{class:"hint"},[n(" A copied secret list contains "),e("b",null,"names only"),n(" — no page and no route can read a value back, so each line is exported empty for you to fill in. Pasting one back therefore changes nothing until you type the values in. ")],-1)),u(q,{pairs:O.value,note:qe,busy:w.value,"skip-empty":"",onApply:K},{default:m(()=>[...t[19]||(t[19]=[e("p",{class:"hint",style:{"margin-top":"0"}},[n(" Each name is stored as a "),e("b",null,"secret"),n(": write-only from the moment it lands. A name already here is replaced; a line with no value is skipped, so a round-trip of this page's own export leaves every secret as it was. ")],-1)])]),_:1},8,["pairs","busy"])])]),e("section",Se,[e("h2",Ce,d(i.value?i.value.secret?`Replace the value of ${i.value.name}`:`Edit ${i.value.name}`:"Add a variable or secret"),1),e("form",{onSubmit:X(G,["prevent"])},[e("div",De,[e("label",Me,[t[21]||(t[21]=e("span",null,"Name",-1)),M(e("input",{"onUpdate:modelValue":t[0]||(t[0]=l=>s.value.name=l),type:"text",placeholder:"DB_PASSWORD",spellcheck:"false",autocomplete:"off",disabled:!!i.value},null,8,Re),[[Y,s.value.name,void 0,{trim:!0}]])]),e("label",Ue,[t[22]||(t[22]=e("span",null,"Value",-1)),M(e("input",{"onUpdate:modelValue":t[1]||(t[1]=l=>s.value.value=l),type:s.value.secret?"password":"text",placeholder:(c=i.value)!=null&&c.secret?"enter the new value":"eu-central",spellcheck:"false",autocomplete:"off"},null,8,Te),[[ee,s.value.value]])])]),s.value.name&&!z.test(s.value.name)?(o(),r("p",ze," Letters, digits and _ only, not starting with a digit — it has to be referenceable in a spec. ")):y("",!0),e("div",Ie,[e("label",Pe,[M(e("input",{"onUpdate:modelValue":t[2]||(t[2]=l=>s.value.secret=l),type:"checkbox",disabled:((b=i.value)==null?void 0:b.secret)===!0},null,8,Be),[[te,s.value.secret]]),t[23]||(t[23]=n(" Secret — the value can never be read back ",-1))])]),(f=i.value)!=null&&f.secret?(o(),r("p",Le," A secret cannot be turned back into a readable variable — that would reveal a value stored write-only. Delete it and create a variable instead, re-entering the value. ")):y("",!0),e("div",je,[e("button",{class:"primary",type:"submit",disabled:!Z.value},d(i.value?"Save":"Add"),9,He),i.value?(o(),r("button",{key:0,type:"button",class:"ghost",onClick:N},"Cancel")):y("",!0),i.value?y("",!0):(o(),r("span",We,[t[24]||(t[24]=n(" Reference it as ",-1)),e("code",null,"$"+d("{")+d(s.value.secret?"secrets":"vars")+"."+d(s.value.name||"NAME")+d("}"),1),t[25]||(t[25]=n(" in any spec ",-1))]))]),u(ae,{what:"storing this value",method:"PUT",path:`/api/host-vars/${s.value.name||"NAME"}`,body:{value:s.value.secret?"…":s.value.value||"…",secret:s.value.secret}},null,8,["path","body"])],32)])],64))])}}}),lt=ne(Fe,[["__scopeId","data-v-cf1f35b2"]]);export{lt as default};
|
|
3
|
-
//# sourceMappingURL=VariablesView-
|
|
3
|
+
//# sourceMappingURL=VariablesView-CH3Ktrp1.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VariablesView-CzeCc-0m.js","sources":["../../src/views/VariablesView.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Host variables & secrets — the GitHub model, scoped to this host.\n *\n * TWO SECTIONS BECAUSE THEY ARE TWO CONTRACTS. A variable's value is configuration anyone may read\n * back; a secret's value goes IN through this form and never comes back OUT — the list shows its\n * name and when it changed, nothing else, because the server keeps no route that returns it. The\n * form says so at the moment of storing, not in documentation nobody reads.\n *\n * Specs reference these as `${vars.NAME}` and `${secrets.NAME}` — spelled differently from plain\n * `${NAME}` on purpose, so a spec that reads host state says so on its face.\n *\n * THE TWO CONTRACTS DECIDE WHAT `VarIO` MAY DO HERE, and each panel mounts its own:\n * - A copied secret list carries NAMES ONLY. There is no value to copy — the API never returns\n * one — and copying the mask would put `••••••••` in a `.env` someone then deploys with,\n * which is a real value that fails somewhere far away from here. Empty is honest, and pstack\n * treats an empty variable as undefined (invariant 7), so a paste of one fails loudly at\n * resolve time instead of quietly running with a wrong value.\n * - An import therefore SKIPS an entry with no value rather than storing `\"\"` over a live\n * secret. Copy this page's secrets and paste them straight back and nothing changes — which is\n * the only safe answer for an export that cannot contain what it lists.\n * - Only merge is offered. A \"replace\" would have to delete the names a paste omits, and this\n * page's deletes are one confirmed click each for a reason.\n */\nimport { computed, ref } from 'vue';\nimport { api, problem } from '../api/client';\nimport type { VarPair } from '../api/types';\nimport { toast } from '../composables/useToasts';\nimport ActionButton from '../components/ActionButton.vue';\nimport EquivalentCommand from '../components/EquivalentCommand.vue';\nimport ErrorNote from '../components/ErrorNote.vue';\nimport InfoHint from '../components/InfoHint.vue';\nimport RelativeTime from '../components/RelativeTime.vue';\nimport SkeletonList from '../components/SkeletonList.vue';\nimport RefreshButton from '../components/RefreshButton.vue';\nimport VarIO from '../components/VarIO.vue';\n\ntype Entry = { name: string; value: string | null; secret: boolean; updatedAt: number };\n\nconst entries = ref<Entry[]>([]);\nconst loaded = ref(false);\nconst listError = ref('');\nconst unsupported = ref(false);\n\nconst vars = computed(() => entries.value.filter((e) => !e.secret));\nconst secrets = computed(() => entries.value.filter((e) => e.secret));\n\nconst varPairs = computed<VarPair[]>(() => vars.value.map((e) => ({ k: e.name, v: e.value ?? '' })));\n/** Names with no values — see the header. The comment travels with the `.env` export. */\nconst secretPairs = computed<VarPair[]>(() => secrets.value.map((e) => ({ k: e.name, v: '' })));\nconst WITHHELD =\n 'Secret VALUES are not included: this server has no route that returns one.\\nEach name is listed with an empty value for you to fill in.';\n\nconst form = ref({ name: '', value: '', secret: false });\nconst saving = ref(false);\nconst importing = ref(false);\n/** Editing state: the row being replaced. For a secret this means re-entering the value. */\nconst editing = ref<Entry | null>(null);\n\nconst NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst canSave = computed(\n () => !saving.value && NAME.test(form.value.name) && form.value.value.length > 0,\n);\n\nasync function load(): Promise<void> {\n const r = await api.get<{ entries: Entry[] }>('/api/host-vars');\n loaded.value = true;\n if (r.status === 404) {\n unsupported.value = true;\n return;\n }\n if (!r.ok) {\n listError.value = problem(r, 'load the variables');\n return;\n }\n listError.value = '';\n entries.value = r.body.entries ?? [];\n}\nvoid load();\n\nfunction startEdit(e: Entry): void {\n editing.value = e;\n form.value = { name: e.name, value: e.value ?? '', secret: e.secret };\n}\n\nfunction cancelEdit(): void {\n editing.value = null;\n form.value = { name: '', value: '', secret: false };\n}\n\nasync function save(): Promise<void> {\n saving.value = true;\n const r = await api.put(`/api/host-vars/${form.value.name}`, {\n value: form.value.value,\n secret: form.value.secret,\n });\n saving.value = false;\n if (!r.ok) {\n listError.value = problem(r, `store ${form.value.name}`);\n return;\n }\n listError.value = '';\n toast('ok', `Stored ${form.value.name}.`);\n cancelEdit();\n void load();\n}\n\n/**\n * One PUT per entry: there is no bulk route, and inventing one so a paste box can be one request\n * is a route this feature does not need. Sequential, and every failure is named — \"3 of 5 stored,\n * these 2 failed\" is something an operator can act on; a single red toast is not.\n */\nasync function importEntries(secret: boolean, pairs: VarPair[]): Promise<void> {\n importing.value = true;\n const failed: string[] = [];\n for (const e of pairs) {\n const r = await api.put(`/api/host-vars/${encodeURIComponent(e.k)}`, { value: e.v, secret });\n if (!r.ok) failed.push(e.k);\n }\n importing.value = false;\n const noun = secret ? 'secret' : 'variable';\n const stored = pairs.length - failed.length;\n if (failed.length) {\n listError.value = `Stored ${stored} of ${pairs.length} ${noun}s — ${failed.join(', ')} could not be stored.`;\n } else {\n listError.value = '';\n toast('ok', `Stored ${stored} ${noun}${stored === 1 ? '' : 's'}.`);\n }\n void load();\n}\n\nconst importVars = (pairs: VarPair[]): Promise<void> => importEntries(false, pairs);\nconst importSecrets = (pairs: VarPair[]): Promise<void> => importEntries(true, pairs);\n\nasync function remove(e: Entry): Promise<void> {\n const r = await api.del(`/api/host-vars/${e.name}`);\n if (!r.ok) {\n listError.value = problem(r, `delete ${e.name}`);\n return;\n }\n toast('ok', `Deleted ${e.name}. Specs referencing it will fail to resolve until it exists again.`);\n if (editing.value?.name === e.name) cancelEdit();\n void load();\n}\n</script>\n\n<template>\n <div>\n <div class=\"page-head\">\n <div>\n <h1>Variables & secrets</h1>\n <div class=\"sub\">\n Host-level values every spec can reference\n <InfoHint label=\"how a spec uses these\">\n Write <code>${vars.NAME}</code> or <code>${secrets.NAME}</code> anywhere a spec\n interpolates — the <code>env:</code> block, hooks, compose settings. The spelling is\n deliberate: plain <code>${NAME}</code> keeps meaning \"whatever the request supplied\", so\n the two can never collide.\n </InfoHint>\n </div>\n </div>\n <span class=\"grow\" />\n <RefreshButton :run=\"load\" />\n </div>\n\n <ErrorNote v-if=\"listError\" :text=\"listError\" title=\"Something went wrong.\" />\n\n <section v-if=\"unsupported\" class=\"panel\">\n <div class=\"banner plain\">\n <b>This server has no variable store.</b>\n <p>It is an older build of pstack. Upgrade the host to use host-level variables.</p>\n </div>\n </section>\n\n <template v-else>\n <div class=\"grid-2\">\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Variables</h2>\n <InfoHint label=\"what belongs here\">\n Configuration that is fine to read back: a region, an image tag, the log level a\n preview should run at. Referenced as <code>${vars.NAME}</code>.\n </InfoHint>\n <span class=\"grow\" />\n <span class=\"mute\" style=\"font-size: var(--t-xs)\">{{ vars.length }}</span>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"2\" />\n <table v-else-if=\"vars.length\" class=\"cards\">\n <thead>\n <tr>\n <th>Name</th>\n <th>Value</th>\n <th>Updated</th>\n <th aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"e in vars\" :key=\"e.name\">\n <td class=\"name\" data-label=\"name\"><code>{{ e.name }}</code></td>\n <td class=\"break\" data-label=\"value\">{{ e.value }}</td>\n <td class=\"dim nowrap\" data-label=\"updated\"><RelativeTime :at=\"e.updatedAt\" /></td>\n <td class=\"right nowrap\" data-label=\"\">\n <button class=\"ghost sm\" @click=\"startEdit(e)\">Edit</button>\n <ActionButton class=\"danger sm\" :confirm=\"`Delete ${e.name}?`\" @run=\"remove(e)\">\n Delete\n </ActionButton>\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"hint\">No variables yet.</p>\n\n <VarIO :pairs=\"varPairs\" :busy=\"importing\" skip-empty @apply=\"importVars\">\n <p class=\"hint\" style=\"margin-top: 0\">\n Each name is stored on this host as a variable. A name already here is overwritten;\n names this paste does not mention are left alone, and nothing is deleted.\n </p>\n </VarIO>\n </section>\n\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Secrets</h2>\n <InfoHint label=\"what makes a secret different\">\n The value is write-only: no page and no API route ever returns it, and it is scrubbed\n from job and container logs by content. Referenced as <code>${secrets.NAME}</code> —\n refused in <code>stack:</code>, because a stack name appears in container names and\n hostnames that cannot be redacted.\n </InfoHint>\n <span class=\"grow\" />\n <span class=\"mute\" style=\"font-size: var(--t-xs)\">{{ secrets.length }}</span>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"2\" />\n <table v-else-if=\"secrets.length\" class=\"cards\">\n <thead>\n <tr>\n <th>Name</th>\n <th>Updated</th>\n <th aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"e in secrets\" :key=\"e.name\">\n <td class=\"name\" data-label=\"name\"><code>{{ e.name }}</code></td>\n <td class=\"dim nowrap\" data-label=\"updated\"><RelativeTime :at=\"e.updatedAt\" /></td>\n <td class=\"right nowrap\" data-label=\"\">\n <button class=\"ghost sm\" @click=\"startEdit(e)\">Replace</button>\n <ActionButton class=\"danger sm\" :confirm=\"`Delete ${e.name}?`\" @run=\"remove(e)\">\n Delete\n </ActionButton>\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"hint\">No secrets yet.</p>\n\n <!--\n Said on the page, not only in the copied file: an export that cannot contain the thing\n it lists has to admit that where the button is.\n -->\n <p class=\"hint\">\n A copied secret list contains <b>names only</b> — no page and no route can read a value\n back, so each line is exported empty for you to fill in. Pasting one back therefore\n changes nothing until you type the values in.\n </p>\n <VarIO\n :pairs=\"secretPairs\"\n :note=\"WITHHELD\"\n :busy=\"importing\"\n skip-empty\n @apply=\"importSecrets\"\n >\n <p class=\"hint\" style=\"margin-top: 0\">\n Each name is stored as a <b>secret</b>: write-only from the moment it lands. A name\n already here is replaced; a line with no value is skipped, so a round-trip of this\n page's own export leaves every secret as it was.\n </p>\n </VarIO>\n </section>\n </div>\n\n <section class=\"panel\">\n <h2 class=\"section\" style=\"margin-bottom: var(--s3)\">\n {{ editing ? (editing.secret ? `Replace the value of ${editing.name}` : `Edit ${editing.name}`) : 'Add a variable or secret' }}\n </h2>\n\n <form @submit.prevent=\"save\">\n <div class=\"row wrap two-up\">\n <label class=\"field grow\">\n <span>Name</span>\n <input\n v-model.trim=\"form.name\"\n type=\"text\"\n placeholder=\"DB_PASSWORD\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n :disabled=\"!!editing\"\n />\n </label>\n <label class=\"field grow\">\n <span>Value</span>\n <input\n v-model=\"form.value\"\n :type=\"form.secret ? 'password' : 'text'\"\n :placeholder=\"editing?.secret ? 'enter the new value' : 'eu-central'\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n />\n </label>\n </div>\n <p v-if=\"form.name && !NAME.test(form.name)\" class=\"s-failed\" style=\"margin-top: var(--s2)\">\n Letters, digits and _ only, not starting with a digit — it has to be referenceable in a spec.\n </p>\n\n <div class=\"row\" style=\"margin-top: var(--s3)\">\n <label class=\"check\">\n <input v-model=\"form.secret\" type=\"checkbox\" :disabled=\"editing?.secret === true\" />\n Secret — the value can never be read back\n </label>\n </div>\n <p v-if=\"editing?.secret\" class=\"hint\">\n A secret cannot be turned back into a readable variable — that would reveal a value\n stored write-only. Delete it and create a variable instead, re-entering the value.\n </p>\n\n <div class=\"row\" style=\"margin-top: var(--s4)\">\n <button class=\"primary\" type=\"submit\" :disabled=\"!canSave\">\n {{ editing ? 'Save' : 'Add' }}\n </button>\n <button v-if=\"editing\" type=\"button\" class=\"ghost\" @click=\"cancelEdit\">Cancel</button>\n <span v-if=\"!editing\" class=\"mute\" style=\"font-size: var(--t-sm)\">\n Reference it as <code>${{ '{' }}{{ form.secret ? 'secrets' : 'vars' }}.{{ form.name || 'NAME' }}{{ '}' }}</code> in any spec\n </span>\n </div>\n <EquivalentCommand\n what=\"storing this value\"\n method=\"PUT\"\n :path=\"`/api/host-vars/${form.name || 'NAME'}`\"\n :body=\"{ value: form.secret ? '…' : form.value || '…', secret: form.secret }\"\n />\n </form>\n </section>\n </template>\n </div>\n</template>\n\n<style scoped>\ntd.right {\n text-align: end;\n}\ntd.right button {\n margin-inline-start: var(--s2);\n}\n.two-up > .field {\n /* A form caps itself even though the page does not — a 1300px-wide text input reads as a text\n editor, not a field. See docs/ui-rules.md. */\n flex: 1 1 200px;\n max-width: 420px;\n}\n</style>\n"],"names":["WITHHELD","entries","ref","loaded","listError","unsupported","vars","computed","e","secrets","varPairs","secretPairs","form","saving","importing","editing","NAME","canSave","load","r","api","problem","startEdit","cancelEdit","save","toast","importEntries","secret","pairs","failed","noun","stored","importVars","importSecrets","remove","_a","_createElementBlock","_createElementVNode","_hoisted_1","_cache","_hoisted_2","_createVNode","InfoHint","RefreshButton","_createBlock","ErrorNote","_openBlock","_hoisted_3","_Fragment","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_toDisplayString","_hoisted_8","_renderList","_hoisted_9","_hoisted_10","_hoisted_11","RelativeTime","_hoisted_12","$event","_hoisted_13","ActionButton","_hoisted_14","SkeletonList","VarIO","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","_hoisted_27","_hoisted_29","_vModelDynamic","_hoisted_31","_hoisted_32","_hoisted_33","_b","_vModelCheckbox","_c","_hoisted_35","_hoisted_36","_hoisted_37","_hoisted_38","EquivalentCommand"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VariablesView-CH3Ktrp1.js","sources":["../../src/views/VariablesView.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Host variables & secrets — the GitHub model, scoped to this host.\n *\n * TWO SECTIONS BECAUSE THEY ARE TWO CONTRACTS. A variable's value is configuration anyone may read\n * back; a secret's value goes IN through this form and never comes back OUT — the list shows its\n * name and when it changed, nothing else, because the server keeps no route that returns it. The\n * form says so at the moment of storing, not in documentation nobody reads.\n *\n * Specs reference these as `${vars.NAME}` and `${secrets.NAME}` — spelled differently from plain\n * `${NAME}` on purpose, so a spec that reads host state says so on its face.\n *\n * THE TWO CONTRACTS DECIDE WHAT `VarIO` MAY DO HERE, and each panel mounts its own:\n * - A copied secret list carries NAMES ONLY. There is no value to copy — the API never returns\n * one — and copying the mask would put `••••••••` in a `.env` someone then deploys with,\n * which is a real value that fails somewhere far away from here. Empty is honest, and pstack\n * treats an empty variable as undefined (invariant 7), so a paste of one fails loudly at\n * resolve time instead of quietly running with a wrong value.\n * - An import therefore SKIPS an entry with no value rather than storing `\"\"` over a live\n * secret. Copy this page's secrets and paste them straight back and nothing changes — which is\n * the only safe answer for an export that cannot contain what it lists.\n * - Only merge is offered. A \"replace\" would have to delete the names a paste omits, and this\n * page's deletes are one confirmed click each for a reason.\n */\nimport { computed, ref } from 'vue';\nimport { api, problem } from '../api/client';\nimport type { VarPair } from '../api/types';\nimport { toast } from '../composables/useToasts';\nimport ActionButton from '../components/ActionButton.vue';\nimport EquivalentCommand from '../components/EquivalentCommand.vue';\nimport ErrorNote from '../components/ErrorNote.vue';\nimport InfoHint from '../components/InfoHint.vue';\nimport RelativeTime from '../components/RelativeTime.vue';\nimport SkeletonList from '../components/SkeletonList.vue';\nimport RefreshButton from '../components/RefreshButton.vue';\nimport VarIO from '../components/VarIO.vue';\n\ntype Entry = { name: string; value: string | null; secret: boolean; updatedAt: number };\n\nconst entries = ref<Entry[]>([]);\nconst loaded = ref(false);\nconst listError = ref('');\nconst unsupported = ref(false);\n\nconst vars = computed(() => entries.value.filter((e) => !e.secret));\nconst secrets = computed(() => entries.value.filter((e) => e.secret));\n\nconst varPairs = computed<VarPair[]>(() => vars.value.map((e) => ({ k: e.name, v: e.value ?? '' })));\n/** Names with no values — see the header. The comment travels with the `.env` export. */\nconst secretPairs = computed<VarPair[]>(() => secrets.value.map((e) => ({ k: e.name, v: '' })));\nconst WITHHELD =\n 'Secret VALUES are not included: this server has no route that returns one.\\nEach name is listed with an empty value for you to fill in.';\n\nconst form = ref({ name: '', value: '', secret: false });\nconst saving = ref(false);\nconst importing = ref(false);\n/** Editing state: the row being replaced. For a secret this means re-entering the value. */\nconst editing = ref<Entry | null>(null);\n\nconst NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst canSave = computed(\n () => !saving.value && NAME.test(form.value.name) && form.value.value.length > 0,\n);\n\nasync function load(): Promise<void> {\n const r = await api.get<{ entries: Entry[] }>('/api/host-vars');\n loaded.value = true;\n if (r.status === 404) {\n unsupported.value = true;\n return;\n }\n if (!r.ok) {\n listError.value = problem(r, 'load the variables');\n return;\n }\n listError.value = '';\n entries.value = r.body.entries ?? [];\n}\nvoid load();\n\nfunction startEdit(e: Entry): void {\n editing.value = e;\n form.value = { name: e.name, value: e.value ?? '', secret: e.secret };\n}\n\nfunction cancelEdit(): void {\n editing.value = null;\n form.value = { name: '', value: '', secret: false };\n}\n\nasync function save(): Promise<void> {\n saving.value = true;\n const r = await api.put(`/api/host-vars/${form.value.name}`, {\n value: form.value.value,\n secret: form.value.secret,\n });\n saving.value = false;\n if (!r.ok) {\n listError.value = problem(r, `store ${form.value.name}`);\n return;\n }\n listError.value = '';\n toast('ok', `Stored ${form.value.name}.`);\n cancelEdit();\n void load();\n}\n\n/**\n * One PUT per entry: there is no bulk route, and inventing one so a paste box can be one request\n * is a route this feature does not need. Sequential, and every failure is named — \"3 of 5 stored,\n * these 2 failed\" is something an operator can act on; a single red toast is not.\n */\nasync function importEntries(secret: boolean, pairs: VarPair[]): Promise<void> {\n importing.value = true;\n const failed: string[] = [];\n for (const e of pairs) {\n const r = await api.put(`/api/host-vars/${encodeURIComponent(e.k)}`, { value: e.v, secret });\n if (!r.ok) failed.push(e.k);\n }\n importing.value = false;\n const noun = secret ? 'secret' : 'variable';\n const stored = pairs.length - failed.length;\n if (failed.length) {\n listError.value = `Stored ${stored} of ${pairs.length} ${noun}s — ${failed.join(', ')} could not be stored.`;\n } else {\n listError.value = '';\n toast('ok', `Stored ${stored} ${noun}${stored === 1 ? '' : 's'}.`);\n }\n void load();\n}\n\nconst importVars = (pairs: VarPair[]): Promise<void> => importEntries(false, pairs);\nconst importSecrets = (pairs: VarPair[]): Promise<void> => importEntries(true, pairs);\n\nasync function remove(e: Entry): Promise<void> {\n const r = await api.del(`/api/host-vars/${e.name}`);\n if (!r.ok) {\n listError.value = problem(r, `delete ${e.name}`);\n return;\n }\n toast('ok', `Deleted ${e.name}. Specs referencing it will fail to resolve until it exists again.`);\n if (editing.value?.name === e.name) cancelEdit();\n void load();\n}\n</script>\n\n<template>\n <div>\n <div class=\"page-head\">\n <div>\n <h1>Variables & secrets</h1>\n <div class=\"sub\">\n Host-level values every spec can reference\n <InfoHint label=\"how a spec uses these\">\n Write <code>${vars.NAME}</code> or <code>${secrets.NAME}</code> anywhere a spec\n interpolates — the <code>env:</code> block, hooks, compose settings. The spelling is\n deliberate: plain <code>${NAME}</code> keeps meaning \"whatever the request supplied\", so\n the two can never collide.\n </InfoHint>\n </div>\n </div>\n <span class=\"grow\" />\n <RefreshButton :run=\"load\" />\n </div>\n\n <ErrorNote v-if=\"listError\" :text=\"listError\" title=\"Something went wrong.\" />\n\n <section v-if=\"unsupported\" class=\"panel\">\n <div class=\"banner plain\">\n <b>This server has no variable store.</b>\n <p>It is an older build of pstack. Upgrade the host to use host-level variables.</p>\n </div>\n </section>\n\n <template v-else>\n <div class=\"grid-2\">\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Variables</h2>\n <InfoHint label=\"what belongs here\">\n Configuration that is fine to read back: a region, an image tag, the log level a\n preview should run at. Referenced as <code>${vars.NAME}</code>.\n </InfoHint>\n <span class=\"grow\" />\n <span class=\"mute\" style=\"font-size: var(--t-xs)\">{{ vars.length }}</span>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"2\" />\n <table v-else-if=\"vars.length\" class=\"cards\">\n <thead>\n <tr>\n <th>Name</th>\n <th>Value</th>\n <th>Updated</th>\n <th aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"e in vars\" :key=\"e.name\">\n <td class=\"name\" data-label=\"name\"><code>{{ e.name }}</code></td>\n <td class=\"break\" data-label=\"value\">{{ e.value }}</td>\n <td class=\"dim nowrap\" data-label=\"updated\"><RelativeTime :at=\"e.updatedAt\" /></td>\n <td class=\"right nowrap\" data-label=\"\">\n <button class=\"ghost sm\" @click=\"startEdit(e)\">Edit</button>\n <ActionButton class=\"danger sm\" :confirm=\"`Delete ${e.name}?`\" @run=\"remove(e)\">\n Delete\n </ActionButton>\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"hint\">No variables yet.</p>\n\n <VarIO :pairs=\"varPairs\" :busy=\"importing\" skip-empty @apply=\"importVars\">\n <p class=\"hint\" style=\"margin-top: 0\">\n Each name is stored on this host as a variable. A name already here is overwritten;\n names this paste does not mention are left alone, and nothing is deleted.\n </p>\n </VarIO>\n </section>\n\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Secrets</h2>\n <InfoHint label=\"what makes a secret different\">\n The value is write-only: no page and no API route ever returns it, and it is scrubbed\n from job and container logs by content. Referenced as <code>${secrets.NAME}</code> —\n refused in <code>stack:</code>, because a stack name appears in container names and\n hostnames that cannot be redacted.\n </InfoHint>\n <span class=\"grow\" />\n <span class=\"mute\" style=\"font-size: var(--t-xs)\">{{ secrets.length }}</span>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"2\" />\n <table v-else-if=\"secrets.length\" class=\"cards\">\n <thead>\n <tr>\n <th>Name</th>\n <th>Updated</th>\n <th aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"e in secrets\" :key=\"e.name\">\n <td class=\"name\" data-label=\"name\"><code>{{ e.name }}</code></td>\n <td class=\"dim nowrap\" data-label=\"updated\"><RelativeTime :at=\"e.updatedAt\" /></td>\n <td class=\"right nowrap\" data-label=\"\">\n <button class=\"ghost sm\" @click=\"startEdit(e)\">Replace</button>\n <ActionButton class=\"danger sm\" :confirm=\"`Delete ${e.name}?`\" @run=\"remove(e)\">\n Delete\n </ActionButton>\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"hint\">No secrets yet.</p>\n\n <!--\n Said on the page, not only in the copied file: an export that cannot contain the thing\n it lists has to admit that where the button is.\n -->\n <p class=\"hint\">\n A copied secret list contains <b>names only</b> — no page and no route can read a value\n back, so each line is exported empty for you to fill in. Pasting one back therefore\n changes nothing until you type the values in.\n </p>\n <VarIO\n :pairs=\"secretPairs\"\n :note=\"WITHHELD\"\n :busy=\"importing\"\n skip-empty\n @apply=\"importSecrets\"\n >\n <p class=\"hint\" style=\"margin-top: 0\">\n Each name is stored as a <b>secret</b>: write-only from the moment it lands. A name\n already here is replaced; a line with no value is skipped, so a round-trip of this\n page's own export leaves every secret as it was.\n </p>\n </VarIO>\n </section>\n </div>\n\n <section class=\"panel\">\n <h2 class=\"section\" style=\"margin-bottom: var(--s3)\">\n {{ editing ? (editing.secret ? `Replace the value of ${editing.name}` : `Edit ${editing.name}`) : 'Add a variable or secret' }}\n </h2>\n\n <form @submit.prevent=\"save\">\n <div class=\"row wrap two-up\">\n <label class=\"field grow\">\n <span>Name</span>\n <input\n v-model.trim=\"form.name\"\n type=\"text\"\n placeholder=\"DB_PASSWORD\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n :disabled=\"!!editing\"\n />\n </label>\n <label class=\"field grow\">\n <span>Value</span>\n <input\n v-model=\"form.value\"\n :type=\"form.secret ? 'password' : 'text'\"\n :placeholder=\"editing?.secret ? 'enter the new value' : 'eu-central'\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n />\n </label>\n </div>\n <p v-if=\"form.name && !NAME.test(form.name)\" class=\"s-failed\" style=\"margin-top: var(--s2)\">\n Letters, digits and _ only, not starting with a digit — it has to be referenceable in a spec.\n </p>\n\n <div class=\"row\" style=\"margin-top: var(--s3)\">\n <label class=\"check\">\n <input v-model=\"form.secret\" type=\"checkbox\" :disabled=\"editing?.secret === true\" />\n Secret — the value can never be read back\n </label>\n </div>\n <p v-if=\"editing?.secret\" class=\"hint\">\n A secret cannot be turned back into a readable variable — that would reveal a value\n stored write-only. Delete it and create a variable instead, re-entering the value.\n </p>\n\n <div class=\"row\" style=\"margin-top: var(--s4)\">\n <button class=\"primary\" type=\"submit\" :disabled=\"!canSave\">\n {{ editing ? 'Save' : 'Add' }}\n </button>\n <button v-if=\"editing\" type=\"button\" class=\"ghost\" @click=\"cancelEdit\">Cancel</button>\n <span v-if=\"!editing\" class=\"mute\" style=\"font-size: var(--t-sm)\">\n Reference it as <code>${{ '{' }}{{ form.secret ? 'secrets' : 'vars' }}.{{ form.name || 'NAME' }}{{ '}' }}</code> in any spec\n </span>\n </div>\n <EquivalentCommand\n what=\"storing this value\"\n method=\"PUT\"\n :path=\"`/api/host-vars/${form.name || 'NAME'}`\"\n :body=\"{ value: form.secret ? '…' : form.value || '…', secret: form.secret }\"\n />\n </form>\n </section>\n </template>\n </div>\n</template>\n\n<style scoped>\ntd.right {\n text-align: end;\n}\ntd.right button {\n margin-inline-start: var(--s2);\n}\n.two-up > .field {\n /* A form caps itself even though the page does not — a 1300px-wide text input reads as a text\n editor, not a field. See docs/ui-rules.md. */\n flex: 1 1 200px;\n max-width: 420px;\n}\n</style>\n"],"names":["WITHHELD","entries","ref","loaded","listError","unsupported","vars","computed","e","secrets","varPairs","secretPairs","form","saving","importing","editing","NAME","canSave","load","r","api","problem","startEdit","cancelEdit","save","toast","importEntries","secret","pairs","failed","noun","stored","importVars","importSecrets","remove","_a","_createElementBlock","_createElementVNode","_hoisted_1","_cache","_hoisted_2","_createVNode","InfoHint","RefreshButton","_createBlock","ErrorNote","_openBlock","_hoisted_3","_Fragment","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_toDisplayString","_hoisted_8","_renderList","_hoisted_9","_hoisted_10","_hoisted_11","RelativeTime","_hoisted_12","$event","_hoisted_13","ActionButton","_hoisted_14","SkeletonList","VarIO","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","_hoisted_27","_hoisted_29","_vModelDynamic","_hoisted_31","_hoisted_32","_hoisted_33","_b","_vModelCheckbox","_c","_hoisted_35","_hoisted_36","_hoisted_37","_hoisted_38","EquivalentCommand"],"mappings":"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"}
|