isotone 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/chunk-A4DX2V4C.js +7 -0
- package/dist/chunk-A4DX2V4C.js.map +1 -0
- package/dist/{chunk-DD767R7E.js → chunk-AP7O357L.js} +6 -2
- package/dist/chunk-AP7O357L.js.map +1 -0
- package/dist/{chunk-QT27DAPQ.js → chunk-UMVPMPVE.js} +2 -3
- package/dist/{chunk-QT27DAPQ.js.map → chunk-UMVPMPVE.js.map} +1 -1
- package/dist/{chunk-GDVZ3B6N.js → chunk-YWMOXPE7.js} +43 -6
- package/dist/chunk-YWMOXPE7.js.map +1 -0
- package/dist/{chunk-7WGLOXRQ.js → chunk-ZBULKB3S.js} +3 -45
- package/dist/chunk-ZBULKB3S.js.map +1 -0
- package/dist/cli.js +10 -309
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +1 -0
- package/dist/core.js +2 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/isotone.global.js +18 -18
- package/dist/serve.js +3 -2
- package/dist/serve.js.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/vite-plugin.js +4 -3
- package/dist/vite-plugin.js.map +1 -1
- package/docs/migration.md +31 -83
- package/docs/updates.md +3 -3
- package/package.json +1 -1
- package/dist/chunk-7WGLOXRQ.js.map +0 -1
- package/dist/chunk-DD767R7E.js.map +0 -1
- package/dist/chunk-GDVZ3B6N.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
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 type OverlayVariant,\n type OverlayVariantStatus,\n type RangeVariantAxis,\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 );\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\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 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\nexport function addVariantFile(\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 { 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 if (!existsSync(sourceAbs)) throw new Error(`\"${file}\" does not exist`);\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 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[] } {\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 files: string[] = [];\n for (const key of Object.keys(manifest.files)) {\n const overrideAbs = join(vDir, key);\n const destAbs = join(root, key);\n mkdirSync(dirname(destAbs), { recursive: true });\n writeFileSync(destAbs, readFileSync(overrideAbs));\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 };\n });\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/**\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 }).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 }).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' });\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' },\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\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 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 = 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 = 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 * 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 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 return json(409, {\n error: `\"${a}\" and \"${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":[]}
|
package/dist/cli.js
CHANGED
|
@@ -13,26 +13,26 @@ import {
|
|
|
13
13
|
syncVariant,
|
|
14
14
|
variantDir,
|
|
15
15
|
writeActive
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-ZBULKB3S.js";
|
|
17
17
|
import {
|
|
18
18
|
overlayGroups,
|
|
19
19
|
variantAppliesTo
|
|
20
20
|
} from "./chunk-SNGBNVH2.js";
|
|
21
21
|
import {
|
|
22
|
-
checkUpdate
|
|
23
|
-
|
|
24
|
-
} from "./chunk-QT27DAPQ.js";
|
|
22
|
+
checkUpdate
|
|
23
|
+
} from "./chunk-UMVPMPVE.js";
|
|
25
24
|
import {
|
|
26
25
|
readNotes,
|
|
27
26
|
updateNotes
|
|
28
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-AP7O357L.js";
|
|
28
|
+
import "./chunk-A4DX2V4C.js";
|
|
29
29
|
|
|
30
30
|
// src/cli/index.ts
|
|
31
31
|
import { realpathSync } from "fs";
|
|
32
32
|
import { fileURLToPath } from "url";
|
|
33
33
|
|
|
34
34
|
// src/cli/run.ts
|
|
35
|
-
import { resolve as
|
|
35
|
+
import { resolve as resolve2 } from "path";
|
|
36
36
|
|
|
37
37
|
// src/cli/init.ts
|
|
38
38
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
@@ -124,7 +124,7 @@ function runInit(args, io) {
|
|
|
124
124
|
}
|
|
125
125
|
const { value: approachArg, rest: r1 } = extractFlag(args, "--approach");
|
|
126
126
|
const { present: yes } = extractBoolFlag(r1, "--yes");
|
|
127
|
-
const
|
|
127
|
+
const json = args.includes("--json");
|
|
128
128
|
if (help) {
|
|
129
129
|
out(io, "Usage: isotone init look at this repo and recommend an approach");
|
|
130
130
|
out(io, " isotone init --approach <name> print the setup for overlay, host or serve");
|
|
@@ -142,7 +142,7 @@ function runInit(args, io) {
|
|
|
142
142
|
errOut(io, `Unknown approach "${approach}". Use host, overlay or serve.`);
|
|
143
143
|
return 1;
|
|
144
144
|
}
|
|
145
|
-
if (
|
|
145
|
+
if (json) {
|
|
146
146
|
const instructions = [];
|
|
147
147
|
printApproach({ ...io, stdout: { write: (line) => instructions.push(line) } }, approach, host);
|
|
148
148
|
out(io, JSON.stringify({
|
|
@@ -757,308 +757,10 @@ async function runCheckUpdate(args, io) {
|
|
|
757
757
|
return result.status === "unavailable" ? 1 : 0;
|
|
758
758
|
}
|
|
759
759
|
|
|
760
|
-
// src/cli/migrate.ts
|
|
761
|
-
import { execFileSync } from "child_process";
|
|
762
|
-
import { createHash, randomUUID } from "crypto";
|
|
763
|
-
import { existsSync as existsSync3, lstatSync, readFileSync as readFileSync2, readdirSync as readdirSync2, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
764
|
-
import { basename, dirname, extname, join as join3, relative as relative2, resolve as resolve2 } from "path";
|
|
765
|
-
var OLD = "iso-iterate";
|
|
766
|
-
var NEW = "isotone";
|
|
767
|
-
var TEXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".json", ".jsonc", ".md", ".mdx", ".html", ".css", ".scss", ".sass", ".less", ".vue", ".svelte", ".yaml", ".yml", ".toml", ".sh"]);
|
|
768
|
-
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".nuxt", ".output", "coverage", ".turbo"]);
|
|
769
|
-
var LOCKS = /* @__PURE__ */ new Set(["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"]);
|
|
770
|
-
function git(cwd, args, input) {
|
|
771
|
-
return execFileSync("git", args, { cwd, input, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 32 * 1024 * 1024 }).trimEnd();
|
|
772
|
-
}
|
|
773
|
-
function renamed(text) {
|
|
774
|
-
const keys = [];
|
|
775
|
-
const protect = (match, prefix, quote, value) => {
|
|
776
|
-
if (value === "iso-iterate-feedback" || value === "iso-iterate:knobs") return match;
|
|
777
|
-
const index = keys.push(value) - 1;
|
|
778
|
-
return `${prefix}${quote}\0KEY${index}\0${quote}`;
|
|
779
|
-
};
|
|
780
|
-
const protectedText = text.replace(/(\b(?:key|storageKey)\s*:\s*|["'](?:key|storageKey)["']\s*:\s*)(["'`])([\s\S]*?)\2/g, protect).replace(/(\b(?:localStorage|sessionStorage)\.(?:getItem|setItem|removeItem)\(\s*)(["'`])([\s\S]*?)\2/g, protect);
|
|
781
|
-
let result = protectedText.replace(/\biso-iterate@\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?/g, `${NEW}@${version}`).replaceAll(OLD, NEW).replaceAll("iso-iter-", "isotone-").replaceAll("isoIterate", "isotone").replaceAll("IsoIterate", "Isotone").replaceAll("ISO_ITERATE", "ISOTONE").replaceAll("ISO_VARIANT", "ISOTONE_VARIANT");
|
|
782
|
-
keys.forEach((value, index) => {
|
|
783
|
-
result = result.replaceAll(`\0KEY${index}\0`, value);
|
|
784
|
-
});
|
|
785
|
-
return result;
|
|
786
|
-
}
|
|
787
|
-
function present(path) {
|
|
788
|
-
try {
|
|
789
|
-
lstatSync(path);
|
|
790
|
-
return true;
|
|
791
|
-
} catch {
|
|
792
|
-
return false;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
function textContents(buffer) {
|
|
796
|
-
if (buffer.includes(0)) return null;
|
|
797
|
-
const text = buffer.toString("utf8");
|
|
798
|
-
return Buffer.from(text).compare(buffer) === 0 && !feedback(text) ? text : null;
|
|
799
|
-
}
|
|
800
|
-
function safeFile(root, path) {
|
|
801
|
-
const parts = relative2(root, path).split(/[\\/]/);
|
|
802
|
-
if (parts.includes("..") || parts.some((part) => SKIP.has(part))) return false;
|
|
803
|
-
let current = root;
|
|
804
|
-
for (const part of parts) {
|
|
805
|
-
current = join3(current, part);
|
|
806
|
-
try {
|
|
807
|
-
if (lstatSync(current).isSymbolicLink()) return false;
|
|
808
|
-
} catch {
|
|
809
|
-
return false;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return lstatSync(path).isFile();
|
|
813
|
-
}
|
|
814
|
-
function walk(directory, visit, legacy, skipped) {
|
|
815
|
-
for (const entry of readdirSync2(directory, { withFileTypes: true })) {
|
|
816
|
-
const path = join3(directory, entry.name);
|
|
817
|
-
if (entry.isSymbolicLink()) {
|
|
818
|
-
if (entry.name === `.${OLD}`) legacy(path);
|
|
819
|
-
continue;
|
|
820
|
-
}
|
|
821
|
-
if (entry.isDirectory()) {
|
|
822
|
-
if (SKIP.has(entry.name)) continue;
|
|
823
|
-
if (present(join3(path, ".git"))) {
|
|
824
|
-
skipped?.(path);
|
|
825
|
-
continue;
|
|
826
|
-
}
|
|
827
|
-
if (entry.name === `.${OLD}`) legacy(path);
|
|
828
|
-
else if (entry.name !== ".isotone") walk(path, visit, legacy, skipped);
|
|
829
|
-
} else if (entry.isFile()) visit(path);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
function json(text) {
|
|
833
|
-
try {
|
|
834
|
-
const data = JSON.parse(text);
|
|
835
|
-
return data && typeof data === "object" && !Array.isArray(data) ? data : null;
|
|
836
|
-
} catch {
|
|
837
|
-
return null;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
function feedback(text) {
|
|
841
|
-
try {
|
|
842
|
-
const data = JSON.parse(text);
|
|
843
|
-
return Array.isArray(data) && data.some((note) => note && typeof note === "object" && typeof note.feedback === "string" && typeof note.id === "string");
|
|
844
|
-
} catch {
|
|
845
|
-
return false;
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
function packageText(text, path, conflicts) {
|
|
849
|
-
const pkg = json(text);
|
|
850
|
-
if (!pkg) {
|
|
851
|
-
conflicts.push(`Invalid package.json: ${path}`);
|
|
852
|
-
return text;
|
|
853
|
-
}
|
|
854
|
-
for (const key of ["overrides", "resolutions", "pnpm"]) {
|
|
855
|
-
if (JSON.stringify(pkg[key])?.includes(OLD)) conflicts.push(`Legacy package in ${path} (${key}). Review custom overrides/resolutions before migrating.`);
|
|
856
|
-
}
|
|
857
|
-
let changed = false;
|
|
858
|
-
const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
859
|
-
if (sections.some((key) => Object.hasOwn(Object(pkg[key]), OLD)) && sections.some((key) => Object.hasOwn(Object(pkg[key]), NEW))) {
|
|
860
|
-
conflicts.push(`Both package names exist in ${path}. Resolve their dependency declarations before migrating.`);
|
|
861
|
-
return text;
|
|
862
|
-
}
|
|
863
|
-
for (const key of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
|
|
864
|
-
const deps = pkg[key];
|
|
865
|
-
if (!deps || typeof deps !== "object" || Array.isArray(deps) || !Object.hasOwn(deps, OLD)) continue;
|
|
866
|
-
const values = deps;
|
|
867
|
-
values[NEW] = version;
|
|
868
|
-
delete values[OLD];
|
|
869
|
-
changed = true;
|
|
870
|
-
}
|
|
871
|
-
if (!changed) return renamed(text);
|
|
872
|
-
const indent = /\n([\t ]+)"/.exec(text)?.[1] ?? " ";
|
|
873
|
-
return renamed(JSON.stringify(pkg, null, indent)) + (text.endsWith("\n") ? "\n" : "");
|
|
874
|
-
}
|
|
875
|
-
function installStep(root, override) {
|
|
876
|
-
let directory = root;
|
|
877
|
-
let manager = override;
|
|
878
|
-
while (true) {
|
|
879
|
-
const pkg = existsSync3(join3(directory, "package.json")) ? json(readFileSync2(join3(directory, "package.json"), "utf8")) : null;
|
|
880
|
-
const declared = typeof pkg?.packageManager === "string" ? /^(npm|pnpm|yarn|bun)@/.exec(pkg.packageManager)?.[1] : void 0;
|
|
881
|
-
const found = /* @__PURE__ */ new Set();
|
|
882
|
-
if (existsSync3(join3(directory, "package-lock.json")) || existsSync3(join3(directory, "npm-shrinkwrap.json"))) found.add("npm");
|
|
883
|
-
if (existsSync3(join3(directory, "pnpm-lock.yaml"))) found.add("pnpm");
|
|
884
|
-
if (existsSync3(join3(directory, "yarn.lock"))) found.add("yarn");
|
|
885
|
-
if (existsSync3(join3(directory, "bun.lock")) || existsSync3(join3(directory, "bun.lockb"))) found.add("bun");
|
|
886
|
-
if (declared || found.size || dirname(directory) === directory || existsSync3(join3(directory, ".git"))) {
|
|
887
|
-
if (!manager && typeof pkg?.packageManager === "string" && !declared) return { install: null, warning: "Unsupported packageManager; run its install command to regenerate the lockfile." };
|
|
888
|
-
if (!manager && !declared && found.size > 1) return { install: null, warning: "Multiple lockfiles: pass --manager npm, pnpm, yarn or bun for an install command." };
|
|
889
|
-
manager ??= declared ?? [...found][0] ?? "npm";
|
|
890
|
-
const command = `${manager} install`;
|
|
891
|
-
return { install: { cwd: pkg || found.size ? directory : root, command } };
|
|
892
|
-
}
|
|
893
|
-
directory = dirname(directory);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
function planMigration(cwd, manager) {
|
|
897
|
-
let root = resolve2(cwd);
|
|
898
|
-
let tracked = null;
|
|
899
|
-
try {
|
|
900
|
-
root = git(root, ["rev-parse", "--show-toplevel"]);
|
|
901
|
-
tracked = git(root, ["ls-files", "-z"]).split("\0").filter(Boolean);
|
|
902
|
-
} catch {
|
|
903
|
-
}
|
|
904
|
-
const plan = { root, edits: [], moves: [], blobs: /* @__PURE__ */ new Map(), conflicts: [], install: null, installs: [], warnings: [] };
|
|
905
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
906
|
-
const legacyRoots = /* @__PURE__ */ new Set();
|
|
907
|
-
const addLegacy = (path) => {
|
|
908
|
-
if (legacyRoots.has(path)) return;
|
|
909
|
-
legacyRoots.add(path);
|
|
910
|
-
if (lstatSync(path).isSymbolicLink()) {
|
|
911
|
-
plan.conflicts.push(`Legacy directory is a symlink: ${relative2(root, path)}. Migrate its target separately.`);
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
const destination = join3(dirname(path), ".isotone");
|
|
915
|
-
if (present(destination)) plan.conflicts.push(`Both ${relative2(root, path)} and ${relative2(root, destination)} exist; nothing will be overwritten.`);
|
|
916
|
-
plan.moves.push({ from: path, to: destination });
|
|
917
|
-
walk(path, (file) => candidates.add(file), (nested) => plan.conflicts.push(`Nested legacy directory: ${relative2(root, nested)}`));
|
|
918
|
-
};
|
|
919
|
-
if (tracked) for (const file of tracked) candidates.add(join3(root, file));
|
|
920
|
-
walk(root, (path) => {
|
|
921
|
-
const name = basename(path);
|
|
922
|
-
if (!tracked || dirname(path) === root && (["AGENTS.md", "package.json", ".gitignore", ".npmignore"].includes(name) || /^(?:vite|next|react-router|svelte|astro|nuxt)\.config\./.test(name))) candidates.add(path);
|
|
923
|
-
}, addLegacy, (path) => plan.warnings.push(`Separate Git checkout skipped: ${relative2(root, path)}. Migrate it separately.`));
|
|
924
|
-
if (tracked) {
|
|
925
|
-
const exclude = resolve2(root, git(root, ["rev-parse", "--git-path", "info/exclude"]));
|
|
926
|
-
if (existsSync3(exclude) && lstatSync(exclude).isFile() && !lstatSync(exclude).isSymbolicLink()) {
|
|
927
|
-
const before = readFileSync2(exclude, "utf8");
|
|
928
|
-
const after = renamed(before);
|
|
929
|
-
if (before !== after) plan.edits.push({ path: exclude, before, after, mode: lstatSync(exclude).mode, gitExclude: true });
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
for (const file of candidates) {
|
|
933
|
-
const name = basename(file);
|
|
934
|
-
if (LOCKS.has(name) || name === ".iteration-feedback.json" || !safeFile(root, file)) continue;
|
|
935
|
-
if (relative2(root, file).split(/[\\/]/).some((part) => part !== `.${OLD}` && renamed(part) !== part)) {
|
|
936
|
-
plan.conflicts.push(`Legacy name in a file path: ${relative2(root, file)}. Rename that file or directory and its local references before migrating.`);
|
|
937
|
-
continue;
|
|
938
|
-
}
|
|
939
|
-
if (!TEXT.has(extname(file)) && ![".gitignore", ".npmignore"].includes(name)) continue;
|
|
940
|
-
const before = textContents(readFileSync2(file));
|
|
941
|
-
if (before === null) continue;
|
|
942
|
-
for (const match of before.matchAll(/\biso-iterate@([^\s"'`;,)}\]]+)/g)) {
|
|
943
|
-
if (!/^(?:latest|\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?)$/.test(match[1])) {
|
|
944
|
-
plan.conflicts.push(`Custom package version constraint in ${relative2(root, file)}. Replace it with the desired Isotone version before migrating.`);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
const after = name === "package.json" ? packageText(before, relative2(root, file), plan.conflicts) : renamed(before);
|
|
948
|
-
if (after !== before) plan.edits.push({ path: file, before, after, mode: lstatSync(file).mode });
|
|
949
|
-
}
|
|
950
|
-
if (tracked) for (const file of candidates) {
|
|
951
|
-
if (basename(file) !== "variant.json" || !safeFile(root, file) || ![...legacyRoots].some((path) => file.startsWith(`${path}/`))) continue;
|
|
952
|
-
const before = readFileSync2(file, "utf8");
|
|
953
|
-
const existing = plan.edits.find((edit) => edit.path === file);
|
|
954
|
-
const data = json(existing?.after ?? before);
|
|
955
|
-
if (!data?.files || typeof data.files !== "object") continue;
|
|
956
|
-
let changed = false;
|
|
957
|
-
for (const [key, entry] of Object.entries(data.files)) {
|
|
958
|
-
if (!TEXT.has(extname(key)) && ![".gitignore", ".npmignore"].includes(basename(key))) continue;
|
|
959
|
-
if (!entry || typeof entry !== "object" || typeof entry.base !== "string" || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(entry.base)) continue;
|
|
960
|
-
let base;
|
|
961
|
-
try {
|
|
962
|
-
base = textContents(execFileSync("git", ["cat-file", "blob", entry.base], { cwd: root, stdio: ["pipe", "pipe", "pipe"], maxBuffer: 32 * 1024 * 1024 }));
|
|
963
|
-
} catch {
|
|
964
|
-
continue;
|
|
965
|
-
}
|
|
966
|
-
if (base === null || basename(key) === ".iteration-feedback.json") continue;
|
|
967
|
-
const after = basename(key) === "package.json" ? packageText(base, key, []) : renamed(base);
|
|
968
|
-
if (after === base) continue;
|
|
969
|
-
const hash = createHash(entry.base.length === 64 ? "sha256" : "sha1").update(`blob ${Buffer.byteLength(after)}\0`).update(after).digest("hex");
|
|
970
|
-
entry.base = hash;
|
|
971
|
-
plan.blobs.set(hash, after);
|
|
972
|
-
changed = true;
|
|
973
|
-
}
|
|
974
|
-
if (changed) {
|
|
975
|
-
const after = `${JSON.stringify(data, null, 2)}
|
|
976
|
-
`;
|
|
977
|
-
if (existing) existing.after = after;
|
|
978
|
-
else plan.edits.push({ path: file, before, after, mode: lstatSync(file).mode });
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
const packages = plan.edits.filter((edit) => basename(edit.path) === "package.json" && edit.before.includes(`"${OLD}"`));
|
|
982
|
-
for (const edit of packages) {
|
|
983
|
-
const step = installStep(dirname(edit.path), manager);
|
|
984
|
-
if (step.install && !plan.installs.some((existing) => existing.cwd === step.install?.cwd && existing.command === step.install.command)) plan.installs.push(step.install);
|
|
985
|
-
if (step.warning) plan.warnings.push(`${relative2(root, edit.path)}: ${step.warning}`);
|
|
986
|
-
}
|
|
987
|
-
plan.install = plan.installs.length === 1 ? plan.installs[0] : null;
|
|
988
|
-
if (!tracked) plan.warnings.push("No Git repository: review the changed text files before installing.");
|
|
989
|
-
return plan;
|
|
990
|
-
}
|
|
991
|
-
function atomicWrite(path, text, mode) {
|
|
992
|
-
const temporary = `${path}.${randomUUID()}.migrate`;
|
|
993
|
-
try {
|
|
994
|
-
writeFileSync2(temporary, text, { flag: "wx", mode });
|
|
995
|
-
renameSync(temporary, path);
|
|
996
|
-
} finally {
|
|
997
|
-
if (existsSync3(temporary)) unlinkSync(temporary);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
function apply(plan) {
|
|
1001
|
-
for (const edit of plan.edits) if (!(edit.gitExclude ? lstatSync(edit.path).isFile() && !lstatSync(edit.path).isSymbolicLink() : safeFile(plan.root, edit.path)) || readFileSync2(edit.path, "utf8") !== edit.before) throw new Error(`File changed during migration: ${relative2(plan.root, edit.path)}. Run the preview again.`);
|
|
1002
|
-
for (const move of plan.moves) if (present(move.to) || lstatSync(move.from).isSymbolicLink()) throw new Error(`Directory changed during migration: ${relative2(plan.root, move.from)}.`);
|
|
1003
|
-
const written = [], moved = [];
|
|
1004
|
-
try {
|
|
1005
|
-
for (const [hash, contents] of plan.blobs) if (git(plan.root, ["hash-object", "--no-filters", "-w", "--stdin"], contents) !== hash) throw new Error("Could not preserve a variant merge base.");
|
|
1006
|
-
for (const edit of plan.edits) {
|
|
1007
|
-
atomicWrite(edit.path, edit.after, edit.mode);
|
|
1008
|
-
written.push(edit);
|
|
1009
|
-
}
|
|
1010
|
-
for (const move of plan.moves) {
|
|
1011
|
-
renameSync(move.from, move.to);
|
|
1012
|
-
moved.push(move);
|
|
1013
|
-
}
|
|
1014
|
-
} catch (error) {
|
|
1015
|
-
for (const move of moved.reverse()) renameSync(move.to, move.from);
|
|
1016
|
-
for (const edit of written.reverse()) atomicWrite(edit.path, edit.before, edit.mode);
|
|
1017
|
-
throw error;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
function runMigrate(args, io) {
|
|
1021
|
-
if (args.some((arg) => arg === "--help" || arg === "-h")) {
|
|
1022
|
-
out(io, "Usage: isotone migrate [--write] [--json] [--manager npm|pnpm|yarn|bun]");
|
|
1023
|
-
out(io, "Preview migration from iso-iterate. --write applies it; installing remains an explicit next step.");
|
|
1024
|
-
return 0;
|
|
1025
|
-
}
|
|
1026
|
-
if (parseArgs(args, ["--manager"], ["--write", "--json"]).length) throw new Error("migrate does not take positional arguments.");
|
|
1027
|
-
const manager = valueFor(args, "--manager");
|
|
1028
|
-
if (manager && !["npm", "pnpm", "yarn", "bun"].includes(manager)) throw new Error("--manager must be npm, pnpm, yarn or bun.");
|
|
1029
|
-
const plan = planMigration(io.cwd, manager);
|
|
1030
|
-
const write = args.includes("--write");
|
|
1031
|
-
const changed = plan.edits.length > 0 || plan.moves.length > 0;
|
|
1032
|
-
if (write && !plan.conflicts.length) apply(plan);
|
|
1033
|
-
const summary = {
|
|
1034
|
-
root: plan.root,
|
|
1035
|
-
version,
|
|
1036
|
-
applied: write && !plan.conflicts.length && changed,
|
|
1037
|
-
edits: plan.edits.map((edit) => relative2(plan.root, edit.path)),
|
|
1038
|
-
moves: plan.moves.map((move) => ({ from: relative2(plan.root, move.from), to: relative2(plan.root, move.to) })),
|
|
1039
|
-
conflicts: plan.conflicts,
|
|
1040
|
-
warnings: plan.warnings,
|
|
1041
|
-
install: changed ? plan.install : null,
|
|
1042
|
-
installs: changed ? plan.installs : []
|
|
1043
|
-
};
|
|
1044
|
-
if (args.includes("--json")) out(io, JSON.stringify(summary, null, 2));
|
|
1045
|
-
else {
|
|
1046
|
-
out(io, `${plan.conflicts.length ? "Blocked" : write ? "Applied" : "Preview"}: ${plan.root}`);
|
|
1047
|
-
for (const file of summary.edits) out(io, ` edit ${file}`);
|
|
1048
|
-
for (const move of summary.moves) out(io, ` move ${move.from} \u2192 ${move.to}`);
|
|
1049
|
-
for (const message of [...plan.conflicts, ...plan.warnings]) out(io, message);
|
|
1050
|
-
if (!changed && !plan.conflicts.length) out(io, "No migration changes needed.");
|
|
1051
|
-
else if (!write && !plan.conflicts.length) out(io, "Review this plan, then run isotone migrate --write.");
|
|
1052
|
-
if (!plan.conflicts.length) for (const step of summary.installs) out(io, `After applying, run ${step.command} from ${step.cwd}, then restart the dev server. Lockfiles and feedback are unchanged by migration.`);
|
|
1053
|
-
}
|
|
1054
|
-
return plan.conflicts.length ? 1 : 0;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
760
|
// src/cli/run.ts
|
|
1058
761
|
async function runCli(argv, ioInput = {}) {
|
|
1059
762
|
const io = makeIo(ioInput);
|
|
1060
763
|
try {
|
|
1061
|
-
if (argv[0] === "migrate") return runMigrate(argv.slice(1), io);
|
|
1062
764
|
if (argv[0] === "check-update") return await runCheckUpdate(argv.slice(1), io);
|
|
1063
765
|
if (argv[0] === "serve") return await runServe(argv.slice(1), io);
|
|
1064
766
|
if (argv[0] === "variant") return await runVariant(argv.slice(1), io);
|
|
@@ -1100,7 +802,6 @@ function printHelp(io) {
|
|
|
1100
802
|
out(io);
|
|
1101
803
|
out(io, "Setting a repo up");
|
|
1102
804
|
out(io, " isotone init [--approach overlay|host|serve] [--yes] [--json]");
|
|
1103
|
-
out(io, " isotone migrate [--write] [--json] [--manager npm|pnpm|yarn|bun]");
|
|
1104
805
|
}
|
|
1105
806
|
async function runServe(args, io) {
|
|
1106
807
|
const { serveIteration, DEFAULT_PORT } = await import("./serve.js");
|
|
@@ -1116,7 +817,7 @@ async function runServe(args, io) {
|
|
|
1116
817
|
errOut(io, error.message);
|
|
1117
818
|
return 1;
|
|
1118
819
|
}
|
|
1119
|
-
const file =
|
|
820
|
+
const file = resolve2(io.cwd, valueFor(args, "--file") ?? ".iteration-feedback.json");
|
|
1120
821
|
const port = Number(valueFor(args, "--port") ?? DEFAULT_PORT);
|
|
1121
822
|
const host = valueFor(args, "--host");
|
|
1122
823
|
const theme = valueFor(args, "--theme") ?? "auto";
|
|
@@ -1170,7 +871,7 @@ function runNotes(argv, io) {
|
|
|
1170
871
|
errOut(io, "--days must be a positive number.");
|
|
1171
872
|
return 1;
|
|
1172
873
|
}
|
|
1173
|
-
const file =
|
|
874
|
+
const file = resolve2(io.cwd, valueFor(argv, "--file") ?? ".iteration-feedback.json");
|
|
1174
875
|
const all = has(argv, "--all");
|
|
1175
876
|
const doneIdx = argv.indexOf("--done");
|
|
1176
877
|
const undoneIdx = argv.indexOf("--undone");
|