@clunkyard/cli 0.0.0-stage → 2.0.0
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 +21 -0
- package/README.md +54 -2
- package/archive.cjs +74 -0
- package/catalogue.json +2764 -0
- package/cli.cjs +45 -0
- package/package.json +32 -4
- package/project.cjs +139 -0
- package/runtime.cjs +201 -0
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,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Clunkyard local CLI
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
JavaScript/TypeScript projects use the ordinary npm alias
|
|
29
|
+
`clunkyard@npm:@clunkyard/sdk@2.0.0`. `package-lock.json` pins the reviewed
|
|
30
|
+
integrity. The first build runs `npm ci --ignore-scripts`; the package has no
|
|
31
|
+
install hooks or runtime dependencies. TypeScript uses your installed
|
|
32
|
+
TypeScript 6+ compiler, preferring a project-local installation. Native SDK
|
|
33
|
+
assets currently require Linux x86-64/glibc 2.39+ (Ubuntu 24.04+ or WSL2).
|
|
34
|
+
JavaScript, TypeScript and the Vane/Cadence/Glyph Node routes use WebAssembly.
|
|
35
|
+
|
|
36
|
+
`@2` receives compatible tooling updates. For an exact tool version, run
|
|
37
|
+
`npx --yes @clunkyard/cli@2.0.0` or set
|
|
38
|
+
`CLUNKYARD='npx --yes @clunkyard/cli@2.0.0'` with Make. Commit the npm lockfile
|
|
39
|
+
for reproducible JavaScript/TypeScript dependencies. Match replays record the
|
|
40
|
+
game rules and robot actions; local timings do not qualify hosted budgets.
|
|
41
|
+
|
|
42
|
+
For an older generated kit checkout, commit your work and run
|
|
43
|
+
`npx --yes @clunkyard/cli@2 migrate`. It removes only byte-recognized generated
|
|
44
|
+
helper files, preserves robot source and custom README text, and refuses to
|
|
45
|
+
overwrite customized build scripts or dependency manifests. Historical kit
|
|
46
|
+
archives remain available with their original tooling.
|
|
47
|
+
|
|
48
|
+
Robot builds and local matches execute programs as your user. Use trusted
|
|
49
|
+
source and tools. For online play, use
|
|
50
|
+
[`@clunkyard/git`](https://www.npmjs.com/package/@clunkyard/git) or Clunkyard's
|
|
51
|
+
GitHub integration, then run hosted trials before entering a tournament.
|
|
52
|
+
|
|
53
|
+
[SDK and adapters](https://clunkyard.com/sdk) ·
|
|
54
|
+
[Local trials](https://clunkyard.com/sdk/local-trials) ·
|
|
55
|
+
[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 };
|