UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

125 lines 6.11 kB
import { isHandoffLevel } from './handoff-level.js'; import { nodeRegistryFs, registryPath } from './registry.js'; /** * The one-time notice for the per-project preference tier B5 deleted (#840). * * Every other retirement on that branch is silent by design — a renamed key is an unknown key, and * an unknown key is dropped. This one is not silent, because dropping it moves in the *permissive* * direction and the permissive thing is an outward action under the user's name: a repo they had * set to "don't publish" falls through to the user-level default, which pushes the branch and opens * a pull request. A setting that quietly stops applying is a bug report; a setting that quietly * starts publishing is a pull request on someone else's repo. * * It is a report, not a migration: nothing here is honoured, carried, or written back. It names * what the file holds and the line that replaces it, and the block goes on being ignored. */ /** The retired block's key in the registry file. Read to report it; never written. */ const RETIRED_KEY = 'projectPreferences'; /** * Every spelling a block could say "how far this repo publishes" in: the rung, and the three * booleans it replaced. * * Named rather than interpreted. A block written before the ladder cannot be turned back into a * rung without the mapping this branch deleted, and resurrecting it here would be the migration * code the deletion was about. What the notice needs is only whether this repo's publishing was * set here at all, because that decides whether the row warns or reassures — and a row that * reassures about a repo whose block said "don't publish" is the failure this exists to prevent. */ const PUBLISH_KEYS = ['handoff', 'autoPushBranch', 'autoOpenPr', 'autoMerge']; /** * The retired blocks in the registry file, or `[]` when there are none (which is everyone who * never used the tier, and everyone whose file has been written once since). * * Reads the raw file rather than going through `readRegistry`, which drops the block on the way in * — that is the point of the removal, and it is what leaves this the only reader. */ export async function readRetiredProjectSettings(fs = nodeRegistryFs(), env = process.env) { let parsed; try { parsed = JSON.parse(await fs.read(registryPath(env))); } catch { return []; // missing or malformed: the same nothing readRegistry answers } if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return []; const stored = parsed[RETIRED_KEY]; if (typeof stored !== 'object' || stored === null || Array.isArray(stored)) return []; // The project list is the same file's, so an id resolves to the path the user knows it by. const projects = parsed.projects; const paths = new Map(); if (Array.isArray(projects)) { for (const project of projects) { const record = project; if (typeof record?.id === 'string' && typeof record.path === 'string') paths.set(record.id, record.path); } } const retired = []; for (const [projectId, block] of Object.entries(stored)) { if (typeof block !== 'object' || block === null || Array.isArray(block)) continue; const keys = Object.keys(block); if (!keys.length) continue; const handoff = block['handoff']; retired.push({ // A project since removed from the registry still had settings; name it by the id rather // than dropping it, since the point is that nothing stopped applying silently. path: paths.get(projectId) ?? projectId, keys, publishes: keys.some(key => PUBLISH_KEYS.includes(key)), ...(isHandoffLevel(handoff) ? { handoff } : {}), }); } return retired; } /** * The boot snapshot, and why there is one. * * `writeRegistry` rewrites the file from the fields it knows, so the first preference write of a * session erases the block this notice is about. A notice raised lazily — at the first publish, * say — would race that erasure and lose, so the read happens once at boot, ahead of every write * path, and the dashboard is served from what it found. */ let captured = []; /** Read the blocks and hold them for this process. Called once, first thing, by the daemon. */ export async function captureRetiredProjectSettings(fs, env = process.env) { captured = await readRetiredProjectSettings(fs, env).catch(() => []); return captured; } /** What the boot read found. Empty until {@link captureRetiredProjectSettings} has run. */ export function retiredProjectSettings() { return captured; } /** * What a repo's row has to say, as both surfaces say it. * * A block that set publishing gets the line to write and what happens without it, because that is * the direction that acts outward on its own. Everything else is told what applies instead, which * is true only for a block that did *not* set publishing — hence the flag rather than the rung. */ export function retiredProjectAdvice(project) { if (!project.publishes) return 'Your own settings apply to it now.'; const rung = project.handoff ?? 'local | push | pr | merge'; return `Add \`handoff: ${rung}\` to its the-framework.yml — without it, agents there push their branch and open a pull request.`; } /** * The notice as the daemon's log prints it: one line naming the tier, then one per project. * * Empty for an empty list, so the caller prints nothing rather than a heading over nothing. */ export function retiredProjectSettingsNotice(retired) { if (!retired.length) return []; const lines = [ `[framework] per-project settings are no longer read — a repo's settings live in its own the-framework.yml.`, ]; for (const project of retired) { lines.push(`[framework] ${project.path} had ${project.keys.join(', ')}. ${retiredProjectAdvice(project)}`); } return lines; } //# sourceMappingURL=retired-project-settings.js.map