All checks were successful
The legacy Go agent was never deployed, so the entire backend command surface
published to a dead cmd.server/cmd.wipe/files.cmd void. Route it all to the
Rust agent's instance-scoped subjects.
Agent (corrosion-host-agent, alpha.10):
- New src/wipe.rs + 'wipe' func on {instance}.cmd: stop -> delete game files by
type (map/blueprint/full, with optional backup) -> restart. Jailed to the
instance root, symlink-safe (lstat, no cross-boundary follow — Lesson 26).
8 tests incl. jail-escape + symlink-skip proofs. Agent suite 64 tests green.
Backend (NestJS):
- InstancesService is now @Global with license-scoped convenience wrappers
(lifecycleForLicense/rconForLicense/writeFileForLicense/readFileForLicense/
deleteFileForLicense/wipeForLicense) + resolveDefaultInstance (license ->
primary instance).
- Routed to the agent: servers start/stop/restart/command; players kick/banid/
unban via RCON; schedules restart/announce/command/plugin-reload; wipes ->
wipeForLicense (real wipe now); plugins reload/unload/upload via rcon+file
ops; all 9 plugin-config module applies -> writeFileForLicense + oxide.reload
rcon, imports -> readFileForLicense (server:// prefix stripped).
- Honestly gated (need agent funcs not yet built): server deploy-from-panel,
Oxide install, one-click uMod install -> 503 coming-soon instead of dead
publishes.
Backend tsc green; agent cargo test green (64).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
166 lines
5.8 KiB
TypeScript
166 lines
5.8 KiB
TypeScript
import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { GatherConfig } from '../../entities/gather-config.entity';
|
|
import { InstancesService } from '../instances/instances.service';
|
|
import { CreateGatherConfigDto } from './dto/create-gather-config.dto';
|
|
import { UpdateGatherConfigDto } from './dto/update-gather-config.dto';
|
|
|
|
@Injectable()
|
|
export class GatherService {
|
|
private readonly logger = new Logger(GatherService.name);
|
|
|
|
constructor(
|
|
@InjectRepository(GatherConfig)
|
|
private readonly gatherRepo: Repository<GatherConfig>,
|
|
private readonly instancesService: InstancesService,
|
|
) {}
|
|
|
|
/** List configs for a license (summaries — no JSONB) */
|
|
async getConfigs(licenseId: string) {
|
|
const configs = await this.gatherRepo.find({
|
|
where: { license_id: licenseId },
|
|
select: ['id', 'config_name', 'description', 'is_active', 'created_at', 'updated_at'],
|
|
order: { created_at: 'DESC' },
|
|
});
|
|
return { configs };
|
|
}
|
|
|
|
/** Get full config with JSONB data */
|
|
async getConfig(licenseId: string, configId: string) {
|
|
const config = await this.gatherRepo.findOne({
|
|
where: { id: configId, license_id: licenseId },
|
|
});
|
|
if (!config) throw new NotFoundException('Gather config not found');
|
|
return { config };
|
|
}
|
|
|
|
/** Create a new config */
|
|
async createConfig(licenseId: string, dto: CreateGatherConfigDto) {
|
|
const config = this.gatherRepo.create({
|
|
license_id: licenseId,
|
|
config_name: dto.config_name,
|
|
description: dto.description || null,
|
|
config_data: dto.config_data || {},
|
|
});
|
|
const saved = await this.gatherRepo.save(config);
|
|
return { config: saved };
|
|
}
|
|
|
|
/** Update an existing config */
|
|
async updateConfig(licenseId: string, configId: string, dto: UpdateGatherConfigDto) {
|
|
const config = await this.gatherRepo.findOne({
|
|
where: { id: configId, license_id: licenseId },
|
|
});
|
|
if (!config) throw new NotFoundException('Gather config not found');
|
|
|
|
if (dto.config_name !== undefined) config.config_name = dto.config_name;
|
|
if (dto.description !== undefined) config.description = dto.description;
|
|
if (dto.config_data !== undefined) config.config_data = dto.config_data;
|
|
if (dto.is_active !== undefined) config.is_active = dto.is_active;
|
|
config.updated_at = new Date();
|
|
|
|
const saved = await this.gatherRepo.save(config);
|
|
return { config: saved };
|
|
}
|
|
|
|
/** Delete a config */
|
|
async deleteConfig(licenseId: string, configId: string) {
|
|
const result = await this.gatherRepo.delete({ id: configId, license_id: licenseId });
|
|
if (result.affected === 0) throw new NotFoundException('Gather config not found');
|
|
return { deleted: true };
|
|
}
|
|
|
|
/** Deploy config to game server via NATS */
|
|
async applyToServer(licenseId: string, configId: string) {
|
|
const config = await this.gatherRepo.findOne({
|
|
where: { id: configId, license_id: licenseId },
|
|
});
|
|
if (!config) throw new NotFoundException('Gather config not found');
|
|
|
|
const jsonString = JSON.stringify(config.config_data, null, 2);
|
|
|
|
try {
|
|
// Write GatherManager.json via Rust agent
|
|
await this.instancesService.writeFileForLicense(
|
|
licenseId,
|
|
'oxide/config/GatherManager.json',
|
|
jsonString,
|
|
);
|
|
|
|
// Reload GatherManager plugin via RCON
|
|
await this.instancesService.rconForLicense(licenseId, 'oxide.reload GatherManager');
|
|
|
|
// Mark this config as active, deactivate others
|
|
await this.gatherRepo.update({ license_id: licenseId }, { is_active: false });
|
|
await this.gatherRepo.update(
|
|
{ id: configId, license_id: licenseId },
|
|
{ is_active: true, updated_at: new Date() },
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
message: `Config "${config.config_name}" deployed to server`,
|
|
config_name: config.config_name,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(`Failed to deploy gather config: ${(error as Error).message}`);
|
|
throw new HttpException(
|
|
'Failed to deploy gather config — agent may be offline',
|
|
HttpStatus.SERVICE_UNAVAILABLE,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Import GatherManager.json from game server via NATS */
|
|
async importFromServer(licenseId: string, configName: string, description?: string) {
|
|
try {
|
|
// Read GatherManager.json from server via Rust agent
|
|
const result = await this.instancesService.readFileForLicense(
|
|
licenseId,
|
|
'oxide/config/GatherManager.json',
|
|
);
|
|
|
|
if (!result) {
|
|
throw new HttpException(
|
|
'No response from agent — it may be offline',
|
|
HttpStatus.SERVICE_UNAVAILABLE,
|
|
);
|
|
}
|
|
|
|
// Parse the response content as JSON
|
|
const responseData = (result as any).content;
|
|
let configData: Record<string, any>;
|
|
|
|
if (typeof responseData === 'string') {
|
|
configData = JSON.parse(responseData);
|
|
} else if (typeof responseData === 'object') {
|
|
configData = responseData;
|
|
} else {
|
|
throw new HttpException(
|
|
'Unexpected response format from agent',
|
|
HttpStatus.BAD_GATEWAY,
|
|
);
|
|
}
|
|
|
|
// Create new gather config row
|
|
const config = this.gatherRepo.create({
|
|
license_id: licenseId,
|
|
config_name: configName,
|
|
description: description || 'Imported from server',
|
|
config_data: configData,
|
|
});
|
|
const saved = await this.gatherRepo.save(config);
|
|
|
|
return { config: saved };
|
|
} catch (error) {
|
|
if (error instanceof HttpException) throw error;
|
|
this.logger.error(`Failed to import gather config from server: ${(error as Error).message}`);
|
|
throw new HttpException(
|
|
'Failed to import gather config — agent may be offline',
|
|
HttpStatus.SERVICE_UNAVAILABLE,
|
|
);
|
|
}
|
|
}
|
|
}
|