mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
add documentation for ai agents (#3115)
* 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
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
||||
import { NO_TIMEOUT, SIGTERM } from "../../../base/lib/types"
|
||||
import { DEFAULT_SIGTERM_TIMEOUT } from '.'
|
||||
import { NO_TIMEOUT, SIGTERM } from '../../../base/lib/types'
|
||||
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { SubContainer } from "../util/SubContainer"
|
||||
import { Drop, splitCommand } from "../util"
|
||||
import * as cp from "child_process"
|
||||
import * as fs from "node:fs/promises"
|
||||
import { DaemonCommandType, ExecCommandOptions, ExecFnOptions } from "./Daemons"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { SubContainer } from '../util/SubContainer'
|
||||
import { Drop, splitCommand } from '../util'
|
||||
import * as cp from 'child_process'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import { DaemonCommandType, ExecCommandOptions, ExecFnOptions } from './Daemons'
|
||||
|
||||
export class CommandController<
|
||||
Manifest extends T.SDKManifest,
|
||||
@@ -31,7 +31,7 @@ export class CommandController<
|
||||
exec: DaemonCommandType<Manifest, C>,
|
||||
) => {
|
||||
try {
|
||||
if ("fn" in exec) {
|
||||
if ('fn' in exec) {
|
||||
const abort = new AbortController()
|
||||
const cell: { ctrl: CommandController<Manifest, C> } = {
|
||||
ctrl: new CommandController<Manifest, C>(
|
||||
@@ -63,9 +63,9 @@ export class CommandController<
|
||||
if (T.isUseEntrypoint(exec.command)) {
|
||||
const imageMeta: T.ImageMetadata = await fs
|
||||
.readFile(`/media/startos/images/${subcontainer!.imageId}.json`, {
|
||||
encoding: "utf8",
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.catch(() => "{}")
|
||||
.catch(() => '{}')
|
||||
.then(JSON.parse)
|
||||
commands = imageMeta.entrypoint ?? []
|
||||
commands = commands.concat(
|
||||
@@ -85,21 +85,21 @@ export class CommandController<
|
||||
env: exec.env,
|
||||
user: exec.user,
|
||||
cwd: exec.cwd,
|
||||
stdio: exec.onStdout || exec.onStderr ? "pipe" : "inherit",
|
||||
stdio: exec.onStdout || exec.onStderr ? 'pipe' : 'inherit',
|
||||
})
|
||||
}
|
||||
|
||||
if (exec.onStdout) childProcess.stdout?.on("data", exec.onStdout)
|
||||
if (exec.onStderr) childProcess.stderr?.on("data", exec.onStderr)
|
||||
if (exec.onStdout) childProcess.stdout?.on('data', exec.onStdout)
|
||||
if (exec.onStderr) childProcess.stderr?.on('data', exec.onStderr)
|
||||
|
||||
const state = { exited: false }
|
||||
const answer = new Promise<null>((resolve, reject) => {
|
||||
childProcess.on("exit", (code) => {
|
||||
childProcess.on('exit', (code) => {
|
||||
state.exited = true
|
||||
if (
|
||||
code === 0 ||
|
||||
code === 143 ||
|
||||
(code === null && childProcess.signalCode == "SIGTERM")
|
||||
(code === null && childProcess.signalCode == 'SIGTERM')
|
||||
) {
|
||||
return resolve(null)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ export class CommandController<
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(new Error("Timed out waiting for js command to exit")),
|
||||
reject(new Error('Timed out waiting for js command to exit')),
|
||||
timeout * 2,
|
||||
),
|
||||
),
|
||||
@@ -151,7 +151,7 @@ export class CommandController<
|
||||
} finally {
|
||||
if (!this.state.exited) {
|
||||
if (this.process instanceof AbortController) this.process.abort()
|
||||
else this.process.kill("SIGKILL")
|
||||
else this.process.kill('SIGKILL')
|
||||
}
|
||||
await this.subcontainer?.destroy()
|
||||
}
|
||||
@@ -161,10 +161,10 @@ export class CommandController<
|
||||
if (!this.state.exited) {
|
||||
if (this.process instanceof AbortController) return this.process.abort()
|
||||
|
||||
if (signal !== "SIGKILL") {
|
||||
if (signal !== 'SIGKILL') {
|
||||
setTimeout(() => {
|
||||
if (this.process instanceof AbortController) this.process.abort()
|
||||
else this.process.kill("SIGKILL")
|
||||
else this.process.kill('SIGKILL')
|
||||
}, timeout)
|
||||
}
|
||||
if (!this.process.kill(signal)) {
|
||||
@@ -180,7 +180,7 @@ export class CommandController<
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(new Error("Timed out waiting for js command to exit")),
|
||||
reject(new Error('Timed out waiting for js command to exit')),
|
||||
timeout * 2,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { asError } from "../../../base/lib/util/asError"
|
||||
import { Drop } from "../util"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { asError } from '../../../base/lib/util/asError'
|
||||
import { Drop } from '../util'
|
||||
import {
|
||||
SubContainer,
|
||||
SubContainerOwned,
|
||||
SubContainerRc,
|
||||
} from "../util/SubContainer"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { DaemonCommandType } from "./Daemons"
|
||||
import { Oneshot } from "./Oneshot"
|
||||
} from '../util/SubContainer'
|
||||
import { CommandController } from './CommandController'
|
||||
import { DaemonCommandType } from './Daemons'
|
||||
import { Oneshot } from './Oneshot'
|
||||
|
||||
const TIMEOUT_INCREMENT_MS = 1000
|
||||
const MAX_TIMEOUT_MS = 30000
|
||||
@@ -87,7 +87,7 @@ export class Daemon<
|
||||
try {
|
||||
fn(success)
|
||||
} catch (e) {
|
||||
console.error("EXIT handler", e)
|
||||
console.error('EXIT handler', e)
|
||||
}
|
||||
}
|
||||
if (success && this.oneshot) {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Signals } from "../../../base/lib/types"
|
||||
import { Signals } from '../../../base/lib/types'
|
||||
|
||||
import { HealthCheckResult } from "../health/checkFns"
|
||||
import { HealthCheckResult } from '../health/checkFns'
|
||||
|
||||
import { Trigger } from "../trigger"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { SubContainer } from "../util/SubContainer"
|
||||
import { Trigger } from '../trigger'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { SubContainer } from '../util/SubContainer'
|
||||
|
||||
import { promisify } from "node:util"
|
||||
import * as CP from "node:child_process"
|
||||
import { promisify } from 'node:util'
|
||||
import * as CP from 'node:child_process'
|
||||
|
||||
export { Daemon } from "./Daemon"
|
||||
export { CommandController } from "./CommandController"
|
||||
import { EXIT_SUCCESS, HealthDaemon } from "./HealthDaemon"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { Oneshot } from "./Oneshot"
|
||||
export { Daemon } from './Daemon'
|
||||
export { CommandController } from './CommandController'
|
||||
import { EXIT_SUCCESS, HealthDaemon } from './HealthDaemon'
|
||||
import { Daemon } from './Daemon'
|
||||
import { CommandController } from './CommandController'
|
||||
import { Oneshot } from './Oneshot'
|
||||
|
||||
export const cpExec = promisify(CP.exec)
|
||||
export const cpExecFile = promisify(CP.execFile)
|
||||
@@ -231,7 +231,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
const res = (options: AddDaemonParams<Manifest, Ids, Id, C> | null) => {
|
||||
if (!options) return prev
|
||||
const daemon =
|
||||
"daemon" in options
|
||||
'daemon' in options
|
||||
? options.daemon
|
||||
: Daemon.of<Manifest>()<C>(
|
||||
this.effects,
|
||||
@@ -369,8 +369,8 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
const healthDaemon = new HealthDaemon<Manifest>(
|
||||
daemon,
|
||||
[...this.healthDaemons],
|
||||
"__RUN_UNTIL_SUCCESS",
|
||||
"EXIT_SUCCESS",
|
||||
'__RUN_UNTIL_SUCCESS',
|
||||
'EXIT_SUCCESS',
|
||||
this.effects,
|
||||
)
|
||||
const daemons = await new Daemons<Manifest, Ids>(this.effects, this.ids, [
|
||||
@@ -400,7 +400,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
if (canShutdown.length === 0) {
|
||||
// Dependency cycle that should not happen, just shutdown remaining daemons
|
||||
console.warn(
|
||||
"Dependency cycle detected, shutting down remaining daemons",
|
||||
'Dependency cycle detected, shutting down remaining daemons',
|
||||
)
|
||||
canShutdown.push(...[...remaining].reverse())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { HealthCheckResult } from "../health/checkFns"
|
||||
import { defaultTrigger } from "../trigger/defaultTrigger"
|
||||
import { Ready } from "./Daemons"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
|
||||
import { HealthCheckResult } from '../health/checkFns'
|
||||
import { defaultTrigger } from '../trigger/defaultTrigger'
|
||||
import { Ready } from './Daemons'
|
||||
import { Daemon } from './Daemon'
|
||||
import { SetHealth, Effects, SDKManifest } from '../../../base/lib/types'
|
||||
|
||||
const oncePromise = <T>() => {
|
||||
let resolve: (value: T) => void
|
||||
@@ -12,7 +12,7 @@ const oncePromise = <T>() => {
|
||||
return { resolve: resolve!, promise }
|
||||
}
|
||||
|
||||
export const EXIT_SUCCESS = "EXIT_SUCCESS" as const
|
||||
export const EXIT_SUCCESS = 'EXIT_SUCCESS' as const
|
||||
|
||||
/**
|
||||
* Wanted a structure that deals with controlling daemons by their health status
|
||||
@@ -22,7 +22,7 @@ export const EXIT_SUCCESS = "EXIT_SUCCESS" as const
|
||||
*
|
||||
*/
|
||||
export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
private _health: HealthCheckResult = { result: "waiting", message: null }
|
||||
private _health: HealthCheckResult = { result: 'waiting', message: null }
|
||||
private healthWatchers: Array<() => unknown> = []
|
||||
private running = false
|
||||
private started?: number
|
||||
@@ -102,21 +102,21 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
}
|
||||
private async setupHealthCheck() {
|
||||
this.daemon?.onExit((success) => {
|
||||
if (success && this.ready === "EXIT_SUCCESS") {
|
||||
this.setHealth({ result: "success", message: null })
|
||||
if (success && this.ready === 'EXIT_SUCCESS') {
|
||||
this.setHealth({ result: 'success', message: null })
|
||||
} else if (!success) {
|
||||
this.setHealth({
|
||||
result: "failure",
|
||||
result: 'failure',
|
||||
message: `${this.id} daemon crashed`,
|
||||
})
|
||||
} else if (!this.daemon?.isOneshot()) {
|
||||
this.setHealth({
|
||||
result: "failure",
|
||||
result: 'failure',
|
||||
message: `${this.id} daemon exited`,
|
||||
})
|
||||
}
|
||||
})
|
||||
if (this.ready === "EXIT_SUCCESS") return
|
||||
if (this.ready === 'EXIT_SUCCESS') return
|
||||
if (this.healthCheckCleanup) return
|
||||
const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({
|
||||
lastResult: this._health.result,
|
||||
@@ -127,7 +127,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
}>()
|
||||
const { promise: exited, resolve: setExited } = oncePromise<null>()
|
||||
new Promise(async () => {
|
||||
if (this.ready === "EXIT_SUCCESS") return
|
||||
if (this.ready === 'EXIT_SUCCESS') return
|
||||
for (
|
||||
let res = await Promise.race([status, trigger.next()]);
|
||||
!res.done;
|
||||
@@ -137,8 +137,8 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
this.ready.fn(),
|
||||
).catch((err) => {
|
||||
return {
|
||||
result: "failure",
|
||||
message: "message" in err ? err.message : String(err),
|
||||
result: 'failure',
|
||||
message: 'message' in err ? err.message : String(err),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -166,23 +166,23 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
private async setHealth(health: HealthCheckResult) {
|
||||
const changed = this._health.result !== health.result
|
||||
this._health = health
|
||||
if (this.resolveReady && health.result === "success") {
|
||||
if (this.resolveReady && health.result === 'success') {
|
||||
this.resolveReady()
|
||||
}
|
||||
if (changed) this.healthWatchers.forEach((watcher) => watcher())
|
||||
if (this.ready === "EXIT_SUCCESS") return
|
||||
if (this.ready === 'EXIT_SUCCESS') return
|
||||
const display = this.ready.display
|
||||
if (!display) {
|
||||
return
|
||||
}
|
||||
let result = health.result
|
||||
if (
|
||||
result === "failure" &&
|
||||
result === 'failure' &&
|
||||
this.started &&
|
||||
performance.now() - this.started <= (this.ready.gracePeriod ?? 10_000)
|
||||
)
|
||||
result = "starting"
|
||||
if (result === "failure") {
|
||||
result = 'starting'
|
||||
if (result === 'failure') {
|
||||
console.error(`Health Check ${this.id} failed:`, health.message)
|
||||
}
|
||||
await this.effects.setHealth({
|
||||
@@ -197,10 +197,10 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
const healths = this.dependencies.map((d) => ({
|
||||
health: d.running && d._health,
|
||||
id: d.id,
|
||||
display: typeof d.ready === "object" ? d.ready.display : null,
|
||||
display: typeof d.ready === 'object' ? d.ready.display : null,
|
||||
}))
|
||||
const waitingOn = healths.filter(
|
||||
(h) => !h.health || h.health.result !== "success",
|
||||
(h) => !h.health || h.health.result !== 'success',
|
||||
)
|
||||
if (waitingOn.length)
|
||||
console.debug(
|
||||
@@ -210,10 +210,10 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
const waitingOnNames = waitingOn.flatMap((w) =>
|
||||
w.display ? [w.display] : [],
|
||||
)
|
||||
const message = waitingOnNames.length ? waitingOnNames.join(", ") : null
|
||||
await this.setHealth({ result: "waiting", message })
|
||||
const message = waitingOnNames.length ? waitingOnNames.join(', ') : null
|
||||
await this.setHealth({ result: 'waiting', message })
|
||||
} else {
|
||||
await this.setHealth({ result: "starting", message: null })
|
||||
await this.setHealth({ result: 'starting', message: null })
|
||||
}
|
||||
await this.changeRunning(!waitingOn.length)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { IdMap, MountOptions } from "../util/SubContainer"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { IdMap, MountOptions } from '../util/SubContainer'
|
||||
|
||||
type MountArray = { mountpoint: string; options: MountOptions }[]
|
||||
|
||||
@@ -13,7 +13,7 @@ type SharedOptions = {
|
||||
*
|
||||
* defaults to "directory"
|
||||
* */
|
||||
type?: "file" | "directory" | "infer"
|
||||
type?: 'file' | 'directory' | 'infer'
|
||||
// /**
|
||||
// * Whether to map uids/gids for the mount
|
||||
// *
|
||||
@@ -35,16 +35,16 @@ type SharedOptions = {
|
||||
|
||||
type VolumeOpts<Manifest extends T.SDKManifest> = {
|
||||
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest */
|
||||
volumeId: Manifest["volumes"][number]
|
||||
volumeId: Manifest['volumes'][number]
|
||||
/** Whether or not the resource should be readonly for this subcontainer */
|
||||
readonly: boolean
|
||||
} & SharedOptions
|
||||
|
||||
type DependencyOpts<Manifest extends T.SDKManifest> = {
|
||||
/** The ID of the dependency */
|
||||
dependencyId: Manifest["id"]
|
||||
dependencyId: Manifest['id']
|
||||
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest of the dependency */
|
||||
volumeId: Manifest["volumes"][number]
|
||||
volumeId: Manifest['volumes'][number]
|
||||
/** Whether or not the resource should be readonly for this subcontainer */
|
||||
readonly: boolean
|
||||
} & SharedOptions
|
||||
@@ -126,11 +126,11 @@ export class Mounts<
|
||||
this.volumes.map((v) => ({
|
||||
mountpoint: v.mountpoint,
|
||||
options: {
|
||||
type: "volume",
|
||||
type: 'volume',
|
||||
volumeId: v.volumeId,
|
||||
subpath: v.subpath,
|
||||
readonly: v.readonly,
|
||||
filetype: v.type ?? "directory",
|
||||
filetype: v.type ?? 'directory',
|
||||
idmap: [],
|
||||
},
|
||||
})),
|
||||
@@ -139,9 +139,9 @@ export class Mounts<
|
||||
this.assets.map((a) => ({
|
||||
mountpoint: a.mountpoint,
|
||||
options: {
|
||||
type: "assets",
|
||||
type: 'assets',
|
||||
subpath: a.subpath,
|
||||
filetype: a.type ?? "directory",
|
||||
filetype: a.type ?? 'directory',
|
||||
idmap: [],
|
||||
},
|
||||
})),
|
||||
@@ -150,12 +150,12 @@ export class Mounts<
|
||||
this.dependencies.map((d) => ({
|
||||
mountpoint: d.mountpoint,
|
||||
options: {
|
||||
type: "pointer",
|
||||
type: 'pointer',
|
||||
packageId: d.dependencyId,
|
||||
volumeId: d.volumeId,
|
||||
subpath: d.subpath,
|
||||
readonly: d.readonly,
|
||||
filetype: d.type ?? "directory",
|
||||
filetype: d.type ?? 'directory',
|
||||
idmap: [],
|
||||
},
|
||||
})),
|
||||
@@ -163,6 +163,6 @@ export class Mounts<
|
||||
}
|
||||
}
|
||||
|
||||
const a = Mounts.of().mountBackups({ subpath: null, mountpoint: "" })
|
||||
const a = Mounts.of().mountBackups({ subpath: null, mountpoint: '' })
|
||||
// @ts-expect-error
|
||||
const m: Mounts<T.SDKManifest, never> = a
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { SubContainer, SubContainerOwned } from "../util/SubContainer"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { DaemonCommandType } from "./Daemons"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { SubContainer, SubContainerOwned } from '../util/SubContainer'
|
||||
import { CommandController } from './CommandController'
|
||||
import { Daemon } from './Daemon'
|
||||
import { DaemonCommandType } from './Daemons'
|
||||
|
||||
/**
|
||||
* This is a wrapper around CommandController that has a state of off, where the command shouldn't be running
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { Daemons } from "./Daemons"
|
||||
import "../../../base/lib/interfaces/ServiceInterfaceBuilder"
|
||||
import "../../../base/lib/interfaces/Origin"
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { Daemons } from './Daemons'
|
||||
import '../../../base/lib/interfaces/ServiceInterfaceBuilder'
|
||||
import '../../../base/lib/interfaces/Origin'
|
||||
|
||||
export const DEFAULT_SIGTERM_TIMEOUT = 60_000
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user