{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAIlF,mCAAmC;AACnC,MAAM,WAAW,aAAa;IAC7B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAED,8CAA8C;AAC9C,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B;;;OAGG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,sBAAsB,GAC/B;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC7B,SAAS,EAAE,yBAAyB,EACpC,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAChC,gBAAgB,GAAG,SAAS,CAG9B;AAwDD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACpC,SAAS,EAAE,yBAAyB,EACpC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAChC,kBAAkB,GAAG,sBAAsB,CA4C7C;AAED,kDAAkD;AAClD,MAAM,WAAW,kBAAkB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACpC,SAAS,EAAE,yBAAyB,EACpC,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAChC,kBAAkB,GAAG,sBAAsB,CAc7C;AAED,2DAA2D;AAC3D,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,sBAAsB,CAE9E","sourcesContent":["/**\n * Renaming and removing a canvas extension.\n *\n * Design: `docs/canvas-extensions-design.md` §13.6. These exist because a canvas\n * has more identity than a file does, and it is spread across four places: the\n * directory name (which *is* the extension id, since discovery keys off\n * position), the `id` a canvas declares, its `displayName`, and whatever the\n * header comment tells the reader to type. Renaming by hand means getting all\n * four right, and getting the `id` wrong is not a typo — it drops the canvas the\n * person is looking at on the next reload, because the instance was opened\n * against a canvas that no longer exists.\n *\n * So the rewriting is deliberately narrow. It changes the three places the\n * scaffold puts the name and nothing else, then **reports every other line the\n * old name still appears on** rather than guessing at prose. A rename that\n * silently edited a description would be worse than one that admits what it left\n * behind.\n */\n\nimport { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { CanvasSearchRoot, DiscoveredCanvasExtension } from \"./discovery.js\";\nimport { CANVAS_ENTRY_FILE } from \"./discovery.js\";\nimport { validateCanvasName } from \"./scaffold.js\";\n\n/** One line the rename changed. */\nexport interface CanvasRewrite {\n\t/** 1-based, so it can be read straight off an editor gutter. */\n\tline: number;\n\tbefore: string;\n\tafter: string;\n}\n\n/** What {@link renameCanvasExtension} did. */\nexport interface CanvasRenameResult {\n\tfrom: string;\n\tto: string;\n\t/** Workspace-relative where possible, for a message a person can act on. */\n\tdir: string;\n\trewrites: CanvasRewrite[];\n\t/**\n\t * Lines where the old name survives, because they are prose rather than\n\t * identity. Surfaced, never silently edited.\n\t */\n\tleftovers: number[];\n}\n\n/**\n * Why a canvas cannot be renamed or removed in place.\n *\n * `packaged` is the interesting one: a plugin's canvases are named by its\n * manifest rather than by where they sit, so moving the directory would either\n * do nothing or break the plugin. That is `/plugin`'s job, not this one's.\n */\nexport type CanvasLifecycleRefusal =\n\t| { reason: \"invalid-name\"; detail: string }\n\t| { reason: \"exists\"; detail: string }\n\t| { reason: \"packaged\"; detail: string }\n\t| { reason: \"unwritable\"; detail: string };\n\n/**\n * Whether this extension is one we may move or delete.\n *\n * The test is positional and deliberately so: an extension is ours to edit when\n * its directory sits *directly* inside one of the search roots, which is exactly\n * the layout `discoverCanvasExtensions` walks. Anything else arrived inside a\n * package — resolved through a manifest by `plugin-canvases.ts` — and its\n * location is that package's business.\n */\nexport function canvasHomeRoot(\n\textension: DiscoveredCanvasExtension,\n\troots: readonly CanvasSearchRoot[],\n): CanvasSearchRoot | undefined {\n\tconst parent = path.dirname(path.resolve(extension.dir));\n\treturn roots.find((root) => path.resolve(root.dir) === parent);\n}\n\n/**\n * Swap the old name for the new one where it is *identity* rather than prose.\n *\n * Two rules, and the first is the interesting one: **a string literal whose\n * entire content is the old name**. That covers `id`, `displayName`, `title` and\n * the scaffold's `const ID = \"…\"` without knowing any of their names, and it\n * does not touch a sentence that merely mentions the canvas — `\"the board is the\n * point of the board\"` is not the string `\"board\"`. A whole-word replacement\n * would have rewritten that sentence, which is worse than leaving it.\n *\n * The second rule keeps the instructions in a comment honest: `/canvas open\n * <old>` would otherwise tell the next reader to type a name that no longer\n * resolves.\n *\n * Anything else is left and reported. The scaffold names itself in exactly three\n * places, all covered here, so a canvas that has not been renamed by hand comes\n * through with nothing left over.\n */\nfunction rewriteEntrySource(source: string, from: string, to: string): { source: string; rewrites: CanvasRewrite[] } {\n\tconst name = escapeForRegExp(from);\n\tconst substitutions: [RegExp, string][] = [\n\t\t// A quoted string that is *only* the old name. Backticks included, but only\n\t\t// without interpolation — `${x}-${from}` is a computed value, not a literal.\n\t\t[new RegExp(`([\"'])${name}\\\\1`, \"g\"), `$1${to}$1`],\n\t\t[new RegExp(`\\`${name}\\``, \"g\"), `\\`${to}\\``],\n\t\t// Instructions in a comment: /canvas open <old>, /canvas reload <old>, …\n\t\t[new RegExp(`(/canvas\\\\s+(?:open|reload|rename|remove|close)\\\\s+)${name}\\\\b`, \"g\"), `$1${to}`],\n\t];\n\n\tconst before = source.split(\"\\n\");\n\tconst after = before.map((line) =>\n\t\tsubstitutions.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), line),\n\t);\n\n\tconst rewrites: CanvasRewrite[] = [];\n\tafter.forEach((line, index) => {\n\t\tif (line !== before[index]) rewrites.push({ line: index + 1, before: before[index] as string, after: line });\n\t});\n\treturn { source: after.join(\"\\n\"), rewrites };\n}\n\n/** Lines still mentioning `name` after the rewrite. */\nfunction remainingMentions(source: string, name: string): number[] {\n\tconst lines: number[] = [];\n\tsource.split(\"\\n\").forEach((line, index) => {\n\t\tif (line.includes(name)) lines.push(index + 1);\n\t});\n\treturn lines;\n}\n\nfunction escapeForRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Rename a canvas extension: its directory, and the name inside its code.\n *\n * The caller is responsible for closing open instances first. This does not do\n * it, because closing is the *session's* concern and this module has no session —\n * but skipping it leaves an instance pointing at a directory that has moved.\n */\nexport function renameCanvasExtension(\n\textension: DiscoveredCanvasExtension,\n\tto: string,\n\troots: readonly CanvasSearchRoot[],\n): CanvasRenameResult | CanvasLifecycleRefusal {\n\tconst invalid = validateCanvasName(to);\n\tif (invalid) return { reason: \"invalid-name\", detail: invalid };\n\tif (to === extension.id) return { reason: \"invalid-name\", detail: `it is already called \"${to}\"` };\n\n\tconst root = canvasHomeRoot(extension, roots);\n\tif (!root) {\n\t\treturn {\n\t\t\treason: \"packaged\",\n\t\t\tdetail: `\"${extension.id}\" came from an installed plugin, which names its canvases in its own manifest. Renaming it here would break the plugin; use /plugin instead.`,\n\t\t};\n\t}\n\n\tconst target = path.join(root.dir, to);\n\tif (existsSync(target)) return { reason: \"exists\", detail: `${target} already exists` };\n\n\tconst entry = path.join(extension.dir, CANVAS_ENTRY_FILE);\n\tlet source: string;\n\ttry {\n\t\tsource = readFileSync(entry, \"utf8\");\n\t} catch (cause) {\n\t\treturn {\n\t\t\treason: \"unwritable\",\n\t\t\tdetail: `could not read ${entry}: ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t};\n\t}\n\n\tconst rewritten = rewriteEntrySource(source, extension.id, to);\n\ttry {\n\t\t// Write before moving: if the write fails the directory has not moved and\n\t\t// nothing is half-renamed.\n\t\tif (rewritten.source !== source) writeFileSync(entry, rewritten.source, \"utf8\");\n\t\trenameSync(extension.dir, target);\n\t} catch (cause) {\n\t\treturn { reason: \"unwritable\", detail: cause instanceof Error ? cause.message : String(cause) };\n\t}\n\n\treturn {\n\t\tfrom: extension.id,\n\t\tto,\n\t\tdir: target,\n\t\trewrites: rewritten.rewrites,\n\t\tleftovers: remainingMentions(rewritten.source, extension.id),\n\t};\n}\n\n/** What {@link removeCanvasExtension} deleted. */\nexport interface CanvasRemoveResult {\n\tid: string;\n\tdir: string;\n}\n\n/**\n * Delete a canvas extension's directory.\n *\n * As with rename, the caller closes open instances first — a deleted directory\n * whose child is still forked leaves a process serving code that no longer\n * exists on disk, which is the most confusing state of all.\n */\nexport function removeCanvasExtension(\n\textension: DiscoveredCanvasExtension,\n\troots: readonly CanvasSearchRoot[],\n): CanvasRemoveResult | CanvasLifecycleRefusal {\n\tconst root = canvasHomeRoot(extension, roots);\n\tif (!root) {\n\t\treturn {\n\t\t\treason: \"packaged\",\n\t\t\tdetail: `\"${extension.id}\" came from an installed plugin. Deleting its directory would leave the plugin broken rather than uninstalled; use /plugin instead.`,\n\t\t};\n\t}\n\ttry {\n\t\trmSync(extension.dir, { recursive: true, force: true });\n\t} catch (cause) {\n\t\treturn { reason: \"unwritable\", detail: cause instanceof Error ? cause.message : String(cause) };\n\t}\n\treturn { id: extension.id, dir: extension.dir };\n}\n\n/** Narrow a lifecycle return value to its refusal case. */\nexport function isCanvasRefusal(value: object): value is CanvasLifecycleRefusal {\n\treturn \"reason\" in value;\n}\n"]}