← 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

35e32ceparent6361249

34 files changed+479−262

.gitignoremodified

+1−1
@@ -2,7 +2,7 @@
22 *.o
33 /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 - Makefile
2-# 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-int
4848 The standard library is not built into the compiler. The compiler only contains the types it cannot work
4949 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 live
51-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 set
5353 `ASH_PATH=/path/to/ash-stdlib/src/main/ash`. Without it, programs that only use `ash.lang` still work and
5454 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-app
116+version: 1.2.0
117+description: What it does
118+author: Ada <ada@example.com>
119+license: MIT
120+homepage: https://example.com/my-app
121+repository: github:ada/my-app
122+bugs: https://example.com/my-app/issues
123+main: demo.Main
124+keywords: ash, cli
125+
126+[dependencies]
127+ash-stdlib: github:<user>/ash-stdlib@v0.1.0
128+
129+[dev-dependencies]
130+test-kit: github:<user>/test-kit
131+
132+[libraries]
133+m: system
134+vulkan: pkg-config:vulkan
135+
136+[scripts]
137+build: ashvm check src/main/ash/demo/Main.ash
138+test: ashvm run src/test/ash/AllTests.ash
139+```
140+
141+```bash
142+apm init # create ash.pkg
143+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, scripts
146+apm run <script> # run a [scripts] entry with sh -c; `apm build` / `apm test` are shorthands
147+```
148+
149+`[libraries]` records the external native libraries a package links; apm stores and lists them, but
150+`ashvm` and `kiln` do not consume them yet. Scripts run with the same trust as `npm run`: only the
151+project's own `ash.pkg` is consulted, never an installed dependency's.
152+
110153 ## Project structure
111154
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 GitHub
164+hearth/apm/ the Ash package manager: apm init/add/install/list/run, dependencies fetched from GitHub
122165 into a project-local @ash-modules/ folder; point ASH_PATH at an installed source root
123166 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 = gcc
22 .DEFAULT_GOAL := all
33 CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc
44
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.d
1616 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 shared
2222 # 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 endef
29-$(foreach s,$(FORGEPACK_SRC),$(eval $(call compile_rule,$(s))))
29+$(foreach s,$(APM_SRC),$(eval $(call compile_rule,$(s))))
3030
31-BIN = ../../bin/forgepack
31+BIN = ../../bin/apm
3232
3333 all: $(BIN)
3434
35-$(BIN): $(FORGEPACK_OBJ)
35+$(BIN): $(APM_OBJ)
3636 mkdir -p ../../bin
37- $(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' -delete

hearth/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 reserved
4546 // for that once there's a real flat index worth serving from it), so a
4647 // git source is the only dependency kind that's actually installable
4748 // 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:" dependency
5757 // installs its OWN source, not whatever it happened to have
58- // 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 (see
88 // ideas/ash_modules.md's phased plan), so a git source is the only thing
9-// forgepack can actually resolve without a server. path:/plain-version
9+// apm can actually resolve without a server. path:/plain-version
1010 // 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 Manifest
7+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 header
53+ 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 load
70+ *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−0

No content changes. View the file on GitHub

hearth/forgepack/src/D/cmd.d → hearth/apm/src/D/cmd.drenamed

+0−0

No content changes. View the file on GitHub

hearth/forgepack/src/D/fsutil.d → hearth/apm/src/D/fsutil.drenamed

+0−0

No content changes. View the file on GitHub

hearth/forgepack/src/D/git_fetch.d → hearth/apm/src/D/git_fetch.drenamed

+0−0

No content changes. View the file on GitHub

hearth/forgepack/src/D/install.d → hearth/apm/src/D/install.drenamed

+0−0

No content changes. View the file on GitHub

hearth/forgepack/src/D/main.d → hearth/apm/src/D/main.drenamed

+0−0

No content changes. View the file on GitHub

hearth/forgepack/src/D/manifest.d → hearth/apm/src/D/manifest.drenamed

+0−0

No content changes. View the file on GitHub

hearth/forgepack/src/D/ui.d → hearth/apm/src/D/ui.drenamed

+0−0

No 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_H
2-#define FORGEPACK_CMD_H
1+#ifndef APM_CMD_H
2+#define APM_CMD_H
33
44 // Each command takes the project directory ("." almost always, since
5-// forgepack is always run from a project's root) and the remaining argv
5+// apm is always run from a project's root) and the remaining argv
66 // 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 #endif

hearth/forgepack/src/H/fsutil.h → hearth/apm/src/H/fsutil.hrenamed

+2−2
@@ -1,5 +1,5 @@
1-#ifndef FORGEPACK_FSUTIL_H
2-#define FORGEPACK_FSUTIL_H
1+#ifndef APM_FSUTIL_H
2+#define APM_FSUTIL_H
33
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_H
2-#define FORGEPACK_GIT_FETCH_H
1+#ifndef APM_GIT_FETCH_H
2+#define APM_GIT_FETCH_H
33
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 the
8-// 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 ever
1111 // touching a shell command string, so a crafted dependency spec can't
1212 // 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 given
15+// Derives the package name apm should use when none was given
1616 // 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_H
2-#define FORGEPACK_INSTALL_H
1+#ifndef APM_INSTALL_H
2+#define APM_INSTALL_H
33 #include "H/manifest.h"
44
55 // Installs one dependency into <project_dir>/@ash-modules/<dep->name>/ if

hearth/apm/src/H/manifest.hadded

+76−0
@@ -0,0 +1,76 @@
1+#ifndef APM_MANIFEST_H
2+#define APM_MANIFEST_H
3+
4+#include <stddef.h>
5+
6+#define MANIFEST_MAX_ENTRIES 64
7+#define MANIFEST_NAME_LEN 128
8+#define MANIFEST_VALUE_LEN 256
9+#define MANIFEST_FILE "ash.pkg"
10+
11+// One "name: value" line of a section. In [dependencies] / [dev-dependencies] the value is a raw
12+// source spec: "github:user/repo" or "github:user/repo@ref" (ref = tag or branch). No registry yet
13+// (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" lines
26+// (blank lines and '#' comments are ignored):
27+//
28+// name: my-app name, version identity
29+// version: 1.2.0
30+// description: What it does description, author, license (SPDX id), homepage,
31+// author: Ada <ada@example.com> repository, bugs metadata, as in package.json
32+// license: MIT
33+// main: demo.Main main entry class (qualified) for `apm run start`-style tooling
34+// keywords: ash, cli keywords comma-separated
35+//
36+// [dependencies] packages fetched into ./@ash-modules/<name>
37+// ash-stdlib: github:user/ash-stdlib@v0.1.0
38+// [dev-dependencies] same, only needed to build and test
39+// [libraries] external native libraries the package links: name: how to find it
40+// 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.ash
43+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+#endif

hearth/forgepack/src/H/ui.h → hearth/apm/src/H/ui.hrenamed

+2−2
@@ -1,5 +1,5 @@
1-#ifndef FORGEPACK_UI_H
2-#define FORGEPACK_UI_H
1+#ifndef APM_UI_H
2+#define APM_UI_H
33
44 // Plain Unicode symbols (box-drawing/dingbats block, not emoji -- no
55 // color-font fallback needed, renders in any monospace terminal font) and

hearth/forgepack/src/O/.gitkeep → hearth/apm/src/O/.gitkeeprenamed

+0−0

No content changes. View the file on GitHub

hearth/apm/tests/scripts/smoke_test.shadded

+77−0
@@ -0,0 +1,77 @@
1+#!/usr/bin/env bash
2+# apm/tests/scripts/smoke_test.sh -- exercises the CLI end to end
3+# (init/add/install/list/run) against a throwaway scratch directory. Not a
4+# .ash regression suite like ashvm/kiln have (apm doesn't run Ash
5+# programs, it manages their dependencies) -- this is the equivalent
6+# check for this project: does the real CLI, talking to the real
7+# filesystem and the real `git`, do what it says.
8+set -euo pipefail
9+cd "$(dirname "$0")/../.." # -> apm/
10+
11+make -s
12+
13+BIN="$(cd ../../bin && pwd)/apm" # absolute -- the script cd's into a scratch dir below
14+SCRATCH=$(mktemp -d)
15+trap 'rm -rf "$SCRATCH"' EXIT
16+
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-modules
30+"$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: demo
40+version: 1.2.0
41+description: A demo package
42+author: Ada <ada@example.com>
43+license: MIT
44+homepage: https://example.com/demo
45+repository: github:ada/demo
46+bugs: https://example.com/demo/issues
47+main: demo.Main
48+keywords: ash, demo
49+
50+[dependencies]
51+Hello-World: github:octocat/Hello-World
52+
53+[libraries]
54+m: system
55+vulkan: pkg-config:vulkan
56+
57+[scripts]
58+build: echo building > built.txt
59+test: echo testing
60+PKG
61+"$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"; do
65+ grep -q -- "$want" ash.pkg || fail "ash.pkg lost '$want' after add --dev"
66+done
67+[ -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"; fi
74+
75+if "$BIN" bogus-command > /dev/null 2>&1; then fail "unknown command should exit non-zero"; fi
76+
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 JSON
84+ // Bug (found while writing the first real apm module, a JSON
8585 // codec -- any imported function with a `local` inside a loop failed
8686 // to compile): try_declare_import_constant() has no way to tell "a
8787 // 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 expression
183183 // (`local x = json.load(...);`, where expr.c's primary() already
184184 // handles this same `ident.member(...)` shape) -- found missing
185- // while writing the first real forgepack module, whose whole point
185+ // while writing the first real apm module, whose whole point
186186 // (`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-declared
229- // variable -- found missing while writing the first real forgepack
229+ // variable -- found missing while writing the first real apm
230230 // module (a JSON codec, which reassigns a loop position variable
231231 // 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 load
38- *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_H
2-#define FORGEPACK_MANIFEST_H
3-
4-#define MANIFEST_MAX_DEPS 64
5-#define MANIFEST_NAME_LEN 128
6-#define MANIFEST_VALUE_LEN 256
7-#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/range
12-// 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 malformed
29-// (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-#endif

