@petercjl/infinite-canvas-cli 0.1.1 → 0.1.2
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 +6 -0
- package/bin/infinite-canvas.mjs +6 -3
- package/capabilities.json +31 -1
- package/package.json +4 -2
- package/skills/infinite-canvas-video/SKILL.md +4 -0
- package/skills/infinite-canvas-video/references/runtime.md +2 -0
- package/src/contracts.mjs +5 -1
- package/src/core.mjs +1 -3
- package/src/lifecycle.mjs +23 -8
- package/src/postinstall.mjs +2 -0
- package/src/process-execution.mjs +65 -0
package/README.md
CHANGED
|
@@ -8,6 +8,12 @@ Each board file uses exactly one browser tab. Concurrent videos run in separate
|
|
|
8
8
|
|
|
9
9
|
`doctor` distinguishes Chrome installation from runtime state. An installed executable passes the browser dependency check while Chrome is stopped. `browser open` launches or reuses the dedicated instance; video execution prepares it automatically. A port/profile conflict is reported explicitly. Chrome outside the standard paths can be selected with `INFINITE_CANVAS_CHROME_BIN`.
|
|
10
10
|
|
|
11
|
+
## Installation lifecycle
|
|
12
|
+
|
|
13
|
+
Installing or updating this plugin means preparing both the CLI and its companion Skill for the current Agent. A successful npm command alone is a partial installation. The executing Agent resolves its own Skill directory, reads `infinite-canvas skill source --json`, and completes `infinite-canvas install --agent AGENT_NAME --path SKILL_DIRECTORY --json`. Verify the CLI version and a receipt with `complete=true`, `skill.installed=true` and `skill.current=true`, then load the installed Skill (refresh the Agent session when required). npm installation prints this handoff; if lifecycle scripts are disabled, follow this same flow explicitly. Unknown destinations require resolution before completion; no other Agent directory is changed.
|
|
14
|
+
|
|
15
|
+
For updates use `infinite-canvas update install --yes --agent AGENT_NAME --path SKILL_DIRECTORY --json`; this synchronizes the selected Skill even if the npm version is unchanged, and refuses missing or edited/unmanaged targets before package replacement. Direct npm upgrades must be followed by the same `install` verification step.
|
|
16
|
+
|
|
11
17
|
## Agent usage
|
|
12
18
|
|
|
13
19
|
Provide the board ID, prompt and reference image attachments in their intended order. Send the Agent:
|
package/bin/infinite-canvas.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import {META,ROOT,need,parse,readJSON,execute,stateRoot,newJSON,redact,safeId} from '../src/core.mjs';
|
|
6
6
|
import {manifest,HELP} from '../src/contracts.mjs';
|
|
7
|
-
import {automaticCheck,checkUpdate,installUpdate,updatePolicy,SKILL,skillStatus,installSkill} from '../src/lifecycle.mjs';
|
|
7
|
+
import {automaticCheck,checkUpdate,installUpdate,updatePolicy,SKILL,skillStatus,installSkill,completeInstallation,installationContract} from '../src/lifecycle.mjs';
|
|
8
8
|
|
|
9
9
|
const out=v=>console.log(JSON.stringify(v,null,2));
|
|
10
10
|
async function main(argv){
|
|
@@ -12,11 +12,14 @@ async function main(argv){
|
|
|
12
12
|
if(!command||['help','--help','-h'].includes(command))return console.log(HELP);
|
|
13
13
|
need(Number(process.versions.node.split('.')[0])>=22,'CAPABILITY_UNAVAILABLE','Node.js 22 or newer is required.');
|
|
14
14
|
if(['version','--version','-v'].includes(command)){const {args,options}=parse(argv.slice(1),{booleans:['json']});need(!args.length,'INVALID_ARGUMENT','Unexpected arguments.');return options.json?out({ok:true,name:META.name,version:META.version,entry:path.join(ROOT,'bin/infinite-canvas.mjs')}):console.log('infinite-canvas '+META.version);}
|
|
15
|
-
if(argv.includes('--help')||argv.includes('-h')){need(['video','task','frame','browser','board','skill','update','doctor','capabilities'].includes(command),'UNKNOWN_COMMAND','Unknown command.');return console.log(HELP);}
|
|
15
|
+
if(argv.includes('--help')||argv.includes('-h')){need(['install','video','task','frame','browser','board','skill','update','doctor','capabilities'].includes(command),'UNKNOWN_COMMAND','Unknown command.');return console.log(HELP);}
|
|
16
|
+
if(command==='install'){
|
|
17
|
+
const {options:o,args}=parse(argv.slice(1),{values:['agent','path'],booleans:['json']});need(!args.length,'INVALID_ARGUMENT','Unexpected arguments.');return out(await completeInstallation(o.agent,o.path));
|
|
18
|
+
}
|
|
16
19
|
if(command==='_worker'){need(argv.length===2,'INVALID_ARGUMENT','Invalid worker invocation.');const {processJob}=await import('../src/workflow.mjs');return out(await processJob(action));}
|
|
17
20
|
if(command==='skill'){
|
|
18
21
|
const {options:o,args}=parse(rest,{values:['agent','path'],booleans:['json']});need(!args.length,'INVALID_ARGUMENT','Unexpected arguments.');
|
|
19
|
-
if(action==='source')return out({ok:true,source:SKILL,name:'infinite-canvas-video',version:META.version,installation:{named_agents:['codex'],fallback_agent:'generic',custom_path_required:true,scope:'selected Agent only'}});
|
|
22
|
+
if(action==='source')return out({ok:true,source:SKILL,name:'infinite-canvas-video',version:META.version,installation:{named_agents:['codex'],fallback_agent:'generic',custom_path_required:true,scope:'selected Agent only',...installationContract}});
|
|
20
23
|
if(action==='status')return out(await skillStatus(o.agent||'codex',o.path));
|
|
21
24
|
need(['install','update'].includes(action),'UNKNOWN_COMMAND','Use skill source/status/install/update.');return out(await installSkill(o.agent||'codex',o.path));
|
|
22
25
|
}
|
package/capabilities.json
CHANGED
|
@@ -2,7 +2,21 @@
|
|
|
2
2
|
"schema": "infinite-canvas-capabilities",
|
|
3
3
|
"schema_version": "1.0.0",
|
|
4
4
|
"package": "@petercjl/infinite-canvas-cli",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.2",
|
|
6
|
+
"installationLifecycle": {
|
|
7
|
+
"required_components": [
|
|
8
|
+
"cli",
|
|
9
|
+
"skill"
|
|
10
|
+
],
|
|
11
|
+
"target_scope": "current Agent only",
|
|
12
|
+
"target_resolution": "Agent resolves its own Skill directory; pass --agent and --path",
|
|
13
|
+
"install": "infinite-canvas install --agent AGENT --path SKILL_DIRECTORY --json",
|
|
14
|
+
"update": "infinite-canvas update install --yes --agent AGENT --path SKILL_DIRECTORY --json",
|
|
15
|
+
"verification": "CLI entry/version and managed Skill current=true; partial installation is incomplete",
|
|
16
|
+
"skillRequired": true,
|
|
17
|
+
"updateRequiresSkillSync": true,
|
|
18
|
+
"unresolvedTarget": "SKILL_TARGET_REQUIRED; no implicit cross-Agent installation"
|
|
19
|
+
},
|
|
6
20
|
"input": {
|
|
7
21
|
"required": [
|
|
8
22
|
"canvas_id",
|
|
@@ -65,6 +79,22 @@
|
|
|
65
79
|
"generic:windows": "built-in Chrome discovery/startup implemented; actual host video end-to-end verification pending"
|
|
66
80
|
},
|
|
67
81
|
"commandContracts": {
|
|
82
|
+
"install": {
|
|
83
|
+
"options": {
|
|
84
|
+
"--agent": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"required": true
|
|
87
|
+
},
|
|
88
|
+
"--path": {
|
|
89
|
+
"type": "string"
|
|
90
|
+
},
|
|
91
|
+
"--json": {
|
|
92
|
+
"type": "boolean"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"sideEffects": "local-write",
|
|
96
|
+
"output": "Verified CLI and current Agent companion Skill; complete=true only when both are ready"
|
|
97
|
+
},
|
|
68
98
|
"video generate": {
|
|
69
99
|
"options": {
|
|
70
100
|
"--canvas": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@petercjl/infinite-canvas-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Drive native Infinite Canvas Agent conversations and organize reference images and results in a Frame.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"test": "node --test",
|
|
26
|
-
"check": "node --check bin/infinite-canvas.mjs && node --test"
|
|
26
|
+
"check": "node --check bin/infinite-canvas.mjs && node --test",
|
|
27
|
+
"postinstall": "node src/postinstall.mjs",
|
|
28
|
+
"check:packed": "node scripts/check-packed-install.mjs"
|
|
27
29
|
},
|
|
28
30
|
"dependencies": {
|
|
29
31
|
"playwright-core": "1.62.1",
|
|
@@ -19,6 +19,10 @@ Resolve `infinite-canvas` from PATH. Run `version --json`, `capabilities --json`
|
|
|
19
19
|
|
|
20
20
|
External dependencies are Node.js 22+, `playwright-core` and `image-size` (package managed), installed Google Chrome on macOS or Windows, and `sealseek-media` with image upload, canvas get/add capabilities. This Skill invokes the helper through the CLI contract; it does not compose another generation Skill. Browser authentication stays in the user's dedicated Chrome profile. Never copy cookies or tokens into a prompt, output or package.
|
|
21
21
|
|
|
22
|
+
## Installation and update
|
|
23
|
+
|
|
24
|
+
Treat plugin installation/update as one operation covering the CLI and this companion Skill. Resolve the current Agent’s own Skill directory from its configuration/catalog, read `skill source --json`, then run `install --agent AGENT_NAME --path SKILL_DIRECTORY --json`. For a package upgrade use `update install --yes --agent AGENT_NAME --path SKILL_DIRECTORY --json`; even an unchanged version must reconcile the Skill. If npm was installed/upgraded directly or scripts were disabled, run the same `install` completion step afterward. Completion requires the actual CLI version plus `complete=true`, `skill.installed=true` and `skill.current=true`. Load the synchronized Skill; refresh the host session if discovery requires it. Missing targets or synchronization failure are an incomplete installation. Only the current/requested Agent is synchronized.
|
|
25
|
+
|
|
22
26
|
## Main line
|
|
23
27
|
|
|
24
28
|
1. Validate required inputs and authorization. Run `doctor --json`: its browser check means Chrome is available to launch; `browser_ready` separately reports runtime readiness. Use `browser open --json` to prepare Chrome. The CLI discovers and launches the system browser itself with fixed loopback 9223 and the persistent current-user `.dianshang-chrome-profile`; a separate launcher installation is unnecessary. For a UI compatibility check use `doctor --live --canvas ID --json`. On missing login, have the user sign in in that Chrome window, then return here. The CLI uses CDP port 9223 and the dedicated ecommerce profile. Enforce the single-tab constraint below before any submission or canvas write.
|
|
@@ -5,6 +5,8 @@ These commands are for the executing Agent and developers.
|
|
|
5
5
|
Browser invariant: one board file, one tab. Reuse the existing tab for that canvas ID. Concurrent tasks use distinct native conversations and captured session/run IDs within that tab. `BOARD_SELECTION_REQUIRED` means resolve duplicate tabs before continuing; preserve the existing tasks. UI submission and canvas writes are queued per board, while remote runs can progress concurrently.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
+
infinite-canvas install --agent AGENT_NAME --path RESOLVED_SKILL_DIR --json
|
|
9
|
+
infinite-canvas update install --yes --agent AGENT_NAME --path RESOLVED_SKILL_DIR --json
|
|
8
10
|
infinite-canvas version --json
|
|
9
11
|
infinite-canvas capabilities --json
|
|
10
12
|
infinite-canvas doctor --json
|
package/src/contracts.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {META} from './core.mjs';
|
|
2
|
+
import {installationContract} from './lifecycle.mjs';
|
|
2
3
|
const s={type:'string'},b={type:'boolean'};
|
|
3
4
|
export const commandContracts={
|
|
5
|
+
'install':{options:{'--agent':{...s,required:true},'--path':s,'--json':b},sideEffects:'local-write',output:'Verified CLI and current Agent companion Skill; complete=true only when both are ready'},
|
|
4
6
|
'video generate':{options:{'--canvas':s,'--prompt':s,'--prompt-file':s,'--reference':{...s,repeatable:true},'--output':s,'--request-id':s,'--new':b,'--submit':b,'--dry-run':b,'--json':b},sideEffects:'local-write',externalMutation:'With --submit: import references, send native Agent conversation, import a missing video, group selected elements in a Frame.',paid:'Only native webpage sending may charge credits.'},
|
|
5
7
|
'task get|wait|resume|sync':{options:{'--timeout':s,'--json':b},sideEffects:'local-write',externalMutation:'resume continues original task; sync reapplies existing video archival and grouping; never a second generation.'},
|
|
6
8
|
'frame group':{options:{'--canvas':s,'--elements':s,'--frame-id':s,'--name':s,'--submit':b,'--json':b},sideEffects:'local-write',externalMutation:'With --submit: reparent existing nodes and preserve their identities.'},
|
|
@@ -13,6 +15,7 @@ export const commandContracts={
|
|
|
13
15
|
'update auto':{options:{'--json':b},sideEffects:'local-write'}
|
|
14
16
|
};
|
|
15
17
|
export const manifest={schema:'infinite-canvas-capabilities',schema_version:'1.0.0',package:META.name,version:META.version,
|
|
18
|
+
installationLifecycle:{...installationContract,skillRequired:true,updateRequiresSkillSync:true,unresolvedTarget:'SKILL_TARGET_REQUIRED; no implicit cross-Agent installation'},
|
|
16
19
|
input:{required:['canvas_id','prompt','references'],references:'ordered PNG/JPEG/WebP local files or HTTPS URLs'},
|
|
17
20
|
output:{terminal:'local MP4 + saved Frame containing original reference nodes and one video node',fields:['task_id','status','canvas_id','session_id','run_id','frame_id','reference_element_ids','video_element_id','artifacts']},
|
|
18
21
|
operations:['browser.status','browser.open','board.inspect','video.generate.native-agent','task.inspect','task.wait','task.resume','task.sync','frame.group-existing'],
|
|
@@ -44,10 +47,11 @@ board inspect --canvas ID --json
|
|
|
44
47
|
doctor [--live --canvas ID] --json
|
|
45
48
|
capabilities --json
|
|
46
49
|
version [--json]
|
|
50
|
+
install --agent AGENT_NAME --path SKILL_DIRECTORY --json
|
|
47
51
|
skill source --json
|
|
48
52
|
skill status|install|update [--agent codex|generic|AGENT_NAME] [--path DIR] --json
|
|
49
53
|
update check --json
|
|
50
|
-
update install --yes
|
|
54
|
+
update install --yes --agent AGENT_NAME [--path DIR] --json
|
|
51
55
|
update auto on|off|status --json
|
|
52
56
|
|
|
53
57
|
Runtime: Node >=22; installed Google Chrome on macOS/Windows; sealseek-media.
|
package/src/core.mjs
CHANGED
|
@@ -4,12 +4,10 @@ import path from 'node:path';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import {fileURLToPath} from 'node:url';
|
|
7
|
-
|
|
8
|
-
import {promisify} from 'node:util';
|
|
7
|
+
export {execute} from './process-execution.mjs';
|
|
9
8
|
|
|
10
9
|
export const ROOT=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
11
10
|
export const META=JSON.parse(await fs.readFile(path.join(ROOT,'package.json'),'utf8'));
|
|
12
|
-
export const execute=promisify(execFile);
|
|
13
11
|
export class Fault extends Error {constructor(code,message,details={}){super(message);this.code=code;this.details=details;}}
|
|
14
12
|
export function need(value,code,message,details){if(!value)throw new Fault(code,message,details);return value;}
|
|
15
13
|
export const canonical=v=>JSON.stringify(sort(v));
|
package/src/lifecycle.mjs
CHANGED
|
@@ -5,6 +5,13 @@ import crypto from 'node:crypto';
|
|
|
5
5
|
import {ROOT,META,need,exists,readJSON,newJSON,newFile,replaceOwned,stateRoot,hash,mkdir,lock,execute,Fault} from './core.mjs';
|
|
6
6
|
|
|
7
7
|
export const SKILL=path.join(ROOT,'skills','infinite-canvas-video');
|
|
8
|
+
export const installationContract={required_components:['cli','skill'],target_scope:'current Agent only',target_resolution:'Agent resolves its own Skill directory; pass --agent and --path',install:'infinite-canvas install --agent AGENT --path SKILL_DIRECTORY --json',update:'infinite-canvas update install --yes --agent AGENT --path SKILL_DIRECTORY --json',verification:'CLI entry/version and managed Skill current=true; partial installation is incomplete'};
|
|
9
|
+
export async function completeInstallation(agent,destination){
|
|
10
|
+
need(agent,'SKILL_TARGET_REQUIRED','Resolve the current Agent and its Skill directory, then run install --agent AGENT --path DIRECTORY.');
|
|
11
|
+
const skill=await installSkill(agent,destination);
|
|
12
|
+
need(skill.installed&&skill.current,'SKILL_SYNC_FAILED','CLI and companion Skill must both be ready.');
|
|
13
|
+
return {ok:true,complete:true,version:META.version,cli:{entry:path.join(ROOT,'bin/infinite-canvas.mjs'),version:META.version},skill,changed:skill.changed};
|
|
14
|
+
}
|
|
8
15
|
export async function treeFiles(root,prefix=''){
|
|
9
16
|
const out=[];for(const e of await fs.readdir(path.join(root,prefix),{withFileTypes:true})){need(!e.isSymbolicLink(),'UNMANAGED_STATE','Skill resources must not be symbolic links.');const rel=path.posix.join(prefix,e.name);if(e.isDirectory())out.push(...await treeFiles(root,rel));else out.push(rel);}return out.sort();
|
|
10
17
|
}
|
|
@@ -65,16 +72,24 @@ export async function checkUpdate({force=false,fetcher=fetch,now=Date.now()}={})
|
|
|
65
72
|
});
|
|
66
73
|
}
|
|
67
74
|
export async function automaticCheck(){try{const r=await checkUpdate();if(r.update_available&&!r.cached)process.stderr.write(JSON.stringify({update_available:true,current:META.version,latest:r.latest,installation:'explicit update install --yes required'})+'\n');}catch(e){process.stderr.write(JSON.stringify({update_check:'unavailable',code:e.code||'UPDATE_BUSY'})+'\n');}}
|
|
68
|
-
export async function installUpdate({yes,agent,destination}={}){
|
|
75
|
+
export async function installUpdate({yes,agent,destination}={}, {check=checkUpdate,packageRoot=ROOT}={}){
|
|
69
76
|
need(yes,'PERMISSION_REQUIRED','Package update requires --yes.');
|
|
70
|
-
need(!await exists(path.join(
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
need(!await exists(path.join(packageRoot,'.git')),'SOURCE_CHECKOUT','This is a Git-owned source checkout; update it through its repository.');
|
|
78
|
+
need(agent,'SKILL_TARGET_REQUIRED','Package updates require the current Agent and its Skill target.');
|
|
79
|
+
const target=await skillStatus(agent,destination);need(!target.edited?.length,'LOCAL_EDITS','Preserve edited Skill files before updating.');
|
|
80
|
+
const version=await check({force:true});need(version.published&&version.available,'REGISTRY_UNAVAILABLE','A published version is unavailable.');
|
|
81
|
+
if(version.latest===META.version)return completeInstallation(agent,destination);
|
|
73
82
|
const backup=path.join(stateRoot(),'package-backups',crypto.randomUUID());await mkdir(path.dirname(backup));await fs.cp(ROOT,backup,{recursive:true,errorOnExist:true,force:false});
|
|
74
83
|
try{
|
|
75
|
-
await execute(process.
|
|
76
|
-
const entry=
|
|
77
|
-
|
|
78
|
-
return {
|
|
84
|
+
const npmCLI=await resolveNpmCLI();await execute(process.execPath,[npmCLI,'install','--global',META.name+'@'+version.latest],{timeout:180000,maxBuffer:1024*1024});
|
|
85
|
+
const entry=path.join(ROOT,'bin/infinite-canvas.mjs');const r=JSON.parse((await execute(process.execPath,[entry,'version','--json'],{timeout:15000})).stdout);need(r.version===version.latest,'UPDATE_VERIFY_FAILED','Installed CLI version differs.');
|
|
86
|
+
const args=[entry,'install','--agent',agent,'--path',target.destination,'--json'];const receipt=JSON.parse((await execute(process.execPath,args,{timeout:30000})).stdout);need(receipt.ok&&receipt.complete&&receipt.skill?.current,'SKILL_SYNC_FAILED','Package upgraded but selected Skill did not synchronize.');
|
|
87
|
+
return {...receipt,changed:true,backup};
|
|
79
88
|
}catch(e){throw new Fault('UPDATE_FAILED','Update did not verify. The complete previous package is backed up.',{backup,cause:e.code||e.message});}
|
|
80
89
|
}
|
|
90
|
+
async function resolveNpmCLI(){
|
|
91
|
+
const candidates=[process.env.npm_execpath,path.join(path.dirname(process.execPath),'node_modules/npm/bin/npm-cli.js')];
|
|
92
|
+
for(const dir of (process.env.PATH||'').split(path.delimiter)){const bin=path.join(dir,process.platform==='win32'?'npm.cmd':'npm');try{const real=await fs.realpath(bin);candidates.push(real,path.join(path.dirname(real),'node_modules/npm/bin/npm-cli.js'));}catch{}}
|
|
93
|
+
for(const file of candidates.filter(Boolean)){if(path.basename(file)==='npm-cli.js'&&await exists(file))return file;}
|
|
94
|
+
throw new Fault('NPM_UNAVAILABLE','Cannot locate the installed npm CLI JavaScript entry.');
|
|
95
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import {installationContract} from './lifecycle.mjs';
|
|
2
|
+
console.log(JSON.stringify({package:'@petercjl/infinite-canvas-cli',complete:false,code:'SKILL_SETUP_REQUIRED',message:'The CLI and bundled Skill are installed in the npm package. The current Agent must install/verify the companion Skill in its own Skill directory before reporting installation or update complete.',...installationContract}));
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {execFile} from 'node:child_process';
|
|
4
|
+
import {promisify} from 'node:util';
|
|
5
|
+
|
|
6
|
+
const run = promisify(execFile);
|
|
7
|
+
const failure = (code, message) => Object.assign(new Error(message), {code});
|
|
8
|
+
async function fileExists(file) {
|
|
9
|
+
try { return (await fs.stat(file)).isFile(); }
|
|
10
|
+
catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return false; throw error; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Resolve only a declared Node package bin. Never evaluate a batch script. */
|
|
14
|
+
export async function npmShimEntry(shim) {
|
|
15
|
+
const text = await fs.readFile(shim, 'utf8');
|
|
16
|
+
const name = path.basename(shim, path.extname(shim));
|
|
17
|
+
const candidates = [...text.matchAll(/%(?:dp0%|~dp0)[\\/]([^"\r\n%]+\.(?:mjs|cjs|js))/gi)]
|
|
18
|
+
.map(match => path.resolve(path.dirname(shim), ...match[1].split(/[\\/]/)));
|
|
19
|
+
for (const entry of new Set(candidates)) {
|
|
20
|
+
if (!await fileExists(entry)) continue;
|
|
21
|
+
for (let directory = path.dirname(entry);;) {
|
|
22
|
+
const manifest = path.join(directory, 'package.json');
|
|
23
|
+
if (await fileExists(manifest)) {
|
|
24
|
+
let pkg;
|
|
25
|
+
try { pkg = JSON.parse(await fs.readFile(manifest, 'utf8')); }
|
|
26
|
+
catch { break; }
|
|
27
|
+
const bin = typeof pkg.bin === 'string'
|
|
28
|
+
? (pkg.name?.split('/').at(-1) === name ? pkg.bin : undefined)
|
|
29
|
+
: pkg.bin?.[name];
|
|
30
|
+
if (typeof bin === 'string' && path.resolve(directory, bin) === entry) return entry;
|
|
31
|
+
}
|
|
32
|
+
const parent = path.dirname(directory);
|
|
33
|
+
if (parent === directory) break;
|
|
34
|
+
directory = parent;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw failure('UNSUPPORTED_COMMAND_SHIM', 'This batch command has no verifiable npm Node entry. Use a native executable or explicit Node script path.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function resolveWindowsCommand(command, options = {}) {
|
|
41
|
+
const env = options.env ?? process.env;
|
|
42
|
+
const cwd = options.cwd ?? process.cwd();
|
|
43
|
+
const pathKey = Object.keys(env).find(key => key.toLowerCase() === 'path');
|
|
44
|
+
const directories = /[\\/]/.test(command) || path.isAbsolute(command)
|
|
45
|
+
? [''] : (env[pathKey] ?? '').split(path.delimiter).filter(Boolean).map(value => value.replace(/^"|"$/g, ''));
|
|
46
|
+
const extension = path.extname(command).toLowerCase();
|
|
47
|
+
const suffixes = extension ? [''] : ['.com', '.exe', '.cmd', '.bat'];
|
|
48
|
+
for (const directory of directories) for (const suffix of suffixes) {
|
|
49
|
+
const file = path.resolve(cwd, directory, command + suffix);
|
|
50
|
+
if (!await fileExists(file)) continue;
|
|
51
|
+
const ext = path.extname(file).toLowerCase();
|
|
52
|
+
if (['.cmd', '.bat'].includes(ext)) return {file: process.execPath, prefix: [await npmShimEntry(file)]};
|
|
53
|
+
if (['.js', '.mjs', '.cjs'].includes(ext)) return {file: process.execPath, prefix: [file]};
|
|
54
|
+
return {file, prefix: []};
|
|
55
|
+
}
|
|
56
|
+
throw failure('ENOENT', 'Command was not found in the supplied PATH.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function execute(command, args = [], options = {}) {
|
|
60
|
+
// Do not expose a shell execution path: prompts, filenames and URLs are data.
|
|
61
|
+
if (options.shell) throw failure('SHELL_NOT_SUPPORTED', 'Use an executable and an argument array.');
|
|
62
|
+
if (process.platform !== 'win32') return run(command, args, {...options, shell: false});
|
|
63
|
+
const launch = await resolveWindowsCommand(command, options);
|
|
64
|
+
return run(launch.file, [...launch.prefix, ...args], {...options, shell: false, windowsHide: true});
|
|
65
|
+
}
|