mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +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,7 +1,7 @@
|
||||
import { Effects } from "../../../base/lib/Effects"
|
||||
import { Manifest, PackageId } from "../../../base/lib/osBindings"
|
||||
import { DropGenerator, DropPromise } from "../../../base/lib/util/Drop"
|
||||
import { deepEqual } from "../../../base/lib/util/deepEqual"
|
||||
import { Effects } from '../../../base/lib/Effects'
|
||||
import { Manifest, PackageId } from '../../../base/lib/osBindings'
|
||||
import { DropGenerator, DropPromise } from '../../../base/lib/util/Drop'
|
||||
import { deepEqual } from '../../../base/lib/util/deepEqual'
|
||||
|
||||
export class GetServiceManifest<Mapped = Manifest> {
|
||||
constructor(
|
||||
@@ -45,7 +45,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
this.effects.onLeaveContext(() => {
|
||||
resolveCell.resolve()
|
||||
})
|
||||
abort?.addEventListener("abort", () => resolveCell.resolve())
|
||||
abort?.addEventListener('abort', () => resolveCell.resolve())
|
||||
while (this.effects.isInContext && !abort?.aborted) {
|
||||
let callback: () => void = () => {}
|
||||
const waitForNext = new Promise<void>((resolve) => {
|
||||
@@ -64,7 +64,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
}
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
return new Promise<never>((_, rej) => rej(new Error('aborted')))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +72,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
*/
|
||||
watch(abort?: AbortSignal): AsyncGenerator<Mapped, never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
abort?.addEventListener('abort', () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"callback function threw an error @ GetServiceManifest.onChange",
|
||||
'callback function threw an error @ GetServiceManifest.onChange',
|
||||
e,
|
||||
)
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
.catch((e) => callback(null, e))
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"callback function threw an error @ GetServiceManifest.onChange",
|
||||
'callback function threw an error @ GetServiceManifest.onChange',
|
||||
e,
|
||||
),
|
||||
)
|
||||
@@ -123,7 +123,7 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
return next
|
||||
}
|
||||
}
|
||||
throw new Error("context left before predicate passed")
|
||||
throw new Error('context left before predicate passed')
|
||||
}),
|
||||
() => ctrl.abort(),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { T } from ".."
|
||||
import { Effects } from "../../../base/lib/Effects"
|
||||
import { DropGenerator, DropPromise } from "../../../base/lib/util/Drop"
|
||||
import { T } from '..'
|
||||
import { Effects } from '../../../base/lib/Effects'
|
||||
import { DropGenerator, DropPromise } from '../../../base/lib/util/Drop'
|
||||
|
||||
export class GetSslCertificate {
|
||||
constructor(
|
||||
@@ -36,7 +36,7 @@ export class GetSslCertificate {
|
||||
this.effects.onLeaveContext(() => {
|
||||
resolveCell.resolve()
|
||||
})
|
||||
abort?.addEventListener("abort", () => resolveCell.resolve())
|
||||
abort?.addEventListener('abort', () => resolveCell.resolve())
|
||||
while (this.effects.isInContext && !abort?.aborted) {
|
||||
let callback: () => void = () => {}
|
||||
const waitForNext = new Promise<void>((resolve) => {
|
||||
@@ -50,7 +50,7 @@ export class GetSslCertificate {
|
||||
})
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
return new Promise<never>((_, rej) => rej(new Error('aborted')))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +60,7 @@ export class GetSslCertificate {
|
||||
abort?: AbortSignal,
|
||||
): AsyncGenerator<[string, string, string], never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
abort?.addEventListener('abort', () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export class GetSslCertificate {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"callback function threw an error @ GetSslCertificate.onChange",
|
||||
'callback function threw an error @ GetSslCertificate.onChange',
|
||||
e,
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export class GetSslCertificate {
|
||||
.catch((e) => callback(null, e))
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"callback function threw an error @ GetSslCertificate.onChange",
|
||||
'callback function threw an error @ GetSslCertificate.onChange',
|
||||
e,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import * as cp from "child_process"
|
||||
import { promisify } from "util"
|
||||
import { Buffer } from "node:buffer"
|
||||
import { once } from "../../../base/lib/util/once"
|
||||
import { Drop } from "../../../base/lib/util/Drop"
|
||||
import { Mounts } from "../mainFn/Mounts"
|
||||
import { BackupEffects } from "../backup/Backups"
|
||||
import { PathBase } from "./Volume"
|
||||
import * as fs from 'fs/promises'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import * as cp from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { once } from '../../../base/lib/util/once'
|
||||
import { Drop } from '../../../base/lib/util/Drop'
|
||||
import { Mounts } from '../mainFn/Mounts'
|
||||
import { BackupEffects } from '../backup/Backups'
|
||||
import { PathBase } from './Volume'
|
||||
|
||||
export const execFile = promisify(cp.execFile)
|
||||
const False = () => false
|
||||
@@ -28,20 +28,20 @@ const TIMES_TO_WAIT_FOR_PROC = 100
|
||||
async function prepBind(
|
||||
from: string | null,
|
||||
to: string,
|
||||
type: "file" | "directory" | "infer",
|
||||
type: 'file' | 'directory' | 'infer',
|
||||
) {
|
||||
const fromMeta = from ? await fs.stat(from).catch((_) => null) : null
|
||||
const toMeta = await fs.stat(to).catch((_) => null)
|
||||
|
||||
if (type === "file" || (type === "infer" && from && fromMeta?.isFile())) {
|
||||
if (type === 'file' || (type === 'infer' && from && fromMeta?.isFile())) {
|
||||
if (toMeta && toMeta.isDirectory()) await fs.rmdir(to, { recursive: false })
|
||||
if (from && !fromMeta) {
|
||||
await fs.mkdir(from.replace(/\/[^\/]*\/?$/, ""), { recursive: true })
|
||||
await fs.writeFile(from, "")
|
||||
await fs.mkdir(from.replace(/\/[^\/]*\/?$/, ''), { recursive: true })
|
||||
await fs.writeFile(from, '')
|
||||
}
|
||||
if (!toMeta) {
|
||||
await fs.mkdir(to.replace(/\/[^\/]*\/?$/, ""), { recursive: true })
|
||||
await fs.writeFile(to, "")
|
||||
await fs.mkdir(to.replace(/\/[^\/]*\/?$/, ''), { recursive: true })
|
||||
await fs.writeFile(to, '')
|
||||
}
|
||||
} else {
|
||||
if (toMeta && toMeta.isFile() && !toMeta.size) await fs.rm(to)
|
||||
@@ -53,20 +53,20 @@ async function prepBind(
|
||||
async function bind(
|
||||
from: string,
|
||||
to: string,
|
||||
type: "file" | "directory" | "infer",
|
||||
type: 'file' | 'directory' | 'infer',
|
||||
idmap: IdMap[],
|
||||
) {
|
||||
await prepBind(from, to, type)
|
||||
|
||||
const args = ["--bind"]
|
||||
const args = ['--bind']
|
||||
|
||||
if (idmap.length) {
|
||||
args.push(
|
||||
`-oX-mount.idmap=${idmap.map((i) => `b:${i.fromId}:${i.toId}:${i.range}`).join(" ")}`,
|
||||
`-oX-mount.idmap=${idmap.map((i) => `b:${i.fromId}:${i.toId}:${i.range}`).join(' ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
await execFile("mount", [...args, from, to])
|
||||
await execFile('mount', [...args, from, to])
|
||||
}
|
||||
|
||||
export interface SubContainer<
|
||||
@@ -74,7 +74,7 @@ export interface SubContainer<
|
||||
Effects extends T.Effects = T.Effects,
|
||||
> extends Drop,
|
||||
PathBase {
|
||||
readonly imageId: keyof Manifest["images"] & T.ImageId
|
||||
readonly imageId: keyof Manifest['images'] & T.ImageId
|
||||
readonly rootfs: string
|
||||
readonly guid: T.Guid
|
||||
|
||||
@@ -185,21 +185,21 @@ export class SubContainerOwned<
|
||||
private waitProc: () => Promise<null>
|
||||
private constructor(
|
||||
readonly effects: Effects,
|
||||
readonly imageId: keyof Manifest["images"] & T.ImageId,
|
||||
readonly imageId: keyof Manifest['images'] & T.ImageId,
|
||||
readonly rootfs: string,
|
||||
readonly guid: T.Guid,
|
||||
) {
|
||||
super()
|
||||
this.leaderExited = false
|
||||
this.leader = cp.spawn(
|
||||
"start-container",
|
||||
["subcontainer", "launch", rootfs],
|
||||
'start-container',
|
||||
['subcontainer', 'launch', rootfs],
|
||||
{
|
||||
killSignal: "SIGKILL",
|
||||
stdio: "inherit",
|
||||
killSignal: 'SIGKILL',
|
||||
stdio: 'inherit',
|
||||
},
|
||||
)
|
||||
this.leader.on("exit", () => {
|
||||
this.leader.on('exit', () => {
|
||||
this.leaderExited = true
|
||||
})
|
||||
this.waitProc = once(
|
||||
@@ -210,7 +210,7 @@ export class SubContainerOwned<
|
||||
!(await fs.stat(`${this.rootfs}/proc/1`).then((x) => !!x, False))
|
||||
) {
|
||||
if (count++ > TIMES_TO_WAIT_FOR_PROC) {
|
||||
console.debug("Failed to start subcontainer", {
|
||||
console.debug('Failed to start subcontainer', {
|
||||
guid: this.guid,
|
||||
imageId: this.imageId,
|
||||
rootfs: this.rootfs,
|
||||
@@ -228,7 +228,7 @@ export class SubContainerOwned<
|
||||
static async of<Manifest extends T.SDKManifest, Effects extends T.Effects>(
|
||||
effects: Effects,
|
||||
image: {
|
||||
imageId: keyof Manifest["images"] & T.ImageId
|
||||
imageId: keyof Manifest['images'] & T.ImageId
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts:
|
||||
@@ -256,20 +256,20 @@ export class SubContainerOwned<
|
||||
if (mounts) {
|
||||
await res.mount(mounts)
|
||||
}
|
||||
const shared = ["dev", "sys"]
|
||||
const shared = ['dev', 'sys']
|
||||
if (!!sharedRun) {
|
||||
shared.push("run")
|
||||
shared.push('run')
|
||||
}
|
||||
|
||||
await fs.mkdir(`${rootfs}/etc`, { recursive: true })
|
||||
await fs.copyFile("/etc/resolv.conf", `${rootfs}/etc/resolv.conf`)
|
||||
await fs.copyFile('/etc/resolv.conf', `${rootfs}/etc/resolv.conf`)
|
||||
|
||||
for (const dirPart of shared) {
|
||||
const from = `/${dirPart}`
|
||||
const to = `${rootfs}/${dirPart}`
|
||||
await fs.mkdir(from, { recursive: true })
|
||||
await fs.mkdir(to, { recursive: true })
|
||||
await execFile("mount", ["--rbind", from, to])
|
||||
await execFile('mount', ['--rbind', from, to])
|
||||
}
|
||||
|
||||
return res
|
||||
@@ -286,7 +286,7 @@ export class SubContainerOwned<
|
||||
>(
|
||||
effects: Effects,
|
||||
image: {
|
||||
imageId: keyof Manifest["images"] & T.ImageId
|
||||
imageId: keyof Manifest['images'] & T.ImageId
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts:
|
||||
@@ -317,7 +317,7 @@ export class SubContainerOwned<
|
||||
}
|
||||
|
||||
subpath(path: string): string {
|
||||
return path.startsWith("/")
|
||||
return path.startsWith('/')
|
||||
? `${this.rootfs}${path}`
|
||||
: `${this.rootfs}/${path}`
|
||||
}
|
||||
@@ -335,36 +335,36 @@ export class SubContainerOwned<
|
||||
): Promise<this> {
|
||||
for (let mount of mounts.build()) {
|
||||
let { options, mountpoint } = mount
|
||||
const path = mountpoint.startsWith("/")
|
||||
const path = mountpoint.startsWith('/')
|
||||
? `${this.rootfs}${mountpoint}`
|
||||
: `${this.rootfs}/${mountpoint}`
|
||||
if (options.type === "volume") {
|
||||
if (options.type === 'volume') {
|
||||
const subpath = options.subpath
|
||||
? options.subpath.startsWith("/")
|
||||
? options.subpath.startsWith('/')
|
||||
? options.subpath
|
||||
: `/${options.subpath}`
|
||||
: "/"
|
||||
: '/'
|
||||
const from = `/media/startos/volumes/${options.volumeId}${subpath}`
|
||||
|
||||
await bind(from, path, options.filetype, options.idmap)
|
||||
} else if (options.type === "assets") {
|
||||
} else if (options.type === 'assets') {
|
||||
const subpath = options.subpath
|
||||
? options.subpath.startsWith("/")
|
||||
? options.subpath.startsWith('/')
|
||||
? options.subpath
|
||||
: `/${options.subpath}`
|
||||
: "/"
|
||||
: '/'
|
||||
const from = `/media/startos/assets/${subpath}`
|
||||
|
||||
await bind(from, path, options.filetype, options.idmap)
|
||||
} else if (options.type === "pointer") {
|
||||
await prepBind(null, path, "directory")
|
||||
} else if (options.type === 'pointer') {
|
||||
await prepBind(null, path, 'directory')
|
||||
await this.effects.mount({ location: path, target: options })
|
||||
} else if (options.type === "backup") {
|
||||
} else if (options.type === 'backup') {
|
||||
const subpath = options.subpath
|
||||
? options.subpath.startsWith("/")
|
||||
? options.subpath.startsWith('/')
|
||||
? options.subpath
|
||||
: `/${options.subpath}`
|
||||
: "/"
|
||||
: '/'
|
||||
const from = `/media/startos/backup${subpath}`
|
||||
|
||||
await bind(from, path, options.filetype, options.idmap)
|
||||
@@ -381,13 +381,13 @@ export class SubContainerOwned<
|
||||
}
|
||||
return new Promise<null>((resolve, reject) => {
|
||||
try {
|
||||
let timeout = setTimeout(() => this.leader.kill("SIGKILL"), 30000)
|
||||
this.leader.on("exit", () => {
|
||||
let timeout = setTimeout(() => this.leader.kill('SIGKILL'), 30000)
|
||||
this.leader.on('exit', () => {
|
||||
clearTimeout(timeout)
|
||||
resolve(null)
|
||||
})
|
||||
if (!this.leader.kill("SIGTERM")) {
|
||||
reject(new Error("kill(2) failed"))
|
||||
if (!this.leader.kill('SIGTERM')) {
|
||||
reject(new Error('kill(2) failed'))
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
@@ -435,17 +435,17 @@ export class SubContainerOwned<
|
||||
await this.waitProc()
|
||||
const imageMeta: T.ImageMetadata = await fs
|
||||
.readFile(`/media/startos/images/${this.imageId}.json`, {
|
||||
encoding: "utf8",
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.catch(() => "{}")
|
||||
.catch(() => '{}')
|
||||
.then(JSON.parse)
|
||||
let extra: string[] = []
|
||||
let user = imageMeta.user || "root"
|
||||
let user = imageMeta.user || 'root'
|
||||
if (options?.user) {
|
||||
user = options.user
|
||||
delete options.user
|
||||
}
|
||||
let workdir = imageMeta.workdir || "/"
|
||||
let workdir = imageMeta.workdir || '/'
|
||||
if (options?.cwd) {
|
||||
workdir = options.cwd
|
||||
delete options.cwd
|
||||
@@ -456,10 +456,10 @@ export class SubContainerOwned<
|
||||
}
|
||||
}
|
||||
const child = cp.spawn(
|
||||
"start-container",
|
||||
'start-container',
|
||||
[
|
||||
"subcontainer",
|
||||
"exec",
|
||||
'subcontainer',
|
||||
'exec',
|
||||
`--env-file=/media/startos/images/${this.imageId}.env`,
|
||||
`--user=${user}`,
|
||||
`--workdir=${workdir}`,
|
||||
@@ -469,11 +469,11 @@ export class SubContainerOwned<
|
||||
],
|
||||
options || {},
|
||||
)
|
||||
abort?.signal.addEventListener("abort", () => child.kill("SIGKILL"))
|
||||
abort?.signal.addEventListener('abort', () => child.kill('SIGKILL'))
|
||||
if (options?.input) {
|
||||
await new Promise<null>((resolve, reject) => {
|
||||
try {
|
||||
child.stdin.on("error", (e) => reject(e))
|
||||
child.stdin.on('error', (e) => reject(e))
|
||||
child.stdin.write(options.input, (e) => {
|
||||
if (e) {
|
||||
reject(e)
|
||||
@@ -493,25 +493,25 @@ export class SubContainerOwned<
|
||||
}
|
||||
})
|
||||
}
|
||||
const stdout = { data: "" as string }
|
||||
const stderr = { data: "" as string }
|
||||
const stdout = { data: '' as string }
|
||||
const stderr = { data: '' as string }
|
||||
const appendData =
|
||||
(appendTo: { data: string }) => (chunk: string | Buffer | any) => {
|
||||
if (typeof chunk === "string" || chunk instanceof Buffer) {
|
||||
if (typeof chunk === 'string' || chunk instanceof Buffer) {
|
||||
appendTo.data += chunk.toString()
|
||||
} else {
|
||||
console.error("received unexpected chunk", chunk)
|
||||
console.error('received unexpected chunk', chunk)
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
child.on("error", reject)
|
||||
child.on('error', reject)
|
||||
let killTimeout: NodeJS.Timeout | undefined
|
||||
if (timeoutMs !== null && child.pid) {
|
||||
killTimeout = setTimeout(() => child.kill("SIGKILL"), timeoutMs)
|
||||
killTimeout = setTimeout(() => child.kill('SIGKILL'), timeoutMs)
|
||||
}
|
||||
child.stdout.on("data", appendData(stdout))
|
||||
child.stderr.on("data", appendData(stderr))
|
||||
child.on("exit", (code, signal) => {
|
||||
child.stdout.on('data', appendData(stdout))
|
||||
child.stderr.on('data', appendData(stderr))
|
||||
child.on('exit', (code, signal) => {
|
||||
clearTimeout(killTimeout)
|
||||
const result = {
|
||||
exitCode: code,
|
||||
@@ -560,17 +560,17 @@ export class SubContainerOwned<
|
||||
await this.waitProc()
|
||||
const imageMeta: T.ImageMetadata = await fs
|
||||
.readFile(`/media/startos/images/${this.imageId}.json`, {
|
||||
encoding: "utf8",
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.catch(() => "{}")
|
||||
.catch(() => '{}')
|
||||
.then(JSON.parse)
|
||||
let extra: string[] = []
|
||||
let user = imageMeta.user || "root"
|
||||
let user = imageMeta.user || 'root'
|
||||
if (options?.user) {
|
||||
user = options.user
|
||||
delete options.user
|
||||
}
|
||||
let workdir = imageMeta.workdir || "/"
|
||||
let workdir = imageMeta.workdir || '/'
|
||||
if (options?.cwd) {
|
||||
workdir = options.cwd
|
||||
delete options.cwd
|
||||
@@ -585,10 +585,10 @@ export class SubContainerOwned<
|
||||
await this.killLeader()
|
||||
this.leaderExited = false
|
||||
this.leader = cp.spawn(
|
||||
"start-container",
|
||||
'start-container',
|
||||
[
|
||||
"subcontainer",
|
||||
"launch",
|
||||
'subcontainer',
|
||||
'launch',
|
||||
`--env-file=/media/startos/images/${this.imageId}.env`,
|
||||
`--user=${user}`,
|
||||
`--workdir=${workdir}`,
|
||||
@@ -596,9 +596,9 @@ export class SubContainerOwned<
|
||||
this.rootfs,
|
||||
...command,
|
||||
],
|
||||
{ ...options, stdio: "inherit" },
|
||||
{ ...options, stdio: 'inherit' },
|
||||
)
|
||||
this.leader.on("exit", () => {
|
||||
this.leader.on('exit', () => {
|
||||
this.leaderExited = true
|
||||
})
|
||||
return this.leader as cp.ChildProcessWithoutNullStreams
|
||||
@@ -606,22 +606,22 @@ export class SubContainerOwned<
|
||||
|
||||
async spawn(
|
||||
command: string[],
|
||||
options: CommandOptions & StdioOptions = { stdio: "inherit" },
|
||||
options: CommandOptions & StdioOptions = { stdio: 'inherit' },
|
||||
): Promise<cp.ChildProcess> {
|
||||
await this.waitProc()
|
||||
const imageMeta: T.ImageMetadata = await fs
|
||||
.readFile(`/media/startos/images/${this.imageId}.json`, {
|
||||
encoding: "utf8",
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.catch(() => "{}")
|
||||
.catch(() => '{}')
|
||||
.then(JSON.parse)
|
||||
let extra: string[] = []
|
||||
let user = imageMeta.user || "root"
|
||||
let user = imageMeta.user || 'root'
|
||||
if (options?.user) {
|
||||
user = options.user
|
||||
delete options.user
|
||||
}
|
||||
let workdir = imageMeta.workdir || "/"
|
||||
let workdir = imageMeta.workdir || '/'
|
||||
if (options.cwd) {
|
||||
workdir = options.cwd
|
||||
delete options.cwd
|
||||
@@ -634,10 +634,10 @@ export class SubContainerOwned<
|
||||
}
|
||||
}
|
||||
return cp.spawn(
|
||||
"start-container",
|
||||
'start-container',
|
||||
[
|
||||
"subcontainer",
|
||||
"exec",
|
||||
'subcontainer',
|
||||
'exec',
|
||||
`--env-file=/media/startos/images/${this.imageId}.env`,
|
||||
`--user=${user}`,
|
||||
`--workdir=${workdir}`,
|
||||
@@ -665,7 +665,7 @@ export class SubContainerOwned<
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void> {
|
||||
const fullPath = this.subpath(path)
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, "")
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, '')
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
return fs.writeFile(fullPath, data, options)
|
||||
}
|
||||
@@ -709,7 +709,7 @@ export class SubContainerRc<
|
||||
static async of<Manifest extends T.SDKManifest, Effects extends T.Effects>(
|
||||
effects: Effects,
|
||||
image: {
|
||||
imageId: keyof Manifest["images"] & T.ImageId
|
||||
imageId: keyof Manifest['images'] & T.ImageId
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts:
|
||||
@@ -737,7 +737,7 @@ export class SubContainerRc<
|
||||
>(
|
||||
effects: Effects,
|
||||
image: {
|
||||
imageId: keyof Manifest["images"] & T.ImageId
|
||||
imageId: keyof Manifest['images'] & T.ImageId
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts:
|
||||
@@ -783,7 +783,7 @@ export class SubContainerRc<
|
||||
const rcs = --this.subcontainer.rcs
|
||||
if (rcs <= 0) {
|
||||
this.destroying = this.subcontainer.destroy()
|
||||
if (rcs < 0) console.error(new Error("UNREACHABLE: rcs < 0").stack)
|
||||
if (rcs < 0) console.error(new Error('UNREACHABLE: rcs < 0').stack)
|
||||
}
|
||||
}
|
||||
if (this.destroying) {
|
||||
@@ -850,7 +850,7 @@ export class SubContainerRc<
|
||||
|
||||
async spawn(
|
||||
command: string[],
|
||||
options: CommandOptions & StdioOptions = { stdio: "inherit" },
|
||||
options: CommandOptions & StdioOptions = { stdio: 'inherit' },
|
||||
): Promise<cp.ChildProcess> {
|
||||
return this.subcontainer.spawn(command, options)
|
||||
}
|
||||
@@ -910,23 +910,23 @@ export type MountOptions =
|
||||
| MountOptionsBackup
|
||||
|
||||
export type MountOptionsVolume = {
|
||||
type: "volume"
|
||||
type: 'volume'
|
||||
volumeId: string
|
||||
subpath: string | null
|
||||
readonly: boolean
|
||||
filetype: "file" | "directory" | "infer"
|
||||
filetype: 'file' | 'directory' | 'infer'
|
||||
idmap: IdMap[]
|
||||
}
|
||||
|
||||
export type MountOptionsAssets = {
|
||||
type: "assets"
|
||||
type: 'assets'
|
||||
subpath: string | null
|
||||
filetype: "file" | "directory" | "infer"
|
||||
filetype: 'file' | 'directory' | 'infer'
|
||||
idmap: { fromId: number; toId: number; range: number }[]
|
||||
}
|
||||
|
||||
export type MountOptionsPointer = {
|
||||
type: "pointer"
|
||||
type: 'pointer'
|
||||
packageId: string
|
||||
volumeId: string
|
||||
subpath: string | null
|
||||
@@ -935,9 +935,9 @@ export type MountOptionsPointer = {
|
||||
}
|
||||
|
||||
export type MountOptionsBackup = {
|
||||
type: "backup"
|
||||
type: 'backup'
|
||||
subpath: string | null
|
||||
filetype: "file" | "directory" | "infer"
|
||||
filetype: 'file' | 'directory' | 'infer'
|
||||
idmap: { fromId: number; toId: number; range: number }[]
|
||||
}
|
||||
function wait(time: number) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import * as fs from 'node:fs/promises'
|
||||
import * as T from '../../../base/lib/types'
|
||||
|
||||
/**
|
||||
* Common interface for objects that have a subpath method (Volume, SubContainer, etc.)
|
||||
@@ -27,7 +27,7 @@ export class Volume<Id extends string = string> implements PathBase {
|
||||
* @param subpath Path relative to the volume root
|
||||
*/
|
||||
subpath(subpath: string): string {
|
||||
return subpath.startsWith("/")
|
||||
return subpath.startsWith('/')
|
||||
? `${this.path}${subpath}`
|
||||
: `${this.path}/${subpath}`
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export class Volume<Id extends string = string> implements PathBase {
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void> {
|
||||
const fullPath = this.subpath(subpath)
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, "")
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, '')
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
return fs.writeFile(fullPath, data, options)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export class Volume<Id extends string = string> implements PathBase {
|
||||
* Type-safe volumes object that provides Volume instances for each volume defined in the manifest
|
||||
*/
|
||||
export type Volumes<Manifest extends T.SDKManifest> = {
|
||||
[K in Manifest["volumes"][number]]: Volume<K>
|
||||
[K in Manifest['volumes'][number]]: Volume<K>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as matches from "ts-matches"
|
||||
import * as YAML from "yaml"
|
||||
import * as TOML from "@iarna/toml"
|
||||
import * as INI from "ini"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import * as fs from "node:fs/promises"
|
||||
import { asError, deepEqual } from "../../../base/lib/util"
|
||||
import { DropGenerator, DropPromise } from "../../../base/lib/util/Drop"
|
||||
import { PathBase } from "./Volume"
|
||||
import * as matches from 'ts-matches'
|
||||
import * as YAML from 'yaml'
|
||||
import * as TOML from '@iarna/toml'
|
||||
import * as INI from 'ini'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import { asError, deepEqual } from '../../../base/lib/util'
|
||||
import { DropGenerator, DropPromise } from '../../../base/lib/util/Drop'
|
||||
import { PathBase } from './Volume'
|
||||
|
||||
const previousPath = /(.+?)\/([^/]*)$/
|
||||
|
||||
@@ -17,14 +17,14 @@ const exists = (path: string) =>
|
||||
)
|
||||
|
||||
async function onCreated(path: string) {
|
||||
if (path === "/") return
|
||||
if (!path.startsWith("/")) path = `${process.cwd()}/${path}`
|
||||
if (path === '/') return
|
||||
if (!path.startsWith('/')) path = `${process.cwd()}/${path}`
|
||||
if (await exists(path)) {
|
||||
return
|
||||
}
|
||||
const split = path.split("/")
|
||||
const split = path.split('/')
|
||||
const filename = split.pop()
|
||||
const parent = split.join("/")
|
||||
const parent = split.join('/')
|
||||
await onCreated(parent)
|
||||
const ctrl = new AbortController()
|
||||
const watch = fs.watch(parent, { persistent: false, signal: ctrl.signal })
|
||||
@@ -43,7 +43,7 @@ async function onCreated(path: string) {
|
||||
}
|
||||
for await (let event of watch) {
|
||||
if (event.filename === filename) {
|
||||
ctrl.abort("finished")
|
||||
ctrl.abort('finished')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,8 @@ function fileMerge(...args: any[]): any {
|
||||
else if (
|
||||
res &&
|
||||
arg &&
|
||||
typeof res === "object" &&
|
||||
typeof arg === "object" &&
|
||||
typeof res === 'object' &&
|
||||
typeof arg === 'object' &&
|
||||
!Array.isArray(res) &&
|
||||
!Array.isArray(arg)
|
||||
) {
|
||||
@@ -70,7 +70,7 @@ function fileMerge(...args: any[]): any {
|
||||
}
|
||||
|
||||
function filterUndefined<A>(a: A): A {
|
||||
if (a && typeof a === "object") {
|
||||
if (a && typeof a === 'object') {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map(filterUndefined) as A
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export type Transformers<Raw = unknown, Transformed = unknown> = {
|
||||
|
||||
type ToPath = string | { base: PathBase; subpath: string }
|
||||
function toPath(path: ToPath): string {
|
||||
if (typeof path === "string") {
|
||||
if (typeof path === 'string') {
|
||||
return path
|
||||
}
|
||||
return path.base.subpath(path.subpath)
|
||||
@@ -195,7 +195,7 @@ export class FileHelper<A> {
|
||||
if (!(await exists(this.path))) {
|
||||
return null
|
||||
}
|
||||
return await fs.readFile(this.path).then((data) => data.toString("utf-8"))
|
||||
return await fs.readFile(this.path).then((data) => data.toString('utf-8'))
|
||||
}
|
||||
|
||||
private async readFile(): Promise<unknown> {
|
||||
@@ -251,7 +251,7 @@ export class FileHelper<A> {
|
||||
while (effects.isInContext && !abort?.aborted) {
|
||||
if (await exists(this.path)) {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
abort?.addEventListener('abort', () => ctrl.abort())
|
||||
const watch = fs.watch(this.path, {
|
||||
persistent: false,
|
||||
signal: ctrl.signal,
|
||||
@@ -266,7 +266,7 @@ export class FileHelper<A> {
|
||||
})
|
||||
.catch((e) => console.error(asError(e)))
|
||||
if (!prev || !eq(prev.value, newRes)) {
|
||||
console.error("yielding", JSON.stringify({ prev: prev, newRes }))
|
||||
console.error('yielding', JSON.stringify({ prev: prev, newRes }))
|
||||
yield newRes
|
||||
}
|
||||
prev = { value: newRes }
|
||||
@@ -276,7 +276,7 @@ export class FileHelper<A> {
|
||||
await onCreated(this.path).catch((e) => console.error(asError(e)))
|
||||
}
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
return new Promise<never>((_, rej) => rej(new Error('aborted')))
|
||||
}
|
||||
|
||||
private readOnChange<B>(
|
||||
@@ -296,7 +296,7 @@ export class FileHelper<A> {
|
||||
if (res.cancel) ctrl.abort()
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"callback function threw an error @ FileHelper.read.onChange",
|
||||
'callback function threw an error @ FileHelper.read.onChange',
|
||||
e,
|
||||
)
|
||||
}
|
||||
@@ -305,7 +305,7 @@ export class FileHelper<A> {
|
||||
.catch((e) => callback(null, e))
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"callback function threw an error @ FileHelper.read.onChange",
|
||||
'callback function threw an error @ FileHelper.read.onChange',
|
||||
e,
|
||||
),
|
||||
)
|
||||
@@ -359,7 +359,7 @@ export class FileHelper<A> {
|
||||
const: (effects: T.Effects) => this.readConst(effects, map, eq),
|
||||
watch: (effects: T.Effects, abort?: AbortSignal) => {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
abort?.addEventListener('abort', () => ctrl.abort())
|
||||
return DropGenerator.of(
|
||||
this.readWatch(effects, map, eq, ctrl.signal),
|
||||
() => ctrl.abort(),
|
||||
@@ -620,15 +620,15 @@ export class FileHelper<A> {
|
||||
(inData) =>
|
||||
Object.entries(inData)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join("\n"),
|
||||
.join('\n'),
|
||||
(inString) =>
|
||||
Object.fromEntries(
|
||||
inString
|
||||
.split("\n")
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => !line.startsWith("#") && line.includes("="))
|
||||
.filter((line) => !line.startsWith('#') && line.includes('='))
|
||||
.map((line) => {
|
||||
const pos = line.indexOf("=")
|
||||
const pos = line.indexOf('=')
|
||||
return [line.slice(0, pos), line.slice(pos + 1)]
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from "../../../base/lib/util"
|
||||
export { GetSslCertificate } from "./GetSslCertificate"
|
||||
export { GetServiceManifest, getServiceManifest } from "./GetServiceManifest"
|
||||
export * from '../../../base/lib/util'
|
||||
export { GetSslCertificate } from './GetSslCertificate'
|
||||
export { GetServiceManifest, getServiceManifest } from './GetServiceManifest'
|
||||
|
||||
export { Drop } from "../../../base/lib/util/Drop"
|
||||
export { Volume, Volumes } from "./Volume"
|
||||
export { Drop } from '../../../base/lib/util/Drop'
|
||||
export { Volume, Volumes } from './Volume'
|
||||
|
||||
Reference in New Issue
Block a user