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:
Aiden McClelland
2026-02-06 00:10:16 +01:00
committed by GitHub
parent 86ca23c093
commit f2142f0bb3
280 changed files with 6793 additions and 5515 deletions

View File

@@ -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(),
)

View File

@@ -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,
),
)

View File

@@ -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) {

View File

@@ -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>
}
/**

View File

@@ -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)]
}),
),

View File

@@ -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'