@hanamesh/dsh-app-host 0.6.18 → 0.6.21
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 +3 -1
- package/dist/client-ui.d.ts +4 -1
- package/dist/client-ui.js +37 -31
- package/dist/install-target/fixtures/catalog.json +99 -8
- package/dist/install-target/fixtures/install-failure-cases.json +385 -35
- package/dist/install-target/fixtures/reference-consumer.js +1 -1
- package/dist/install-target/suite.d.ts +2 -1
- package/dist/install-target/suite.js +26 -6
- package/dist/installation-status.d.ts +1 -1
- package/dist/library/catalog.js +24 -7
- package/dist/library/github-target.js +42 -0
- package/dist/library/handoff.js +5 -2
- package/dist/library/install.js +27 -17
- package/dist/library/installation-status.js +3 -3
- package/dist/library/recovery.js +5 -3
- package/dist/library/routes.js +1 -0
- package/dist/library/service.js +37 -21
- package/dist/library/suite.js +24 -11
- package/docs/CONTRACT.md +2 -2
- package/docs/INSTALLATION_STATUS.md +4 -2
- package/docs/INSTALL_TARGET.md +20 -4
- package/docs/LIBRARY.md +3 -1
- package/docs/LICENSES.json +1 -1
- package/docs/SECURITY.md +2 -0
- package/package.json +1 -1
- package/schemas/catalog-provider-page.schema.json +97 -10
- package/schemas/install-target.schema.json +252 -38
|
@@ -17,42 +17,392 @@
|
|
|
17
17
|
"latest": "1.2.3"
|
|
18
18
|
},
|
|
19
19
|
"cases": [
|
|
20
|
-
{
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
"expect": {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"expect": {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
20
|
+
{
|
|
21
|
+
"id": "RB01",
|
|
22
|
+
"mode": "new-install",
|
|
23
|
+
"stage": "before-add",
|
|
24
|
+
"note": "fixed catalog target is unavailable: refuse before an operation or CLI mutation starts",
|
|
25
|
+
"expect": {
|
|
26
|
+
"code": "NOT_INSTALLABLE",
|
|
27
|
+
"status": "not-started",
|
|
28
|
+
"actions": []
|
|
29
|
+
},
|
|
30
|
+
"targetUnavailable": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "RB02",
|
|
34
|
+
"mode": "new-install",
|
|
35
|
+
"stage": "add",
|
|
36
|
+
"note": "official CLI fails before writing",
|
|
37
|
+
"inject": [
|
|
38
|
+
[
|
|
39
|
+
"add",
|
|
40
|
+
"before"
|
|
41
|
+
]
|
|
42
|
+
],
|
|
43
|
+
"expect": {
|
|
44
|
+
"code": "DSH_PLUGIN_FAILED",
|
|
45
|
+
"status": "not-needed",
|
|
46
|
+
"actions": []
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "RB03",
|
|
51
|
+
"mode": "new-install",
|
|
52
|
+
"stage": "add",
|
|
53
|
+
"note": "official CLI fails after writing package/lock/module",
|
|
54
|
+
"inject": [
|
|
55
|
+
[
|
|
56
|
+
"add",
|
|
57
|
+
"after"
|
|
58
|
+
]
|
|
59
|
+
],
|
|
60
|
+
"expect": {
|
|
61
|
+
"code": "DSH_PLUGIN_FAILED",
|
|
62
|
+
"status": "restored",
|
|
63
|
+
"actions": [
|
|
64
|
+
"dsh plugin remove $package"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "RB04",
|
|
70
|
+
"mode": "new-install",
|
|
71
|
+
"stage": "definition",
|
|
72
|
+
"note": "add succeeded, application definition is invalid",
|
|
73
|
+
"invalidDefinition": true,
|
|
74
|
+
"expect": {
|
|
75
|
+
"code": "INVALID_DEFINITION",
|
|
76
|
+
"status": "restored",
|
|
77
|
+
"actions": [
|
|
78
|
+
"dsh plugin remove $package"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "RB05",
|
|
84
|
+
"mode": "new-install",
|
|
85
|
+
"stage": "provision",
|
|
86
|
+
"note": "provision fails before promote (runtime untouched)",
|
|
87
|
+
"inject": [
|
|
88
|
+
[
|
|
89
|
+
"provision",
|
|
90
|
+
"before"
|
|
91
|
+
]
|
|
92
|
+
],
|
|
93
|
+
"expect": {
|
|
94
|
+
"code": "E_DOWNLOAD",
|
|
95
|
+
"status": "restored",
|
|
96
|
+
"actions": [
|
|
97
|
+
"dsh plugin remove $package"
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "RB06",
|
|
103
|
+
"mode": "new-install",
|
|
104
|
+
"stage": "provision",
|
|
105
|
+
"note": "provision fails after promoting a newly claimed runtime item",
|
|
106
|
+
"inject": [
|
|
107
|
+
[
|
|
108
|
+
"provision",
|
|
109
|
+
"after"
|
|
110
|
+
]
|
|
111
|
+
],
|
|
112
|
+
"expect": {
|
|
113
|
+
"code": "E_VERIFY",
|
|
114
|
+
"status": "restored",
|
|
115
|
+
"actions": [
|
|
116
|
+
"dsh plugin remove $package",
|
|
117
|
+
"provision remove runtime"
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "RB07",
|
|
123
|
+
"mode": "upgrade",
|
|
124
|
+
"stage": "before-add",
|
|
125
|
+
"note": "fixed catalog target is unavailable: refuse before an operation or CLI mutation starts",
|
|
126
|
+
"expect": {
|
|
127
|
+
"code": "NOT_INSTALLABLE",
|
|
128
|
+
"status": "not-started",
|
|
129
|
+
"actions": []
|
|
130
|
+
},
|
|
131
|
+
"targetUnavailable": true
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"id": "RB08",
|
|
135
|
+
"mode": "upgrade",
|
|
136
|
+
"stage": "add",
|
|
137
|
+
"note": "upgrade: official CLI fails after writing the new version",
|
|
138
|
+
"inject": [
|
|
139
|
+
[
|
|
140
|
+
"add",
|
|
141
|
+
"after"
|
|
142
|
+
]
|
|
143
|
+
],
|
|
144
|
+
"expect": {
|
|
145
|
+
"code": "DSH_PLUGIN_FAILED",
|
|
146
|
+
"status": "restored",
|
|
147
|
+
"actions": [
|
|
148
|
+
"dsh plugin add --save-exact $package@$previous"
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "RB09",
|
|
154
|
+
"mode": "upgrade",
|
|
155
|
+
"stage": "definition",
|
|
156
|
+
"note": "upgrade: new definition is invalid; original exact version comes back",
|
|
157
|
+
"invalidDefinition": true,
|
|
158
|
+
"expect": {
|
|
159
|
+
"code": "INVALID_DEFINITION",
|
|
160
|
+
"status": "restored",
|
|
161
|
+
"actions": [
|
|
162
|
+
"dsh plugin add --save-exact $package@$previous"
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"id": "RB10",
|
|
168
|
+
"mode": "upgrade",
|
|
169
|
+
"stage": "provision",
|
|
170
|
+
"note": "upgrade: new runtime promoted then failed; original runtime is provisioned again from the restored definition",
|
|
171
|
+
"inject": [
|
|
172
|
+
[
|
|
173
|
+
"provision",
|
|
174
|
+
"after"
|
|
175
|
+
]
|
|
176
|
+
],
|
|
177
|
+
"expect": {
|
|
178
|
+
"code": "E_VERIFY",
|
|
179
|
+
"status": "restored",
|
|
180
|
+
"actions": [
|
|
181
|
+
"dsh plugin add --save-exact $package@$previous",
|
|
182
|
+
"provision runtime"
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"id": "RB11",
|
|
188
|
+
"mode": "new-install",
|
|
189
|
+
"stage": "definition",
|
|
190
|
+
"note": "compensation itself fails: the original error is kept and the residue is reported",
|
|
191
|
+
"invalidDefinition": true,
|
|
192
|
+
"inject": [
|
|
193
|
+
[
|
|
194
|
+
"remove",
|
|
195
|
+
"before"
|
|
196
|
+
]
|
|
197
|
+
],
|
|
198
|
+
"expect": {
|
|
199
|
+
"code": "INVALID_DEFINITION",
|
|
200
|
+
"status": "failed",
|
|
201
|
+
"recoveryError": "DSH_PLUGIN_FAILED",
|
|
202
|
+
"actions": [
|
|
203
|
+
"dsh plugin remove $package"
|
|
204
|
+
],
|
|
205
|
+
"residue": [
|
|
206
|
+
"dependency",
|
|
207
|
+
"module",
|
|
208
|
+
"lockfile"
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"id": "RB12",
|
|
214
|
+
"mode": "upgrade",
|
|
215
|
+
"stage": "provision",
|
|
216
|
+
"note": "upgrade compensation fails: original version cannot be added back",
|
|
217
|
+
"inject": [
|
|
218
|
+
[
|
|
219
|
+
"provision",
|
|
220
|
+
"after"
|
|
221
|
+
],
|
|
222
|
+
[
|
|
223
|
+
"add",
|
|
224
|
+
"before",
|
|
225
|
+
1
|
|
226
|
+
]
|
|
227
|
+
],
|
|
228
|
+
"expect": {
|
|
229
|
+
"code": "E_VERIFY",
|
|
230
|
+
"status": "failed",
|
|
231
|
+
"recoveryError": "DSH_PLUGIN_FAILED",
|
|
232
|
+
"actions": [
|
|
233
|
+
"dsh plugin add --save-exact $package@$previous"
|
|
234
|
+
],
|
|
235
|
+
"residue": [
|
|
236
|
+
"dependency",
|
|
237
|
+
"module",
|
|
238
|
+
"lockfile",
|
|
239
|
+
"runtime:runtime"
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"id": "RB13",
|
|
245
|
+
"mode": "new-install",
|
|
246
|
+
"stage": "definition",
|
|
247
|
+
"note": "a concurrent profile writer is never undone; the lockfile cannot be proven restored",
|
|
248
|
+
"invalidDefinition": true,
|
|
249
|
+
"concurrent": true,
|
|
250
|
+
"expect": {
|
|
251
|
+
"code": "INVALID_DEFINITION",
|
|
252
|
+
"status": "failed",
|
|
253
|
+
"actions": [
|
|
254
|
+
"dsh plugin remove $package"
|
|
255
|
+
],
|
|
256
|
+
"residue": [
|
|
257
|
+
"lockfile"
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"id": "RB14",
|
|
263
|
+
"mode": "new-install",
|
|
264
|
+
"stage": "provision",
|
|
265
|
+
"note": "unowned files in a newly claimed runtime are kept and reported, not deleted",
|
|
266
|
+
"inject": [
|
|
267
|
+
[
|
|
268
|
+
"provision",
|
|
269
|
+
"unowned"
|
|
270
|
+
],
|
|
271
|
+
[
|
|
272
|
+
"provision",
|
|
273
|
+
"after"
|
|
274
|
+
]
|
|
275
|
+
],
|
|
276
|
+
"expect": {
|
|
277
|
+
"code": "E_VERIFY",
|
|
278
|
+
"status": "failed",
|
|
279
|
+
"actions": [
|
|
280
|
+
"dsh plugin remove $package",
|
|
281
|
+
"provision remove runtime"
|
|
282
|
+
],
|
|
283
|
+
"residue": [
|
|
284
|
+
"runtime:runtime"
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"id": "RB15",
|
|
290
|
+
"mode": "new-install",
|
|
291
|
+
"stage": "add",
|
|
292
|
+
"kind": "plugin",
|
|
293
|
+
"note": "plugin bundle: CLI fails after selecting the bundle; selection and lock come back",
|
|
294
|
+
"inject": [
|
|
295
|
+
[
|
|
296
|
+
"add",
|
|
297
|
+
"after"
|
|
298
|
+
]
|
|
299
|
+
],
|
|
300
|
+
"expect": {
|
|
301
|
+
"code": "DSH_PLUGIN_FAILED",
|
|
302
|
+
"status": "restored",
|
|
303
|
+
"actions": [
|
|
304
|
+
"dsh plugin remove $package"
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
}
|
|
50
308
|
],
|
|
51
309
|
"presentation": [
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
310
|
+
{
|
|
311
|
+
"id": "RP01",
|
|
312
|
+
"note": "restored new install is a failure that left nothing behind",
|
|
313
|
+
"recovery": {
|
|
314
|
+
"status": "restored",
|
|
315
|
+
"mode": "new-install",
|
|
316
|
+
"stage": "definition",
|
|
317
|
+
"previousVersion": null,
|
|
318
|
+
"actions": [
|
|
319
|
+
"dsh plugin remove @hanamesh-fixture/install-target-app"
|
|
320
|
+
],
|
|
321
|
+
"checks": [],
|
|
322
|
+
"residue": []
|
|
323
|
+
},
|
|
324
|
+
"expect": {
|
|
325
|
+
"installed": false,
|
|
326
|
+
"restored": true
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"id": "RP02",
|
|
331
|
+
"note": "restored upgrade names the original version",
|
|
332
|
+
"recovery": {
|
|
333
|
+
"status": "restored",
|
|
334
|
+
"mode": "upgrade",
|
|
335
|
+
"stage": "provision",
|
|
336
|
+
"previousVersion": "0.3.0",
|
|
337
|
+
"actions": [],
|
|
338
|
+
"checks": [],
|
|
339
|
+
"residue": []
|
|
340
|
+
},
|
|
341
|
+
"expect": {
|
|
342
|
+
"installed": false,
|
|
343
|
+
"restored": true,
|
|
344
|
+
"version": "0.3.0"
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"id": "RP03",
|
|
349
|
+
"note": "nothing was changed before the failure",
|
|
350
|
+
"recovery": {
|
|
351
|
+
"status": "not-needed",
|
|
352
|
+
"mode": "new-install",
|
|
353
|
+
"stage": "before-add",
|
|
354
|
+
"previousVersion": null,
|
|
355
|
+
"actions": [],
|
|
356
|
+
"checks": [],
|
|
357
|
+
"residue": []
|
|
358
|
+
},
|
|
359
|
+
"expect": {
|
|
360
|
+
"installed": false,
|
|
361
|
+
"restored": true
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"id": "RP04",
|
|
366
|
+
"note": "compensation failed: never shown as success or as rolled back",
|
|
367
|
+
"recovery": {
|
|
368
|
+
"status": "failed",
|
|
369
|
+
"mode": "upgrade",
|
|
370
|
+
"stage": "provision",
|
|
371
|
+
"previousVersion": "0.3.0",
|
|
372
|
+
"actions": [],
|
|
373
|
+
"checks": [],
|
|
374
|
+
"residue": [
|
|
375
|
+
{
|
|
376
|
+
"item": "module",
|
|
377
|
+
"reason": "NOT_RESTORED"
|
|
378
|
+
}
|
|
379
|
+
],
|
|
380
|
+
"error": {
|
|
381
|
+
"code": "DSH_PLUGIN_FAILED"
|
|
382
|
+
},
|
|
383
|
+
"originals": {
|
|
384
|
+
"dir": "/data/library-recovery/op",
|
|
385
|
+
"files": [
|
|
386
|
+
"package.json"
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
"expect": {
|
|
391
|
+
"installed": false,
|
|
392
|
+
"restored": false,
|
|
393
|
+
"residue": [
|
|
394
|
+
"module"
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"id": "RP05",
|
|
400
|
+
"note": "an rc.9 provider sends no recovery: unknown, not restored",
|
|
401
|
+
"recovery": null,
|
|
402
|
+
"expect": {
|
|
403
|
+
"installed": false,
|
|
404
|
+
"restored": false
|
|
405
|
+
}
|
|
406
|
+
}
|
|
57
407
|
]
|
|
58
408
|
}
|
|
@@ -19,7 +19,7 @@ export function createReferenceConsumer(options={}){
|
|
|
19
19
|
navigate(workspaceUrl,target){
|
|
20
20
|
const fields={...target,...(contractVersion===undefined?{}:{contractVersion})};
|
|
21
21
|
if(validate(schema,fields).length)return{ok:false,code:'INVALID_INSTALL_TARGET'};
|
|
22
|
-
const url=new URL(workspaceUrl);url.hash=contract.fragmentPrefix.slice(1)+new URLSearchParams(fields);
|
|
22
|
+
const url=new URL(workspaceUrl);url.hash=contract.fragmentPrefix.slice(1)+new URLSearchParams({...fields,...(fields.installTarget?{installTarget:JSON.stringify(fields.installTarget)}:{})});
|
|
23
23
|
return{ok:true,url:url.href};
|
|
24
24
|
},
|
|
25
25
|
/** rc.10: present a library.install-failed event; only a schema-valid restored/not-needed recovery counts as restored. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Install-target contract conformance suite (hanamesh.install-target v1). */
|
|
2
2
|
export interface ContractCaseResult {id:string;semantic:string;note:string;outcome:'pass'|'fail'|'skipped';detail?:string;}
|
|
3
3
|
export interface ContractSuiteReport {suite:'provider'|'consumer'|'chain';contract:'hanamesh.install-target';contractVersion:string;total:number;pass:number;fail:number;skipped:number;results:ContractCaseResult[];[key:string]:unknown;}
|
|
4
|
-
|
|
4
|
+
import type {PackageInstallTarget} from '../client-ui.js';
|
|
5
|
+
export interface InstallTargetFields {itemId?:string;packageName?:string;contractVersion?:string;installTarget?:PackageInstallTarget;}
|
|
5
6
|
export interface ProviderDeclaration {contract:string;contractVersion:string;supported:string[];schema?:string;}
|
|
6
7
|
/** A caller handing catalog identities to the market by normal navigation of its own workspace. */
|
|
7
8
|
export interface InstallTargetConsumer {
|
|
@@ -43,6 +43,7 @@ export function validate(schema,value,ref='#/$defs/target'){
|
|
|
43
43
|
}
|
|
44
44
|
if(type==='object'){
|
|
45
45
|
for(const key of node.required??[])if(!(key in value))errors.push(`${path}: missing ${key}`);
|
|
46
|
+
for(const [key,required] of Object.entries(node.dependentRequired??{}))if(key in value)for(const field of required)if(!(field in value))errors.push(`${path}: ${key} requires ${field}`);
|
|
46
47
|
for(const [key,child] of Object.entries(node.properties??{}))if(key in value)check(child,value[key],`${path}.${key}`);
|
|
47
48
|
if(node.additionalProperties===false)for(const key of Object.keys(value))if(!(key in (node.properties??{})))errors.push(`${path}: field ${key} is not part of the contract`);
|
|
48
49
|
}
|
|
@@ -95,7 +96,7 @@ export async function sourceRecoveryProfile({recovery},item={}){
|
|
|
95
96
|
if(item.concurrent)profile.concurrentAdd('@fixture/concurrent','1.0.0');
|
|
96
97
|
// Named single-source contract fixture; not production registry policy.
|
|
97
98
|
const installer=emit=>createLibraryInstaller({registryRules:async()=>({registryPlan:(_requested,configured)=>[configured.registry],attributeFailure:()=>'other'}),pluginManager:()=>({registries:async()=>({registry:'https://source-fixture.invalid/',fallbackRegistries:[],resolved:'https://source-fixture.invalid/'})}),profileDir:profile.profileDir,profileName:profile.profileName,dataRoot:profile.dataRoot,nodeBinary:'/source-fixture/bin/node',dshBin:'/source-fixture/dsh/lib/bin.js',
|
|
98
|
-
fetchImpl:profile.fetchImpl(
|
|
99
|
+
fetchImpl:profile.fetchImpl({[app.packageName]:app.latest,[plugin.packageName]:plugin.latest}),spawn:profile.spawn,provision:profile.provision,remove:profile.remove,ledger:profile.ledger,emit});
|
|
99
100
|
return{profile,target,installer};
|
|
100
101
|
}
|
|
101
102
|
/** Independent readback of the SOURCE profile: exact package.json/lockfile text, resolved module version, runtime ledger without timestamps. */
|
|
@@ -105,13 +106,29 @@ async function profileState({profile},name,appId){
|
|
|
105
106
|
const ledger=Object.fromEntries(Object.entries((await profile.ledger(join(profile.dataRoot,'runtimes',appId))).items).map(([id,{installedAt:_,...entry}])=>[id,entry]));
|
|
106
107
|
return{manifest:await read(join(profile.profileDir,'package.json')),lock:await read(join(profile.profileDir,'pnpm-lock.yaml')),module:module===null?null:JSON.parse(module).version,ledger};
|
|
107
108
|
}
|
|
108
|
-
/** Runs
|
|
109
|
+
/** Runs a real fixed-target failure on a fresh SOURCE profile, or verifies admission refusal before installation. */
|
|
109
110
|
export async function runRecoveryCase(contract,item,{check=true}={}){
|
|
110
111
|
const {schema,recovery}=contract;const source=await sourceRecoveryProfile(contract,item);const {profile,target}=source;const appId=recovery.application.appId;
|
|
111
|
-
|
|
112
|
+
let catalogPath=contract.catalogPath;
|
|
113
|
+
if(item.targetUnavailable){
|
|
114
|
+
const catalog=await json(new URL(contract.provider.catalog,FIXTURES)),row=catalog.items.find(row=>row.packageName===target.packageName);
|
|
115
|
+
row.oneClickInstall={allowed:false,reason:'npm-unchecked'};
|
|
116
|
+
for(const method of row.installMethods){delete method.target;method.targetUnavailableReason='npm-unchecked';}
|
|
117
|
+
catalogPath=join(profile.profileDir,'unavailable-catalog.json');await writeFile(catalogPath,JSON.stringify(catalog));
|
|
118
|
+
}
|
|
119
|
+
const host=await startProvider({...contract,catalogPath},{profile,installer:source.installer});
|
|
112
120
|
try{
|
|
113
121
|
const sentinel=join(profile.dataRoot,'principal',appId,'local','data-v1','single','user.db');await mkdir(dirname(sentinel),{recursive:true});await writeFile(sentinel,'keep');
|
|
114
122
|
const before=await profileState(source,target.packageName,appId);
|
|
123
|
+
if(item.targetUnavailable){
|
|
124
|
+
const calls=profile.calls.length;let refused;
|
|
125
|
+
try{await host.service.install({packageName:target.packageName});}catch(error){refused=error;}
|
|
126
|
+
expectEqual(refused?.code,item.expect.code,'unresolved fixed target is refused');
|
|
127
|
+
expectEqual(host.installs,[],'no installer call');expectEqual(profile.calls.length,calls,'no CLI mutation');
|
|
128
|
+
expectEqual(host.service.events().events,[],'no install operation');expectEqual(await profileState(source,target.packageName,appId),before,'profile unchanged');
|
|
129
|
+
expectEqual(await readFile(sentinel,'utf8'),'keep','application data unchanged');
|
|
130
|
+
return{type:'catalog-refused',code:refused.code};
|
|
131
|
+
}
|
|
115
132
|
const started=await host.service.install({packageName:target.packageName});await host.settled();
|
|
116
133
|
const events=host.service.events().events.filter(event=>event.operationId===started.operationId),failed=events.find(event=>event.type==='library.install-failed');
|
|
117
134
|
if(!failed||events.some(event=>event.type==='library.install-done'))throw new Error('expected one failed operation, got '+JSON.stringify(events.map(event=>event.type)));
|
|
@@ -122,7 +139,7 @@ export async function runRecoveryCase(contract,item,{check=true}={}){
|
|
|
122
139
|
expectEqual(result.actions,item.expect.actions.map(fill),'compensation actions');
|
|
123
140
|
expectEqual(result.error?.code??null,item.expect.recoveryError??null,'compensation error');
|
|
124
141
|
expectEqual([...new Set(result.residue.map(row=>row.item))].sort(),[...(item.expect.residue??[])].sort(),'residue');
|
|
125
|
-
expectEqual(profile.calls.filter(call=>call[0]==='add'&&call.at(-1)===`${target.packageName}@${target.latest}`).length,
|
|
142
|
+
expectEqual(profile.calls.filter(call=>call[0]==='add'&&call.at(-1)===`${target.packageName}@${target.latest}`).length,1,'add attempts (no automatic retry)');
|
|
126
143
|
expectEqual(await readFile(sentinel,'utf8'),'keep','application data under dataRoot');
|
|
127
144
|
const after=await profileState(source,target.packageName,appId);
|
|
128
145
|
if(result.status==='failed'){
|
|
@@ -272,7 +289,8 @@ export async function runConsumerSuite(consumer){
|
|
|
272
289
|
const prefix=contract.contract.fragmentPrefix;if(!url.hash.startsWith(prefix))throw new Error('fragment does not start with '+prefix);
|
|
273
290
|
const params=new URLSearchParams(url.hash.slice(prefix.length)),keys=[...params.keys()];
|
|
274
291
|
if(new Set(keys).size!==keys.length)throw new Error('duplicate fragment fields');
|
|
275
|
-
const fields=Object.fromEntries(params)
|
|
292
|
+
const fields=Object.fromEntries(params);if(fields.installTarget!==undefined)fields.installTarget=JSON.parse(fields.installTarget);
|
|
293
|
+
const errors=validate(schema,fields);if(errors.length)throw new Error(errors.join('; '));
|
|
276
294
|
expectEqual(fields,{...item.target,...(declared===undefined?{}:{contractVersion:declared})},'fragment fields');
|
|
277
295
|
});
|
|
278
296
|
for(const item of cases.handshake)await record(item.id,'handshake',item.note,async()=>{
|
|
@@ -306,7 +324,9 @@ export async function runChainSuite(consumer){
|
|
|
306
324
|
const result=await consumer.navigate(workspaceUrl,item.target);if(result?.ok!==true){if(item.optional)return'skipped';throw new Error('consumer refused');}
|
|
307
325
|
await client.navigate(new URL(result.url).hash);
|
|
308
326
|
if(!client.button('确认安装'))throw new Error('market did not reach confirmation: '+client.text().slice(0,400));
|
|
309
|
-
const
|
|
327
|
+
const fields=Object.fromEntries(new URLSearchParams(new URL(result.url).hash.slice(contract.contract.fragmentPrefix.length)));
|
|
328
|
+
if(fields.installTarget!==undefined)fields.installTarget=JSON.parse(fields.installTarget);
|
|
329
|
+
const resolved=await client.module.openInstallTarget(fields);
|
|
310
330
|
expectEqual(resolved.itemId,item.resolves,'resolved catalog item');expectEqual([host.posts.length,host.installs.length],[0,0],'installs');
|
|
311
331
|
await client.click('取消');
|
|
312
332
|
});
|
|
@@ -12,7 +12,7 @@ export interface InstallationStatusRequest {contractVersion:'1';consent:'read-in
|
|
|
12
12
|
export interface InstallationObservation {
|
|
13
13
|
channel:'hanamesh'|'dsh'|'claude'|'codex';
|
|
14
14
|
scope:{id:'current-runtime'|'other-runtime'|'agent-cli'|'desktop-app';kind:'responding-runtime'|'unobserved-runtime'|'official-user-profile'|'desktop-app';label:string};
|
|
15
|
-
status:'installed'|'not-installed'|'incomplete'|'unknown';version:string|null;versionKind:'
|
|
15
|
+
status:'installed'|'not-installed'|'incomplete'|'unknown';version:string|null;versionKind:'suite-bundle'|'agent-distribution';
|
|
16
16
|
components:readonly {name:string;version:string|null;active?:boolean;status?:string}[];
|
|
17
17
|
identity:{pluginName:string;marketplace:string}|null;
|
|
18
18
|
reason:{code:string;message:string;action:string}|null;
|
package/dist/library/catalog.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import semver from 'semver';
|
|
2
|
+
import {packageInstallTarget,catalogInstallationAllowed} from './github-target.js';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { AppHostError, requireCondition } from '../errors.js';
|
|
4
5
|
|
|
@@ -107,9 +108,9 @@ function suitePresentation(value,item){
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function validateHanaItem(item,maxSourceLinks){
|
|
110
|
-
const optionalText={description:5000
|
|
111
|
+
const optionalText={description:5000};
|
|
111
112
|
const source=item?.source;
|
|
112
|
-
requireCondition(own(item,['id','itemId','packageName','name','displayName','summary','catalogKind','source','description','homepage','
|
|
113
|
+
requireCondition(own(item,['id','itemId','packageName','name','displayName','summary','catalogKind','source','description','homepage','categories','keywords','repository','package','publisher','updatedAt','official','suite','installMethods','oneClickInstall'])&&
|
|
113
114
|
(item.official===undefined||typeof item.official==='boolean')&&(item.suite===undefined||suitePresentation(item.suite,item))&&
|
|
114
115
|
plain(item.id)&&item.id===item.itemId&&
|
|
115
116
|
textWithin(item.name,160,true)&&textWithin(item.displayName,120,true)&&textWithin(item.summary,1000)&&
|
|
@@ -129,6 +130,22 @@ function validateHanaItem(item,maxSourceLinks){
|
|
|
129
130
|
own(link,['sourceId','sourceRef','sourceUrl','firstSeenAt','lastSeenAt'])&&textWithin(link.sourceId,120,true)&&textWithin(link.sourceRef,512,true)&&
|
|
130
131
|
displayUrl(link.sourceUrl)&×tamp(link.firstSeenAt)&×tamp(link.lastSeenAt)),
|
|
131
132
|
'INVALID_CATALOG_PAGE','HanaMesh catalog item does not match its public schema.');
|
|
133
|
+
requireCondition(item.oneClickInstall===undefined||(own(item.oneClickInstall,['allowed','reason'])&&typeof item.oneClickInstall.allowed==='boolean'&&
|
|
134
|
+
(item.oneClickInstall.reason===null||textWithin(item.oneClickInstall.reason,160,true))),
|
|
135
|
+
'INVALID_CATALOG_PAGE','Invalid catalog installation permission.');
|
|
136
|
+
if(item.installMethods!==undefined){
|
|
137
|
+
requireCondition(Array.isArray(item.installMethods),'INVALID_CATALOG_PAGE','Invalid catalog installation methods.');
|
|
138
|
+
for(const method of item.installMethods){
|
|
139
|
+
requireCondition(own(method,['channel','kind','spec','command','allowBuild','sourceId','sourceRef','npmOwnership','target','targetUnavailableReason'])&&
|
|
140
|
+
['hanamesh','dsh','claude','codex'].includes(method.channel)&&['npm','github','agent'].includes(method.kind)&&
|
|
141
|
+
textWithin(method.spec,4096,true)&&(method.command===null||textWithin(method.command,4096,true))&&
|
|
142
|
+
textWithin(method.sourceId,120,true)&&textWithin(method.sourceRef,512,true)&&Array.isArray(method.allowBuild)&&
|
|
143
|
+
method.allowBuild.every(entry=>textWithin(entry,2048,true)),
|
|
144
|
+
'INVALID_CATALOG_PAGE','Invalid catalog installation method.');
|
|
145
|
+
if(method.targetUnavailableReason!==undefined)requireCondition(textWithin(method.targetUnavailableReason,160,true),'INVALID_CATALOG_PAGE','Invalid unavailable installation target reason.');
|
|
146
|
+
if(method.target!==undefined){try{packageInstallTarget(method.target);}catch{throw new AppHostError('INVALID_CATALOG_PAGE','Invalid fixed catalog package target.');}}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
132
149
|
}
|
|
133
150
|
|
|
134
151
|
function validateItem(item){
|
|
@@ -156,11 +173,11 @@ export function validateProviderPage(value,{maxItems=200,maxSourceLinks=MAX_SOUR
|
|
|
156
173
|
return structuredClone(value);
|
|
157
174
|
}
|
|
158
175
|
|
|
159
|
-
export function isApplication(item){return Boolean(item?.categories?.includes('hanamesh-app')&&item?.package?.registry==='npm');}
|
|
176
|
+
export function isApplication(item){return Boolean(item?.categories?.includes('hanamesh-app')&&(item?.package?.registry==='npm'||item?.installTarget));}
|
|
160
177
|
/** rc.28 market: an npm-backed entry that is not a HanaMesh application installs as a plain DSH plugin (`dsh plugin add`). */
|
|
161
|
-
export function isPlugin(item){return Boolean(item?.package?.registry==='npm'&&!isApplication(item));}
|
|
178
|
+
export function isPlugin(item){return Boolean((item?.package?.registry==='npm'||item?.installTarget)&&!isApplication(item));}
|
|
162
179
|
/** `application` | `plugin` | `listing` (repository-only / no npm package: browse only). */
|
|
163
|
-
export function itemKind(item){return isApplication(item)?'application':isPlugin(item)?'plugin':'listing';}
|
|
180
|
+
export function itemKind(item){return !catalogInstallationAllowed(item)?'listing':isApplication(item)?'application':isPlugin(item)?'plugin':'listing';}
|
|
164
181
|
|
|
165
182
|
/** Exact semver compare without a dependency: numeric core, then a prerelease sorts below its release; unparsable → 0. */
|
|
166
183
|
export function compareVersions(a,b){
|
|
@@ -172,8 +189,8 @@ export function compareVersions(a,b){
|
|
|
172
189
|
if(np&&nq){if(+p!==+q)return +p<+q?-1:1;}else if(np!==nq)return np?-1:1;else if(p!==q)return p<q?-1:1;}
|
|
173
190
|
return 0;
|
|
174
191
|
}
|
|
175
|
-
/**
|
|
176
|
-
export function upgradeAvailable(
|
|
192
|
+
/** The reviewed target version is strictly newer than the installed version. */
|
|
193
|
+
export function upgradeAvailable(targetVersion,installedVersion){return compareVersions(targetVersion,installedVersion)>0;}
|
|
177
194
|
|
|
178
195
|
async function responseJson(response,label,maxBytes=MAX_BYTES,providerErrors=false){
|
|
179
196
|
if(!providerErrors)requireCondition(response.ok,'CATALOG_HTTP_ERROR',`${label} returned HTTP ${response.status}.`,{status:response.status},502);
|