isotone 1.1.3 → 1.2.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/AGENTS.md +79 -5
- package/CHANGELOG.md +34 -0
- package/README.md +64 -14
- package/dist/{IterationPanel-phJRYtUO.d.ts → IterationPanel-Bq77d5NT.d.ts} +3 -1
- package/dist/{chunk-K5RAC2BT.js → chunk-2FY2JNPG.js} +3 -5
- package/dist/chunk-2FY2JNPG.js.map +1 -0
- package/dist/{chunk-RKZWO37H.js → chunk-BTJHQXF7.js} +85 -1
- package/dist/chunk-BTJHQXF7.js.map +1 -0
- package/dist/chunk-JCTWHR7U.js +13 -0
- package/dist/chunk-JCTWHR7U.js.map +1 -0
- package/dist/{chunk-YTEUMO3O.js → chunk-KRS57POF.js} +17 -33
- package/dist/chunk-KRS57POF.js.map +1 -0
- package/dist/chunk-L6O4HYGP.js +1150 -0
- package/dist/chunk-L6O4HYGP.js.map +1 -0
- package/dist/chunk-MX4RQJFV.js +9 -0
- package/dist/chunk-MX4RQJFV.js.map +1 -0
- package/dist/chunk-YPDV3PX4.js +142 -0
- package/dist/chunk-YPDV3PX4.js.map +1 -0
- package/dist/cli.js +1040 -148
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +92 -2
- package/dist/core.js +10 -3
- package/dist/index.d.ts +13 -28
- package/dist/index.js +586 -257
- package/dist/index.js.map +1 -1
- package/dist/isotone.global.js +98 -42
- package/dist/serve.d.ts +5 -3
- package/dist/serve.js +60 -23
- package/dist/serve.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.js +24 -11
- package/dist/server.js.map +1 -1
- package/dist/vite-plugin.d.ts +4 -3
- package/dist/vite-plugin.js +23 -19
- package/dist/vite-plugin.js.map +1 -1
- package/docs/blast.md +164 -0
- package/docs/integrations.md +75 -15
- package/docs/migration.md +19 -0
- package/docs/updates.md +110 -4
- package/docs/variants.md +65 -7
- package/package.json +2 -1
- package/dist/chunk-A4DX2V4C.js +0 -7
- package/dist/chunk-A4DX2V4C.js.map +0 -1
- package/dist/chunk-BRBXIGZP.js +0 -593
- package/dist/chunk-BRBXIGZP.js.map +0 -1
- package/dist/chunk-K5RAC2BT.js.map +0 -1
- package/dist/chunk-LXW6WFOC.js +0 -402
- package/dist/chunk-LXW6WFOC.js.map +0 -1
- package/dist/chunk-RKZWO37H.js.map +0 -1
- package/dist/chunk-RQ2YI5KS.js +0 -24
- package/dist/chunk-RQ2YI5KS.js.map +0 -1
- package/dist/chunk-YTEUMO3O.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/variants-store.ts"],"sourcesContent":["/**\n * isotone — overlay variants store (node only).\n *\n * An overlay variant is a directory of alternative source files under\n * `.isotone/variants/<id>/`, mirrored at their repo-relative paths, plus\n * a `variant.json` manifest recording which git blob each override was made\n * from. That blob is the three-way merge base: when the real file moves on,\n * `syncVariant` diffs the override against what it started from and what\n * the file is now, the same way a branch rebases. Everything here is\n * synchronous and framework-free so the CLI and the Vite plugin (a\n * different owner) can both sit on top of it.\n */\nimport { execFileSync } from 'node:child_process';\nimport { createHash, randomUUID } from 'node:crypto';\nimport {\n existsSync,\n mkdirSync,\n lstatSync,\n readdirSync,\n readFileSync,\n realpathSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\n\nimport type { IterationElement } from './core/index';\nimport {\n type ColorVariantAxis,\n type KnobAxis,\n OVERLAY_GROUP_ID,\n overlayGroups,\n type OverlayVariant,\n type OverlayVariantStatus,\n type RangeVariantAxis,\n type SelectVariantAxis,\n} from './core/variants';\nimport { ensureFileIgnored, type NotesRequest, type NotesResponse } from './store';\nimport { withFileLock, writeJson } from './file-store';\n\n/** Default location of the variants tree, relative to the project root. */\nexport const DEFAULT_VARIANTS_DIR = '.isotone/variants';\n\n/** Relative to root. Holds `{ \"active\": [\"<id>\", ...] }`, at most one id per\n * group. The Vite plugin keeps this out of git; nothing here assumes that. */\nexport const ACTIVE_FILE = '.isotone/active.json';\n\n/** Relative to root. A JSON array of {@link KnobAxis}; committed, since the\n * defaults in it are part of the design. Missing or malformed reads as none. */\nexport const KNOBS_FILE = '.isotone/knobs.json';\n\nexport interface VariantsStoreOptions {\n /** Project root: Vite's root or the CLI's cwd. Absolute. */\n root: string;\n /** Variants directory, relative to root or absolute. Default {@link DEFAULT_VARIANTS_DIR}. */\n dir?: string;\n}\n\ninterface ActiveFileShape {\n active: string[];\n}\n\n/** Agents in separate processes may edit the same manifest. Keep their\n * read-modify-write operations together, including promotion and removal. */\nfunction mutateVariants<T>(opts: VariantsStoreOptions, change: () => T): T {\n return withFileLock(join(variantsDir(opts), '.mutation'), change);\n}\n\n/* ── paths ────────────────────────────────────────────────────────────── */\n\nexport function variantsDir(opts: VariantsStoreOptions): string {\n const dir = opts.dir ?? DEFAULT_VARIANTS_DIR;\n return isAbsolute(dir) ? dir : resolve(opts.root, dir);\n}\n\nexport function variantDir(opts: VariantsStoreOptions, id: string): string {\n if (!isValidVariantId(id)) throw new Error(`Invalid variant id \"${id}\"`);\n return join(variantsDir(opts), id);\n}\n\nexport function isValidVariantId(id: string): boolean {\n return /^[a-z0-9][a-z0-9-]{0,63}$/.test(id);\n}\n\n/** POSIX-ify a path node gave us as platform-native (a no-op on POSIX,\n * where `sep` already is `/`; keeps manifest keys portable regardless). */\nfunction toPosixPath(p: string): string {\n return p.split(sep).join('/');\n}\n\n/** `relative()` puts you outside the base with a leading `..` (or, on a\n * different drive on Windows, an absolute path back out) — either way the\n * caller asked for something outside the tree we're allowed to touch. */\nfunction isEscaping(rel: string): boolean {\n return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel);\n}\n\n/** Check existing ancestors too: a lexical child may be a symlink outside\n * the tree, including a not-yet-created file beneath a symlinked directory. */\nfunction staysInside(base: string, file: string): boolean {\n const canonical = (path: string): string => {\n let existing = resolve(path);\n while (dirname(existing) !== existing) {\n try { lstatSync(existing); break; } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n existing = dirname(existing);\n }\n }\n return resolve(realpathSync(existing), relative(existing, resolve(path)));\n };\n return !isEscaping(relative(canonical(base), canonical(file)));\n}\n\n/**\n * Resolve `file` (repo-relative, or absolute inside root) against `root`\n * and return both the absolute path and its POSIX repo-relative manifest\n * key. Throws for anything that escapes root, so every caller that takes a\n * user-supplied path gets the same guard.\n */\nfunction repoRelativeKey(root: string, file: string): { abs: string; key: string } {\n const abs = isAbsolute(file) ? resolve(file) : resolve(root, file);\n const rel = relative(root, abs);\n if (!rel || isEscaping(rel) || !isSafeManifestKey(toPosixPath(rel)) || !staysInside(root, abs)) {\n throw new Error(`\"${file}\" escapes the project root`);\n }\n return { abs, key: toPosixPath(rel) };\n}\n\n/* ── manifest read/write ──────────────────────────────────────────────── */\n\n/**\n * A manifest key is joined onto the variant dir and onto the project root,\n * so a key that climbs out of either (`../../x`, an absolute path, or a\n * backslash that Windows would read as a separator) would let a hand-edited\n * or checked-in variant.json reach files outside both trees. Such a manifest\n * is not a variant at all.\n */\nfunction isSafeManifestKey(key: string): boolean {\n if (!key || isAbsolute(key) || /[\\\\:\\0]/.test(key) || key === 'variant.json') return false;\n return !key.split('/').some((segment) => !segment || segment === '.' || segment === '..' || segment === '.git');\n}\n\nfunction isPlausibleVariant(v: unknown): v is OverlayVariant {\n if (!v || typeof v !== 'object' || Array.isArray(v)) return false;\n const o = v as Record<string, unknown>;\n return (\n typeof o.id === 'string' &&\n typeof o.label === 'string' &&\n typeof o.createdAt === 'string' &&\n (o.route === undefined || typeof o.route === 'string') &&\n (o.description === undefined || typeof o.description === 'string') &&\n (o.group === undefined || (typeof o.group === 'string' && OVERLAY_GROUP_ID.test(o.group))) &&\n (o.groupLabel === undefined || typeof o.groupLabel === 'string') &&\n !!o.files &&\n typeof o.files === 'object' &&\n !Array.isArray(o.files) &&\n Object.entries(o.files).every(([key, entry]) => isSafeManifestKey(key) && entry && typeof entry === 'object' &&\n ('base' in entry) && (entry.base === null || (typeof entry.base === 'string' && /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(entry.base))) &&\n (!('added' in entry) || entry.added === true))\n );\n}\n\nexport function readVariant(opts: VariantsStoreOptions, id: string): OverlayVariant | null {\n // An id that is not a valid directory name (`..`, a path with a slash)\n // would resolve to somewhere outside the variants dir; there is no\n // variant there by definition.\n if (!isValidVariantId(id)) return null;\n const file = join(variantDir(opts, id), 'variant.json');\n if (!existsSync(file)) return null;\n try {\n const data = JSON.parse(readFileSync(file, 'utf8'));\n const dir = variantDir(opts, id);\n return isPlausibleVariant(data) && data.id === id && staysInside(variantsDir(opts), file) &&\n Object.keys(data.files).every((key) => staysInside(opts.root, join(opts.root, key)) && staysInside(dir, join(dir, key))) ? data : null;\n } catch {\n // Junk directory: unreadable or not JSON. listVariants skips it.\n return null;\n }\n}\n\nexport function writeVariant(opts: VariantsStoreOptions, v: OverlayVariant): void {\n if (!isPlausibleVariant(v)) throw new Error('Invalid variant manifest');\n const dir = variantDir(opts, v.id);\n if (!staysInside(variantsDir(opts), dir)) throw new Error('Variant directory escapes the variants tree');\n mkdirSync(dir, { recursive: true });\n writeJson(join(dir, 'variant.json'), v);\n}\n\n/** Whether a tracked file has drifted from what its override was derived\n * from. `base: null` (never hashed, or the manifest was hand-edited to\n * simulate an unreachable blob) counts as stale too: there's nothing to\n * compare against, so we can't claim it's unchanged. */\nfunction isStale(base: string | null, currentHash: string | null): boolean {\n return base === null || currentHash === null || currentHash !== base;\n}\n\n/** Every valid manifest, oldest first, without the per-file status that\n * {@link listVariants} hashes for. */\nexport function readVariants(opts: VariantsStoreOptions): OverlayVariant[] {\n const dir = variantsDir(opts);\n if (!existsSync(dir)) return [];\n return readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory())\n .map((e) => readVariant(opts, e.name))\n .filter((v): v is OverlayVariant => v !== null)\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n}\n\nexport function listVariants(opts: VariantsStoreOptions): OverlayVariantStatus[] {\n const dir = variantsDir(opts);\n if (!existsSync(dir)) return [];\n const root = resolve(opts.root);\n const ids = readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory())\n .map((e) => e.name);\n\n const statuses: OverlayVariantStatus[] = [];\n const hashes = new Map<string, string | null>();\n for (const id of ids) {\n const manifest = readVariant(opts, id);\n if (!manifest) continue; // no manifest, or invalid JSON: not a variant\n const stale: string[] = [];\n const missing: string[] = [];\n for (const [file, entry] of Object.entries(manifest.files)) {\n const overrideAbs = join(dir, id, file);\n if (!existsSync(overrideAbs)) {\n missing.push(file);\n continue;\n }\n // A file the variant adds is current for as long as the project still\n // lacks it; once a real one appears, the two need a person.\n if (entry.added) {\n if (existsSync(join(root, file))) stale.push(file);\n continue;\n }\n const hashKey = `${entry.base?.length}:${file}`;\n if (!hashes.has(hashKey)) hashes.set(hashKey, hashFile(join(root, file), entry.base));\n if (isStale(entry.base, hashes.get(hashKey) ?? null)) stale.push(file);\n }\n statuses.push({ ...manifest, stale, missing });\n }\n statuses.sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n return statuses;\n}\n\nexport function createVariant(\n opts: VariantsStoreOptions,\n input: {\n id: string;\n label?: string;\n route?: string;\n element?: IterationElement | null;\n note?: string;\n description?: string;\n group?: string;\n groupLabel?: string;\n },\n): OverlayVariant {\n return mutateVariants(opts, () => {\n if (!isValidVariantId(input.id)) {\n throw new Error(`Invalid variant id \"${input.id}\"`);\n }\n if (input.group !== undefined) assertGroupId(input.group);\n if (readVariant(opts, input.id)) {\n throw new Error(`Variant \"${input.id}\" already exists`);\n }\n const variant: OverlayVariant = {\n id: input.id,\n label: input.label || input.id,\n createdAt: new Date().toISOString(),\n files: {},\n ...(input.route !== undefined ? { route: input.route } : {}),\n ...(input.element !== undefined ? { element: input.element } : {}),\n ...(input.note !== undefined ? { note: input.note } : {}),\n ...(input.description !== undefined ? { description: input.description } : {}),\n ...(input.group !== undefined ? { group: input.group } : {}),\n ...(input.groupLabel !== undefined ? { groupLabel: input.groupLabel } : {}),\n };\n writeVariant(opts, variant);\n return variant;\n });\n}\n\nfunction assertGroupId(group: string): void {\n if (!OVERLAY_GROUP_ID.test(group)) {\n throw new Error(`Invalid group id \"${group}\" (lowercase letters, digits and hyphens)`);\n }\n}\n\n/** Put an existing variant into a group, so two variants made separately\n * become alternatives of one decision. `groupLabel` is left as it was\n * unless given. */\nexport function setVariantGroup(\n opts: VariantsStoreOptions,\n id: string,\n group: string,\n groupLabel?: string,\n): OverlayVariant {\n return mutateVariants(opts, () => {\n const manifest = readVariant(opts, id);\n if (!manifest) throw new Error(`No variant \"${id}\"`);\n assertGroupId(group);\n manifest.group = group;\n if (groupLabel !== undefined) manifest.groupLabel = groupLabel;\n writeVariant(opts, manifest);\n return manifest;\n });\n}\n\n/**\n * Copy a real file into the variant. With `create`, the file is one the\n * project does not have yet: the variant gets an empty file at that path,\n * and the plugin resolves imports of it while the variant is loaded.\n */\nexport function addVariantFile(\n opts: VariantsStoreOptions,\n id: string,\n file: string,\n { create = false }: { create?: boolean } = {},\n): OverlayVariant {\n return mutateVariants(opts, () => {\n const manifest = readVariant(opts, id);\n if (!manifest) throw new Error(`No variant \"${id}\"`);\n const root = resolve(opts.root);\n const { abs: sourceAbs, key } = repoRelativeKey(root, file);\n // Idempotent: a file already tracked keeps its override and its base\n // untouched. Re-adding is a no-op, not a refresh (sync is how you refresh).\n if (Object.hasOwn(manifest.files, key)) return manifest;\n const destAbs = join(variantDir(opts, id), key);\n if (!staysInside(variantDir(opts, id), destAbs)) throw new Error('Override path escapes the variant directory');\n if (create) {\n if (existsSync(sourceAbs)) throw new Error(`\"${file}\" already exists; add it without --new`);\n mkdirSync(dirname(destAbs), { recursive: true });\n if (!existsSync(destAbs)) writeFileSync(destAbs, '');\n manifest.files = { ...manifest.files, [key]: { base: null, added: true } };\n writeVariant(opts, manifest);\n return manifest;\n }\n if (!existsSync(sourceAbs)) throw new Error(`\"${file}\" does not exist. Use --new for a file the variant adds.`);\n mkdirSync(dirname(destAbs), { recursive: true });\n writeFileSync(destAbs, readFileSync(sourceAbs));\n manifest.files = { ...manifest.files, [key]: { base: blobOf(sourceAbs) } };\n writeVariant(opts, manifest);\n return manifest;\n });\n}\n\nexport function removeVariantFile(\n opts: VariantsStoreOptions,\n id: string,\n file: string,\n): OverlayVariant {\n return mutateVariants(opts, () => {\n const manifest = readVariant(opts, id);\n if (!manifest) throw new Error(`No variant \"${id}\"`);\n const root = resolve(opts.root);\n const { key } = repoRelativeKey(root, file);\n if (Object.hasOwn(manifest.files, key)) {\n delete manifest.files[key];\n rmSync(join(variantDir(opts, id), key), { force: true });\n writeVariant(opts, manifest);\n }\n return manifest;\n });\n}\n\n/** The marker `git merge-file` leaves at the top of a conflicted hunk, with\n * the label `syncVariant` gives the override side. */\nconst CONFLICT_MARKER = '<<<<<<< variant';\n\n/**\n * Land a variant: copy every override over its real file, then delete the\n * variant. Refuses when an override would silently discard work: a file\n * whose real source moved on since the override was made (run `syncVariant`\n * first) or an override still carrying merge conflict markers. `force`\n * skips both checks for the case where the reviewer has decided the\n * override wins as-is.\n */\nexport function promoteVariant(\n opts: VariantsStoreOptions,\n id: string,\n { force = false }: { force?: boolean } = {},\n): { files: string[]; rebased: string[] } {\n return mutateVariants(opts, () => {\n // Checked before the manifest read so the rmSync below can never be\n // handed a directory the id escaped into.\n if (!isValidVariantId(id)) throw new Error(`Invalid variant id \"${id}\"`);\n const manifest = readVariant(opts, id);\n if (!manifest) throw new Error(`No variant \"${id}\"`);\n const active = readActive(opts);\n const root = resolve(opts.root);\n const vDir = variantDir(opts, id);\n const missing = Object.keys(manifest.files).filter((key) => !existsSync(join(vDir, key)));\n if (missing.length) throw new Error(`\"${id}\" has missing overrides: ${missing.join(', ')}. Restore or remove them before promoting.`);\n if (!force) {\n const status = listVariants(opts).find((v) => v.id === id);\n if (status?.stale.length) {\n // A null base is stale by definition, but the file did not necessarily\n // change: there was simply no git to record what it looked like.\n const reasons = status.stale.map((file) =>\n manifest.files[file]?.base === null\n ? `no base recorded for ${file} (not a git repo when the override was made)`\n : `${file} changed since the override was made`,\n );\n throw new Error(\n `\"${id}\" is stale (${reasons.join('; ')}). Run \\`variant sync ${id}\\` first, or promote with --force.`,\n );\n }\n const conflicted = Object.keys(manifest.files).filter((key) => {\n const overrideAbs = join(vDir, key);\n return existsSync(overrideAbs) && readFileSync(overrideAbs, 'utf8').includes(CONFLICT_MARKER);\n });\n if (conflicted.length) {\n throw new Error(\n `\"${id}\" has unresolved conflict markers in ${conflicted.join(', ')}. Resolve them, or promote with --force.`,\n );\n }\n }\n const siblings = overlayGroups(readVariants(opts)).find((g) => g.members.some((m) => m.id === id))\n ?.members.filter((m) => m.id !== id) ?? [];\n const files: string[] = [];\n const landed = new Map<string, { before: string | null; after: string | null }>();\n for (const key of Object.keys(manifest.files)) {\n const overrideAbs = join(vDir, key);\n const destAbs = join(root, key);\n const before = existsSync(destAbs) ? currentBlob(destAbs) : null;\n mkdirSync(dirname(destAbs), { recursive: true });\n writeFileSync(destAbs, readFileSync(overrideAbs));\n landed.set(key, { before, after: blobOf(destAbs) });\n files.push(key);\n }\n rmSync(vDir, { recursive: true, force: true });\n if (active.includes(id)) writeActive(opts, active.filter((a) => a !== id));\n return { files, rebased: rebaseSiblings(opts, siblings, landed) };\n });\n}\n\n/**\n * A sibling is another answer to the decision the promote just settled, so the\n * promoted file is what it now differs from: reading it as out of date would\n * only send someone to merge two alternatives into each other. Only a copy made\n * from the version the promote replaced moves. One that was already behind\n * stays stale, because the real file changed in ways nobody has looked at.\n */\nfunction rebaseSiblings(\n opts: VariantsStoreOptions,\n siblings: OverlayVariant[],\n landed: Map<string, { before: string | null; after: string | null }>,\n): string[] {\n const rebased: string[] = [];\n for (const sibling of siblings) {\n let moved = false;\n for (const [key, entry] of Object.entries(sibling.files)) {\n const change = landed.get(key);\n if (!change?.after || !(entry.added || (entry.base !== null && entry.base === change.before))) continue;\n sibling.files[key] = { base: change.after };\n moved = true;\n }\n if (!moved) continue;\n writeVariant(opts, sibling);\n rebased.push(sibling.id);\n }\n return rebased;\n}\n\nexport function dropVariant(opts: VariantsStoreOptions, id: string): void {\n mutateVariants(opts, () => {\n // `variant drop ..` would otherwise resolve to the variants dir's parent\n // and rmSync it, recursively.\n if (!isValidVariantId(id)) throw new Error(`Invalid variant id \"${id}\"`);\n const active = readActive(opts);\n rmSync(variantDir(opts, id), { recursive: true, force: true });\n if (active.includes(id)) writeActive(opts, active.filter((a) => a !== id));\n });\n}\n\n/* ── git blob hashing ─────────────────────────────────────────────────── */\n\n/** A failed lookup is an answer here (`null`), not something to print. */\nconst GIT_QUIET: ['ignore', 'pipe', 'ignore'] = ['ignore', 'pipe', 'ignore'];\n\n/**\n * `git hash-object` looks up the repository from its cwd, not from the\n * path it's hashing, so every call here runs with cwd set to the file's own\n * directory — always inside the right repo regardless of where the caller's\n * process happens to be.\n */\nexport function blobOf(absPath: string): string | null {\n try {\n return execFileSync('git', ['hash-object', '--no-filters', '-w', absPath], {\n cwd: dirname(absPath),\n encoding: 'utf8',\n stdio: GIT_QUIET,\n }).trim();\n } catch {\n return null;\n }\n}\n\nexport function currentBlob(absPath: string): string | null {\n try {\n return execFileSync('git', ['hash-object', '--no-filters', absPath], {\n cwd: dirname(absPath),\n encoding: 'utf8',\n stdio: GIT_QUIET,\n }).trim();\n } catch {\n return null;\n }\n}\n\n/** Git blob hashing is just a header and the file bytes. Existing bases tell\n * us the repository's hash format, avoiding a process per file on every poll. */\nfunction hashFile(file: string, base: string | null): string | null {\n if (base === null) return null;\n try {\n const contents = readFileSync(file);\n return createHash(base.length === 64 ? 'sha256' : 'sha1')\n .update(`blob ${contents.length}\\0`).update(contents).digest('hex');\n } catch { return null; }\n}\n\nfunction catFile(cwd: string, hash: string): string | null {\n try {\n return execFileSync('git', ['cat-file', '-p', hash], { cwd, encoding: 'utf8', stdio: GIT_QUIET });\n } catch {\n return null; // the blob isn't reachable (gc'd, or never written)\n }\n}\n\n/** Runs `git merge-file -p <override> <base> <current>`, capturing stdout\n * whether it exits clean (0) or with conflict markers. execFileSync throws\n * on a nonzero exit, but the thrown error still carries stdout and the exit\n * status, so both are read from there. `status` is null when git never\n * ran to completion (killed by a signal, or not found). */\nfunction mergeFile(overrideAbs: string, baseAbs: string, currentAbs: string): {\n status: number | null;\n stdout: string;\n} {\n try {\n const stdout = execFileSync(\n 'git',\n [\n 'merge-file',\n '-p',\n '-L',\n 'variant',\n '-L',\n 'base',\n '-L',\n 'default',\n overrideAbs,\n baseAbs,\n currentAbs,\n ],\n { encoding: 'utf8', stdio: GIT_QUIET },\n );\n return { status: 0, stdout };\n } catch (err) {\n const e = err as { status?: number | null; stdout?: string };\n return { status: e.status ?? null, stdout: e.stdout ?? '' };\n }\n}\n\n/** `git merge-file` exits with the number of conflicts it left, so 1..127\n * is a conflicted merge whose stdout still carries the whole file (with\n * markers). 128 and up, or no status at all, is git failing outright. */\nfunction isConflictStatus(status: number | null): boolean {\n return status !== null && status >= 1 && status <= 127;\n}\n\n/** The base a later sync merges from has to be in git's object store. A file\n * git does not track yet is in there only if this writes it. */\nfunction recordBase(absPath: string, hash: string): string {\n return blobOf(absPath) ?? hash;\n}\n\nexport function syncVariant(\n opts: VariantsStoreOptions,\n id: string,\n): { merged: string[]; conflicts: string[]; unchanged: string[] } {\n return mutateVariants(opts, () => {\n const manifest = readVariant(opts, id);\n if (!manifest) throw new Error(`No variant \"${id}\"`);\n const root = resolve(opts.root);\n const vDir = variantDir(opts, id);\n\n const merged: string[] = [];\n const conflicts: string[] = [];\n const unchanged: string[] = [];\n let dirty = false;\n\n for (const [file, entry] of Object.entries(manifest.files)) {\n const currentAbs = join(root, file);\n const overrideAbs = join(vDir, file);\n if (entry.added) {\n // Nothing to merge into; a real file that appeared meanwhile is a\n // conflict for a person to settle.\n (existsSync(currentAbs) ? conflicts : unchanged).push(file);\n continue;\n }\n const currentHash = currentBlob(currentAbs);\n\n if (!isStale(entry.base, currentHash)) {\n unchanged.push(file);\n continue;\n }\n\n // No base to diff against (never hashed, or the blob is unreachable), or\n // no current file to merge into: nothing to three-way merge, so the\n // override is left as-is and flagged for a person to sort out.\n const baseContent = entry.base !== null ? catFile(dirname(currentAbs), entry.base) : null;\n if (baseContent === null || currentHash === null) {\n conflicts.push(file);\n continue;\n }\n\n const baseTmp = join(tmpdir(), `isotone-base-${randomUUID()}`);\n writeFileSync(baseTmp, baseContent);\n try {\n const result = mergeFile(overrideAbs, baseTmp, currentAbs);\n if (result.status === 0) {\n writeFileSync(overrideAbs, result.stdout);\n entry.base = recordBase(currentAbs, currentHash);\n dirty = true;\n merged.push(file);\n } else if (isConflictStatus(result.status)) {\n // The markers go into the override for a person to resolve, and the\n // base advances anyway, the way git records a conflicted merge:\n // the current file is now what the override was reconciled against,\n // so resolving the markers is all that stands between it and\n // promote. Left behind, the base would make the resolved file read\n // as stale and a second sync would re-conflict the same hunks; the\n // marker check in promoteVariant is the guard that remains.\n writeFileSync(overrideAbs, result.stdout);\n entry.base = recordBase(currentAbs, currentHash);\n dirty = true;\n conflicts.push(file);\n } else {\n // Git failed outright; its empty stdout must not replace the\n // override, and the base stays where it was.\n conflicts.push(file);\n }\n } finally {\n rmSync(baseTmp, { force: true });\n }\n }\n\n if (dirty) writeVariant(opts, manifest);\n return { merged, conflicts, unchanged };\n });\n}\n\n/* ── active variants / vite load() lookup ─────────────────────────── */\n\n/**\n * The override that answers for `absSourcePath` under the active set, or\n * `null` when the real file stands. The first active variant whose manifest\n * names the file wins; `readActive` and the endpoint keep the set free of\n * two variants over one file, so in practice there is only ever one.\n */\nexport function resolveOverride(\n opts: VariantsStoreOptions,\n active: string[],\n absSourcePath: string,\n): string | null {\n if (active.length === 0) return null;\n const root = resolve(opts.root);\n const relToRoot = relative(root, resolve(absSourcePath));\n if (isEscaping(relToRoot)) return null;\n const key = toPosixPath(relToRoot);\n for (const id of active) {\n const manifest = readVariant(opts, id);\n if (!manifest || !Object.hasOwn(manifest.files, key)) continue;\n const overrideAbs = join(variantDir(opts, id), key);\n if (existsSync(overrideAbs)) return overrideAbs;\n }\n return null;\n}\n\nexport function overrideSourcePath(\n opts: VariantsStoreOptions,\n absOverridePath: string,\n): { id: string; file: string } | null {\n const rel = relative(variantsDir(opts), resolve(absOverridePath));\n if (!rel || isEscaping(rel)) return null;\n const [id, ...rest] = toPosixPath(rel).split('/');\n if (!id || rest.length === 0) return null;\n return { id, file: rest.join('/') };\n}\n\n/**\n * Two active variants over the same file, as `[first, second, file]`, or\n * `null` when the set is consistent. `load` can only serve one copy of a\n * file, so such a set is refused by the endpoint and trimmed by\n * {@link readActive}. Ids without a manifest are skipped; the callers\n * check existence themselves.\n */\nexport function activeConflict(\n opts: VariantsStoreOptions,\n ids: string[],\n): [string, string, string] | null {\n const owner = new Map<string, string>();\n for (const id of ids) {\n const manifest = readVariant(opts, id);\n for (const file of Object.keys(manifest?.files ?? {})) {\n const first = owner.get(file);\n if (first !== undefined && first !== id) return [first, id, file];\n owner.set(file, id);\n }\n }\n return null;\n}\n\n/**\n * Variants in another group that override one of this variant's files. Groups\n * are independent selectors, so picking both of these fails at activation.\n */\nexport function crossGroupOverlaps(opts: VariantsStoreOptions, id: string): { file: string; other: string }[] {\n const variants = readVariants(opts);\n const self = variants.find((v) => v.id === id);\n if (!self) return [];\n const siblings = overlayGroups(variants).find((g) => g.members.some((m) => m.id === id))?.members ?? [];\n return variants.flatMap((other) => other.id === id || siblings.includes(other) ? [] :\n Object.keys(other.files).filter((file) => Object.hasOwn(self.files, file)).map((file) => ({ file, other: other.id })));\n}\n\n/**\n * The active set, cleaned: ids whose variant is gone (dropped or promoted\n * since the file was written) are dropped, and of two variants over one\n * file the later one is dropped, so `load` never has to choose. The file's\n * older shape, one id or `null`, still reads.\n */\n/** The host's knobs, dropping entries that are not a usable axis. */\nexport function readKnobs(opts: VariantsStoreOptions): KnobAxis[] {\n const file = resolve(opts.root, KNOBS_FILE);\n if (!existsSync(file)) return [];\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(file, 'utf8'));\n } catch {\n return [];\n }\n if (!Array.isArray(parsed)) return [];\n const seen = new Set<string>();\n return parsed.filter((k): k is KnobAxis => {\n if (!k || typeof k !== 'object') return false;\n const a = k as Partial<KnobAxis>;\n if (typeof a.id !== 'string' || !isValidVariantId(a.id) || typeof a.label !== 'string') return false;\n if (a.route !== undefined && typeof a.route !== 'string' &&\n (!Array.isArray(a.route) || !a.route.every((route) => typeof route === 'string'))) return false;\n if (a.showWhen !== undefined && (!a.showWhen || typeof a.showWhen.axis !== 'string' ||\n (a.showWhen.equals !== undefined && typeof a.showWhen.equals !== 'string') ||\n (a.showWhen.not !== undefined && typeof a.showWhen.not !== 'string'))) return false;\n if (a.section !== undefined && typeof a.section !== 'string') return false;\n if (a.kind === 'range') {\n const { min, max, step, defaultValue } = a as Partial<RangeVariantAxis>;\n if (typeof min !== 'number' || typeof max !== 'number' || typeof step !== 'number' || typeof defaultValue !== 'number') return false;\n return [min, max, step, defaultValue, max - min].every(Number.isFinite) &&\n min < max && step > 0 && defaultValue >= min && defaultValue <= max &&\n (a.unit === undefined || typeof a.unit === 'string');\n }\n if (a.kind === 'color') {\n const c = a as Partial<ColorVariantAxis>;\n const hex = (value: unknown) => typeof value === 'string' && /^#(?:[a-f0-9]{3}|[a-f0-9]{6})$/i.test(value);\n return hex(c.defaultColor) && Array.isArray(c.swatches) && c.swatches.every((s) =>\n s && typeof s.id === 'string' && typeof s.label === 'string' && hex(s.value));\n }\n if (a.kind === 'select') {\n const s = a as Partial<SelectVariantAxis>;\n return Array.isArray(s.options) && s.options.length > 0 &&\n s.options.every((o) => o && typeof o.id === 'string' && typeof o.label === 'string') &&\n s.options.some((o) => o.id === s.defaultOption);\n }\n return false;\n }).filter((axis) => {\n if (seen.has(axis.id)) return false;\n seen.add(axis.id);\n return true;\n });\n}\n\nexport function readActive(opts: VariantsStoreOptions): string[] {\n const file = join(resolve(opts.root), ACTIVE_FILE);\n if (!existsSync(file)) return [];\n let raw: unknown;\n try {\n raw = (JSON.parse(readFileSync(file, 'utf8')) as { active?: unknown }).active;\n } catch {\n return [];\n }\n const ids = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];\n const kept: string[] = [];\n const seen = new Set<string>();\n const owner = new Set<string>();\n for (const id of ids) {\n if (typeof id !== 'string' || seen.has(id)) continue;\n seen.add(id);\n const manifest = readVariant(opts, id);\n if (!manifest) continue;\n const files = Object.keys(manifest.files);\n if (files.some((f) => owner.has(f))) continue;\n for (const f of files) owner.add(f);\n kept.push(id);\n }\n return kept;\n}\n\nexport function writeActive(opts: VariantsStoreOptions, ids: string[]): void {\n const file = join(resolve(opts.root), ACTIVE_FILE);\n mkdirSync(dirname(file), { recursive: true });\n const active = Array.from(new Set(ids));\n writeJson(file, { active } satisfies ActiveFileShape);\n // Which variants a machine is looking at is local state, never a commit;\n // the CLI writes this too, so the exclusion cannot live in the plugin alone.\n ensureFileIgnored(file);\n}\n\n/* ── the endpoint handler ─────────────────────────────────────────────── */\n\n/**\n * The transport-agnostic core of the variants endpoint, mirroring\n * {@link import('./store').notesResponse}. `req.url` is the path relative\n * to wherever the caller mounted `VARIANTS_ENDPOINT` (\"/\", \"/active\"), so\n * this function never needs to know the mount point.\n */\nexport function variantsResponse(opts: VariantsStoreOptions, req: NotesRequest): NotesResponse {\n const method = req.method ?? '';\n const path = (req.url ?? '/').split('?')[0] || '/';\n const json = (status: number, body: unknown): NotesResponse => ({\n status,\n headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },\n body,\n });\n\n if (method === 'GET' && path === '/') {\n return json(200, { variants: listVariants(opts), active: readActive(opts), knobs: readKnobs(opts) });\n }\n\n if (method === 'POST' && path === '/active') {\n const body = req.body;\n if (body === null || typeof body !== 'object' || Array.isArray(body)) {\n return json(400, { error: 'body must be a JSON object' });\n }\n const raw = (body as { active?: unknown }).active;\n if (!Array.isArray(raw) || !raw.every((id) => typeof id === 'string')) {\n return json(400, { error: 'active must be an array of variant ids' });\n }\n const active = Array.from(new Set(raw as string[]));\n for (const id of active) {\n // Malformed, not merely unknown: a 404 would imply the shape was fine.\n if (!isValidVariantId(id)) return json(400, { error: `invalid variant id \"${id}\"` });\n }\n for (const id of active) {\n if (!readVariant(opts, id)) return json(404, { error: `unknown variant \"${id}\"` });\n }\n const conflict = activeConflict(opts, active);\n if (conflict) {\n const [a, b, file] = conflict;\n const groups = overlayGroups(readVariants(opts));\n const named = (id: string) => {\n const label = groups.find((group) => group.members.some((member) => member.id === id))?.label;\n return label ? `\"${id}\" (${label})` : `\"${id}\"`;\n };\n return json(409, {\n error: `${named(a)} and ${named(b)} both override ${file}; only one can be active`,\n conflict,\n });\n }\n writeActive(opts, active);\n return json(200, { active });\n }\n\n return json(405, { error: 'method not allowed' });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/id.ts","../src/core/index.ts"],"sourcesContent":["let fallbackCounter = 0;\n\n/** IDs must also work on HTTP development hosts, where randomUUID is absent. */\nexport function createNoteId(): string {\n const crypto = globalThis.crypto;\n if (typeof crypto?.randomUUID === 'function') return crypto.randomUUID();\n\n if (typeof crypto?.getRandomValues === 'function') {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n }\n\n // Old browsers without Web Crypto still need unique, stable retry IDs.\n fallbackCounter += 1;\n return `${Date.now().toString(36)}-${fallbackCounter.toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n","/**\n * isotone — core.\n *\n * The framework-agnostic half of the internal feedback loop. Holds the note\n * model, the element descriptor, the dev-store client (file-backed via a Vite\n * plugin, `localStorage` fallback), and the route visibility matcher. This\n * module has no React and no Vite dependency and is fully unit-testable in a\n * `node` env.\n */\nimport { request } from './request';\nimport { FEEDBACK_STORAGE_KEY, readBrowserStorage, writeBrowserStorage } from './browser-storage';\nimport { MAX_FEEDBACK_LENGTH } from './limits';\nimport { createNoteId } from './id';\n\n/** A UI element picked with the panel's selector mode. */\nexport interface IterationElement {\n /** Lowercase tag name, e.g. \"button\". */\n tag: string;\n /** Trimmed text-content snippet (may be empty for visual elements). */\n text: string;\n /** CSS path to the element. A pin made in a browser matches only this element. */\n selector: string;\n /** Identifying attributes: role, name, aria-label, data-*, class and so on. */\n attributes?: Record<string, string>;\n /** React components around the element, outermost first. */\n component?: string[];\n /** `file:line` of the JSX, where the host's React still records it. */\n source?: string;\n /** Labelled or headed ancestors, outermost first. */\n within?: string[];\n /** The closest ancestor's text, for an element with none of its own. */\n near?: string;\n /** Where it sat on the page when pinned, in CSS pixels from the document's corner. */\n rect?: { x: number; y: number; w: number; h: number };\n}\n\n/** A durable reference to a selector or one of its options, not a DOM path. */\nexport interface VariantReference {\n axisId: string;\n label: string;\n optionId?: string;\n optionLabel?: string;\n value?: string;\n}\n\nexport interface VariantRemoval {\n axisId: string;\n action: 'remove-option' | 'remove-axis' | 'keep-default';\n optionId?: string;\n}\n\nexport interface NoteContext {\n /** Pending instruction: deleting its note cancels it before the agent acts. */\n variantAction?: VariantRemoval;\n elements?: IterationElement[];\n variant?: VariantReference | null;\n}\n\n/** A note written in the panel, read and answered by the coding agent. */\nexport interface IterationNote extends NoteContext {\n id: string;\n /** The route the note was written on, e.g. \"/projects\". */\n route: string;\n feedback: string;\n element: IterationElement | null;\n done: boolean;\n doneAt?: string | null;\n /** Set while an agent says it is working on the note. Stale after a while. */\n workingAt?: string | null;\n createdAt: string;\n /** The window size the note was written at — layout feedback only means\n * something at the breakpoint it was seen on. */\n viewport?: { w: number; h: number };\n /** Host-specific context the store round-trips without interpreting — see\n * `StoredNote.payload`. */\n payload?: unknown;\n}\n\n/** The minimal structural surface the descriptor builder needs from a DOM\n * node. `children` is `readonly` so both a real `HTMLCollection` and a plain\n * test array satisfy it; the builder materializes it with `Array.from`. */\nexport interface NodeLike {\n tagName: string;\n id?: string | null;\n parentElement?: NodeLike | null;\n children?: readonly NodeLike[];\n textContent?: string | null;\n /** One-based child position, when the adapter can provide it cheaply. */\n indexInParent?: number;\n /** Whether a selector finds this node and nothing else. Without one, the\n * path stops at an id or the depth cap, unchecked. */\n isOnlyMatch?(selector: string): boolean;\n /** What else identifies the node, beyond its path. */\n context?(): Partial<IterationElement>;\n}\n\n/**\n * Descriptor of a picked element for locating it in code. Walks up the tree\n * collecting `#id` (preferred) or `tag:nth-child(n)` segments. The panel finds\n * pins again with `querySelector`, which takes the first match, so with a real\n * DOM the walk goes on past `maxDepth` until the path matches this element\n * alone. Kept framework-free so it is unit-testable.\n */\nexport function describeIterationElement(\n el: NodeLike,\n maxDepth = 4,\n): IterationElement {\n const parts: string[] = [];\n let node: NodeLike | null | undefined = el;\n let depth = 0;\n const only = el.isOnlyMatch ? (path: string[]) => el.isOnlyMatch?.(path.join(\" > \")) === true : null;\n let settled = false;\n while (node && node.tagName !== \"BODY\" && (only || depth < maxDepth)) {\n if (node.id) {\n // Escape every code point: valid for leading digits, punctuation and\n // framework-generated ids without depending on the host's CSS global.\n const id = Array.from(node.id, (c) => `\\\\${c.codePointAt(0)?.toString(16)} `).join('');\n const segment = `#${/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(node.id) ? node.id : id}`;\n // A page can repeat an id, and then it identifies nothing.\n if (!only || only([segment, ...parts])) {\n parts.unshift(segment);\n settled = true;\n break;\n }\n }\n let part = node.tagName.toLowerCase();\n const parent: NodeLike | null | undefined = node.parentElement;\n if (parent) {\n const index = node.indexInParent ?? Array.from(parent.children ?? []).indexOf(node) + 1;\n if (index > 0) part += `:nth-child(${index})`;\n }\n parts.unshift(part);\n node = parent;\n depth++;\n // The depth cap still keeps short paths from resting on a single\n // `button:nth-child(1)`, which the next button on the page would take.\n if (only && depth >= maxDepth && only(parts)) {\n settled = true;\n break;\n }\n }\n // A path can reach the body and still match elsewhere when it starts lower\n // down, as `div > button` does. Rooting it leaves one match.\n if (only && !settled && !only(parts)) parts.unshift(\"body\");\n return {\n tag: el.tagName.toLowerCase(),\n text: (el.textContent ?? \"\").trim().replace(/\\s+/g, \" \").slice(0, 80),\n selector: parts.join(\" > \"),\n ...el.context?.(),\n };\n}\n\nconst IDENTIFYING_ATTRIBUTES = ['role', 'type', 'name', 'href', 'aria-label', 'title', 'alt', 'placeholder'];\n\n/** Names from React's dev-mode bookkeeping on the DOM node. Absent on other\n * frameworks and production builds, which simply leaves the fields out. */\nfunction reactContext(el: Element): Pick<IterationElement, 'component' | 'source'> {\n type Fiber = {\n type?: unknown;\n return?: Fiber | null;\n _debugSource?: { fileName?: string; lineNumber?: number };\n };\n type Named = { displayName?: string; name?: string; render?: Named; type?: Named };\n const key = Object.keys(el).find((name) => name.startsWith('__reactFiber$'));\n let fiber: Fiber | null | undefined = key ? (el as unknown as Record<string, Fiber | null>)[key] : null;\n const names: string[] = [];\n let source: string | undefined;\n while (fiber && names.length < 5) {\n if (!source && fiber._debugSource?.fileName) {\n source = `${fiber._debugSource.fileName}:${fiber._debugSource.lineNumber ?? 0}`;\n }\n const type = fiber.type as Named | string | null | undefined;\n const named = type && typeof type !== 'string' ? (type.render ?? type.type ?? type) : null;\n const name = named?.displayName ?? named?.name;\n // Capitalized is a component; anything else is a minified or internal name.\n if (name && /^[A-Z]/.test(name) && names[0] !== name) names.unshift(name);\n fiber = fiber.return;\n }\n return { ...(names.length ? { component: names } : {}), ...(source ? { source } : {}) };\n}\n\n/** Everything about a pinned element that says which one it is, for the agent\n * reading the note later and without the page open. */\nfunction elementContext(el: Element): Partial<IterationElement> {\n const attributes: Record<string, string> = {};\n for (const name of IDENTIFYING_ATTRIBUTES) {\n const value = el.getAttribute(name);\n if (value) attributes[name] = value;\n }\n for (const { name, value } of Array.from(el.attributes).filter((a) => a.name.startsWith('data-')).slice(0, 6)) {\n if (value) attributes[name] = value;\n }\n const className = el.getAttribute('class');\n if (className) attributes.class = className;\n\n const within: string[] = [];\n let near: string | undefined;\n const ownText = renderedText(el).trim();\n for (let up = el.parentElement; up && up.tagName !== 'BODY'; up = up.parentElement) {\n if (!ownText && !near) {\n const text = renderedText(up).trim().replace(/\\s+/g, ' ');\n if (text) near = text.slice(0, 80);\n }\n if (within.length === 4) continue;\n const tag = up.tagName.toLowerCase();\n const label = up.getAttribute('aria-label');\n const test = up.getAttribute('data-testid');\n // Only a heading above the element titles it; a later one names the next section.\n let heading: Element | null = null;\n try {\n heading = Array.from(up.querySelectorAll(':scope > :is(h1,h2,h3,h4,h5,h6), :scope > * > :is(h1,h2,h3,h4,h5,h6)'))\n .filter((candidate) => candidate.compareDocumentPosition(el) & 4).pop() ?? null;\n } catch {}\n if (label) within.unshift(`<${tag}> labelled \"${label}\"`);\n else if (test) within.unshift(`<${tag} data-testid=\"${test}\">`);\n else if (heading && heading !== el) within.unshift(`<${tag}> headed \"${(heading.textContent ?? '').trim().replace(/\\s+/g, ' ').slice(0, 60)}\"`);\n }\n const box = el.getBoundingClientRect();\n const view = el.ownerDocument?.defaultView;\n return {\n ...(Object.keys(attributes).length ? { attributes } : {}),\n ...reactContext(el),\n ...(within.length ? { within } : {}),\n ...(near ? { near } : {}),\n ...(box.width || box.height ? {\n rect: {\n x: Math.round(box.left + (view?.scrollX ?? 0)),\n y: Math.round(box.top + (view?.scrollY ?? 0)),\n w: Math.round(box.width),\n h: Math.round(box.height),\n },\n } : {}),\n };\n}\n\n/** Lazy DOM views: describing one element never walks unrelated subtrees. */\n/**\n * The element's text as a reader sees it. `textContent` runs block children\n * together (\"Active projects12+2 this month\"), so prefer the browser's\n * `innerText`, and without one (jsdom, detached nodes) put a space around\n * every child that is not inline.\n */\nfunction renderedText(el: Element): string {\n const inner = (el as HTMLElement).innerText;\n if (typeof inner === 'string' && el.isConnected) return inner;\n const view = el.ownerDocument?.defaultView;\n const walk = (node: Node): string => {\n if (node.nodeType === 3) return node.nodeValue ?? '';\n if (node.nodeType !== 1) return '';\n const text = Array.from(node.childNodes, walk).join('');\n const display = view?.getComputedStyle(node as Element).display ?? '';\n return node === el || display.startsWith('inline') ? text : ` ${text} `;\n };\n return walk(el);\n}\n\nexport function elementAsNode(el: Element): NodeLike {\n const cache = new WeakMap<Element, NodeLike>();\n const view = (n: Element | null | undefined): NodeLike | null => {\n if (!n) return null;\n const cached = cache.get(n);\n if (cached) return cached;\n const created: NodeLike = {\n tagName: n.tagName,\n id: n.id || null,\n get textContent() { return renderedText(n); },\n get parentElement() { return view(n.parentElement); },\n get children() { return Array.from(n.children, (child) => view(child) as NodeLike); },\n get indexInParent() {\n let index = 1;\n for (let sibling = n.previousElementSibling; sibling; sibling = sibling.previousElementSibling) index++;\n return index;\n },\n };\n cache.set(n, created);\n return created;\n };\n const root = view(el);\n if (root === null) {\n throw new Error('unreachable: a real element always maps to a view');\n }\n root.isOnlyMatch = (selector) => {\n try {\n const found = el.ownerDocument.querySelectorAll(selector);\n return found.length === 1 && found[0] === el;\n } catch {\n return false;\n }\n };\n root.context = () => elementContext(el);\n return root;\n}\n\n/** A matcher over a console route: include a page only when it is not hidden\n * (denylist) or, when a visible list is given, only when it matches. */\nexport interface RouteGateOptions {\n /** Exact routes (or prefixes ending in `*`) to NEVER show the panel on,\n * e.g. the auth flow. Defaults to the auth/login/error pages. */\n hidden?: string[];\n /** When set, visible only on these routes/prefixes (allowlist). Overrides\n * `hidden` — used when a repo wants an explicit allow-list. */\n visible?: string[];\n /** Prefixes always barred regardless of `visible` (e.g. `/api`, `/auth`). */\n alwaysHidden?: string[];\n}\n\nconst DEFAULT_HIDDEN = [\"/login\", \"/logout\", \"/register\", \"/error\", \"/_error\"];\n\nfunction matchRoutePattern(route: string, pattern: string): boolean {\n if (pattern.endsWith('*')) return route.startsWith(pattern.slice(0, -1));\n if (pattern.endsWith(\"/\")) return route.startsWith(pattern);\n return route === pattern || route.startsWith(`${pattern}/`);\n}\n\n/** Whether the panel may mount on the given route. Pure and unit-testable. */\nexport function isRouteVisible(route: string, rule: RouteGateOptions = {}): boolean {\n const hidden = rule.hidden ?? DEFAULT_HIDDEN;\n const alwaysHidden = rule.alwaysHidden ?? [\"/api\", \"/auth\", \"/login\"];\n // A prefix match on the \"always\" list is unconditionally hidden — the panel\n // must never appear on auth or API surfaces regardless of allow-list.\n for (const p of alwaysHidden) {\n if (route === p || route.startsWith(`${p}/`)) return false;\n }\n if (rule.visible && rule.visible.length > 0) {\n return rule.visible.some((v) => matchRoutePattern(route, v));\n }\n return !hidden.some((h) => matchRoutePattern(route, h));\n}\n\n/* ---------------------------------------------------------------------------\n * Store: client transport for the notes. The Vite plugin serves the endpoint\n * (same-origin, dev-only); a `localStorage` fallback keeps notes when the\n * dev server is unreachable. The endpoint and storage namespace are\n * configurable so multi-app origins don't collide.\n * ------------------------------------------------------------------------- */\n\nexport interface IterationStoreConfig {\n /** Absolute or same-origin URL of the notes endpoint. Must be set (by the\n * Vite plugin's `iteration()` options or a framework preset) for a shared\n * file-backed store; when omitted, only localStorage persists. */\n endpoint?: string | null;\n /** localStorage namespace. Default-scoped to the package; set it per hosted\n * app when two apps share an origin. */\n key?: string;\n}\n\n/** Where the last `saveNote` landed; `null` before the first one. */\nexport type SaveLocation = \"server\" | \"local\";\n\nexport function createIterationStore(cfg: IterationStoreConfig = {}) {\n const endpoint = cfg.endpoint ?? null;\n const key = cfg.key ?? FEEDBACK_STORAGE_KEY;\n let lastSave: SaveLocation | null = null;\n let migration: Promise<void> | null = null;\n\n function parseLocal(raw: string | null): IterationNote[] {\n try {\n const items = JSON.parse(raw ?? '[]') as IterationNote[];\n if (!Array.isArray(items) || !items.every((note) => note && typeof note.id === 'string' &&\n typeof note.route === 'string' && typeof note.feedback === 'string' && typeof note.createdAt === 'string')) throw new Error('Invalid notes');\n return items.map((i) => ({ ...i, done: i.done ?? false })).map((i) => ({\n ...i,\n element: i.element ?? null,\n }));\n } catch (cause) {\n throw new Error('Could not read local feedback. Browser storage has been left untouched.', { cause });\n }\n }\n\n function localAll(): IterationNote[] {\n let storage: Storage;\n try { storage = window.localStorage; } catch { return []; }\n return parseLocal(readBrowserStorage(storage, key, (current, previous) => {\n const merged = new Map([...parseLocal(previous), ...parseLocal(current)].map(note => [note.id, note]));\n return JSON.stringify([...merged.values()]);\n }));\n }\n\n function localWrite(items: IterationNote[]) {\n writeBrowserStorage(window.localStorage, key, JSON.stringify(items));\n }\n\n function acknowledge(item: IterationNote): void {\n const current = localAll();\n const snapshot = JSON.stringify(item);\n const remaining = current.filter((note) => note.id !== item.id || JSON.stringify(note) !== snapshot);\n if (remaining.length !== current.length) localWrite(remaining);\n }\n\n async function flushLocalBacklog(): Promise<void> {\n if (!endpoint) return;\n const backlog = localAll();\n if (backlog.length === 0) return;\n for (const item of backlog) {\n // Notes saved by an older client may exceed today's server limit.\n // Keep them in the browser, but let valid notes behind them sync.\n if (item.feedback.trim().length > MAX_FEEDBACK_LENGTH) continue;\n let rejected = false;\n const saved = await request(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n id: item.id,\n createdAt: item.createdAt,\n route: item.route,\n feedback: item.feedback,\n element: item.element ?? null,\n ...(item.elements !== undefined ? { elements: item.elements } : {}),\n ...(item.variant !== undefined ? { variant: item.variant } : {}),\n ...(item.variantAction !== undefined ? { variantAction: item.variantAction } : {}),\n ...(item.payload !== undefined ? { payload: item.payload } : {}),\n ...(item.viewport ? { viewport: item.viewport } : {}),\n }),\n }, async (res) => {\n if (!res.ok) {\n rejected = res.status >= 400 && res.status < 500;\n return null;\n }\n return await res.json() as { id?: unknown };\n });\n if (!saved || typeof saved.id !== 'string' || !saved.id) {\n if (rejected) continue;\n return; // A transient failure or missing acknowledgement stops the retry.\n }\n // A save may have happened while the request was in flight. Remove\n // only the exact snapshot acknowledged by the server, never the queue.\n acknowledge(item);\n }\n }\n\n function migrateLocalBacklog(): Promise<void> {\n migration ??= flushLocalBacklog().finally(() => { migration = null; });\n return migration;\n }\n\n async function readRemote(url: string): Promise<IterationNote[]> {\n await migrateLocalBacklog().catch(() => {});\n const local = localAll();\n try {\n const remote = await request(url, undefined, async (res) => {\n if (!res.ok) throw new Error(`Loading iteration notes failed (${res.status})`);\n return await res.json() as IterationNote[];\n });\n const pendingIds = new Set(local.map((note) => note.id));\n return [...remote.filter((note) => !pendingIds.has(note.id)), ...local];\n } catch (error) {\n if (local.length) return local;\n throw error;\n }\n }\n\n /** Notes for one route, or every note on every route when `route` is omitted. */\n async function listNotes(route?: string): Promise<IterationNote[]> {\n if (!endpoint) {\n return localAll()\n .filter((i) => route === undefined || i.route === route)\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt));\n }\n return (await readRemote(\n route === undefined ? `${endpoint}?all=1` : `${endpoint}?route=${encodeURIComponent(route)}`,\n )).filter((note) => route === undefined || note.route === route)\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt));\n }\n\n /**\n * The open notes on every route, but cheaply: `rev` is the revision of the\n * last answer, and an unchanged server replies with no rows (`notes: null`).\n * Older servers ignore `rev` and return the list, which counts as changed.\n * Notes this browser kept while the server was away are pushed first, and\n * stay in the result until the server has them. Failures throw, so the\n * caller can back off.\n */\n async function pollNotes(rev: string | null): Promise<{ rev: string | null; notes: IterationNote[] | null }> {\n if (!endpoint) return { rev: null, notes: localAll().filter((n) => !n.done) };\n if (localAll().length) await migrateLocalBacklog().catch(() => {});\n const local = localAll();\n const data = await request(`${endpoint}?rev=${encodeURIComponent(rev ?? '')}`, undefined, async (res) => {\n if (!res.ok) throw new Error(`Loading iteration notes failed (${res.status})`);\n return await res.json() as IterationNote[] | { rev?: unknown; unchanged?: unknown; notes?: unknown };\n });\n if (Array.isArray(data)) return { rev: null, notes: merge(data, local) };\n const next = typeof data?.rev === 'string' ? data.rev : null;\n // The panel already holds its own local saves, so an unchanged server means\n // nothing to replace.\n if (data?.unchanged === true && next !== null) return { rev: next, notes: null };\n if (!Array.isArray(data?.notes)) throw new Error('Unexpected notes response');\n return { rev: next, notes: merge(data.notes as IterationNote[], local) };\n }\n\n function merge(remote: IterationNote[], local: IterationNote[]): IterationNote[] {\n const pendingIds = new Set(local.map((note) => note.id));\n return [...remote.filter((note) => !pendingIds.has(note.id)), ...local];\n }\n\n /** Every still-open note across all routes, newest first. */\n async function pendingNotes(): Promise<IterationNote[]> {\n if (!endpoint) {\n return localAll()\n .filter((i) => !i.done)\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt));\n }\n return (await readRemote(endpoint)).filter((note) => !note.done)\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt));\n }\n\n function saveLocal(\n route: string,\n feedback: string,\n element: IterationElement | null,\n id: string | null,\n payload?: unknown,\n viewport?: { w: number; h: number },\n context?: NoteContext,\n ): string {\n const all = localAll();\n let savedId: string;\n const defaultsRequest = feedback.startsWith('[set-defaults]');\n const existing = id ? all.find((f) => f.id === id && f.route === route) : defaultsRequest ? all.filter(n => !n.done && n.route === route && n.feedback.startsWith('[set-defaults]')).sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] : undefined;\n if (existing) {\n if (defaultsRequest && !id) {\n delete existing.elements;\n delete existing.variant;\n delete existing.variantAction;\n delete existing.payload;\n }\n existing.feedback = feedback;\n existing.element = element;\n if (payload !== undefined) existing.payload = payload;\n if (defaultsRequest) {\n existing.createdAt = new Date().toISOString();\n if (viewport) existing.viewport = viewport;\n }\n if (context) Object.assign(existing, context);\n if (existing.elements) existing.element = existing.elements[0] ?? null;\n savedId = existing.id;\n } else {\n savedId = id ?? createNoteId();\n all.push({\n id: savedId,\n route,\n feedback,\n element,\n done: false,\n createdAt: new Date().toISOString(),\n ...(viewport ? { viewport } : {}),\n ...(payload !== undefined ? { payload } : {}),\n ...context,\n ...(context?.elements ? { element: context.elements[0] ?? null } : {}),\n });\n }\n localWrite(defaultsRequest ? all.filter(n => n.id === savedId || n.done || n.route !== route || !n.feedback.startsWith('[set-defaults]')) : all);\n return savedId;\n }\n\n async function saveNote(\n route: string,\n feedback: string,\n element: IterationElement | null,\n id: string | null,\n /** Opaque host context, attached only when the caller sends it. */\n payload?: unknown,\n /** Window size at write time; attached on create only. */\n viewport?: { w: number; h: number },\n context?: NoteContext,\n ): Promise<string> {\n if (feedback.trim().length > MAX_FEEDBACK_LENGTH) {\n throw new RangeError(`Feedback must be at most ${MAX_FEEDBACK_LENGTH} characters.`);\n }\n if (!endpoint) {\n const savedId = saveLocal(route, feedback, element, id, payload, viewport, context);\n lastSave = \"local\";\n return savedId;\n }\n // Stable across a lost response and its retry: the server treats this as\n // an upsert, so reconnecting cannot create a second copy of the note.\n const savedId = id ?? (feedback.startsWith('[set-defaults]') ? null : createNoteId());\n const pendingBefore = localAll().find((note) => note.id === savedId);\n const saveOffline = () => {\n const localId = saveLocal(route, feedback, element, savedId, payload, viewport, context);\n lastSave = 'local';\n return localId;\n };\n let res: Response | undefined;\n let result: { id?: unknown; error?: unknown } | null;\n try {\n result = await request(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n id: savedId,\n route,\n feedback,\n element,\n ...context,\n ...(payload !== undefined ? { payload } : {}),\n ...(viewport ? { viewport } : {}),\n }),\n }, async (response) => {\n res = response;\n if (!response.ok && response.status >= 500) throw new Error(`Saving iteration note failed (${response.status})`);\n return await response.json().catch((error) => {\n if (response.ok) throw error;\n return null;\n }) as { id?: unknown; error?: unknown } | null;\n });\n } catch {\n if (res && !res.ok && res.status < 500) throw new Error(`Saving iteration note failed (${res.status})`);\n return saveOffline();\n }\n // Validation errors require a correction, not a permanently stuck queue.\n if (!res?.ok) {\n throw new Error(`Saving iteration note failed (${res?.status})${typeof result?.error === 'string' ? `: ${result.error}` : ''}`);\n }\n if (!result || typeof result.id !== 'string' || !result.id) return saveOffline();\n if (pendingBefore) acknowledge(pendingBefore);\n lastSave = 'server';\n return result.id;\n }\n\n async function deleteNote(id: string): Promise<void> {\n if (!endpoint) {\n localWrite(localAll().filter((f) => f.id !== id));\n return;\n }\n const res = await request(`${endpoint}?id=${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n });\n if (!res.ok) throw new Error(`Deleting iteration note failed (${res.status})`);\n const local = localAll();\n if (local.some((note) => note.id === id)) localWrite(local.filter((note) => note.id !== id));\n }\n\n return {\n listNotes,\n pollNotes,\n pendingNotes,\n saveNote,\n deleteNote,\n endpoint,\n /** Where the last save landed, for the panel's sync notice. */\n get lastSave(): SaveLocation | null {\n return lastSave;\n },\n get hasUnsyncedNotes(): boolean {\n return localAll().length > 0;\n },\n };\n}\nexport * from './variants';\n"],"mappings":"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","names":["savedId"]}
|
package/dist/chunk-LXW6WFOC.js
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MAX_FEEDBACK_LENGTH
|
|
3
|
-
} from "./chunk-A4DX2V4C.js";
|
|
4
|
-
|
|
5
|
-
// src/store.ts
|
|
6
|
-
import { execFileSync } from "child_process";
|
|
7
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
8
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
9
|
-
import { basename, dirname as dirname2, isAbsolute, join, relative, resolve, sep } from "path";
|
|
10
|
-
|
|
11
|
-
// src/file-store.ts
|
|
12
|
-
import { randomUUID } from "crypto";
|
|
13
|
-
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
14
|
-
import { dirname } from "path";
|
|
15
|
-
function writeJson(file, value) {
|
|
16
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
17
|
-
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
18
|
-
try {
|
|
19
|
-
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
|
|
20
|
-
`, { flag: "wx", mode: 384 });
|
|
21
|
-
renameSync(temporary, file);
|
|
22
|
-
} finally {
|
|
23
|
-
rmSync(temporary, { force: true });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function withFileLock(file, work) {
|
|
27
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
28
|
-
const lock = `${file}.lock`;
|
|
29
|
-
const deadline = Date.now() + 2e3;
|
|
30
|
-
while (true) {
|
|
31
|
-
try {
|
|
32
|
-
const fd = openSync(lock, "wx", 384);
|
|
33
|
-
try {
|
|
34
|
-
writeFileSync(fd, String(process.pid));
|
|
35
|
-
} finally {
|
|
36
|
-
closeSync(fd);
|
|
37
|
-
}
|
|
38
|
-
break;
|
|
39
|
-
} catch (error) {
|
|
40
|
-
if (error.code !== "EEXIST") throw error;
|
|
41
|
-
try {
|
|
42
|
-
const owner = Number(readFileSync(lock, "utf8"));
|
|
43
|
-
if (Number.isSafeInteger(owner) && owner > 0) {
|
|
44
|
-
try {
|
|
45
|
-
process.kill(owner, 0);
|
|
46
|
-
} catch (cause) {
|
|
47
|
-
if (cause.code === "ESRCH") {
|
|
48
|
-
rmSync(lock, { force: true });
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
} catch {
|
|
54
|
-
}
|
|
55
|
-
if (Date.now() >= deadline) throw new Error(`Local store is busy: ${file}. Retry after the other writer finishes.`);
|
|
56
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
return work();
|
|
61
|
-
} finally {
|
|
62
|
-
rmSync(lock, { force: true });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// src/store.ts
|
|
67
|
-
var PAYLOAD_MAX = 65536;
|
|
68
|
-
function readNotes(file) {
|
|
69
|
-
if (!existsSync(file)) return [];
|
|
70
|
-
try {
|
|
71
|
-
const raw = readFileSync2(file, "utf8").trim();
|
|
72
|
-
if (!raw) return [];
|
|
73
|
-
const data = JSON.parse(raw);
|
|
74
|
-
if (!Array.isArray(data) || !data.every((note) => note && typeof note === "object" && typeof note.id === "string" && typeof note.route === "string" && typeof note.feedback === "string" && typeof note.createdAt === "string")) {
|
|
75
|
-
throw new Error("expected an array of notes");
|
|
76
|
-
}
|
|
77
|
-
return data;
|
|
78
|
-
} catch (cause) {
|
|
79
|
-
throw new Error(`Could not read notes from ${file}; the file has been left untouched.`, { cause });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
function writeNotes(file, notes) {
|
|
83
|
-
writeJson(file, notes);
|
|
84
|
-
}
|
|
85
|
-
function updateNotes(file, update) {
|
|
86
|
-
withFileLock(file, () => writeNotes(file, update(readNotes(file))));
|
|
87
|
-
}
|
|
88
|
-
function ensureFileIgnored(file) {
|
|
89
|
-
try {
|
|
90
|
-
const dir = realpathSync(dirname2(resolve(file)));
|
|
91
|
-
const absolute = join(dir, basename(file));
|
|
92
|
-
const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
93
|
-
cwd: dir,
|
|
94
|
-
encoding: "utf8",
|
|
95
|
-
// Outside a repo git writes "fatal: not a git repository" to stderr, and
|
|
96
|
-
// inheriting it prints that over the server's own startup banner. The
|
|
97
|
-
// throw is the signal we act on; the message is noise.
|
|
98
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
99
|
-
}).trim();
|
|
100
|
-
const pattern = excludePattern(gitRoot, absolute);
|
|
101
|
-
if (!pattern) return;
|
|
102
|
-
const infoExclude = excludeFile(dir);
|
|
103
|
-
const existing = existsSync(infoExclude) ? readFileSync2(infoExclude, "utf8") : "";
|
|
104
|
-
if (hasExcludePattern(existing, pattern)) return;
|
|
105
|
-
const marker = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
106
|
-
const heading = "# isotone (local dev feedback loop)";
|
|
107
|
-
const comment = existing.includes(heading) ? "" : `${heading}
|
|
108
|
-
`;
|
|
109
|
-
mkdirSync2(dirname2(infoExclude), { recursive: true });
|
|
110
|
-
writeFileSync2(infoExclude, `${existing}${marker}${comment}${pattern}
|
|
111
|
-
`);
|
|
112
|
-
} catch {
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function ensureDirIgnored(dir) {
|
|
116
|
-
try {
|
|
117
|
-
const absolute = realpathSync(resolve(dir));
|
|
118
|
-
const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
119
|
-
cwd: absolute,
|
|
120
|
-
encoding: "utf8",
|
|
121
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
122
|
-
}).trim();
|
|
123
|
-
const base = excludePattern(gitRoot, absolute);
|
|
124
|
-
if (!base) return;
|
|
125
|
-
const pattern = `${base}/`;
|
|
126
|
-
const infoExclude = excludeFile(absolute);
|
|
127
|
-
const existing = existsSync(infoExclude) ? readFileSync2(infoExclude, "utf8") : "";
|
|
128
|
-
if (hasExcludePattern(existing, pattern) || hasExcludePattern(existing, base)) return;
|
|
129
|
-
const marker = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
130
|
-
mkdirSync2(dirname2(infoExclude), { recursive: true });
|
|
131
|
-
writeFileSync2(
|
|
132
|
-
infoExclude,
|
|
133
|
-
`${existing}${marker}# isotone (local overlay variants)
|
|
134
|
-
${pattern}
|
|
135
|
-
`
|
|
136
|
-
);
|
|
137
|
-
} catch {
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
function excludeFile(cwd) {
|
|
141
|
-
const reported = execFileSync("git", ["rev-parse", "--git-path", "info/exclude"], {
|
|
142
|
-
cwd,
|
|
143
|
-
encoding: "utf8",
|
|
144
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
145
|
-
}).trim();
|
|
146
|
-
return resolve(cwd, reported);
|
|
147
|
-
}
|
|
148
|
-
function excludePattern(gitRoot, absolute) {
|
|
149
|
-
const rel = relative(gitRoot, absolute);
|
|
150
|
-
if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
return `/${rel.split(sep).join("/")}`;
|
|
154
|
-
}
|
|
155
|
-
function hasExcludePattern(exclude, pattern) {
|
|
156
|
-
const bare = pattern.replace(/^\//, "");
|
|
157
|
-
return exclude.split(/\r?\n/).some((line) => line.trim() === pattern || line.trim() === bare);
|
|
158
|
-
}
|
|
159
|
-
function toItem(n) {
|
|
160
|
-
const item = {
|
|
161
|
-
id: n.id,
|
|
162
|
-
route: n.route,
|
|
163
|
-
feedback: n.feedback,
|
|
164
|
-
element: n.element,
|
|
165
|
-
done: n.done,
|
|
166
|
-
createdAt: n.createdAt
|
|
167
|
-
};
|
|
168
|
-
if (n.workingAt && !n.done) item.workingAt = n.workingAt;
|
|
169
|
-
if (n.viewport !== void 0) item.viewport = n.viewport;
|
|
170
|
-
if (n.payload !== void 0) item.payload = n.payload;
|
|
171
|
-
return {
|
|
172
|
-
...item,
|
|
173
|
-
...n.elements !== void 0 ? { elements: n.elements } : {},
|
|
174
|
-
...n.variant !== void 0 ? { variant: n.variant } : {},
|
|
175
|
-
...n.variantAction !== void 0 ? { variantAction: n.variantAction } : {}
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
function notesResponse(file, req) {
|
|
179
|
-
if (req.method === "POST" || req.method === "DELETE") {
|
|
180
|
-
return withFileLock(file, () => handleNotesRequest(file, req));
|
|
181
|
-
}
|
|
182
|
-
return handleNotesRequest(file, req);
|
|
183
|
-
}
|
|
184
|
-
function handleNotesRequest(file, req) {
|
|
185
|
-
const method = req.method ?? "";
|
|
186
|
-
const query = Object.fromEntries(
|
|
187
|
-
new URL(req.url ?? "/", "http://localhost").searchParams
|
|
188
|
-
);
|
|
189
|
-
const json = (status, body) => ({
|
|
190
|
-
status,
|
|
191
|
-
headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
|
|
192
|
-
body
|
|
193
|
-
});
|
|
194
|
-
if (method === "GET") {
|
|
195
|
-
const polled = query.rev !== void 0;
|
|
196
|
-
const rev = polled ? notesRevision(file) : "";
|
|
197
|
-
if (polled && rev === String(query.rev)) return json(200, { rev, unchanged: true });
|
|
198
|
-
const route = String(query.route ?? "");
|
|
199
|
-
const all = query.all !== void 0 && String(query.all) !== "" && String(query.all) !== "0";
|
|
200
|
-
const notes = polled ? readNotesAt(file, rev) : readNotes(file);
|
|
201
|
-
const rows = (all ? [...notes] : route ? notes.filter((n) => n.route === route) : notes.filter((n) => !n.done)).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).map(toItem);
|
|
202
|
-
return json(200, polled ? { rev, notes: rows } : rows);
|
|
203
|
-
}
|
|
204
|
-
if (method === "DELETE") {
|
|
205
|
-
const id = String(query.id ?? "");
|
|
206
|
-
if (!id) return json(400, { error: "id is required" });
|
|
207
|
-
writeNotes(file, readNotes(file).filter((n) => n.id !== id));
|
|
208
|
-
return json(200, { ok: true });
|
|
209
|
-
}
|
|
210
|
-
if (method === "POST") {
|
|
211
|
-
const body = req.body ?? {};
|
|
212
|
-
const { id, route, feedback, element, payload, viewport } = body;
|
|
213
|
-
if (id !== void 0 && id !== null && (typeof id !== "string" || !id || id.length > 200)) {
|
|
214
|
-
return json(400, { error: "id must be a non-empty string of at most 200 characters" });
|
|
215
|
-
}
|
|
216
|
-
if (typeof route !== "string" || route === "") {
|
|
217
|
-
return json(400, { error: "route is required" });
|
|
218
|
-
}
|
|
219
|
-
const cleanFeedback = typeof feedback === "string" ? feedback.trim() : "";
|
|
220
|
-
if (cleanFeedback === "") {
|
|
221
|
-
return json(400, { error: "feedback is required" });
|
|
222
|
-
}
|
|
223
|
-
if (cleanFeedback.length > MAX_FEEDBACK_LENGTH) return json(400, { error: `feedback must be at most ${MAX_FEEDBACK_LENGTH} characters` });
|
|
224
|
-
if (payload !== void 0 && !withinPayloadCap(payload)) {
|
|
225
|
-
return json(400, {
|
|
226
|
-
error: `payload exceeds ${PAYLOAD_MAX} bytes or is not serializable`
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
const cleanViewport = viewportOf(viewport);
|
|
230
|
-
if (body.elements !== void 0 && (!Array.isArray(body.elements) || body.elements.length > 50)) {
|
|
231
|
-
return json(400, { error: "elements must be an array of at most 50 elements" });
|
|
232
|
-
}
|
|
233
|
-
const elements = body.elements === void 0 ? void 0 : body.elements.map(elementOf).filter((el) => el !== null);
|
|
234
|
-
const variant = body.variant === void 0 ? void 0 : variantOf(body.variant);
|
|
235
|
-
const variantAction = removalOf(body.variantAction);
|
|
236
|
-
if (body.variantAction !== void 0 && !variantAction) return json(400, { error: "Invalid variant action" });
|
|
237
|
-
let cleanElement = null;
|
|
238
|
-
if (element && typeof element === "object" && !Array.isArray(element)) {
|
|
239
|
-
cleanElement = {
|
|
240
|
-
tag: cap(String(element.tag ?? ""), 30),
|
|
241
|
-
text: cap(String(element.text ?? ""), 120),
|
|
242
|
-
selector: cap(String(element.selector ?? ""), MAX_SELECTOR),
|
|
243
|
-
...elementContext(element)
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
const notes = readNotes(file);
|
|
247
|
-
const defaultsRequest = cleanFeedback.startsWith("[set-defaults]");
|
|
248
|
-
const previousDefaults = defaultsRequest && !notes.some((note) => note.id === id) ? notes.filter((n) => !n.done && n.route === route && n.feedback.startsWith("[set-defaults]")).sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] : void 0;
|
|
249
|
-
const updateId = previousDefaults?.id ?? id;
|
|
250
|
-
if (typeof updateId === "string" && updateId !== "") {
|
|
251
|
-
const existing = notes.find((n) => n.id === updateId && n.route === route);
|
|
252
|
-
if (existing) {
|
|
253
|
-
if (previousDefaults) {
|
|
254
|
-
delete existing.elements;
|
|
255
|
-
delete existing.variant;
|
|
256
|
-
delete existing.variantAction;
|
|
257
|
-
delete existing.payload;
|
|
258
|
-
}
|
|
259
|
-
existing.feedback = cleanFeedback;
|
|
260
|
-
existing.element = cleanElement;
|
|
261
|
-
if (elements !== void 0) {
|
|
262
|
-
existing.elements = elements;
|
|
263
|
-
existing.element = elements[0] ?? null;
|
|
264
|
-
} else if (existing.elements) existing.element = existing.elements[0] ?? null;
|
|
265
|
-
if (variant !== void 0) existing.variant = variant;
|
|
266
|
-
if (variantAction) existing.variantAction = variantAction;
|
|
267
|
-
if (payload !== void 0) existing.payload = payload;
|
|
268
|
-
if (defaultsRequest && !existing.done) {
|
|
269
|
-
existing.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
270
|
-
if (cleanViewport) existing.viewport = cleanViewport;
|
|
271
|
-
writeNotes(file, notes.filter((n) => n.id === existing.id || n.done || n.route !== route || !n.feedback.startsWith("[set-defaults]")));
|
|
272
|
-
} else writeNotes(file, notes);
|
|
273
|
-
return json(200, { id: existing.id });
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (typeof id === "string" && notes.some((note) => note.id === id)) {
|
|
277
|
-
return json(409, { error: "This note belongs to a different route" });
|
|
278
|
-
}
|
|
279
|
-
const newId = typeof id === "string" ? id : randomUUID2();
|
|
280
|
-
const createdAt = typeof body.createdAt === "string" && Number.isFinite(Date.parse(body.createdAt)) ? new Date(body.createdAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
281
|
-
notes.push({
|
|
282
|
-
id: newId,
|
|
283
|
-
route,
|
|
284
|
-
feedback: cleanFeedback,
|
|
285
|
-
element: cleanElement,
|
|
286
|
-
done: false,
|
|
287
|
-
doneAt: null,
|
|
288
|
-
createdAt,
|
|
289
|
-
...cleanViewport ? { viewport: cleanViewport } : {},
|
|
290
|
-
...payload !== void 0 ? { payload } : {},
|
|
291
|
-
...elements !== void 0 ? { elements, element: elements[0] ?? null } : {},
|
|
292
|
-
...variant !== void 0 ? { variant } : {},
|
|
293
|
-
...variantAction ? { variantAction } : {}
|
|
294
|
-
});
|
|
295
|
-
writeNotes(file, notes);
|
|
296
|
-
return json(201, { id: newId });
|
|
297
|
-
}
|
|
298
|
-
return json(405, { error: "method not allowed" });
|
|
299
|
-
}
|
|
300
|
-
var parsed = /* @__PURE__ */ new Map();
|
|
301
|
-
function readNotesAt(file, rev) {
|
|
302
|
-
const cached = parsed.get(file);
|
|
303
|
-
if (cached && cached.rev === rev && rev !== "none") return cached.notes;
|
|
304
|
-
const notes = readNotes(file);
|
|
305
|
-
parsed.set(file, { rev, notes });
|
|
306
|
-
return notes;
|
|
307
|
-
}
|
|
308
|
-
function notesRevision(file) {
|
|
309
|
-
try {
|
|
310
|
-
const stat = statSync(file, { bigint: true });
|
|
311
|
-
return `${stat.ino.toString(36)}.${stat.mtimeNs.toString(36)}.${stat.size.toString(36)}`;
|
|
312
|
-
} catch {
|
|
313
|
-
return "none";
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
function cap(v, max) {
|
|
317
|
-
return v.slice(0, max);
|
|
318
|
-
}
|
|
319
|
-
function elementOf(value) {
|
|
320
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
321
|
-
const v = value;
|
|
322
|
-
if (typeof v.selector !== "string" || !v.selector) return null;
|
|
323
|
-
return {
|
|
324
|
-
tag: cap(String(v.tag ?? ""), 30),
|
|
325
|
-
text: cap(String(v.text ?? ""), 120),
|
|
326
|
-
selector: cap(v.selector, MAX_SELECTOR),
|
|
327
|
-
...elementContext(v)
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
var MAX_SELECTOR = 2e3;
|
|
331
|
-
function stringList(value, count, length) {
|
|
332
|
-
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item !== "").slice(0, count).map((item) => cap(item, length)) : [];
|
|
333
|
-
}
|
|
334
|
-
function elementContext(value) {
|
|
335
|
-
const v = value;
|
|
336
|
-
const out = {};
|
|
337
|
-
if (v.attributes && typeof v.attributes === "object" && !Array.isArray(v.attributes)) {
|
|
338
|
-
const entries = Object.entries(v.attributes).filter((entry) => typeof entry[1] === "string" && !UNSAFE_KEYS.has(entry[0])).slice(0, 20).map(([name, text]) => [cap(name, 60), cap(text, 300)]);
|
|
339
|
-
if (entries.length) out.attributes = Object.fromEntries(entries);
|
|
340
|
-
}
|
|
341
|
-
const component = stringList(v.component, 8, 80);
|
|
342
|
-
if (component.length) out.component = component;
|
|
343
|
-
const within = stringList(v.within, 6, 120);
|
|
344
|
-
if (within.length) out.within = within;
|
|
345
|
-
if (typeof v.source === "string" && v.source) out.source = cap(v.source, 300);
|
|
346
|
-
if (typeof v.near === "string" && v.near) out.near = cap(v.near, 120);
|
|
347
|
-
const rect = v.rect;
|
|
348
|
-
if (rect && typeof rect === "object" && [rect.x, rect.y, rect.w, rect.h].every((n) => typeof n === "number" && Number.isFinite(n))) {
|
|
349
|
-
out.rect = { x: rect.x, y: rect.y, w: rect.w, h: rect.h };
|
|
350
|
-
}
|
|
351
|
-
return out;
|
|
352
|
-
}
|
|
353
|
-
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
354
|
-
function variantOf(value) {
|
|
355
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
356
|
-
const v = value;
|
|
357
|
-
if (typeof v.axisId !== "string" || typeof v.label !== "string") return null;
|
|
358
|
-
return {
|
|
359
|
-
axisId: cap(v.axisId, 200),
|
|
360
|
-
label: cap(v.label, 200),
|
|
361
|
-
...typeof v.optionId === "string" ? { optionId: cap(v.optionId, 200) } : {},
|
|
362
|
-
...typeof v.optionLabel === "string" ? { optionLabel: cap(v.optionLabel, 200) } : {},
|
|
363
|
-
...typeof v.value === "string" ? { value: cap(v.value, 200) } : {}
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
function viewportOf(v) {
|
|
367
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) return null;
|
|
368
|
-
const w = Math.round(Number(v.w));
|
|
369
|
-
const h = Math.round(Number(v.h));
|
|
370
|
-
if (!Number.isFinite(w) || !Number.isFinite(h)) return null;
|
|
371
|
-
if (w <= 0 || h <= 0 || w > 1e5 || h > 1e5) return null;
|
|
372
|
-
return { w, h };
|
|
373
|
-
}
|
|
374
|
-
function withinPayloadCap(payload) {
|
|
375
|
-
try {
|
|
376
|
-
return Buffer.byteLength(JSON.stringify(payload), "utf8") <= PAYLOAD_MAX;
|
|
377
|
-
} catch {
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
var NOTES_ENDPOINT = "/api/iteration/notes";
|
|
382
|
-
function removalOf(value) {
|
|
383
|
-
if (!value || typeof value !== "object") return;
|
|
384
|
-
const v = value;
|
|
385
|
-
if (typeof v.axisId !== "string" || !v.axisId || v.axisId.length > 200 || !["remove-option", "remove-axis", "keep-default"].includes(v.action ?? "")) return;
|
|
386
|
-
if (v.action === "remove-option" && (typeof v.optionId !== "string" || !v.optionId || v.optionId.length > 200)) return;
|
|
387
|
-
return { axisId: v.axisId, action: v.action, ...typeof v.optionId === "string" ? { optionId: v.optionId.slice(0, 200) } : {} };
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export {
|
|
391
|
-
writeJson,
|
|
392
|
-
withFileLock,
|
|
393
|
-
PAYLOAD_MAX,
|
|
394
|
-
readNotes,
|
|
395
|
-
writeNotes,
|
|
396
|
-
updateNotes,
|
|
397
|
-
ensureFileIgnored,
|
|
398
|
-
ensureDirIgnored,
|
|
399
|
-
notesResponse,
|
|
400
|
-
NOTES_ENDPOINT
|
|
401
|
-
};
|
|
402
|
-
//# sourceMappingURL=chunk-LXW6WFOC.js.map
|