{"version":3,"file":"projectSettings.cjs","names":["path","DOOM_PACKAGE_NAME","isDoomPackagePath","isRecord","os","fs","readJson","piExtensionAliasPath"],"sources":["../../../src/builders/cli/projectSettings.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { DOOM_PACKAGE_NAME, isDoomPackagePath } from '@agimon-ai/doompi-core/doom-package';\nimport { isRecord, readJson, writeFileAtomic } from '@agimon-ai/doompi-core/runtime-json';\nimport type { JsonObject } from '@agimon-ai/doompi-core/runtime-json';\n\nimport { piExtensionAliasPath } from './piExtensionAlias';\n\n/**\n * Pi's project settings, as `doompi sync` reconciles them.\n *\n * Pi documents project settings as overriding user settings, but resource\n * resolution unions the two scopes and dedupes only by the resolved file's real\n * path. A repository that registers DoomPi alongside the stable user entry\n * therefore loads two installs of the same package against one home-scoped\n * worktree state. Sync removes the redundant project registration and leaves every\n * other key alone: the repository still overrides the user scope, through its own\n * `.doom` configuration and the runtime staged under `~/.pi/.doom/sync`.\n */\n\nconst PI_DIRECTORY = '.pi';\nconst SETTINGS_FILE = 'settings.json';\nconst EXTENSIONS_KEY = 'extensions';\nconst PACKAGES_KEY = 'packages';\nconst PACKAGE_SOURCE_KEY = 'source';\nconst HOME_ALIAS = '~';\nconst HOME_ALIAS_PREFIX = '~/';\n/** Pattern-form entries Pi filters with rather than resolves. */\nconst PATTERN_PREFIXES = ['!', '+', '-'] as const;\n\n/**\n * Reported by `sync --check` and `build`, and repaired by `sync`.\n *\n * Lives here rather than with either command because both report the same\n * finding about the same file, and the detector below is what decides it.\n */\nexport const DUPLICATE_REGISTRATION_DRIFT = 'duplicate DoomPi registration in .pi/settings.json';\n\n/** Pi's project configuration directory, the base its relative paths resolve against. */\nexport function projectPiDirectory(repoRoot: string): string {\n  return path.join(repoRoot, PI_DIRECTORY);\n}\n\nexport function projectPiSettingsPath(repoRoot: string): string {\n  return path.join(projectPiDirectory(repoRoot), SETTINGS_FILE);\n}\n\nfunction resolveSettingsPath(value: string, baseDirectory: string, homeDirectory: string): string {\n  if (value === HOME_ALIAS) return homeDirectory;\n  if (value.startsWith(HOME_ALIAS_PREFIX)) return path.join(homeDirectory, value.slice(HOME_ALIAS_PREFIX.length));\n  return path.resolve(baseDirectory, value);\n}\n\n/**\n * Whether one settings entry names this package.\n *\n * The bare package name is matched textually because that is the spelling sync\n * writes into the user scope, and a resolved path is matched by walking to the\n * package it belongs to, which is what catches `../packages/cli/doompi` and a\n * direct reference to a file inside it. Pattern entries select among already\n * resolved paths rather than naming one, so they are left for the user to own.\n */\nfunction isDoomEntry(value: string, baseDirectory: string, homeDirectory: string): boolean {\n  const normalized = value.replaceAll('\\\\', '/').trim();\n  if (normalized === DOOM_PACKAGE_NAME) return true;\n  if (PATTERN_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return false;\n  return isDoomPackagePath(resolveSettingsPath(normalized, baseDirectory, homeDirectory));\n}\n\nfunction packageSource(entry: unknown): string | undefined {\n  if (typeof entry === 'string') return entry;\n  if (isRecord(entry) && typeof entry[PACKAGE_SOURCE_KEY] === 'string') return entry[PACKAGE_SOURCE_KEY];\n  return undefined;\n}\n\n/**\n * Drops DoomPi from one resource list, or reports that the key should go.\n *\n * A list that empties out is removed rather than left behind: an empty array is\n * indistinguishable from an absent one to Pi, and keeping it would leave the\n * repository looking like it still configures something.\n */\nfunction withoutDoomEntries(value: unknown, baseDirectory: string, homeDirectory: string): unknown {\n  if (!Array.isArray(value)) return value;\n  const kept = value.filter((entry) => {\n    const source = packageSource(entry);\n    return source === undefined || !isDoomEntry(source, baseDirectory, homeDirectory);\n  });\n  if (kept.length === value.length) return value;\n  return kept.length > 0 ? kept : undefined;\n}\n\n/** Produces the project settings content for a reconciliation, without writing it. */\nexport function mergeProjectPiSettings(\n  current: JsonObject,\n  repoRoot: string,\n  homeDirectory: string = os.homedir(),\n): JsonObject {\n  const baseDirectory = projectPiDirectory(repoRoot);\n  const merged: JsonObject = { ...current };\n  for (const key of [EXTENSIONS_KEY, PACKAGES_KEY]) {\n    if (!(key in merged)) continue;\n    const next = withoutDoomEntries(merged[key], baseDirectory, homeDirectory);\n    if (next === undefined) delete merged[key];\n    else merged[key] = next;\n  }\n  return merged;\n}\n\n/** Reads Pi's project settings, or nothing when the repository declares none. */\nexport function readProjectPiSettings(repoRoot: string): JsonObject | undefined {\n  const settingsPath = projectPiSettingsPath(repoRoot);\n  if (!fs.existsSync(settingsPath)) return undefined;\n  const settings = readJson(settingsPath);\n  return isRecord(settings) ? settings : {};\n}\n\nexport function serializeProjectPiSettings(settings: JsonObject): string {\n  return `${JSON.stringify(settings, null, 2)}\\n`;\n}\n\n/** True when the repository still registers DoomPi alongside the user scope. */\nexport function projectRegistersDoom(repoRoot: string, homeDirectory: string = os.homedir()): boolean {\n  const settings = readProjectPiSettings(repoRoot);\n  if (!settings) return false;\n  return (\n    serializeProjectPiSettings(mergeProjectPiSettings(settings, repoRoot, homeDirectory)) !==\n    serializeProjectPiSettings(settings)\n  );\n}\n\n/**\n * Removes the alias a repository-scoped registration needed.\n *\n * Only a symbolic link is removed, for the same reason sync refuses to replace\n * one: anything else at that path was not created here.\n */\nfunction removeProjectExtensionAlias(repoRoot: string): void {\n  const aliasPath = piExtensionAliasPath(projectPiDirectory(repoRoot));\n  if (fs.lstatSync(aliasPath, { throwIfNoEntry: false })?.isSymbolicLink()) {\n    fs.rmSync(aliasPath, { force: true });\n  }\n}\n\n/**\n * Reconciles the repository's Pi settings with the user-scope registration.\n *\n * The file is never created and never deleted, only rewritten: an absent one\n * means the repository registers nothing, and an existing one is a repository\n * root marker that other DoomPi code walks up to find.\n */\nexport function writeProjectPiSettings(repoRoot: string, homeDirectory: string = os.homedir()): string | undefined {\n  const settings = readProjectPiSettings(repoRoot);\n  removeProjectExtensionAlias(repoRoot);\n  if (!settings) return undefined;\n\n  const merged = mergeProjectPiSettings(settings, repoRoot, homeDirectory);\n  const serialized = serializeProjectPiSettings(merged);\n  if (serialized === serializeProjectPiSettings(settings)) return undefined;\n\n  const settingsPath = projectPiSettingsPath(repoRoot);\n  writeFileAtomic(settingsPath, serialized);\n  return settingsPath;\n}\n"],"mappings":"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"}