@petercjl/ecom-cli 0.1.2 → 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/UPDATE.md CHANGED
@@ -4,6 +4,8 @@ User prompt: “帮我更新 @petercjl/ecom-cli 和它的配套 Skill。”
4
4
 
5
5
  Resolve the currently selected Agent and exact Skill destination before mutation. Inspect the declared command contract and managed status. When current, reuse the verified installation; a same-version update repairs a missing or stale unedited Skill.
6
6
 
7
+ Backups live outside host Skill discovery in the user's Ecom CLI state directory (`ECOM_CLI_STATE_DIR` may select an absolute alternative). Version 0.1.1/0.1.2 callers use their historical installer during handoff. After upgrading these versions, invoke `skill status` through the new entry; if `recovery.required=true`, run that entry's `skill update` for the same exact target. This relocates the selected target's legacy backup chain with byte preservation and records old/new paths in `migratedBackups`. Verify recovery is clear before refreshing host discovery. Later updaters delegate activation to the acquired version's installer.
8
+
7
9
  Run the selected Skill's isolated CLI with `update install --agent <name> --target <absolute-skill-directory> --yes --json`. It selects stable `latest` (or an explicit stable `--version`), acquires the candidate through npm with scripts disabled, stages the full runtime plus Skill, preserves the previous target as a uniquely named backup, activates and verifies hashes/version. Use the new receipt's entry. The optional global bootstrap package remains independently installed; it is not the selected Agent runtime. Refresh host discovery and load the resulting Skill before reporting completion.
8
10
 
9
11
  Acquisition records a target-scoped operation journal and retains candidate location, requested version and previous version. For failure, inspect the journal and `skill status` before any new attempt. A directory lock, local edits or unmanaged target stops replacement. Failed rename restores the previous directory when possible and reports recovery evidence. Retain backups and candidates until an authorized cleanup. To roll back, verify a recorded backup, preserve the current target and restore that backup as an explicit maintenance action; do not overwrite user content.
package/capabilities.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@petercjl/ecom-cli",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
5
5
  "skills": [
6
6
  "ecom-cli"
7
7
  ],
@@ -311,7 +311,7 @@
311
311
  "updateGuide": "UPDATE.md",
312
312
  "install": "Resolve current Agent exact Skill target; run skill install --agent NAME --target PATH; load Skill through host discovery.",
313
313
  "update": "Run selected Skill runtime update install --agent NAME --target PATH --yes; verify returned activated entry and refresh host discovery.",
314
- "verification": "Verify CLI and Skill versions, managed hashes and current=true; host loads installed SKILL.md independently."
314
+ "verification": "Verify CLI and Skill versions, managed hashes, current=true and recovery.required=false; host loads installed SKILL.md independently."
315
315
  },
