diff --git a/Cargo.lock b/Cargo.lock index 20c5d2f..8531f6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3423,6 +3423,7 @@ dependencies = [ "async-trait", "bitflags 2.6.0", "clap", + "clap_complete", "color-eyre", "dashmap", "data-encoding", diff --git a/Cargo.toml b/Cargo.toml index 2372000..91f09b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ once_cell = "1" # version must match on clap in `deno` clap = "=4.5" +clap_complete = "=4.5.24" + shadow-rs = "0.36.0" diff --git a/modules/envs/mod.ts b/modules/envs/mod.ts index fd24b0e..1568484 100644 --- a/modules/envs/mod.ts +++ b/modules/envs/mod.ts @@ -1,6 +1,6 @@ export * from "./types.ts"; -import { cliffy_cmd, zod } from "../../deps/cli.ts"; +import { zod } from "../../deps/cli.ts"; import { $, detectShellPath, Json, unwrapZodRes } from "../../utils/mod.ts"; import validators from "./types.ts"; import type { @@ -90,9 +90,11 @@ export class EnvsModule extends ModuleBase { : { envKey: envKeyMaybe ?? ecx.activeEnv }; } - const commonFlags = { + const commonFlags: CliCommand["flags"] = { taskEnv: { - value_name: "taskName", + short: "t", + long: "task-env", + value_name: "TASK NAME", help: "Activate the environment used by the named task", exclusive: true, }, @@ -100,14 +102,14 @@ export class EnvsModule extends ModuleBase { const commonArgs = { envKey: { - value_name: "envKey", + value_name: "ENV KEY", }, }; return [ { name: "envs", - aliases: ["e"], + visible_aliases: ["e"], about: "Envs module, reproducable posix shells environments.", sub_commands: [ { @@ -140,11 +142,14 @@ export class EnvsModule extends ModuleBase { ...commonArgs, }, action: async function ( - { flags: { taskEnv }, args: { envName: envKeyMaybe } }, + { + flags: { taskEnv: taskKeyMaybe }, + args: { envName: envKeyMaybe }, + }, ) { const { envKey } = envKeyArgs({ - taskKeyMaybe: taskEnv, - envKeyMaybe, + taskKeyMaybe: taskKeyMaybe as string, + envKeyMaybe: envKeyMaybe as string, }); await activateEnv(envKey); }, @@ -161,11 +166,14 @@ export class EnvsModule extends ModuleBase { ...commonArgs, }, action: async function ( - { flags: { taskEnv }, args: { envName: envKeyMaybe } }, + { + flags: { taskEnv: taskKeyMaybe }, + args: { envName: envKeyMaybe }, + }, ) { const { envKey, envName } = envKeyArgs({ - taskKeyMaybe: taskEnv, - envKeyMaybe, + taskKeyMaybe: taskKeyMaybe as string, + envKeyMaybe: envKeyMaybe as string, }); await reduceAndCookEnv(gcx, ecx, envKey, envName ?? envKey); }, @@ -184,11 +192,14 @@ export class EnvsModule extends ModuleBase { ...commonArgs, }, action: async function ( - { flags: { taskEnv }, args: { envName: envKeyMaybe } }, + { + flags: { taskEnv: taskKeyMaybe }, + args: { envName: envKeyMaybe }, + }, ) { const { envKey } = envKeyArgs({ - taskKeyMaybe: taskEnv, - envKeyMaybe, + taskKeyMaybe: taskKeyMaybe as string, + envKeyMaybe: envKeyMaybe as string, }); const env = ecx.config.envs[envKey]; if (!env) { @@ -213,11 +224,11 @@ export class EnvsModule extends ModuleBase { ...commonArgs, }, action: async function ( - { flags: { taskEnv }, args: { envName: envKeyMaybe } }, + { flags: { taskEnv: taskKeyMaybe }, args: { envName: envKeyMaybe } }, ) { const { envKey, envName } = envKeyArgs({ - taskKeyMaybe: taskEnv, - envKeyMaybe, + taskKeyMaybe: taskKeyMaybe as string, + envKeyMaybe: envKeyMaybe as string, }); await reduceAndCookEnv( gcx, @@ -230,158 +241,6 @@ export class EnvsModule extends ModuleBase { }, ]; } - commands2() { - const gcx = this.gcx; - const ecx = getEnvsCtx(this.gcx); - - function envKeyArgs( - args: { - taskKeyMaybe?: string; - envKeyMaybe?: string; - }, - ) { - const { envKeyMaybe, taskKeyMaybe } = args; - if (taskKeyMaybe && envKeyMaybe) { - throw new Error( - "--task-env option can not be combined with [envName] argument", - ); - } - if (taskKeyMaybe) { - const tasksCx = getTasksCtx(gcx); - const taskDef = tasksCx.config.tasks[taskKeyMaybe]; - if (!taskDef) { - throw new Error(`no task found under key "${taskKeyMaybe}"`); - } - return { envKey: taskDef.envKey }; - } - const actualKey = ecx.config.envsNamed[envKeyMaybe ?? ecx.activeEnv]; - return actualKey - ? { envKey: actualKey, envName: envKeyMaybe ?? ecx.activeEnv } - : { envKey: envKeyMaybe ?? ecx.activeEnv }; - } - - return { - envs: new cliffy_cmd - .Command() - .description("Envs module, reproducable posix shells environments.") - .alias("e") - // .alias("env") - .action(function () { - this.showHelp(); - }) - .command( - "ls", - new cliffy_cmd.Command() - .description("List environments defined in the ghjkfile.") - .action(() => { - // deno-lint-ignore no-console - console.log( - Object.entries(ecx.config.envsNamed) - // envs that have names which start with underscors - // don't show up in the cli list - .filter(([key]) => !key.startsWith("_")) - .map(([name, hash]) => { - const { desc } = ecx.config.envs[hash]; - return `${name}${desc ? ": " + desc : ""}`; - }) - .join("\n"), - ); - }), - ) - .command( - "activate", - new cliffy_cmd.Command() - .description(`Activate an environment. - -- If no [envName] is specified and no env is currently active, this activates the configured default env [${ecx.config.defaultEnv}].`) - .arguments("[envName:string]") - .option( - "-t, --task-env ", - "Synchronize to the environment used by the named task", - { standalone: true }, - ) - .action(async function ({ taskEnv }, envKeyMaybe) { - const { envKey } = envKeyArgs({ - taskKeyMaybe: taskEnv, - envKeyMaybe, - }); - await activateEnv(envKey); - }), - ) - .command( - "cook", - new cliffy_cmd.Command() - .description(`Cooks the environment to a posix shell. - -- If no [envName] is specified, this will cook the active env [${ecx.activeEnv}]`) - .arguments("[envName:string]") - .option( - "-t, --task-env ", - "Synchronize to the environment used by the named task", - { standalone: true }, - ) - .action(async function ({ taskEnv }, envKeyMaybe) { - const { envKey, envName } = envKeyArgs({ - taskKeyMaybe: taskEnv, - envKeyMaybe, - }); - await reduceAndCookEnv(gcx, ecx, envKey, envName ?? envKey); - }), - ) - .command( - "show", - new cliffy_cmd.Command() - .description(`Show details about an environment. - -- If no [envName] is specified, this shows details of the active env [${ecx.activeEnv}]. -- If no [envName] is specified and no env is active, this shows details of the default env [${ecx.config.defaultEnv}]. - `) - .arguments("[envName:string]") - .option( - "-t, --task-env ", - "Synchronize to the environment used by the named task", - { standalone: true }, - ) - .action(async function ({ taskEnv }, envKeyMaybe) { - const { envKey } = envKeyArgs({ - taskKeyMaybe: taskEnv, - envKeyMaybe, - }); - const env = ecx.config.envs[envKey]; - if (!env) { - throw new Error(`no env found under "${envKey}"`); - } - // deno-lint-ignore no-console - console.log($.inspect(await showableEnv(gcx, env, envKey))); - }), - ), - sync: new cliffy_cmd.Command() - .description(`Synchronize your shell to what's in your config. - -Cooks and activates an environment. -- If no [envName] is specified and no env is currently active, this syncs the configured default env [${ecx.config.defaultEnv}]. -- If the environment is already active, this doesn't launch a new shell.`) - .arguments("[envName:string]") - .option( - "-t, --task-env ", - "Synchronize to the environment used by the named task", - { standalone: true }, - ) - .action(async function ({ taskEnv }, envKeyMaybe) { - const { envKey, envName } = envKeyArgs({ - taskKeyMaybe: taskEnv, - envKeyMaybe, - }); - await reduceAndCookEnv( - gcx, - ecx, - envKey, - envName ?? envKey, - ); - await activateEnv(envKey); - }), - }; - } loadLockEntry(raw: Json) { const entry = lockValidator.parse(raw); diff --git a/modules/ports/mod.ts b/modules/ports/mod.ts index 811d09e..4221e65 100644 --- a/modules/ports/mod.ts +++ b/modules/ports/mod.ts @@ -1,6 +1,6 @@ export * from "./types.ts"; -import { cliffy_cmd, Table, zod } from "../../deps/cli.ts"; +import { Table, zod } from "../../deps/cli.ts"; import { $, Json, unwrapZodRes } from "../../utils/mod.ts"; import logger from "../../utils/logger.ts"; import validators, { @@ -35,6 +35,7 @@ import type { Provision, ProvisionReducer } from "../envs/types.ts"; import { getPortsCtx } from "./inter.ts"; import { updateInstall } from "./utils.ts"; import { getEnvsCtx } from "../envs/inter.ts"; +import { CliCommand } from "../../src/deno_systems/types.ts"; export type PortsCtx = { config: PortsModuleConfigX; @@ -111,166 +112,60 @@ export class PortsModule extends ModuleBase { } override commands() { - return []; - } - - commands2() { const gcx = this.gcx; const pcx = getPortsCtx(gcx); - return { - ports: new cliffy_cmd.Command() - .alias("p") - .action(function () { - this.showHelp(); - }) - .description("Ports module, install programs into your env.") - .command( - "resolve", - new cliffy_cmd.Command() - .description(`Resolve all installs declared in config. - -- Useful to pre-resolve and add all install configs to the lockfile.`) - .action(async function () { - // scx contains a reference counted db connection - // somewhere deep in there - // so we need to use `using` - await using scx = await syncCtxFromGhjk(gcx); - for (const [_id, set] of Object.entries(pcx.config.sets)) { - void await buildInstallGraph(scx, set); - } - }), - ) - .command( - "outdated", - new cliffy_cmd.Command() - .description("Show a version table for installs.") - .option( - "-u, --update-install ", - "Update specific install", - ) - .option("-n, --update-all", "Update all installs") - .action(async (opts) => { - const envsCtx = getEnvsCtx(gcx); - const envName = envsCtx.activeEnv; - - const installSets = pcx.config.sets; - - let currInstallSetId; - { - const activeEnvName = envsCtx.activeEnv; - const activeEnv = envsCtx.config - .envs[ - envsCtx.config.envsNamed[activeEnvName] ?? activeEnvName - ]; - if (!activeEnv) { - throw new Error( - `No env found under given name "${activeEnvName}"`, - ); - } - - const instSetRef = activeEnv.provides.filter((prov) => - prov.ty === installSetRefProvisionTy - )[0] as InstallSetRefProvision; - - currInstallSetId = instSetRef.setId; - } - const currInstallSet = installSets[currInstallSetId]; - const allowedDeps = currInstallSet.allowedBuildDeps; - - const rows = []; - const { - installedPortsVersions: installed, - latestPortsVersions: latest, - installConfigs, - } = await getOldNewVersionComparison( - gcx, - envName, - allowedDeps, - ); - for (let [installId, installedVersion] of installed.entries()) { - let latestVersion = latest.get(installId); - if (!latestVersion) { - throw new Error( - `Couldn't find the latest version for install id: ${installId}`, - ); - } - - if (latestVersion[0] === "v") { - latestVersion = latestVersion.slice(1); - } - if (installedVersion[0] === "v") { - installedVersion = installedVersion.slice(1); - } - - const config = installConfigs.get(installId); - - if (!config) { - throw new Error( - `Config not found for install id: ${installId}`, - ); - } - - if (config["specifiedVersion"]) { - latestVersion = "=" + latestVersion; - } - - const presentableConfig = { ...config }; - ["buildDepConfigs", "version", "specifiedVersion"].map( - (key) => { - delete presentableConfig[key]; - }, - ); - const row = [ - $.inspect(presentableConfig), - installedVersion, - latestVersion, - ]; - rows.push(row); - } - - if (opts.updateInstall) { - const installName = opts.updateInstall; - // TODO: convert from install name to install id, after port module refactor - let installId!: string; - const newVersion = latest.get(installId); - if (!newVersion) { - logger().info( - `Error while fetching the latest version for: ${installName}`, - ); - return; - } - await updateInstall(gcx, installId, newVersion, allowedDeps); - return; - } - - if (opts.updateAll) { - for (const [installId, newVersion] of latest.entries()) { - await updateInstall(gcx, installId, newVersion, allowedDeps); - } - return; - } - - const _versionTable = new Table() - .header(["Install Config", "Old Version", "New Version"]) - .body(rows) - .border() - .padding(1) - .indent(2) - .maxColWidth(30) - .render(); - }), - ) - .command( - "cleanup", - new cliffy_cmd.Command() - .description("TODO") - .action(function () { - throw new Error("TODO"); - }), - ), - }; + const out: CliCommand[] = [{ + name: "ports", + visible_aliases: ["p"], + about: "Ports module, install programs into your env.", + sub_commands: [ + { + name: "resolve", + about: "Resolve all installs declared in config.", + before_long_help: + `- Useful to pre-resolve and add all install configs to the lockfile.`, + action: async function () { + // scx contains a reference counted db connection + // somewhere deep in there + // so we need to use `using` + await using scx = await syncCtxFromGhjk(gcx); + for (const [_id, set] of Object.entries(pcx.config.sets)) { + void await buildInstallGraph(scx, set); + } + }, + }, + { + name: "outdated", + about: "Show a version table for installs.", + flags: { + updateInstall: { + short: "u", + long: "update-install", + value_name: "INSTALL_ID", + }, + updateAll: { + short: "a", + long: "update-all", + action: "SetTrue", + }, + }, + action: async function ( + { flags: { updateInstall, updateAll } }, + ) { + await outdatedCommand( + gcx, + pcx, + updateInstall as string | undefined, + updateAll as string | undefined, + ); + }, + }, + ], + }]; + return out; } + loadLockEntry(raw: Json) { const entry = lockValidator.parse(raw); @@ -304,6 +199,122 @@ export class PortsModule extends ModuleBase { } } +async function outdatedCommand( + gcx: GhjkCtx, + pcx: PortsCtx, + updateInstallFlag?: string, + updateAllFlag?: string, +) { + const envsCtx = getEnvsCtx(gcx); + const envName = envsCtx.activeEnv; + + const installSets = pcx.config.sets; + + let currInstallSetId; + { + const activeEnvName = envsCtx.activeEnv; + const activeEnv = envsCtx.config + .envs[ + envsCtx.config.envsNamed[activeEnvName] ?? activeEnvName + ]; + if (!activeEnv) { + throw new Error( + `No env found under given name "${activeEnvName}"`, + ); + } + + const instSetRef = activeEnv.provides.filter((prov) => + prov.ty === installSetRefProvisionTy + )[0] as InstallSetRefProvision; + + currInstallSetId = instSetRef.setId; + } + const currInstallSet = installSets[currInstallSetId]; + const allowedDeps = currInstallSet.allowedBuildDeps; + + const rows = []; + const { + installedPortsVersions: installed, + latestPortsVersions: latest, + installConfigs, + } = await getOldNewVersionComparison( + gcx, + envName, + allowedDeps, + ); + for (let [installId, installedVersion] of installed.entries()) { + let latestVersion = latest.get(installId); + if (!latestVersion) { + throw new Error( + `Couldn't find the latest version for install id: ${installId}`, + ); + } + + if (latestVersion[0] === "v") { + latestVersion = latestVersion.slice(1); + } + if (installedVersion[0] === "v") { + installedVersion = installedVersion.slice(1); + } + + const config = installConfigs.get(installId); + + if (!config) { + throw new Error( + `Config not found for install id: ${installId}`, + ); + } + + if (config["specifiedVersion"]) { + latestVersion = "=" + latestVersion; + } + + const presentableConfig = { ...config }; + ["buildDepConfigs", "version", "specifiedVersion"].map( + (key) => { + delete presentableConfig[key]; + }, + ); + const row = [ + $.inspect(presentableConfig), + installedVersion, + latestVersion, + ]; + rows.push(row); + } + + if (updateInstallFlag) { + const installName = updateInstallFlag; + // TODO: convert from install name to install id, after port module refactor + let installId!: string; + const newVersion = latest.get(installId); + if (!newVersion) { + logger().info( + `Error while fetching the latest version for: ${installName}`, + ); + return; + } + await updateInstall(gcx, installId, newVersion, allowedDeps); + return; + } + + if (updateAllFlag) { + for (const [installId, newVersion] of latest.entries()) { + await updateInstall(gcx, installId, newVersion, allowedDeps); + } + return; + } + + const _versionTable = new Table() + .header(["Install Config", "Old Version", "New Version"]) + .body(rows) + .border() + .padding(1) + .indent(2) + .maxColWidth(30) + .render(); +} + async function getOldNewVersionComparison( gcx: GhjkCtx, envName: string, diff --git a/modules/tasks/mod.ts b/modules/tasks/mod.ts index 62ac0c1..a7846ec 100644 --- a/modules/tasks/mod.ts +++ b/modules/tasks/mod.ts @@ -1,6 +1,6 @@ export * from "./types.ts"; -import { cliffy_cmd, zod } from "../../deps/cli.ts"; +import { zod } from "../../deps/cli.ts"; import { Json, unwrapZodRes } from "../../utils/mod.ts"; import validators from "./types.ts"; @@ -11,6 +11,7 @@ import { ModuleBase } from "../mod.ts"; import { buildTaskGraph, execTask, type TaskGraph } from "./exec.ts"; import { Blackboard } from "../../host/types.ts"; import { getTasksCtx } from "./inter.ts"; +import { CliCommand } from "../../src/deno_systems/types.ts"; export type TasksCtx = { config: TasksModuleConfigX; @@ -47,54 +48,47 @@ export class TasksModule extends ModuleBase { } override commands() { - return []; - } - - commands2() { const gcx = this.gcx; const tcx = getTasksCtx(this.gcx); const namedSet = new Set(tcx.config.tasksNamed); - const commands = Object.keys(tcx.config.tasks) - .sort() - .map( - (key) => { - const def = tcx.config.tasks[key]; - const cmd = new cliffy_cmd.Command() - .name(key) - .useRawArgs() - .action(async (_, ...args) => { - await execTask( - gcx, - tcx.config, - tcx.taskGraph, - key, - args, - ); - }); - if (def.desc) { - cmd.description(def.desc); - } - if (!namedSet.has(key)) { - cmd.hidden(); - } - return cmd; - }, - ); - const root = new cliffy_cmd.Command() - .alias("x") - .action(function () { - this.showHelp(); - }) - .description(`Tasks module. - -The named tasks in your ghjkfile will be listed here.`); - for (const cmd of commands) { - root.command(cmd.getName(), cmd); - } - return { - tasks: root, - }; + const out: CliCommand[] = [{ + name: "tasks", + visible_aliases: ["x"], + about: "Tasks module, execute your task programs.", + before_long_help: "The named tasks in your ghjkfile will be listed here.", + sub_commands: [ + ...Object.keys(tcx.config.tasks) + .sort() + .map( + (key) => { + const def = tcx.config.tasks[key]; + return { + name: key, + about: def.desc, + hide: !namedSet.has(key), + args: { + raw: { + value_name: "TASK ARGS", + trailing_var_arg: true, + allow_hyphen_values: true, + }, + }, + action: async ({ args: { raw } }) => { + await execTask( + gcx, + tcx.config, + tcx.taskGraph, + key, + (raw as string[]) ?? [], + ); + }, + } as CliCommand; + }, + ), + ], + }]; + return out; } loadLockEntry(raw: Json) { diff --git a/src/deno_systems/types.ts b/src/deno_systems/types.ts index 6e8b1f3..2cc7273 100644 --- a/src/deno_systems/types.ts +++ b/src/deno_systems/types.ts @@ -26,10 +26,23 @@ const cliArg = zod.object({ "EmailAddress", ]).optional(), + action: zod.enum([ + "Set", + "Append", + "SetTrue", + "SetFalse", + "Count", + "Help", + "HelpShort", + "HelpLong", + "Version", + ]).optional(), + required: zod.boolean().optional(), global: zod.boolean().optional(), hide: zod.boolean().optional(), exclusive: zod.boolean().optional(), + trailing_var_arg: zod.boolean().optional(), env: zod.string().optional(), @@ -50,7 +63,6 @@ const cliFlag = cliArg.extend({ const cliCommandBase = zod.object({ name: zod.string(), - short_flag: charSchema.optional(), aliases: zod.string().array().optional(), visible_aliases: zod.string().array().optional(), @@ -64,9 +76,17 @@ const cliCommandBase = zod.object({ flags: zod.record(cliFlag).optional().optional(), }); +const flagsAndArgs = zod.record( + zod.union([ + zod.string(), + zod.string().array(), + zod.string().array().array(), + ]).optional(), +); + const cliActionArgs = zod.object({ - flags: zod.record(zod.string().optional()), - args: zod.record(zod.string().optional()), + flags: flagsAndArgs, + args: flagsAndArgs, }); const cliCommandActionBase = cliCommandBase.extend({ diff --git a/src/denort/lib.rs b/src/denort/lib.rs index b84d8f7..bfacbb7 100644 --- a/src/denort/lib.rs +++ b/src/denort/lib.rs @@ -89,7 +89,7 @@ pub async fn worker( } } } - std::mem::forget(cx); + // std::mem::forget(cx); trace!("deno worker done"); } .instrument(tracing::trace_span!("deno-worker")), @@ -159,7 +159,7 @@ async fn module_worker( .expect_or_log("channel error"), } } - std::mem::forget(module_cx); + // std::mem::forget(module_cx); trace!("module worker done"); } .instrument(tracing::trace_span!( diff --git a/src/ghjk/Cargo.toml b/src/ghjk/Cargo.toml index 5335c17..605a9ea 100644 --- a/src/ghjk/Cargo.toml +++ b/src/ghjk/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "ghjk" +description = "Programmable runtime manager." version.workspace = true edition.workspace = true @@ -13,12 +14,17 @@ deno_core.workspace = true serde = "1" serde_json = "1" + ahash = { version = "0.8", features = ["serde"] } +indexmap = { version = "2.6.0", features = ["serde"] } # serde_repr = { version = "0.1" } regex = "1.10" + rand = "0.8" time = { version = "0.3", features = ["serde"] } +nix = { version = "0.29.0", features = ["signal"] } + once_cell = "1.19" parking_lot = "*" bitflags = "*" @@ -46,24 +52,25 @@ tracing-subscriber.workspace = true tracing-appender = "0.2" tracing-futures = "0.2" +async-trait = "0.1.83" +futures-concurrency = "7.6.2" +futures = { version = "=0.3.30", default-features = false, features = ["std", "async-await"] } tokio = { workspace = true, features = ["full", "parking_lot"] } tokio-stream = "0.1" dashmap = { version = "5.5", features = ["serde"]} clap = { workspace = true, features = ["derive", "env"] } +clap_complete = "=4.5.24" shadow-rs.workspace = true -nix = { version = "0.29.0", features = ["signal"] } -indexmap = { version = "2.6.0", features = ["serde"] } -futures-concurrency = "7.6.2" -futures = { version = "=0.3.30", default-features = false, features = ["std", "async-await"] } + multihash = "0.19.2" json-canon = "0.1.3" data-encoding = "2.6.0" sha2 = "0.10.8" + pathdiff = "0.2.2" directories = "5.0.1" -async-trait = "0.1.83" [build-dependencies] shadow-rs.workspace = true diff --git a/src/ghjk/cli.rs b/src/ghjk/cli.rs index 88bdd51..83fcdc6 100644 --- a/src/ghjk/cli.rs +++ b/src/ghjk/cli.rs @@ -1,3 +1,7 @@ +use std::process::ExitCode; + +use clap::builder::styling::AnsiColor; + use crate::interlude::*; use crate::systems::{CliCommandAction, SystemCliCommand}; @@ -5,7 +9,7 @@ use crate::{host, systems, utils, Config}; const DENO_UNSTABLE_FLAGS: &[&str] = &["worker-options", "kv"]; -pub async fn cli() -> Res<()> { +pub async fn cli() -> Res { let cwd = std::env::current_dir()?; let config = { @@ -64,7 +68,7 @@ pub async fn cli() -> Res<()> { }; let Some(quick_err) = try_quick_cli(&config).await? else { - return Ok(()); + return Ok(ExitCode::SUCCESS); }; let Some(ghjk_dir_path) = config.ghjkdir_path.clone() else { @@ -146,7 +150,9 @@ pub async fn cli() -> Res<()> { }; for cmd in sys_cmds { - root_cmd = root_cmd.subcommand(cmd); + // apply styles again here due to propagation + // breaking for these dynamic subcommands for some reason + root_cmd = root_cmd.subcommand(cmd.styles(CLAP_STYLE)); } let matches = match root_cmd.try_get_matches() { @@ -160,7 +166,7 @@ pub async fn cli() -> Res<()> { match QuickComands::from_arg_matches(&matches) { Ok(QuickComands::Print { commands }) => { _ = commands.action(&config, Some(&systems.config))?; - return Ok(()); + return Ok(ExitCode::SUCCESS); } Err(err) => { let kind = err.kind(); @@ -186,9 +192,9 @@ pub async fn cli() -> Res<()> { }; let Some(action) = action.action else { - action.clap.print_long_help()?; systems.write_lockfile_or_log().await; - return Ok(()); + action.clap.print_long_help()?; + return Ok(std::process::ExitCode::FAILURE); }; let res = action(action_matches.clone()) @@ -199,7 +205,7 @@ pub async fn cli() -> Res<()> { deno_cx.terminate().await?; - res + res.map(|()| ExitCode::SUCCESS) } /// Sections of the CLI do not require loading a ghjkfile. @@ -235,8 +241,18 @@ pub async fn try_quick_cli(config: &Config) -> Res> { Ok(None) } -#[derive(clap::Parser, Debug)] -#[command(version, about)] +const CLAP_STYLE: clap::builder::Styles = clap::builder::Styles::styled() + .header(AnsiColor::Yellow.on_default()) + .usage(AnsiColor::BrightBlue.on_default()) + .literal(AnsiColor::BrightBlue.on_default()) + .placeholder(AnsiColor::BrightBlue.on_default()); + +#[derive(Debug, clap::Parser)] +#[clap( + version, + about, + styles = CLAP_STYLE +)] struct Cli { #[command(subcommand)] quick_commands: QuickComands, @@ -341,6 +357,7 @@ async fn commands_from_systems( ) } let mut commands = vec![]; + let mut conflict_tracker = HashMap::new(); let mut actions = SysCmdActions::new(); for (id, sys_inst) in &systems.sys_instances { let cmds = sys_inst @@ -349,6 +366,13 @@ async fn commands_from_systems( .wrap_err_with(|| format!("error getting commands for system: {id}"))?; for cmd in cmds { let (sys_cmd, clap_cmd) = inner(cmd); + + if let Some(conflict) = conflict_tracker.insert(sys_cmd.name.clone(), id) { + eyre::bail!( + "system commannd conflict under name {:?} for modules {conflict:?} and {id:?}", + sys_cmd.name.clone(), + ); + } actions.insert(sys_cmd.name.clone(), sys_cmd); commands.push(clap_cmd); } diff --git a/src/ghjk/main.rs b/src/ghjk/main.rs index aa3714e..0daf983 100644 --- a/src/ghjk/main.rs +++ b/src/ghjk/main.rs @@ -39,7 +39,7 @@ mod utils; use crate::interlude::*; -fn main() -> Res<()> { +fn main() -> Res { // FIXME: change signal handler for children // FIXME: use unix_sigpipe once https://github.com/rust-lang/rust/issues/97889 lands unsafe { @@ -54,8 +54,7 @@ fn main() -> Res<()> { tokio::runtime::Builder::new_current_thread() .enable_all() .build()? - .block_on(cli::cli())?; - Ok(()) + .block_on(cli::cli()) } use shadow_rs::shadow; diff --git a/src/ghjk/systems.rs b/src/ghjk/systems.rs index 62259dd..c0b34f1 100644 --- a/src/ghjk/systems.rs +++ b/src/ghjk/systems.rs @@ -136,4 +136,14 @@ pub struct SystemCliCommand { pub action: Option, } +impl std::fmt::Debug for SystemCliCommand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SystemCliCommand") + .field("name", &self.name) + .field("sub_commands", &self.sub_commands) + .field("actions", &self.action.is_some()) + .finish() + } +} + pub type ConfigBlackboard = Arc>; diff --git a/src/ghjk/systems/deno.rs b/src/ghjk/systems/deno.rs index a38f0d2..63b1ba5 100644 --- a/src/ghjk/systems/deno.rs +++ b/src/ghjk/systems/deno.rs @@ -10,7 +10,7 @@ mod cli; #[derive(Clone)] pub struct DenoSystemsContext { callbacks: crate::ext::CallbacksHandle, - exit_code_channel: Arc>>>, + exit_code_channel: Arc>>>>, term_signal: tokio::sync::watch::Sender, #[allow(unused)] hostcalls: crate::ext::Hostcalls, @@ -18,7 +18,7 @@ pub struct DenoSystemsContext { impl DenoSystemsContext { #[allow(unused)] - pub async fn terminate(&mut self) -> Res { + pub async fn terminate(&mut self) -> Res<()> { let channel = { let mut opt = self.exit_code_channel.lock().expect_or_log("mutex error"); opt.take() @@ -27,7 +27,7 @@ impl DenoSystemsContext { eyre::bail!("already terminated") }; self.term_signal.send(true).expect_or_log("channel error"); - Ok(channel.await.expect_or_log("channel error")) + channel.await.expect_or_log("channel error") } } @@ -114,20 +114,41 @@ pub async fn systems_from_deno( ) .await?; worker.execute().await?; - let (exit_code_channel, term_signal, _) = worker.drive_till_exit().await?; + let (mut exit_code_channel, term_signal, _) = worker.drive_till_exit().await?; + + let manifests = tokio::select! { + res = &mut exit_code_channel => { + let exit_code = res + .expect_or_log("channel error") + .wrap_err("deno systems error building manifests")?; + eyre::bail!("premature exit of deno systems before manifests were sent: exit code = {exit_code}"); + }, + manifests = manifests_rx.recv() => { + manifests.expect_or_log("channel error") + } + }; + + let manifests: Vec = + serde_json::from_value(manifests).wrap_err("protocol error")?; + let dcx = gcx.deno.clone(); let join_exit_code_watcher = tokio::spawn(async { - let exit_code = exit_code_channel - .await - .expect_or_log("channel error") - .wrap_err("error on event loop for deno systems") - .unwrap_or_log(); - if exit_code != 0 { - // TODO: exit signals - error!(%exit_code, "deno systems died with non-zero exit code"); - } - exit_code + let err = match exit_code_channel.await.expect_or_log("channel error") { + Ok(0) => return Ok(()), + Ok(exit_code) => { + error!(%exit_code, "deno systems died with non-zero exit code"); + let err = ferr!("deno systems died with non-zero exit code: {exit_code}"); + error!("{err}"); + err + } + Err(err) => err.wrap_err("error on event loop for deno systems"), + }; + // TODO: better exit signals + debug!("killing whole deno context"); + dcx.terminate().await.unwrap(); + Err(err) }); + let exit_code_channel = Arc::new(std::sync::Mutex::new(Some(join_exit_code_watcher))); let scx = DenoSystemsContext { @@ -137,9 +158,6 @@ pub async fn systems_from_deno( exit_code_channel, }; - let manifests = manifests_rx.recv().await.expect_or_log("channel error"); - let manifests: Vec = - serde_json::from_value(manifests).wrap_err("protocol error")?; let manifests = manifests .into_iter() .map(|desc| { diff --git a/src/ghjk/systems/deno/cli.rs b/src/ghjk/systems/deno/cli.rs index 3aad4a2..057d8b8 100644 --- a/src/ghjk/systems/deno/cli.rs +++ b/src/ghjk/systems/deno/cli.rs @@ -7,8 +7,8 @@ pub struct CliCommandDesc { pub hide: Option, - pub short_flag: Option, pub aliases: Option>, + pub visible_aliases: Option>, pub about: Option, pub before_help: Option, @@ -30,17 +30,17 @@ impl CliCommandDesc { if let Some(val) = self.hide { cmd = cmd.hide(val) } - if let Some(val) = self.short_flag { - cmd = cmd.short_flag(val) - } if let Some(val) = self.aliases { cmd = cmd.aliases(val) } + if let Some(val) = self.visible_aliases { + cmd = cmd.visible_aliases(val) + } if let Some(val) = &self.about { cmd = cmd.about(val) } if let Some(val) = self.before_help { - cmd = cmd.before_long_help(val) + cmd = cmd.before_help(val) } if let Some(val) = self.before_long_help { cmd = cmd.before_long_help(val) @@ -68,7 +68,6 @@ impl CliCommandDesc { for desc in val { let id = desc.name.clone(); let scmd = desc.into_clap(scx.clone()); - // cmd = cmd.subcommand(scmd.clap.take().unwrap()); subcommands.insert(id.into(), scmd); } subcommands @@ -92,7 +91,6 @@ impl CliCommandDesc { None }; - crate::systems::SystemCliCommand { name: name.into(), clap: cmd, @@ -160,6 +158,10 @@ pub struct CliArgDesc { pub global: Option, pub hide: Option, pub exclusive: Option, + pub trailing_var_arg: Option, + pub allow_hyphen_values: Option, + + pub action: Option, pub value_name: Option, pub value_hint: Option, @@ -194,6 +196,16 @@ impl CliArgDesc { if let Some(val) = self.exclusive { arg = arg.exclusive(val) } + if let Some(val) = self.trailing_var_arg { + arg = arg.num_args(..).trailing_var_arg(val) + } + if let Some(val) = self.allow_hyphen_values { + arg = arg.allow_hyphen_values(val) + } + + if let Some(val) = self.action { + arg = arg.action(clap::ArgAction::from(val)) + } if let Some(val) = self.value_name { arg = arg.value_name(val) @@ -319,3 +331,33 @@ impl From for clap::ValueHint { } } } + +#[derive(Deserialize, Debug)] +pub enum ArgActionSerde { + Set, + Append, + SetTrue, + SetFalse, + Count, + Help, + HelpShort, + HelpLong, + Version, +} + +impl From for clap::ArgAction { + fn from(val: ArgActionSerde) -> Self { + use ArgActionSerde::*; + match val { + Set => clap::ArgAction::Set, + Append => clap::ArgAction::Append, + SetTrue => clap::ArgAction::SetTrue, + SetFalse => clap::ArgAction::SetFalse, + Count => clap::ArgAction::Count, + Help => clap::ArgAction::Help, + HelpShort => clap::ArgAction::HelpShort, + HelpLong => clap::ArgAction::HelpLong, + Version => clap::ArgAction::Version, + } + } +} diff --git a/src/xtask/main.rs b/src/xtask/main.rs index 803fe49..a1841ab 100644 --- a/src/xtask/main.rs +++ b/src/xtask/main.rs @@ -9,6 +9,8 @@ mod interlude { pub use tracing::{debug, error, info, trace, warn}; pub use tracing_unwrap::*; } +use clap::builder::styling::AnsiColor; + use crate::interlude::*; mod utils; @@ -48,22 +50,32 @@ fn main() -> Res<()> { Arc::new(std::vec::Vec::new), vec![], ) - } - /* Commands::Run { argv } => denort::run_sync( - denort::deno::deno_runtime::deno_core::resolve_url_or_path("ghjk.ts", &cwd).unwrap(), - Some("deno.jsonc".into()), - denort::deno::args::PermissionFlags { - allow_all: true, - ..Default::default() - }, - Arc::new(std::vec::Vec::new), - ), */ + } /* Commands::Run { argv } => denort::run_sync( + denort::deno::deno_runtime::deno_core::resolve_url_or_path("ghjk.ts", &cwd).unwrap(), + Some("deno.jsonc".into()), + denort::deno::args::PermissionFlags { + allow_all: true, + ..Default::default() + }, + Arc::new(std::vec::Vec::new), + ), */ } + Ok(()) } +const CLAP_STYLE: clap::builder::Styles = clap::builder::Styles::styled() + .header(AnsiColor::Yellow.on_default()) + .usage(AnsiColor::Green.on_default()) + .literal(AnsiColor::Green.on_default()) + .placeholder(AnsiColor::Green.on_default()); + #[derive(Debug, clap::Parser)] -#[clap(version, about)] +#[clap( + version, + about, + styles = CLAP_STYLE +)] struct Args { #[clap(subcommand)] command: Commands, diff --git a/utils/mod.ts b/utils/mod.ts index 2e77256..0082d8b 100644 --- a/utils/mod.ts +++ b/utils/mod.ts @@ -43,7 +43,7 @@ export const jsonSchema: zod.ZodType = zod.lazy(() => ); export function dbg(val: T, ...more: unknown[]) { - logger().debug(() => val, ...more, "DBG"); + logger().debug("DBG", val, ...more); return val; }