@xopcai/xopc 0.0.282 → 0.0.284

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.
Files changed (184) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{activity-store-iPtG94qr.js → activity-store-CwbIMGLY.js} +1 -1
  4. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-Cny5rATT.js → agent-avatar-dicebear-DjiOld-S.js} +1 -1
  5. package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-OyQ8OVZv.js → agent-defaults-settings-panel-D9qP10BK.js} +1 -1
  6. package/dist/gateway/static/root/assets/{agents-DJeh_Ewo.js → agents-D-bkOuAl.js} +1 -1
  7. package/dist/gateway/static/root/assets/{api-KMJzMqvZ.js → api-3lphBVuY.js} +1 -1
  8. package/dist/gateway/static/root/assets/{app-management-settings-panel-ZqQKaZEI.js → app-management-settings-panel-B2nwCXxE.js} +1 -1
  9. package/dist/gateway/static/root/assets/{appearance-settings-CfXsQkKO.js → appearance-settings-BXO4Ej88.js} +1 -1
  10. package/dist/gateway/static/root/assets/{apps-page-k-QnVhKW.js → apps-page-BKwBBwJN.js} +1 -1
  11. package/dist/gateway/static/root/assets/{archive-plugin-BrW53Hap.js → archive-plugin-6ZRqNjla.js} +1 -1
  12. package/dist/gateway/static/root/assets/{attachment-load-DkfFWHiS.js → attachment-load-CmPZalcW.js} +1 -1
  13. package/dist/gateway/static/root/assets/{attachment-preview-renderer-CfEDFYPE.js → attachment-preview-renderer-wzIchDSv.js} +4 -4
  14. package/dist/gateway/static/root/assets/attachment-process-heavy-Db4nTY5N.js +5 -0
  15. package/dist/gateway/static/root/assets/{automations-page-e-5we6kD.js → automations-page-Bc3WbK5n.js} +1 -1
  16. package/dist/gateway/static/root/assets/{automations-workspace-DPnlkws1.js → automations-workspace-B2-f9qZu.js} +1 -1
  17. package/dist/gateway/static/root/assets/binary-plugins-OWR6cxpc.js +2 -0
  18. package/dist/gateway/static/root/assets/{block-editor-CfeF1OmA.js → block-editor-Ca-tDQXm.js} +1 -1
  19. package/dist/gateway/static/root/assets/{browser-automation-inputs-BTIufVbM.js → browser-automation-inputs-Dxy2_p3U.js} +1 -1
  20. package/dist/gateway/static/root/assets/{browser-automations-page-DnsuWKRu.js → browser-automations-page-CibQIxpi.js} +1 -1
  21. package/dist/gateway/static/root/assets/{browser-settings-page-C8xyhnxx.js → browser-settings-page-Dtn4rMeq.js} +1 -1
  22. package/dist/gateway/static/root/assets/{capabilities-settings-panel-CBllOLTT.js → capabilities-settings-panel-DiwtR9MD.js} +1 -1
  23. package/dist/gateway/static/root/assets/{channels-settings-C-A-VUE2.js → channels-settings-pTdR1q1K.js} +1 -1
  24. package/dist/gateway/static/root/assets/{channels-status-swr-DMYQNs-J.js → channels-status-swr-Bh-G9mdZ.js} +1 -1
  25. package/dist/gateway/static/root/assets/{chat-terminal-dock-C_Fw1R3A.js → chat-terminal-dock-lbzbSpV3.js} +1 -1
  26. package/dist/gateway/static/root/assets/{connectors-page-CtsMo3H4.js → connectors-page-BSQ7Hs2w.js} +1 -1
  27. package/dist/gateway/static/root/assets/{date-picker-CPFxjTrN.js → date-picker-tYvbTfZT.js} +1 -1
  28. package/dist/gateway/static/root/assets/{dependency-picker-C_5KqzjI.js → dependency-picker-B8yo7-7V.js} +1 -1
  29. package/dist/gateway/static/root/assets/{desktop-pet-DLJsC5D6.js → desktop-pet-BUGkGB4-.js} +1 -1
  30. package/dist/gateway/static/root/assets/{desktop-pet-settings-BdI1iSgz.js → desktop-pet-settings-DNeVGZNp.js} +1 -1
  31. package/dist/gateway/static/root/assets/{device-pairing-wizard-BnvPLhc9.js → device-pairing-wizard-CYkT7ecp.js} +1 -1
  32. package/dist/gateway/static/root/assets/{extension-debug-page-BNS_TkZQ.js → extension-debug-page-CiK0QI2p.js} +1 -1
  33. package/dist/gateway/static/root/assets/{extension-page-DMSDZ7Mb.js → extension-page-CirPNHJd.js} +1 -1
  34. package/dist/gateway/static/root/assets/{extension-settings-page-CeGCWMt0.js → extension-settings-page-Baua1_Le.js} +1 -1
  35. package/dist/gateway/static/root/assets/{gateway-config-swr-Cpmyrr6j.js → gateway-config-swr-Co4hHe0i.js} +1 -1
  36. package/dist/gateway/static/root/assets/{gateway-settings-DKOTYUma.js → gateway-settings-CzpSYWex.js} +1 -1
  37. package/dist/gateway/static/root/assets/{gateway-startup-retry-DoiPeb65.js → gateway-startup-retry-DBPNJG-e.js} +1 -1
  38. package/dist/gateway/static/root/assets/{heartbeat-settings-gsdL_Yij.js → heartbeat-settings-DTM_kYpE.js} +1 -1
  39. package/dist/gateway/static/root/assets/{home-page-CbeAdS1T.js → home-page-CSSJvdd6.js} +1 -1
  40. package/dist/gateway/static/root/assets/{image-generation-api-CqjHWl2T.js → image-generation-api-D0rMTumD.js} +1 -1
  41. package/dist/gateway/static/root/assets/imports-page-XqVVYbAp.js +1 -0
  42. package/dist/gateway/static/root/assets/{index-enFXJJCU.js → index-C9aQA4Dr.js} +10 -10
  43. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-CCLtJ8s8.js → keyboard-shortcuts-settings-CMUaF61L.js} +1 -1
  44. package/dist/gateway/static/root/assets/{local-app-workbench-page-DzC6SrXj.js → local-app-workbench-page-CZSgTYuI.js} +1 -1
  45. package/dist/gateway/static/root/assets/{local-apps-page-3LdgP3NO.js → local-apps-page-CHxyT09t.js} +1 -1
  46. package/dist/gateway/static/root/assets/{locale-store-1urrBBCQ.js → locale-store-DO6JJ1l2.js} +1 -1
  47. package/dist/gateway/static/root/assets/{logs-page-DHClKtm6.js → logs-page-z5ktnEzX.js} +1 -1
  48. package/dist/gateway/static/root/assets/{management-settings-BaCo9Nuo.js → management-settings-jXbA2S54.js} +1 -1
  49. package/dist/gateway/static/root/assets/{markdown-view-BZmcFIWa.js → markdown-view-CKInRsDh.js} +1 -1
  50. package/dist/gateway/static/root/assets/{media-plugins-BMjJppvG.js → media-plugins-BaPkSvW7.js} +1 -1
  51. package/dist/gateway/static/root/assets/messages-BgZRsCkG.js +3 -0
  52. package/dist/gateway/static/root/assets/{note-detail-panel-hYO2jNhL.js → note-detail-panel-DSaZfkna.js} +3 -3
  53. package/dist/gateway/static/root/assets/{notes-page-BT66x22W.js → notes-page-CePEyv_C.js} +1 -1
  54. package/dist/gateway/static/root/assets/{page-tabs-BCszXX_F.js → page-tabs-CdYemUOu.js} +1 -1
  55. package/dist/gateway/static/root/assets/pdf-runtime-DU_sMPjG.js +2 -0
  56. package/dist/gateway/static/root/assets/pdf.worker.min-B4E34YdZ.js +48 -0
  57. package/dist/gateway/static/root/assets/{preview-open-alternatives-CyM0sanh.js → preview-open-alternatives-DzgF1yYO.js} +1 -1
  58. package/dist/gateway/static/root/assets/{proactive-page-CVDGA5sB.js → proactive-page-Cy2Gm2f8.js} +1 -1
  59. package/dist/gateway/static/root/assets/{product-open-page-lXAbj85Z.js → product-open-page-iQ-eW6Aj.js} +1 -1
  60. package/dist/gateway/static/root/assets/{project-detail-page-Bp6TAMPM.js → project-detail-page-BliwYpns.js} +1 -1
  61. package/dist/gateway/static/root/assets/{projects-page-UhOIiDcg.js → projects-page-Dm-BqnQn.js} +1 -1
  62. package/dist/gateway/static/root/assets/{registry-api-CxQstD00.js → registry-api-DMNudT60.js} +1 -1
  63. package/dist/gateway/static/root/assets/{remote-access-hub-DJIkMFT4.js → remote-access-hub-C_XuQ9XR.js} +1 -1
  64. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-BUFCgRdZ.js → runtime-tools-settings-panel-u_wVepf7.js} +1 -1
  65. package/dist/gateway/static/root/assets/{schema-form-B01ksA8M.js → schema-form-YdZj2xdI.js} +1 -1
  66. package/dist/gateway/static/root/assets/{sessions-page-CQOAwPDt.js → sessions-page-Drt1aS2G.js} +1 -1
  67. package/dist/gateway/static/root/assets/settings-advanced-gate-BAUd06_9.js +1 -0
  68. package/dist/gateway/static/root/assets/{settings-form-section-DvnjZBa3.js → settings-form-section-Bw6xKOX5.js} +1 -1
  69. package/dist/gateway/static/root/assets/{settings-page-DZRkpbCh.js → settings-page-CV8kVdnE.js} +1 -1
  70. package/dist/gateway/static/root/assets/{setup-status-panel-DVaEiAVY.js → setup-status-panel-BHI9e5Tw.js} +1 -1
  71. package/dist/gateway/static/root/assets/share-preview-page-BHwjHlrV.js +2 -0
  72. package/dist/gateway/static/root/assets/{shares-settings-XiCqMDAe.js → shares-settings-DOAOjZBQ.js} +1 -1
  73. package/dist/gateway/static/root/assets/skill-reload-api-DU0VQpHj.js +1 -0
  74. package/dist/gateway/static/root/assets/skills-page-DRKdb6O5.js +2 -0
  75. package/dist/gateway/static/root/assets/{skills-page.utils-BryVNYll.js → skills-page.utils-DYfaLtts.js} +1 -1
  76. package/dist/gateway/static/root/assets/{system-settings-panel-mKFDe6aZ.js → system-settings-panel-CWrWpKV-.js} +1 -1
  77. package/dist/gateway/static/root/assets/{task-detail-page-Cb0RZNcJ.js → task-detail-page-DiPgMmBM.js} +1 -1
  78. package/dist/gateway/static/root/assets/{theme-store-Dc5s20V3.js → theme-store-CDc7Wqy5.js} +1 -1
  79. package/dist/gateway/static/root/assets/{tunnel-api-FcI3wKy3.js → tunnel-api-Bmsy-5Fs.js} +1 -1
  80. package/dist/gateway/static/root/assets/{use-autosave-CDgdtjTC.js → use-autosave-D-FRib85.js} +1 -1
  81. package/dist/gateway/static/root/assets/{user-model-page-Cnshc-Un.js → user-model-page-DxRaRdA0.js} +1 -1
  82. package/dist/gateway/static/root/assets/{voice-api-key-field-Ce8cb5mN.js → voice-api-key-field-iQ7EBzII.js} +1 -1
  83. package/dist/gateway/static/root/assets/{voice-config-api-Mk78BrpP.js → voice-config-api-DNMWA2tx.js} +1 -1
  84. package/dist/gateway/static/root/assets/{work-discovery-overlay-CacFTtXe.js → work-discovery-overlay-BesuhDlH.js} +1 -1
  85. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-V-5HuEUq.js → workflow-run-setup-panel-Du81FoCy.js} +1 -1
  86. package/dist/gateway/static/root/assets/{workflows-page-1OJimIfo.js → workflows-page-CZikdgNw.js} +1 -1
  87. package/dist/gateway/static/root/index.html +9 -9
  88. package/dist/package.js +1 -1
  89. package/dist/src/agent/agent-manager.js +4 -4
  90. package/dist/src/agent/background-review/run-background-review.js +1 -1
  91. package/dist/src/agent/embedded/model-runtime.js +1 -1
  92. package/dist/src/agent/embedded/run-for-session.js +1 -1
  93. package/dist/src/agent/image/tool-model-config.js +1 -1
  94. package/dist/src/agent/mcp/oauth/mcp-oauth-store.js +1 -1
  95. package/dist/src/agent/service.js +2 -2
  96. package/dist/src/agent/session/session-inspector.js +1 -1
  97. package/dist/src/agent/skills/hub-lock.js +1 -1
  98. package/dist/src/agent/tasks/task-judge-service.js +3 -3
  99. package/dist/src/agent/tools/xopc-use-tool.js +2 -2
  100. package/dist/src/ai-assist/text-assist.js +1 -1
  101. package/dist/src/auth/credentials.js +2 -2
  102. package/dist/src/auth/oauth/xopc-cloud.js +1 -1
  103. package/dist/src/auth/profiles/store.js +1 -1
  104. package/dist/src/auth/sync-provider-auth.js +2 -2
  105. package/dist/src/browser/providers/browser-ext-install.js +1 -1
  106. package/dist/src/cli/command-loaders.js +1 -0
  107. package/dist/src/cli/command-manifest.js +4 -0
  108. package/dist/src/cli/commands/agent.js +2 -2
  109. package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
  110. package/dist/src/cli/commands/doctor/checks/provider-auth.js +3 -3
  111. package/dist/src/cli/commands/gateway/lifecycle.js +1 -1
  112. package/dist/src/cli/commands/import.d.ts +2 -0
  113. package/dist/src/cli/commands/import.js +30 -0
  114. package/dist/src/cli/commands/models.js +2 -2
  115. package/dist/src/cli/commands/onboard.js +1 -1
  116. package/dist/src/cli/commands/platform.js +1 -1
  117. package/dist/src/cli/commands/profile.js +1 -1
  118. package/dist/src/cli/commands/tunnel.js +1 -1
  119. package/dist/src/cli/commands/update.js +1 -1
  120. package/dist/src/cli/extension-cli-register.js +1 -1
  121. package/dist/src/config/index.js +5 -5
  122. package/dist/src/config/loader.js +2 -2
  123. package/dist/src/config/models-json.js +1 -1
  124. package/dist/src/extensions/lockfile.js +1 -1
  125. package/dist/src/gateway/agents-admin.js +1 -1
  126. package/dist/src/gateway/file-path-classifier.js +1 -1
  127. package/dist/src/gateway/hono/lib/config-payload.js +1 -1
  128. package/dist/src/gateway/hono/routes/imports.d.ts +3 -0
  129. package/dist/src/gateway/hono/routes/imports.js +44 -0
  130. package/dist/src/gateway/hono/routes/lazy-bundles.js +8 -0
  131. package/dist/src/gateway/hono/routes/models.js +1 -1
  132. package/dist/src/gateway/hono/routes/workflows.js +2 -2
  133. package/dist/src/gateway/run-loop.js +1 -1
  134. package/dist/src/gateway/service/config-coordinator.js +1 -1
  135. package/dist/src/gateway/service.js +9 -7
  136. package/dist/src/imports/compatibility.d.ts +7 -0
  137. package/dist/src/imports/compatibility.js +174 -0
  138. package/dist/src/imports/contextImport.d.ts +3 -0
  139. package/dist/src/imports/contextImport.js +56 -0
  140. package/dist/src/imports/files.d.ts +13 -0
  141. package/dist/src/imports/files.js +77 -0
  142. package/dist/src/imports/importService.d.ts +45 -0
  143. package/dist/src/imports/importService.js +417 -0
  144. package/dist/src/imports/planner.d.ts +11 -0
  145. package/dist/src/imports/planner.js +98 -0
  146. package/dist/src/imports/productImport.d.ts +10 -0
  147. package/dist/src/imports/productImport.js +148 -0
  148. package/dist/src/imports/runtime.d.ts +5 -0
  149. package/dist/src/imports/runtime.js +56 -0
  150. package/dist/src/imports/scanner.d.ts +14 -0
  151. package/dist/src/imports/scanner.js +159 -0
  152. package/dist/src/imports/secrets.d.ts +2 -0
  153. package/dist/src/imports/secrets.js +9 -0
  154. package/dist/src/imports/sources.d.ts +37 -0
  155. package/dist/src/imports/sources.js +75 -0
  156. package/dist/src/imports/types.d.ts +91 -0
  157. package/dist/src/imports/types.js +11 -0
  158. package/dist/src/knowledge-memory/repository.js +4 -2
  159. package/dist/src/providers/auth-runtime/resolve-auth.js +1 -1
  160. package/dist/src/providers/index.js +2 -2
  161. package/dist/src/providers/model-registry.js +1 -1
  162. package/dist/src/providers/register-runtime-providers.js +1 -1
  163. package/dist/src/storage/sqlite/import-repository.d.ts +19 -0
  164. package/dist/src/storage/sqlite/import-repository.js +35 -0
  165. package/dist/src/storage/sqlite/migrations/174_capability_imports.sql +11 -0
  166. package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
  167. package/dist/src/storage/sqlite/migrations/runner.js +3 -3
  168. package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
  169. package/dist/src/storage/sqlite/schema.js +1 -1
  170. package/dist/src/tasks/home-query-service.js +2 -2
  171. package/dist/src/tui/tui-startup-resources.js +1 -1
  172. package/dist/src/tui/tui.js +1 -1
  173. package/dist/src/work-discovery/service.js +2 -2
  174. package/dist/src/workflows/service/workflow-run-service.js +2 -2
  175. package/package.json +4 -3
  176. package/dist/gateway/static/root/assets/attachment-process-heavy-C5CqLCsX.js +0 -5
  177. package/dist/gateway/static/root/assets/binary-plugins-CWIswfXA.js +0 -2
  178. package/dist/gateway/static/root/assets/excel-worksheet-utils-DpdB-DjU.js +0 -1
  179. package/dist/gateway/static/root/assets/messages-DYFcjtmG.js +0 -3
  180. package/dist/gateway/static/root/assets/pdf.worker.min-CHFwMXne.mjs +0 -29
  181. package/dist/gateway/static/root/assets/settings-advanced-gate-CFuqbGI-.js +0 -1
  182. package/dist/gateway/static/root/assets/share-preview-page-vz0UK56q.js +0 -2
  183. package/dist/gateway/static/root/assets/skill-reload-api-C3D8WanU.js +0 -1
  184. package/dist/gateway/static/root/assets/skills-page-CxMWwR6A.js +0 -2