316
316
  "automaticUpdates": {
317
317
  "enabledByDefault": true,
@@ -5,6 +5,20 @@ import crypto from 'node:crypto';
5
5
  import {spawnSync} from 'node:child_process';
6
6
  import {root,pkg,fail,exists,tree,writeNew} from './common.mjs';
7
7
  const receiptName='.ecom-receipt.json';
8
+ function legacyBackup(target,backup) {
9
+ return typeof backup==='string'&&path.dirname(backup)===path.dirname(target)&&path.basename(backup).startsWith(path.basename(target)+'.ecom-backup-')&&exists(backup);
10
+ }
11
+ function recoveryDirectory(target) {
12
+ const state=process.env.ECOM_CLI_STATE_DIR??path.join(os.homedir(),'.ecom-cli');
13
+ if(!path.isAbsolute(state))throw fail('INVALID_ARGUMENT','ECOM_CLI_STATE_DIR must be absolute');
14
+ const directory=path.join(state,'recovery');
15
+ if(directory===target||directory.startsWith(target+path.sep)||directory.startsWith(path.dirname(target)+path.sep))throw fail('TARGET_CONFLICT','Recovery directory must be outside the host Skill discovery directory');
16
+ for(let p=directory;p!==path.dirname(p);p=path.dirname(p)) {
17
+ if(exists(p)&&fs.lstatSync(p).isSymbolicLink()&&!(process.platform==='darwin'&&['/var','/tmp'].includes(p)&&fs.lstatSync(p).uid===0))throw fail('UNSAFE_PATH','Recovery path must use physical directories');
18
+ }
19
+ fs.mkdirSync(directory,{recursive:true,mode:0o700});
20
+ return directory;
21
+ }
8
22
  export const source=(base=root)=>({ok:true,name:'ecom-cli',package:pkg.name,version:JSON.parse(fs.readFileSync(path.join(base,'package.json'),'utf8')).version,path:path.join(base,'skills/ecom-cli'),provenance:{ownership:'npm-package',registryContentVerified:false}});
9
23
  export function targetOf(options) {
10
24
  if(!options.agent || !/^[a-z][a-z0-9-]{0,63}$/.test(options.agent)) throw fail('INVALID_ARGUMENT','Supply a valid --agent name; unnamed hosts use the generic adapter');
@@ -27,18 +41,21 @@ export function status(options,base=root) {
27
41
  const canonicalCurrent=managed && Object.entries(canonical).every(([name,hash])=>files[name]===hash) && Object.keys(files).filter(n=>!n.startsWith('.runtime/')).length===Object.keys(canonical).length;
28
42
  let runtimeVersion;try{runtimeVersion=JSON.parse(fs.readFileSync(path.join(target,'.runtime/package.json'),'utf8')).version;}catch{}
29
43
  const current=!!(managed&&!edited&&canonicalCurrent&&runtimeVersion===identity.version&&receipt.version===identity.version);
30
- return {...identity,target,installed,managed,current,edited:!!edited,adapter:'generic',agent:options.agent,entry:path.join(target,'.runtime/bin/ecom-cli.mjs'),verification:{verified:current,algorithm:'sha256'},recovery:{required:installed&&!current,action:edited?'Preserve local edits; reconcile them before update':'Run skill update for this exact target'},hostDiscovery:{authority:'host',verified:false},...(managed?{installedVersion:receipt.version,backup:receipt.backup??null}:{})};
44
+ const layoutRecovery=managed&&legacyBackup(target,receipt.backup);
45
+ return {...identity,target,installed,managed,current,edited:!!edited,adapter:'generic',agent:options.agent,entry:path.join(target,'.runtime/bin/ecom-cli.mjs'),verification:{verified:current,algorithm:'sha256'},recovery:{required:installed&&(!current||layoutRecovery),action:edited?'Preserve local edits; reconcile them before update':layoutRecovery?'Run skill update using this new entry to relocate legacy backups outside host discovery':'Run skill update for this exact target'},hostDiscovery:{authority:'host',verified:false},...(managed?{installedVersion:receipt.version,backup:receipt.backup??null,migratedBackups:receipt.migratedBackups??[]}:{})};
31
46
  }
32
47
  export function installSkill(options,base=root) {
33
48
  const before=status(options,base), target=before.target;
34
- if(before.current) return {...before,changed:false};
49
+ if(before.current&&!before.recovery.required) return {...before,changed:false};
35
50
  if(before.installed&&!before.managed) throw fail('UNMANAGED_TARGET','Existing Skill is user-owned; choose a fresh destination');
36
51
  if(before.edited) throw fail('LOCAL_EDITS','Managed Skill has local edits; preserve and reconcile them before update');
37
52
  const parent=path.dirname(target);fs.mkdirSync(parent,{recursive:true});
53
+ const recovery=recoveryDirectory(target);
38
54
  const lock=target+'.ecom-lock';let fd;
39
55
  try {fd=fs.openSync(lock,'wx',0o600);}catch{throw fail('TARGET_LOCKED','Another maintenance operation holds the target lock');}
40
- const operationId=crypto.randomUUID(), staging=target+'.ecom-stage-'+operationId;
41
- const backup=before.installed?target+'.ecom-backup-'+operationId:null;
56
+ const operationId=crypto.randomUUID(), staging=path.join(parent,'.'+path.basename(target)+'.ecom-stage-'+operationId);
57
+ const backup=before.installed?path.join(recovery,operationId):null;
58
+ const migratedBackups=[];
42
59
  let activated=false;
43
60
  try {
44
61
  tree(base);
@@ -48,18 +65,30 @@ export function installSkill(options,base=root) {
48
65
  fs.cpSync(path.join(base,name),path.join(runtime,name),{recursive:true,errorOnExist:true,force:false});
49
66
  }
50
67
  const manifest=source(base);
51
- writeNew(path.join(staging,receiptName),{schemaVersion:1,package:pkg.name,name:'ecom-cli',version:manifest.version,target,agent:options.agent,operationId,backup,files:tree(staging)});
52
68
  // Recheck edits after staging, before replacement.
53
69
  const again=status(options,base);
54
70
  if(again.edited || again.installed!==before.installed) throw fail('TARGET_CHANGED','Skill changed during preparation');
71
+ let legacy=before.backup;
72
+ while(legacyBackup(target,legacy)) {
73
+ const info=fs.lstatSync(legacy);
74
+ if(!info.isDirectory()||info.isSymbolicLink())throw fail('UNSAFE_PATH','Legacy backup must be a physical directory');
75
+ const record=JSON.parse(fs.readFileSync(path.join(legacy,receiptName),'utf8'));
76
+ if(record.package!==pkg.name||record.target!==target||record.agent!==options.agent)throw fail('UNMANAGED_TARGET','Legacy backup ownership does not match selected target');
77
+ const destination=path.join(recovery,'legacy-'+crypto.randomUUID());
78
+ fs.renameSync(legacy,destination);
79
+ migratedBackups.push({from:legacy,to:destination,version:record.version});
80
+ legacy=record.backup;
81
+ }
82
+ writeNew(path.join(staging,receiptName),{schemaVersion:1,package:pkg.name,name:'ecom-cli',version:manifest.version,target,agent:options.agent,operationId,backup,migratedBackups,files:tree(staging)});
55
83
  if(backup) fs.renameSync(target,backup);
56
84
  try {fs.renameSync(staging,target);activated=true;}catch(e){if(backup&&!exists(target))fs.renameSync(backup,target);throw e;}
57
85
  const result=status(options,base);
58
86
  if(!result.current) throw fail('VERIFY_FAILED','Installation integrity verification failed');
59
87
  return {...result,changed:true,operationId,backup,completion:{cli:true,skill:true,hostDiscovery:false},remainingActions:['Load installed SKILL.md through the current Agent discovery mechanism']};
60
88
  } catch(e) {
89
+ if(!activated)for(const moved of migratedBackups.toReversed())if(!exists(moved.from)&&exists(moved.to)){try{fs.renameSync(moved.to,moved.from);}catch{/* Preserve both recorded identities for manual recovery. */}}
61
90
  if(!activated&&exists(staging)) fs.rmSync(staging,{recursive:true});
62
- throw fail(e.code||'INSTALL_FAILED',e.code?e.message:'Skill activation failed; the previous installation is preserved',{operationId,recovery:{backup,target,activated,action:'Inspect skill status before retrying'}});
91
+ throw fail(e.code||'INSTALL_FAILED',e.code?e.message:'Skill activation failed; the previous installation is preserved',{operationId,recovery:{backup,target,activated,migratedBackups,action:'Inspect skill status before retrying'}});
63
92
  } finally {fs.closeSync(fd);fs.unlinkSync(lock);}
64
93
  }
65
94
  const cacheRoot=()=>path.join(os.homedir(),'.cache','ecom-cli');
@@ -97,6 +126,10 @@ export async function updateInstall(options) {
97
126
  if(acquired.status!==0) throw fail('PACKAGE_ACQUISITION_FAILED','Candidate installation failed; existing runtime and Skill retained',{operationId,recovery:{journal,stage:'acquisition',action:'Inspect candidate and operation record before retrying'}});
98
127
  const candidate=path.join(directory,'node_modules/@petercjl/ecom-cli');
99
128
  if(source(candidate).version!==selected) throw fail('VERSION_MISMATCH','Acquired candidate version differs from requested version',{operationId,recovery:{journal}});
100
- const result=installSkill(options,candidate);
129
+ // The acquired version owns its installer semantics; historical callers may
130
+ // require one new-entry skill update to reconcile their legacy backup layout.
131
+ const handoff=spawnSync(process.execPath,[path.join(candidate,'bin/ecom-cli.mjs'),'skill','update','--agent',options.agent,'--target',before.target,'--json'],{encoding:'utf8',timeout:30000});
132
+ let result;try{result=JSON.parse(handoff.stdout);}catch{}
133
+ if(handoff.status!==0||!result?.ok)throw fail('CANDIDATE_ACTIVATION_FAILED','New-version installer failed; inspect target status and recorded candidate',{operationId,recovery:{journal,stage:'activation',candidate,backup:result?.error?.recovery?.backup,action:'Use the recorded candidate entry to inspect status and reconcile the exact target'}});
101
134
  return {...result,operationId,journal,selectedVersion:selected,bootstrapRetained:true,activation:'Use the entry in this receipt; the selected Skill contains its own CLI runtime'};
102
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@petercjl/ecom-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Agent CLI and portable Skill for Ecom browser containers",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -33,6 +33,8 @@ Resolve the current Agent's exact Skill destination from host configuration; use
33
33
 
34
34
  For an authorized package update, invoke the installed entry's `update install --agent NAME --target PATH --yes --json`. It acquires stable latest and activates CLI plus Skill together. Use the new entry from its receipt, verify CLI/Skill versions and SHA-256 `current=true`, then refresh and load this exact Skill through host discovery. Preserve receipts and backups. Local edits and unmanaged destinations require reconciliation before replacement; other Agent installations remain untouched. Automatic task-entry checks only notify; offline checks retain the installed runtime.
35
35
 
36
+ Always inspect `skill status` using the newly activated entry. If `recovery.required=true` identifies legacy backups in host discovery, run the new entry's `skill update` for that same target and verify recovery clears. Backups are preserved outside the Skill discovery directory; migration records identify both old and new locations.
37
+
36
38
  ## QA and evolution
37
39
 
38
40
  Success requires actual structured CLI output, not inferred browser state. Maintenance additionally requires integrity plus native host discovery. Runtime checks do not claim clean-context regression or other Agents/OS tests. Add new commands to the browser capability layer, CLI schema, tests and this canonical Skill together in an authorized development cycle. This simple deterministic Skill uses direct internalization of user-confirmed scope and verified CLI contracts; private knowledge bases are not runtime dependencies.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@petercjl/ecom-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }