mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
* add documentation for ai agents * docs: consolidate CLAUDE.md and CONTRIBUTING.md, add style guidelines - Refactor CLAUDE.md to reference CONTRIBUTING.md for build/test/format info - Expand CONTRIBUTING.md with comprehensive build targets, env vars, and testing - Add code style guidelines section with conventional commits - Standardize SDK prettier config to use single quotes (matching web) - Add project-level Claude Code settings to disable co-author attribution * style(sdk): apply prettier with single quotes Run prettier across sdk/base and sdk/package to apply the standardized quote style (single quotes matching web). * docs: add USER.md for per-developer TODO filtering - Add agents/USER.md to .gitignore (contains user identifier) - Document session startup flow in CLAUDE.md: - Create USER.md if missing, prompting for identifier - Filter TODOs by @username tags - Offer relevant TODOs on session start * docs: add i18n documentation task to agent TODOs * docs: document i18n ID patterns in core/ Add agents/i18n-patterns.md covering rust-i18n setup, translation file format, t!() macro usage, key naming conventions, and locale selection. Remove completed TODO item and add reference in CLAUDE.md. * chore: clarify that all builds work on any OS with Docker
320 lines
9.5 KiB
TypeScript
320 lines
9.5 KiB
TypeScript
import { ExtendedVersion, VersionRange } from '../../../base/lib/exver'
|
|
import * as T from '../../../base/lib/types'
|
|
import {
|
|
InitFn,
|
|
InitKind,
|
|
InitScript,
|
|
InitScriptOrFn,
|
|
UninitFn,
|
|
UninitScript,
|
|
UninitScriptOrFn,
|
|
} from '../../../base/lib/inits'
|
|
import { Graph, Vertex, once } from '../util'
|
|
import { IMPOSSIBLE, VersionInfo } from './VersionInfo'
|
|
|
|
export async function getDataVersion(effects: T.Effects) {
|
|
const versionStr = await effects.getDataVersion()
|
|
if (!versionStr) return null
|
|
try {
|
|
return ExtendedVersion.parse(versionStr)
|
|
} catch (_) {
|
|
return VersionRange.parse(versionStr)
|
|
}
|
|
}
|
|
|
|
export async function setDataVersion(
|
|
effects: T.Effects,
|
|
version: ExtendedVersion | VersionRange | null,
|
|
) {
|
|
return effects.setDataVersion({ version: version?.toString() || null })
|
|
}
|
|
|
|
function isExver(v: ExtendedVersion | VersionRange): v is ExtendedVersion {
|
|
return 'satisfies' in v
|
|
}
|
|
|
|
function isRange(v: ExtendedVersion | VersionRange): v is VersionRange {
|
|
return 'satisfiedBy' in v
|
|
}
|
|
|
|
export function overlaps(
|
|
a: ExtendedVersion | VersionRange,
|
|
b: ExtendedVersion | VersionRange,
|
|
) {
|
|
return (
|
|
(isRange(a) && isRange(b) && a.intersects(b)) ||
|
|
(isRange(a) && isExver(b) && a.satisfiedBy(b)) ||
|
|
(isExver(a) && isRange(b) && a.satisfies(b)) ||
|
|
(isExver(a) && isExver(b) && a.equals(b))
|
|
)
|
|
}
|
|
|
|
export class VersionGraph<CurrentVersion extends string>
|
|
implements InitScript, UninitScript
|
|
{
|
|
protected initFn = this.init.bind(this)
|
|
protected uninitFn = this.uninit.bind(this)
|
|
private readonly graph: () => Graph<
|
|
ExtendedVersion | VersionRange,
|
|
((opts: { effects: T.Effects }) => Promise<void>) | undefined
|
|
>
|
|
dump(): string {
|
|
return this.graph().dump((metadata) => metadata?.toString())
|
|
}
|
|
private constructor(
|
|
readonly current: VersionInfo<CurrentVersion>,
|
|
versions: Array<VersionInfo<any>>,
|
|
private readonly preInstall?: InitScriptOrFn<'install'>,
|
|
private readonly uninstall?: UninitScript | UninitFn,
|
|
) {
|
|
this.graph = once(() => {
|
|
const graph = new Graph<
|
|
ExtendedVersion | VersionRange,
|
|
((opts: { effects: T.Effects }) => Promise<void>) | undefined
|
|
>()
|
|
const flavorMap: Record<
|
|
string,
|
|
[
|
|
ExtendedVersion,
|
|
VersionInfo<any>,
|
|
Vertex<
|
|
ExtendedVersion | VersionRange,
|
|
((opts: { effects: T.Effects }) => Promise<void>) | undefined
|
|
>,
|
|
][]
|
|
> = {}
|
|
for (let version of [current, ...versions]) {
|
|
const v = ExtendedVersion.parse(version.options.version)
|
|
const vertex = graph.addVertex(v, [], [])
|
|
const flavor = v.flavor || ''
|
|
if (!flavorMap[flavor]) {
|
|
flavorMap[flavor] = []
|
|
}
|
|
flavorMap[flavor].push([v, version, vertex])
|
|
}
|
|
for (let flavor in flavorMap) {
|
|
flavorMap[flavor].sort((a, b) => a[0].compareForSort(b[0]))
|
|
let prev:
|
|
| [
|
|
ExtendedVersion,
|
|
VersionInfo<any>,
|
|
Vertex<
|
|
ExtendedVersion | VersionRange,
|
|
((opts: { effects: T.Effects }) => Promise<void>) | undefined
|
|
>,
|
|
]
|
|
| undefined = undefined
|
|
for (let [v, version, vertex] of flavorMap[flavor]) {
|
|
if (version.options.migrations.up !== IMPOSSIBLE) {
|
|
let range
|
|
if (prev) {
|
|
graph.addEdge(version.options.migrations.up, prev[2], vertex)
|
|
range = VersionRange.anchor('>=', prev[0]).and(
|
|
VersionRange.anchor('<', v),
|
|
)
|
|
} else {
|
|
range = VersionRange.anchor('<', v)
|
|
}
|
|
const vRange = graph.addVertex(range, [], [])
|
|
graph.addEdge(version.options.migrations.up, vRange, vertex)
|
|
}
|
|
|
|
if (version.options.migrations.down !== IMPOSSIBLE) {
|
|
let range
|
|
if (prev) {
|
|
graph.addEdge(version.options.migrations.down, vertex, prev[2])
|
|
range = VersionRange.anchor('>=', prev[0]).and(
|
|
VersionRange.anchor('<', v),
|
|
)
|
|
} else {
|
|
range = VersionRange.anchor('<', v)
|
|
}
|
|
const vRange = graph.addVertex(range, [], [])
|
|
graph.addEdge(version.options.migrations.down, vertex, vRange)
|
|
}
|
|
|
|
if (version.options.migrations.other) {
|
|
for (let rangeStr in version.options.migrations.other) {
|
|
const range = VersionRange.parse(rangeStr)
|
|
const vRange = graph.addVertex(range, [], [])
|
|
const migration = version.options.migrations.other[rangeStr]
|
|
if (migration.up) graph.addEdge(migration.up, vRange, vertex)
|
|
if (migration.down) graph.addEdge(migration.down, vertex, vRange)
|
|
for (let matching of graph.findVertex(
|
|
(v) => isExver(v.metadata) && v.metadata.satisfies(range),
|
|
)) {
|
|
if (migration.up) graph.addEdge(migration.up, matching, vertex)
|
|
if (migration.down)
|
|
graph.addEdge(migration.down, vertex, matching)
|
|
}
|
|
}
|
|
}
|
|
|
|
prev = [v, version, vertex]
|
|
}
|
|
}
|
|
return graph
|
|
})
|
|
}
|
|
currentVersion = once(() =>
|
|
ExtendedVersion.parse(this.current.options.version),
|
|
)
|
|
/**
|
|
* Each exported `VersionInfo.of()` should be imported and provided as an argument to this function.
|
|
*
|
|
* ** The current version must be the FIRST argument. **
|
|
*/
|
|
static of<
|
|
CurrentVersion extends string,
|
|
OtherVersions extends Array<VersionInfo<any>>,
|
|
>(options: {
|
|
current: VersionInfo<CurrentVersion>
|
|
other: OtherVersions
|
|
/**
|
|
* A script to run only on fresh install
|
|
*/
|
|
preInstall?: InitScriptOrFn<'install'>
|
|
/**
|
|
* A script to run only on uninstall
|
|
*/
|
|
uninstall?: UninitScriptOrFn
|
|
}) {
|
|
return new VersionGraph(
|
|
options.current,
|
|
options.other,
|
|
options.preInstall,
|
|
options.uninstall,
|
|
)
|
|
}
|
|
async migrate({
|
|
effects,
|
|
from,
|
|
to,
|
|
}: {
|
|
effects: T.Effects
|
|
from: ExtendedVersion | VersionRange
|
|
to: ExtendedVersion | VersionRange
|
|
}): Promise<ExtendedVersion | VersionRange> {
|
|
if (overlaps(from, to)) return from
|
|
const graph = this.graph()
|
|
if (from && to) {
|
|
const path = graph.shortestPath(
|
|
(v) => overlaps(v.metadata, from),
|
|
(v) => overlaps(v.metadata, to),
|
|
)
|
|
if (path) {
|
|
console.log(
|
|
`Migrating ${
|
|
path.reduce<{ acc: string; prev: string | null }>(
|
|
({ acc, prev }, x) => ({
|
|
acc:
|
|
acc +
|
|
(prev && prev != x.from.metadata.toString()
|
|
? ` (as ${prev})`
|
|
: '') +
|
|
' -> ' +
|
|
x.to.metadata.toString(),
|
|
prev: x.to.metadata.toString(),
|
|
}),
|
|
{ acc: from.toString(), prev: null },
|
|
).acc
|
|
}`,
|
|
)
|
|
let dataVersion = from
|
|
for (let edge of path) {
|
|
if (edge.metadata) {
|
|
await edge.metadata({ effects })
|
|
}
|
|
dataVersion = edge.to.metadata
|
|
await setDataVersion(effects, edge.to.metadata)
|
|
}
|
|
return dataVersion
|
|
}
|
|
}
|
|
throw new Error(
|
|
`cannot migrate from ${from.toString()} to ${to.toString()}`,
|
|
)
|
|
}
|
|
canMigrateFrom = once(() =>
|
|
Array.from(
|
|
this.graph().reverseBreadthFirstSearch((v) =>
|
|
overlaps(v.metadata, this.currentVersion()),
|
|
),
|
|
)
|
|
.reduce(
|
|
(acc, x) =>
|
|
acc.or(
|
|
isRange(x.metadata)
|
|
? x.metadata
|
|
: VersionRange.anchor('=', x.metadata),
|
|
),
|
|
VersionRange.none(),
|
|
)
|
|
.normalize(),
|
|
)
|
|
canMigrateTo = once(() =>
|
|
Array.from(
|
|
this.graph().breadthFirstSearch((v) =>
|
|
overlaps(v.metadata, this.currentVersion()),
|
|
),
|
|
)
|
|
.reduce(
|
|
(acc, x) =>
|
|
acc.or(
|
|
isRange(x.metadata)
|
|
? x.metadata
|
|
: VersionRange.anchor('=', x.metadata),
|
|
),
|
|
VersionRange.none(),
|
|
)
|
|
.normalize(),
|
|
)
|
|
|
|
async init(effects: T.Effects, kind: InitKind): Promise<void> {
|
|
const from = await getDataVersion(effects)
|
|
if (from) {
|
|
await this.migrate({
|
|
effects,
|
|
from,
|
|
to: this.currentVersion(),
|
|
})
|
|
} else {
|
|
kind = 'install' // implied by !dataVersion
|
|
if (this.preInstall)
|
|
if ('init' in this.preInstall) await this.preInstall.init(effects, kind)
|
|
else await this.preInstall(effects, kind)
|
|
await effects.setDataVersion({ version: this.current.options.version })
|
|
}
|
|
}
|
|
|
|
async uninit(
|
|
effects: T.Effects,
|
|
target: VersionRange | ExtendedVersion | null,
|
|
): Promise<void> {
|
|
if (target) {
|
|
const from = await getDataVersion(effects)
|
|
if (from) {
|
|
target = await this.migrate({
|
|
effects,
|
|
from,
|
|
to: target,
|
|
})
|
|
}
|
|
} else {
|
|
if (this.uninstall)
|
|
if ('uninit' in this.uninstall)
|
|
await this.uninstall.uninit(effects, target)
|
|
else await this.uninstall(effects, target)
|
|
}
|
|
await setDataVersion(effects, target)
|
|
}
|
|
}
|
|
|
|
// prettier-ignore
|
|
export type EnsureUniqueId<A, B = A, OtherVersions = never> =
|
|
B extends [] ? A :
|
|
B extends [VersionInfo<infer Version>, ...infer Rest] ? (
|
|
Version extends OtherVersions ? "One or more versions are not unique"[] :
|
|
EnsureUniqueId<A, Rest, Version | OtherVersions>
|
|
) : "There exists a migration that is not a Migration"[]
|