@petercjl/infinite-canvas-cli 0.1.1 → 0.1.3

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 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:
@@ -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.1",
5
+ "version": "0.1.3",
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",
@@ -43,6 +57,12 @@
43
57
  "concurrency": "separate conversations in the same tab; queued UI submissions and canvas writes; parallel remote runs",
44
58
  "submission": "click native Send",
45
59
  "directGenerationAPI": false,
60
+ "mediaHelperScope": [
61
+ "canvas.get",
62
+ "canvas.add",
63
+ "image.upload"
64
+ ],
65
+ "requestedVideoParameters": "Convey task requirements in the prepared native Agent prompt; explicit final prompts stay unchanged; verify actual artifact metadata.",
46
66
  "recovery": "saved sessionId/runId; read-only run and history APIs",
47
67
  "model": "inherit webpage settings"
48
68
  },
@@ -65,6 +85,22 @@
65
85
  "generic:windows": "built-in Chrome discovery/startup implemented; actual host video end-to-end verification pending"
66
86
  },
67
87
  "commandContracts": {
88
+ "install": {
89
+ "options": {
90
+ "--agent": {
91
+ "type": "string",
92
+ "required": true
93
+ },
94
+ "--path": {
95
+ "type": "string"
96
+ },
97
+ "--json": {
98
+ "type": "boolean"
99
+ }
100
+ },
101
+ "sideEffects": "local-write",
102
+ "output": "Verified CLI and current Agent companion Skill; complete=true only when both are ready"
103
+ },
68
104
  "video generate": {
69
105
  "options": {
70
106
  "--canvas": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@petercjl/infinite-canvas-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",
@@ -9,7 +9,7 @@ Execute an explicitly selected canvas video request with the installed `infinite
9
9
 
10
10
  ## Input and output
11
11
 
12
- Require a board file ID, the exact prompt and 1–20 ordered PNG, JPEG or WebP references (local paths or HTTPS URLs). Ask only for missing required inputs. Preserve the prompt and image order; 图1 means the first attached image. Inherit model settings from the webpage. An authorized generation request allows one paid native Agent submission. A planning or dry-run request allows validation only.
12
+ Require an existing board file ID or an explicit request to create a named board, the exact prompt and 1–20 ordered PNG, JPEG or WebP references (local paths or HTTPS URLs). Ask only for missing required inputs. Preserve the prompt and image order; 图1 means the first attached image. Use the webpage model and native Agent workflow. When composing the execution prompt from a task, include the requested duration, aspect ratio, resolution and reference order in natural language. Preserve a final prompt explicitly supplied verbatim; resolve conflicts with webpage settings before submission. The absence of separate CLI flags is not evidence that the webpage Agent cannot receive these requirements. An authorized generation request allows one paid native Agent submission. A planning or dry-run request allows validation only.
13
13
 
14
14
  Return the local MP4 from `artifacts[].path`, task ID, board ID and Frame ID. Success means `status=succeeded`, a nonempty downloaded video, original reference node IDs plus one video node in the saved Frame, and verified readback. A generated video with unfinished grouping is a partial result, reported with its task ID and recovery action.
15
15
 
@@ -19,6 +19,18 @@ 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
+ ## Generation route and helper boundary
23
+
24
+ Video generation in this Skill always uses `infinite-canvas video generate`: one native Agent conversation and the webpage Send button. `sealseek-media` supplies canvas and media-management operations only. Never call its video/image generation commands or load its generation Skill as an alternative workflow. A missing flag, authentication error or UI issue does not authorize changing the generation route.
25
+
26
+ Use the supplied board ID. If the user explicitly requests a new board, create it through the helper's documented `canvas create --title TITLE --submit --json`, retain the returned canvas ID and return to this Skill's native video flow. The helper may require its own CLI login for canvas reads/writes and image upload; browser login and CLI authentication are separate. On `AUTH_REJECTED`, complete the helper's normal `auth login` flow and verify it before resuming the same canvas operation. Login does not authorize generation.
27
+
28
+ Inspect `capabilities` and dry-run output to distinguish explicit command options, inherited webpage settings and requirements conveyed in the prompt. Report a specific unverified or unsupported requirement accurately. Verify the actual returned video's dimensions and duration when inspection is available; a prompt or local dry run alone is not proof of delivery parameters.
29
+
30
+ ## Installation and update
31
+
32
+ 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.
33
+
22
34
  ## Main line
23
35
 
24
36
  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
@@ -36,3 +38,12 @@ The CLI never prints browser credentials. Changing source, a live webpage deploy
36
38
  Only Google Chrome is an external browser prerequisite. macOS discovery checks system and current-user Applications; Windows checks per-user and Program Files locations, then App Paths registry entries. `INFINITE_CANVAS_CHROME_BIN` selects an actual executable for a custom installation. No extra launcher is required. `INFINITE_CANVAS_BROWSER_BIN` remains an explicit legacy override and is verified against the same port/profile contract.
37
39
 
38
40
  `browser status` is read-only. `chrome.installed` and `available` describe installation; `running` and `ready` describe the dedicated instance. A stopped but installed Chrome passes the browser dependency check in `doctor`. `browser open` and normal video execution launch/reuse it, verify process ownership, loopback CDP 9223 and the current-user profile. On Windows this profile is under the current home directory; on macOS it is under the current home directory with the same `.dianshang-chrome-profile` name. Windows uses built-in PowerShell process/TCP queries; macOS uses bounded nonblocking system process queries. Startup is queued so concurrent jobs share the browser. `doctor --live --canvas ID` requires the running, authenticated, editable board as well.
41
+
42
+
43
+ ## Native video route and Windows text I/O
44
+
45
+ `infinite-canvas video generate` sends the exact prepared prompt to the webpage Agent. Duration, aspect ratio and resolution requested in a task belong in the prepared natural-language prompt unless an explicitly selected final prompt must remain verbatim. The CLI does not advertise separate flags for these settings; preserve the native route and distinguish webpage settings from prompt-level requirements. Verify actual output metadata before reporting exact dimensions or duration.
46
+
47
+ `sealseek-media` is a canvas/media helper, including board creation, reads, uploads and adding existing media. It is not this Skill's generation route. Its CLI credentials can require a separate login even while the dedicated Chrome is logged in. Resolve that dependency and return to the native flow; do not submit a second generation through the helper.
48
+
49
+ On Windows, use the host's declared shell and text-encoding contract. Keep JSON intact and parse it before selecting fields; a truncated text excerpt is not the complete result. Console replacement characters are inconclusive: inspect UTF-8 bytes or hashes before changing prompts or source files. If native CLI output or redirection is decoded incorrectly, report the host encoding issue. Preserve the existing task ID and submission receipt; recovery inspects that same task and never repeats a paid Send to obtain readable output.
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,10 +15,11 @@ 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'],
19
- generationTransport:{kind:'native-browser-ui',newConversationPerLogicalVideo:true,singleTabPerCanvas:true,concurrency:'separate conversations in the same tab; queued UI submissions and canvas writes; parallel remote runs',submission:'click native Send',directGenerationAPI:false,recovery:'saved sessionId/runId; read-only run and history APIs',model:'inherit webpage settings'},
22
+ generationTransport:{kind:'native-browser-ui',newConversationPerLogicalVideo:true,singleTabPerCanvas:true,concurrency:'separate conversations in the same tab; queued UI submissions and canvas writes; parallel remote runs',submission:'click native Send',directGenerationAPI:false,mediaHelperScope:['canvas.get','canvas.add','image.upload'],requestedVideoParameters:'Convey task requirements in the prepared native Agent prompt; explicit final prompts stay unchanged; verify actual artifact metadata.',recovery:'saved sessionId/runId; read-only run and history APIs',model:'inherit webpage settings'},
20
23
  dependencies:{node:'>=22',browser:'Installed Google Chrome; built-in macOS/Windows discovery and startup on loopback 9223 with current-user dedicated profile',canvasAPI:'sealseek-media >=0.6.7, public CLI commands',libraries:['playwright-core','image-size'],optional:['ffprobe']},
21
24
  platforms:{'codex:macos':'tested: six native Agent video tasks across two boards; one tab per board; downloads, deduplication and Frame readback verified','generic:macos':'spec-compatible, runtime untested','sealseek:macos':'spec-compatible, runtime untested','generic:windows':'built-in Chrome discovery/startup implemented; actual host video end-to-end verification pending'},
22
25
  commandContracts,
@@ -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 [--agent AGENT_NAME --path DIR] --json
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
- import {execFile} from 'node:child_process';
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(ROOT,'.git')),'SOURCE_CHECKOUT','This is a Git-owned source checkout; update it through its repository.');
71
- const version=await checkUpdate({force:true});need(version.published&&version.available,'REGISTRY_UNAVAILABLE','A published version is unavailable.');
72
- if(version.latest===META.version){const skill=agent?await installSkill(agent,destination):null;return {ok:true,changed:!!skill?.changed,version:META.version,skill};}
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.platform==='win32'?'npm.cmd':'npm',['install','--global',META.name+'@'+version.latest],{timeout:180000,maxBuffer:1024*1024});
76
- const entry=process.platform==='win32'?'infinite-canvas.cmd':'infinite-canvas';const r=JSON.parse((await execute(entry,['version','--json'],{timeout:15000})).stdout);need(r.version===version.latest,'UPDATE_VERIFY_FAILED','Installed CLI version differs.');
77
- let skill;if(agent){const args=['skill','update','--agent',agent,'--json',...(destination?['--path',destination]:[])];skill=JSON.parse((await execute(entry,args,{timeout:30000})).stdout);need(skill.ok,'SKILL_SYNC_FAILED','Package upgraded but selected Skill did not synchronize.');}
78
- return {ok:true,version:r.version,changed:true,backup,skill};
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
+ }
package/src/media-cli.mjs CHANGED
@@ -4,6 +4,7 @@ import {execute,newJSON,readJSON,exists,canonical,need,Fault} from './core.mjs';
4
4
  export class MediaCLI {
5
5
  constructor(){this.bin=process.env.INFINITE_CANVAS_MEDIA_BIN||'sealseek-media';}
6
6
  async call(args){
7
+ need(['canvas get','canvas add','image upload'].includes(args.slice(0,2).join(' ')), 'MEDIA_HELPER_SCOPE', 'The media helper only reads/writes canvas media. Video generation uses the native Infinite Canvas Agent.');
7
8
  try{const {stdout}=await execute(this.bin,[...args,'--json'],{timeout:90000,maxBuffer:32*1024*1024,env:{...process.env,SEALSEEK_MEDIA_AUTO_UPDATE:'0'}});const r=JSON.parse(stdout);need(r.ok!==false,r.error?.code||'MEDIA_API_FAILED',r.error?.message||'Canvas operation failed.');return r;}
8
9
  catch(e){if(e instanceof Fault)throw e;let r;try{r=JSON.parse(e.stdout);}catch{}throw new Fault(r?.error?.code||'MEDIA_CLI_FAILED',r?.error?.message||'sealseek-media could not complete this operation.',{command:args.slice(0,2),cause:e.code});}
9
10
  }
@@ -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
+ }