← Commit history
feat(apm): rename forgepack to apm; npm-style ash.pkg fields, dev-dependencies, libraries and scripts; add apm run
lennoxrose committed on Oct 18, 2026 at 01:13 UTC
34 files changed+479−262
.gitignoremodified
+1−1@@ -2,7 +2,7 @@22
*.o33
/bin/44
5
-# forgepack-installed dependencies (see ideas/ash_modules.md) -- fetched,5
+# apm-installed dependencies (see ideas/ash_modules.md) -- fetched,66
# never hand-edited, same spirit as node_modules/77
@ash-modules/88
CHANGELOG.mdmodified
+6−0@@ -1,3 +1,9 @@1
+## 2026-10-18 (apm: the package manager, richer ash.pkg)2
+- **`forgepack` is now `apm`** ("Ash package manager"): folder `hearth/apm`, binary `bin/apm`, `make @apm`. Older changelog entries keep the old name.3
+- **`ash.pkg` carries npm-style metadata**: `name`, `version`, `description`, `author`, `license`, `homepage`, `repository`, `bugs`, `main` (entry class) and `keywords`, plus sections `[dependencies]`, `[dev-dependencies]`, `[libraries]` (external native libraries, recorded and listed only; `ashvm`/`kiln` do not consume them yet) and `[scripts]` (build instructions). Unknown sections warn; unknown header keys are ignored; files written before this change still load.4
+- New commands: `apm run <script>` (runs a `[scripts]` entry with `sh -c` from the project directory; `apm build` and `apm test` are shorthands), `apm add ... --dev`. `apm install` installs dev-dependencies too; `apm list` shows metadata, installed state, libraries and scripts. Only the project's own scripts run, never a dependency's.5
+- Smoke test covers a full-manifest round trip through `apm add`, `--dev`, `list`, `build` and `run`.6
+17
## 2026-10-18 (standard library is optional: ash-stdlib)28
- **`hearth/front/stdlib` is gone.** `ash.util`, `ash.io`, `ash.json`, `ash.math`, `ash.time`, `ash.cli`, `ash.test`, `ash.concurrent`, `ash.net`, `ash.web`, `ash.game` and `ash.desktop` moved to the separate `ash-stdlib` package (`../ash-stdlib`, with an `ash.pkg` and README), to be published as its own repository and installed with forgepack.39
- **Core types are compiled in.** `ash.lang`, `ash.lang.function` and `ash.util.Iterator` (the for-each cursor) are the types the type checker and lowering name directly; they now ship inside `libashfront.a` as C string data (`front/src/jcore`), looked up before the source roots and parsed only when reached. Nothing is read from disk for them, so programs that only use `ash.lang` run with no stdlib installed. They are still Ash source inside those strings: a native class table built directly in C would be a larger rewrite of the checker/lowering and was not done.Makefilemodified
+2−2@@ -1,6 +1,6 @@11
# Ash - Makefile2
-# Every project lives in hearth/ (ashvm, kiln, forgepack, pyre, front) and builds into the root bin/.3
-# `make` makes all subprojects, such as ashvm, kiln, forgepack, etc.2
+# Every project lives in hearth/ (ashvm, kiln, apm, pyre, front) and builds into the root bin/.3
+# `make` makes all subprojects, such as ashvm, kiln, apm, etc.44
# `make clean` cleans all subprojects.55
# `make @<subproject>` cleans and builds only that subproject, e.g. `make @ashvm`66
README.mdmodified
+45−2@@ -48,7 +48,7 @@ bin/kiln Main.ash -o main --target=windows --link=shared --wrap-int4848
The standard library is not built into the compiler. The compiler only contains the types it cannot work4949
without (`ash.lang`, the functional interfaces, `ash.util.Iterator`); `ash.util` (collections, `Optional`,5050
`Stream`), `ash.io`, `ash.json`, `ash.math`, `ash.time`, `ash.cli`, `ash.test` and the model packages live51
-in the separate `ash-stdlib` package. Install it with forgepack (`forgepack add ash-stdlib github:<user>/ash-stdlib`,51
+in the separate `ash-stdlib` package. Install it with apm (`apm add github:<user>/ash-stdlib`,5252
which lands in `./@ash-modules/ash-stdlib` and is found automatically), or set5353
`ASH_PATH=/path/to/ash-stdlib/src/main/ash`. Without it, programs that only use `ash.lang` still work and5454
anything else reports a `cannot find type` error that names the package.@@ -107,6 +107,49 @@ roughly the ceiling for anything short of a full optimizing native compiler.107107
- `kiln`'s heap grows as needed (64MB at a time, `brk` on Linux / `VirtualAlloc` on Windows) rather than crashing at a fixed size, but nothing is freed until the process exits. `ashvm` has an opt-in collector (`make GC=1`).108108
- `kiln`'s Windows output is checked without a Windows machine: `hearth/kiln/tests/scripts/windows_emulated_check.sh` runs every test program as a PE under the Unicorn x86-64 emulator (kernel32 faked over the real filesystem) and compares it with the Linux build. `hearth/kiln/tests/scripts/windows_wine_check.sh` does the same under Wine (an independent `kernel32`). Neither is Windows itself.109109
110
+## Package manager (`apm`)111
+112
+A project is described by `ash.pkg`, a flat `key: value` header plus `[section]` blocks (like `package.json`):113
+114
+```115
+name: my-app116
+version: 1.2.0117
+description: What it does118
+author: Ada <ada@example.com>119
+license: MIT120
+homepage: https://example.com/my-app121
+repository: github:ada/my-app122
+bugs: https://example.com/my-app/issues123
+main: demo.Main124
+keywords: ash, cli125
+126
+[dependencies]127
+ash-stdlib: github:<user>/ash-stdlib@v0.1.0128
+129
+[dev-dependencies]130
+test-kit: github:<user>/test-kit131
+132
+[libraries]133
+m: system134
+vulkan: pkg-config:vulkan135
+136
+[scripts]137
+build: ashvm check src/main/ash/demo/Main.ash138
+test: ashvm run src/test/ash/AllTests.ash139
+```140
+141
+```bash142
+apm init # create ash.pkg143
+apm add github:user/repo[@ref] [--as name] [--dev]144
+apm install # fetch [dependencies] and [dev-dependencies] into ./@ash-modules/145
+apm list # metadata, dependencies (and whether installed), libraries, scripts146
+apm run <script> # run a [scripts] entry with sh -c; `apm build` / `apm test` are shorthands147
+```148
+149
+`[libraries]` records the external native libraries a package links; apm stores and lists them, but150
+`ashvm` and `kiln` do not consume them yet. Scripts run with the same trust as `npm run`: only the151
+project's own `ash.pkg` is consulted, never an installed dependency's.152
+110153
## Project structure111154
112155
Every project lives in `hearth/` and builds into `bin/`; the repository root only holds@@ -118,7 +161,7 @@ hearth/front/ the only frontend: lexer, parser, AST, type checker, lowering,118161
Built as libashfront.a, linked by ashvm and kiln. There is no front-end executable.119162
hearth/ashvm/ the VM engine: compiler (IR -> bytecode), value, vm, builtins, diagnostics, app (CLI + REPL)120163
hearth/kiln/ the native compiler: IR -> machine code -> ELF/PE (no VM, no libc)121
-hearth/forgepack/ package manager: forgepack init/add/install/list, dependencies fetched from GitHub164
+hearth/apm/ the Ash package manager: apm init/add/install/list/run, dependencies fetched from GitHub122165
into a project-local @ash-modules/ folder; point ASH_PATH at an installed source root123166
to compile against it (see ideas/ash_modules.md)124167
hearth/pyre/ GPU/CPU compute backends (C ABI)hearth/forgepack/Makefile → hearth/apm/Makefilerenamed
+8−8@@ -2,7 +2,7 @@ CC = gcc22
.DEFAULT_GOAL := all33
CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc44
5
-FORGEPACK_SRC = \5
+APM_SRC = \66
src/C/main.c \77
src/C/manifest.c \88
src/C/fsutil.c \@@ -15,8 +15,8 @@ FORGEPACK_SRC = \1515
# src/C/x.c -> src/O/x.o, src/D/x.d1616
obj_of = $(patsubst %.c,%.o,$(subst /C/,/O/,$(1)))1717
dep_of = $(patsubst %.c,%.d,$(subst /C/,/D/,$(1)))18
-FORGEPACK_OBJ = $(foreach s,$(FORGEPACK_SRC),$(call obj_of,$(s)))19
-FORGEPACK_DEP = $(foreach s,$(FORGEPACK_SRC),$(call dep_of,$(s)))18
+APM_OBJ = $(foreach s,$(APM_SRC),$(call obj_of,$(s)))19
+APM_DEP = $(foreach s,$(APM_SRC),$(call dep_of,$(s)))2020
2121
# -MMD -MP: see ashvm/Makefile's and kiln/Makefile's comment -- a shared2222
# header edit must rebuild every .o that includes it, not just the .c@@ -26,17 +26,17 @@ $(call obj_of,$(1)): $(1)2626
@mkdir -p $$(dir $$@) $$(dir $(call dep_of,$(1)))2727
$$(CC) $$(CFLAGS) -MMD -MP -MF $(call dep_of,$(1)) -MT $$@ -c $$< -o $$@2828
endef29
-$(foreach s,$(FORGEPACK_SRC),$(eval $(call compile_rule,$(s))))29
+$(foreach s,$(APM_SRC),$(eval $(call compile_rule,$(s))))3030
31
-BIN = ../../bin/forgepack31
+BIN = ../../bin/apm3232
3333
all: $(BIN)3434
35
-$(BIN): $(FORGEPACK_OBJ)35
+$(BIN): $(APM_OBJ)3636
mkdir -p ../../bin37
- $(CC) $(CFLAGS) -o $(BIN) $(FORGEPACK_OBJ)37
+ $(CC) $(CFLAGS) -o $(BIN) $(APM_OBJ)3838
39
--include $(FORGEPACK_DEP)39
+-include $(APM_DEP)4040
4141
clean:4242
find src -name '*.o' -deletehearth/forgepack/src/C/cmd.c → hearth/apm/src/C/cmd.crenamed
+80−21@@ -1,5 +1,6 @@11
#define _XOPEN_SOURCE 700 // exposes PATH_MAX under -std=c11 (see ashvm/kiln's import_paths.c)22
#include <stdio.h>3
+#include <stdlib.h>34
#include <string.h>45
#include <unistd.h>56
#include <limits.h>@@ -38,22 +39,24 @@ int cmd_init(const char *project_dir, int argc, char **argv) {3839
return 0;3940
}4041
41
-// forgepack add github:user/repo[@ref] [--as <name>]42
+// apm add github:user/repo[@ref] [--as <name>] [--dev]4243
//43
-// GitHub-only for now -- forgepack has no registry/index yet (it will,44
+// GitHub-only for now -- apm has no registry/index yet (it will,4445
// see ideas/ash_modules.md's phased plan; ash-project.org is reserved4546
// for that once there's a real flat index worth serving from it), so a4647
// git source is the only dependency kind that's actually installable4748
// without a server.4849
int cmd_add(const char *project_dir, int argc, char **argv) {4950
if (argc < 1 || !git_fetch_is_github_spec(argv[0])) {50
- ui_err("usage: forgepack add github:<user>/<repo>[@ref] [--as <name>]");51
+ ui_err("usage: apm add github:<user>/<repo>[@ref] [--as <name>] [--dev]");5152
return 1;5253
}5354
const char *source = argv[0];5455
const char *explicit_name = NULL;55
- for (int i = 1; i < argc - 1; i++) {56
- if (strcmp(argv[i], "--as") == 0) explicit_name = argv[i + 1];56
+ int dev = 0;57
+ for (int i = 1; i < argc; i++) {58
+ if (strcmp(argv[i], "--as") == 0 && i + 1 < argc) explicit_name = argv[++i];59
+ else if (strcmp(argv[i], "--dev") == 0) dev = 1;5760
}5861
5962
char name[MANIFEST_NAME_LEN];@@ -70,8 +73,9 @@ int cmd_add(const char *project_dir, int argc, char **argv) {7073
manifest_init_default(&m, project_name);7174
ui_info("no %s here, creating one", MANIFEST_FILE);7275
}73
- if (!manifest_set_dependency(&m, name, source)) {74
- ui_err("dependency table full (max %d)", MANIFEST_MAX_DEPS);76
+ EntryList *target = dev ? &m.dev_deps : &m.deps;77
+ if (!entry_set(target, name, source)) {78
+ ui_err("dependency table full (max %d)", MANIFEST_MAX_ENTRIES);7579
return 1;7680
}7781
if (!manifest_save(project_dir, &m)) return 1;@@ -80,7 +84,7 @@ int cmd_add(const char *project_dir, int argc, char **argv) {8084
snprintf(ash_modules, sizeof(ash_modules), "%s/@ash-modules", project_dir);8185
fsutil_mkdir_p(ash_modules);8286
83
- Dependency *dep = manifest_find_dependency(&m, name);87
+ Dependency *dep = entry_find(target, name);8488
if (!install_dependency(project_dir, dep)) {8589
ui_err("failed to install '%s'", name);8690
return 1;@@ -89,51 +93,106 @@ int cmd_add(const char *project_dir, int argc, char **argv) {8993
return 0;9094
}9195
96
+static int install_all(const char *project_dir, EntryList *list) {97
+ int ok = 1;98
+ for (int i = 0; i < list->count; i++) {99
+ if (!install_dependency(project_dir, &list->items[i])) ok = 0;100
+ }101
+ return ok;102
+}103
+92104
int cmd_install(const char *project_dir, int argc, char **argv) {93105
(void)argc; (void)argv;94106
Manifest m;95107
if (!manifest_load(project_dir, &m)) {96
- ui_err("no %s here -- run 'forgepack init' first", MANIFEST_FILE);108
+ ui_err("no %s here -- run 'apm init' first", MANIFEST_FILE);97109
return 1;98110
}99111
100112
char ash_modules[1024];101113
snprintf(ash_modules, sizeof(ash_modules), "%s/@ash-modules", project_dir);102114
fsutil_mkdir_p(ash_modules);103115
104
- if (m.dep_count == 0) {116
+ if (m.deps.count + m.dev_deps.count == 0) {105117
ui_info("%s has no dependencies", m.name);106118
return 0;107119
}108120
109121
ui_header("%s@%s", m.name, m.version);110
- int ok = 1;111
- for (int i = 0; i < m.dep_count; i++) {112
- if (!install_dependency(project_dir, &m.deps[i])) ok = 0;113
- }122
+ int ok = install_all(project_dir, &m.deps);123
+ if (!install_all(project_dir, &m.dev_deps)) ok = 0;114124
if (ok) ui_ok("done");115125
else ui_err("one or more dependencies failed to install");116126
return ok ? 0 : 1;117127
}118128
129
+static void list_dependencies(const char *project_dir, const char *title, EntryList *list) {130
+ if (list->count == 0) return;131
+ ui_header("%s", title);132
+ for (int i = 0; i < list->count; i++) {133
+ char dest[1024];134
+ snprintf(dest, sizeof(dest), "%s/@ash-modules/%s", project_dir, list->items[i].name);135
+ if (fsutil_dir_exists(dest)) ui_bullet("%s: %s", list->items[i].name, list->items[i].value);136
+ else ui_bullet("%s: %s (not installed)", list->items[i].name, list->items[i].value);137
+ }138
+}139
+140
+static void list_entries(const char *title, EntryList *list) {141
+ if (list->count == 0) return;142
+ ui_header("%s", title);143
+ for (int i = 0; i < list->count; i++) ui_bullet("%s: %s", list->items[i].name, list->items[i].value);144
+}145
+119146
int cmd_list(const char *project_dir, int argc, char **argv) {120147
(void)argc; (void)argv;121148
Manifest m;122149
if (!manifest_load(project_dir, &m)) {123
- ui_err("no %s here -- run 'forgepack init' first", MANIFEST_FILE);150
+ ui_err("no %s here -- run 'apm init' first", MANIFEST_FILE);124151
return 1;125152
}126153
127154
ui_header("%s@%s", m.name, m.version);128
- if (m.dep_count == 0) {155
+ if (m.description[0]) ui_bullet("%s", m.description);156
+ if (m.author[0]) ui_bullet("author: %s", m.author);157
+ if (m.license[0]) ui_bullet("license: %s", m.license);158
+ if (m.main[0]) ui_bullet("main: %s", m.main);159
+ if (m.deps.count + m.dev_deps.count + m.libs.count + m.scripts.count == 0) {129160
ui_bullet("(no dependencies)");130161
return 0;131162
}132
- for (int i = 0; i < m.dep_count; i++) {133
- char dest[1024];134
- snprintf(dest, sizeof(dest), "%s/@ash-modules/%s", project_dir, m.deps[i].name);135
- if (fsutil_dir_exists(dest)) ui_bullet("%s: %s", m.deps[i].name, m.deps[i].value);136
- else ui_bullet("%s: %s (not installed)", m.deps[i].name, m.deps[i].value);163
+ list_dependencies(project_dir, "dependencies", &m.deps);164
+ list_dependencies(project_dir, "dev-dependencies", &m.dev_deps);165
+ list_entries("libraries", &m.libs);166
+ list_entries("scripts", &m.scripts);167
+ return 0;168
+}169
+170
+// apm run <script> runs [scripts] <script> of ash.pkg with `sh -c`, from the project directory.171
+// `apm build` and `apm test` are shorthands for `apm run build` / `apm run test`.172
+// Only the project's own ash.pkg is consulted; scripts of installed dependencies never run.173
+int cmd_run(const char *project_dir, int argc, char **argv) {174
+ Manifest m;175
+ if (!manifest_load(project_dir, &m)) {176
+ ui_err("no %s here -- run 'apm init' first", MANIFEST_FILE);177
+ return 1;178
+ }179
+ if (argc < 1) {180
+ ui_err("usage: apm run <script>");181
+ list_entries("scripts", &m.scripts);182
+ return 1;183
+ }184
+ Entry *script = entry_find(&m.scripts, argv[0]);185
+ if (!script) {186
+ ui_err("no script '%s' in the [scripts] section of %s", argv[0], MANIFEST_FILE);187
+ return 1;188
+ }189
+ ui_info("%s: %s", script->name, script->value);190
+ char cmd[MANIFEST_VALUE_LEN + 1100];191
+ snprintf(cmd, sizeof(cmd), "cd '%s' && %s", project_dir, script->value);192
+ int status = system(cmd);193
+ if (status != 0) {194
+ ui_err("script '%s' failed", script->name);195
+ return 1;137196
}138197
return 0;139198
}hearth/forgepack/src/C/fsutil.c → hearth/apm/src/C/fsutil.crenamed
+1−1@@ -55,7 +55,7 @@ int fsutil_copy_dir(const char *src_dir, const char *dest_dir) {5555
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;5656
// Never copy a nested @ash-modules/ or .git -- a "path:" dependency5757
// installs its OWN source, not whatever it happened to have58
- // installed/cloned locally when you ran `forgepack add`.58
+ // installed/cloned locally when you ran `apm add`.5959
if (strcmp(entry->d_name, "@ash-modules") == 0 || strcmp(entry->d_name, ".git") == 0) continue;6060
6161
char src_path[1024], dest_path[1024];hearth/forgepack/src/C/git_fetch.c → hearth/apm/src/C/git_fetch.crenamed
+2−2@@ -61,7 +61,7 @@ void git_fetch_infer_name(const char *spec, char *out, int out_size) {6161
int git_fetch_github(const char *spec, const char *dest_dir) {6262
char user[128], repo[128], ref[128];6363
if (!parse_github_spec(spec, user, sizeof(user), repo, sizeof(repo), ref, sizeof(ref))) {64
- fprintf(stderr, "forgepack: malformed github spec '%s' (expected github:user/repo or github:user/repo@ref)\n", spec);64
+ fprintf(stderr, "apm: malformed github spec '%s' (expected github:user/repo or github:user/repo@ref)\n", spec);6565
return 0;6666
}6767
@@ -76,7 +76,7 @@ int git_fetch_github(const char *spec, const char *dest_dir) {7676
7777
int rc = system(cmd);7878
if (rc != 0) {79
- fprintf(stderr, "forgepack: 'git clone' failed for %s\n", spec);79
+ fprintf(stderr, "apm: 'git clone' failed for %s\n", spec);8080
return 0;8181
}8282
return 1;hearth/forgepack/src/C/install.c → hearth/apm/src/C/install.crenamed
+1−1@@ -6,7 +6,7 @@66
77
// GitHub-only for now, deliberately -- no registry/index yet (see88
// ideas/ash_modules.md's phased plan), so a git source is the only thing9
-// forgepack can actually resolve without a server. path:/plain-version9
+// apm can actually resolve without a server. path:/plain-version1010
// dependencies are a later addition, not scoped in yet.1111
int install_dependency(const char *project_dir, const Dependency *dep) {1212
char dest[1024];hearth/forgepack/src/C/main.c → hearth/apm/src/C/main.crenamed
+11−5@@ -4,12 +4,13 @@44
#include "H/ui.h"55
66
static void print_usage(void) {7
- printf("forgepack -- package manager for Ash (github: sources only, for now)\n\n");7
+ printf("apm -- the Ash package manager (github: sources only, for now)\n\n");88
printf("usage:\n");9
- printf(" forgepack init\n");10
- printf(" forgepack add github:<user>/<repo>[@ref] [--as <name>]\n");11
- printf(" forgepack install\n");12
- printf(" forgepack list\n");9
+ printf(" apm init\n");10
+ printf(" apm add github:<user>/<repo>[@ref] [--as <name>] [--dev]\n");11
+ printf(" apm install\n");12
+ printf(" apm list\n");13
+ printf(" apm run <script> run a [scripts] entry of ash.pkg (apm build / apm test = run build / run test)\n");1314
}1415
1516
int main(int argc, char **argv) {@@ -24,6 +25,11 @@ int main(int argc, char **argv) {2425
if (strcmp(command, "add") == 0) return cmd_add(project_dir, rest_argc, rest_argv);2526
if (strcmp(command, "install") == 0) return cmd_install(project_dir, rest_argc, rest_argv);2627
if (strcmp(command, "list") == 0) return cmd_list(project_dir, rest_argc, rest_argv);28
+ if (strcmp(command, "run") == 0) return cmd_run(project_dir, rest_argc, rest_argv);29
+ if (strcmp(command, "build") == 0 || strcmp(command, "test") == 0) {30
+ char *script[] = { (char *)command };31
+ return cmd_run(project_dir, 1, script);32
+ }2733
2834
ui_err("unknown command '%s'", command);2935
print_usage();hearth/apm/src/C/manifest.cadded
+128−0@@ -0,0 +1,128 @@1
+#include <stdio.h>2
+#include <stdlib.h>3
+#include <string.h>4
+#include "H/manifest.h"5
+6
+// header keys in file order: where each one lives in Manifest7
+static const struct { const char *key; size_t offset; size_t size; } header_keys[] = {8
+ { "name", offsetof(Manifest, name), sizeof(((Manifest *)0)->name) },9
+ { "version", offsetof(Manifest, version), sizeof(((Manifest *)0)->version) },10
+ { "description", offsetof(Manifest, description), sizeof(((Manifest *)0)->description) },11
+ { "author", offsetof(Manifest, author), sizeof(((Manifest *)0)->author) },12
+ { "license", offsetof(Manifest, license), sizeof(((Manifest *)0)->license) },13
+ { "homepage", offsetof(Manifest, homepage), sizeof(((Manifest *)0)->homepage) },14
+ { "repository", offsetof(Manifest, repository), sizeof(((Manifest *)0)->repository) },15
+ { "bugs", offsetof(Manifest, bugs), sizeof(((Manifest *)0)->bugs) },16
+ { "main", offsetof(Manifest, main), sizeof(((Manifest *)0)->main) },17
+ { "keywords", offsetof(Manifest, keywords), sizeof(((Manifest *)0)->keywords) },18
+};19
+#define HEADER_KEYS ((int)(sizeof(header_keys) / sizeof(header_keys[0])))20
+21
+static const struct { const char *title; size_t offset; } sections[] = {22
+ { "dependencies", offsetof(Manifest, deps) },23
+ { "dev-dependencies", offsetof(Manifest, dev_deps) },24
+ { "libraries", offsetof(Manifest, libs) },25
+ { "scripts", offsetof(Manifest, scripts) },26
+};27
+#define SECTIONS ((int)(sizeof(sections) / sizeof(sections[0])))28
+29
+void manifest_init_default(Manifest *m, const char *project_name) {30
+ memset(m, 0, sizeof(*m));31
+ snprintf(m->name, sizeof(m->name), "%s", project_name);32
+ snprintf(m->version, sizeof(m->version), "0.1.0");33
+}34
+35
+// Trims leading/trailing whitespace in place, returns the trimmed start.36
+static char *trim(char *s) {37
+ while (*s == ' ' || *s == '\t') s++;38
+ size_t len = strlen(s);39
+ while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r' || s[len - 1] == ' ' || s[len - 1] == '\t')) {40
+ s[--len] = '\0';41
+ }42
+ return s;43
+}44
+45
+int manifest_load(const char *dir, Manifest *out) {46
+ char path[1024];47
+ snprintf(path, sizeof(path), "%s/%s", dir, MANIFEST_FILE);48
+ FILE *f = fopen(path, "r");49
+ if (!f) return 0;50
+51
+ memset(out, 0, sizeof(*out));52
+ EntryList *section = NULL; // NULL while in the header53
+ char line[1024];54
+ while (fgets(line, sizeof(line), f)) {55
+ char *l = trim(line);56
+ if (l[0] == '\0' || l[0] == '#') continue;57
+ if (l[0] == '[') {58
+ section = NULL;59
+ for (int i = 0; i < SECTIONS; i++) {60
+ char title[64];61
+ snprintf(title, sizeof(title), "[%s]", sections[i].title);62
+ if (strcmp(l, title) == 0) section = (EntryList *)((char *)out + sections[i].offset);63
+ }64
+ if (!section) fprintf(stderr, "apm: unknown section '%s' in %s, ignoring it\n", l, MANIFEST_FILE);65
+ continue;66
+ }67
+68
+ char *colon = strchr(l, ':');69
+ if (!colon) continue; // ignore malformed lines rather than aborting the whole load70
+ *colon = '\0';71
+ char *key = trim(l);72
+ char *value = trim(colon + 1);73
+74
+ if (section) {75
+ if (!entry_set(section, key, value))76
+ fprintf(stderr, "apm: too many entries (max %d), ignoring '%s'\n", MANIFEST_MAX_ENTRIES, key);77
+ continue;78
+ }79
+ for (int i = 0; i < HEADER_KEYS; i++) {80
+ if (strcmp(key, header_keys[i].key) == 0) snprintf((char *)out + header_keys[i].offset, header_keys[i].size, "%s", value);81
+ }82
+ }83
+ fclose(f);84
+ return 1;85
+}86
+87
+int manifest_save(const char *dir, const Manifest *m) {88
+ char path[1024];89
+ snprintf(path, sizeof(path), "%s/%s", dir, MANIFEST_FILE);90
+ FILE *f = fopen(path, "w");91
+ if (!f) {92
+ fprintf(stderr, "apm: could not write %s\n", path);93
+ return 0;94
+ }95
+96
+ for (int i = 0; i < HEADER_KEYS; i++) {97
+ const char *value = (const char *)m + header_keys[i].offset;98
+ if (value[0]) fprintf(f, "%s: %s\n", header_keys[i].key, value);99
+ }100
+ for (int i = 0; i < SECTIONS; i++) {101
+ const EntryList *list = (const EntryList *)((const char *)m + sections[i].offset);102
+ if (list->count == 0) continue;103
+ fprintf(f, "\n[%s]\n", sections[i].title);104
+ for (int j = 0; j < list->count; j++) fprintf(f, "%s: %s\n", list->items[j].name, list->items[j].value);105
+ }106
+ fclose(f);107
+ return 1;108
+}109
+110
+int entry_set(EntryList *list, const char *name, const char *value) {111
+ Entry *existing = entry_find(list, name);112
+ if (existing) {113
+ snprintf(existing->value, sizeof(existing->value), "%s", value);114
+ return 1;115
+ }116
+ if (list->count >= MANIFEST_MAX_ENTRIES) return 0;117
+ Entry *e = &list->items[list->count++];118
+ snprintf(e->name, sizeof(e->name), "%s", name);119
+ snprintf(e->value, sizeof(e->value), "%s", value);120
+ return 1;121
+}122
+123
+Entry *entry_find(EntryList *list, const char *name) {124
+ for (int i = 0; i < list->count; i++) {125
+ if (strcmp(list->items[i].name, name) == 0) return &list->items[i];126
+ }127
+ return NULL;128
+}hearth/forgepack/src/C/ui.c → hearth/apm/src/C/ui.crenamed
+0−0No content changes. View the file on GitHub
hearth/forgepack/src/D/cmd.d → hearth/apm/src/D/cmd.drenamed
+0−0No content changes. View the file on GitHub
hearth/forgepack/src/D/fsutil.d → hearth/apm/src/D/fsutil.drenamed
+0−0No content changes. View the file on GitHub
hearth/forgepack/src/D/git_fetch.d → hearth/apm/src/D/git_fetch.drenamed
+0−0No content changes. View the file on GitHub
hearth/forgepack/src/D/install.d → hearth/apm/src/D/install.drenamed
+0−0No content changes. View the file on GitHub
hearth/forgepack/src/D/main.d → hearth/apm/src/D/main.drenamed
+0−0No content changes. View the file on GitHub
hearth/forgepack/src/D/manifest.d → hearth/apm/src/D/manifest.drenamed
+0−0No content changes. View the file on GitHub
hearth/forgepack/src/D/ui.d → hearth/apm/src/D/ui.drenamed
+0−0No content changes. View the file on GitHub
hearth/forgepack/src/H/cmd.h → hearth/apm/src/H/cmd.hrenamed
+4−3@@ -1,12 +1,13 @@1
-#ifndef FORGEPACK_CMD_H2
-#define FORGEPACK_CMD_H1
+#ifndef APM_CMD_H2
+#define APM_CMD_H33
44
// Each command takes the project directory ("." almost always, since5
-// forgepack is always run from a project's root) and the remaining argv5
+// apm is always run from a project's root) and the remaining argv66
// past the subcommand name. Returns an exit code.77
int cmd_init(const char *project_dir, int argc, char **argv);88
int cmd_add(const char *project_dir, int argc, char **argv);99
int cmd_install(const char *project_dir, int argc, char **argv);1010
int cmd_list(const char *project_dir, int argc, char **argv);11
+int cmd_run(const char *project_dir, int argc, char **argv);1112
1213
#endifhearth/forgepack/src/H/fsutil.h → hearth/apm/src/H/fsutil.hrenamed
+2−2@@ -1,5 +1,5 @@1
-#ifndef FORGEPACK_FSUTIL_H2
-#define FORGEPACK_FSUTIL_H1
+#ifndef APM_FSUTIL_H2
+#define APM_FSUTIL_H33
44
int fsutil_dir_exists(const char *path);55
hearth/forgepack/src/H/git_fetch.h → hearth/apm/src/H/git_fetch.hrenamed
+4−4@@ -1,18 +1,18 @@1
-#ifndef FORGEPACK_GIT_FETCH_H2
-#define FORGEPACK_GIT_FETCH_H1
+#ifndef APM_GIT_FETCH_H2
+#define APM_GIT_FETCH_H33
44
// Recognizes "github:user/repo" and "github:user/repo@ref".55
int git_fetch_is_github_spec(const char *value);66
77
// Clones the given "github:..." spec into dest_dir (shells out to the8
-// system `git` binary -- forgepack has no git implementation of its own,8
+// system `git` binary -- apm has no git implementation of its own,99
// same as every other package manager that supports a git source).1010
// user/repo/ref are validated against a strict charset before ever1111
// touching a shell command string, so a crafted dependency spec can't1212
// inject anything. Returns 1 on success.1313
int git_fetch_github(const char *spec, const char *dest_dir);1414
15
-// Derives the package name forgepack should use when none was given15
+// Derives the package name apm should use when none was given1616
// explicitly ("add github:user/repo" -> "repo").1717
void git_fetch_infer_name(const char *spec, char *out, int out_size);1818
hearth/forgepack/src/H/install.h → hearth/apm/src/H/install.hrenamed
+2−2@@ -1,5 +1,5 @@1
-#ifndef FORGEPACK_INSTALL_H2
-#define FORGEPACK_INSTALL_H1
+#ifndef APM_INSTALL_H2
+#define APM_INSTALL_H33
#include "H/manifest.h"44
55
// Installs one dependency into <project_dir>/@ash-modules/<dep->name>/ ifhearth/apm/src/H/manifest.hadded
+76−0@@ -0,0 +1,76 @@1
+#ifndef APM_MANIFEST_H2
+#define APM_MANIFEST_H3
+4
+#include <stddef.h>5
+6
+#define MANIFEST_MAX_ENTRIES 647
+#define MANIFEST_NAME_LEN 1288
+#define MANIFEST_VALUE_LEN 2569
+#define MANIFEST_FILE "ash.pkg"10
+11
+// One "name: value" line of a section. In [dependencies] / [dev-dependencies] the value is a raw12
+// source spec: "github:user/repo" or "github:user/repo@ref" (ref = tag or branch). No registry yet13
+// (see ideas/ash_modules.md), so plain versions and local "path:" dependencies aren't supported.14
+typedef struct {15
+ char name[MANIFEST_NAME_LEN];16
+ char value[MANIFEST_VALUE_LEN];17
+} Entry;18
+typedef Entry Dependency;19
+20
+typedef struct {21
+ Entry items[MANIFEST_MAX_ENTRIES];22
+ int count;23
+} EntryList;24
+25
+// ash.pkg is a flat "key: value" header followed by "[section]" blocks of "name: value" lines26
+// (blank lines and '#' comments are ignored):27
+//28
+// name: my-app name, version identity29
+// version: 1.2.030
+// description: What it does description, author, license (SPDX id), homepage,31
+// author: Ada <ada@example.com> repository, bugs metadata, as in package.json32
+// license: MIT33
+// main: demo.Main main entry class (qualified) for `apm run start`-style tooling34
+// keywords: ash, cli keywords comma-separated35
+//36
+// [dependencies] packages fetched into ./@ash-modules/<name>37
+// ash-stdlib: github:user/ash-stdlib@v0.1.038
+// [dev-dependencies] same, only needed to build and test39
+// [libraries] external native libraries the package links: name: how to find it40
+// m: system (recorded and listed; not yet consumed by ashvm or kiln)41
+// [scripts] named shell commands, run with `apm run <name>`42
+// build: ashvm check src/main/ash/demo/Main.ash43
+typedef struct {44
+ char name[MANIFEST_NAME_LEN];45
+ char version[32];46
+ char description[MANIFEST_VALUE_LEN];47
+ char author[MANIFEST_NAME_LEN];48
+ char license[64];49
+ char homepage[MANIFEST_VALUE_LEN];50
+ char repository[MANIFEST_VALUE_LEN];51
+ char bugs[MANIFEST_VALUE_LEN];52
+ char main[MANIFEST_NAME_LEN];53
+ char keywords[MANIFEST_VALUE_LEN];54
+ EntryList deps;55
+ EntryList dev_deps;56
+ EntryList libs;57
+ EntryList scripts;58
+} Manifest;59
+60
+void manifest_init_default(Manifest *m, const char *project_name);61
+62
+// Reads ash.pkg from `dir` (a directory path, not the file itself).63
+// Returns 1 on success, 0 if the file doesn't exist.64
+int manifest_load(const char *dir, Manifest *out);65
+66
+// Writes ash.pkg into `dir`, overwriting any existing one.67
+int manifest_save(const char *dir, const Manifest *m);68
+69
+// Adds a new entry or overwrites an existing one with the same name.70
+// Returns 0 if the table is full.71
+int entry_set(EntryList *list, const char *name, const char *value);72
+73
+// Returns NULL if no entry with that name exists.74
+Entry *entry_find(EntryList *list, const char *name);75
+76
+#endifhearth/forgepack/src/H/ui.h → hearth/apm/src/H/ui.hrenamed
+2−2@@ -1,5 +1,5 @@1
-#ifndef FORGEPACK_UI_H2
-#define FORGEPACK_UI_H1
+#ifndef APM_UI_H2
+#define APM_UI_H33
44
// Plain Unicode symbols (box-drawing/dingbats block, not emoji -- no55
// color-font fallback needed, renders in any monospace terminal font) andhearth/forgepack/src/O/.gitkeep → hearth/apm/src/O/.gitkeeprenamed
+0−0No content changes. View the file on GitHub
hearth/apm/tests/scripts/smoke_test.shadded
+77−0@@ -0,0 +1,77 @@1
+#!/usr/bin/env bash2
+# apm/tests/scripts/smoke_test.sh -- exercises the CLI end to end3
+# (init/add/install/list/run) against a throwaway scratch directory. Not a4
+# .ash regression suite like ashvm/kiln have (apm doesn't run Ash5
+# programs, it manages their dependencies) -- this is the equivalent6
+# check for this project: does the real CLI, talking to the real7
+# filesystem and the real `git`, do what it says.8
+set -euo pipefail9
+cd "$(dirname "$0")/../.." # -> apm/10
+11
+make -s12
+13
+BIN="$(cd ../../bin && pwd)/apm" # absolute -- the script cd's into a scratch dir below14
+SCRATCH=$(mktemp -d)15
+trap 'rm -rf "$SCRATCH"' EXIT16
+17
+fail() { echo "FAIL: $1"; exit 1; }18
+19
+cd "$SCRATCH"20
+21
+"$BIN" init > /dev/null || fail "init"22
+[ -f ash.pkg ] || fail "ash.pkg not created"23
+grep -q "^name:" ash.pkg || fail "ash.pkg missing name"24
+25
+"$BIN" add github:octocat/Hello-World > /dev/null || fail "add"26
+grep -q "Hello-World: github:octocat/Hello-World" ash.pkg || fail "dependency not recorded"27
+[ -d @ash-modules/Hello-World ] || fail "dependency not installed"28
+29
+rm -rf @ash-modules30
+"$BIN" install > /dev/null || fail "install"31
+[ -d @ash-modules/Hello-World ] || fail "install did not restore the dependency"32
+33
+"$BIN" install > /dev/null || fail "re-install (idempotency)"34
+35
+"$BIN" list | grep -q "Hello-World" || fail "list didn't show the dependency"36
+37
+# full manifest: every field and section survives a load/save round trip (add rewrites ash.pkg)38
+cat > ash.pkg <<'PKG'39
+name: demo40
+version: 1.2.041
+description: A demo package42
+author: Ada <ada@example.com>43
+license: MIT44
+homepage: https://example.com/demo45
+repository: github:ada/demo46
+bugs: https://example.com/demo/issues47
+main: demo.Main48
+keywords: ash, demo49
+50
+[dependencies]51
+Hello-World: github:octocat/Hello-World52
+53
+[libraries]54
+m: system55
+vulkan: pkg-config:vulkan56
+57
+[scripts]58
+build: echo building > built.txt59
+test: echo testing60
+PKG61
+"$BIN" add github:octocat/Spoon-Knife --dev > /dev/null || fail "add --dev"62
+for want in "^description: A demo package" "^author: Ada <ada@example.com>" "^license: MIT" "^main: demo.Main" \63
+ "^keywords: ash, demo" "^m: system" "^vulkan: pkg-config:vulkan" "^build: echo building" \64
+ "^\[dev-dependencies\]" "^Spoon-Knife: github:octocat/Spoon-Knife"; do65
+ grep -q -- "$want" ash.pkg || fail "ash.pkg lost '$want' after add --dev"66
+done67
+[ -d @ash-modules/Spoon-Knife ] || fail "dev dependency not installed"68
+"$BIN" list | grep -q "vulkan: pkg-config:vulkan" || fail "list didn't show libraries"69
+70
+"$BIN" build > /dev/null || fail "apm build"71
+[ "$(cat built.txt)" == "building" ] || fail "build script did not run"72
+out=$("$BIN" run test) && grep -q "^testing$" <<<"$out" || fail "apm run test"73
+if "$BIN" run nonexistent > /dev/null 2>&1; then fail "unknown script should exit non-zero"; fi74
+75
+if "$BIN" bogus-command > /dev/null 2>&1; then fail "unknown command should exit non-zero"; fi76
+77
+echo "apm smoke test: OK"hearth/ashvm/src/compiler/C/stmt.cmodified
+3−3@@ -81,7 +81,7 @@ static void local_statement(void) {8181
int len = previous.length;8282
expect(TOKEN_EQUAL, "expected '=' after variable name");8383
84
- // Bug (found while writing the first real forgepack module, a JSON84
+ // Bug (found while writing the first real apm module, a JSON8585
// codec -- any imported function with a `local` inside a loop failed8686
// to compile): try_declare_import_constant() has no way to tell "a8787
// top-level local in an imported file" from "an ordinary local deep@@ -182,7 +182,7 @@ static void identifier_statement(void) {182182
// discarding the result) rather than nested in an expression183183
// (`local x = json.load(...);`, where expr.c's primary() already184184
// handles this same `ident.member(...)` shape) -- found missing185
- // while writing the first real forgepack module, whose whole point185
+ // while writing the first real apm module, whose whole point186186
// (`json.save(path, value);`) is exactly this shape.187187
if (current.type == TOKEN_DOT) {188188
advance_token();@@ -226,7 +226,7 @@ static void identifier_statement(void) {226226
return;227227
}228228
// Bare reassignment (`x = expr;`, no `local`) on an already-declared229
- // variable -- found missing while writing the first real forgepack229
+ // variable -- found missing while writing the first real apm230230
// module (a JSON codec, which reassigns a loop position variable231231
// throughout). kiln already supports this (its local_statement()232232
// reassigns an existing slot in place rather than requiring `local`hearth/forgepack/src/C/manifest.cremoved
+0−98@@ -1,98 +0,0 @@1
-#include <stdio.h>2
-#include <stdlib.h>3
-#include <string.h>4
-#include "H/manifest.h"5
-6
-void manifest_init_default(Manifest *m, const char *project_name) {7
- memset(m, 0, sizeof(*m));8
- snprintf(m->name, sizeof(m->name), "%s", project_name);9
- snprintf(m->version, sizeof(m->version), "0.1.0");10
-}11
-12
-// Trims leading/trailing whitespace in place, returns the trimmed start.13
-static char *trim(char *s) {14
- while (*s == ' ' || *s == '\t') s++;15
- size_t len = strlen(s);16
- while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r' || s[len - 1] == ' ' || s[len - 1] == '\t')) {17
- s[--len] = '\0';18
- }19
- return s;20
-}21
-22
-int manifest_load(const char *dir, Manifest *out) {23
- char path[1024];24
- snprintf(path, sizeof(path), "%s/%s", dir, MANIFEST_FILE);25
- FILE *f = fopen(path, "r");26
- if (!f) return 0;27
-28
- memset(out, 0, sizeof(*out));29
- int in_deps = 0;30
- char line[512];31
- while (fgets(line, sizeof(line), f)) {32
- char *l = trim(line);33
- if (l[0] == '\0' || l[0] == '#') continue;34
- if (strcmp(l, "[dependencies]") == 0) { in_deps = 1; continue; }35
-36
- char *colon = strchr(l, ':');37
- if (!colon) continue; // ignore malformed lines rather than aborting the whole load38
- *colon = '\0';39
- char *key = trim(l);40
- char *value = trim(colon + 1);41
-42
- if (!in_deps) {43
- if (strcmp(key, "name") == 0) snprintf(out->name, sizeof(out->name), "%s", value);44
- else if (strcmp(key, "version") == 0) snprintf(out->version, sizeof(out->version), "%s", value);45
- } else {46
- if (out->dep_count >= MANIFEST_MAX_DEPS) {47
- fprintf(stderr, "forgepack: too many dependencies (max %d), ignoring '%s'\n", MANIFEST_MAX_DEPS, key);48
- continue;49
- }50
- Dependency *d = &out->deps[out->dep_count++];51
- snprintf(d->name, sizeof(d->name), "%s", key);52
- snprintf(d->value, sizeof(d->value), "%s", value);53
- }54
- }55
- fclose(f);56
- return 1;57
-}58
-59
-int manifest_save(const char *dir, const Manifest *m) {60
- char path[1024];61
- snprintf(path, sizeof(path), "%s/%s", dir, MANIFEST_FILE);62
- FILE *f = fopen(path, "w");63
- if (!f) {64
- fprintf(stderr, "forgepack: could not write %s\n", path);65
- return 0;66
- }67
-68
- fprintf(f, "name: %s\n", m->name);69
- fprintf(f, "version: %s\n", m->version);70
- if (m->dep_count > 0) {71
- fprintf(f, "\n[dependencies]\n");72
- for (int i = 0; i < m->dep_count; i++) {73
- fprintf(f, "%s: %s\n", m->deps[i].name, m->deps[i].value);74
- }75
- }76
- fclose(f);77
- return 1;78
-}79
-80
-int manifest_set_dependency(Manifest *m, const char *name, const char *value) {81
- Dependency *existing = manifest_find_dependency(m, name);82
- if (existing) {83
- snprintf(existing->value, sizeof(existing->value), "%s", value);84
- return 1;85
- }86
- if (m->dep_count >= MANIFEST_MAX_DEPS) return 0;87
- Dependency *d = &m->deps[m->dep_count++];88
- snprintf(d->name, sizeof(d->name), "%s", name);89
- snprintf(d->value, sizeof(d->value), "%s", value);90
- return 1;91
-}92
-93
-Dependency *manifest_find_dependency(Manifest *m, const char *name) {94
- for (int i = 0; i < m->dep_count; i++) {95
- if (strcmp(m->deps[i].name, name) == 0) return &m->deps[i];96
- }97
- return NULL;98
-}hearth/forgepack/src/H/manifest.hremoved
+0−42@@ -1,42 +0,0 @@1
-#ifndef FORGEPACK_MANIFEST_H2
-#define FORGEPACK_MANIFEST_H3
-4
-#define MANIFEST_MAX_DEPS 645
-#define MANIFEST_NAME_LEN 1286
-#define MANIFEST_VALUE_LEN 2567
-#define MANIFEST_FILE "ash.pkg"8
-9
-// A dependency's value is a raw spec string. GitHub-only for now --10
-// "github:user/repo" or "github:user/repo@ref" (ref = tag or branch).11
-// No registry yet (see ideas/ash_modules.md), so a plain version/range12
-// or a local "path:" dependency aren't supported yet either.13
-typedef struct {14
- char name[MANIFEST_NAME_LEN];15
- char value[MANIFEST_VALUE_LEN];16
-} Dependency;17
-18
-typedef struct {19
- char name[MANIFEST_NAME_LEN];20
- char version[32];21
- Dependency deps[MANIFEST_MAX_DEPS];22
- int dep_count;23
-} Manifest;24
-25
-void manifest_init_default(Manifest *m, const char *project_name);26
-27
-// Reads ash.pkg from `dir` (a directory path, not the file itself).28
-// Returns 1 on success, 0 if the file doesn't exist or is malformed29
-// (a message is printed to stderr either way).30
-int manifest_load(const char *dir, Manifest *out);31
-32
-// Writes ash.pkg into `dir`, overwriting any existing one.33
-int manifest_save(const char *dir, const Manifest *m);34
-35
-// Adds a new dependency or overwrites an existing one with the same name.36
-// Returns 0 if the table is full.37
-int manifest_set_dependency(Manifest *m, const char *name, const char *value);38
-39
-// Returns NULL if no dependency with that name exists.40
-Dependency *manifest_find_dependency(Manifest *m, const char *name);41
-42
-#endifhearth/forgepack/tests/scripts/smoke_test.shremoved
+0−39@@ -1,39 +0,0 @@1
-#!/usr/bin/env bash2
-# forgepack/tests/scripts/smoke_test.sh -- exercises the CLI end to end3
-# (init/add/install/list) against a throwaway scratch directory. Not a4
-# .ash regression suite like ashvm/kiln have (forgepack doesn't run Ash5
-# programs, it manages their dependencies) -- this is the equivalent6
-# check for this project: does the real CLI, talking to the real7
-# filesystem and the real `git`, do what it says.8
-set -euo pipefail9
-cd "$(dirname "$0")/../.." # -> forgepack/10
-11
-make -s12
-13
-BIN="$(cd ../../bin && pwd)/forgepack" # absolute -- the script cd's into a scratch dir below14
-SCRATCH=$(mktemp -d)15
-trap 'rm -rf "$SCRATCH"' EXIT16
-17
-fail() { echo "FAIL: $1"; exit 1; }18
-19
-cd "$SCRATCH"20
-21
-"$BIN" init > /dev/null || fail "init"22
-[ -f ash.pkg ] || fail "ash.pkg not created"23
-grep -q "^name:" ash.pkg || fail "ash.pkg missing name"24
-25
-"$BIN" add github:octocat/Hello-World > /dev/null || fail "add"26
-grep -q "Hello-World: github:octocat/Hello-World" ash.pkg || fail "dependency not recorded"27
-[ -d @ash-modules/Hello-World ] || fail "dependency not installed"28
-29
-rm -rf @ash-modules30
-"$BIN" install > /dev/null || fail "install"31
-[ -d @ash-modules/Hello-World ] || fail "install did not restore the dependency"32
-33
-"$BIN" install > /dev/null || fail "re-install (idempotency)"34
-35
-"$BIN" list | grep -q "Hello-World" || fail "list didn't show the dependency"36
-37
-if "$BIN" bogus-command > /dev/null 2>&1; then fail "unknown command should exit non-zero"; fi38
-39
-echo "forgepack smoke test: OK"hearth/front/src/jtypes/C/classes.cmodified
+2−2@@ -30,7 +30,7 @@ void jt_add_root(const char *dir) { j_list_push(&roots, j_strdup(dir)); }3030
3131
static int dir_exists(const char *d) { return access(d, F_OK) == 0; }3232
33
-/* Packages installed by forgepack: each child C of <project>/@ash-modules and <project>/ash.libs is a33
+/* Packages installed by apm: each child C of <project>/@ash-modules and <project>/ash.libs is a3434
* source root, at C/src/main/ash when that exists and at C itself otherwise. */3535
static void add_package_roots(const char *project, const char *folder) {3636
char *dir = j_fmt("%s/%s", project, folder);@@ -304,7 +304,7 @@ JType *jt_resolve_type(Node *tn, JClass *ctx, JMethod *mctx, int report) {304304
}305305
if (!c) {306306
if (report) {307
- if (!jt_find_class("ash.util.List")) cerr(tn, "cannot find type '%s' (the standard library is the optional ash-stdlib package: install it with forgepack or point ASH_PATH at its src/main/ash)", tn->name);307
+ if (!jt_find_class("ash.util.List")) cerr(tn, "cannot find type '%s' (the standard library is the optional ash-stdlib package: install it with apm or point ASH_PATH at its src/main/ash)", tn->name);308308
else cerr(tn, "cannot find type '%s'", tn->name);309309
}310310
return ty_error();hearth/kiln/src/parser/C/core/parser.cmodified
+2−2@@ -67,7 +67,7 @@ static void local_statement(void) {6767
6868
const char *ns; int ns_len;6969
get_import_namespace(&ns, &ns_len);70
- // Bug (found while writing the first real forgepack module, a JSON70
+ // Bug (found while writing the first real apm module, a JSON7171
// codec -- any imported function with a `local` inside a loop failed7272
// to compile, mirrored from the identical ashvm bug): this only7373
// makes sense at block_depth == 0 (a genuinely top-level `local` in@@ -266,7 +266,7 @@ static void assignment_statement(void) {266266
// (`local x = json.load(...);`, where codegen/C/expressions/expr.c's267267
// primary() already handles this same `ident.member(...)` shape) --268268
// mirrors the identical ashvm fix, found missing the same way (the269
- // first real forgepack module's whole point is exactly this shape).269
+ // first real apm module's whole point is exactly this shape).270270
if (current.type == TOKEN_DOT) {271271
advance_token();272272
expect(TOKEN_IDENTIFIER, "expected name after '.'");ideas/ash_modules.mdmodified
+20−20@@ -15,7 +15,7 @@1515
5. [Resolution Order](#resolution-order)1616
6. [Versioning](#versioning)1717
7. [The Registry](#the-registry)18
-8. [CLI: `forgepack`](#cli-forgepack)18
+8. [CLI: `apm`](#cli-apm)1919
9. [Publishing a Package](#publishing-a-package)2020
10. [Development Phases](#development-phases)2121
11. [Design Principles](#design-principles)@@ -105,7 +105,7 @@ my-project/105105
106106
- Flat, not nested — same lesson npm learned the hard way (npm 2's deeply nested `node_modules` was replaced by npm 3's flat layout). A dependency's *own* dependencies also land flat in the top-level `@ash-modules/`, deduped by version where possible.107107
- A version conflict (two dependencies need incompatible versions of the same package) gets a single nested fallback: `@ash-modules/<name>/@ash-modules/<name>@<version>/` — resolution always checks the nearest `@ash-modules/` first, same lookup order Node uses for `node_modules`.108
-- `@ash-modules/` is machine-generated and gitignored, like `O/`/`D/` already are for build output — `forgepack install` regenerates it from `ash.lock`.108
+- `@ash-modules/` is machine-generated and gitignored, like `O/`/`D/` already are for build output — `apm install` regenerates it from `ash.lock`.109109
110110
---111111
@@ -128,32 +128,32 @@ Semver (`MAJOR.MINOR.PATCH`), kept deliberately simple for v1:128128
- An exact version (`2.1.0`) always means exactly that version.129129
- A caret range (`^1.3.0`) means "`>=1.3.0`, `<2.0.0`" — the usual meaning, allows patch/minor updates, blocks breaking ones.130130
- No `~`, no `>=<`, no OR-ranges in v1 — if a real package needs more than caret ranges, add it then, not speculatively now.131
-- `ash.lock` pins every resolved package (direct and transitive) to one exact version and a content hash, so `forgepack install` is always reproducible — the manifest describes *intent*, the lockfile describes *what actually got installed*.131
+- `ash.lock` pins every resolved package (direct and transitive) to one exact version and a content hash, so `apm install` is always reproducible — the manifest describes *intent*, the lockfile describes *what actually got installed*.132132
133133
---134134
135135
## The Registry136136
137137
Start without a hosted server at all — two sources, both file-based:138138
139
-1. **Git.** `forgepack add github:someone/some-package` clones a tag/commit straight into `@ash-modules/`. Zero infrastructure, works day one, same escape hatch cargo/npm/go modules all still support.140
-2. **A flat static index**, once there's more than a handful of packages: a single JSON (or plain-text) file mapping `name -> [versions] -> tarball URL`, served from anywhere static (GitHub Pages, a S3 bucket) — no backend service, no database, no auth to build first. `forgepack publish` is just "upload a tarball + update the index."139
+1. **Git.** `apm add github:someone/some-package` clones a tag/commit straight into `@ash-modules/`. Zero infrastructure, works day one, same escape hatch cargo/npm/go modules all still support.140
+2. **A flat static index**, once there's more than a handful of packages: a single JSON (or plain-text) file mapping `name -> [versions] -> tarball URL`, served from anywhere static (GitHub Pages, a S3 bucket) — no backend service, no database, no auth to build first. `apm publish` is just "upload a tarball + update the index."141141
142142
A real registry with search, download counts, a web UI — all later, once there's something worth searching. Don't build npm's infrastructure for zero packages.143143
144144
---145145
146
-## CLI: `forgepack`146
+## CLI: `apm`147147
148148
```text149
-forgepack init # creates ash.pkg in the current directory150
-forgepack install # reads ash.pkg + ash.lock, populates @ash-modules/151
-forgepack add <name>[@version] # adds to ash.pkg, resolves, installs, updates ash.lock152
-forgepack add github:user/repo # git-sourced dependency153
-forgepack remove <name> # removes from ash.pkg, re-resolves, prunes @ash-modules/154
-forgepack update [<name>] # re-resolves within manifest ranges, rewrites ash.lock155
-forgepack publish # packages the current directory, pushes to the index156
-forgepack list # prints the resolved dependency tree149
+apm init # creates ash.pkg in the current directory150
+apm install # reads ash.pkg + ash.lock, populates @ash-modules/151
+apm add <name>[@version] # adds to ash.pkg, resolves, installs, updates ash.lock152
+apm add github:user/repo # git-sourced dependency153
+apm remove <name> # removes from ash.pkg, re-resolves, prunes @ash-modules/154
+apm update [<name>] # re-resolves within manifest ranges, rewrites ash.lock155
+apm publish # packages the current directory, pushes to the index156
+apm list # prints the resolved dependency tree157157
```158158
159159
Sample `list` output:@@ -170,7 +170,7 @@ my-project@0.1.0170170
171171
## Publishing a Package172172
173
-A package is just a project with no `main` entry point expectation — `ash.pkg`'s `name`/`version`, a `<name>.ash` entry file at the root, optionally its own `ash.libs/`/dependencies. `forgepack publish`:173
+A package is just a project with no `main` entry point expectation — `ash.pkg`'s `name`/`version`, a `<name>.ash` entry file at the root, optionally its own `ash.libs/`/dependencies. `apm publish`:174174
175175
1. Validates `ash.pkg` (name/version present, version not already published).176176
2. Tars up the directory (minus `@ash-modules/`, minus anything in a `.ashignore`).@@ -182,10 +182,10 @@ A package is just a project with no `main` entry point expectation — `ash.pkg`182182
183183
| Phase | Goal | Details |184184
|-------|------|---------|185
-| **1. Manifest + local install** | Prove the shape works | `ash.pkg` parsing, `@ash-modules/` resolution added to `import_resolve_path()`, `forgepack install` reading only git/local-path sources (no index yet). |186
-| **2. Lockfile** | Reproducible installs | `ash.lock` generation, content hashing, `forgepack install` becoming deterministic from the lockfile alone. |187
-| **3. Static index** | Discoverability without a server | Flat JSON index, `forgepack publish`/`add <name>` against it, version resolution against caret ranges. |188
-| **4. Version conflict handling** | Real-world dependency graphs | Nested fallback resolution, `forgepack list` tree view, dedup pass. |185
+| **1. Manifest + local install** | Prove the shape works | `ash.pkg` parsing, `@ash-modules/` resolution added to `import_resolve_path()`, `apm install` reading only git/local-path sources (no index yet). |186
+| **2. Lockfile** | Reproducible installs | `ash.lock` generation, content hashing, `apm install` becoming deterministic from the lockfile alone. |187
+| **3. Static index** | Discoverability without a server | Flat JSON index, `apm publish`/`add <name>` against it, version resolution against caret ranges. |188
+| **4. Version conflict handling** | Real-world dependency graphs | Nested fallback resolution, `apm list` tree view, dedup pass. |189189
| **5. Registry service** *(maybe, later)* | Search, stats, web UI | Only once there's enough real packages to justify it. |190190
191191
---@@ -206,5 +206,5 @@ A package is just a project with no `main` entry point expectation — `ash.pkg`206206
- **Git dependency pinning:** a tag can move, a branch definitely does. `ash.lock` should probably pin a commit hash for git sources, not just a tag name.207207
- **Circular package dependencies:** `ashvm`'s `@import` already handles circular *file* imports (see `imports_circular_*` tests) — need to confirm that still holds once "file" becomes "a file inside someone else's installed package."208208
- **Namespace clashes with `ash.libs/`:** a project with a local `ash.libs/http/` and a dependency also named `http` — local wins per the resolution order above, but should probably at least warn.209
-- **Removing packages:** `forgepack remove` pruning `@ash-modules/` safely needs to know what's still a transitive dependency of something else first.209
+- **Removing packages:** `apm remove` pruning `@ash-modules/` safely needs to know what's still a transitive dependency of something else first.210210
- **No native deps today — forever?** If a package ever genuinely needs to call into C (e.g. for something `kiln` can't do in pure Ash), there's no story for that yet. Worth deciding explicitly rather than backing into it.