@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clunkyard/cli",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Shared local builds, matches and replays for Clunkyard robots",
5
5
  "license": "MIT",
6
6
  "engines": {
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${tools.length?' and your existing language tools: '+tools.map(tool=>'\`'+tool+'\`').join(', '):''}.
49
- ${portable?'This language uses the portable Node/WebAssembly route.':'Native SDK assets require Linux x86-64 and glibc 2.39+ (Ubuntu 24.04+ or WSL2 Ubuntu 24.04+).'}
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.0.0\` or set \`CLUNKYARD='npx --yes @clunkyard/cli@2.0.0'\` with Make. SDK/engine caches are identified by their content hashes and remain reusable across CLI updates.
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 result=spawnSync(command,args,{cwd:root,env:environment(),stdio:'inherit',...options});
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','libclunkyard.so.2'),LD_LIBRARY_PATH:paths,
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
- if(!npmSDK&&!lock.portable&&(process.platform!=='linux'||process.arch!=='x64'))throw new Error('This native SDK release supports Linux x86-64 (Ubuntu 24.04+/WSL2). Use JavaScript/Vane/Cadence/Glyph for the portable Node route; other native platforms are not yet qualified.');
96
+ checkNativePlatform();
47
97
  await download(lock.engine,engine);
48
98
  if(npmSDK)installNpmSDK();
49
- else if(!process.env.CLUNKYARD_SDK)await download(lock.sdk,sdk);
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
- run('make',['-C',directory,'install',`SDK=${sdk}`,`ROOT=${engine}`,`BUILD=${path.join(cache,'tmp','binding-'+binding)}`]);
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||lock.sdk.sha256;
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]=process.env.CC||'cc';if(m.language==='cpp')args[0]=process.env.CXX||'c++';run(args[0],args.slice(1),{cwd:copied});}}
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
- 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,'--seed',seed,'--ticks','360','--turn-ms','2000','--startup-ms','60000','--replay',replay]);
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?.length))){
164
- const ok=toolAvailable(tool);
165
- console.log(`${ok?'found':'missing'} ${tool}`);if(!ok)missing++;
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
- const glibc=process.report?.getReport().header.glibcVersionRuntime;
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.':'Native assets: Linux x86-64, glibc 2.39+; Ubuntu 24.04+/WSL2.');
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};