@@ -0,0 +1,417 @@
1
+ import { createLogger } from "../utils/logger/index.js";
2
+ import { init_logger } from "../utils/logger.js";
3
+ import { init_write_file_atomic, writeTextAtomicSync } from "../infra/write-file-atomic.js";
4
+ import { prepareManagedSkillTempDir } from "../agent/skills/managed-store.js";
5
+ import { ImportError } from "./types.js";
6
+ import { fileHash, readTree } from "./files.js";
7
+ import { parseSkill } from "./compatibility.js";
8
+ import { ImportRepository } from "../storage/sqlite/import-repository.js";
9
+ import { canonicalTarget, createImportPlan, targetInventory, targetRevision } from "./planner.js";
10
+ import { scanLocal } from "./scanner.js";
11
+ import { dirname, join } from "node:path";
12
+ import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
13
+ import { randomUUID } from "node:crypto";
14
+ import yaml from "js-yaml";
15
+ import lockfile from "proper-lockfile";
16
+ //#region src/imports/importService.ts
17
+ init_write_file_atomic();
18
+ init_logger();
19
+ const log = createLogger("ImportService");
20
+ const RETENTION_MS = 720 * 60 * 60 * 1e3;
21
+ var ImportService = class {
22
+ repo;
23
+ root;
24
+ constructor(options) {
25
+ this.options = options;
26
+ this.repo = new ImportRepository(options.owner);
27
+ this.root = join(canonicalTarget(options.stateDir), "imports");
28
+ mkdirSync(this.root, {
29
+ recursive: true,
30
+ mode: 448
31
+ });
32
+ }
33
+ scan(input) {
34
+ return this.storeScan(scanLocal(input));
35
+ }
36
+ getScan(id) {
37
+ return this.repo.get("scan", id);
38
+ }
39
+ storeScan(scan) {
40
+ const dir = join(this.root, scan.id);
41
+ mkdirSync(dir, { mode: 448 });
42
+ for (const item of scan.candidates) if (item.kind === "skill" && item.compatibility === "compatible") writeTextAtomicSync(join(dir, `${item.id}.json`), JSON.stringify(item.files));
43
+ const manifest = {
44
+ ...scan,
45
+ candidates: scan.candidates.map((c) => ({
46
+ ...c,
47
+ connected: this.options.isConnected?.(c) ?? false,
48
+ files: c.files.map((f) => ({
49
+ ...f,
50
+ data: ""
51
+ }))
52
+ }))
53
+ };
54
+ this.repo.save("scan", manifest);
55
+ return manifest;
56
+ }
57
+ plan(scanId, target, choices) {
58
+ this.options.validateTarget?.(target);
59
+ const plan = createImportPlan(this.getScan(scanId), target, choices, this.options.reservedNames?.(target));
60
+ this.repo.save("plan", plan);
61
+ return plan;
62
+ }
63
+ async importSkills(scan, target) {
64
+ const inventory = targetInventory(target.root);
65
+ const names = new Set([...this.options.reservedNames?.(target) ?? [], ...inventory.flatMap((i) => [i.directory, i.name])].map((n) => n.toLowerCase()));
66
+ const choices = [];
67
+ const result = {
68
+ imported: 0,
69
+ skipped: 0,
70
+ issues: []
71
+ };
72
+ const previous = /* @__PURE__ */ new Map();
73
+ for (const job of this.repo.jobs(-1)) {
74
+ if (job.plan.target.root !== canonicalTarget(target.root)) continue;
75
+ const origin = this.getScan(job.plan.scanId);
76
+ for (const item of job.items.filter((i) => i.status === "active")) {
77
+ const candidate = origin.candidates.find((c) => c.id === item.action.candidateId);
78
+ if (candidate?.source === scan.source && !previous.has(candidate.location)) previous.set(candidate.location, { name: item.action.name });
79
+ }
80
+ }
81
+ for (const candidate of scan.candidates.filter((c) => c.kind === "skill")) {
82
+ if (candidate.connected) {
83
+ result.skipped++;
84
+ continue;
85
+ }
86
+ if (candidate.compatibility !== "compatible") {
87
+ result.issues.push({
88
+ name: candidate.name,
89
+ reason: candidate.findings.join("; ")
90
+ });
91
+ continue;
92
+ }
93
+ const prior = previous.get(candidate.location);
94
+ const priorTarget = prior && inventory.find((i) => i.directory === prior.name);
95
+ if (prior && priorTarget) {
96
+ const files = JSON.parse(readFileSync(join(this.root, scan.id, `${candidate.id}.json`), "utf8"));
97
+ if (fileHash(this.renameSkill(files, prior.name)) === priorTarget.hash) result.skipped++;
98
+ else result.issues.push({
99
+ name: candidate.name,
100
+ reason: "The previously imported skill has changed. Existing content was kept."
101
+ });
102
+ continue;
103
+ }
104
+ if (inventory.find((i) => i.name.toLowerCase() === candidate.name.toLowerCase())?.hash === candidate.hash) {
105
+ result.skipped++;
106
+ continue;
107
+ }
108
+ let name = candidate.name;
109
+ if (names.has(name.toLowerCase())) name = `${candidate.source}-${candidate.name}`.slice(0, 63);
110
+ const files = JSON.parse(readFileSync(join(this.root, scan.id, `${candidate.id}.json`), "utf8"));
111
+ const hash = fileHash(this.renameSkill(files, name));
112
+ if (inventory.some((i) => i.name === name && i.hash === hash)) {
113
+ result.skipped++;
114
+ continue;
115
+ }
116
+ if (names.has(name.toLowerCase())) {
117
+ result.issues.push({
118
+ name: candidate.name,
119
+ reason: "An existing skill was kept. Manage it in Skills to make changes."
120
+ });
121
+ continue;
122
+ }
123
+ names.add(name.toLowerCase());
124
+ choices.push({
125
+ candidateId: candidate.id,
126
+ operation: name === candidate.name ? "create" : "rename",
127
+ name
128
+ });
129
+ }
130
+ if (!choices.length) return result;
131
+ const plan = this.plan(scan.id, target, choices);
132
+ const staged = this.apply(plan.id, plan.id);
133
+ const job = await this.activate(staged.id, staged.items.filter((i) => i.status === "staged").map((i) => i.action.candidateId));
134
+ for (const item of job.items) if (item.status === "active") result.imported++;
135
+ else if (item.status === "skipped") result.skipped++;
136
+ else result.issues.push({
137
+ name: item.action.name,
138
+ reason: item.error ?? "Could not import this skill. Try again."
139
+ });
140
+ return result;
141
+ }
142
+ getPlan(id) {
143
+ return this.repo.get("plan", id);
144
+ }
145
+ apply(planId, key) {
146
+ if (!key || key.length > 128) throw new ImportError("invalid_key", "An idempotency key is required");
147
+ return this.lock(() => {
148
+ const previous = this.repo.byKey(key);
149
+ if (previous) {
150
+ if (previous.plan.id !== planId) throw new ImportError("conflict", "Idempotency key belongs to a different plan", 409);
151
+ return previous;
152
+ }
153
+ const plan = this.getPlan(planId);
154
+ this.options.validateTarget?.(plan.target);
155
+ if (Date.now() > plan.expiresAt || canonicalTarget(plan.target.root) !== plan.target.root || targetRevision(plan.target.root) !== plan.targetRevision) throw new ImportError("plan_stale", "Target changed or plan expired; preview again", 409);
156
+ const job = {
157
+ id: randomUUID(),
158
+ plan,
159
+ createdAt: Date.now(),
160
+ expiresAt: Date.now() + RETENTION_MS,
161
+ items: plan.actions.map((action) => ({
162
+ action,
163
+ status: action.operation === "skip" ? "skipped" : "staged"
164
+ }))
165
+ };
166
+ const dir = join(this.root, job.id);
167
+ mkdirSync(dir, { mode: 448 });
168
+ const scan = this.getScan(plan.scanId);
169
+ try {
170
+ for (const item of job.items) {
171
+ if (item.status === "skipped") continue;
172
+ const candidate = scan.candidates.find((c) => c.id === item.action.candidateId);
173
+ if (candidate.kind !== "skill") continue;
174
+ const files = JSON.parse(readFileSync(join(this.root, scan.id, `${candidate.id}.json`), "utf8"));
175
+ if (fileHash(files) !== candidate.hash) throw new ImportError("source_changed", "Snapshot changed; scan again", 409);
176
+ const rewritten = this.renameSkill(files, item.action.name);
177
+ item.afterHash = fileHash(rewritten);
178
+ writeTextAtomicSync(join(dir, `${candidate.id}.json`), JSON.stringify(rewritten));
179
+ }
180
+ this.repo.save("job", job, key);
181
+ } catch (error) {
182
+ rmSync(dir, {
183
+ recursive: true,
184
+ force: true
185
+ });
186
+ throw error;
187
+ }
188
+ log.info({
189
+ jobId: job.id,
190
+ count: job.items.length
191
+ }, "Import staged");
192
+ return job;
193
+ });
194
+ }
195
+ renameSkill(files, name) {
196
+ return files.map((file) => {
197
+ if (file.path !== "SKILL.md") return file;
198
+ const { metadata, body } = parseSkill(Buffer.from(file.data, "base64").toString("utf8"));
199
+ if (metadata.name === name) return file;
200
+ return {
201
+ ...file,
202
+ data: Buffer.from(`---\n${yaml.dump({
203
+ ...metadata,
204
+ name
205
+ })}---\n${body}`).toString("base64")
206
+ };
207
+ });
208
+ }
209
+ getJob(id) {
210
+ return this.lock(() => this.recover(this.repo.get("job", id)));
211
+ }
212
+ async recoverAndClean() {
213
+ const release = await lockfile.lock(this.root, {
214
+ realpath: false,
215
+ retries: {
216
+ retries: 15,
217
+ minTimeout: 250,
218
+ maxTimeout: 1e3
219
+ }
220
+ });
221
+ try {
222
+ for (const saved of this.repo.jobs(-1)) {
223
+ const job = this.recover(saved);
224
+ if (Date.now() <= job.expiresAt || canonicalTarget(job.plan.target.root) !== job.plan.target.root) continue;
225
+ for (const item of job.items) if (item.backupPath && existsSync(item.backupPath) && this.targetHash(item.backupPath) === item.action.beforeHash) rmSync(item.backupPath, {
226
+ recursive: true,
227
+ force: true
228
+ });
229
+ rmSync(join(this.root, job.id), {
230
+ recursive: true,
231
+ force: true
232
+ });
233
+ }
234
+ for (const scan of this.repo.scansBefore(Date.now() - RETENTION_MS - 1440 * 60 * 1e3)) rmSync(join(this.root, scan.id), {
235
+ recursive: true,
236
+ force: true
237
+ });
238
+ } finally {
239
+ await release();
240
+ }
241
+ }
242
+ history() {
243
+ return this.lock(() => this.repo.jobs().map((job) => this.recover(job)));
244
+ }
245
+ async activate(id, candidateIds) {
246
+ const job = this.lock(() => {
247
+ const current = this.recover(this.repo.get("job", id));
248
+ this.assertLive(current);
249
+ this.options.validateTarget?.(current.plan.target);
250
+ const scan = this.getScan(current.plan.scanId);
251
+ this.checkSelection(current, candidateIds);
252
+ for (const candidateId of candidateIds) {
253
+ const item = current.items.find((i) => i.action.candidateId === candidateId);
254
+ if (item.status === "active") continue;
255
+ if (!["staged", "failed"].includes(item.status)) throw new ImportError("invalid_state", "Item cannot be activated");
256
+ if (scan.candidates.find((c) => c.id === candidateId).kind !== "skill") throw new ImportError("needs_setup", "Review the MCP draft in connection settings before configuring it");
257
+ try {
258
+ this.publish(current, item);
259
+ } catch (error) {
260
+ if (item.status !== "publishing") item.status = "failed";
261
+ item.error = error instanceof ImportError ? error.message : "Publishing failed; retry after checking target permissions";
262
+ }
263
+ this.repo.save("job", current);
264
+ }
265
+ return current;
266
+ });
267
+ await this.options.refreshSkills?.();
268
+ return job;
269
+ }
270
+ publish(job, item) {
271
+ const root = job.plan.target.root;
272
+ if (canonicalTarget(root) !== root) throw new ImportError("unsafe_target", "Target location changed", 409);
273
+ const dest = join(root, item.action.name);
274
+ if (this.targetHash(dest) !== item.action.beforeHash) throw new ImportError("plan_stale", "Target changed after preview", 409);
275
+ const others = this.options.reservedNames?.(job.plan.target) ?? [];
276
+ if (item.action.operation !== "replace" && others.some((name) => name.toLowerCase() === item.action.name.toLowerCase())) throw new ImportError("conflict", "Skill name is now in use", 409);
277
+ const files = JSON.parse(readFileSync(join(this.root, job.id, `${item.action.candidateId}.json`), "utf8"));
278
+ if (fileHash(files) !== item.afterHash) throw new ImportError("source_changed", "Staged files changed", 409);
279
+ const { tempDir } = prepareManagedSkillTempDir(item.action.name, root);
280
+ try {
281
+ for (const file of files) {
282
+ const path = join(tempDir, file.path);
283
+ mkdirSync(dirname(path), {
284
+ recursive: true,
285
+ mode: 448
286
+ });
287
+ writeFileSync(path, Buffer.from(file.data, "base64"), {
288
+ mode: file.executable ? 448 : 384,
289
+ flag: "wx"
290
+ });
291
+ }
292
+ if (fileHash(readTree(tempDir)) !== item.afterHash) throw new ImportError("source_changed", "Staged verification failed", 409);
293
+ item.backupPath = join(root, `.tmp-import-backup-${job.id}-${item.action.candidateId}`);
294
+ item.status = "publishing";
295
+ delete item.error;
296
+ this.repo.save("job", job);
297
+ if (existsSync(dest)) renameSync(dest, item.backupPath);
298
+ renameSync(tempDir, dest);
299
+ item.status = "active";
300
+ } catch (error) {
301
+ if (item.backupPath && existsSync(item.backupPath) && !existsSync(dest)) renameSync(item.backupPath, dest);
302
+ throw error;
303
+ } finally {
304
+ rmSync(tempDir, {
305
+ recursive: true,
306
+ force: true
307
+ });
308
+ }
309
+ }
310
+ async rollback(id, candidateIds) {
311
+ const job = this.lock(() => {
312
+ const current = this.recover(this.repo.get("job", id));
313
+ this.assertLive(current);
314
+ this.checkSelection(current, candidateIds);
315
+ for (const candidateId of candidateIds) {
316
+ const item = current.items.find((i) => i.action.candidateId === candidateId);
317
+ if (["rolled_back", "skipped"].includes(item.status)) continue;
318
+ if (item.status === "staged" || item.status === "failed") {
319
+ item.status = "rolled_back";
320
+ this.repo.save("job", current);
321
+ continue;
322
+ }
323
+ const dest = join(current.plan.target.root, item.action.name);
324
+ if (canonicalTarget(current.plan.target.root) !== current.plan.target.root || this.targetHash(dest) !== item.afterHash) {
325
+ item.error = "Target changed after import; rollback would overwrite your edits";
326
+ this.repo.save("job", current);
327
+ continue;
328
+ }
329
+ if (item.action.beforeHash !== null && (!item.backupPath || this.targetHash(item.backupPath) !== item.action.beforeHash)) {
330
+ item.error = "Backup is missing or changed; manual review required";
331
+ this.repo.save("job", current);
332
+ continue;
333
+ }
334
+ item.status = "rolling_back";
335
+ this.repo.save("job", current);
336
+ const trash = this.trashPath(current, item);
337
+ renameSync(dest, trash);
338
+ if (item.backupPath && existsSync(item.backupPath)) {
339
+ if (this.targetHash(item.backupPath) !== item.action.beforeHash) throw new ImportError("rollback_conflict", "Backup changed; manual review required", 409);
340
+ renameSync(item.backupPath, dest);
341
+ }
342
+ item.status = "rolled_back";
343
+ delete item.error;
344
+ this.repo.save("job", current);
345
+ rmSync(trash, {
346
+ recursive: true,
347
+ force: true
348
+ });
349
+ }
350
+ return current;
351
+ });
352
+ await this.options.refreshSkills?.();
353
+ return job;
354
+ }
355
+ trashPath(job, item) {
356
+ return join(job.plan.target.root, `.tmp-import-undo-${job.id}-${item.action.candidateId}`);
357
+ }
358
+ recover(job) {
359
+ for (const item of job.items) {
360
+ if (!["publishing", "rolling_back"].includes(item.status)) continue;
361
+ const root = job.plan.target.root;
362
+ if (canonicalTarget(root) !== root) throw new ImportError("unsafe_target", "Target location changed", 409);
363
+ const dest = join(root, item.action.name);
364
+ const hash = this.targetHash(dest);
365
+ if (item.status === "publishing") if (hash === item.afterHash) item.status = "active";
366
+ else if (hash === item.action.beforeHash) item.status = "staged";
367
+ else if (!hash && item.action.beforeHash !== null && item.backupPath && this.targetHash(item.backupPath) === item.action.beforeHash) {
368
+ renameSync(item.backupPath, dest);
369
+ item.status = "staged";
370
+ } else {
371
+ item.status = "failed";
372
+ item.error = "Publication interrupted and target changed; manual review required";
373
+ }
374
+ else {
375
+ const trash = this.trashPath(job, item);
376
+ if (!hash && item.action.beforeHash !== null && item.backupPath && this.targetHash(item.backupPath) === item.action.beforeHash) renameSync(item.backupPath, dest);
377
+ else if (hash === item.afterHash && !existsSync(trash)) {
378
+ item.status = "active";
379
+ this.repo.save("job", job);
380
+ continue;
381
+ } else if (hash !== item.action.beforeHash) {
382
+ item.error = "Rollback interrupted; target needs review";
383
+ this.repo.save("job", job);
384
+ continue;
385
+ }
386
+ item.status = "rolled_back";
387
+ rmSync(trash, {
388
+ recursive: true,
389
+ force: true
390
+ });
391
+ }
392
+ this.repo.save("job", job);
393
+ }
394
+ return job;
395
+ }
396
+ targetHash(path) {
397
+ if (!existsSync(path)) return null;
398
+ if (canonicalTarget(path) !== path || lstatSync(path).isSymbolicLink()) throw new ImportError("unsafe_target", "Target is a symbolic link", 409);
399
+ return fileHash(readTree(path));
400
+ }
401
+ assertLive(job) {
402
+ if (Date.now() > job.expiresAt) throw new ImportError("expired", "Import recovery period has expired", 409);
403
+ }
404
+ checkSelection(job, ids) {
405
+ if (!ids.length || new Set(ids).size !== ids.length || ids.some((id) => !job.items.some((i) => i.action.candidateId === id))) throw new ImportError("invalid_selection", "Select valid import items");
406
+ }
407
+ lock(run) {
408
+ const release = lockfile.lockSync(this.root, { realpath: false });
409
+ try {
410
+ return run();
411
+ } finally {
412
+ release();
413
+ }
414
+ }
415
+ };
416
+ //#endregion
417
+ export { ImportService };
@@ -0,0 +1,11 @@
1
+ import { type ImportAction, type ImportPlan, type ImportScan, type ImportTarget } from './types.js';
2
+ export declare function targetInventory(root: string): Array<{
3
+ directory: string;
4
+ name: string;
5
+ hash: string;
6
+ }>;
7
+ export declare function targetRevision(root: string): string;
8
+ export declare function canonicalTarget(root: string): string;
9
+ export declare function createImportPlan(scan: ImportScan, target: ImportTarget, choices: Array<Pick<ImportAction, 'candidateId' | 'operation'> & {
10
+ name?: string;
11
+ }>, reservedNames?: string[]): ImportPlan;
@@ -0,0 +1,98 @@
1
+ import { isValidSkillId } from "../agent/skills/managed-store.js";
2
+ import { ImportError } from "./types.js";
3
+ import { IMPORT_LIMITS, digest, fileHash, readTree } from "./files.js";
4
+ import { parseSkill } from "./compatibility.js";
5
+ import { join, relative, resolve } from "node:path";
6
+ import { existsSync, readdirSync, realpathSync } from "node:fs";
7
+ import { randomUUID } from "node:crypto";
8
+ //#region src/imports/planner.ts
9
+ function targetInventory(root) {
10
+ if (!existsSync(root)) return [];
11
+ let bytes = 0;
12
+ let count = 0;
13
+ return readdirSync(root, { withFileTypes: true }).filter((e) => !e.name.startsWith(".")).map((entry) => {
14
+ if (!entry.isDirectory() || entry.isSymbolicLink()) throw new ImportError("unsafe_target", "Target contains a link or non-directory item", 409);
15
+ const files = readTree(join(root, entry.name));
16
+ bytes += files.reduce((n, f) => n + Buffer.byteLength(f.data, "base64"), 0);
17
+ count += files.length;
18
+ if (bytes > IMPORT_LIMITS.total || count > IMPORT_LIMITS.count) throw new ImportError("limit_exceeded", "Target inventory exceeds import limits", 413);
19
+ const primary = files.find((f) => f.path === "SKILL.md");
20
+ let name = entry.name;
21
+ try {
22
+ if (primary) name = String(parseSkill(Buffer.from(primary.data, "base64").toString("utf8")).metadata.name);
23
+ } catch {}
24
+ return {
25
+ directory: entry.name,
26
+ name,
27
+ hash: fileHash(files)
28
+ };
29
+ }).sort((a, b) => a.directory.localeCompare(b.directory));
30
+ }
31
+ function targetRevision(root) {
32
+ return digest(targetInventory(root));
33
+ }
34
+ function canonicalTarget(root) {
35
+ let current = resolve(root);
36
+ while (!existsSync(current)) {
37
+ const parent = resolve(current, "..");
38
+ if (parent === current) break;
39
+ current = parent;
40
+ }
41
+ return join(realpathSync(current), relative(current, resolve(root)));
42
+ }
43
+ function createImportPlan(scan, target, choices, reservedNames = []) {
44
+ if (Date.now() - scan.createdAt > 1440 * 60 * 1e3) throw new ImportError("plan_stale", "Scan expired; scan again", 409);
45
+ target = {
46
+ ...target,
47
+ root: canonicalTarget(target.root)
48
+ };
49
+ const inventory = targetInventory(target.root);
50
+ const used = /* @__PURE__ */ new Set();
51
+ const candidates = /* @__PURE__ */ new Set();
52
+ const normalize = (s) => s.normalize("NFC").toLowerCase();
53
+ const actions = choices.map((choice) => {
54
+ const item = scan.candidates.find((c) => c.id === choice.candidateId);
55
+ if (!item || candidates.has(item.id)) throw new ImportError("invalid_selection", "Invalid or duplicate candidate");
56
+ candidates.add(item.id);
57
+ const name = choice.name ?? item.name;
58
+ if (!isValidSkillId(name)) throw new ImportError("invalid_name", "Choose a name using letters, numbers, dot, dash or underscore");
59
+ if (choice.operation !== "skip" && !["compatible", "needs_setup"].includes(item.compatibility)) throw new ImportError("incompatible", "Selected item requires manual adaptation");
60
+ if (item.scope === "project" && !target.projectId && choice.operation !== "skip") throw new ImportError("scope_mismatch", "Project capabilities must remain project-scoped");
61
+ if (item.kind !== "skill" && choice.operation !== "skip" && choice.operation !== "create") throw new ImportError("invalid_operation", "MCP drafts only support create");
62
+ const existing = item.kind === "skill" ? inventory.find((e) => normalize(e.directory) === normalize(name) || normalize(e.name) === normalize(name)) : void 0;
63
+ const hash = existing?.hash ?? null;
64
+ if (choice.operation !== "skip") {
65
+ const key = `${item.kind}:${normalize(name)}`;
66
+ if (used.has(key)) throw new ImportError("conflict", "Two selected items have the same target name", 409);
67
+ used.add(key);
68
+ if (existing?.hash === item.hash && choice.operation === "create") return {
69
+ candidateId: item.id,
70
+ operation: "skip",
71
+ name,
72
+ beforeHash: hash
73
+ };
74
+ if (choice.operation === "replace") {
75
+ if (!existing || existing.directory !== name || existing.name !== name) throw new ImportError("conflict", "Replace requires an exact managed directory and skill name match", 409);
76
+ } else if (existing || item.kind === "skill" && reservedNames.some((n) => normalize(n) === normalize(name))) throw new ImportError("conflict", "Name is already used; choose another name or explicitly replace a managed skill", 409);
77
+ if (choice.operation === "create" && name !== item.name) throw new ImportError("invalid_operation", "Use rename when changing the skill name");
78
+ }
79
+ return {
80
+ candidateId: item.id,
81
+ operation: choice.operation,
82
+ name,
83
+ beforeHash: hash
84
+ };
85
+ });
86
+ if (!actions.length) throw new ImportError("invalid_selection", "Select at least one item");
87
+ return {
88
+ id: randomUUID(),
89
+ scanId: scan.id,
90
+ createdAt: Date.now(),
91
+ expiresAt: Date.now() + 1440 * 60 * 1e3,
92
+ target,
93
+ targetRevision: digest(inventory),
94
+ actions
95
+ };
96
+ }
97
+ //#endregion
98
+ export { canonicalTarget, createImportPlan, targetInventory, targetRevision };
@@ -0,0 +1,10 @@
1
+ import type { ImportService } from './importService.js';
2
+ import { type ImportSource, type ProductImportResult } from './types.js';
3
+ export declare function listImportSources(owner: string): {
4
+ lastImport: ProductImportResult;
5
+ id: ImportSource;
6
+ name: string;
7
+ detected: boolean;
8
+ }[];
9
+ /** One product action. Destination services retain their existing scope and trust policies. */
10
+ export declare function importProduct(service: ImportService, source: ImportSource, requestId?: string, owner?: string): Promise<ProductImportResult>;
@@ -0,0 +1,148 @@
1
+ import { resolveStateDir } from "../config/paths-state.js";
2
+ import { init_paths } from "../config/paths.js";
3
+ import { ProjectService } from "../projects/project-service.js";
4
+ import { ImportError } from "./types.js";
5
+ import { importContextDocument } from "./contextImport.js";
6
+ import { ImportRepository } from "../storage/sqlite/import-repository.js";
7
+ import { detectImportSources } from "./sources.js";
8
+ import { resolveImportTarget } from "./runtime.js";
9
+ import { isAbsolute, join, parse } from "node:path";
10
+ import { mkdirSync, realpathSync, statSync } from "node:fs";
11
+ import { randomUUID } from "node:crypto";
12
+ import lockfile from "proper-lockfile";
13
+ //#region src/imports/productImport.ts
14
+ init_paths();
15
+ function listImportSources(owner) {
16
+ const runs = new ImportRepository(owner).latestRuns();
17
+ return detectImportSources().map((source) => ({
18
+ ...source,
19
+ lastImport: runs.find((r) => r.source === source.id)
20
+ }));
21
+ }
22
+ /** One product action. Destination services retain their existing scope and trust policies. */
23
+ async function importProduct(service, source, requestId = randomUUID(), owner = "gateway-owner") {
24
+ const root = join(resolveStateDir(), "imports", "product-import");
25
+ mkdirSync(root, {
26
+ recursive: true,
27
+ mode: 448
28
+ });
29
+ const release = await lockfile.lock(root, {
30
+ realpath: false,
31
+ retries: {
32
+ retries: 10,
33
+ minTimeout: 100,
34
+ maxTimeout: 500
35
+ }
36
+ });
37
+ const repo = new ImportRepository(owner);
38
+ try {
39
+ try {
40
+ const previous = repo.get("run", requestId);
41
+ if (previous.source !== source) throw new ImportError("conflict", "Request belongs to another product", 409);
42
+ return previous;
43
+ } catch (error) {
44
+ if (!(error instanceof ImportError) || error.code !== "not_found") throw error;
45
+ }
46
+ if (!detectImportSources().some((s) => s.id === source && s.detected)) throw new ImportError("source_not_found", "This app is no longer available on the xopc device", 404);
47
+ const result = {
48
+ id: requestId,
49
+ source,
50
+ createdAt: Date.now(),
51
+ skills: 0,
52
+ context: 0,
53
+ projects: 0,
54
+ skipped: 0,
55
+ issues: []
56
+ };
57
+ const budget = {
58
+ bytes: 0,
59
+ files: 0
60
+ };
61
+ const scan = service.scan({
62
+ source,
63
+ budget
64
+ });
65
+ const saveContext = (items, projectId) => {
66
+ for (const item of items.filter((c) => c.kind === "rule" && c.scope === (projectId ? "project" : "user"))) {
67
+ if (!item.content || item.compatibility !== "compatible") {
68
+ result.issues.push({
69
+ name: item.name,
70
+ reason: item.findings.join("; ")
71
+ });
72
+ continue;
73
+ }
74
+ try {
75
+ const status = importContextDocument(item, projectId);
76
+ if (status === "created") result.context++;
77
+ else if (status === "unchanged") result.skipped++;
78
+ else result.issues.push({
79
+ name: item.name,
80
+ reason: "Existing context was kept. The source document has changed."
81
+ });
82
+ } catch {
83
+ result.issues.push({
84
+ name: item.name,
85
+ reason: "Unable to save this context document. Try again."
86
+ });
87
+ }
88
+ }
89
+ };
90
+ const importScope = async (scopeScan, projectId) => {
91
+ const scoped = {
92
+ ...scopeScan,
93
+ candidates: scopeScan.candidates.filter((c) => c.scope === (projectId ? "project" : "user"))
94
+ };
95
+ result.issues.push(...scopeScan.diagnostics.map((reason) => ({
96
+ name: source,
97
+ reason
98
+ })));
99
+ saveContext(scoped.candidates, projectId);
100
+ for (const item of scoped.candidates.filter((c) => c.kind === "mcp")) result.issues.push({
101
+ name: item.name,
102
+ reason: "Reconnect this tool in Connections; credentials and executable settings are not transferred."
103
+ });
104
+ try {
105
+ const skills = await service.importSkills(scoped, resolveImportTarget(projectId));
106
+ result.skills += skills.imported;
107
+ result.skipped += skills.skipped;
108
+ result.issues.push(...skills.issues);
109
+ } catch (error) {
110
+ result.issues.push({
111
+ name: projectId ? "Project skills" : "Skills",
112
+ reason: error instanceof ImportError ? error.message : "Unable to import skills. Try again."
113
+ });
114
+ }
115
+ };
116
+ await importScope(scan);
117
+ const projects = new ProjectService();
118
+ const seen = /* @__PURE__ */ new Set();
119
+ for (const path of scan.projectRoots ?? []) try {
120
+ if (!isAbsolute(path) || parse(path).root === path || !statSync(path).isDirectory()) throw new Error("Invalid project");
121
+ const workspaceRoot = realpathSync(path);
122
+ if (seen.has(workspaceRoot)) continue;
123
+ seen.add(workspaceRoot);
124
+ let project = projects.findByWorkspaceRoot(workspaceRoot);
125
+ if (!project) {
126
+ project = projects.create({ workspaceRoot });
127
+ result.projects++;
128
+ } else result.skipped++;
129
+ await importScope(service.scan({
130
+ source,
131
+ projectRoot: workspaceRoot,
132
+ projectOnly: true,
133
+ budget
134
+ }), project.id);
135
+ } catch {
136
+ result.issues.push({
137
+ name: path,
138
+ reason: "Project unavailable; existing content was kept."
139
+ });
140
+ }
141
+ repo.save("run", result);
142
+ return result;
143
+ } finally {
144
+ await release();
145
+ }
146
+ }
147
+ //#endregion
148
+ export { importProduct, listImportSources };