@petercjl/ecom-cli 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/UPDATE.md +2 -0
- package/bin/ecom-cli.mjs +1 -1
- package/capabilities.json +107 -4
- package/lib/browser.mjs +64 -6
- package/lib/cli.mjs +11 -4
- package/lib/maintenance.mjs +40 -7
- package/package.json +1 -1
- package/skills/ecom-cli/SKILL.md +12 -4
- package/skills/ecom-cli/capabilities.json +5 -1
- package/skills/ecom-cli/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ecom CLI
|
|
2
2
|
|
|
3
|
-
Agent CLI and portable Skill for the Ecom desktop browser.
|
|
3
|
+
Agent CLI and portable Skill for the Ecom desktop browser. List existing containers, inspect a selected account’s platform/homepage/login status, and perform explicitly authorized HTTPS JSON GET operations through that container’s native session, preserving the active tab. Ecom must already be running in normal desktop mode. The browser transport currently supports macOS; Windows browser integration is not implemented.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -14,6 +14,12 @@ The Agent needs local Node.js 22+ and terminal/file access. Installation include
|
|
|
14
14
|
|
|
15
15
|
> 用 Ecom CLI 列出我当前的浏览器容器。
|
|
16
16
|
|
|
17
|
+
> 用 Ecom CLI 查看指定账号的平台、首页和登录核验状态。
|
|
18
|
+
|
|
19
|
+
> 用 Ecom CLI,通过指定账号请求我提供的只读接口地址。
|
|
20
|
+
|
|
21
|
+
Provide the account name or stable container ID. For requests, supply an authorized read-only HTTPS endpoint or a trusted registered business operation. The browser permits only its explicitly enabled origin, rejects redirects/HTML and unsupported methods, and retains credentials in its native session. Native account readiness and identity checks remain authoritative. An HTTP 200 JSON response is transport success; the caller evaluates business success.
|
|
22
|
+
|
|
17
23
|
Container names may identify accounts. Use a trusted Agent and share results intentionally. Installing the plugin does not authorize website operations, cookie export or account modification.
|
|
18
24
|
|
|
19
25
|
## Update
|
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/bin/ecom-cli.mjs
CHANGED
|
@@ -4,6 +4,6 @@ try {
|
|
|
4
4
|
const result = await main(process.argv.slice(2));
|
|
5
5
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
6
6
|
} catch (error) {
|
|
7
|
-
process.stdout.write(JSON.stringify({ok:false,error:{code:error.code || 'CLI_ERROR',message:error.code ? error.message : 'Operation failed; inspect doctor and operation status',executed:error.executed ?? false,...(error.operationId ? {operationId:error.operationId} : {}),...(error.recovery ? {recovery:error.recovery} : {})}}) + '\n');
|
|
7
|
+
process.stdout.write(JSON.stringify({ok:false,error:{code:error.code || 'CLI_ERROR',message:error.code ? error.message : 'Operation failed; inspect doctor and operation status',executed:error.executed ?? false,...(error.operationId ? {operationId:error.operationId} : {}),...(error.httpStatus!==undefined?{httpStatus:error.httpStatus}:{}),...(error.recovery ? {recovery:error.recovery} : {})}}) + '\n');
|
|
8
8
|
process.exitCode = error.code === 'INVALID_ARGUMENT' ? 2 : 1;
|
|
9
9
|
}
|
package/capabilities.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@petercjl/ecom-cli",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"skills": [
|
|
6
6
|
"ecom-cli"
|
|
7
7
|
],
|
|
@@ -159,6 +159,98 @@
|
|
|
159
159
|
}
|
|
160
160
|
},
|
|
161
161
|
"sideEffects": "package-and-skill-update"
|
|
162
|
+
},
|
|
163
|
+
"containers inspect": {
|
|
164
|
+
"options": {
|
|
165
|
+
"--json": {
|
|
166
|
+
"type": "boolean"
|
|
167
|
+
},
|
|
168
|
+
"--profile": {
|
|
169
|
+
"type": "string"
|
|
170
|
+
},
|
|
171
|
+
"--instance": {
|
|
172
|
+
"type": "string"
|
|
173
|
+
},
|
|
174
|
+
"--container": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"required": true,
|
|
177
|
+
"description": "Stable container ID or unique exact name"
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"sideEffects": "local-write",
|
|
181
|
+
"authorization": "Read selected account metadata; credentials excluded and homepage credential fields redacted. Automatic update detection only.",
|
|
182
|
+
"output": {
|
|
183
|
+
"container": "id, name, platform, platformName, homeUrl, icon, color, login",
|
|
184
|
+
"login": "state, service, authenticationState, verificationSupported, checking, checkedAt, identityBound, identityConfirmed, expiry"
|
|
185
|
+
},
|
|
186
|
+
"errors": [
|
|
187
|
+
"CONTAINER_NOT_FOUND",
|
|
188
|
+
"CONTAINER_AMBIGUOUS",
|
|
189
|
+
"PROTOCOL_MISMATCH"
|
|
190
|
+
]
|
|
191
|
+
},
|
|
192
|
+
"session request": {
|
|
193
|
+
"options": {
|
|
194
|
+
"--json": {
|
|
195
|
+
"type": "boolean"
|
|
196
|
+
},
|
|
197
|
+
"--profile": {
|
|
198
|
+
"type": "string"
|
|
199
|
+
},
|
|
200
|
+
"--instance": {
|
|
201
|
+
"type": "string"
|
|
202
|
+
},
|
|
203
|
+
"--container": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"required": true,
|
|
206
|
+
"description": "Stable container ID or unique exact name"
|
|
207
|
+
},
|
|
208
|
+
"--url": {
|
|
209
|
+
"type": "string",
|
|
210
|
+
"required": true,
|
|
211
|
+
"description": "HTTPS URL in the origin explicitly enabled by Ecom; no credentials or fragment"
|
|
212
|
+
},
|
|
213
|
+
"--method": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"default": "GET",
|
|
216
|
+
"enum": [
|
|
217
|
+
"GET"
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
"sideEffects": "local-write",
|
|
222
|
+
"authorization": "Explicit user-authorized read operation using selected persistent container; website credentials remain native. GET is a transport restriction, not proof of business read-only semantics.",
|
|
223
|
+
"output": {
|
|
224
|
+
"instance": "Pinned Ecom identity",
|
|
225
|
+
"container": "id and name",
|
|
226
|
+
"httpStatus": "200",
|
|
227
|
+
"data": "Native-redacted JSON response",
|
|
228
|
+
"businessStatus": "not-evaluated"
|
|
229
|
+
},
|
|
230
|
+
"limits": {
|
|
231
|
+
"clientTimeoutMs": 60000,
|
|
232
|
+
"nativeNetworkTimeoutMs": 20000,
|
|
233
|
+
"nativeResponseBytes": 2097152,
|
|
234
|
+
"redirects": "rejected",
|
|
235
|
+
"html": "rejected",
|
|
236
|
+
"concurrency": "native limit 2 requests"
|
|
237
|
+
},
|
|
238
|
+
"errors": [
|
|
239
|
+
"INVALID_URL",
|
|
240
|
+
"METHOD_UNSUPPORTED",
|
|
241
|
+
"CREDENTIAL_INPUT_REJECTED",
|
|
242
|
+
"ORIGIN_NOT_ALLOWED",
|
|
243
|
+
"CONTAINER_NOT_FOUND",
|
|
244
|
+
"CONTAINER_AMBIGUOUS",
|
|
245
|
+
"SHOP_NOT_READY",
|
|
246
|
+
"SHOP_VERIFICATION_FAILED",
|
|
247
|
+
"BUSY",
|
|
248
|
+
"REQUEST_FAILED",
|
|
249
|
+
"NON_JSON_RESPONSE",
|
|
250
|
+
"INSTANCE_CHANGED",
|
|
251
|
+
"PROTOCOL_MISMATCH",
|
|
252
|
+
"OPERATION_TIMEOUT"
|
|
253
|
+
]
|
|
162
254
|
}
|
|
163
255
|
},
|
|
164
256
|
"transport": {
|
|
@@ -311,13 +403,13 @@
|
|
|
311
403
|
"updateGuide": "UPDATE.md",
|
|
312
404
|
"install": "Resolve current Agent exact Skill target; run skill install --agent NAME --target PATH; load Skill through host discovery.",
|
|
313
405
|
"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
|
|
406
|
+
"verification": "Verify CLI and Skill versions, managed hashes, current=true and recovery.required=false; host loads installed SKILL.md independently."
|
|
315
407
|
},
|
|
316
408
|
"automaticUpdates": {
|
|
317
409
|
"enabledByDefault": true,
|
|
318
410
|
"intervalHours": 24,
|
|
319
411
|
"timeoutMs": 1500,
|
|
320
|
-
"trigger": "containers list task entry",
|
|
412
|
+
"trigger": "containers list, containers inspect and session request task entry",
|
|
321
413
|
"configuration": "Stable latest channel; detection only. Explicit update activates only the selected target runtime and Skill together.",
|
|
322
414
|
"disable": "ECOM_CLI_UPDATE_CHECK=0",
|
|
323
415
|
"recovery": "Offline checks retain installed runtime; local edit/ownership conflicts stop replacement; prior target kept as backup. Use recorded candidate and inspect target status before retrying.",
|
|
@@ -340,6 +432,17 @@
|
|
|
340
432
|
"TARGET_REQUIRED",
|
|
341
433
|
"LOCAL_EDITS",
|
|
342
434
|
"UNMANAGED_TARGET",
|
|
343
|
-
"TARGET_LOCKED"
|
|
435
|
+
"TARGET_LOCKED",
|
|
436
|
+
"INVALID_URL",
|
|
437
|
+
"METHOD_UNSUPPORTED",
|
|
438
|
+
"CREDENTIAL_INPUT_REJECTED",
|
|
439
|
+
"ORIGIN_NOT_ALLOWED",
|
|
440
|
+
"CONTAINER_NOT_FOUND",
|
|
441
|
+
"CONTAINER_AMBIGUOUS",
|
|
442
|
+
"SHOP_NOT_READY",
|
|
443
|
+
"SHOP_VERIFICATION_FAILED",
|
|
444
|
+
"BUSY",
|
|
445
|
+
"REQUEST_FAILED",
|
|
446
|
+
"NON_JSON_RESPONSE"
|
|
344
447
|
]
|
|
345
448
|
}
|
package/lib/browser.mjs
CHANGED
|
@@ -26,27 +26,85 @@ export function discover({profile,instance}={}) {
|
|
|
26
26
|
if(candidates.length!==1) throw fail(candidates.length?'INSTANCE_AMBIGUOUS':'INSTANCE_NOT_RUNNING',candidates.length?'Multiple Ecom instances; specify --profile or --instance':'Open an Ecom build supporting this CLI in normal desktop mode');
|
|
27
27
|
return candidates[0];
|
|
28
28
|
}
|
|
29
|
+
const instanceOf=({endpoint,channel})=>({id:endpoint.instanceId,pid:endpoint.pid,channel});
|
|
30
|
+
const credentialKey=/token|cookie|csrf|password|authorization|secret|sessionid|api[-_]?key|access[-_]?key|credential/i;
|
|
31
|
+
function homeUrl(value) {
|
|
32
|
+
try {
|
|
33
|
+
const url=new URL(value);if(url.protocol!=='https:')return null;
|
|
34
|
+
url.username='';url.password='';
|
|
35
|
+
for(const [key,value]of url.searchParams)if(credentialKey.test(key)||credentialKey.test(value)&&/[=%]/.test(value))url.searchParams.set(key,'[REDACTED]');
|
|
36
|
+
if(credentialKey.test(url.hash))url.hash='[REDACTED]';
|
|
37
|
+
return url.href;
|
|
38
|
+
}catch{return null;}
|
|
39
|
+
}
|
|
40
|
+
export function resolveContainer(items,value) {
|
|
41
|
+
if(typeof value!=='string'||!value.trim())throw fail('INVALID_ARGUMENT','Specify --container with a stable ID or unique exact name');
|
|
42
|
+
const matches=items.filter(c=>c.id===value||c.name===value);
|
|
43
|
+
if(matches.length!==1)throw fail(matches.length?'CONTAINER_AMBIGUOUS':'CONTAINER_NOT_FOUND','Select an existing stable container ID or unique exact name');
|
|
44
|
+
return matches[0];
|
|
45
|
+
}
|
|
46
|
+
export function validateReadUrl(value,method='GET') {
|
|
47
|
+
if(method!=='GET')throw fail('METHOD_UNSUPPORTED','This version supports explicit read-only GET operations only');
|
|
48
|
+
let url;try{url=new URL(value);}catch{throw fail('INVALID_URL','Specify an absolute HTTPS URL');}
|
|
49
|
+
if(url.protocol!=='https:'||url.username||url.password||url.hash)throw fail('INVALID_URL','HTTPS URL must have no credentials or fragment');
|
|
50
|
+
for(const [key,value]of url.searchParams)if(credentialKey.test(key)||credentialKey.test(value)&&/[=%]/.test(value))throw fail('CREDENTIAL_INPUT_REJECTED','Credentials remain in the browser; credential query parameters are not accepted');
|
|
51
|
+
return url.href;
|
|
52
|
+
}
|
|
29
53
|
export async function listContainers(options={}) {return readMailbox(discover(options));}
|
|
30
|
-
export async function readMailbox(
|
|
54
|
+
export async function readMailbox(connection,timeoutMs=10000) {
|
|
55
|
+
const body=await callMailbox(connection,{method:'containers.list'},timeoutMs);
|
|
56
|
+
if(!Array.isArray(body.containers)||body.containers.some(c=>typeof c.id!=='string'||typeof c.name!=='string'))throw fail('PROTOCOL_MISMATCH','Invalid container list',{operationId:body.operationId,executed:'unknown'});
|
|
57
|
+
return {...body,instance:instanceOf(connection),containers:body.containers.map(({id,name})=>({id,name}))};
|
|
58
|
+
}
|
|
59
|
+
export async function inspectContainer(options={}) {
|
|
60
|
+
if(!options.container)throw fail('INVALID_ARGUMENT','containers inspect requires --container');
|
|
61
|
+
return inspectInConnection(discover(options),options.container);
|
|
62
|
+
}
|
|
63
|
+
export async function inspectInConnection(connection,selector) {
|
|
64
|
+
const body=await callMailbox(connection,{method:'shops.list'});
|
|
65
|
+
if(!Array.isArray(body.shops)||body.shops.some(c=>typeof c.id!=='string'||typeof c.name!=='string'))throw fail('PROTOCOL_MISMATCH','Invalid account metadata',{operationId:body.operationId,executed:'unknown'});
|
|
66
|
+
const row=resolveContainer(body.shops,selector);
|
|
67
|
+
const string=x=>typeof x==='string'?x:null;
|
|
68
|
+
const container={id:row.id,name:row.name,platform:string(row.platform),platformName:string(row.customPlatformName),homeUrl:homeUrl(row.homeUrl),icon:Number.isInteger(row.icon)?row.icon:null,color:string(row.color),
|
|
69
|
+
login:{state:string(row.state)??'unconfigured',service:string(row.authentication?.service),authenticationState:string(row.authentication?.state),verificationSupported:row.verificationSupported===true,checking:row.checking===true,checkedAt:string(row.checkedAt),identityBound:!!row.identity,identityConfirmed:row.state==='ready'&&!!row.identity,expiry:row.expiry?{kind:string(row.expiry.kind),cookieExpiresAt:string(row.expiry.cookieExpiresAt)}:null}};
|
|
70
|
+
return {ok:true,schemaVersion:1,operationId:body.operationId,instance:instanceOf(connection),container};
|
|
71
|
+
}
|
|
72
|
+
export async function requestSession(options={}) {
|
|
73
|
+
if(!options.container||!options.url)throw fail('INVALID_ARGUMENT','session request requires --container and --url');
|
|
74
|
+
const url=validateReadUrl(options.url,options.method??'GET');
|
|
75
|
+
return requestInConnection(discover(options),{...options,url});
|
|
76
|
+
}
|
|
77
|
+
export async function requestInConnection(connection,options) {
|
|
78
|
+
const url=validateReadUrl(options.url,options.method??'GET');
|
|
79
|
+
if(!connection.endpoint.allowedOrigin||new URL(url).origin!==connection.endpoint.allowedOrigin)throw fail('ORIGIN_NOT_ALLOWED','URL must match the HTTPS origin explicitly enabled by this Ecom instance');
|
|
80
|
+
const listed=await readMailbox(connection);
|
|
81
|
+
const container=resolveContainer(listed.containers,options.container);
|
|
82
|
+
const body=await callMailbox(connection,{method:'session.get',containerId:container.id,url},60000);
|
|
83
|
+
if(body.containerId!==container.id||body.httpStatus!==200||!Object.hasOwn(body,'data'))throw fail('PROTOCOL_MISMATCH','Session response does not match selected container or JSON response contract',{operationId:body.operationId,executed:'unknown'});
|
|
84
|
+
return {ok:true,schemaVersion:1,operationId:body.operationId,instance:instanceOf(connection),container,request:{method:'GET',url},httpStatus:body.httpStatus,data:body.data,businessStatus:'not-evaluated'};
|
|
85
|
+
}
|
|
86
|
+
export async function callMailbox({folder,endpoint},payload,timeoutMs=10000) {
|
|
31
87
|
privatePath(folder,true);privatePath(path.join(folder,'endpoint.json'));
|
|
32
88
|
const current=JSON.parse(fs.readFileSync(path.join(folder,'endpoint.json'),'utf8'));
|
|
33
89
|
if(current.pid!==endpoint.pid || current.instanceId!==endpoint.instanceId || current.authToken!==endpoint.authToken) throw fail('INSTANCE_CHANGED','Instance changed before submission; discover it again');
|
|
34
90
|
const id=crypto.randomUUID(), request=path.join(folder,id+'.request.json'), temporary=path.join(folder,id+'.request.tmp'), response=path.join(folder,id+'.response.json');
|
|
35
91
|
if(exists(request)||exists(response)) throw fail('OPERATION_COLLISION','Request already exists');
|
|
36
|
-
writeNew(temporary,{schemaVersion:1,instanceId:endpoint.instanceId,authToken:endpoint.authToken
|
|
92
|
+
writeNew(temporary,{...payload,schemaVersion:1,instanceId:endpoint.instanceId,authToken:endpoint.authToken});
|
|
37
93
|
fs.renameSync(temporary,request);
|
|
38
94
|
const end=Date.now()+timeoutMs;
|
|
39
95
|
while(Date.now()<end) {
|
|
40
96
|
if(exists(response)) {
|
|
41
97
|
privatePath(response);
|
|
42
98
|
const raw=fs.readFileSync(response,'utf8');
|
|
43
|
-
if(Buffer.byteLength(raw)>1024*1024) throw fail('PROTOCOL_MISMATCH','Oversized response',{operationId:id,executed:'unknown'});
|
|
99
|
+
if(Buffer.byteLength(raw)>(payload.method==='session.get'?2*1024*1024+65536:1024*1024)) throw fail('PROTOCOL_MISMATCH','Oversized response',{operationId:id,executed:'unknown'});
|
|
44
100
|
let body;try{body=JSON.parse(raw);}catch{await pause(25);continue;}
|
|
45
101
|
if(body.schemaVersion!==1 || body.instanceId!==endpoint.instanceId || body.operationId!==id) throw fail('PROTOCOL_MISMATCH','Response does not match this operation',{operationId:id,executed:'unknown'});
|
|
46
102
|
fs.unlinkSync(response);
|
|
47
|
-
if(body.ok!==true)
|
|
48
|
-
|
|
49
|
-
|
|
103
|
+
if(body.ok!==true) {
|
|
104
|
+
const code=/^[A-Z][A-Z0-9_]{0,63}$/.test(body.error?.code??'')?body.error.code:'BROWSER_ERROR';
|
|
105
|
+
throw fail(code,'Ecom refused the request: '+code,{operationId:id,executed:body.error?.executed??'unknown',...(Number.isInteger(body.error?.httpStatus)?{httpStatus:body.error.httpStatus}:{})});
|
|
106
|
+
}
|
|
107
|
+
return body;
|
|
50
108
|
}
|
|
51
109
|
await pause(25);
|
|
52
110
|
}
|
package/lib/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {root,pkg,fail} from './common.mjs';
|
|
4
|
-
import {discover,listContainers} from './browser.mjs';
|
|
4
|
+
import {discover,listContainers,inspectContainer,requestSession,validateReadUrl} from './browser.mjs';
|
|
5
5
|
import {source,status,installSkill,checkUpdate,updateInstall} from './maintenance.mjs';
|
|
6
6
|
export const capabilities=()=>JSON.parse(fs.readFileSync(path.join(root,'capabilities.json'),'utf8'));
|
|
7
7
|
export function parse(argv) {
|
|
@@ -13,7 +13,7 @@ export function parse(argv) {
|
|
|
13
13
|
const key=word.slice(2);
|
|
14
14
|
if(Object.hasOwn(options,key)) throw fail('INVALID_ARGUMENT','Duplicate option '+word);
|
|
15
15
|
if(['json','help','yes'].includes(key))options[key]=true;
|
|
16
|
-
else if(['agent','target','profile','instance','name','version'].includes(key)&&argv[i+1]&&!argv[i+1].startsWith('--')) options[key]=argv[++i];
|
|
16
|
+
else if(['agent','target','profile','instance','name','version','container','url','method'].includes(key)&&argv[i+1]&&!argv[i+1].startsWith('--')) options[key]=argv[++i];
|
|
17
17
|
else throw fail('INVALID_ARGUMENT','Unknown option or missing value: '+word);
|
|
18
18
|
}
|
|
19
19
|
let command=positions.join(' ')||'help';
|
|
@@ -22,11 +22,16 @@ export function parse(argv) {
|
|
|
22
22
|
if(!caps.commandContracts[command]) throw fail('INVALID_ARGUMENT','Unsupported command; use help --json');
|
|
23
23
|
if(command!=='help')for(const key of Object.keys(options))if(!caps.commandContracts[command].options['--'+key])throw fail('INVALID_ARGUMENT','Option is not supported by this command: --'+key);
|
|
24
24
|
if(options.name&&options.name!=='ecom-cli')throw fail('INVALID_ARGUMENT','Unknown Skill name');
|
|
25
|
+
if(command==='containers inspect'&&!options.container)throw fail('INVALID_ARGUMENT','containers inspect requires --container');
|
|
26
|
+
if(command==='session request') {
|
|
27
|
+
if(!options.container||!options.url)throw fail('INVALID_ARGUMENT','session request requires --container and --url');
|
|
28
|
+
options.url=validateReadUrl(options.url,options.method??'GET');
|
|
29
|
+
}
|
|
25
30
|
return {command,options,requested};
|
|
26
31
|
}
|
|
27
32
|
export async function main(argv) {
|
|
28
33
|
const {command,options,requested}=parse(argv);
|
|
29
|
-
if(command==='help')return {ok:true,package:pkg.name,version:pkg.version,usage:'ecom-cli containers list
|
|
34
|
+
if(command==='help')return {ok:true,package:pkg.name,version:pkg.version,usage:'ecom-cli containers list|inspect | session request | skill source|status|install|update | update check|install | doctor; use help --json for required options',...capabilities(),...(capabilities().commandContracts[requested]??{})};
|
|
30
35
|
if(command==='version')return {ok:true,package:pkg.name,version:pkg.version,protocolVersion:1};
|
|
31
36
|
if(command==='capabilities'||command==='schema')return {ok:true,...capabilities()};
|
|
32
37
|
if(command==='skill source') {const item=source();return {...capabilities(),...item,complete:true,items:[item]};}
|
|
@@ -38,9 +43,11 @@ export async function main(argv) {
|
|
|
38
43
|
let instance,error;try{const found=discover(options);instance={id:found.endpoint.instanceId,pid:found.endpoint.pid,channel:found.channel};}catch(e){error={code:e.code,message:e.message};}
|
|
39
44
|
return {ok:!error,package:pkg.name,version:pkg.version,node:process.version,platform:process.platform,transport:'private-local-mailbox',debuggingRequired:false,instance,...(error?{error}:{}),skillSource:source().path};
|
|
40
45
|
}
|
|
41
|
-
if(
|
|
46
|
+
if(['containers list','containers inspect','session request'].includes(command)) {
|
|
42
47
|
const update=await checkUpdate({automatic:true});
|
|
43
48
|
if(update.available)process.stderr.write('Ecom CLI update available: '+update.latest+'; ask the current Agent to update the package and Skill.\n');
|
|
49
|
+
if(command==='containers inspect')return inspectContainer(options);
|
|
50
|
+
if(command==='session request')return requestSession(options);
|
|
44
51
|
return listContainers(options);
|
|
45
52
|
}
|
|
46
53
|
}
|
package/lib/maintenance.mjs
CHANGED
|
@@ -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
|
-
|
|
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?
|
|
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
|
-
|
|
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
package/skills/ecom-cli/SKILL.md
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ecom-cli
|
|
3
|
-
description: List
|
|
3
|
+
description: List and inspect account containers in the running Ecom browser, send explicitly authorized read-only HTTPS JSON requests using a selected container session, diagnose connections, or install/update the Ecom CLI and companion Skill. Use for Ecom account metadata and native-session API access; application business logic stays with the calling workflow.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Ecom CLI
|
|
7
7
|
|
|
8
8
|
## Input → strategy → output
|
|
9
9
|
|
|
10
|
-
Input is
|
|
10
|
+
Input is an Ecom container listing/detail request, an authorized read-only endpoint plus target container, a connection diagnosis, or selected-Agent maintenance intent. The current Agent needs local command execution, file access and Node.js 22+. Output is a container list, projected account metadata, native-redacted JSON response with pinned target identity, connection diagnosis, or installation receipt with a separate host discovery check. Container names may identify accounts; disclose only within the user's request.
|
|
11
11
|
|
|
12
12
|
## Main line
|
|
13
13
|
|
|
14
14
|
1. Resolve this installed Skill's directory from the host's actual discovery record. Its bundled CLI is `.runtime/bin/ecom-cli.mjs` relative to that directory. Invoke it through an available Node.js 22+ runtime, checking host-managed runtime configuration before PATH. Canonical npm source has no `.runtime`; complete its managed installation using the packaged INSTALL.md first.
|
|
15
15
|
2. Read [capabilities.json](capabilities.json) and use [the generic adapter](adapters/generic.json) for any host with the required capabilities. Probe the actual entry with `version --json` and `capabilities --json`; use only declared commands and flags. A missing runtime or required capability ends with CAPABILITY_UNAVAILABLE; do not modify adapters during a task.
|
|
16
|
-
3.
|
|
17
|
-
4.
|
|
16
|
+
3. Choose the supported operation from the live schema. Use `containers list --json` to locate targets, then use a stable ID or unique exact name. Ecom must already be running in normal desktop mode; discovery validates its executable and protected endpoint. Multiple instances require an explicit intended instance/profile rather than a guess.
|
|
17
|
+
4. For account details, run `containers inspect --container VALUE --json`. Report platform, saved homepage and timestamped service-login status. `identityBound` records a saved binding; `identityConfirmed` requires the native ready state. Cookie expiry is a reference, not proof of current service access. Account names are labels and do not grant or remove capabilities. Credential-like homepage fields are redacted.
|
|
18
|
+
5. For a session request, require an explicit authorized read-only endpoint from the user or a trusted business capability’s registered operation. Business selection, endpoint semantics, date/metrics and interpretation belong to that caller. Run `session request --container VALUE --url HTTPS_URL --method GET --json`. GET alone does not prove an endpoint has no side effects; resolve unclear endpoint intent before submission. The CLI pins one instance and container across discovery and execution. Credentials remain native; header/body/credential inputs, redirects and HTML are unsupported. The enabled origin is controlled by Ecom, not expanded by the CLI.
|
|
19
|
+
6. Require `ok=true`, protocol version 1, operation ID and matching instance/container IDs. For requests, require the JSON response contract and distinguish HTTP success from business success: `businessStatus=not-evaluated` requires the caller’s platform-specific check. Preserve structured errors and executed status. These operations do not navigate or activate tabs.
|
|
18
20
|
|
|
19
21
|
## Diagnosis and recovery
|
|
20
22
|
|
|
23
|
+
- SHOP_NOT_READY: inspect this exact account. Complete login and platform-identity verification through its Ecom account management page; a cookie expiry or account name does not satisfy readiness. If the native adapter cannot verify identity, report that gap and stop the data request. Return to step 5 only after readiness is established.
|
|
24
|
+
- SHOP_VERIFICATION_FAILED: stop; the browser’s pre-request identity check failed. Preserve the selected target and report the failure.
|
|
25
|
+
- ORIGIN_NOT_ALLOWED, CREDENTIAL_INPUT_REJECTED, METHOD_UNSUPPORTED or INVALID_URL: correct the intended input within the declared contract; do not broaden browser permissions.
|
|
26
|
+
- REQUEST_FAILED or NON_JSON_RESPONSE: preserve HTTP/operation/execute fields; handle login, permission or challenge through the original container. Do not infer business success.
|
|
21
27
|
- INSTANCE_NOT_RUNNING: run `doctor --json`. Ask the user to open a compatible Ecom version. Do not enable remote debugging or substitute another browser.
|
|
22
28
|
- INSTANCE_AMBIGUOUS: obtain the intended instance or profile, then return to step 3.
|
|
23
29
|
- INSTANCE_CHANGED: no request was submitted; rediscover before returning to step 3.
|
|
@@ -33,6 +39,8 @@ Resolve the current Agent's exact Skill destination from host configuration; use
|
|
|
33
39
|
|
|
34
40
|
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
41
|
|
|
42
|
+
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.
|
|
43
|
+
|
|
36
44
|
## QA and evolution
|
|
37
45
|
|
|
38
46
|
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.
|
|
@@ -24,13 +24,17 @@
|
|
|
24
24
|
"output_fields": [
|
|
25
25
|
"ok",
|
|
26
26
|
"containers",
|
|
27
|
+
"container",
|
|
27
28
|
"instance",
|
|
29
|
+
"httpStatus",
|
|
30
|
+
"data",
|
|
31
|
+
"businessStatus",
|
|
28
32
|
"error"
|
|
29
33
|
],
|
|
30
34
|
"side_effects": {
|
|
31
35
|
"external_mutation": false,
|
|
32
36
|
"may_cost_money": false,
|
|
33
|
-
"authorization": "
|
|
37
|
+
"authorization": "Container metadata reads and explicitly authorized read-only HTTPS operations; website Set-Cookie may refresh the native session. Selected-target maintenance requires installation/update intent."
|
|
34
38
|
},
|
|
35
39
|
"errors": [
|
|
36
40
|
"CAPABILITY_UNAVAILABLE",
|