hearth/forgepack/tests/scripts/smoke_test.shremoved

+0−39
@@ -1,39 +0,0 @@
1-#!/usr/bin/env bash
2-# forgepack/tests/scripts/smoke_test.sh -- exercises the CLI end to end
3-# (init/add/install/list) against a throwaway scratch directory. Not a
4-# .ash regression suite like ashvm/kiln have (forgepack doesn't run Ash
5-# programs, it manages their dependencies) -- this is the equivalent
6-# check for this project: does the real CLI, talking to the real
7-# filesystem and the real `git`, do what it says.
8-set -euo pipefail
9-cd "$(dirname "$0")/../.." # -> forgepack/
10-
11-make -s
12-
13-BIN="$(cd ../../bin && pwd)/forgepack" # absolute -- the script cd's into a scratch dir below
14-SCRATCH=$(mktemp -d)
15-trap 'rm -rf "$SCRATCH"' EXIT
16-
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-modules
30-"$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"; fi
38-
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 a
33+/* Packages installed by apm: each child C of <project>/@ash-modules and <project>/ash.libs is a
3434 * 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 JSON
70+ // Bug (found while writing the first real apm module, a JSON
7171 // codec -- any imported function with a `local` inside a loop failed
7272 // to compile, mirrored from the identical ashvm bug): this only
7373 // 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's
267267 // primary() already handles this same `ident.member(...)` shape) --
268268 // mirrors the identical ashvm fix, found missing the same way (the
269- // 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 Registry
136136
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 ```text
149-forgepack init # creates ash.pkg in the current directory
150-forgepack install # reads ash.pkg + ash.lock, populates @ash-modules/
151-forgepack add <name>[@version] # adds to ash.pkg, resolves, installs, updates ash.lock
152-forgepack add github:user/repo # git-sourced dependency
153-forgepack remove <name> # removes from ash.pkg, re-resolves, prunes @ash-modules/
154-forgepack update [<name>] # re-resolves within manifest ranges, rewrites ash.lock
155-forgepack publish # packages the current directory, pushes to the index
156-forgepack list # prints the resolved dependency tree
149+apm init # creates ash.pkg in the current directory
150+apm install # reads ash.pkg + ash.lock, populates @ash-modules/
151+apm add <name>[@version] # adds to ash.pkg, resolves, installs, updates ash.lock
152+apm add github:user/repo # git-sourced dependency
153+apm remove <name> # removes from ash.pkg, re-resolves, prunes @ash-modules/
154+apm update [<name>] # re-resolves within manifest ranges, rewrites ash.lock
155+apm publish # packages the current directory, pushes to the index
156+apm list # prints the resolved dependency tree
157157 ```
158158
159159 Sample `list` output:
@@ -170,7 +170,7 @@ my-project@0.1.0
170170
171171 ## Publishing a Package
172172
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.