041a19f060
* feat: add service-managed self-update * chore: add temporary self-update test scripts * chore: simplify temporary script markdown fences * chore: run frontend in temporary self-update scripts * fix: show self-update job progress after apply * chore: add real npm self-update test script * fix: isolate npm self-update prefix * fix: detect npm service installs from bin shim * chore: reset service before real npm update test * chore: expand real npm status diagnostics * fix: preserve service home during self-update * fix(cli): tee self-update helper output to a log file * fix(cli): avoid launchd bootstrap race when reloading the macOS service * fix(backend): run macOS self-update helper once via transient launchd plist * fix(web): show self-update progress and lock the apply button until the update lands * fix(cli): reinstall via the kandev wrapper on Homebrew self-update * chore: remove temp self-update test scripts * fix(web): drop unused apply path from useUpdates hook * fix(backend): snapshot install state once in apply preflight * docs: align self-update docs with transient LaunchAgent flow * chore(release): run cli bundle from apps workspace root * fix(backend): guard against concurrent self-update applies * fix(backend): check scheme and helper paths in self-update apply * fix(backend): drop duplicate binary name from npx manual command * fix(cli): allow --help for service self-update without --intent * fix(cli): harden self-update dir perms and darwin uid resolution * fix(web): set required POST fields last in applyUpdate * docs: use JSON body notation for the apply endpoint * fix(backend): expire self-update guard so a stuck helper cannot wedge apply * refactor(cli): collapse redundant writeSync branch in self-update log
172 lines
4.9 KiB
TypeScript
172 lines
4.9 KiB
TypeScript
import { ParseError } from "../args";
|
|
|
|
export type ServiceAction =
|
|
| "install"
|
|
| "uninstall"
|
|
| "start"
|
|
| "stop"
|
|
| "restart"
|
|
| "status"
|
|
| "logs"
|
|
| "config"
|
|
| "self-update";
|
|
|
|
export type ServiceArgs = {
|
|
action: ServiceAction;
|
|
/** Install as a system unit/daemon instead of a per-user one. */
|
|
system?: boolean;
|
|
/** Backend port to bake into the unit (passed via KANDEV_SERVER_PORT). */
|
|
port?: number;
|
|
/** Override KANDEV_HOME_DIR baked into the unit. */
|
|
homeDir?: string;
|
|
/** Skip the Linux enable-linger prompt during install. */
|
|
noBootStart?: boolean;
|
|
/** Tail logs (only valid with `logs`). */
|
|
follow?: boolean;
|
|
/** Print help and exit. */
|
|
showHelp?: boolean;
|
|
/** Intent JSON path consumed by the hidden self-update helper. */
|
|
intent?: string;
|
|
/** Print the self-update command plan without mutating the install. */
|
|
dryRun?: boolean;
|
|
};
|
|
|
|
const VALID_ACTIONS = new Set<ServiceAction>([
|
|
"install",
|
|
"uninstall",
|
|
"start",
|
|
"stop",
|
|
"restart",
|
|
"status",
|
|
"logs",
|
|
"config",
|
|
"self-update",
|
|
]);
|
|
|
|
export function parseServiceArgs(argv: string[]): ServiceArgs {
|
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
return { action: "install", showHelp: true };
|
|
}
|
|
const head = argv[0];
|
|
if (!VALID_ACTIONS.has(head as ServiceAction)) {
|
|
throw new ParseError(
|
|
`unknown service action "${head}". expected one of: ${[...VALID_ACTIONS].join(", ")}`,
|
|
);
|
|
}
|
|
const out: ServiceArgs = { action: head as ServiceAction };
|
|
for (let i = 1; i < argv.length; i += 1) {
|
|
const arg = argv[i];
|
|
if (arg === "--help" || arg === "-h") {
|
|
out.showHelp = true;
|
|
continue;
|
|
}
|
|
if (arg === "--system") {
|
|
out.system = true;
|
|
continue;
|
|
}
|
|
if (arg === "--no-boot-start") {
|
|
out.noBootStart = true;
|
|
continue;
|
|
}
|
|
if (arg === "-f" || arg === "--follow") {
|
|
out.follow = true;
|
|
continue;
|
|
}
|
|
if (arg === "--dry-run") {
|
|
out.dryRun = true;
|
|
continue;
|
|
}
|
|
if (arg === "--intent") {
|
|
out.intent = takeValue(argv, i, "--intent");
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (arg.startsWith("--intent=")) {
|
|
const value = arg.slice("--intent=".length);
|
|
if (value.length === 0) throw new ParseError("--intent requires a value");
|
|
out.intent = value;
|
|
continue;
|
|
}
|
|
if (arg === "--port") {
|
|
out.port = parsePort(takeValue(argv, i, "--port"), "--port");
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (arg.startsWith("--port=")) {
|
|
out.port = parsePort(arg.slice("--port=".length), "--port");
|
|
continue;
|
|
}
|
|
if (arg === "--home-dir") {
|
|
out.homeDir = takeValue(argv, i, "--home-dir");
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (arg.startsWith("--home-dir=")) {
|
|
const value = arg.slice("--home-dir=".length);
|
|
if (value.length === 0) throw new ParseError("--home-dir requires a value");
|
|
out.homeDir = value;
|
|
continue;
|
|
}
|
|
throw new ParseError(`unknown flag "${arg}" for kandev service ${head}`);
|
|
}
|
|
validateActionFlags(out);
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Reject flag combinations that silently no-op so the user gets immediate
|
|
* feedback instead of a successful command that ignored their input. The
|
|
* matrix is small enough that explicit checks beat a generic flag-applicability
|
|
* table.
|
|
*/
|
|
function validateActionFlags(args: ServiceArgs): void {
|
|
if (args.follow && args.action !== "logs") {
|
|
throw new ParseError(`--follow only applies to 'kandev service logs', not '${args.action}'`);
|
|
}
|
|
if (args.dryRun && args.action !== "self-update") {
|
|
throw new ParseError(
|
|
`--dry-run only applies to 'kandev service self-update', not '${args.action}'`,
|
|
);
|
|
}
|
|
if (args.intent && args.action !== "self-update") {
|
|
throw new ParseError(
|
|
`--intent only applies to 'kandev service self-update', not '${args.action}'`,
|
|
);
|
|
}
|
|
if (args.action === "self-update" && !args.showHelp && !args.intent) {
|
|
throw new ParseError("kandev service self-update requires --intent <path>");
|
|
}
|
|
const installOnly: Array<keyof ServiceArgs> = ["port", "homeDir", "noBootStart"];
|
|
if (args.action !== "install") {
|
|
for (const flag of installOnly) {
|
|
if (args[flag] !== undefined) {
|
|
const display =
|
|
flag === "homeDir"
|
|
? "--home-dir"
|
|
: flag === "noBootStart"
|
|
? "--no-boot-start"
|
|
: `--${flag}`;
|
|
throw new ParseError(
|
|
`${display} only applies to 'kandev service install', not '${args.action}'`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function takeValue(argv: string[], i: number, flag: string): string {
|
|
const v = argv[i + 1];
|
|
if (v === undefined || v.startsWith("-")) {
|
|
throw new ParseError(`${flag} requires a value`);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
function parsePort(raw: string, flag: string): number {
|
|
const n = Number(raw);
|
|
if (raw === "" || !Number.isInteger(n) || n < 1 || n > 65535) {
|
|
throw new ParseError(`${flag} value must be an integer between 1 and 65535, got "${raw}"`);
|
|
}
|
|
return n;
|
|
}
|