@clunkyard/cli 2.0.0 → 2.1.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/README.md +14 -5
- package/catalogue.json +518 -288
- package/package.json +1 -1
- package/project.cjs +3 -4
- package/runtime.cjs +95 -17
package/package.json
CHANGED
package/project.cjs
CHANGED
|
@@ -41,12 +41,11 @@ function makefile() {
|
|
|
41
41
|
|
|
42
42
|
function readme(language,entry) {
|
|
43
43
|
const row=catalogue.languages[language],portable=nodeLanguage(language)||row.portable;
|
|
44
|
-
const tools=row.tools.filter(tool=>!['node','make'].includes(tool));
|
|
45
44
|
const guide=language==='typescript'?'javascript':language==='wat'||language.endsWith('-wasm')?'wasm':language;
|
|
46
45
|
return `# A Clunkyard robot in ${language}
|
|
47
46
|
|
|
48
|
-
Edit \`${entry}\`, build, play a real local match and inspect its replay. Install Node 20+ with npm
|
|
49
|
-
${portable?'This language uses the portable Node/WebAssembly route.':'
|
|
47
|
+
Edit \`${entry}\`, build, play a real local match and inspect its replay. Install [Node 20+ with npm](https://nodejs.org/en/download) and your language's normal compiler or interpreter.
|
|
48
|
+
${portable?'This language uses the portable Node/WebAssembly route.':'Check the [supported operating systems and toolchain installers](https://clunkyard.com/sdk/player-kit#supported-platforms) for this language. Hosted builds and trials are available when its local toolchain or SDK is unavailable on your computer.'}
|
|
50
49
|
|
|
51
50
|
\`\`\`sh
|
|
52
51
|
git clone https://github.com/clunkyard/starter-${language}.git
|
|
@@ -72,7 +71,7 @@ npx --yes @clunkyard/cli@2 match 77 3d zero-g
|
|
|
72
71
|
|
|
73
72
|
Keep a baseline commit. Repeat several seeds and rulesets; compare contacts, battery, aim, shots, hits and eliminations in the replay. Local trials run programs as your user. Use hosted trials to measure arena budgets before a tournament.
|
|
74
73
|
|
|
75
|
-
The CLI packages SDK ${catalogue.sdkVersion}/protocol 2 asset identities from the immutable [${catalogue.release} release](https://github.com/clunkyard/game-kit/releases/tag/${catalogue.release}). \`@2\` receives compatible tooling fixes without copying scripts into your robot repository. To reproduce an exact tool version, use \`npx --yes @clunkyard/cli@2.
|
|
74
|
+
The CLI packages SDK ${catalogue.sdkVersion}/protocol 2 asset identities from the immutable [${catalogue.release} release](https://github.com/clunkyard/game-kit/releases/tag/${catalogue.release}). \`@2\` receives compatible tooling fixes without copying scripts into your robot repository. To reproduce an exact tool version, use \`npx --yes @clunkyard/cli@2.1.0\` or set \`CLUNKYARD='npx --yes @clunkyard/cli@2.1.0'\` with Make. SDK/engine caches are identified by their content hashes and remain reusable across CLI updates.
|
|
76
75
|
|
|
77
76
|
## Play online
|
|
78
77
|
|
package/runtime.cjs
CHANGED
|
@@ -6,28 +6,78 @@ const {extract,readAsset}=require('./archive.cjs');
|
|
|
6
6
|
const catalogue=require('./catalogue.json');
|
|
7
7
|
const {readManifest}=require('./project.cjs');
|
|
8
8
|
|
|
9
|
+
function selectPlatformSdk(lock,platform=process.platform,arch=process.arch) {
|
|
10
|
+
if(lock.portable)return lock.sdk;
|
|
11
|
+
const key=`${platform}-${arch}`,asset=lock.sdkPlatforms?.[key];
|
|
12
|
+
if(asset)return asset;
|
|
13
|
+
if(key==='linux-x64')return lock.sdk;
|
|
14
|
+
throw new Error(`No native ${lock.language} SDK is released for ${key}. Use a hosted build or trial, or install a matching SDK with CLUNKYARD_SDK. See the language guide for toolchain requirements.`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Execute the worker's environment-prefixed recipes without requiring Unix env
|
|
18
|
+
// on the player's machine. Arguments stay separate, including paths with spaces.
|
|
19
|
+
function recipeInvocation(command,args,{cwd,env}) {
|
|
20
|
+
if(command!=='env')return {command,args,cwd,env};
|
|
21
|
+
args=[...args];env={...env};
|
|
22
|
+
if(args[0]==='--chdir'){args.shift();cwd=args.shift();}
|
|
23
|
+
while(args.length&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(args[0])){
|
|
24
|
+
const value=args.shift(),index=value.indexOf('=');env[value.slice(0,index)]=value.slice(index+1);
|
|
25
|
+
}
|
|
26
|
+
command=args.shift();
|
|
27
|
+
if(!command)throw new Error('The local recipe has no executable.');
|
|
28
|
+
return {command,args,cwd,env};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function bindingCompilerIdentity(lock,environment,probe=spawnSync,platform=process.platform){
|
|
32
|
+
return lock.tools.filter(tool=>!['node','make'].includes(tool)&&!(platform==='win32'&&lock.language==='rust'&&tool==='cc')).map(tool=>{
|
|
33
|
+
const variable={rustc:'RUSTC',cc:'CC','c++':'CXX',swiftc:'SWIFTC'}[tool];
|
|
34
|
+
const command=variable&&environment[variable]||tool;
|
|
35
|
+
const result=probe(command,[tool==='javac'?'-version':'--version'],{env:environment,encoding:'utf8',timeout:10000,maxBuffer:64*1024});
|
|
36
|
+
return {command,status:result.status,version:(String(result.stdout||'')+String(result.stderr||'')).slice(0,4096),error:result.error?.code||null};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function selectLocalResourceMode(bot,supported) {
|
|
41
|
+
if(supported.includes('open'))return 'open';
|
|
42
|
+
const metered=supported.find(mode=>mode==='dsl'||mode==='wasm');
|
|
43
|
+
if(metered)return metered;
|
|
44
|
+
throw new Error(`${bot.name} supports resource modes [${supported.join(', ')}]. Local trials need open, dsl or wasm; use a hosted trial for trusted budgets.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
9
47
|
function createRuntime(project,entry=path.join(__dirname,'cli.cjs')) {
|
|
10
48
|
const root=path.resolve(project),initial=readManifest(root),lock=catalogue.languages[initial.language];
|
|
11
49
|
const cache=path.join(root,'.clunkyard');
|
|
12
50
|
const engine=path.join(cache,'engine-'+lock.engine.sha256.slice(0,16));
|
|
13
|
-
const sdk=process.env.CLUNKYARD_SDK||path.join(cache,'sdk-'+lock.sdk.sha256.slice(0,16));
|
|
14
51
|
const npmSDK=['javascript','typescript'].includes(initial.language)?catalogue.npmSDK:null;
|
|
52
|
+
const suppliedSdk=process.env.CLUNKYARD_SDK?path.resolve(process.env.CLUNKYARD_SDK):null;
|
|
53
|
+
let sdkAsset,platformError,suppliedMetadata;
|
|
54
|
+
try{sdkAsset=npmSDK?lock.sdk:selectPlatformSdk(lock);}catch(error){platformError=error;}
|
|
55
|
+
if(suppliedSdk&&fs.existsSync(path.join(suppliedSdk,'platform.json')))suppliedMetadata=JSON.parse(fs.readFileSync(path.join(suppliedSdk,'platform.json'),'utf8'));
|
|
56
|
+
const sdk=suppliedSdk||path.join(cache,'sdk-'+(sdkAsset?.sha256.slice(0,16)||'unavailable'));
|
|
57
|
+
const library=suppliedMetadata?.library||sdkAsset?.library||({darwin:'libclunkyard.2.dylib',win32:'clunkyard.dll'}[process.platform]||'libclunkyard.so.2');
|
|
58
|
+
if(path.basename(library)!==library||library.includes('\\'))throw new Error('SDK library must be a file name.');
|
|
15
59
|
const manifest=()=>readManifest(root);
|
|
16
60
|
const sha=data=>crypto.createHash('sha256').update(data).digest('hex');
|
|
17
61
|
const skip=new Set(['.git','.clunkyard','build','replays','node_modules','__pycache__','obj','bin']);
|
|
18
62
|
|
|
19
63
|
function run(command, args, options={}) {
|
|
20
|
-
const
|
|
64
|
+
const invocation=recipeInvocation(command,args,{cwd:options.cwd||root,env:options.env||environment()});
|
|
65
|
+
command=invocation.command;
|
|
66
|
+
if(process.platform==='win32'&&command==='python3')command=process.env.PYTHON||'python';
|
|
67
|
+
const result=spawnSync(command,invocation.args,{stdio:'inherit',...options,cwd:invocation.cwd,env:invocation.env});
|
|
21
68
|
if(result.error)throw new Error(`${command}: ${result.error.message}. See README.md for this language's toolchain.`);
|
|
22
69
|
if(result.status!==0)throw new Error(`${command} exited with status ${result.status}`);
|
|
23
70
|
return result;
|
|
24
71
|
}
|
|
25
72
|
function environment() {
|
|
26
73
|
const paths=[path.join(sdk,'lib'),process.env.LD_LIBRARY_PATH].filter(Boolean).join(path.delimiter);
|
|
27
|
-
return {...process.env,CLUNKYARD_SDK:sdk,CLUNKYARD_PROJECT:root,CLUNKYARD_LIB:path.join(sdk,'lib',
|
|
74
|
+
return {...process.env,CLUNKYARD_SDK:sdk,CLUNKYARD_PROJECT:root,CLUNKYARD_LIB:path.join(sdk,'lib',library),LD_LIBRARY_PATH:paths,
|
|
75
|
+
...(process.platform==='darwin'?{DYLD_LIBRARY_PATH:[path.join(sdk,'lib'),process.env.DYLD_LIBRARY_PATH].filter(Boolean).join(path.delimiter)}:{}),
|
|
76
|
+
...(process.platform==='win32'?{PATH:[path.join(sdk,'lib'),process.env.PATH].filter(Boolean).join(path.delimiter),CC:cCompiler(false),CXX:cCompiler(true)}:{}),
|
|
28
77
|
TMPDIR:path.join(cache,'tmp'),NODE_PATH:npmSDK?path.join(root,'node_modules'):path.join(sdk,'bindings','javascript'),
|
|
29
78
|
DOTNET_CLI_TELEMETRY_OPTOUT:'1',DOTNET_NOLOGO:'1'};
|
|
30
79
|
}
|
|
80
|
+
function cCompiler(cpp){return process.env[cpp?'CXX':'CC']||(process.platform==='win32'?(toolAvailable(cpp?'clang++':'clang')?(cpp?'clang++':'clang'):'cl'):(cpp?'c++':'cc'));}
|
|
31
81
|
function safeCache() {
|
|
32
82
|
if(fs.existsSync(cache)&&(!fs.lstatSync(cache).isDirectory()||fs.lstatSync(cache).isSymbolicLink()))throw new Error('.clunkyard must be an ordinary directory');
|
|
33
83
|
fs.mkdirSync(cache,{recursive:true});fs.mkdirSync(path.join(cache,'tmp'),{recursive:true});
|
|
@@ -43,12 +93,21 @@ async function download(asset, destination) {
|
|
|
43
93
|
}
|
|
44
94
|
async function setup() {
|
|
45
95
|
safeCache();
|
|
46
|
-
|
|
96
|
+
checkNativePlatform();
|
|
47
97
|
await download(lock.engine,engine);
|
|
48
98
|
if(npmSDK)installNpmSDK();
|
|
49
|
-
else if(!
|
|
99
|
+
else if(!suppliedSdk)await download(sdkAsset,sdk);
|
|
50
100
|
else if(fs.readFileSync(path.join(sdk,'VERSION'),'utf8').trim()!==lock.sdkVersion)throw new Error('CLUNKYARD_SDK points to another SDK version');
|
|
51
101
|
}
|
|
102
|
+
function checkNativePlatform(){
|
|
103
|
+
if(npmSDK||lock.portable)return;
|
|
104
|
+
if(suppliedSdk){
|
|
105
|
+
if(suppliedMetadata&&suppliedMetadata.platform!==`${process.platform}-${process.arch}`)throw new Error(`CLUNKYARD_SDK targets ${suppliedMetadata.platform}, not ${process.platform}-${process.arch}.`);
|
|
106
|
+
if(!suppliedMetadata&&process.platform!=='linux')throw new Error('A custom native SDK on this platform needs platform.json from the native SDK builder.');
|
|
107
|
+
}else if(platformError)throw platformError;
|
|
108
|
+
const glibc=process.report?.getReport().header.glibcVersionRuntime;
|
|
109
|
+
if(process.platform==='linux'&&(!glibc||Number(glibc.split('.')[0])<2||(Number(glibc.split('.')[0])===2&&Number(glibc.split('.')[1])<39)))throw new Error('This native Linux SDK needs glibc 2.39+ (Ubuntu 24.04+ or WSL2). Use a hosted build or trial on other systems.');
|
|
110
|
+
}
|
|
52
111
|
function installNpmSDK() {
|
|
53
112
|
const packageFile=path.join(root,'package.json'),lockFile=path.join(root,'package-lock.json');
|
|
54
113
|
if(!fs.existsSync(packageFile)||!fs.existsSync(lockFile))throw new Error('JavaScript/TypeScript projects need the npm SDK dependency and package-lock.json. Run clunkyard migrate for an older generated starter, or follow the SDK npm guide.');
|
|
@@ -87,7 +146,9 @@ function rebuildBinding() {
|
|
|
87
146
|
// This downloads no compiler and keeps native modules compatible locally.
|
|
88
147
|
for(const binding of npmSDK?[]:(lock.rebuildBindings||[])){
|
|
89
148
|
const directory=path.join(sdk,'binding-src',binding);
|
|
90
|
-
|
|
149
|
+
const installer=path.join(directory,'install.cjs');
|
|
150
|
+
if(fs.existsSync(installer))run(process.execPath,[installer,sdk]);
|
|
151
|
+
else run('make',['-C',directory,'install',`SDK=${sdk}`,`ROOT=${engine}`,`BUILD=${path.join(cache,'tmp','binding-'+binding)}`]);
|
|
91
152
|
}
|
|
92
153
|
}
|
|
93
154
|
function typescriptCompiler() {
|
|
@@ -108,11 +169,11 @@ function typescriptCompiler() {
|
|
|
108
169
|
}
|
|
109
170
|
async function build() {
|
|
110
171
|
await setup();const m=manifest(),out=path.join(cache,'build');fs.mkdirSync(out,{recursive:true});
|
|
111
|
-
const bindingStamp=path.join(cache,'bindings-ready'),bindingIdentity=npmSDK?.integrity||
|
|
172
|
+
const bindingStamp=path.join(cache,'bindings-ready'),bindingIdentity=npmSDK?.integrity||sha(Buffer.from(JSON.stringify({asset:suppliedSdk?{sdk,metadata:suppliedMetadata||{},version:lock.sdkVersion}:sdkAsset.sha256,compilers:lock.rebuildBindings?.length?bindingCompilerIdentity(lock,environment()):[]})));
|
|
112
173
|
if(!fs.existsSync(bindingStamp)||fs.readFileSync(bindingStamp,'utf8')!==bindingIdentity){rebuildBinding();fs.writeFileSync(bindingStamp,bindingIdentity);}
|
|
113
174
|
const copied=path.join(cache,'source');fs.rmSync(copied,{recursive:true,force:true});sources(root,copied);
|
|
114
175
|
const action=npmSDK&&m.language==='typescript'?[...typescriptCompiler(),'--ignoreConfig','--ignoreDeprecations','6.0','--target','ES2022','--module','commonjs','--rootDir',copied,'--outDir',out,path.join(copied,m.entry)]:worker(copied).command(m.language,path.join(copied,m.entry),out,m);
|
|
115
|
-
if(action){const steps=Array.isArray(action[0])?action:[action];for(const step of steps){const args=step.map(rewrite);if(m.language==='c')args[0]=
|
|
176
|
+
if(action){const steps=Array.isArray(action[0])?action:[action];for(const step of steps){const args=step.map(rewrite);if(m.language==='c')args[0]=cCompiler(false);if(m.language==='cpp')args[0]=cCompiler(true);run(args[0],args.slice(1),{cwd:copied});}}
|
|
116
177
|
const runtimeSource=path.join(out,'source');fs.rmSync(runtimeSource,{recursive:true,force:true});sources(copied,runtimeSource);
|
|
117
178
|
if(m.language==='typescript')fs.writeFileSync(path.join(out,'package.json'),'{"type":"commonjs"}\n');
|
|
118
179
|
fs.writeFileSync(path.join(cache,'local-bot.json'),JSON.stringify({...m,command:[process.execPath,entry,'--project',root,'stdio']},null,2)+'\n');
|
|
@@ -136,7 +197,24 @@ async function match(args) {
|
|
|
136
197
|
await build();
|
|
137
198
|
fs.mkdirSync(path.join(root,'replays'),{recursive:true});
|
|
138
199
|
const replay=path.join(root,'replays',`trial-${seed}.json`);
|
|
139
|
-
|
|
200
|
+
const {supportedResourceModes}=require(path.join(engine,'src','resources.js'));
|
|
201
|
+
const m=manifest(),resourceMode=selectLocalResourceMode(m,supportedResourceModes(m));
|
|
202
|
+
if(resourceMode==='open'){
|
|
203
|
+
run(process.execPath,[path.join(engine,'src','cli.js'),'match','--bot','@'+path.join(cache,'local-bot.json'),'--bot','@'+path.join(engine,'bots','practice','bot.json'),'--mode',mode,'--ruleset',ruleset,'--resource-mode',resourceMode,'--seed',seed,'--ticks','360','--turn-ms','2000','--startup-ms','60000','--replay',replay]);
|
|
204
|
+
}else{
|
|
205
|
+
// The pinned engine already supports mixed Reflex entries: the player's
|
|
206
|
+
// Wasm/DSL robot and the bundled Vane rival each keep their own meter.
|
|
207
|
+
const {resolveBot,validateSelection}=require(path.join(engine,'src','roster.js'));
|
|
208
|
+
const {runMatch,saveReplay}=require(path.join(engine,'src','match.js'));
|
|
209
|
+
const player=resolveBot('@'+path.join(cache,'local-bot.json'),[],engine,mode,ruleset);
|
|
210
|
+
const rival=resolveBot('@'+path.join(engine,'bots','practice','bot.json'),[],engine,mode,ruleset);
|
|
211
|
+
player.resourcePolicy={mode:resourceMode};rival.resourcePolicy={mode:'dsl'};
|
|
212
|
+
const bots=[player,rival];validateSelection(bots,mode,ruleset);
|
|
213
|
+
const result=await runMatch(bots,{mode,ruleset,seed:Number(seed),maxTicks:360,turnMs:2000,startupMs:60000,resourcePolicy:{v:3,class:'reflex',policy:'fair-compute-v1'}});
|
|
214
|
+
saveReplay(result.replay,replay);
|
|
215
|
+
console.log(`Result: ${result.result?.winners?.join(', ')||'draw'} (${result.result?.reason||'unknown'})`);
|
|
216
|
+
console.log(`Replay: ${replay}`);
|
|
217
|
+
}
|
|
140
218
|
const failures=JSON.parse(fs.readFileSync(replay,'utf8')).failures||[];
|
|
141
219
|
if(failures.length)throw new Error(`Match recorded ${failures.map(value=>`${value.botId}: ${value.code}`).join(', ')}. Inspect ${replay}.`);
|
|
142
220
|
}
|
|
@@ -160,12 +238,13 @@ function toolAvailable(tool){
|
|
|
160
238
|
function doctor(){
|
|
161
239
|
console.log(`Clunkyard SDK ${lock.sdkVersion}; ${lock.language}; ${process.platform}/${process.arch}; Node ${process.versions.node}`);
|
|
162
240
|
let missing=0;
|
|
163
|
-
for(const tool of lock.tools.filter(tool=>tool!=='make'||(!npmSDK&&lock.rebuildBindings?.
|
|
164
|
-
const
|
|
165
|
-
|
|
241
|
+
for(const tool of lock.tools.filter(tool=>!(process.platform==='win32'&&lock.language==='rust'&&tool==='cc')).filter(tool=>tool!=='make'||(!npmSDK&&lock.rebuildBindings?.some(binding=>!sdkAsset?.portableBindingInstallers?.includes(binding)&&!fs.existsSync(path.join(sdk,'binding-src',binding,'install.cjs')))))){
|
|
242
|
+
const command=process.platform==='win32'?({python3:process.env.PYTHON||'python',cc:cCompiler(false),'c++':cCompiler(true)}[tool]||tool):tool;
|
|
243
|
+
const ok=toolAvailable(command);
|
|
244
|
+
console.log(`${ok?'found':'missing'} ${command}`);if(!ok)missing++;
|
|
166
245
|
}
|
|
167
246
|
for(const check of lock.requirements?.checks||[]){
|
|
168
|
-
let command=[check.command];
|
|
247
|
+
let command=[process.platform==='win32'&&check.command==='python3'?(process.env.PYTHON||'python'):check.command];
|
|
169
248
|
if(npmSDK&&check.command==='tsc'){try{command=typescriptCompiler()}catch{}}
|
|
170
249
|
const result=spawnSync(command[0],[...command.slice(1),...check.args],{encoding:'utf8',env:environment(),timeout:10000});
|
|
171
250
|
const output=String(result.stdout||'')+String(result.stderr||'');
|
|
@@ -176,10 +255,9 @@ function doctor(){
|
|
|
176
255
|
if(lock.language==='clojure'){try{console.log('ok Clojure runtime: '+clojureClasspath())}catch(error){console.log('missing '+error.message);missing++}}
|
|
177
256
|
if(lock.language==='scala'&&process.env.SCALA_LIBRARY_JAR&&!fs.existsSync(process.env.SCALA_LIBRARY_JAR)){console.log('missing SCALA_LIBRARY_JAR file');missing++}
|
|
178
257
|
if(lock.language==='scala'&&!process.env.SCALA_LIBRARY_JAR&&!fs.existsSync('/usr/share/scala-2.11/lib/scala-library.jar')&&spawnSync('scala',['-version'],{timeout:10000}).error){console.log('missing Scala runtime; set SCALA_LIBRARY_JAR or install the scala launcher.');missing++}
|
|
179
|
-
|
|
180
|
-
if(!npmSDK&&!lock.portable&&(process.platform!=='linux'||process.arch!=='x64'||!glibc||Number(glibc.split('.')[0])<2||(Number(glibc.split('.')[0])===2&&Number(glibc.split('.')[1])<39))){console.log('unsupported native platform: use Linux x86-64 with glibc 2.39+ (Ubuntu 24.04+ or WSL2).');missing++}
|
|
258
|
+
try{checkNativePlatform()}catch(error){console.log(error.message);missing++}
|
|
181
259
|
for(const note of lock.requirements?.notes||[])console.log(note);
|
|
182
|
-
console.log(npmSDK||lock.portable?'Portable Node route.'
|
|
260
|
+
console.log(npmSDK||lock.portable?'Portable Node route.':`Native SDK: ${process.platform}-${process.arch}${suppliedSdk?' (user-provided)':sdkAsset?.minimumOS?'; minimum OS '+sdkAsset.minimumOS:''}.`);
|
|
183
261
|
console.log('No toolchains are downloaded automatically. See README.md for official installers.');
|
|
184
262
|
if(missing)process.exitCode=1;
|
|
185
263
|
}
|
|
@@ -198,4 +276,4 @@ async function main(action='build',args=[]){
|
|
|
198
276
|
return {main};
|
|
199
277
|
}
|
|
200
278
|
|
|
201
|
-
module.exports={createRuntime};
|
|
279
|
+
module.exports={createRuntime,selectLocalResourceMode,selectPlatformSdk,recipeInvocation,bindingCompilerIdentity};
|