@clunkyard/cli 0.0.0-stage → 2.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clunkyard contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,59 @@
1
- # Temporary Holding Version
1
+ # Clunkyard local CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Build a robot, play a real local match and inspect its replay with Node 20+,
4
+ npm and your existing language toolchain:
5
+
6
+ ```sh
7
+ npx --yes @clunkyard/cli@2 new javascript my-robot
8
+ cd my-robot
9
+ npx --yes @clunkyard/cli@2 doctor
10
+ npx --yes @clunkyard/cli@2 build
11
+ npx --yes @clunkyard/cli@2 match 42 2d classic
12
+ npx --yes @clunkyard/cli@2 replay
13
+ ```
14
+
15
+ Or clone any [language starter](https://github.com/clunkyard/game-kit) and use
16
+ `make doctor`, `make`, `make match` and `make replay`. A project contains robot
17
+ source, `bot.json` and normal language build metadata. The shared CLI owns the
18
+ tooling; it does not copy a bootstrapper, archive reader or custom lock file
19
+ into every repository. `--project DIRECTORY` works from another directory.
20
+
21
+ The package contains the reviewed SDK 2.0.0/protocol 2 catalogue, checksums and
22
+ build recipes from the immutable `v2.0.0-kit.2` release. It downloads only the
23
+ engine and selected language SDK. It never downloads compilers or the
24
+ all-language builder. SDK/engine caches under ignored `.clunkyard/` are keyed
25
+ by their content hashes and remain reusable across CLI updates. Other
26
+ commands are `list`, `setup`, `clean` and `migrate`.
27
+
28
+ Local matches use a resource mode supported by `bot.json`. Open-capable robots
29
+ use Open; Wasm- or DSL-only robots keep their fuel or operation meter and play
30
+ the bundled Vane rival with its DSL meter.
31
+
32
+ JavaScript/TypeScript projects use the ordinary npm alias
33
+ `clunkyard@npm:@clunkyard/sdk@2.0.0`. `package-lock.json` pins the reviewed
34
+ integrity. The first build runs `npm ci --ignore-scripts`; the package has no
35
+ install hooks or runtime dependencies. TypeScript uses your installed
36
+ TypeScript 6+ compiler, preferring a project-local installation. Native SDK
37
+ assets currently require Linux x86-64/glibc 2.39+ (Ubuntu 24.04+ or WSL2).
38
+ JavaScript, TypeScript and the Vane/Cadence/Glyph Node routes use WebAssembly.
39
+
40
+ `@2` receives compatible tooling updates. For an exact tool version, run
41
+ `npx --yes @clunkyard/cli@2.0.1` or set
42
+ `CLUNKYARD='npx --yes @clunkyard/cli@2.0.1'` with Make. Commit the npm lockfile
43
+ for reproducible JavaScript/TypeScript dependencies. Match replays record the
44
+ game rules and robot actions; local timings do not qualify hosted budgets.
45
+
46
+ For an older generated kit checkout, commit your work and run
47
+ `npx --yes @clunkyard/cli@2 migrate`. It removes only byte-recognized generated
48
+ helper files, preserves robot source and custom README text, and refuses to
49
+ overwrite customized build scripts or dependency manifests. Historical kit
50
+ archives remain available with their original tooling.
51
+
52
+ Robot builds and local matches execute programs as your user. Use trusted
53
+ source and tools. For online play, use
54
+ [`@clunkyard/git`](https://www.npmjs.com/package/@clunkyard/git) or Clunkyard's
55
+ GitHub integration, then run hosted trials before entering a tournament.
56
+
57
+ [SDK and adapters](https://clunkyard.com/sdk) ·
58
+ [Local trials](https://clunkyard.com/sdk/local-trials) ·
59
+ [Private Git](https://clunkyard.com/sdk/private-git)
package/archive.cjs ADDED
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const zlib = require('node:zlib');
6
+ const crypto = require('node:crypto');
7
+
8
+ async function readAsset(asset) {
9
+ let bytes;
10
+ if(process.env.CLUNKYARD_RELEASE_DIRECTORY)bytes=fs.readFileSync(path.join(process.env.CLUNKYARD_RELEASE_DIRECTORY,asset.file));
11
+ else {
12
+ const response=await fetch(asset.url,{signal:AbortSignal.timeout(120000)});
13
+ if(!response.ok)throw new Error(`Download failed (${response.status}): ${asset.url}`);
14
+ const chunks=[];let size=0;
15
+ for await(const chunk of response.body){size+=chunk.length;if(size>asset.bytes)throw new Error('Release download exceeds its recorded size');chunks.push(chunk);}
16
+ bytes=Buffer.concat(chunks,size);
17
+ }
18
+ if(bytes.length!==asset.bytes||crypto.createHash('sha256').update(bytes).digest('hex')!==asset.sha256)throw new Error(`Release checksum mismatch: ${asset.file}`);
19
+ return bytes;
20
+ }
21
+
22
+ // Release archives contain ordinary files and directories only. They never
23
+ // carry checkout hooks, symlinks or absolute paths.
24
+ function archive(root) {
25
+ const parts = [];
26
+ function visit(directory, relative = '') {
27
+ for (const item of fs.readdirSync(directory, { withFileTypes:true }).sort((a,b) => a.name.localeCompare(b.name))) {
28
+ const name = relative ? `${relative}/${item.name}` : item.name;
29
+ const file = path.join(directory, item.name), stat = fs.statSync(file);
30
+ if (stat.isDirectory()) { visit(file, name); continue; }
31
+ if (!stat.isFile()) throw new Error(`Unsupported release file: ${name}`);
32
+ const data = fs.readFileSync(file), header = Buffer.alloc(512);
33
+ const slash = name.lastIndexOf('/'), leaf = name.length > 100 ? name.slice(slash + 1) : name, prefix = name.length > 100 ? name.slice(0, slash) : '';
34
+ if (Buffer.byteLength(leaf) > 100 || Buffer.byteLength(prefix) > 155) throw new Error(`Release path too long: ${name}`);
35
+ header.write(leaf, 0, 100); header.write(`${(stat.mode & 0o111 ? 0o755 : 0o644).toString(8).padStart(7,'0')}\0`, 100);
36
+ header.write('0000000\0',108); header.write('0000000\0',116);
37
+ header.write(`${data.length.toString(8).padStart(11,'0')}\0`,124); header.write('00000000000\0',136);
38
+ header.fill(32,148,156); header[156]=48; header.write('ustar\0',257); header.write('00',263); header.write(prefix,345,155);
39
+ const checksum = [...header].reduce((n,b) => n+b,0); header.write(`${checksum.toString(8).padStart(6,'0')}\0 `,148);
40
+ parts.push(header,data,Buffer.alloc((512-data.length%512)%512));
41
+ }
42
+ }
43
+ visit(root); parts.push(Buffer.alloc(1024)); return zlib.gzipSync(Buffer.concat(parts), { level:9 });
44
+ }
45
+
46
+ function extract(bytes, root, maxBytes = 256 * 1024 * 1024) {
47
+ const data = zlib.gunzipSync(bytes, { maxOutputLength:maxBytes });
48
+ let cursor = 0, count = 0;
49
+ while (cursor + 512 <= data.length) {
50
+ const header = data.subarray(cursor,cursor+512); cursor += 512;
51
+ if (header.every(byte => byte===0)) return;
52
+ const text = (start,end) => header.subarray(start,end).toString('utf8').replace(/\0.*$/s,'');
53
+ const leaf = text(0,100), prefix = text(345,500), name = prefix ? `${prefix}/${leaf}` : leaf;
54
+ if (++count > 10000 || !name || name.includes('\\') || name.split('/').some(part => !part || part==='.' || part==='..' || part.toLowerCase()==='.git') || path.isAbsolute(name)) throw new Error('Unsafe release archive path');
55
+ if (![0,48].includes(header[156])) throw new Error('Release archive contains a link or unsupported object');
56
+ const expected = parseInt(text(148,156).trim(),8), copy = Buffer.from(header); copy.fill(32,148,156);
57
+ if (expected !== [...copy].reduce((n,b) => n+b,0)) throw new Error('Release archive header failed verification');
58
+ const size = parseInt(text(124,136).trim(),8);
59
+ if (!Number.isSafeInteger(size) || size < 0 || cursor+size > data.length) throw new Error('Invalid release archive size');
60
+ let parent = root;
61
+ for (const part of name.split('/').slice(0,-1)) {
62
+ parent = path.join(parent,part);
63
+ if (fs.existsSync(parent) && (!fs.lstatSync(parent).isDirectory() || fs.lstatSync(parent).isSymbolicLink())) throw new Error('Unsafe release directory');
64
+ fs.mkdirSync(parent,{recursive:true});
65
+ }
66
+ const file = path.join(root,...name.split('/'));
67
+ if (fs.existsSync(file)) throw new Error('Release archive repeats an existing path');
68
+ fs.writeFileSync(file,data.subarray(cursor,cursor+size),{mode:parseInt(text(100,108),8)&0o111?0o755:0o644,flag:'wx'});
69
+ cursor += Math.ceil(size/512)*512;
70
+ }
71
+ throw new Error('Incomplete release archive');
72
+ }
73
+
74
+ module.exports = { archive, extract, readAsset };