@gamecrate/cli 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gamecrate.js +246 -246
- package/dist/lib.js +296 -0
- package/dist/types/cli/args.d.ts +40 -0
- package/dist/types/cli/help.d.ts +7 -0
- package/dist/types/cli/output.d.ts +56 -0
- package/dist/types/config/builtin.d.ts +3 -0
- package/dist/types/config/jsonc.d.ts +5 -0
- package/dist/types/config/load.d.ts +45 -0
- package/dist/types/config/validate.d.ts +12 -0
- package/dist/types/docker/identity.d.ts +6 -0
- package/dist/types/docker/preflight.d.ts +3 -0
- package/dist/types/docker/run.d.ts +37 -0
- package/dist/types/docker/spec.d.ts +24 -0
- package/dist/types/docker/window.d.ts +21 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/launch/generate.d.ts +8 -0
- package/dist/types/launch/instance.d.ts +20 -0
- package/dist/types/launch/prepare.d.ts +44 -0
- package/dist/types/launch/resolve.d.ts +17 -0
- package/dist/types/launch/stage.d.ts +13 -0
- package/dist/types/lib.d.ts +3 -0
- package/dist/types/mods/modindex.d.ts +29 -0
- package/dist/types/mods/staleness.d.ts +28 -0
- package/dist/types/mods/worktree.d.ts +18 -0
- package/dist/types/plugin.d.ts +35 -0
- package/dist/types/types.d.ts +394 -0
- package/package.json +15 -10
- package/src/cli/args.ts +0 -592
- package/src/cli/help.ts +0 -193
- package/src/cli/output.ts +0 -246
- package/src/config/builtin.ts +0 -19
- package/src/config/jsonc.ts +0 -21
- package/src/config/load.ts +0 -387
- package/src/config/validate.ts +0 -0
- package/src/docker/identity.ts +0 -25
- package/src/docker/preflight.ts +0 -243
- package/src/docker/run.ts +0 -212
- package/src/docker/spec.ts +0 -357
- package/src/docker/window.ts +0 -152
- package/src/index.ts +0 -875
- package/src/launch/generate.ts +0 -151
- package/src/launch/instance.ts +0 -106
- package/src/launch/prepare.ts +0 -332
- package/src/launch/resolve.ts +0 -383
- package/src/launch/stage.ts +0 -97
- package/src/lib.ts +0 -22
- package/src/mods/modindex.ts +0 -539
- package/src/mods/staleness.ts +0 -125
- package/src/mods/worktree.ts +0 -107
- package/src/plugin.ts +0 -152
- package/src/types.ts +0 -423
package/src/cli/help.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import type { GameConfig, RootConfig } from '../types'
|
|
2
|
-
import { GamecrateError, Exit, own } from '../types'
|
|
3
|
-
import type { Option } from 'commander'
|
|
4
|
-
import { FLAG_ENV, GLOBAL_FLAGS, SUBCOMMANDS, buildProgram, suggest } from './args'
|
|
5
|
-
import type { SubcommandSpec } from './args'
|
|
6
|
-
|
|
7
|
-
const NAME = 'gamecrate'
|
|
8
|
-
|
|
9
|
-
/** Help and completion describe whatever the parser accepts, never a second list. */
|
|
10
|
-
function flags(): readonly Option[] {
|
|
11
|
-
return buildProgram().options
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Three levels: no topic gives the top level, a subcommand name gives its usage,
|
|
16
|
-
* a game name gives that game's profiles and modes.
|
|
17
|
-
*/
|
|
18
|
-
export function renderHelp(topic?: string, config?: RootConfig): string {
|
|
19
|
-
if (!topic) return topLevel(config)
|
|
20
|
-
|
|
21
|
-
const sub = SUBCOMMANDS.find((s) => s.name === topic)
|
|
22
|
-
if (sub) return subcommandHelp(sub)
|
|
23
|
-
|
|
24
|
-
const game = own(config?.games, topic)
|
|
25
|
-
if (game) return gameHelp(topic, game)
|
|
26
|
-
|
|
27
|
-
const candidates = [...SUBCOMMANDS.map((s) => s.name), ...Object.keys(config?.games ?? {})]
|
|
28
|
-
const hint = suggest(topic, candidates)
|
|
29
|
-
throw new GamecrateError(
|
|
30
|
-
`no help topic ${topic}`,
|
|
31
|
-
Exit.Usage,
|
|
32
|
-
hint ? `did you mean ${hint}?` : `topics: ${candidates.join(', ')}`,
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function topLevel(config?: RootConfig): string {
|
|
37
|
-
const lines = [
|
|
38
|
-
`${NAME} — run modded games in containers`,
|
|
39
|
-
'',
|
|
40
|
-
'usage:',
|
|
41
|
-
` ${NAME} <game> [profile] [flags] [-- game args]`,
|
|
42
|
-
` ${NAME} <subcommand> [args] [flags]`,
|
|
43
|
-
'',
|
|
44
|
-
'subcommands:',
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
const verbs = SUBCOMMANDS.map((s) => [`${s.name} ${s.usage}`.trim(), s.summary] as const)
|
|
48
|
-
lines.push(...columns(verbs, 2))
|
|
49
|
-
lines.push('', 'flags:')
|
|
50
|
-
lines.push(...columns(flags().map(flagRow), 2))
|
|
51
|
-
|
|
52
|
-
const games = Object.entries(config?.games ?? {})
|
|
53
|
-
if (games.length > 0) {
|
|
54
|
-
lines.push('', 'games:')
|
|
55
|
-
lines.push(...columns(games.map(([name, game]) => [name, gameSummary(game)] as const), 2))
|
|
56
|
-
lines.push('', `${NAME} help <game> lists that game's profiles.`)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
lines.push('', 'Game args go after a bare --. Env vars are GAMECRATE_ prefixed fallbacks only.')
|
|
60
|
-
return lines.join('\n') + '\n'
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function subcommandHelp(sub: SubcommandSpec): string {
|
|
64
|
-
const lines = [`usage: ${NAME} ${sub.name} ${sub.usage}`.trimEnd() + ' [flags]', '', ` ${sub.summary}`]
|
|
65
|
-
|
|
66
|
-
const names = [...sub.flags, ...GLOBAL_FLAGS]
|
|
67
|
-
const all = flags()
|
|
68
|
-
const specs = names
|
|
69
|
-
.map((name) => all.find((f) => f.long === name))
|
|
70
|
-
.filter((f): f is Option => f !== undefined)
|
|
71
|
-
if (specs.length > 0) {
|
|
72
|
-
lines.push('', 'flags:')
|
|
73
|
-
lines.push(...columns(specs.map(flagRow), 2))
|
|
74
|
-
}
|
|
75
|
-
if (sub.name === 'run') {
|
|
76
|
-
lines.push('', ` The subcommand slot defaults to run, so \`${NAME} <game> <profile>\` works.`)
|
|
77
|
-
}
|
|
78
|
-
return lines.join('\n') + '\n'
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function gameHelp(name: string, game: GameConfig): string {
|
|
82
|
-
const lines = [`usage: ${NAME} ${name} [profile] [flags] [-- game args]`, '', 'profiles:']
|
|
83
|
-
|
|
84
|
-
const rows: (readonly [string, string])[] = []
|
|
85
|
-
for (const [profile, config] of Object.entries(game.profiles)) {
|
|
86
|
-
const notes: string[] = []
|
|
87
|
-
if (config.alias) notes.push(`alias for ${config.alias}`)
|
|
88
|
-
if (config.extends) notes.push(`extends ${config.extends}`)
|
|
89
|
-
if (config.autoDependencies) notes.push('auto dependencies')
|
|
90
|
-
const count = config.mods?.length ?? 0
|
|
91
|
-
if (!config.alias) notes.push(count === 1 ? '1 entry' : `${count} entries`)
|
|
92
|
-
if (config.aliases?.length) notes.push(`aka ${config.aliases.join(', ')}`)
|
|
93
|
-
rows.push([profile, notes.join(', ')])
|
|
94
|
-
}
|
|
95
|
-
if (rows.length === 0) rows.push(['(none declared)', ''])
|
|
96
|
-
lines.push(...columns(rows, 2))
|
|
97
|
-
|
|
98
|
-
if (game.aliases && Object.keys(game.aliases).length > 0) {
|
|
99
|
-
lines.push('', 'mod name aliases:')
|
|
100
|
-
lines.push(...columns(Object.entries(game.aliases).map(([k, v]) => [k, v] as const), 2))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
lines.push('', `modes: ${game.modes.join(', ')}`)
|
|
104
|
-
lines.push(`core: ${game.core}`)
|
|
105
|
-
if (game.dlc.length > 0) lines.push(`dlc: ${game.dlc.join(', ')}`)
|
|
106
|
-
lines.push(`game files: ${game.gameFiles.source === 'mount' ? game.gameFiles.host ?? '(unset)' : game.image.ref}`)
|
|
107
|
-
return lines.join('\n') + '\n'
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function gameSummary(game: GameConfig): string {
|
|
111
|
-
const count = Object.keys(game.profiles).length
|
|
112
|
-
return `${count === 1 ? '1 profile' : `${count} profiles`}; modes ${game.modes.join(', ')}`
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function flagRow(spec: Option): readonly [string, string] {
|
|
116
|
-
const notes: string[] = []
|
|
117
|
-
if (Array.isArray(spec.defaultValue)) notes.push('repeatable')
|
|
118
|
-
const env = FLAG_ENV[spec.long ?? '']
|
|
119
|
-
if (env) notes.push(`$${env}`)
|
|
120
|
-
const summary = spec.description
|
|
121
|
-
return [spec.flags, notes.length > 0 ? `${summary} (${notes.join(', ')})` : summary]
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** The `<id>` part of a flag string, for the shell completions. */
|
|
125
|
-
function placeholder(spec: Option): string {
|
|
126
|
-
return spec.flags.split(/[ ,]+/).find((token) => token.startsWith('<')) ?? ''
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function columns(rows: readonly (readonly [string, string])[], indent: number): string[] {
|
|
130
|
-
const width = Math.max(0, ...rows.map((r) => r[0].length))
|
|
131
|
-
const pad = ' '.repeat(indent)
|
|
132
|
-
return rows.map(([left, right]) => (right ? `${pad}${left.padEnd(width)} ${right}` : `${pad}${left}`))
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function renderCompletion(shell: 'bash' | 'zsh'): string {
|
|
136
|
-
const verbs = SUBCOMMANDS.map((s) => s.name).join(' ')
|
|
137
|
-
const options = flags()
|
|
138
|
-
const names = options.flatMap((f) => (f.short ? [f.long!, f.short] : [f.long!])).join(' ')
|
|
139
|
-
const valueFlags = options.filter((f) => f.required).map((f) => f.long!)
|
|
140
|
-
const fn = `_${NAME.replace(/-/g, '_')}`
|
|
141
|
-
|
|
142
|
-
if (shell === 'bash') {
|
|
143
|
-
const cases = options
|
|
144
|
-
.filter((f) => f.argChoices)
|
|
145
|
-
.map((f) => ` ${f.long}) COMPREPLY=($(compgen -W "${f.argChoices!.join(' ')}" -- "$cur")); return ;;`)
|
|
146
|
-
.join('\n')
|
|
147
|
-
return `${fn}() {
|
|
148
|
-
local cur prev
|
|
149
|
-
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
150
|
-
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
151
|
-
case "$prev" in
|
|
152
|
-
${cases}
|
|
153
|
-
${valueFlags.join('|')}) return ;;
|
|
154
|
-
esac
|
|
155
|
-
if [[ "$cur" == -* ]]; then
|
|
156
|
-
COMPREPLY=($(compgen -W "${names}" -- "$cur"))
|
|
157
|
-
return
|
|
158
|
-
fi
|
|
159
|
-
if [[ $COMP_CWORD -eq 1 ]]; then
|
|
160
|
-
local games
|
|
161
|
-
games=$(${NAME} list --json 2>/dev/null | grep -oE '"[A-Za-z0-9._-]+"' | tr -d '"')
|
|
162
|
-
COMPREPLY=($(compgen -W "${verbs} $games" -- "$cur"))
|
|
163
|
-
fi
|
|
164
|
-
}
|
|
165
|
-
complete -F ${fn} ${NAME}
|
|
166
|
-
`
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const zshVerbs = SUBCOMMANDS.map((s) => ` '${s.name}:${s.summary.replace(/'/g, "'\\''")}'`).join('\n')
|
|
170
|
-
const zshFlags = options.map((f) => {
|
|
171
|
-
const desc = f.description.replace(/'/g, "'\\''").replace(/[[\]:]/g, '')
|
|
172
|
-
const arg = placeholder(f)
|
|
173
|
-
const value = arg ? `:${arg.replace(/[<>]/g, '')}:${f.argChoices ? `(${f.argChoices.join(' ')})` : '_files'}` : ''
|
|
174
|
-
const repeat = Array.isArray(f.defaultValue) ? '*' : ''
|
|
175
|
-
return ` '${repeat}${f.long}[${desc}]${value}'`
|
|
176
|
-
}).join('\n')
|
|
177
|
-
|
|
178
|
-
return `#compdef ${NAME}
|
|
179
|
-
|
|
180
|
-
${fn}() {
|
|
181
|
-
local -a verbs
|
|
182
|
-
verbs=(
|
|
183
|
-
${zshVerbs}
|
|
184
|
-
)
|
|
185
|
-
_arguments -s \\
|
|
186
|
-
${zshFlags} \\
|
|
187
|
-
'1: :{_describe verb verbs}' \\
|
|
188
|
-
'*:: :->rest'
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
${fn} "$@"
|
|
192
|
-
`
|
|
193
|
-
}
|
package/src/cli/output.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, rmSync, symlinkSync, unlinkSync, writeSync } from 'node:fs'
|
|
2
|
-
import { basename, join, resolve } from 'node:path'
|
|
3
|
-
import type { Readable } from 'node:stream'
|
|
4
|
-
import { containerName } from '../docker/spec'
|
|
5
|
-
import { staleWarning } from '../mods/staleness'
|
|
6
|
-
import type { LaunchPlan, Problem, ResolvedMod } from '../types'
|
|
7
|
-
import { Exit } from '../types'
|
|
8
|
-
|
|
9
|
-
/** Tool status. Never stdout: stdout belongs to the game. */
|
|
10
|
-
export function status(message: string): void {
|
|
11
|
-
process.stderr.write(line(message))
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function warn(message: string): void {
|
|
15
|
-
process.stderr.write(line(`warning: ${message}`))
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface OutputRedirect {
|
|
19
|
-
close(): void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function redirectOutput(path: string): OutputRedirect {
|
|
23
|
-
const fd = openSync(path, 'w')
|
|
24
|
-
const stdout = process.stdout.write
|
|
25
|
-
const stderr = process.stderr.write
|
|
26
|
-
let open = true
|
|
27
|
-
const write = ((
|
|
28
|
-
chunk: string | Uint8Array,
|
|
29
|
-
encodingOrCallback?: BufferEncoding | (() => void),
|
|
30
|
-
callback?: () => void,
|
|
31
|
-
) => {
|
|
32
|
-
append(fd, chunk)
|
|
33
|
-
const done = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback
|
|
34
|
-
done?.()
|
|
35
|
-
return true
|
|
36
|
-
}) as typeof process.stdout.write
|
|
37
|
-
process.stdout.write = write
|
|
38
|
-
process.stderr.write = write
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
close() {
|
|
42
|
-
if (!open) return
|
|
43
|
-
open = false
|
|
44
|
-
process.stdout.write = stdout
|
|
45
|
-
process.stderr.write = stderr
|
|
46
|
-
closeSync(fd)
|
|
47
|
-
},
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function forwardOutput(stream: Readable, target: NodeJS.WriteStream): Promise<void> {
|
|
52
|
-
for await (const chunk of stream) target.write(chunk as Buffer)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function line(message: string): string {
|
|
56
|
-
return message.endsWith('\n') ? message : `${message}\n`
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Every collected failure at once, grouped by location. Resolution stops before any
|
|
61
|
-
* side effect, so reporting the first problem only would hide the rest.
|
|
62
|
-
*/
|
|
63
|
-
export function reportProblems(problems: Problem[]): never {
|
|
64
|
-
if (problems.length === 0) {
|
|
65
|
-
process.stderr.write(line('resolution failed with no reported detail'))
|
|
66
|
-
process.exit(Exit.Resolution)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const groups = new Map<string, Problem[]>()
|
|
70
|
-
for (const problem of problems) {
|
|
71
|
-
const group = groups.get(problem.where)
|
|
72
|
-
if (group) group.push(problem)
|
|
73
|
-
else groups.set(problem.where, [problem])
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const out = [`${problems.length} problem${problems.length === 1 ? '' : 's'}:`]
|
|
77
|
-
for (const [where, group] of groups) {
|
|
78
|
-
out.push(` ${where}`)
|
|
79
|
-
for (const problem of group) {
|
|
80
|
-
out.push(` ${problem.message}`)
|
|
81
|
-
if (problem.suggestion) out.push(` did you mean ${problem.suggestion}?`)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
process.stderr.write(`${out.join('\n')}\n`)
|
|
85
|
-
process.exit(Exit.Resolution)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface PlanModPayload {
|
|
89
|
-
packageId: string
|
|
90
|
-
kind: ResolvedMod['kind']
|
|
91
|
-
hostDir: string
|
|
92
|
-
containerDir: string
|
|
93
|
-
origin: 'explicit' | 'auto'
|
|
94
|
-
stale: boolean
|
|
95
|
-
staleReport?: ResolvedMod['staleReport']
|
|
96
|
-
workshopId?: number
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface PlanPayload {
|
|
100
|
-
game: string
|
|
101
|
-
profile: string
|
|
102
|
-
instance?: string
|
|
103
|
-
mode: string
|
|
104
|
-
marker?: string
|
|
105
|
-
timeoutSeconds: number
|
|
106
|
-
renderWaitSeconds: number
|
|
107
|
-
profileDir: string
|
|
108
|
-
instanceDir: string
|
|
109
|
-
containerName: string
|
|
110
|
-
dataDirHost: string
|
|
111
|
-
stageDirHost: string
|
|
112
|
-
logsDirHost: string
|
|
113
|
-
mods: PlanModPayload[]
|
|
114
|
-
warnings: string[]
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Regenerated from the mods rather than stored, because a successful `--build` clears a stale
|
|
119
|
-
* report and the warning has to disappear with it.
|
|
120
|
-
*/
|
|
121
|
-
export function planWarnings(plan: LaunchPlan): string[] {
|
|
122
|
-
if (!plan.warnOnStale) return plan.warnings
|
|
123
|
-
const stale = plan.mods
|
|
124
|
-
.filter((mod) => mod.staleReport !== undefined)
|
|
125
|
-
.map((mod) => staleWarning(mod.packageId, mod.staleReport!))
|
|
126
|
-
return [...plan.warnings, ...stale]
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** The `--print-plan --json` payload. Bind mounts leave no host-readable link to assert on. */
|
|
130
|
-
export function planPayload(plan: LaunchPlan): PlanPayload {
|
|
131
|
-
return {
|
|
132
|
-
game: plan.game,
|
|
133
|
-
profile: plan.profile,
|
|
134
|
-
...(plan.instance === undefined ? {} : { instance: plan.instance }),
|
|
135
|
-
mode: plan.mode,
|
|
136
|
-
...(plan.marker === undefined ? {} : { marker: plan.marker }),
|
|
137
|
-
timeoutSeconds: plan.timeoutSeconds,
|
|
138
|
-
renderWaitSeconds: plan.renderWaitSeconds,
|
|
139
|
-
profileDir: resolve(plan.profileDir),
|
|
140
|
-
instanceDir: resolve(plan.instanceDir),
|
|
141
|
-
containerName: containerName(plan),
|
|
142
|
-
dataDirHost: resolve(plan.dataDirHost),
|
|
143
|
-
stageDirHost: resolve(plan.stageDirHost),
|
|
144
|
-
logsDirHost: resolve(plan.logsDirHost),
|
|
145
|
-
mods: plan.mods.map((mod) => ({
|
|
146
|
-
packageId: mod.packageId,
|
|
147
|
-
kind: mod.kind,
|
|
148
|
-
hostDir: resolve(mod.hostDir),
|
|
149
|
-
containerDir: mod.containerDir,
|
|
150
|
-
origin: mod.explicit ? 'explicit' : 'auto',
|
|
151
|
-
stale: mod.stale === true,
|
|
152
|
-
...(mod.staleReport === undefined ? {} : { staleReport: mod.staleReport }),
|
|
153
|
-
...(mod.workshopId === undefined ? {} : { workshopId: mod.workshopId }),
|
|
154
|
-
})),
|
|
155
|
-
warnings: planWarnings(plan),
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function printPlan(plan: LaunchPlan, asJson: boolean): void {
|
|
160
|
-
const payload = planPayload(plan)
|
|
161
|
-
if (asJson) {
|
|
162
|
-
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`)
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const title = payload.instance === undefined
|
|
167
|
-
? `${payload.game} ${payload.profile} (${payload.mode})`
|
|
168
|
-
: `${payload.game} ${payload.profile} / ${payload.instance} (${payload.mode})`
|
|
169
|
-
const out = [
|
|
170
|
-
title,
|
|
171
|
-
` profile ${payload.profileDir}`,
|
|
172
|
-
` instance ${payload.instanceDir}`,
|
|
173
|
-
` container ${payload.containerName}`,
|
|
174
|
-
` data ${payload.dataDirHost}`,
|
|
175
|
-
` stage ${payload.stageDirHost}`,
|
|
176
|
-
` logs ${payload.logsDirHost}`,
|
|
177
|
-
]
|
|
178
|
-
if (payload.marker !== undefined) out.push(` marker ${payload.marker}`)
|
|
179
|
-
out.push(` timeout ${payload.timeoutSeconds}s, render wait ${payload.renderWaitSeconds}s`)
|
|
180
|
-
out.push(` mods ${payload.mods.length}`)
|
|
181
|
-
|
|
182
|
-
const width = Math.max(0, ...payload.mods.map((m) => m.packageId.length))
|
|
183
|
-
for (const mod of payload.mods) {
|
|
184
|
-
const notes: string[] = [mod.kind, mod.origin]
|
|
185
|
-
if (mod.stale) notes.push('stale')
|
|
186
|
-
out.push(` ${mod.packageId.padEnd(width)} ${notes.join(' ')} ${mod.hostDir} -> ${mod.containerDir}`)
|
|
187
|
-
}
|
|
188
|
-
for (const warning of payload.warnings) out.push(` warning: ${warning}`)
|
|
189
|
-
process.stdout.write(`${out.join('\n')}\n`)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Sortable lexicographically and safe on every filesystem: 20260730T142335123Z. */
|
|
193
|
-
export function runTimestamp(now: Date = new Date()): string {
|
|
194
|
-
return now.toISOString().replace(/[-:.]/g, '')
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Makes <logsDir>/runs/<ts>, repoints `current` at it, rotates the old ones, returns the dir. */
|
|
198
|
-
export function openRunLog(logsDir: string, now?: Date): string {
|
|
199
|
-
const runsDir = join(logsDir, 'runs')
|
|
200
|
-
mkdirSync(runsDir, { recursive: true })
|
|
201
|
-
|
|
202
|
-
const dir = uniqueRunDir(runsDir, runTimestamp(now))
|
|
203
|
-
mkdirSync(dir)
|
|
204
|
-
linkCurrent(logsDir, dir)
|
|
205
|
-
rotateRuns(logsDir, 10)
|
|
206
|
-
return dir
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const encoder = new TextEncoder()
|
|
210
|
-
|
|
211
|
-
function append(fd: number, chunk: string | Uint8Array): void {
|
|
212
|
-
writeSync(fd, typeof chunk === 'string' ? encoder.encode(chunk) : chunk)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function uniqueRunDir(runsDir: string, stamp: string): string {
|
|
216
|
-
let candidate = join(runsDir, stamp)
|
|
217
|
-
for (let n = 2; existsSync(candidate); n++) candidate = join(runsDir, `${stamp}-${n}`)
|
|
218
|
-
return candidate
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function linkCurrent(logsDir: string, target: string): void {
|
|
222
|
-
const link = join(logsDir, 'current')
|
|
223
|
-
try {
|
|
224
|
-
lstatSync(link)
|
|
225
|
-
unlinkSync(link)
|
|
226
|
-
} catch {
|
|
227
|
-
// No previous link; nothing to clear.
|
|
228
|
-
}
|
|
229
|
-
symlinkSync(join('runs', basename(target)), link, 'dir')
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Landmine 7: a 180MB Player-prev.log was 68% of a profile tree. Retention is the cap. */
|
|
233
|
-
export function rotateRuns(logsDir: string, keep: number): string[] {
|
|
234
|
-
const runsDir = join(logsDir, 'runs')
|
|
235
|
-
if (keep < 1 || !existsSync(runsDir)) return []
|
|
236
|
-
|
|
237
|
-
const dirs = readdirSync(runsDir, { withFileTypes: true })
|
|
238
|
-
.filter((entry) => entry.isDirectory())
|
|
239
|
-
.map((entry) => entry.name)
|
|
240
|
-
.sort()
|
|
241
|
-
.reverse()
|
|
242
|
-
|
|
243
|
-
const removed = dirs.slice(keep)
|
|
244
|
-
for (const name of removed) rmSync(join(runsDir, name), { recursive: true, force: true })
|
|
245
|
-
return removed
|
|
246
|
-
}
|
package/src/config/builtin.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { Settings } from '../types'
|
|
2
|
-
|
|
3
|
-
export const DEFAULT_DATA_ROOT = '~/.local/share/gamecrate'
|
|
4
|
-
|
|
5
|
-
export const DEFAULT_SETTINGS: Settings = {
|
|
6
|
-
width: 1920,
|
|
7
|
-
height: 1080,
|
|
8
|
-
devMode: true,
|
|
9
|
-
runInBackground: true,
|
|
10
|
-
resetModsConfigOnCrash: false,
|
|
11
|
-
gpu: true,
|
|
12
|
-
audio: true,
|
|
13
|
-
input: false,
|
|
14
|
-
network: 'bridge',
|
|
15
|
-
display: 'x11',
|
|
16
|
-
memory: '8g',
|
|
17
|
-
cpus: 6,
|
|
18
|
-
pidsLimit: 1024,
|
|
19
|
-
}
|
package/src/config/jsonc.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { parse, printParseErrorCode } from 'jsonc-parser'
|
|
2
|
-
import type { ParseError } from 'jsonc-parser'
|
|
3
|
-
import { GamecrateError, Exit } from '../types'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* JSON with line and block comments and trailing commas. jsonc-parser recovers from
|
|
7
|
-
* syntax errors and still returns a value, so the error list is the only success signal.
|
|
8
|
-
*/
|
|
9
|
-
export function parseJsonc(text: string): unknown {
|
|
10
|
-
const errors: ParseError[] = []
|
|
11
|
-
const value = parse(text, errors, { allowTrailingComma: true, allowEmptyContent: false })
|
|
12
|
-
const first = errors[0]
|
|
13
|
-
if (first !== undefined) {
|
|
14
|
-
throw new GamecrateError(
|
|
15
|
-
'config is not valid JSON',
|
|
16
|
-
Exit.Config,
|
|
17
|
-
`${printParseErrorCode(first.error)} at offset ${first.offset}`,
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
return value
|
|
21
|
-
}
|