@crowdedkingdoms/ckx-kit 0.0.0-stage → 0.1.1-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE-APACHE +176 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +76 -2
  4. package/checks/compute-toolchain/host-catalog.json +68 -0
  5. package/crates/ckx-sdk/Cargo.toml +17 -0
  6. package/crates/ckx-sdk/LICENSE-APACHE +176 -0
  7. package/crates/ckx-sdk/LICENSE-MIT +21 -0
  8. package/crates/ckx-sdk/README.md +47 -0
  9. package/crates/ckx-sdk/src/abi.rs +115 -0
  10. package/crates/ckx-sdk/src/cron.rs +717 -0
  11. package/crates/ckx-sdk/src/emit.rs +140 -0
  12. package/crates/ckx-sdk/src/lib.rs +588 -0
  13. package/crates/ckx-sdk/src/model.rs +482 -0
  14. package/crates/ckx-sdk/src/node.rs +942 -0
  15. package/crates/ckx-sdk/src/rt.rs +519 -0
  16. package/crates/crowdy-client-sdk/Cargo.toml +15 -0
  17. package/crates/crowdy-client-sdk/LICENSE-APACHE +176 -0
  18. package/crates/crowdy-client-sdk/LICENSE-MIT +21 -0
  19. package/crates/crowdy-client-sdk/README.md +25 -0
  20. package/crates/crowdy-client-sdk/src/lib.rs +698 -0
  21. package/dist/checks/src/build/client-capabilities.js +123 -0
  22. package/dist/checks/src/build/exec-source.js +343 -0
  23. package/dist/checks/src/build/rust-source-guard.js +165 -0
  24. package/dist/checks/src/build/wasm-inspect.js +249 -0
  25. package/dist/cli/src/build.js +99 -0
  26. package/dist/cli/src/crate.js +64 -0
  27. package/dist/cli/src/doctor.js +51 -0
  28. package/dist/cli/src/kit.js +66 -0
  29. package/dist/cli/src/main.js +109 -0
  30. package/dist/cli/src/new.js +74 -0
  31. package/dist/cli/src/tools.js +104 -0
  32. package/kit.pin +9 -0
  33. package/package.json +81 -4
  34. package/rust-toolchain.toml +6 -0
  35. package/starters/ckx.json +9 -0
  36. package/starters/grid-mod/Cargo.toml +13 -0
  37. package/starters/grid-mod/src/lib.rs +82 -0
  38. package/starters/matchmaker/Cargo.toml +13 -0
  39. package/starters/matchmaker/src/lib.rs +362 -0
  40. package/starters/npc-mobs/Cargo.toml +14 -0
  41. package/starters/npc-mobs/src/lib.rs +469 -0
  42. package/starters/session/Cargo.toml +13 -0
  43. package/starters/session/src/lib.rs +395 -0
  44. package/starters/world-tick/Cargo.toml +14 -0
  45. package/starters/world-tick/src/lib.rs +346 -0
  46. package/templates/client/Cargo.toml +16 -0
  47. package/templates/client/src/lib.rs +31 -0
  48. package/templates/hub/Cargo.toml +11 -0
  49. package/templates/hub/src/lib.rs +73 -0
  50. package/templates/spoke/Cargo.toml +11 -0
  51. package/templates/spoke/src/lib.rs +53 -0
  52. package/tools/instrument/Cargo.lock +140 -0
  53. package/tools/instrument/Cargo.toml +11 -0
  54. package/tools/instrument/src/main.rs +165 -0
  55. package/vendor.lock +167 -0
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TEMPLATES = void 0;
4
+ exports.newProject = newProject;
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ const kit_1 = require("./kit");
8
+ /** What `new` can start from: the platform's starters, and the kit's own templates. */
9
+ exports.TEMPLATES = {
10
+ mod: { dir: ['starters', 'grid-mod'], sdk: 'ckx-sdk', about: "a mod: a player's code on a grid they own" },
11
+ client: { dir: ['templates', 'client'], sdk: 'crowdy-client-sdk', about: "a mod's CLIENT half, run in visitors' browsers" },
12
+ hub: { dir: ['templates', 'hub'], sdk: 'ckx-sdk', about: 'a hub: state and one handler at a time' },
13
+ spoke: { dir: ['templates', 'spoke'], sdk: 'ckx-sdk', about: 'a spoke: stateless, scaled out' },
14
+ 'world-tick': { dir: ['starters', 'world-tick'], sdk: 'ckx-sdk', about: "the starter pack's root hub: clock, weather, resources, events" },
15
+ matchmaker: { dir: ['starters', 'matchmaker'], sdk: 'ckx-sdk', about: 'a rating queue that reserves sessions' },
16
+ session: { dir: ['starters', 'session'], sdk: 'ckx-sdk', about: 'one game from lobby to result' },
17
+ 'npc-mobs': { dir: ['starters', 'npc-mobs'], sdk: 'ckx-sdk', about: "an area's mobs and NPCs" },
18
+ };
19
+ const CRATE_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
20
+ function copyTree(from, to) {
21
+ fs.mkdirSync(to, { recursive: true });
22
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
23
+ if (entry.name === 'target')
24
+ continue;
25
+ const src = path.join(from, entry.name);
26
+ const dst = path.join(to, entry.name);
27
+ if (entry.isDirectory())
28
+ copyTree(src, dst);
29
+ else
30
+ fs.copyFileSync(src, dst);
31
+ }
32
+ }
33
+ /** The manifest with `[package] name` set and the SDK's line naming this kit's version. */
34
+ function personalise(manifest, name, sdk) {
35
+ let section = '';
36
+ let renamed = false;
37
+ return manifest
38
+ .split('\n')
39
+ .map((line) => {
40
+ const header = /^\s*\[([^\]]+)\]\s*$/.exec(line);
41
+ if (header) {
42
+ section = header[1].trim();
43
+ return line;
44
+ }
45
+ if (section === 'package' && !renamed && /^\s*name\s*=/.test(line)) {
46
+ renamed = true;
47
+ return `name = "${name}"`;
48
+ }
49
+ if (section === 'dependencies' && new RegExp(`^\\s*${sdk}\\s*=`).test(line)) {
50
+ return `${sdk} = "${(0, kit_1.sdkRequirement)(sdk)}"`;
51
+ }
52
+ return line;
53
+ })
54
+ .join('\n');
55
+ }
56
+ function newProject(template, name, into) {
57
+ const spec = exports.TEMPLATES[template];
58
+ if (!spec)
59
+ throw new Error(`no template '${template}'; one of: ${Object.keys(exports.TEMPLATES).join(', ')}`);
60
+ if (!CRATE_NAME.test(name)) {
61
+ throw new Error(`crate name '${name}' must be lowercase letters, digits, '-' or '_', starting with a letter`);
62
+ }
63
+ if (template === 'mod' && name.length > 48)
64
+ throw new Error("a mod's name is at most 48 characters");
65
+ const dir = path.resolve(into ?? name);
66
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length > 0)
67
+ throw new Error(`${dir} exists and is not empty`);
68
+ copyTree((0, kit_1.kitPath)(...spec.dir), dir);
69
+ const manifest = path.join(dir, 'Cargo.toml');
70
+ fs.writeFileSync(manifest, personalise(fs.readFileSync(manifest, 'utf8'), name, spec.sdk));
71
+ fs.copyFileSync((0, kit_1.kitPath)('rust-toolchain.toml'), path.join(dir, 'rust-toolchain.toml'));
72
+ fs.writeFileSync(path.join(dir, '.gitignore'), 'target/\n');
73
+ return dir;
74
+ }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.which = which;
4
+ exports.run = run;
5
+ exports.cargo = cargo;
6
+ exports.cacheDir = cacheDir;
7
+ exports.instrumentBin = instrumentBin;
8
+ exports.wasmOpt = wasmOpt;
9
+ exports.sha256 = sha256;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_crypto_1 = require("node:crypto");
12
+ const fs = require("node:fs");
13
+ const os = require("node:os");
14
+ const path = require("node:path");
15
+ const kit_1 = require("./kit");
16
+ /** A program on PATH, resolved; null when it is not installed. */
17
+ function which(cmd) {
18
+ for (const dir of (process.env.PATH ?? '').split(path.delimiter).filter(Boolean)) {
19
+ const candidate = path.join(dir, cmd);
20
+ try {
21
+ fs.accessSync(candidate, fs.constants.X_OK);
22
+ return candidate;
23
+ }
24
+ catch {
25
+ // not in this directory
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ /**
31
+ * Runs a step. Its output goes to stderr, so `--json` keeps stdout for the report; a step that
32
+ * fails throws with the step's name.
33
+ */
34
+ function run(label, cmd, args, options = {}) {
35
+ const result = (0, node_child_process_1.spawnSync)(cmd, args, { stdio: ['ignore', process.stderr, process.stderr], ...options });
36
+ if (result.error) {
37
+ throw new Error(`${label}: ${result.error.code === 'ENOENT' ? `${cmd} is not installed` : result.error.message}`);
38
+ }
39
+ if (result.status !== 0)
40
+ throw new Error(`${label} failed (exit ${result.status ?? result.signal})`);
41
+ }
42
+ function cargo() {
43
+ const bin = which('cargo');
44
+ if (!bin)
45
+ throw new Error('cargo is not installed: install Rust with rustup (https://rustup.rs)');
46
+ return bin;
47
+ }
48
+ function cacheDir() {
49
+ return (process.env.CKX_KIT_CACHE ??
50
+ path.join(process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache'), 'ckx-kit'));
51
+ }
52
+ function copyTree(from, to) {
53
+ fs.mkdirSync(to, { recursive: true });
54
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
55
+ if (entry.name === 'target')
56
+ continue;
57
+ const src = path.join(from, entry.name);
58
+ const dst = path.join(to, entry.name);
59
+ if (entry.isDirectory())
60
+ copyTree(src, dst);
61
+ else
62
+ fs.copyFileSync(src, dst);
63
+ }
64
+ }
65
+ function treeHash(dir, h = (0, node_crypto_1.createHash)('sha256'), rel = '') {
66
+ for (const entry of fs.readdirSync(path.join(dir, rel), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
67
+ if (entry.name === 'target')
68
+ continue;
69
+ const child = rel ? `${rel}/${entry.name}` : entry.name;
70
+ if (entry.isDirectory())
71
+ treeHash(dir, h, child);
72
+ else
73
+ h.update(child).update('\0').update(fs.readFileSync(path.join(dir, child))).update('\0');
74
+ }
75
+ return h;
76
+ }
77
+ /**
78
+ * The platform's `instrument`, which meters a CLIENT half: built from the kit's copy with the
79
+ * pinned Rust and its lock on first use, and cached by the copy's contents.
80
+ */
81
+ function instrumentBin() {
82
+ const source = (0, kit_1.kitPath)('tools', 'instrument');
83
+ const key = treeHash(source).update(fs.readFileSync((0, kit_1.kitPath)('rust-toolchain.toml'))).digest('hex').slice(0, 16);
84
+ const dir = path.join(cacheDir(), `instrument-${key}`);
85
+ const bin = path.join(dir, 'target', 'release', process.platform === 'win32' ? 'instrument.exe' : 'instrument');
86
+ if (fs.existsSync(bin))
87
+ return bin;
88
+ copyTree(source, dir);
89
+ fs.copyFileSync((0, kit_1.kitPath)('rust-toolchain.toml'), path.join(dir, 'rust-toolchain.toml'));
90
+ process.stderr.write('== instrument (first use: building the platform\'s metering tool)\n');
91
+ run('cargo build (instrument)', cargo(), ['build', '--release', '--locked'], { cwd: dir });
92
+ if (!fs.existsSync(bin))
93
+ throw new Error(`instrument did not build at ${bin}`);
94
+ return bin;
95
+ }
96
+ function wasmOpt() {
97
+ const bin = which('wasm-opt');
98
+ if (!bin)
99
+ throw new Error('wasm-opt is not installed: install binaryen (apt install binaryen, brew install binaryen)');
100
+ return bin;
101
+ }
102
+ function sha256(bytes) {
103
+ return (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
104
+ }
package/kit.pin ADDED
@@ -0,0 +1,9 @@
1
+ # The platform toolchain release this kit was copied from (sync-kit.sh in the platform's
2
+ # repository). The paths in `copied` are byte-identical to it: change them there, never here.
3
+ toolchain_version=v1.9.0
4
+ toolchain_commit=8b4ef8929c4fd1bb7ccf604032ad75b970aca1a2
5
+ sdk_commit=b4194c7a13e2f3b74279cb4f715c4bcdaba70023
6
+ ckx_sdk_version=0.8.0
7
+ crowdy_client_sdk_version=0.1.0
8
+ rust_version=1.97.1
9
+ copied=crates/ckx-sdk/src crates/crowdy-client-sdk/src tools/instrument/Cargo.toml tools/instrument/Cargo.lock tools/instrument/src checks/src/build/exec-source.ts checks/src/build/exec-source.spec.ts checks/src/build/rust-source-guard.ts checks/src/build/rust-source-guard.spec.ts checks/src/build/wasm-inspect.ts checks/src/build/wasm-inspect.spec.ts checks/src/build/client-capabilities.ts checks/src/build/client-capabilities.spec.ts checks/src/test-support/wasm-module.ts checks/compute-toolchain/host-catalog.json starters rust-toolchain.toml vendor.lock
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@crowdedkingdoms/ckx-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1-dev.1",
4
+ "description": "Build, check and test Crowded Kingdoms ck-exec mods, hubs and spokes on your machine, as the platform builds them",
5
+ "license": "MIT OR Apache-2.0",
6
+ "homepage": "https://docs.crowdedkingdoms.com/exec/intro",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/CrowdedKingdoms/ckx-kit.git"
10
+ },
11
+ "bugs": "https://github.com/CrowdedKingdoms/ckx-kit/issues",
12
+ "keywords": [
13
+ "crowded-kingdoms",
14
+ "ck-exec",
15
+ "wasm",
16
+ "gamedev",
17
+ "rust"
18
+ ],
19
+ "bin": {
20
+ "ckx-kit": "dist/cli/src/main.js"
21
+ },
22
+ "files": [
23
+ "dist/cli/src",
24
+ "dist/checks/src/build",
25
+ "crates/*/Cargo.toml",
26
+ "crates/*/README.md",
27
+ "crates/*/LICENSE-MIT",
28
+ "crates/*/LICENSE-APACHE",
29
+ "crates/*/src/**",
30
+ "tools/instrument/Cargo.toml",
31
+ "tools/instrument/Cargo.lock",
32
+ "tools/instrument/src",
33
+ "starters/ckx.json",
34
+ "starters/*/Cargo.toml",
35
+ "starters/*/src/**",
36
+ "templates/*/Cargo.toml",
37
+ "templates/*/src/**",
38
+ "checks/compute-toolchain/host-catalog.json",
39
+ "rust-toolchain.toml",
40
+ "vendor.lock",
41
+ "kit.pin",
42
+ "LICENSE-MIT",
43
+ "LICENSE-APACHE"
44
+ ],
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc -p tsconfig.build.json && chmod +x dist/cli/src/main.js",
53
+ "typecheck": "tsc -p tsconfig.json --noEmit",
54
+ "test": "jest",
55
+ "test:cli": "node --test cli/test/cli.test.mjs",
56
+ "policy": "node scripts/ci/check-content-policy.mjs"
57
+ },
58
+ "devDependencies": {
59
+ "@types/jest": "29.5.14",
60
+ "@types/node": "22.15.2",
61
+ "jest": "29.7.0",
62
+ "ts-jest": "29.3.2",
63
+ "typescript": "5.8.3"
64
+ },
65
+ "jest": {
66
+ "rootDir": "checks/src",
67
+ "testRegex": ".*\\.spec\\.ts$",
68
+ "moduleFileExtensions": [
69
+ "js",
70
+ "json",
71
+ "ts"
72
+ ],
73
+ "transform": {
74
+ "^.+\\.(t|j)s$": [
75
+ "ts-jest",
76
+ {
77
+ "tsconfig": "<rootDir>/../../tsconfig.json"
78
+ }
79
+ ]
80
+ },
81
+ "testEnvironment": "node"
82
+ }
83
+ }
@@ -0,0 +1,6 @@
1
+ [toolchain]
2
+ # Platform pin for compute-module builds. Bump deliberately: the artifact
3
+ # cache keys include this version, so a bump recompiles every module.
4
+ channel = "1.97.1"
5
+ targets = ["wasm32-wasip1", "wasm32-unknown-unknown"]
6
+ profile = "minimal"
@@ -0,0 +1,9 @@
1
+ {
2
+ "root": "world",
3
+ "types": {
4
+ "world": { "kind": "hub", "crate": "world-tick", "wasm": "world_tick.wasm", "client": true, "persist_every_ms": 10000 },
5
+ "matchmaker": { "kind": "hub", "parent": "world", "crate": "matchmaker", "wasm": "matchmaker.wasm", "client": true, "calls": ["session"] },
6
+ "session": { "kind": "hub", "parent": "matchmaker", "crate": "session", "wasm": "session.wasm", "client": true },
7
+ "npcs": { "kind": "hub", "parent": "world", "crate": "npc-mobs", "wasm": "npc_mobs.wasm", "client": true, "calls": ["world"], "scopes": ["world.read"] }
8
+ }
9
+ }
@@ -0,0 +1,13 @@
1
+ [package]
2
+ name = "grid-mod"
3
+ description = "Starter mod: a player's code on a grid they own; greets visitors and follows what happens in the grid"
4
+ version = "0.1.0"
5
+ edition = "2024"
6
+ publish = false
7
+
8
+ [lib]
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ ckx-sdk = "0.8.0"
13
+ serde = { version = "1", features = ["derive"] }
@@ -0,0 +1,82 @@
1
+ //! A mod: a player's code on a grid they own, deployed with `execModDeploy` rather than in the
2
+ //! app's manifest. One instance runs per grid, keyed by the grid id, in its owner's own
3
+ //! sandbox; its node API calls reach only that grid, and only while the owner still owns it.
4
+ //! It calls no other node, and only players call it.
5
+ //!
6
+ //! Endpoints: `visit` (players), `state` (anyone) and `probe`, which tries to call the app's
7
+ //! root hub and returns the refusal. What happens in the grid arrives at `on_world`.
8
+
9
+ use ckx_sdk::prelude::*;
10
+
11
+ #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
12
+ pub struct GridMod {
13
+ pub grid: u64,
14
+ pub visits: u64,
15
+ pub last_visitor: Option<u64>,
16
+ /// World events heard, by kind, and the chunk of the last one.
17
+ pub actors: u64,
18
+ pub voxels: u64,
19
+ pub last_chunk: Option<[i64; 3]>,
20
+ }
21
+
22
+ impl GridMod {
23
+ fn heard(&mut self, events: &[WorldEvent]) {
24
+ for e in events {
25
+ match e.kind.as_str() {
26
+ WorldEvent::ACTORS => self.actors += 1,
27
+ WorldEvent::VOXELS => self.voxels += 1,
28
+ _ => continue,
29
+ }
30
+ self.last_chunk = Some(e.chunk);
31
+ }
32
+ }
33
+ }
34
+
35
+ impl Hub for GridMod {
36
+ fn spawn(ctx: &Ctx, _seed: &[u8]) -> Result<Self> {
37
+ let grid = ctx.key.parse().map_err(|_| Error::new("a mod's key is its grid id"))?;
38
+ Ok(Self { grid, ..Self::default() })
39
+ }
40
+
41
+ fn load(_ctx: &Ctx, snapshot: &[u8], _from_version: u64) -> Result<Self> {
42
+ decode(snapshot)
43
+ }
44
+
45
+ fn persist(&mut self, _ctx: &Ctx) -> Result<Vec<u8>> {
46
+ encode(self)
47
+ }
48
+
49
+ fn handle(&mut self, ctx: &Ctx, call: Call<'_>) -> Result<Vec<u8>> {
50
+ match call.method {
51
+ "visit" => {
52
+ self.last_visitor = Some(call.player()?);
53
+ self.visits += 1;
54
+ encode(&format!("welcome to grid {}, visitor {}", self.grid, self.visits))
55
+ }
56
+ "state" => encode(&*self),
57
+ "probe" => encode(&ctx.call("world", "", "state", &encode(&())?).err().map(|e| e.to_string())),
58
+ m => Err(Error(format!("no endpoint `{m}`"))),
59
+ }
60
+ }
61
+
62
+ fn on_world(&mut self, ctx: &Ctx, events: &[WorldEvent]) -> Result<()> {
63
+ self.heard(events);
64
+ ctx.persist_now();
65
+ Ok(())
66
+ }
67
+ }
68
+
69
+ ckx_sdk::export_hub!(GridMod);
70
+
71
+ #[cfg(test)]
72
+ mod tests {
73
+ use super::*;
74
+
75
+ #[test]
76
+ fn world_events_are_counted_by_kind() {
77
+ let mut m = GridMod { grid: 7, ..GridMod::default() };
78
+ let e = |kind: &str, x| WorldEvent { grid: 7, kind: kind.into(), chunk: [x, 0, 0] };
79
+ m.heard(&[e(WorldEvent::ACTORS, 1), e(WorldEvent::VOXELS, 2), e("weather", 3)]);
80
+ assert_eq!((m.actors, m.voxels, m.last_chunk), (1, 1, Some([2, 0, 0])));
81
+ }
82
+ }
@@ -0,0 +1,13 @@
1
+ [package]
2
+ name = "matchmaker"
3
+ description = "Starter: a rating queue with widening windows and accept-gated matches that reserve a session"
4
+ version = "0.1.0"
5
+ edition = "2024"
6
+ publish = false
7
+
8
+ [lib]
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ ckx-sdk = "0.8.0"
13
+ serde = { version = "1", features = ["derive"] }