From 87073b5409f3c65fa1a3fd0d55e4a53009574db1 Mon Sep 17 00:00:00 2001 From: theMackabu Date: Wed, 18 Feb 2026 00:51:32 -0800 Subject: [PATCH] add watch mode functionality and improve CLI error handling --- include/ant.h | 2 +- include/cli/misc.h | 3 + include/utils.h | 4 +- include/watch.h | 11 ++ meson.build | 14 +- meson/messages.py | 20 +++ sources.json | 4 +- src/cli/misc.c | 28 +++- src/core/messages.toml | 44 ++++++ src/main.c | 234 ++++++++++++++++++----------- src/utils.c | 17 ++- src/watch.c | 333 +++++++++++++++++++++++++++++++++++++++++ tests/server.async.js | 1 + 13 files changed, 610 insertions(+), 105 deletions(-) create mode 100644 include/watch.h create mode 100644 meson/messages.py create mode 100644 src/core/messages.toml create mode 100644 src/watch.c diff --git a/include/ant.h b/include/ant.h index f2345db..3659df4 100644 --- a/include/ant.h +++ b/include/ant.h @@ -190,4 +190,4 @@ bool js_is_slot_prop(jsoff_t header); jsoff_t js_next_prop(jsoff_t header); jsoff_t js_loadoff(ant_t *js, jsoff_t off); -#endif \ No newline at end of file +#endif diff --git a/include/cli/misc.h b/include/cli/misc.h index 9e6f92f..1481f0e 100644 --- a/include/cli/misc.h +++ b/include/cli/misc.h @@ -1,6 +1,8 @@ #ifndef MISC_H #define MISC_H + #include +struct arg_end; typedef struct { const char *s; @@ -12,5 +14,6 @@ typedef struct { void print_flags_help(FILE *fp, void **argtable); void print_flag(FILE *fp, flag_help_t f); +void print_errors(FILE *fp, struct arg_end *end); #endif \ No newline at end of file diff --git a/include/utils.h b/include/utils.h index cf972e7..13c8641 100644 --- a/include/utils.h +++ b/include/utils.h @@ -1,6 +1,6 @@ #ifndef ANT_UTILS_H #define ANT_UTILS_H -#define ARGTABLE_COUNT 8 +#define ARGTABLE_COUNT 10 #include #include @@ -36,4 +36,4 @@ char *cstr_init( #define CSTR_INIT(buf, src, len) \ cstr_init(&(buf), buf##_stack, sizeof(buf##_stack), (src), (len)) -#endif \ No newline at end of file +#endif diff --git a/include/watch.h b/include/watch.h new file mode 100644 index 0000000..336d384 --- /dev/null +++ b/include/watch.h @@ -0,0 +1,11 @@ +#ifndef ANT_WATCH_H +#define ANT_WATCH_H + +#include + +int ant_watch_run( + int argc, char **argv, + const char *entry_file, bool no_clear_screen +); + +#endif diff --git a/meson.build b/meson.build index 2f33afe..e7f779c 100644 --- a/meson.build +++ b/meson.build @@ -64,6 +64,13 @@ libant_dep = declare_dependency( dependencies: ant_deps + [oxc_dep, pkg_dep] ) +messages_h = custom_target('messages_h', + input: 'src/core/messages.toml', + output: 'messages.h', + command: [python, files('meson/messages.py'), '@INPUT@'], + capture: true, +) + link_args = [] if get_option('static_link') link_args += ['-static'] @@ -71,7 +78,6 @@ endif executable( 'ant', - files('src/main.c'), - dependencies: libant_dep, - link_args: link_args -) \ No newline at end of file + files('src/main.c', 'src/watch.c') + [messages_h], + dependencies: libant_dep, link_args: link_args +) diff --git a/meson/messages.py b/meson/messages.py new file mode 100644 index 0000000..0aaf4b7 --- /dev/null +++ b/meson/messages.py @@ -0,0 +1,20 @@ +import tomllib, sys + +with open(sys.argv[1], "rb") as f: + messages = tomllib.load(f)["messages"] + +print("#ifndef MESSAGES_H") +print("#define MESSAGES_H") +print("") +print("typedef struct {") +for name in messages: + print(f" const char *{name};") +print("} ant_messages_t;") +print("") +print("static const ant_messages_t msg = {") +for name, value in messages.items(): + escaped = value.encode("unicode_escape").decode().replace('"', '\\"') + print(f' .{name} = "{escaped}",') +print("};") +print("") +print("#endif") \ No newline at end of file diff --git a/sources.json b/sources.json index a79476a..4080ecd 100644 --- a/sources.json +++ b/sources.json @@ -1,11 +1,11 @@ { "engine": { "patterns": ["src/*.c", "src/esm/*.c", "src/cli/*.c", "src/modules/*.c"], - "exclude": ["src/main.c"] + "exclude": ["src/main.c", "src/watch.c"] }, "library": { "patterns": ["src/*.c", "src/esm/*.c", "src/modules/*.c"], - "exclude": ["src/main.c"] + "exclude": ["src/main.c", "src/watch.c"] }, "core": { "patterns": ["src/core/**/*.ts"], diff --git a/src/cli/misc.c b/src/cli/misc.c index 2f90a3d..f878544 100644 --- a/src/cli/misc.c +++ b/src/cli/misc.c @@ -1,4 +1,5 @@ #include "cli/misc.h" +#include "messages.h" #include #include @@ -26,12 +27,23 @@ void print_flag(FILE *fp, flag_help_t f) { void print_flags_help(FILE *fp, void **argtable) { struct arg_hdr **table = (struct arg_hdr **)argtable; for (int i = 0; !(table[i]->flag & ARG_TERMINATOR); i++) { - struct arg_hdr *hdr = table[i]; - if (!hdr->glossary) continue; - print_flag(fp, (flag_help_t){ - .s = hdr->shortopts, .l = hdr->longopts, - .d = hdr->datatype, .g = hdr->glossary, - .opt = hdr->flag & ARG_HASOPTVALUE, - }); - } + struct arg_hdr *hdr = table[i]; + if (!hdr->glossary) continue; + print_flag(fp, (flag_help_t){ + .s = hdr->shortopts, .l = hdr->longopts, + .d = hdr->datatype, .g = hdr->glossary, + .opt = hdr->flag & ARG_HASOPTVALUE, + });} +} + +void print_errors(FILE *fp, struct arg_end *end) { + for (int i = 0; i < end->count; i++) { + int error = end->error[i]; + const char *argval = end->argval[i] ? end->argval[i] : ""; + switch (error) { + case ARG_ENOMATCH: { crfprintf(fp, msg.arg_unexpected, argval); break; } + case ARG_EMISSARG: { crfprintf(fp, msg.arg_opt_needed, argval); break; } + case ARG_ELONGOPT: { crfprintf(fp, msg.arg_invalid, argval); break; } + default: { if (error > 0) crfprintf(fp, msg.opt_invalid, error); break; } + }} fputc('\n', fp); } \ No newline at end of file diff --git a/src/core/messages.toml b/src/core/messages.toml new file mode 100644 index 0000000..8725d02 --- /dev/null +++ b/src/core/messages.toml @@ -0,0 +1,44 @@ +[messages] +ant_help_flags = "Flags:\n" +ant_command_extra = "\n --help Print help text for command.\n" + +snapshot_warn = "{warn}: Failed to load snapshot: %s\n" +unknown_flag_warn = "{warn}: Unknown ANT_DEBUG flag: \"%s\"\n" + +failed_to_fetch = "{error}: Could not fetch \"%s\": %s\n" +module_not_found = "{error}: Module not found: \"%s\"\n" +type_strip_failed = "{error}: Type stripping failed (%d)\n" +watch_entrypoint_error = "{error}: --watch requires a local file entrypoint.\n" +watch_module_error = "{error}: --watch only supports module file execution.\n" +watch_subcommand_error = "{error}: --watch is only supported when executing a module file.\n" +misuse_clear_screen = "{error}: --no-clear-screen can only be used with --watch.\n" + +ant_allocation_fatal = "{fatal}: Failed to allocate for Ant.\n" +oom_fatal = "FATAL: Out of memory\n" +argument_fatal = "{fatal}: Failed to resolve positional argument index.\n" + +arg_unexpected = "{error}: Unexpected argument %s\n" +arg_opt_needed = "{error}: Option %s requires an argument\n" +arg_invalid = "{error}: Invalid option %s\n" +opt_invalid = "{error}: Invalid option -%c\n" + +ant_help_header = """\ +{let h=bold, arg=cyan, name='Ant'}\ +<$h+red>{name} is a tiny JavaScript runtime and package manager ({version})\ +<$h>Usage: {~name} [module.js] <$arg>[...flags]
\ +<$h>{~name} <><$arg>[...args]\ +If no module file is specified, {name} starts in REPL mode.\ +""" + +watch_spawn_failed = "{error}: Failed to spawn watched process: %s\n" +watch_entrypoint_missing = "{error}: --watch requires a local file entrypoint.\n" +watch_file_failed = "{error}: Failed to watch %s: %s\n" +watch_child_error = "{error}: Failed to terminate child process: %s\n" + +watch_warn_normal = "{warn}: Watcher error on %s: %s\n" +watch_loop_cleanup = "{warn}: Watch loop cleanup incomplete: %s\n" +watch_graceful_term = "{warn}: Failed to terminate child process gracefully: %s\n" + +watch_start_fatal = "{fatal}: Failed to initialize watch mode.\n" +watch_loop_fatal = "{fatal}: Failed to initialize watch loop: %s\n" +watch_loop_handles_fatal = "{fatal}: Failed to initialize watch handles: %s\n" diff --git a/src/main.c b/src/main.c index 4fced6e..5828876 100644 --- a/src/main.c +++ b/src/main.c @@ -13,11 +13,13 @@ #include "ant.h" #include "repl.h" #include "utils.h" +#include "watch.h" #include "reactor.h" #include "runtime.h" #include "snapshot.h" #include "esm/remote.h" #include "internal.h" +#include "messages.h" #include "cli/pkg.h" #include "cli/misc.h" @@ -87,7 +89,7 @@ static void parse_ant_debug(const char *flag) { if (strcmp(mode, "hex") == 0 || strcmp(mode, "all") == 0) crprintf_set_debug_hex(true); } - else crfprintf(stderr, "{warn}: Unknown ANT_DEBUG flag: \"%s\"\n", flag); + else crfprintf(stderr, msg.unknown_flag_warn, flag); } static const subcommand_t *find_subcommand(const char *name) { @@ -103,10 +105,27 @@ static void print_subcommands(void) { for (const subcommand_t *cmd = subcommands; cmd->name; cmd++) { crprintf(" %s %s\n", cmd->name, cmd->desc); } - crprintf("\n --help Print help text for command.\n"); + crprintf(msg.ant_command_extra); printf("\n"); } +static void print_commands(void **argtable) { + crprintf(msg.ant_help_header); + + print_subcommands(); + crprintf(msg.ant_help_flags); + + print_flags_help(stdout, argtable); + print_flag(stdout, (flag_help_t){ .l = "verbose", .g = "enable verbose output" }); + print_flag(stdout, (flag_help_t){ .l = "no-color", .g = "disable colored output" }); +} + +static int find_argv_token_index(int argc, char **argv, const char *token) { + if (!token) return -1; + for (int i = 1; i < argc; i++) if (argv[i] == token) return i; + return -1; +} + static char *read_stdin(size_t *len) { size_t cap = 4096; *len = 0; @@ -187,7 +206,7 @@ static int execute_module(struct js *js, const char *filename) { buffer = esm_fetch_url(filename, &len, &error); if (!buffer) { - crfprintf(stderr, "{error}: Could not fetch \"%s\": %s\n", filename, error ? error : "unknown error"); + crfprintf(stderr, msg.failed_to_fetch, filename, error ? error : "unknown error"); free(error); return EXIT_FAILURE; } @@ -195,7 +214,7 @@ static int execute_module(struct js *js, const char *filename) { } else { buffer = read_file(filename, &len); if (!buffer) { - crfprintf(stderr, "{error}: Module not found: \"%s\"\n", filename); + crfprintf(stderr, msg.module_not_found, filename); return EXIT_FAILURE; } @@ -213,7 +232,7 @@ static int execute_module(struct js *js, const char *filename) { if (is_typescript_file(filename)) { int result = OXC_strip_types(buffer, filename, buffer, len + 1); if (result < 0) { - crfprintf(stderr, "{error}: Type stripping failed (%d)\n", result); + crfprintf(stderr, msg.type_strip_failed, result); free(buffer); return EXIT_FAILURE; } @@ -238,7 +257,8 @@ static int execute_module(struct js *js, const char *filename) { } int main(int argc, char *argv[]) { - int filtered_argc = 0; + int filtered_argc = 0; int original_argc = argc; + char **original_argv = argv; const char *binary_name = strrchr(argv[0], '/'); binary_name = binary_name ? binary_name + 1 : argv[0]; @@ -266,71 +286,45 @@ int main(int argc, char *argv[]) { else filtered_argv[filtered_argc++] = argv[i]; } - if (filtered_argc >= 2 && filtered_argv[1][0] != '-') { - const subcommand_t *cmd = find_subcommand(filtered_argv[1]); - if (cmd) { - int exitcode = cmd->fn(filtered_argc - 1, filtered_argv + 1); - free(filtered_argv); - return exitcode; - } - - if (pkg_script_exists("package.json", filtered_argv[1])) { - int exitcode = pkg_cmd_run(filtered_argc, filtered_argv); - free(filtered_argv); - return exitcode; - } - } - argc = filtered_argc; argv = filtered_argv; - struct arg_str *eval = arg_str0("e", "eval", "