@crowdedkingdoms/ckx-kit 0.0.0-stage → 0.1.0-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,249 @@
1
+ "use strict";
2
+ /**
3
+ * Readers for the few WASM sections the build pipeline checks without instantiating a module:
4
+ * exported function signatures and the declared memory maximum, and the CLIENT ABI check
5
+ * CLIENT builds pass.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.CLIENT_MAX_MEMORY_MB = exports.CLIENT_ARTIFACT_MAX_BYTES = exports.CLIENT_ABI_IMPORTS = exports.WASM_OPT_ARGS = void 0;
9
+ exports.readU32Leb = readU32Leb;
10
+ exports.readWasmName = readWasmName;
11
+ exports.readFunctionExportTypes = readFunctionExportTypes;
12
+ exports.readDefinedMemoryMaximum = readDefinedMemoryMaximum;
13
+ exports.validateClientArtifact = validateClientArtifact;
14
+ function readU32Leb(bytes, offset, end) {
15
+ let value = 0;
16
+ let shift = 0;
17
+ for (let i = 0; i < 5 && offset < end; i++, offset++) {
18
+ const byte = bytes[offset];
19
+ value += (byte & 0x7f) * 2 ** shift;
20
+ if ((byte & 0x80) === 0) {
21
+ return { value, next: offset + 1 };
22
+ }
23
+ shift += 7;
24
+ }
25
+ throw new Error('malformed WASM u32 LEB');
26
+ }
27
+ function readWasmName(bytes, offset, end) {
28
+ const length = readU32Leb(bytes, offset, end);
29
+ const next = length.next + length.value;
30
+ if (next > end)
31
+ throw new Error('truncated WASM name');
32
+ return {
33
+ value: new TextDecoder().decode(bytes.subarray(length.next, next)),
34
+ next,
35
+ };
36
+ }
37
+ /** Resolve exported function names to their declared core-WASM signatures. */
38
+ function readFunctionExportTypes(bytes) {
39
+ const types = [];
40
+ const functionTypeIndices = [];
41
+ const exportedFunctions = new Map();
42
+ let offset = 8;
43
+ while (offset < bytes.length) {
44
+ const sectionId = bytes[offset++];
45
+ const sectionSize = readU32Leb(bytes, offset, bytes.length);
46
+ offset = sectionSize.next;
47
+ const sectionEnd = offset + sectionSize.value;
48
+ if (sectionEnd > bytes.length)
49
+ throw new Error('truncated WASM section');
50
+ if (sectionId === 1) {
51
+ const count = readU32Leb(bytes, offset, sectionEnd);
52
+ offset = count.next;
53
+ for (let i = 0; i < count.value; i++) {
54
+ if (bytes[offset++] !== 0x60) {
55
+ throw new Error('unsupported WASM function type');
56
+ }
57
+ const paramCount = readU32Leb(bytes, offset, sectionEnd);
58
+ offset = paramCount.next;
59
+ const params = Array.from(bytes.subarray(offset, offset + paramCount.value));
60
+ offset += paramCount.value;
61
+ const resultCount = readU32Leb(bytes, offset, sectionEnd);
62
+ offset = resultCount.next;
63
+ const results = Array.from(bytes.subarray(offset, offset + resultCount.value));
64
+ offset += resultCount.value;
65
+ if (offset > sectionEnd)
66
+ throw new Error('truncated WASM function type');
67
+ types.push({ params, results });
68
+ }
69
+ }
70
+ else if (sectionId === 2) {
71
+ const count = readU32Leb(bytes, offset, sectionEnd);
72
+ offset = count.next;
73
+ for (let i = 0; i < count.value; i++) {
74
+ offset = readWasmName(bytes, offset, sectionEnd).next;
75
+ offset = readWasmName(bytes, offset, sectionEnd).next;
76
+ const kind = bytes[offset++];
77
+ if (kind !== 0) {
78
+ throw new Error('client ABI parser encountered a non-function import');
79
+ }
80
+ const typeIndex = readU32Leb(bytes, offset, sectionEnd);
81
+ offset = typeIndex.next;
82
+ functionTypeIndices.push(typeIndex.value);
83
+ }
84
+ }
85
+ else if (sectionId === 3) {
86
+ const count = readU32Leb(bytes, offset, sectionEnd);
87
+ offset = count.next;
88
+ for (let i = 0; i < count.value; i++) {
89
+ const typeIndex = readU32Leb(bytes, offset, sectionEnd);
90
+ offset = typeIndex.next;
91
+ functionTypeIndices.push(typeIndex.value);
92
+ }
93
+ }
94
+ else if (sectionId === 7) {
95
+ const count = readU32Leb(bytes, offset, sectionEnd);
96
+ offset = count.next;
97
+ for (let i = 0; i < count.value; i++) {
98
+ const name = readWasmName(bytes, offset, sectionEnd);
99
+ offset = name.next;
100
+ const kind = bytes[offset++];
101
+ const index = readU32Leb(bytes, offset, sectionEnd);
102
+ offset = index.next;
103
+ if (kind === 0)
104
+ exportedFunctions.set(name.value, index.value);
105
+ }
106
+ }
107
+ offset = sectionEnd;
108
+ }
109
+ const result = new Map();
110
+ for (const [name, functionIndex] of exportedFunctions) {
111
+ const typeIndex = functionTypeIndices[functionIndex];
112
+ const type = types[typeIndex];
113
+ if (!type)
114
+ throw new Error(`missing WASM type for export '${name}'`);
115
+ result.set(name, type);
116
+ }
117
+ return result;
118
+ }
119
+ /** Read the sole defined wasm32 memory maximum; imported/multiple memories fail closed. */
120
+ function readDefinedMemoryMaximum(bytes) {
121
+ if (bytes.length < 8 ||
122
+ bytes[0] !== 0x00 ||
123
+ bytes[1] !== 0x61 ||
124
+ bytes[2] !== 0x73 ||
125
+ bytes[3] !== 0x6d) {
126
+ throw new Error('malformed WASM header');
127
+ }
128
+ let offset = 8;
129
+ while (offset < bytes.length) {
130
+ const sectionId = bytes[offset++];
131
+ const sectionSize = readU32Leb(bytes, offset, bytes.length);
132
+ offset = sectionSize.next;
133
+ const sectionEnd = offset + sectionSize.value;
134
+ if (sectionEnd > bytes.length)
135
+ throw new Error('truncated WASM section');
136
+ if (sectionId !== 5) {
137
+ offset = sectionEnd;
138
+ continue;
139
+ }
140
+ const count = readU32Leb(bytes, offset, sectionEnd);
141
+ offset = count.next;
142
+ if (count.value !== 1) {
143
+ throw new Error(`artifact must define exactly one memory (got ${count.value})`);
144
+ }
145
+ const flags = readU32Leb(bytes, offset, sectionEnd);
146
+ offset = flags.next;
147
+ // Only wasm32, unshared memory with an explicit maximum is accepted.
148
+ if (flags.value !== 1)
149
+ return null;
150
+ const minimum = readU32Leb(bytes, offset, sectionEnd);
151
+ offset = minimum.next;
152
+ const maximum = readU32Leb(bytes, offset, sectionEnd);
153
+ offset = maximum.next;
154
+ if (minimum.value > maximum.value || offset !== sectionEnd) {
155
+ throw new Error('malformed WASM memory limits');
156
+ }
157
+ return maximum.value;
158
+ }
159
+ return null;
160
+ }
161
+ /** wasm-opt feature flags rustc-emitted modules need (Phase 0 finding). */
162
+ exports.WASM_OPT_ARGS = [
163
+ '-O3',
164
+ '--enable-bulk-memory',
165
+ '--enable-sign-ext',
166
+ '--enable-mutable-globals',
167
+ '--enable-nontrapping-float-to-int',
168
+ ];
169
+ const WASM_PAGE_BYTES = 64 * 1024;
170
+ const I32 = 0x7f;
171
+ const I64 = 0x7e;
172
+ /**
173
+ * What a CLIENT half may import: the `ck` module the CrowdyJS browser glue implements, and
174
+ * `random_get`, the one WASI call it answers for real (crowdy-client-sdk's `random_bytes`).
175
+ */
176
+ exports.CLIENT_ABI_IMPORTS = {
177
+ ck: new Set(['log', 'now_ms', 'state_get', 'state_set', 'host_call']),
178
+ wasi_snapshot_preview1: new Set(['random_get']),
179
+ };
180
+ /** The functions every CLIENT half exports, with their core-WASM signatures. */
181
+ const CLIENT_ABI_FUNCTIONS = {
182
+ init: { params: [], results: [] },
183
+ tick: { params: [I32], results: [] },
184
+ ck_alloc: { params: [I32], results: [I32] },
185
+ ck_free: { params: [I32, I32], results: [] },
186
+ handle_invoke: { params: [I32, I32], results: [I64] },
187
+ };
188
+ const CLIENT_ABI_OPTIONAL = {
189
+ on_event: { params: [I32, I32], results: [] },
190
+ };
191
+ /** The largest CLIENT half this API builds and serves. */
192
+ exports.CLIENT_ARTIFACT_MAX_BYTES = 512 * 1024;
193
+ /** The linear memory `instrument` clamps a CLIENT half to. */
194
+ exports.CLIENT_MAX_MEMORY_MB = 32;
195
+ /**
196
+ * A built CLIENT half fit for the browser glue: at most 512 KiB, compiles, imports only the
197
+ * client ABI, exports `memory`, the `ck_fuel` global `instrument` injects and the entry
198
+ * points with their signatures, and declares a memory maximum within the clamp. Compiling
199
+ * does not run the module.
200
+ */
201
+ async function validateClientArtifact(bytes, maxMemoryMb = exports.CLIENT_MAX_MEMORY_MB) {
202
+ if (bytes.length > exports.CLIENT_ARTIFACT_MAX_BYTES) {
203
+ throw new Error(`module is ${bytes.length} bytes; a CLIENT half is at most ${exports.CLIENT_ARTIFACT_MAX_BYTES}`);
204
+ }
205
+ let module;
206
+ try {
207
+ module = await WebAssembly.compile(bytes);
208
+ }
209
+ catch (err) {
210
+ throw new Error(`malformed WASM module: ${err.message}`);
211
+ }
212
+ for (const imp of WebAssembly.Module.imports(module)) {
213
+ if (imp.kind !== 'function' ||
214
+ !exports.CLIENT_ABI_IMPORTS[imp.module]?.has(imp.name)) {
215
+ throw new Error(`module import ${imp.module}::${imp.name} (${imp.kind}) is not part of the CLIENT ABI`);
216
+ }
217
+ }
218
+ const exported = new Map(WebAssembly.Module.exports(module).map((e) => [e.name, e.kind]));
219
+ if (exported.get('memory') !== 'memory') {
220
+ throw new Error("module does not export memory 'memory'");
221
+ }
222
+ if (exported.get('ck_fuel') !== 'global') {
223
+ throw new Error("module does not export the global 'ck_fuel'; it was not instrumented");
224
+ }
225
+ const expected = {
226
+ ...CLIENT_ABI_FUNCTIONS,
227
+ };
228
+ for (const [name, type] of Object.entries(CLIENT_ABI_OPTIONAL)) {
229
+ if (exported.has(name))
230
+ expected[name] = type;
231
+ }
232
+ const types = readFunctionExportTypes(bytes);
233
+ for (const [name, want] of Object.entries(expected)) {
234
+ if (exported.get(name) !== 'function') {
235
+ throw new Error(`module does not export function '${name}'; build it with crowdy-client-sdk's register_module!`);
236
+ }
237
+ const got = types.get(name);
238
+ if (!got ||
239
+ got.params.join(',') !== want.params.join(',') ||
240
+ got.results.join(',') !== want.results.join(',')) {
241
+ throw new Error(`module export '${name}' has the wrong signature`);
242
+ }
243
+ }
244
+ const maxPages = readDefinedMemoryMaximum(bytes);
245
+ const clampPages = Math.floor((maxMemoryMb * 1024 * 1024) / WASM_PAGE_BYTES);
246
+ if (maxPages === null || maxPages > clampPages) {
247
+ throw new Error(`module memory maximum ${maxPages ?? 'missing'} exceeds the clamp of ${clampPages} pages`);
248
+ }
249
+ }
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MOD_ARTIFACT_MAX_BYTES = void 0;
4
+ exports.check = check;
5
+ exports.build = build;
6
+ const fs = require("node:fs");
7
+ const path = require("node:path");
8
+ const client_capabilities_1 = require("../../checks/src/build/client-capabilities");
9
+ const exec_source_1 = require("../../checks/src/build/exec-source");
10
+ const wasm_inspect_1 = require("../../checks/src/build/wasm-inspect");
11
+ const kit_1 = require("./kit");
12
+ const tools_1 = require("./tools");
13
+ const TARGET = 'wasm32-unknown-unknown';
14
+ /** The largest module a mod may deploy, the platform's mod limit (4 MiB). */
15
+ exports.MOD_ARTIFACT_MAX_BYTES = 4 * 1024 * 1024;
16
+ /** The platform's source checks, on the files it would receive. Throws its refusal. */
17
+ function check(crate) {
18
+ (0, exec_source_1.validateExecCrates)([crate.source], crate.kind);
19
+ }
20
+ /** `execBuild`'s steps for one crate, without the platform's sandbox. */
21
+ async function build(crate, options = {}) {
22
+ check(crate);
23
+ const sdk = exec_source_1.BUILD_SDK[crate.kind];
24
+ const work = path.join(crate.dir, 'target', 'ckx-kit', crate.kind);
25
+ const src = path.join(work, 'crate');
26
+ fs.rmSync(src, { recursive: true, force: true });
27
+ for (const [rel, content] of Object.entries(crate.source.files)) {
28
+ const file = path.join(src, rel);
29
+ fs.mkdirSync(path.dirname(file), { recursive: true });
30
+ fs.writeFileSync(file, rel === 'Cargo.toml' ? (0, exec_source_1.rewriteExecManifest)(content, (0, kit_1.kitPath)('crates', sdk), crate.kind) : content);
31
+ }
32
+ fs.copyFileSync((0, kit_1.kitPath)('rust-toolchain.toml'), path.join(src, 'rust-toolchain.toml'));
33
+ // The registry crates' versions the platform vendors; cargo adds this crate's own entries.
34
+ fs.copyFileSync((0, kit_1.kitPath)('vendor.lock'), path.join(src, 'Cargo.lock'));
35
+ // One target directory can serve many crates (CKX_KIT_TARGET_DIR), so their shared
36
+ // dependencies compile once.
37
+ const targetDir = process.env.CKX_KIT_TARGET_DIR
38
+ ? path.resolve(process.env.CKX_KIT_TARGET_DIR)
39
+ : path.join(work, 'target');
40
+ (0, tools_1.run)(`cargo build --release --target ${TARGET}`, (0, tools_1.cargo)(), ['build', '--release', '--target', TARGET], {
41
+ cwd: src,
42
+ env: { ...process.env, CARGO_TARGET_DIR: targetDir },
43
+ });
44
+ const raw = path.join(targetDir, TARGET, 'release', `${crate.libName}.wasm`);
45
+ if (!fs.existsSync(raw)) {
46
+ throw new Error(`${crate.name}: no .wasm produced; [lib] crate-type must be ["cdylib"]`);
47
+ }
48
+ let bytes;
49
+ let summary;
50
+ if (crate.kind === 'client') {
51
+ const step = path.join(work, 'client');
52
+ fs.rmSync(step, { recursive: true, force: true });
53
+ fs.mkdirSync(step, { recursive: true });
54
+ fs.copyFileSync(raw, path.join(step, 'raw.wasm'));
55
+ (0, tools_1.run)(`instrument --max-memory-mb ${wasm_inspect_1.CLIENT_MAX_MEMORY_MB}`, (0, tools_1.instrumentBin)(), [
56
+ 'raw.wasm',
57
+ 'metered.wasm',
58
+ '--max-memory-mb',
59
+ String(wasm_inspect_1.CLIENT_MAX_MEMORY_MB),
60
+ ], { cwd: step });
61
+ (0, tools_1.run)(`wasm-opt ${wasm_inspect_1.WASM_OPT_ARGS.join(' ')}`, (0, tools_1.wasmOpt)(), [...wasm_inspect_1.WASM_OPT_ARGS, 'metered.wasm', '-o', 'final.wasm'], { cwd: step });
62
+ bytes = fs.readFileSync(path.join(step, 'final.wasm'));
63
+ await (0, wasm_inspect_1.validateClientArtifact)(bytes);
64
+ summary = (0, client_capabilities_1.deriveClientCapabilities)(bytes, await (0, client_capabilities_1.readHostCatalog)((0, kit_1.kitPath)('checks', 'compute-toolchain', 'host-catalog.json')));
65
+ }
66
+ else {
67
+ bytes = fs.readFileSync(raw);
68
+ await (0, exec_source_1.validateExecArtifact)(bytes);
69
+ if (options.mod && bytes.length > exports.MOD_ARTIFACT_MAX_BYTES) {
70
+ throw new Error(`module is ${bytes.length} bytes; a mod's module is at most ${exports.MOD_ARTIFACT_MAX_BYTES}`);
71
+ }
72
+ }
73
+ const out = path.resolve(options.out ?? path.join(crate.dir, 'target', 'ckx-kit', `${crate.name}.wasm`));
74
+ fs.mkdirSync(path.dirname(out), { recursive: true });
75
+ fs.writeFileSync(out, bytes);
76
+ const pin = (0, kit_1.readPin)();
77
+ const report = {
78
+ crate: crate.name,
79
+ kind: crate.kind,
80
+ mod: Boolean(options.mod),
81
+ wasm: out,
82
+ sizeBytes: bytes.length,
83
+ sha256: (0, tools_1.sha256)(bytes),
84
+ notSent: crate.notSent,
85
+ kit: {
86
+ version: (0, kit_1.kitVersion)(),
87
+ toolchain: pin.toolchain_version ?? 'unknown',
88
+ sdk,
89
+ sdkVersion: (0, kit_1.sdkVersion)(sdk),
90
+ rust: pin.rust_version ?? 'unknown',
91
+ },
92
+ };
93
+ if (summary) {
94
+ report.capabilitySummary = JSON.parse((0, client_capabilities_1.canonicalJson)(summary));
95
+ report.capabilityHash = (0, client_capabilities_1.capabilityHash)(summary);
96
+ report.tickIntervalMs = (0, exec_source_1.clientTickIntervalMs)(crate.source.files['Cargo.toml']);
97
+ }
98
+ return report;
99
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readCrate = readCrate;
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const kit_1 = require("./kit");
7
+ const SKIP_DIRS = new Set(['target', '.git', 'node_modules']);
8
+ function walk(dir, rel, out) {
9
+ for (const entry of fs.readdirSync(path.join(dir, rel), { withFileTypes: true })) {
10
+ const child = rel ? `${rel}/${entry.name}` : entry.name;
11
+ if (entry.isDirectory()) {
12
+ if (!SKIP_DIRS.has(entry.name))
13
+ walk(dir, child, out);
14
+ }
15
+ else if (entry.isFile()) {
16
+ out.push(child);
17
+ }
18
+ }
19
+ }
20
+ function sent(rel) {
21
+ return rel === 'Cargo.toml' || rel === 'README.md' || (rel.startsWith('src/') && rel.endsWith('.rs'));
22
+ }
23
+ /** A CLIENT half is the crate that depends on `crowdy-client-sdk`. */
24
+ function detectKind(manifest) {
25
+ let section = '';
26
+ for (const raw of manifest.split(/\r?\n/)) {
27
+ const line = raw.replace(/#.*$/, '').trim();
28
+ const header = line.match(/^\[([^\]]+)\]$/);
29
+ if (header)
30
+ section = header[1].trim();
31
+ else if (section === 'dependencies' && /^crowdy-client-sdk\s*=/.test(line))
32
+ return 'client';
33
+ }
34
+ return 'exec';
35
+ }
36
+ function readCrate(dir, kind) {
37
+ const root = path.resolve(dir);
38
+ const manifestPath = path.join(root, 'Cargo.toml');
39
+ if (!fs.existsSync(manifestPath))
40
+ throw new Error(`${root} has no Cargo.toml`);
41
+ const all = [];
42
+ walk(root, '', all);
43
+ const files = {};
44
+ const notSent = [];
45
+ for (const rel of all.sort()) {
46
+ if (sent(rel))
47
+ files[rel] = fs.readFileSync(path.join(root, rel), 'utf8');
48
+ else
49
+ notSent.push(rel);
50
+ }
51
+ const manifest = files['Cargo.toml'];
52
+ const name = (0, kit_1.manifestValue)(manifest, 'package', 'name');
53
+ if (!name)
54
+ throw new Error('Cargo.toml has no [package] name');
55
+ const libName = (0, kit_1.manifestValue)(manifest, 'lib', 'name') ?? name.replace(/-/g, '_');
56
+ return {
57
+ dir: root,
58
+ name,
59
+ libName,
60
+ kind: kind ?? detectKind(manifest),
61
+ source: { name, files },
62
+ notSent,
63
+ };
64
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.doctor = doctor;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const kit_1 = require("./kit");
6
+ const tools_1 = require("./tools");
7
+ function output(cmd, args) {
8
+ const r = (0, node_child_process_1.spawnSync)(cmd, args, { encoding: 'utf8' });
9
+ return r.status === 0 ? r.stdout.trim() : null;
10
+ }
11
+ /** What a build on this machine needs, and which platform toolchain this kit matches. */
12
+ function doctor() {
13
+ const pin = (0, kit_1.readPin)();
14
+ const rust = pin.rust_version ?? '';
15
+ const findings = [];
16
+ const node = Number(process.versions.node.split('.')[0]);
17
+ findings.push({ ok: node >= 20, what: 'node', detail: `${process.versions.node} (20 or later)` });
18
+ const cargo = (0, tools_1.which)('cargo') ? output('cargo', ['--version']) : null;
19
+ findings.push({ ok: Boolean(cargo), what: 'cargo', detail: cargo ?? 'not installed: https://rustup.rs' });
20
+ if ((0, tools_1.which)('rustup')) {
21
+ const toolchains = output('rustup', ['toolchain', 'list']) ?? '';
22
+ const installed = toolchains.split('\n').some((l) => l.startsWith(`${rust}-`) || l.startsWith(`${rust} `));
23
+ findings.push({
24
+ ok: installed,
25
+ what: `Rust ${rust}`,
26
+ detail: installed ? 'installed' : `missing: rustup toolchain install ${rust} --target wasm32-unknown-unknown`,
27
+ });
28
+ const targets = installed ? (output('rustup', ['target', 'list', '--installed', '--toolchain', rust]) ?? '') : '';
29
+ const wasm = targets.split('\n').includes('wasm32-unknown-unknown');
30
+ findings.push({
31
+ ok: wasm,
32
+ what: 'wasm32-unknown-unknown',
33
+ detail: wasm ? 'installed' : `missing: rustup target add --toolchain ${rust} wasm32-unknown-unknown`,
34
+ });
35
+ }
36
+ else {
37
+ findings.push({ ok: false, what: 'rustup', detail: 'not installed: the kit pins the Rust release the platform builds with' });
38
+ }
39
+ const opt = (0, tools_1.which)('wasm-opt') ? output('wasm-opt', ['--version']) : null;
40
+ findings.push({
41
+ ok: Boolean(opt),
42
+ what: 'wasm-opt',
43
+ detail: opt ?? 'not installed (needed for CLIENT halves): apt install binaryen, brew install binaryen',
44
+ });
45
+ findings.push({
46
+ ok: true,
47
+ what: 'kit',
48
+ detail: `ckx-kit ${(0, kit_1.kitVersion)()}: platform toolchain ${pin.toolchain_version ?? 'unknown'}, ckx-sdk ${(0, kit_1.sdkVersion)('ckx-sdk')}, crowdy-client-sdk ${(0, kit_1.sdkVersion)('crowdy-client-sdk')}, Rust ${rust}`,
49
+ });
50
+ return { findings, ok: findings.every((f) => f.ok) };
51
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KIT_ROOT = void 0;
4
+ exports.kitPath = kitPath;
5
+ exports.readPin = readPin;
6
+ exports.kitVersion = kitVersion;
7
+ exports.manifestValue = manifestValue;
8
+ exports.sdkVersion = sdkVersion;
9
+ exports.sdkRequirement = sdkRequirement;
10
+ const fs = require("node:fs");
11
+ const path = require("node:path");
12
+ /** The package root: this file runs from `dist/cli/src/`. */
13
+ exports.KIT_ROOT = path.resolve(__dirname, '..', '..', '..');
14
+ function kitPath(...parts) {
15
+ return path.join(exports.KIT_ROOT, ...parts);
16
+ }
17
+ /** `kit.pin`: the platform toolchain release this kit was copied from. */
18
+ function readPin() {
19
+ const pin = {};
20
+ const file = kitPath('kit.pin');
21
+ if (!fs.existsSync(file))
22
+ return pin;
23
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
24
+ const m = line.match(/^([a-z_]+)=(.*)$/);
25
+ if (m)
26
+ pin[m[1]] = m[2];
27
+ }
28
+ return pin;
29
+ }
30
+ function kitVersion() {
31
+ return JSON.parse(fs.readFileSync(kitPath('package.json'), 'utf8')).version;
32
+ }
33
+ /** `[section] key = "value"` from a Cargo manifest; one-line values only, as the platform takes. */
34
+ function manifestValue(manifest, section, key) {
35
+ let current = '';
36
+ for (const raw of manifest.split(/\r?\n/)) {
37
+ const line = raw.replace(/#.*$/, '').trim();
38
+ const header = line.match(/^\[([^\]]+)\]$/);
39
+ if (header) {
40
+ current = header[1].trim();
41
+ continue;
42
+ }
43
+ if (current !== section)
44
+ continue;
45
+ const kv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"$/);
46
+ if (kv && kv[1] === key)
47
+ return kv[2];
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * The version of an SDK this kit carries. A release stamps the version published for its tier
53
+ * (`0.8.0-dev.1`, `0.8.0-test.1`, `0.8.0`); a checkout of the kit carries the bare one.
54
+ */
55
+ function sdkVersion(sdk) {
56
+ const manifest = fs.readFileSync(kitPath('crates', sdk, 'Cargo.toml'), 'utf8');
57
+ const version = manifestValue(manifest, 'package', 'version');
58
+ if (!version)
59
+ throw new Error(`crates/${sdk}/Cargo.toml has no version`);
60
+ return version;
61
+ }
62
+ /** How a crate names this kit's SDK: a prerelease is chosen only by naming it exactly. */
63
+ function sdkRequirement(sdk) {
64
+ const version = sdkVersion(sdk);
65
+ return version.includes('-') ? `=${version}` : version;
66
+ }
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const path = require("node:path");
5
+ const node_util_1 = require("node:util");
6
+ const build_1 = require("./build");
7
+ const crate_1 = require("./crate");
8
+ const doctor_1 = require("./doctor");
9
+ const kit_1 = require("./kit");
10
+ const new_1 = require("./new");
11
+ const USAGE = `ckx-kit: build, check and test ck-exec code as the Crowded Kingdoms platform builds it.
12
+
13
+ ckx-kit new <template> <name> [--dir <path>]
14
+ Start a crate. Templates: ${Object.keys(new_1.TEMPLATES).join(', ')}.
15
+ ckx-kit check [dir] [--client | --exec]
16
+ The platform's source checks, on exactly the files it takes (Cargo.toml, README.md,
17
+ and .rs files under src/).
18
+ ckx-kit build [dir] [--mod | --client | --exec] [--out <file>] [--json]
19
+ What execBuild does: the checks, the pinned Rust for wasm32-unknown-unknown with the
20
+ platform's release profile, then the module check. --mod adds the mod limits; a CLIENT
21
+ half (a crowdy-client-sdk crate) is metered and optimized, and its capability summary
22
+ printed.
23
+ ckx-kit doctor
24
+ What a build here needs, and the platform toolchain this kit matches.
25
+
26
+ The platform still builds what you deploy from its source: execModBuild, execModClientBuild,
27
+ execBuild or Crowdy Studio. Docs: https://docs.crowdedkingdoms.com/exec/intro`;
28
+ function kindOf(values) {
29
+ if (values.client && (values.exec || values.mod))
30
+ throw new Error('--client is a CLIENT half; it cannot be --exec or --mod');
31
+ if (values.client)
32
+ return 'client';
33
+ if (values.exec || values.mod)
34
+ return 'exec';
35
+ return undefined;
36
+ }
37
+ async function main(argv) {
38
+ const { values, positionals } = (0, node_util_1.parseArgs)({
39
+ args: argv,
40
+ allowPositionals: true,
41
+ options: {
42
+ mod: { type: 'boolean' },
43
+ client: { type: 'boolean' },
44
+ exec: { type: 'boolean' },
45
+ out: { type: 'string' },
46
+ dir: { type: 'string' },
47
+ json: { type: 'boolean' },
48
+ help: { type: 'boolean', short: 'h' },
49
+ version: { type: 'boolean', short: 'v' },
50
+ },
51
+ });
52
+ const [command, ...rest] = positionals;
53
+ if (values.version) {
54
+ console.log((0, kit_1.kitVersion)());
55
+ return 0;
56
+ }
57
+ if (values.help || !command) {
58
+ console.log(USAGE);
59
+ return command || values.help ? 0 : 2;
60
+ }
61
+ switch (command) {
62
+ case 'new': {
63
+ const [template, name] = rest;
64
+ if (!template || !name)
65
+ throw new Error('usage: ckx-kit new <template> <name> [--dir <path>]');
66
+ const dir = (0, new_1.newProject)(template, name, values.dir);
67
+ const rel = path.relative(process.cwd(), dir) || '.';
68
+ const flag = template === 'mod' ? ' --mod' : '';
69
+ console.log(`created ${rel} (${new_1.TEMPLATES[template].about})\n\n cd ${rel}\n cargo test\n npx ckx-kit build${flag}`);
70
+ return 0;
71
+ }
72
+ case 'check': {
73
+ const crate = (0, crate_1.readCrate)(rest[0] ?? '.', kindOf(values));
74
+ (0, build_1.check)(crate);
75
+ console.log(`${crate.name}: passes the platform's checks (${crate.kind === 'client' ? 'a CLIENT half' : 'a ck-exec crate'})`);
76
+ if (crate.notSent.length)
77
+ console.log(`not sent to the platform: ${crate.notSent.join(', ')}`);
78
+ return 0;
79
+ }
80
+ case 'build': {
81
+ const crate = (0, crate_1.readCrate)(rest[0] ?? '.', kindOf(values));
82
+ const report = await (0, build_1.build)(crate, { mod: values.mod, out: values.out });
83
+ if (values.json) {
84
+ console.log(JSON.stringify(report, null, 2));
85
+ return 0;
86
+ }
87
+ console.log(`${report.crate}: ${report.sizeBytes} bytes, sha256 ${report.sha256}\n ${report.wasm}`);
88
+ if (report.capabilitySummary) {
89
+ console.log(` capabilities ${report.capabilityHash}; host calls: ${report.capabilitySummary.hostFunctions.join(', ') || 'none'}; ticks every ${report.tickIntervalMs} ms`);
90
+ }
91
+ if (report.notSent.length)
92
+ console.log(` not sent to the platform: ${report.notSent.join(', ')}`);
93
+ console.log(` built as platform toolchain ${report.kit.toolchain} builds it (${report.kit.sdk} ${report.kit.sdkVersion}, Rust ${report.kit.rust})`);
94
+ return 0;
95
+ }
96
+ case 'doctor': {
97
+ const { findings, ok } = (0, doctor_1.doctor)();
98
+ for (const f of findings)
99
+ console.log(`${f.ok ? 'ok ' : 'FAIL'} ${f.what}: ${f.detail}`);
100
+ return ok ? 0 : 1;
101
+ }
102
+ default:
103
+ throw new Error(`unknown command '${command}'\n\n${USAGE}`);
104
+ }
105
+ }
106
+ main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
107
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
108
+ process.exit(1);
109
+ });