@yugoo-design/dsh-design-studio 0.1.2 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -89
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/lib/index.d.ts +3 -349
- package/lib/index.js +4 -886
- package/lib/instance-changes-CFLBPBdi.d.ts +280 -0
- package/lib/instance-changes-RvOGeBaE.js +691 -0
- package/lib/node.d.ts +437 -0
- package/lib/node.js +2270 -0
- package/licenses/dsh-distribution.json +160 -70
- package/package.json +18 -6
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { c as prepareStudioPanelPreview, i as exportStudioSourceArchive, l as loadStudioKit, n as normalizeStudioInstanceChanges, o as readStudioContentArchive, r as exportStudioContentArchive, t as STUDIO_INSTANCE_CHANGE_LIMITS } from "./instance-changes-RvOGeBaE.js";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { diagnoseKnowledgeDeclarations } from "@yugoo-design/dsh-design-core";
|
|
4
4
|
//#region src/types.ts
|
|
5
5
|
/**
|
|
6
6
|
* Thrown by a Bundle source port when a file exists but is not decodable
|
|
@@ -37,19 +37,6 @@ function snapshotKit(kit) {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
//#endregion
|
|
40
|
-
//#region src/load.ts
|
|
41
|
-
/** Studio-owned loading of a Kit from its directory source. */
|
|
42
|
-
async function loadStudioKit(fs, kitRoot, options = {}) {
|
|
43
|
-
const root = resolve(kitRoot);
|
|
44
|
-
return await loadDesignKit(fs, root, {
|
|
45
|
-
sourceId: options.sourceId ?? `directory:${root}`,
|
|
46
|
-
...options.bundles ? { resolveBundle: async (pin) => {
|
|
47
|
-
const location = options.bundles.find((entry) => entry.name === pin.name && entry.version === pin.version);
|
|
48
|
-
return location ? loadDesignBundle(fs, location, pin) : void 0;
|
|
49
|
-
} } : {}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
//#endregion
|
|
53
40
|
//#region src/validate.ts
|
|
54
41
|
/**
|
|
55
42
|
* Explicit Kit/Bundle definition validation for the Studio editing experience.
|
|
@@ -153,875 +140,6 @@ async function validateBundle(fs, kitRoot, name) {
|
|
|
153
140
|
}
|
|
154
141
|
}
|
|
155
142
|
//#endregion
|
|
156
|
-
//#region src/panel-preview.ts
|
|
157
|
-
/** Definition-only Panel preview preparation; never writes or runs a Bundle. */
|
|
158
|
-
function failure(code, message) {
|
|
159
|
-
return { diagnostics: [{
|
|
160
|
-
code,
|
|
161
|
-
message
|
|
162
|
-
}] };
|
|
163
|
-
}
|
|
164
|
-
/** Shared finalization over a real Core-compiled Bundle, with optional real Kit context. */
|
|
165
|
-
function finalizeStudioPanelPreview(input) {
|
|
166
|
-
const { compiled, revision, sampleSource } = input;
|
|
167
|
-
const bundle = { ...input.bundle }, kit = input.kit === void 0 ? void 0 : { ...input.kit };
|
|
168
|
-
if (!Number.isSafeInteger(revision) || revision < 0 || typeof sampleSource !== "string") return failure("preview.request", "Select an exact Bundle, text sample JSON and a non-negative editor revision.");
|
|
169
|
-
if ((compiled.catalogName ?? compiled.name) !== bundle.name || String(compiled.version) !== bundle.version) return failure("preview.bundle_pin", "The source does not match the selected Bundle version.");
|
|
170
|
-
const panelYaml = compiled.metadata?.panelYaml;
|
|
171
|
-
if (typeof panelYaml !== "string") return failure("preview.not_declarative", "Only an existing declarative Panel can be previewed.");
|
|
172
|
-
const schema = compiled.metadata?.schemaDocument;
|
|
173
|
-
if (schema === void 0) return failure("preview.definition", "The loader did not produce a declarative Panel and schema.");
|
|
174
|
-
if (new TextEncoder().encode(sampleSource).length > 262144) return failure("preview.size", "Sample JSON is limited to 256 KiB.");
|
|
175
|
-
let sample;
|
|
176
|
-
try {
|
|
177
|
-
sample = JSON.parse(sampleSource);
|
|
178
|
-
} catch {
|
|
179
|
-
return failure("preview.sample_json", "The preview sample must be valid JSON.");
|
|
180
|
-
}
|
|
181
|
-
if (sample === null || typeof sample !== "object" || Array.isArray(sample)) return failure("preview.sample_json", "The preview sample must be a JSON object.");
|
|
182
|
-
try {
|
|
183
|
-
const errors = compileSchema(schema)(sample);
|
|
184
|
-
if (errors.length > 0) return { diagnostics: errors.map((message) => ({
|
|
185
|
-
code: "preview.sample_schema",
|
|
186
|
-
message
|
|
187
|
-
})) };
|
|
188
|
-
return {
|
|
189
|
-
preview: {
|
|
190
|
-
revision,
|
|
191
|
-
...kit === void 0 ? {} : { kit },
|
|
192
|
-
bundle,
|
|
193
|
-
panelYaml,
|
|
194
|
-
sample
|
|
195
|
-
},
|
|
196
|
-
diagnostics: []
|
|
197
|
-
};
|
|
198
|
-
} catch (error) {
|
|
199
|
-
return failure("preview.definition", error instanceof Error ? error.message : String(error));
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Compile one Panel edit against an exact, host-resolved Kit snapshot.
|
|
204
|
-
*
|
|
205
|
-
* The host must supply an immutable/read-confined source port and root for the
|
|
206
|
-
* selected pin, not arbitrary browser paths or a mutable production project.
|
|
207
|
-
* Only panel.yaml is overlaid. All schemas, methods, locales and renderer
|
|
208
|
-
* contracts still come from that snapshot and pass through the runtime loader.
|
|
209
|
-
* The returned sample is fresh JSON and can be edited in a sandboxed preview;
|
|
210
|
-
* this function grants no artifact, filesystem, model or publication authority.
|
|
211
|
-
* A preview consumer must discard a result if its editor revision has changed.
|
|
212
|
-
*/
|
|
213
|
-
async function prepareStudioPanelPreview(source, kitRoot, request, loadOptions = {}) {
|
|
214
|
-
const { panelSource, sampleSource, revision } = request;
|
|
215
|
-
const kitPin = { ...request.kit };
|
|
216
|
-
const bundlePin = { ...request.bundle };
|
|
217
|
-
if (!isAbsolute(kitRoot) || !/^[a-z][a-z0-9_]*$/.test(bundlePin.name) || !Number.isSafeInteger(revision) || revision < 0 || typeof panelSource !== "string" || typeof sampleSource !== "string") return failure("preview.request", "Select an exact Bundle and a non-negative editor revision.");
|
|
218
|
-
if (new TextEncoder().encode(panelSource).length > 1048576 || new TextEncoder().encode(sampleSource).length > 262144) return failure("preview.size", "Panel source is limited to 1 MiB and sample JSON to 256 KiB.");
|
|
219
|
-
let sample;
|
|
220
|
-
try {
|
|
221
|
-
sample = JSON.parse(sampleSource);
|
|
222
|
-
} catch {
|
|
223
|
-
return failure("preview.sample_json", "The preview sample must be valid JSON.");
|
|
224
|
-
}
|
|
225
|
-
if (sample === null || typeof sample !== "object" || Array.isArray(sample)) return failure("preview.sample_json", "The preview sample must be a JSON object.");
|
|
226
|
-
try {
|
|
227
|
-
const base = await loadStudioKit(source, kitRoot, loadOptions);
|
|
228
|
-
if (base.name !== kitPin.name || String(base.version) !== kitPin.version) return failure("preview.kit_pin", "The source does not match the selected Kit version.");
|
|
229
|
-
const bundle = base.skills.find((skill) => (skill.catalogName ?? skill.name) === bundlePin.name);
|
|
230
|
-
if (bundle === void 0 || String(bundle.version) !== bundlePin.version) return failure("preview.bundle_pin", "The source does not match the selected Bundle version.");
|
|
231
|
-
if (typeof bundle.metadata?.panelYaml !== "string") return failure("preview.not_declarative", "Only an existing declarative Panel can be edited in this preview.");
|
|
232
|
-
const panelPath = resolve(bundle.resourceRoot ?? resolve(kitRoot, "skills", bundle.name), "panel.yaml");
|
|
233
|
-
const compiled = (await loadStudioKit({
|
|
234
|
-
readText: (path) => resolve(path) === panelPath ? Promise.resolve(panelSource) : source.readText(path),
|
|
235
|
-
...source.readResource ? { readResource: (path) => resolve(path) === panelPath ? Promise.resolve({
|
|
236
|
-
kind: "text",
|
|
237
|
-
content: panelSource
|
|
238
|
-
}) : source.readResource(path) } : {},
|
|
239
|
-
listDir: (path) => source.listDir(path),
|
|
240
|
-
...source.isDirectory ? { isDirectory: (path) => source.isDirectory(path) } : {},
|
|
241
|
-
...source.realPath ? { realPath: (path) => source.realPath(path) } : {}
|
|
242
|
-
}, kitRoot, loadOptions)).skills.find((skill) => (skill.catalogName ?? skill.name) === bundlePin.name);
|
|
243
|
-
if (compiled === void 0) return failure("preview.bundle_pin", "The source does not match the selected Bundle version.");
|
|
244
|
-
return finalizeStudioPanelPreview({
|
|
245
|
-
compiled,
|
|
246
|
-
revision,
|
|
247
|
-
kit: kitPin,
|
|
248
|
-
bundle: bundlePin,
|
|
249
|
-
sampleSource
|
|
250
|
-
});
|
|
251
|
-
} catch (error) {
|
|
252
|
-
return failure("preview.definition", error instanceof Error ? error.message : String(error));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
//#endregion
|
|
256
|
-
//#region src/panel-drafts.ts
|
|
257
|
-
/** Host-neutral, versioned Panel working copies; not Kit publication. */
|
|
258
|
-
function checkText$1(panelSource, sampleSource) {
|
|
259
|
-
if (typeof panelSource !== "string" || typeof sampleSource !== "string" || new TextEncoder().encode(panelSource).length > 1048576 || new TextEncoder().encode(sampleSource).length > 262144) throw new Error("Panel source must be text ≤1 MiB; sample JSON text ≤256 KiB.");
|
|
260
|
-
}
|
|
261
|
-
function checkRevision$1(record, expected) {
|
|
262
|
-
if (!Number.isSafeInteger(expected) || record.revision !== expected) throw new Error("Draft changed elsewhere. Reopen the latest revision before saving.");
|
|
263
|
-
}
|
|
264
|
-
function content$1(record) {
|
|
265
|
-
const { id: _id, revision: _revision, ...value } = record;
|
|
266
|
-
return value;
|
|
267
|
-
}
|
|
268
|
-
/** Shared semantics used by host adapters; no filesystem or runtime tool access. */
|
|
269
|
-
var StudioPanelDraftService = class {
|
|
270
|
-
repository;
|
|
271
|
-
sources;
|
|
272
|
-
constructor(repository, sources) {
|
|
273
|
-
this.repository = repository;
|
|
274
|
-
this.sources = sources;
|
|
275
|
-
}
|
|
276
|
-
async list() {
|
|
277
|
-
return (await this.repository.list()).map(({ id, revision, kit, bundle, confirmed }) => ({
|
|
278
|
-
id,
|
|
279
|
-
revision,
|
|
280
|
-
kit,
|
|
281
|
-
bundle,
|
|
282
|
-
confirmed
|
|
283
|
-
}));
|
|
284
|
-
}
|
|
285
|
-
read(id) {
|
|
286
|
-
return this.repository.read(id);
|
|
287
|
-
}
|
|
288
|
-
async create(input) {
|
|
289
|
-
const kitPin = { ...input.kit }, bundlePin = { ...input.bundle };
|
|
290
|
-
if (!/^[a-z][a-z0-9_]*$/.test(bundlePin.name)) throw new Error("Select an exact Bundle identity.");
|
|
291
|
-
const sampleSource = input.sampleSource;
|
|
292
|
-
const snapshot = await this.sources.capture(kitPin);
|
|
293
|
-
return this.createRetained(snapshot, kitPin, bundlePin, sampleSource);
|
|
294
|
-
}
|
|
295
|
-
/** Open only the retained snapshot; imported/template pins need not be installed. */
|
|
296
|
-
async createFromSnapshot(input) {
|
|
297
|
-
const kit = { ...input.kit }, bundle = { ...input.bundle }, sampleSource = input.sampleSource;
|
|
298
|
-
if (!/^[a-z][a-z0-9_]*$/.test(bundle.name)) throw new Error("Select an exact Bundle identity.");
|
|
299
|
-
return this.createRetained(await this.sources.open(input.snapshotId), kit, bundle, sampleSource);
|
|
300
|
-
}
|
|
301
|
-
async createRetained(snapshot, kitPin, bundlePin, sampleSource) {
|
|
302
|
-
const kit = await loadStudioKit(snapshot.fs, snapshot.root, snapshot.loadOptions);
|
|
303
|
-
if (kit.name !== kitPin.name || String(kit.version) !== kitPin.version) throw new Error("Source does not match selected Kit pin.");
|
|
304
|
-
const bundle = kit.skills.find((entry) => entry.name === bundlePin.name && String(entry.version) === bundlePin.version);
|
|
305
|
-
if (!bundle || typeof bundle.metadata?.panelYaml !== "string") throw new Error("Select an installed declarative Bundle version.");
|
|
306
|
-
const panelSource = await snapshot.fs.readText(join(bundle.resourceRoot ?? join(snapshot.root, "skills", bundlePin.name), "panel.yaml"));
|
|
307
|
-
if (panelSource === void 0) throw new Error("Panel source is missing.");
|
|
308
|
-
const panel = bundle.metadata.panelManifest;
|
|
309
|
-
const sample = sampleSource ?? JSON.stringify(panel.initial_values ?? {}, null, 2);
|
|
310
|
-
checkText$1(panelSource, sample);
|
|
311
|
-
return this.repository.create({
|
|
312
|
-
kit: kitPin,
|
|
313
|
-
bundle: bundlePin,
|
|
314
|
-
baseSnapshot: snapshot.id,
|
|
315
|
-
panelSource,
|
|
316
|
-
sampleSource: sample,
|
|
317
|
-
confirmed: false
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
/** Saving incomplete source is allowed; confirmation requires successful compilation. */
|
|
321
|
-
async save(input) {
|
|
322
|
-
const { id, expectedRevision, panelSource, sampleSource } = input;
|
|
323
|
-
checkText$1(panelSource, sampleSource);
|
|
324
|
-
const current = await this.repository.read(id);
|
|
325
|
-
checkRevision$1(current, expectedRevision);
|
|
326
|
-
if (current.confirmed) throw new Error("Confirmed snapshot is locked. Create a new draft to edit it.");
|
|
327
|
-
return this.repository.replace(id, expectedRevision, {
|
|
328
|
-
...content$1(current),
|
|
329
|
-
panelSource,
|
|
330
|
-
sampleSource
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
/** Compile an unsaved editor revision without persisting or touching any project. */
|
|
334
|
-
async preview(input) {
|
|
335
|
-
const { id, expectedRevision, panelSource, sampleSource, revision } = input;
|
|
336
|
-
checkText$1(panelSource, sampleSource);
|
|
337
|
-
const current = await this.repository.read(id);
|
|
338
|
-
checkRevision$1(current, expectedRevision);
|
|
339
|
-
const snapshot = await this.sources.open(current.baseSnapshot);
|
|
340
|
-
return prepareStudioPanelPreview(snapshot.fs, snapshot.root, {
|
|
341
|
-
kit: current.kit,
|
|
342
|
-
bundle: current.bundle,
|
|
343
|
-
revision,
|
|
344
|
-
panelSource,
|
|
345
|
-
sampleSource
|
|
346
|
-
}, snapshot.loadOptions);
|
|
347
|
-
}
|
|
348
|
-
async confirm(input) {
|
|
349
|
-
const { id, expectedRevision } = input;
|
|
350
|
-
const current = await this.repository.read(id);
|
|
351
|
-
checkRevision$1(current, expectedRevision);
|
|
352
|
-
const result = await this.preview({
|
|
353
|
-
...current,
|
|
354
|
-
expectedRevision
|
|
355
|
-
});
|
|
356
|
-
if (!result.preview || result.diagnostics.length) return result;
|
|
357
|
-
const draft = current.confirmed ? current : await this.repository.replace(id, expectedRevision, {
|
|
358
|
-
...content$1(current),
|
|
359
|
-
confirmed: true
|
|
360
|
-
});
|
|
361
|
-
return {
|
|
362
|
-
...result,
|
|
363
|
-
draft
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
/** Start another editable revision, sharing the immutable base bytes. */
|
|
367
|
-
async fork(id) {
|
|
368
|
-
const current = await this.repository.read(id);
|
|
369
|
-
return this.repository.create({
|
|
370
|
-
...content$1(current),
|
|
371
|
-
confirmed: false
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
//#endregion
|
|
376
|
-
//#region src/kit-drafts.ts
|
|
377
|
-
/** Kit-owned working-copy edits over retained source; never catalog publication. */
|
|
378
|
-
function checkText(mindset, relations) {
|
|
379
|
-
if (typeof mindset !== "string" || relations !== null && typeof relations !== "string" || new TextEncoder().encode(mindset).length > 1048576 || relations !== null && new TextEncoder().encode(relations).length > 1048576) throw new Error("Kit-owned sources must be UTF-8 text of at most 1 MiB each.");
|
|
380
|
-
}
|
|
381
|
-
function checkRevision(current, expected) {
|
|
382
|
-
if (!Number.isSafeInteger(expected) || current.revision !== expected) throw new Error("Draft changed elsewhere. Reopen the latest revision.");
|
|
383
|
-
}
|
|
384
|
-
function content(record) {
|
|
385
|
-
const { id: _id, revision: _revision, ...value } = record;
|
|
386
|
-
return structuredClone(value);
|
|
387
|
-
}
|
|
388
|
-
function samePins(a, b) {
|
|
389
|
-
const keys = (pins) => pins.map((pin) => `${pin.name}@${pin.version}`).sort();
|
|
390
|
-
return JSON.stringify(keys(a)) === JSON.stringify(keys(b));
|
|
391
|
-
}
|
|
392
|
-
/** Only two Kit-owned files are overlaid; no user-supplied paths or live FS fallback. */
|
|
393
|
-
function overlay(snapshot, record) {
|
|
394
|
-
const root = resolve(snapshot.root);
|
|
395
|
-
const replacements = /* @__PURE__ */ new Map([[resolve(root, "mindset.md"), record.mindsetSource], [resolve(root, "relations.yaml"), record.relationsSource]]);
|
|
396
|
-
return {
|
|
397
|
-
readText: (path) => replacements.has(resolve(path)) ? Promise.resolve(replacements.get(resolve(path)) ?? void 0) : snapshot.fs.readText(path),
|
|
398
|
-
...snapshot.fs.readResource ? { readResource: async (path) => {
|
|
399
|
-
if (!replacements.has(resolve(path))) return snapshot.fs.readResource(path);
|
|
400
|
-
const content = replacements.get(resolve(path));
|
|
401
|
-
return content == null ? void 0 : {
|
|
402
|
-
kind: "text",
|
|
403
|
-
content
|
|
404
|
-
};
|
|
405
|
-
} } : {},
|
|
406
|
-
async listDir(path) {
|
|
407
|
-
if (resolve(path) !== root) return snapshot.fs.listDir(path);
|
|
408
|
-
const entries = new Set(await snapshot.fs.listDir(path));
|
|
409
|
-
entries.add("mindset.md");
|
|
410
|
-
if (record.relationsSource === null) entries.delete("relations.yaml");
|
|
411
|
-
else entries.add("relations.yaml");
|
|
412
|
-
return [...entries].sort();
|
|
413
|
-
},
|
|
414
|
-
...snapshot.fs.isDirectory ? { isDirectory: (path) => replacements.has(resolve(path)) ? Promise.resolve(false) : snapshot.fs.isDirectory(path) } : {},
|
|
415
|
-
...snapshot.fs.realPath ? { realPath: (path) => replacements.has(resolve(path)) ? Promise.resolve(resolve(path)) : snapshot.fs.realPath(path) } : {}
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
var StudioKitDraftService = class {
|
|
419
|
-
repository;
|
|
420
|
-
sources;
|
|
421
|
-
constructor(repository, sources) {
|
|
422
|
-
this.repository = repository;
|
|
423
|
-
this.sources = sources;
|
|
424
|
-
}
|
|
425
|
-
async list() {
|
|
426
|
-
return (await this.repository.list()).map(({ id, revision, baseKit, confirmed }) => ({
|
|
427
|
-
id,
|
|
428
|
-
revision,
|
|
429
|
-
baseKit: { ...baseKit },
|
|
430
|
-
confirmed
|
|
431
|
-
}));
|
|
432
|
-
}
|
|
433
|
-
async read(id) {
|
|
434
|
-
return structuredClone(await this.repository.read(id));
|
|
435
|
-
}
|
|
436
|
-
async create(input) {
|
|
437
|
-
const pin = { ...input.kit };
|
|
438
|
-
const snapshot = await this.sources.capture(pin);
|
|
439
|
-
return this.createRetained(snapshot, pin);
|
|
440
|
-
}
|
|
441
|
-
/** Adopt retained, independently validated content; never resolve an installed pin. */
|
|
442
|
-
async createFromSnapshot(input) {
|
|
443
|
-
const pin = { ...input.kit }, snapshotId = input.snapshotId;
|
|
444
|
-
return this.createRetained(await this.sources.open(snapshotId), pin);
|
|
445
|
-
}
|
|
446
|
-
async createRetained(snapshot, pin) {
|
|
447
|
-
const kit = await loadStudioKit(snapshot.fs, snapshot.root, snapshot.loadOptions);
|
|
448
|
-
if (kit.name !== pin.name || String(kit.version) !== pin.version) throw new Error("Source does not match the selected Kit pin.");
|
|
449
|
-
const mindsetSource = await snapshot.fs.readText(resolve(snapshot.root, "mindset.md"));
|
|
450
|
-
if (mindsetSource === void 0) throw new Error("Kit Mindset source is missing.");
|
|
451
|
-
const relationsSource = await snapshot.fs.readText(resolve(snapshot.root, "relations.yaml")) ?? null;
|
|
452
|
-
checkText(mindsetSource, relationsSource);
|
|
453
|
-
return this.repository.create({
|
|
454
|
-
baseKit: pin,
|
|
455
|
-
baseSnapshot: snapshot.id,
|
|
456
|
-
mounts: kit.skills.map((bundle) => ({
|
|
457
|
-
name: bundle.name,
|
|
458
|
-
version: String(bundle.version)
|
|
459
|
-
})),
|
|
460
|
-
mindsetSource,
|
|
461
|
-
relationsSource,
|
|
462
|
-
confirmed: false
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
/** Invalid intermediate edits can be retained, but cannot be confirmed. */
|
|
466
|
-
async save(input) {
|
|
467
|
-
const { id, expectedRevision, mindsetSource, relationsSource } = input;
|
|
468
|
-
checkText(mindsetSource, relationsSource);
|
|
469
|
-
const current = await this.read(id);
|
|
470
|
-
checkRevision(current, expectedRevision);
|
|
471
|
-
if (current.confirmed) throw new Error("Confirmed Kit draft is locked. Fork it to continue editing.");
|
|
472
|
-
return this.repository.replace(id, expectedRevision, {
|
|
473
|
-
...content(current),
|
|
474
|
-
mindsetSource,
|
|
475
|
-
relationsSource
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
async compile(current) {
|
|
479
|
-
checkText(current.mindsetSource, current.relationsSource);
|
|
480
|
-
const snapshot = await this.sources.open(current.baseSnapshot);
|
|
481
|
-
const base = await loadStudioKit(snapshot.fs, snapshot.root, snapshot.loadOptions);
|
|
482
|
-
if (base.name !== current.baseKit.name || String(base.version) !== current.baseKit.version || !samePins(current.mounts, base.skills.map((bundle) => ({
|
|
483
|
-
name: bundle.name,
|
|
484
|
-
version: String(bundle.version)
|
|
485
|
-
})))) throw new Error("Retained source does not match the working-copy base and mount pins.");
|
|
486
|
-
const fs = overlay(snapshot, current);
|
|
487
|
-
const candidate = await loadStudioKit(fs, snapshot.root, snapshot.loadOptions);
|
|
488
|
-
if (!samePins(current.mounts, candidate.skills.map((bundle) => ({
|
|
489
|
-
name: bundle.name,
|
|
490
|
-
version: String(bundle.version)
|
|
491
|
-
})))) throw new Error("Bundle mounts are read-only in this editor. Use an explicit catalog mount operation.");
|
|
492
|
-
return {
|
|
493
|
-
snapshot,
|
|
494
|
-
fs,
|
|
495
|
-
candidate
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
async validate(input) {
|
|
499
|
-
const { id, expectedRevision } = input;
|
|
500
|
-
const current = await this.read(id);
|
|
501
|
-
checkRevision(current, expectedRevision);
|
|
502
|
-
try {
|
|
503
|
-
const { candidate } = await this.compile(current);
|
|
504
|
-
return {
|
|
505
|
-
valid: true,
|
|
506
|
-
diagnostics: knowledgeDeclarationWarnings(candidate.skills),
|
|
507
|
-
revision: current.revision,
|
|
508
|
-
candidateKit: {
|
|
509
|
-
name: candidate.name,
|
|
510
|
-
version: String(candidate.version)
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
} catch (error) {
|
|
514
|
-
return {
|
|
515
|
-
valid: false,
|
|
516
|
-
revision: current.revision,
|
|
517
|
-
diagnostics: [{
|
|
518
|
-
code: "kit_draft.definition",
|
|
519
|
-
message: error instanceof Error ? error.message : String(error)
|
|
520
|
-
}]
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
async preview(input) {
|
|
525
|
-
const { id, expectedRevision, sampleSource } = input;
|
|
526
|
-
const bundle = { ...input.bundle };
|
|
527
|
-
const current = await this.read(id);
|
|
528
|
-
checkRevision(current, expectedRevision);
|
|
529
|
-
try {
|
|
530
|
-
const { fs, snapshot, candidate } = await this.compile(current);
|
|
531
|
-
if (!current.mounts.some((pin) => pin.name === bundle.name && pin.version === bundle.version)) throw new Error("Select an exact mounted Bundle.");
|
|
532
|
-
const mounted = candidate.skills.find((skill) => skill.name === bundle.name && String(skill.version) === bundle.version);
|
|
533
|
-
const panelSource = await fs.readText(resolve(mounted?.resourceRoot ?? resolve(snapshot.root, "skills", bundle.name), "panel.yaml"));
|
|
534
|
-
if (panelSource === void 0) throw new Error("This Bundle has no declarative Panel preview.");
|
|
535
|
-
return prepareStudioPanelPreview(fs, snapshot.root, {
|
|
536
|
-
kit: {
|
|
537
|
-
name: candidate.name,
|
|
538
|
-
version: String(candidate.version)
|
|
539
|
-
},
|
|
540
|
-
bundle,
|
|
541
|
-
revision: current.revision,
|
|
542
|
-
panelSource,
|
|
543
|
-
sampleSource
|
|
544
|
-
}, snapshot.loadOptions);
|
|
545
|
-
} catch (error) {
|
|
546
|
-
return { diagnostics: [{
|
|
547
|
-
code: "kit_draft.preview",
|
|
548
|
-
message: error instanceof Error ? error.message : String(error)
|
|
549
|
-
}] };
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
async confirm(input) {
|
|
553
|
-
const { id, expectedRevision } = input;
|
|
554
|
-
const current = await this.read(id);
|
|
555
|
-
checkRevision(current, expectedRevision);
|
|
556
|
-
const result = await this.validate({
|
|
557
|
-
id,
|
|
558
|
-
expectedRevision
|
|
559
|
-
});
|
|
560
|
-
if (!result.valid) return result;
|
|
561
|
-
const draft = current.confirmed ? current : await this.repository.replace(id, expectedRevision, {
|
|
562
|
-
...content(current),
|
|
563
|
-
confirmed: true
|
|
564
|
-
});
|
|
565
|
-
return {
|
|
566
|
-
...result,
|
|
567
|
-
draft
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
async fork(id) {
|
|
571
|
-
return this.repository.create({
|
|
572
|
-
...content(await this.read(id)),
|
|
573
|
-
confirmed: false
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
//#endregion
|
|
578
|
-
//#region src/content-archive.ts
|
|
579
|
-
/** Portable regular-file content, not a plugin installer or publication format. */
|
|
580
|
-
const MAX_BYTES = 16777216;
|
|
581
|
-
const MAX_JSON = 33554432;
|
|
582
|
-
const MAX_FILES = 4096;
|
|
583
|
-
const HASH = /^[0-9a-f]{64}$/;
|
|
584
|
-
const sha = (value) => createHash("sha256").update(value).digest("hex");
|
|
585
|
-
const OPAQUE_RESOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
586
|
-
".bin",
|
|
587
|
-
".png",
|
|
588
|
-
".jpg",
|
|
589
|
-
".jpeg",
|
|
590
|
-
".gif",
|
|
591
|
-
".webp",
|
|
592
|
-
".avif",
|
|
593
|
-
".ico",
|
|
594
|
-
".bmp",
|
|
595
|
-
".pdf",
|
|
596
|
-
".woff",
|
|
597
|
-
".woff2",
|
|
598
|
-
".ttf",
|
|
599
|
-
".otf",
|
|
600
|
-
".eot",
|
|
601
|
-
".wasm"
|
|
602
|
-
]);
|
|
603
|
-
/** Shared by archive capture and virtual import; full bytes and directory entries remain intact. */
|
|
604
|
-
function readStudioArchiveResource(path, bytes) {
|
|
605
|
-
return OPAQUE_RESOURCE_EXTENSIONS.has(extname(path).toLowerCase()) ? {
|
|
606
|
-
kind: "opaque",
|
|
607
|
-
byteLength: bytes.byteLength
|
|
608
|
-
} : {
|
|
609
|
-
kind: "text",
|
|
610
|
-
content: new TextDecoder("utf-8", { fatal: true }).decode(bytes)
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
function object(value) {
|
|
614
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw Error("Invalid archive object.");
|
|
615
|
-
return value;
|
|
616
|
-
}
|
|
617
|
-
function keys(value, allowed) {
|
|
618
|
-
if (Object.keys(value).length !== allowed.length || allowed.some((key) => !Object.hasOwn(value, key))) throw Error("Unknown or missing archive fields.");
|
|
619
|
-
}
|
|
620
|
-
function text(value, limit) {
|
|
621
|
-
if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value) > limit || /[\u0000-\u001f\u007f]/u.test(value)) throw Error("Invalid archive metadata text.");
|
|
622
|
-
return value;
|
|
623
|
-
}
|
|
624
|
-
function pin(value, kind) {
|
|
625
|
-
const data = object(value);
|
|
626
|
-
keys(data, ["name", "version"]);
|
|
627
|
-
const name = text(data.name, 128), version = text(data.version, 32);
|
|
628
|
-
if (!/^[a-z][a-z0-9_]*$/.test(name)) throw Error("Unsupported archive content identity.");
|
|
629
|
-
safePath(name);
|
|
630
|
-
safePath(version);
|
|
631
|
-
if (kind === "kit") DesignKitVersion(version);
|
|
632
|
-
else DesignBundleVersion(version);
|
|
633
|
-
return Object.freeze({
|
|
634
|
-
name,
|
|
635
|
-
version
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
function safePath(value) {
|
|
639
|
-
const path = text(value, 1024);
|
|
640
|
-
if (path !== path.normalize("NFC") || /[\\:*?"<>|]/u.test(path)) throw Error("Unsafe archive path.");
|
|
641
|
-
if (/\.(exe|dll|node|so|dylib|com|msi|sys)$/iu.test(path)) throw Error("Native executable payload propagation is unsupported.");
|
|
642
|
-
for (const part of path.split("/")) if (!part || part === "." || part === ".." || [".git", "node_modules"].includes(part.toLowerCase()) || /[. ]$/u.test(part) || /^(con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/iu.test(part)) throw Error("Unsafe archive path.");
|
|
643
|
-
return path;
|
|
644
|
-
}
|
|
645
|
-
function canonical(data) {
|
|
646
|
-
return JSON.stringify(data);
|
|
647
|
-
}
|
|
648
|
-
/** Parse a bounded JSON envelope; hashes include metadata and every original byte. */
|
|
649
|
-
function readStudioContentArchive(source) {
|
|
650
|
-
if (typeof source !== "string" || Buffer.byteLength(source) > MAX_JSON) throw Error("Archive JSON exceeds 32 MiB.");
|
|
651
|
-
const raw = object(JSON.parse(source));
|
|
652
|
-
keys(raw, [
|
|
653
|
-
"format",
|
|
654
|
-
"kind",
|
|
655
|
-
"pin",
|
|
656
|
-
"provenance",
|
|
657
|
-
"dependencies",
|
|
658
|
-
"files",
|
|
659
|
-
"sha256"
|
|
660
|
-
]);
|
|
661
|
-
if (raw.format !== "studio-content-archive/v1" || raw.kind !== "kit" && raw.kind !== "bundle") throw Error("Unsupported archive format.");
|
|
662
|
-
const identity = pin(raw.pin, raw.kind), provenance = object(raw.provenance);
|
|
663
|
-
keys(provenance, ["source", "license"]);
|
|
664
|
-
const origin = Object.freeze({
|
|
665
|
-
source: text(provenance.source, 2048),
|
|
666
|
-
license: text(provenance.license, 2048)
|
|
667
|
-
});
|
|
668
|
-
if (!Array.isArray(raw.dependencies) || raw.dependencies.length > MAX_FILES) throw Error("Invalid archive dependencies.");
|
|
669
|
-
const dependencies = raw.dependencies.map((value) => pin(value, "bundle"));
|
|
670
|
-
const names = /* @__PURE__ */ new Set();
|
|
671
|
-
for (const dependency of dependencies) {
|
|
672
|
-
if (names.has(dependency.name) || raw.kind === "bundle" && dependency.name === identity.name) throw Error("Duplicate or self archive dependency.");
|
|
673
|
-
names.add(dependency.name);
|
|
674
|
-
}
|
|
675
|
-
if (!Array.isArray(raw.files) || !raw.files.length || raw.files.length > MAX_FILES) throw Error("Invalid archive file count.");
|
|
676
|
-
let total = 0;
|
|
677
|
-
const paths = /* @__PURE__ */ new Map();
|
|
678
|
-
const prefixes = /* @__PURE__ */ new Map();
|
|
679
|
-
const directories = /* @__PURE__ */ new Set();
|
|
680
|
-
const files = raw.files.map((value) => {
|
|
681
|
-
const entry = object(value);
|
|
682
|
-
keys(entry, [
|
|
683
|
-
"path",
|
|
684
|
-
"base64",
|
|
685
|
-
"bytes",
|
|
686
|
-
"sha256"
|
|
687
|
-
]);
|
|
688
|
-
const path = safePath(entry.path), folded = path.toLowerCase();
|
|
689
|
-
const segments = path.split("/");
|
|
690
|
-
for (let index = 1; index <= segments.length; index++) {
|
|
691
|
-
const prefix = segments.slice(0, index).join("/"), foldedPrefix = prefix.toLowerCase();
|
|
692
|
-
if (prefixes.has(foldedPrefix) && prefixes.get(foldedPrefix) !== prefix) throw Error("Case-conflicting archive path segments.");
|
|
693
|
-
prefixes.set(foldedPrefix, prefix);
|
|
694
|
-
}
|
|
695
|
-
if (paths.has(folded)) throw Error("Duplicate or case-conflicting archive paths.");
|
|
696
|
-
paths.set(folded, path);
|
|
697
|
-
const parts = folded.split("/");
|
|
698
|
-
parts.pop();
|
|
699
|
-
while (parts.length) {
|
|
700
|
-
directories.add(parts.join("/"));
|
|
701
|
-
parts.pop();
|
|
702
|
-
}
|
|
703
|
-
if (!Number.isSafeInteger(entry.bytes) || entry.bytes < 0 || entry.bytes > MAX_BYTES - total) throw Error("Archive content exceeds 16 MiB or has an invalid byte count.");
|
|
704
|
-
if (typeof entry.base64 !== "string" || entry.base64.length > Math.ceil(MAX_BYTES / 3) * 4 || entry.base64.length % 4 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(entry.base64)) throw Error("Invalid archive base64.");
|
|
705
|
-
const bytes = Buffer.from(entry.base64, "base64");
|
|
706
|
-
if (bytes.toString("base64") !== entry.base64 || entry.bytes !== bytes.length || typeof entry.sha256 !== "string" || !HASH.test(entry.sha256) || sha(bytes) !== entry.sha256) throw Error("Archive file integrity mismatch.");
|
|
707
|
-
total += bytes.length;
|
|
708
|
-
if (total > MAX_BYTES) throw Error("Archive content exceeds 16 MiB.");
|
|
709
|
-
return Object.freeze({
|
|
710
|
-
path,
|
|
711
|
-
base64: entry.base64,
|
|
712
|
-
bytes: bytes.length,
|
|
713
|
-
sha256: entry.sha256
|
|
714
|
-
});
|
|
715
|
-
}).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
716
|
-
if ([...directories].some((path) => paths.has(path))) throw Error("Archive file/directory collision.");
|
|
717
|
-
const data = {
|
|
718
|
-
format: raw.format,
|
|
719
|
-
kind: raw.kind,
|
|
720
|
-
pin: identity,
|
|
721
|
-
provenance: origin,
|
|
722
|
-
dependencies: Object.freeze(dependencies),
|
|
723
|
-
files: Object.freeze(files)
|
|
724
|
-
};
|
|
725
|
-
if (typeof raw.sha256 !== "string" || !HASH.test(raw.sha256) || sha(canonical(data)) !== raw.sha256) throw Error("Archive envelope integrity mismatch.");
|
|
726
|
-
return Object.freeze({
|
|
727
|
-
...data,
|
|
728
|
-
sha256: raw.sha256
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
/** Pure virtual source: explicit text reads stay strict; attached byte assets remain opaque. */
|
|
732
|
-
function virtualSource(files, root) {
|
|
733
|
-
const map = new Map(files.map((file) => [file.path, file.base64]));
|
|
734
|
-
const key = (path) => {
|
|
735
|
-
const result = relative(root, resolve(path));
|
|
736
|
-
if (isAbsolute(result) || result === ".." || result.startsWith(`..${sep}`)) throw Error("Archive validation path escapes source.");
|
|
737
|
-
return result.split(sep).join("/");
|
|
738
|
-
};
|
|
739
|
-
const children = (path) => {
|
|
740
|
-
const prefix = key(path);
|
|
741
|
-
const head = prefix ? `${prefix}/` : "";
|
|
742
|
-
return [...new Set([...map.keys()].filter((file) => file.startsWith(head)).map((file) => file.slice(head.length).split("/")[0]))].sort();
|
|
743
|
-
};
|
|
744
|
-
return {
|
|
745
|
-
async readText(path) {
|
|
746
|
-
const content = map.get(key(path));
|
|
747
|
-
return content === void 0 ? void 0 : new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(content, "base64"));
|
|
748
|
-
},
|
|
749
|
-
async readResource(path) {
|
|
750
|
-
const content = map.get(key(path));
|
|
751
|
-
return content === void 0 ? void 0 : readStudioArchiveResource(path, Buffer.from(content, "base64"));
|
|
752
|
-
},
|
|
753
|
-
async listDir(path) {
|
|
754
|
-
return children(path);
|
|
755
|
-
},
|
|
756
|
-
async isDirectory(path) {
|
|
757
|
-
return children(path).length > 0;
|
|
758
|
-
},
|
|
759
|
-
async realPath(path) {
|
|
760
|
-
key(path);
|
|
761
|
-
return resolve(path);
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
const bundlePin = (skill) => ({
|
|
766
|
-
name: skill.catalogName ?? skill.name,
|
|
767
|
-
version: String(skill.version)
|
|
768
|
-
});
|
|
769
|
-
const samePin = (left, right) => left.name === right.name && left.version === right.version;
|
|
770
|
-
const bundlePrefix = (identity) => `bundles/${identity.name}/${identity.version}/`;
|
|
771
|
-
const within = (root, path) => {
|
|
772
|
-
const rel = relative(root, resolve(path));
|
|
773
|
-
return !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
774
|
-
};
|
|
775
|
-
/** All reads dispatch to captured bytes; there is no live filesystem fallback. */
|
|
776
|
-
function sourceView(snapshot) {
|
|
777
|
-
const bundles = snapshot.bundles ?? [];
|
|
778
|
-
const sources = [...bundles.map((bundle) => bundle.snapshot), snapshot].sort((a, b) => b.root.length - a.root.length);
|
|
779
|
-
const owner = (path) => {
|
|
780
|
-
const source = sources.find((source) => within(source.root, path));
|
|
781
|
-
if (!source) throw Error("Archive validation path escapes captured sources.");
|
|
782
|
-
return source;
|
|
783
|
-
};
|
|
784
|
-
return {
|
|
785
|
-
fs: {
|
|
786
|
-
readText: (path) => owner(path).fs.readText(path),
|
|
787
|
-
async readResource(path) {
|
|
788
|
-
const source = owner(path);
|
|
789
|
-
if (typeof source.readBytes === "function") return readStudioArchiveResource(path, await source.readBytes(path));
|
|
790
|
-
if (source.fs.readResource) return source.fs.readResource(path);
|
|
791
|
-
const content = await source.fs.readText(path);
|
|
792
|
-
return content === void 0 ? void 0 : {
|
|
793
|
-
kind: "text",
|
|
794
|
-
content
|
|
795
|
-
};
|
|
796
|
-
},
|
|
797
|
-
listDir: (path) => owner(path).fs.listDir(path),
|
|
798
|
-
async isDirectory(path) {
|
|
799
|
-
const source = owner(path);
|
|
800
|
-
return source.fs.isDirectory ? source.fs.isDirectory(path) : (await source.fs.listDir(path)).length > 0;
|
|
801
|
-
},
|
|
802
|
-
async realPath(path) {
|
|
803
|
-
const source = owner(path);
|
|
804
|
-
return source.fs.realPath ? source.fs.realPath(path) : resolve(path);
|
|
805
|
-
}
|
|
806
|
-
},
|
|
807
|
-
options: {
|
|
808
|
-
sourceId: snapshot.sourceId,
|
|
809
|
-
bundles: bundles.map((bundle) => ({
|
|
810
|
-
name: bundle.name,
|
|
811
|
-
version: bundle.version,
|
|
812
|
-
rootDir: bundle.snapshot.root,
|
|
813
|
-
sourceId: bundle.sourceId
|
|
814
|
-
}))
|
|
815
|
-
}
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
function archiveSnapshot(archive) {
|
|
819
|
-
const root = resolve("/studio-content-archive-virtual");
|
|
820
|
-
const sourceId = `archive:untrusted:${archive.sha256}`;
|
|
821
|
-
const independent = archive.kind === "kit" && archive.files.some((file) => file.path === "kit.yaml");
|
|
822
|
-
const filesAt = (files, root) => {
|
|
823
|
-
const fs = virtualSource(files, root);
|
|
824
|
-
return {
|
|
825
|
-
root,
|
|
826
|
-
sourceId,
|
|
827
|
-
fs,
|
|
828
|
-
async listFiles() {
|
|
829
|
-
return files.map((file) => file.path);
|
|
830
|
-
},
|
|
831
|
-
async readBytes(path) {
|
|
832
|
-
const file = files.find((file) => resolve(root, file.path) === resolve(path));
|
|
833
|
-
if (!file) throw Error("Missing archive source file.");
|
|
834
|
-
return Buffer.from(file.base64, "base64");
|
|
835
|
-
}
|
|
836
|
-
};
|
|
837
|
-
};
|
|
838
|
-
if (!independent) return filesAt(archive.files, root);
|
|
839
|
-
const bundleFiles = /* @__PURE__ */ new Set();
|
|
840
|
-
const bundles = archive.dependencies.map((identity) => {
|
|
841
|
-
const prefix = bundlePrefix(identity);
|
|
842
|
-
const files = archive.files.filter((file) => file.path.startsWith(prefix)).map((file) => {
|
|
843
|
-
bundleFiles.add(file.path);
|
|
844
|
-
return Object.freeze({
|
|
845
|
-
...file,
|
|
846
|
-
path: file.path.slice(prefix.length)
|
|
847
|
-
});
|
|
848
|
-
});
|
|
849
|
-
if (!files.length) throw Error(`Archive is missing exact Bundle content: ${identity.name}@${identity.version}`);
|
|
850
|
-
return {
|
|
851
|
-
...identity,
|
|
852
|
-
sourceId,
|
|
853
|
-
snapshot: filesAt(files, resolve(root, prefix))
|
|
854
|
-
};
|
|
855
|
-
});
|
|
856
|
-
if (archive.files.some((file) => file.path.startsWith("bundles/") && !bundleFiles.has(file.path))) throw Error("Archive contains unreferenced Bundle content.");
|
|
857
|
-
return {
|
|
858
|
-
...filesAt(archive.files.filter((file) => !bundleFiles.has(file.path)), root),
|
|
859
|
-
bundles
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
/** No writes, extraction, permission restoration, script execution, installation or publication. */
|
|
863
|
-
async function prepareStudioContentImport(source, options = {}) {
|
|
864
|
-
const archive = readStudioContentArchive(source);
|
|
865
|
-
let validation;
|
|
866
|
-
let contents = Object.freeze([]);
|
|
867
|
-
let requiresTrustReview = archive.files.some((file) => /\.(m?js|cjs|[cm]?tsx?|sh|ps1|cmd|bat|py|wasm)$/iu.test(file.path));
|
|
868
|
-
try {
|
|
869
|
-
const snapshot = archiveSnapshot(archive), view = sourceView(snapshot);
|
|
870
|
-
const selected = [];
|
|
871
|
-
if (archive.kind === "kit") {
|
|
872
|
-
const kit = await loadStudioKit(view.fs, snapshot.root, view.options);
|
|
873
|
-
selected.push(...kit.skills);
|
|
874
|
-
if (kit.name !== archive.pin.name || String(kit.version) !== archive.pin.version) throw Error("Archive Kit identity disagrees with source.");
|
|
875
|
-
if (JSON.stringify(kit.skills.map(bundlePin)) !== JSON.stringify(archive.dependencies)) throw Error("Archive Kit member pins disagree with source.");
|
|
876
|
-
if (snapshot.bundles) {
|
|
877
|
-
const kitFiles = archive.files.filter((file) => !file.path.startsWith("bundles/"));
|
|
878
|
-
contents = Object.freeze([Object.freeze({
|
|
879
|
-
kind: "kit",
|
|
880
|
-
pin: archive.pin,
|
|
881
|
-
files: Object.freeze(kitFiles)
|
|
882
|
-
}), ...snapshot.bundles.map((bundle) => Object.freeze({
|
|
883
|
-
kind: "bundle",
|
|
884
|
-
pin: Object.freeze({
|
|
885
|
-
name: bundle.name,
|
|
886
|
-
version: bundle.version
|
|
887
|
-
}),
|
|
888
|
-
files: Object.freeze(archive.files.filter((file) => file.path.startsWith(bundlePrefix(bundle))).map((file) => Object.freeze({
|
|
889
|
-
...file,
|
|
890
|
-
path: file.path.slice(bundlePrefix(bundle).length)
|
|
891
|
-
})))
|
|
892
|
-
}))]);
|
|
893
|
-
} else contents = Object.freeze([Object.freeze({
|
|
894
|
-
kind: "kit",
|
|
895
|
-
pin: archive.pin,
|
|
896
|
-
files: archive.files
|
|
897
|
-
})]);
|
|
898
|
-
} else {
|
|
899
|
-
const bundle = await loadDesignBundle(snapshot.fs, {
|
|
900
|
-
rootDir: snapshot.root,
|
|
901
|
-
sourceId: snapshot.sourceId
|
|
902
|
-
}, archive.pin);
|
|
903
|
-
selected.push(bundle.skill);
|
|
904
|
-
const context = options.validationContext;
|
|
905
|
-
if (archive.dependencies.length && (!context || !isAbsolute(context.root))) throw Error("Independent Bundle dependency validation requires a host-resolved immutable Kit context.");
|
|
906
|
-
if (context) {
|
|
907
|
-
if (!isAbsolute(context.root)) throw Error("Archive context must have a host-resolved absolute root.");
|
|
908
|
-
const contextView = sourceView(context);
|
|
909
|
-
const kit = await loadStudioKit(contextView.fs, context.root, contextView.options);
|
|
910
|
-
if (archive.dependencies.some((dependency) => !kit.skills.some((skill) => samePin(bundlePin(skill), dependency)))) throw Error("Archive Bundle dependency is unresolved in context.");
|
|
911
|
-
const existing = kit.skills.find((skill) => samePin(bundlePin(skill), archive.pin));
|
|
912
|
-
if (existing?.resourceRoot) {
|
|
913
|
-
const local = virtualSource(archive.files, existing.resourceRoot);
|
|
914
|
-
const owns = (path) => within(existing.resourceRoot, path);
|
|
915
|
-
await loadStudioKit({
|
|
916
|
-
readText: (path) => owns(path) ? local.readText(path) : contextView.fs.readText(path),
|
|
917
|
-
readResource: (path) => owns(path) ? local.readResource(path) : contextView.fs.readResource(path),
|
|
918
|
-
listDir: (path) => owns(path) ? local.listDir(path) : contextView.fs.listDir(path),
|
|
919
|
-
async isDirectory(path) {
|
|
920
|
-
return owns(path) ? local.isDirectory(path) : contextView.fs.isDirectory(path);
|
|
921
|
-
},
|
|
922
|
-
async realPath(path) {
|
|
923
|
-
return owns(path) ? resolve(path) : contextView.fs.realPath(path);
|
|
924
|
-
}
|
|
925
|
-
}, context.root, contextView.options);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
contents = Object.freeze([Object.freeze({
|
|
929
|
-
kind: "bundle",
|
|
930
|
-
pin: archive.pin,
|
|
931
|
-
files: archive.files
|
|
932
|
-
})]);
|
|
933
|
-
}
|
|
934
|
-
requiresTrustReview ||= selected.some((skill) => {
|
|
935
|
-
const manifest = skill.metadata?.skillManifest;
|
|
936
|
-
const rules = skill.metadata?.validationDocument;
|
|
937
|
-
return manifest?.presentation?.mode === "native" || !!rules?.rules?.some((rule) => typeof rule.check?.driver === "string");
|
|
938
|
-
});
|
|
939
|
-
validation = Object.freeze({
|
|
940
|
-
valid: true,
|
|
941
|
-
diagnostics: Object.freeze([])
|
|
942
|
-
});
|
|
943
|
-
} catch (error) {
|
|
944
|
-
contents = Object.freeze([]);
|
|
945
|
-
validation = Object.freeze({
|
|
946
|
-
valid: false,
|
|
947
|
-
diagnostics: Object.freeze([Object.freeze({
|
|
948
|
-
code: "archive.definition",
|
|
949
|
-
message: error instanceof Error ? error.message : String(error),
|
|
950
|
-
subject: archive.pin.name
|
|
951
|
-
})])
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
return Object.freeze({
|
|
955
|
-
archive,
|
|
956
|
-
files: archive.files,
|
|
957
|
-
contents,
|
|
958
|
-
executionGranted: false,
|
|
959
|
-
requiresTrustReview,
|
|
960
|
-
validation
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
/** Capture only immutable host snapshots, preserving full exact referenced trees. */
|
|
964
|
-
async function exportStudioContentArchive(snapshot, input) {
|
|
965
|
-
const request = JSON.parse(JSON.stringify(input));
|
|
966
|
-
if (!isAbsolute(snapshot.root)) throw Error("Archive source root must be host-resolved absolute.");
|
|
967
|
-
const view = sourceView(snapshot), kit = await loadStudioKit(view.fs, snapshot.root, view.options);
|
|
968
|
-
const selected = request.kind === "kit" ? kit : kit.skills.find((skill) => samePin(bundlePin(skill), request.pin));
|
|
969
|
-
if (!selected || !samePin(request.kind === "kit" ? {
|
|
970
|
-
name: selected.name,
|
|
971
|
-
version: String(selected.version)
|
|
972
|
-
} : bundlePin(selected), request.pin)) throw Error("Export pin disagrees with snapshot.");
|
|
973
|
-
const files = [];
|
|
974
|
-
let total = 0;
|
|
975
|
-
const append = async (source, prefix, selectPrefix = "") => {
|
|
976
|
-
const all = await source.listFiles();
|
|
977
|
-
if (all.length > MAX_FILES) throw Error("Snapshot file count exceeds 4096.");
|
|
978
|
-
for (const path of [...all].sort()) {
|
|
979
|
-
safePath(path);
|
|
980
|
-
if (selectPrefix && !path.startsWith(selectPrefix)) continue;
|
|
981
|
-
const target = prefix + path.slice(selectPrefix.length);
|
|
982
|
-
safePath(target);
|
|
983
|
-
const bytes = await source.readBytes(resolve(source.root, path));
|
|
984
|
-
total += bytes.byteLength;
|
|
985
|
-
if (total > MAX_BYTES || files.length >= MAX_FILES) throw Error("Archive content exceeds 16 MiB or 4096 files.");
|
|
986
|
-
files.push({
|
|
987
|
-
path: target,
|
|
988
|
-
base64: Buffer.from(bytes).toString("base64"),
|
|
989
|
-
bytes: bytes.byteLength,
|
|
990
|
-
sha256: sha(bytes)
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
};
|
|
994
|
-
if (snapshot.bundles) {
|
|
995
|
-
const referenced = kit.skills.map((skill) => {
|
|
996
|
-
const identity = bundlePin(skill), matches = snapshot.bundles.filter((bundle) => samePin(bundle, identity));
|
|
997
|
-
if (matches.length !== 1) throw Error("Missing or ambiguous exact Bundle snapshot.");
|
|
998
|
-
return matches[0];
|
|
999
|
-
});
|
|
1000
|
-
if (request.kind === "kit") {
|
|
1001
|
-
if ((await snapshot.listFiles()).some((path) => path === "bundles" || path.startsWith("bundles/"))) throw Error("Kit-owned files collide with archive Bundle transport paths.");
|
|
1002
|
-
await append(snapshot, "");
|
|
1003
|
-
for (const bundle of referenced) await append(bundle.snapshot, bundlePrefix(bundle));
|
|
1004
|
-
} else await append(referenced.find((bundle) => samePin(bundle, request.pin)).snapshot, "");
|
|
1005
|
-
} else await append(snapshot, "", request.kind === "bundle" ? `skills/${selected.name}/` : "");
|
|
1006
|
-
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1007
|
-
const data = {
|
|
1008
|
-
format: "studio-content-archive/v1",
|
|
1009
|
-
kind: request.kind,
|
|
1010
|
-
pin: request.pin,
|
|
1011
|
-
provenance: request.provenance,
|
|
1012
|
-
dependencies: request.kind === "kit" ? kit.skills.map(bundlePin) : request.dependencies ?? [],
|
|
1013
|
-
files
|
|
1014
|
-
};
|
|
1015
|
-
if (request.kind === "kit" && request.dependencies !== void 0 && JSON.stringify(request.dependencies) !== JSON.stringify(data.dependencies)) throw Error("Explicit Kit pins disagree with source membership.");
|
|
1016
|
-
const serialized = JSON.stringify({
|
|
1017
|
-
...data,
|
|
1018
|
-
sha256: sha(canonical(data))
|
|
1019
|
-
});
|
|
1020
|
-
const prepared = await prepareStudioContentImport(serialized, { validationContext: snapshot });
|
|
1021
|
-
if (!prepared.validation.valid) throw Error(prepared.validation.diagnostics[0].message);
|
|
1022
|
-
return serialized;
|
|
1023
|
-
}
|
|
1024
|
-
//#endregion
|
|
1025
143
|
//#region src/composition.ts
|
|
1026
144
|
function pinKey(name, version) {
|
|
1027
145
|
return `${name}\u0000${version}`;
|
|
@@ -1429,4 +547,4 @@ var StudioBundleSourceService = class {
|
|
|
1429
547
|
}
|
|
1430
548
|
};
|
|
1431
549
|
//#endregion
|
|
1432
|
-
export { StudioBinaryContentError, StudioBundleSourceService,
|
|
550
|
+
export { STUDIO_INSTANCE_CHANGE_LIMITS, StudioBinaryContentError, StudioBundleSourceService, StudioManagementService, buildStudioCompositionCatalog, exportStudioContentArchive, exportStudioSourceArchive, loadStudioKit, normalizeStudioInstanceChanges, prepareStudioPanelPreview, readStudioContentArchive, searchStudioBundles, snapshotKit, validateBundle, validateKit };
|