dsh-comfyui 0.4.0 → 0.5.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/lib/routes.js CHANGED
@@ -2,6 +2,7 @@ import { errorMessage, readJsonBody, readRawBody, sameOrigin, sendJson } from '.
2
2
  import { analyzeWorkflowParameters, comboChildInfo, inputOptions, numberSpecOf, refreshParameterMetadata, uploadKindOf } from './params.js';
3
3
  import { collectMedia, historyErrorMessage, mediaProxyUrl } from './comfyui.js';
4
4
  import { MAX_ASSET_BYTES, SKILL_MAIN, SKILL_PRESET_DIRS, joinFrontmatter, splitFrontmatter } from './skillpack.js';
5
+ import { MAX_IMPORT_BYTES, analyzeImportPackage, applyImportPackage, buildExportPackage } from './transfer.js';
5
6
  import { unlink } from 'node:fs/promises';
6
7
  import { spawn } from 'node:child_process';
7
8
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
@@ -936,6 +937,186 @@ export function mountComfyUIRoutes(ctx, runtime) {
936
937
  sendJson(response, 200, { ok: true });
937
938
  }),
938
939
  }));
940
+ // ── Workflow preset transfer (export / import) ──────────────────────────
941
+ // Export bundles selected library workflows (API format) with their
942
+ // parameters and skill packs into one .zip download; import parses an
943
+ // uploaded package and then writes the selection back as NEW workflows.
944
+ // The packaging, validation and pack writes live in transfer.ts — these
945
+ // routes only shape HTTP (same-origin, body cap, content types).
946
+ //
947
+ // The export body comes in two shapes on purpose: JSON `{ ids }` from a
948
+ // fetch caller, and an urlencoded form (`ids` repeated) from the panel's
949
+ // hidden-form submit — the panel downloads natively so the browser, not
950
+ // JS blob plumbing, owns the whole transfer.
951
+ //
952
+ // The native form download means the panel never sees the response body,
953
+ // so the facts of the last package ride back through `export/last`: the
954
+ // panel reads it right after submitting and reports what was ACTUALLY
955
+ // packaged, not what the checkboxes believed.
956
+ let lastExport = null;
957
+ disposers.push(webServer.register({
958
+ kind: 'exact',
959
+ path: '/comfyui/workflows/export/last',
960
+ handler: withHint(async (request, response) => {
961
+ if (!methodIs(request, 'GET')) {
962
+ sendJson(response, 405, { error: 'method not allowed' });
963
+ return;
964
+ }
965
+ sendJson(response, 200, { last: lastExport });
966
+ }),
967
+ }));
968
+ disposers.push(webServer.register({
969
+ kind: 'exact',
970
+ path: '/comfyui/workflows/export',
971
+ handler: withHint(async (request, response) => {
972
+ if (!methodIs(request, 'POST')) {
973
+ sendJson(response, 405, { error: 'method not allowed' });
974
+ return;
975
+ }
976
+ let ids;
977
+ const contentType = String(request.headers['content-type'] ?? '');
978
+ if (contentType.includes('application/x-www-form-urlencoded')) {
979
+ if (!sameOrigin(request)) {
980
+ sendJson(response, 403, { error: 'forbidden: same-origin requests only' });
981
+ return;
982
+ }
983
+ const raw = await readRawBody(request);
984
+ ids = [...new URLSearchParams(raw.toString('utf8')).getAll('ids')];
985
+ }
986
+ else {
987
+ const body = await readSameOriginPost(request, response);
988
+ if (body === undefined)
989
+ return;
990
+ ids = body.ids;
991
+ }
992
+ const result = await buildExportPackage(runtime, ids);
993
+ if (!result.ok) {
994
+ sendJson(response, 400, { error: result.error });
995
+ return;
996
+ }
997
+ // Audit trail on purpose: download managers dedupe by URL, so "which
998
+ // file is which" became a live user question — the log answers it with
999
+ // the exact packaged set per request, and the receipt the panel shows
1000
+ // carries the file name + size so a stale download can't masquerade as
1001
+ // this export.
1002
+ lastExport = { at: new Date().toISOString(), count: result.count, names: result.names, warnings: result.warnings, filename: result.filename, size: result.bytes.length };
1003
+ ctx.logger.info(`comfyui: 导出 ${result.count} 个工作流 → ${result.filename}(${result.names.join('、')})${result.warnings.length > 0 ? ` 警告:${result.warnings.join(';')}` : ''}`);
1004
+ response.writeHead(200, {
1005
+ 'content-type': 'application/zip',
1006
+ 'content-length': String(result.bytes.length),
1007
+ 'cache-control': 'no-store',
1008
+ // ASCII file name on purpose: it rides a Content-Disposition header
1009
+ // without RFC 5987 encoding and stays readable on every OS.
1010
+ 'content-disposition': `attachment; filename="${result.filename}"`,
1011
+ });
1012
+ response.end(result.bytes);
1013
+ }),
1014
+ }));
1015
+ // Native GET download: the file name rides IN the URL path. A download
1016
+ // manager that derives the saved name from the URL, or re-fetches the URL
1017
+ // itself, still lands on the correct name — and because the export is a
1018
+ // pure read, on the genuine bytes for exactly these ids. This is the
1019
+ // antidote to the intercepted-POST downloads that kept saving stale or
1020
+ // renamed files on one setup (skills "sometimes missing" was those files).
1021
+ // Exact routes above win for `/export` and `/export/last`; this prefix only
1022
+ // sees `/export/<filename>`.
1023
+ disposers.push(webServer.register({
1024
+ kind: 'prefix',
1025
+ path: '/comfyui/workflows/export',
1026
+ handler: withHint(async (request, response) => {
1027
+ if (!methodIs(request, 'GET')) {
1028
+ sendJson(response, 405, { error: 'method not allowed' });
1029
+ return;
1030
+ }
1031
+ const url = new URL(request.url ?? '/', 'http://localhost');
1032
+ const filename = decodeURIComponent(url.pathname.slice('/comfyui/workflows/export/'.length));
1033
+ // The name is client-supplied but strictly patterned: it only ever
1034
+ // feeds a header value and the receipt, never a filesystem path.
1035
+ if (!/^dsh-comfyui-presets-[0-9A-Za-z-]+\.zip$/.test(filename)) {
1036
+ sendJson(response, 400, { error: 'bad export file name' });
1037
+ return;
1038
+ }
1039
+ const result = await buildExportPackage(runtime, url.searchParams.getAll('ids'));
1040
+ if (!result.ok) {
1041
+ sendJson(response, 400, { error: result.error });
1042
+ return;
1043
+ }
1044
+ lastExport = { at: new Date().toISOString(), count: result.count, names: result.names, warnings: result.warnings, filename, size: result.bytes.length };
1045
+ ctx.logger.info(`comfyui: 导出 ${result.count} 个工作流 → ${filename}(${result.names.join('、')})${result.warnings.length > 0 ? ` 警告:${result.warnings.join(';')}` : ''}`);
1046
+ response.writeHead(200, {
1047
+ 'content-type': 'application/zip',
1048
+ 'content-length': String(result.bytes.length),
1049
+ 'cache-control': 'no-store',
1050
+ 'content-disposition': `attachment; filename="${filename}"`,
1051
+ });
1052
+ response.end(result.bytes);
1053
+ }),
1054
+ }));
1055
+ // Import is two-phase with zero server-side staging: analyze parses the
1056
+ // uploaded bytes and lists what the package offers; apply re-reads the
1057
+ // same bytes the client still holds and writes the selection. Selection
1058
+ // travels by manifest index (stable, because the same bytes are parsed
1059
+ // again), which avoids URL-encoding arbitrary package ids.
1060
+ disposers.push(webServer.register({
1061
+ kind: 'exact',
1062
+ path: '/comfyui/workflows/import/analyze',
1063
+ handler: withHint(async (request, response) => {
1064
+ if (!methodIs(request, 'POST')) {
1065
+ sendJson(response, 405, { error: 'method not allowed' });
1066
+ return;
1067
+ }
1068
+ if (!sameOrigin(request)) {
1069
+ sendJson(response, 403, { error: 'forbidden: same-origin requests only' });
1070
+ return;
1071
+ }
1072
+ const raw = await readRawBody(request);
1073
+ if (raw.length > MAX_IMPORT_BYTES) {
1074
+ sendJson(response, 413, { error: `预设包不能超过 ${Math.floor(MAX_IMPORT_BYTES / 1024 / 1024)} MB` });
1075
+ return;
1076
+ }
1077
+ const result = analyzeImportPackage(raw);
1078
+ if (!result.ok) {
1079
+ sendJson(response, 400, { error: result.error });
1080
+ return;
1081
+ }
1082
+ sendJson(response, 200, result);
1083
+ }),
1084
+ }));
1085
+ disposers.push(webServer.register({
1086
+ kind: 'exact',
1087
+ path: '/comfyui/workflows/import/apply',
1088
+ handler: withHint(async (request, response) => {
1089
+ if (!methodIs(request, 'POST')) {
1090
+ sendJson(response, 405, { error: 'method not allowed' });
1091
+ return;
1092
+ }
1093
+ if (!sameOrigin(request)) {
1094
+ sendJson(response, 403, { error: 'forbidden: same-origin requests only' });
1095
+ return;
1096
+ }
1097
+ const url = new URL(request.url ?? '/', 'http://localhost');
1098
+ const selected = (url.searchParams.get('select') ?? '')
1099
+ .split(',')
1100
+ .map((part) => Number.parseInt(part, 10))
1101
+ .filter((index) => Number.isInteger(index) && index >= 0);
1102
+ const raw = await readRawBody(request);
1103
+ if (raw.length > MAX_IMPORT_BYTES) {
1104
+ sendJson(response, 413, { error: `预设包不能超过 ${Math.floor(MAX_IMPORT_BYTES / 1024 / 1024)} MB` });
1105
+ return;
1106
+ }
1107
+ try {
1108
+ const result = await applyImportPackage(runtime, raw, selected);
1109
+ if (!result.ok) {
1110
+ sendJson(response, 400, { error: result.error });
1111
+ return;
1112
+ }
1113
+ sendJson(response, 200, result);
1114
+ }
1115
+ catch (error) {
1116
+ sendJson(response, 500, { error: errorMessage(error) });
1117
+ }
1118
+ }),
1119
+ }));
939
1120
  disposers.push(webServer.register({
940
1121
  kind: 'exact',
941
1122
  path: '/comfyui/workflows/run',
package/lib/skill.js CHANGED
@@ -148,5 +148,6 @@ TTS-Audio-Suite(\`{comfyuiDir}/custom_nodes/tts_audio_suite\`)的"🎭 Chara
148
148
  - 面板"ComfyUI 端保存"分区列出每个图工作流:未提取显示"未提取",已提取显示派生出的运行工作流列表。
149
149
  - 点**提取**:单分量直接拆;多分量弹出选项(分析报告 + 整体/按分量/主流程)。也可点**查看**看节点清单和 JSON。
150
150
  - 用户也可以不经过图,直接在插件库"新建工作流"粘贴 API JSON(或选择 .json 文件)作为运行工作流导入。
151
+ - 工具栏"导出预设 / 导入预设":导出把选中的运行工作流连同参数与技能包打包成一个 .zip 预设包;导入选中包内的工作流后**创建为新工作流**(重名自动加"(导入)"后缀),不会覆盖库里已有的工作流,技能包一并还原(超限文件会跳过并提示)。用户要换机 / 备份 / 分享工作流时引导用这对按钮。
151
152
  `,
152
153
  };
@@ -16,9 +16,18 @@ export declare const MAX_FILE_BYTES: number;
16
16
  /** Imported binaries live in `assets/` and never enter the prompt as text, so
17
17
  * they get a larger budget than the documents the model reads verbatim. */
18
18
  export declare const MAX_ASSET_BYTES: number;
19
- /** A pack is documentation, not storage — these caps keep it that way. */
20
- export declare const MAX_PACK_FILES = 100;
19
+ /** A pack is documentation, not storage — these caps keep it that way. The
20
+ * byte cap is the real storage guard; the file count only stops pathological
21
+ * noise, so it sits well above real bulk-copied packs (a ComfyUI music
22
+ * template library lands at 1000+ small .txt files and MUST survive a
23
+ * preset export → import round trip). */
24
+ export declare const MAX_PACK_FILES = 2000;
21
25
  export declare const MAX_PACK_BYTES: number;
26
+ /** Per-file size budget by extension: binaries never enter the prompt as
27
+ * text, so they get the larger budget wherever they live. Exported because
28
+ * the preset importer pre-filters oversized files per file (skip + warning)
29
+ * before handing the batch to {@link SkillPackStore.writeMany}. */
30
+ export declare function sizeLimitOf(file: string): number;
22
31
  /** One file inside a pack. */
23
32
  export interface SkillFileInfo {
24
33
  /** Path relative to the pack root, always forward-slashed (`references/styles.md`). */
@@ -152,6 +161,16 @@ export declare class SkillPackStore {
152
161
  * can never bypass a limit the editor honours.
153
162
  */
154
163
  writeBytes(slug: string, path: string, bytes: Buffer): Promise<SkillPackResult<SkillPackInfo>>;
164
+ /** Bulk write for preset restore: ONE limit check for the whole batch
165
+ * instead of per-file enumeration. Per-file checks re-count the entire
166
+ * pack on every write, so restoring a 1000-file pack that way is O(n²)
167
+ * filesystem calls — a frozen-looking import for minutes. Grammar and
168
+ * per-file size limits are still enforced here; the batch is refused
169
+ * whole (the caller reports which file tripped), never written partially. */
170
+ writeMany(slug: string, entries: ReadonlyArray<{
171
+ path: string;
172
+ bytes: Buffer;
173
+ }>): Promise<SkillPackResult<SkillPackInfo>>;
155
174
  /** Read one file as raw bytes (the panel's image preview and downloads). */
156
175
  readBytes(slug: string, path: string): Promise<SkillPackResult<Buffer>>;
157
176
  /** Rename one file; SKILL.md is fixed and both sides are validated. */
@@ -247,6 +266,14 @@ export interface WorkflowSkillPacks {
247
266
  path: string;
248
267
  pack: WorkflowSkillPack;
249
268
  }>>;
269
+ /** Bulk restore path: every entry's `path` is already bucket-qualified
270
+ * (`references/x.txt` or `SKILL.md`). One limit check for the whole batch —
271
+ * see {@link SkillPackStore.writeMany} for why per-file checks are not
272
+ * good enough at 1000-file scale. */
273
+ importFiles(id: string, entries: ReadonlyArray<{
274
+ path: string;
275
+ bytes: Buffer;
276
+ }>): Promise<SkillPackResult<WorkflowSkillPack>>;
250
277
  writeFile(id: string, path: string, content: string): Promise<SkillPackResult<WorkflowSkillPack>>;
251
278
  renameFile(id: string, from: string, to: string): Promise<SkillPackResult<WorkflowSkillPack>>;
252
279
  deleteFile(id: string, path: string): Promise<SkillPackResult<WorkflowSkillPack>>;
package/lib/skillpack.js CHANGED
@@ -23,7 +23,7 @@
23
23
  * follows.
24
24
  */
25
25
  import { mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from 'node:fs/promises';
26
- import { isAbsolute, join, relative, resolve } from 'node:path';
26
+ import { isAbsolute, dirname, join, relative, resolve } from 'node:path';
27
27
  /**
28
28
  * Sub-directories offered by default. These are suggestions, not a whitelist:
29
29
  * a pack may hold any directory whose name matches {@link DIR_NAME}, so a user
@@ -49,11 +49,25 @@ export const MAX_FILE_BYTES = 256 * 1024;
49
49
  /** Imported binaries live in `assets/` and never enter the prompt as text, so
50
50
  * they get a larger budget than the documents the model reads verbatim. */
51
51
  export const MAX_ASSET_BYTES = 4 * 1024 * 1024;
52
- /** A pack is documentation, not storage — these caps keep it that way. */
53
- export const MAX_PACK_FILES = 100;
52
+ /** A pack is documentation, not storage — these caps keep it that way. The
53
+ * byte cap is the real storage guard; the file count only stops pathological
54
+ * noise, so it sits well above real bulk-copied packs (a ComfyUI music
55
+ * template library lands at 1000+ small .txt files and MUST survive a
56
+ * preset export → import round trip). */
57
+ export const MAX_PACK_FILES = 2000;
54
58
  export const MAX_PACK_BYTES = 20 * 1024 * 1024;
55
- /** File names: letters, digits, CJK, dot, dash, underscore. No separators, no leading dot. */
56
- const FILE_NAME = /^[A-Za-z0-9_一-龥][A-Za-z0-9._一-龥-]{0,63}$/;
59
+ /** Per-file size budget by extension: binaries never enter the prompt as
60
+ * text, so they get the larger budget wherever they live. Exported because
61
+ * the preset importer pre-filters oversized files per file (skip + warning)
62
+ * before handing the batch to {@link SkillPackStore.writeMany}. */
63
+ export function sizeLimitOf(file) {
64
+ return ASSET_EXTENSIONS.includes(extensionOf(file)) ? MAX_ASSET_BYTES : MAX_FILE_BYTES;
65
+ }
66
+ /** File names: letters, digits, CJK, dot, dash, underscore. No separators, no
67
+ * leading dot. 128 chars, not 64: bulk-copied packs carry long descriptive
68
+ * template names ("c-pop-guofeng-traditional-chinese-style-cinematic-ballad"),
69
+ * and a cap they fail makes those files silently invisible to info()/export. */
70
+ const FILE_NAME = /^[A-Za-z0-9_一-龥][A-Za-z0-9._一-龥-]{0,127}$/;
57
71
  /** Directory names this module generates and later trusts only after re-validation. */
58
72
  const SLUG = /^[a-z0-9][a-z0-9-]{0,79}$/;
59
73
  /** Sub-directory names inside a pack: same shape as a file name, no dots. */
@@ -490,6 +504,54 @@ export class SkillPackStore {
490
504
  await writeFile(target.value, bytes);
491
505
  return this.info(slug);
492
506
  }
507
+ /** Bulk write for preset restore: ONE limit check for the whole batch
508
+ * instead of per-file enumeration. Per-file checks re-count the entire
509
+ * pack on every write, so restoring a 1000-file pack that way is O(n²)
510
+ * filesystem calls — a frozen-looking import for minutes. Grammar and
511
+ * per-file size limits are still enforced here; the batch is refused
512
+ * whole (the caller reports which file tripped), never written partially. */
513
+ async writeMany(slug, entries) {
514
+ if (entries.length === 0)
515
+ return this.info(slug);
516
+ const before = await this.info(slug);
517
+ if (!before.ok)
518
+ return before;
519
+ const existingByPath = new Map(before.value.files.map((file) => [file.path, file.size]));
520
+ let existingReplacedBytes = 0;
521
+ let incomingBytes = 0;
522
+ let newCount = 0;
523
+ const plan = [];
524
+ for (const entry of entries) {
525
+ const target = this.fileOf(slug, entry.path);
526
+ if (!target.ok)
527
+ return target;
528
+ const parsed = parseSkillPath(entry.path);
529
+ if (!parsed.ok)
530
+ return parsed;
531
+ const limit = ASSET_EXTENSIONS.includes(extensionOf(parsed.value.file)) ? MAX_ASSET_BYTES : MAX_FILE_BYTES;
532
+ if (entry.bytes.length > limit) {
533
+ return fail(`单个文件不能超过 ${Math.floor(limit / 1024)} KB(${entry.path} 当前 ${Math.ceil(entry.bytes.length / 1024)} KB)`);
534
+ }
535
+ const existingSize = existingByPath.get(entry.path);
536
+ if (existingSize === undefined)
537
+ newCount += 1;
538
+ else
539
+ existingReplacedBytes += existingSize;
540
+ incomingBytes += entry.bytes.length;
541
+ plan.push({ target: target.value, bytes: entry.bytes });
542
+ }
543
+ if (before.value.files.length + newCount > MAX_PACK_FILES) {
544
+ return fail(`一个技能包最多 ${MAX_PACK_FILES} 个文件`);
545
+ }
546
+ if (before.value.totalBytes - existingReplacedBytes + incomingBytes > MAX_PACK_BYTES) {
547
+ return fail(`一个技能包最多 ${Math.floor(MAX_PACK_BYTES / 1024 / 1024)} MB`);
548
+ }
549
+ for (const item of plan) {
550
+ await mkdir(dirname(item.target), { recursive: true });
551
+ await writeFile(item.target, item.bytes);
552
+ }
553
+ return this.info(slug);
554
+ }
493
555
  /** Read one file as raw bytes (the panel's image preview and downloads). */
494
556
  async readBytes(slug, path) {
495
557
  const target = this.fileOf(slug, path);
@@ -738,6 +800,12 @@ export function createWorkflowSkillPacks(host) {
738
800
  return resolved;
739
801
  return afterMutation(id, packs.write(resolved.value.slug, path, content));
740
802
  },
803
+ async importFiles(id, entries) {
804
+ const resolved = await resolvePack(id);
805
+ if (!resolved.ok)
806
+ return resolved;
807
+ return afterMutation(id, packs.writeMany(resolved.value.slug, entries));
808
+ },
741
809
  async renameFile(id, from, to) {
742
810
  const resolved = await resolvePack(id);
743
811
  if (!resolved.ok)
@@ -0,0 +1,158 @@
1
+ import type { WorkflowParameter } from './params.js';
2
+ import { type StoredWorkflow } from './store.js';
3
+ import { type SkillPackResult, type WorkflowSkillPack } from './skillpack.js';
4
+ /** Marker plus layout version of the packages this module writes. */
5
+ export declare const PRESET_FORMAT = "dsh-comfyui-workflow-preset";
6
+ export declare const PRESET_VERSION = 1;
7
+ /** Hard cap on the uploaded archive (route-level check before parsing). */
8
+ export declare const MAX_IMPORT_BYTES: number;
9
+ /** One workflow record inside preset.json. */
10
+ export interface PresetWorkflow {
11
+ /** Id in the EXPORTING library; import mints a fresh one. */
12
+ id: string;
13
+ name: string;
14
+ description: string;
15
+ tags?: string[];
16
+ parameters?: WorkflowParameter[];
17
+ requireSkill?: boolean;
18
+ /** Skill-pack contents; null when the workflow has no pack (or it was unreadable). */
19
+ skill: PresetSkillRef | null;
20
+ workflow: Record<string, {
21
+ class_type: string;
22
+ inputs: Record<string, unknown>;
23
+ }>;
24
+ }
25
+ /** What the panel and tools need to know about one archived pack. */
26
+ export interface PresetSkillRef {
27
+ files: Array<{
28
+ path: string;
29
+ size: number;
30
+ }>;
31
+ /** Sub-directories, kept so empty ones survive the round trip. */
32
+ dirs: string[];
33
+ }
34
+ /** The whole manifest. `count` mirrors `workflows.length` on export and is
35
+ * recomputed (not trusted) on import. */
36
+ export interface PresetManifest {
37
+ format: string;
38
+ version: number;
39
+ exportedAt: string;
40
+ pluginVersion: string;
41
+ count: number;
42
+ workflows: PresetWorkflow[];
43
+ }
44
+ /** Structural slice of the runtime the transfer routes operate through. */
45
+ export interface TransferHost {
46
+ listWorkflows(): Promise<StoredWorkflow[]>;
47
+ getWorkflow(id: string): Promise<StoredWorkflow | undefined>;
48
+ saveWorkflow(input: {
49
+ name: string;
50
+ description: string;
51
+ workflow: unknown;
52
+ parameters?: WorkflowParameter[];
53
+ tags?: string[];
54
+ }): Promise<{
55
+ ok: true;
56
+ workflow: StoredWorkflow;
57
+ } | {
58
+ ok: false;
59
+ error: string;
60
+ }>;
61
+ skillPacks: {
62
+ info(id: string): Promise<WorkflowSkillPack | undefined>;
63
+ readRaw(id: string, path: string): Promise<SkillPackResult<{
64
+ bytes: Buffer;
65
+ contentType: string;
66
+ }>>;
67
+ enable(id: string): Promise<SkillPackResult<WorkflowSkillPack>>;
68
+ disable(id: string): Promise<SkillPackResult<true>>;
69
+ writeFile(id: string, path: string, content: string): Promise<SkillPackResult<WorkflowSkillPack>>;
70
+ importFile(id: string, file: string, bytes: Buffer, bucket?: string): Promise<SkillPackResult<{
71
+ path: string;
72
+ pack: WorkflowSkillPack;
73
+ }>>;
74
+ importFiles(id: string, entries: ReadonlyArray<{
75
+ path: string;
76
+ bytes: Buffer;
77
+ }>): Promise<SkillPackResult<WorkflowSkillPack>>;
78
+ makeDir(id: string, name: string): Promise<SkillPackResult<WorkflowSkillPack>>;
79
+ setRequired(id: string, required: boolean): Promise<SkillPackResult<WorkflowSkillPack>>;
80
+ };
81
+ }
82
+ export type ExportResult = {
83
+ ok: true;
84
+ bytes: Uint8Array;
85
+ filename: string;
86
+ count: number;
87
+ names: string[];
88
+ warnings: string[];
89
+ } | {
90
+ ok: false;
91
+ error: string;
92
+ };
93
+ /** One listed workflow inside an analyzed package. */
94
+ export interface ImportCandidate {
95
+ /** Position in the manifest; the apply call selects by this index, which
96
+ * stays stable because the same bytes are parsed again. */
97
+ index: number;
98
+ id: string;
99
+ name: string;
100
+ description: string;
101
+ tags: string[];
102
+ paramCount: number;
103
+ skill: {
104
+ fileCount: number;
105
+ totalBytes: number;
106
+ required: boolean;
107
+ } | null;
108
+ /** Non-fatal problems already visible at analysis time. */
109
+ warnings: string[];
110
+ }
111
+ export interface ImportAnalysis {
112
+ version: number;
113
+ exportedAt: string;
114
+ pluginVersion: string;
115
+ workflows: ImportCandidate[];
116
+ }
117
+ /** Per-workflow result of an import; `ok` entries carry their new identity. */
118
+ export interface ImportOutcome {
119
+ index: number;
120
+ name: string;
121
+ ok: boolean;
122
+ newName?: string;
123
+ newId?: string;
124
+ error?: string;
125
+ warnings: string[];
126
+ }
127
+ /**
128
+ * Build one preset package from the selected library workflows.
129
+ * @param host - the runtime face (workflow store + skill packs).
130
+ * @param ids - workflow ids to export; unknown ids are skipped with a warning.
131
+ */
132
+ export declare function buildExportPackage(host: TransferHost, ids: unknown): Promise<ExportResult>;
133
+ /**
134
+ * Analyze one uploaded package: list the workflows it offers, with per-item
135
+ * warnings for anything that would fail at apply time. No disk writes.
136
+ */
137
+ export declare function analyzeImportPackage(bytes: Buffer): {
138
+ ok: true;
139
+ analysis: ImportAnalysis;
140
+ } | {
141
+ ok: false;
142
+ error: string;
143
+ };
144
+ /**
145
+ * Import the selected workflows from one uploaded package. Every workflow
146
+ * becomes a NEW library record (fresh id, name suffixed on clash); skill
147
+ * packs land in freshly slugged directories, so nothing on disk is reused
148
+ * or overwritten.
149
+ * @param selected - manifest indexes to import (as listed by analyze).
150
+ */
151
+ export declare function applyImportPackage(host: TransferHost, bytes: Buffer, selected: unknown): Promise<{
152
+ ok: true;
153
+ results: ImportOutcome[];
154
+ imported: number;
155
+ } | {
156
+ ok: false;
157
+ error: string;
158
+ }>;