@aigne/afs-cli 1.11.0-beta.11 → 1.11.0-beta.13

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 (157) hide show
  1. package/dist/cli.cjs +3 -2
  2. package/dist/cli.mjs +3 -2
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/config/afs-loader.cjs +36 -315
  5. package/dist/config/afs-loader.d.cts.map +1 -1
  6. package/dist/config/afs-loader.d.mts +2 -1
  7. package/dist/config/afs-loader.d.mts.map +1 -1
  8. package/dist/config/afs-loader.mjs +28 -307
  9. package/dist/config/afs-loader.mjs.map +1 -1
  10. package/dist/config/credential-helpers.cjs +303 -0
  11. package/dist/config/credential-helpers.d.mts +2 -0
  12. package/dist/config/credential-helpers.mjs +300 -0
  13. package/dist/config/credential-helpers.mjs.map +1 -0
  14. package/dist/config/loader.cjs +3 -1
  15. package/dist/config/loader.mjs +3 -2
  16. package/dist/config/loader.mjs.map +1 -1
  17. package/dist/config/program-install.cjs +450 -0
  18. package/dist/config/program-install.d.mts +1 -0
  19. package/dist/config/program-install.mjs +444 -0
  20. package/dist/config/program-install.mjs.map +1 -0
  21. package/dist/core/commands/connect.cjs +53 -0
  22. package/dist/core/commands/connect.d.mts +2 -0
  23. package/dist/core/commands/connect.mjs +55 -0
  24. package/dist/core/commands/connect.mjs.map +1 -0
  25. package/dist/core/commands/daemon.cjs +211 -0
  26. package/dist/core/commands/daemon.d.mts +2 -0
  27. package/dist/core/commands/daemon.mjs +212 -0
  28. package/dist/core/commands/daemon.mjs.map +1 -0
  29. package/dist/core/commands/explain.cjs +3 -1
  30. package/dist/core/commands/explain.mjs +3 -1
  31. package/dist/core/commands/explain.mjs.map +1 -1
  32. package/dist/core/commands/explore.cjs +47 -12
  33. package/dist/core/commands/explore.mjs +47 -12
  34. package/dist/core/commands/explore.mjs.map +1 -1
  35. package/dist/core/commands/gen-agent-md.cjs +126 -0
  36. package/dist/core/commands/gen-agent-md.d.mts +2 -0
  37. package/dist/core/commands/gen-agent-md.mjs +125 -0
  38. package/dist/core/commands/gen-agent-md.mjs.map +1 -0
  39. package/dist/core/commands/index.cjs +13 -1
  40. package/dist/core/commands/index.d.cts.map +1 -1
  41. package/dist/core/commands/index.d.mts +6 -0
  42. package/dist/core/commands/index.d.mts.map +1 -1
  43. package/dist/core/commands/index.mjs +13 -1
  44. package/dist/core/commands/index.mjs.map +1 -1
  45. package/dist/core/commands/install.cjs +139 -0
  46. package/dist/core/commands/install.d.mts +2 -0
  47. package/dist/core/commands/install.mjs +140 -0
  48. package/dist/core/commands/install.mjs.map +1 -0
  49. package/dist/core/commands/ls.cjs +14 -2
  50. package/dist/core/commands/ls.d.cts +2 -0
  51. package/dist/core/commands/ls.d.cts.map +1 -1
  52. package/dist/core/commands/ls.d.mts +2 -0
  53. package/dist/core/commands/ls.d.mts.map +1 -1
  54. package/dist/core/commands/ls.mjs +14 -2
  55. package/dist/core/commands/ls.mjs.map +1 -1
  56. package/dist/core/commands/mcp-bridge.cjs +201 -0
  57. package/dist/core/commands/mcp-bridge.d.mts +2 -0
  58. package/dist/core/commands/mcp-bridge.mjs +201 -0
  59. package/dist/core/commands/mcp-bridge.mjs.map +1 -0
  60. package/dist/core/commands/read.cjs +20 -7
  61. package/dist/core/commands/read.d.cts +2 -0
  62. package/dist/core/commands/read.d.cts.map +1 -1
  63. package/dist/core/commands/read.d.mts +2 -0
  64. package/dist/core/commands/read.d.mts.map +1 -1
  65. package/dist/core/commands/read.mjs +20 -7
  66. package/dist/core/commands/read.mjs.map +1 -1
  67. package/dist/core/commands/search.cjs +5 -1
  68. package/dist/core/commands/search.mjs +5 -1
  69. package/dist/core/commands/search.mjs.map +1 -1
  70. package/dist/core/commands/stat.mjs.map +1 -1
  71. package/dist/core/commands/types.d.cts +2 -0
  72. package/dist/core/commands/types.d.cts.map +1 -1
  73. package/dist/core/commands/types.d.mts +2 -0
  74. package/dist/core/commands/types.d.mts.map +1 -1
  75. package/dist/core/commands/types.mjs.map +1 -1
  76. package/dist/core/commands/vault.cjs +289 -0
  77. package/dist/core/commands/vault.d.mts +2 -0
  78. package/dist/core/commands/vault.mjs +289 -0
  79. package/dist/core/commands/vault.mjs.map +1 -0
  80. package/dist/core/commands/write.cjs +19 -6
  81. package/dist/core/commands/write.d.cts +2 -1
  82. package/dist/core/commands/write.d.cts.map +1 -1
  83. package/dist/core/commands/write.d.mts +2 -1
  84. package/dist/core/commands/write.d.mts.map +1 -1
  85. package/dist/core/commands/write.mjs +19 -6
  86. package/dist/core/commands/write.mjs.map +1 -1
  87. package/dist/core/executor/index.cjs +95 -19
  88. package/dist/core/executor/index.d.cts +4 -0
  89. package/dist/core/executor/index.d.cts.map +1 -1
  90. package/dist/core/executor/index.d.mts +4 -0
  91. package/dist/core/executor/index.d.mts.map +1 -1
  92. package/dist/core/executor/index.mjs +95 -19
  93. package/dist/core/executor/index.mjs.map +1 -1
  94. package/dist/core/formatters/index.d.mts +1 -0
  95. package/dist/core/formatters/install.cjs +40 -0
  96. package/dist/core/formatters/install.d.mts +1 -0
  97. package/dist/core/formatters/install.mjs +36 -0
  98. package/dist/core/formatters/install.mjs.map +1 -0
  99. package/dist/core/formatters/vault.cjs +36 -0
  100. package/dist/core/formatters/vault.mjs +32 -0
  101. package/dist/core/formatters/vault.mjs.map +1 -0
  102. package/dist/credential/auth-server.cjs +22 -4
  103. package/dist/credential/auth-server.mjs +22 -4
  104. package/dist/credential/auth-server.mjs.map +1 -1
  105. package/dist/credential/index.d.mts +2 -1
  106. package/dist/credential/mcp-auth-context.cjs +21 -5
  107. package/dist/credential/mcp-auth-context.mjs +21 -5
  108. package/dist/credential/mcp-auth-context.mjs.map +1 -1
  109. package/dist/credential/resolver.cjs +11 -3
  110. package/dist/credential/resolver.mjs +11 -3
  111. package/dist/credential/resolver.mjs.map +1 -1
  112. package/dist/credential/vault-store.d.mts +1 -0
  113. package/dist/daemon/config-manager.cjs +279 -0
  114. package/dist/daemon/config-manager.mjs +279 -0
  115. package/dist/daemon/config-manager.mjs.map +1 -0
  116. package/dist/daemon/manager.cjs +164 -0
  117. package/dist/daemon/manager.mjs +157 -0
  118. package/dist/daemon/manager.mjs.map +1 -0
  119. package/dist/daemon/server.cjs +220 -0
  120. package/dist/daemon/server.mjs +220 -0
  121. package/dist/daemon/server.mjs.map +1 -0
  122. package/dist/mcp/http-transport.cjs +14 -1
  123. package/dist/mcp/http-transport.mjs +14 -1
  124. package/dist/mcp/http-transport.mjs.map +1 -1
  125. package/dist/mcp/server.cjs +4 -2
  126. package/dist/mcp/server.mjs +4 -2
  127. package/dist/mcp/server.mjs.map +1 -1
  128. package/dist/mcp/tools.cjs +62 -12
  129. package/dist/mcp/tools.mjs +62 -12
  130. package/dist/mcp/tools.mjs.map +1 -1
  131. package/dist/program/daemon-integration.cjs +46 -0
  132. package/dist/program/daemon-integration.mjs +45 -0
  133. package/dist/program/daemon-integration.mjs.map +1 -0
  134. package/dist/program/program-manager.cjs +166 -0
  135. package/dist/program/program-manager.mjs +166 -0
  136. package/dist/program/program-manager.mjs.map +1 -0
  137. package/dist/program/trigger-scanner.cjs +148 -0
  138. package/dist/program/trigger-scanner.mjs +148 -0
  139. package/dist/program/trigger-scanner.mjs.map +1 -0
  140. package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
  141. package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +11 -0
  142. package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs.map +1 -0
  143. package/dist/providers/vault/dist/encrypted-file.cjs +158 -0
  144. package/dist/providers/vault/dist/encrypted-file.mjs +153 -0
  145. package/dist/providers/vault/dist/encrypted-file.mjs.map +1 -0
  146. package/dist/providers/vault/dist/index.cjs +405 -0
  147. package/dist/providers/vault/dist/index.mjs +400 -0
  148. package/dist/providers/vault/dist/index.mjs.map +1 -0
  149. package/dist/providers/vault/dist/key-resolver.cjs +181 -0
  150. package/dist/providers/vault/dist/key-resolver.mjs +180 -0
  151. package/dist/providers/vault/dist/key-resolver.mjs.map +1 -0
  152. package/dist/repl.cjs +109 -14
  153. package/dist/repl.d.cts.map +1 -1
  154. package/dist/repl.d.mts.map +1 -1
  155. package/dist/repl.mjs +109 -14
  156. package/dist/repl.mjs.map +1 -1
  157. package/package.json +27 -20
@@ -0,0 +1,166 @@
1
+ import { joinURL } from "ufo";
2
+ import { createProgramAFS } from "@aigne/afs";
3
+
4
+ //#region src/program/program-manager.ts
5
+ /**
6
+ * ProgramManager — manages activation and deactivation of installed programs.
7
+ *
8
+ * Activation creates a persistent Runtime AFS, registers EventBus subscriptions
9
+ * and cron jobs for trigger-bearing scripts. Deactivation cleans up everything.
10
+ */
11
+ var ProgramManager = class {
12
+ activated = /* @__PURE__ */ new Map();
13
+ reloadLock = null;
14
+ deps;
15
+ constructor(deps) {
16
+ this.deps = deps;
17
+ }
18
+ /**
19
+ * Activate a single program by ID.
20
+ * Creates Runtime AFS, registers event/cron subscriptions.
21
+ * If already activated, deactivates first then re-activates.
22
+ */
23
+ async activate(programId) {
24
+ const program = (await this.deps.listPrograms()).find((p) => p.id === programId);
25
+ if (!program) throw new Error(`Program "${programId}" not found in installed programs`);
26
+ if (this.activated.has(programId)) await this.deactivate(programId);
27
+ const triggerInfo = await this.deps.scanTriggers(program.installPath);
28
+ if (!triggerInfo || triggerInfo.triggers.length === 0) return;
29
+ const createAFS = this.deps.createProgramAFS ?? createProgramAFS;
30
+ const dataPath = this.deps.dataDir(programId);
31
+ const mountOverrides = await this.deps.readMountOverrides?.(programId) ?? [];
32
+ const createAFSOptions = {};
33
+ if (this.deps.createProvider) createAFSOptions.createProvider = this.deps.createProvider;
34
+ if (mountOverrides.length > 0) createAFSOptions.mountOverrides = mountOverrides;
35
+ const { afs, manifest, ownedProviders } = await createAFS(program.mountPath, dataPath, this.deps.globalAFS, Object.keys(createAFSOptions).length > 0 ? createAFSOptions : void 0);
36
+ const eventUnsubs = [];
37
+ const cronHandles = [];
38
+ for (const trigger of triggerInfo.triggers) if (trigger.trigger.kind === "event" && trigger.trigger.path) {
39
+ if (afs.subscribe) {
40
+ const unsub = afs.subscribe({ path: trigger.trigger.path }, (event) => {
41
+ this.deps.onTrigger?.(programId, trigger.scriptPath, trigger.jobName, event);
42
+ this.executeTriggerJob(programId, afs, trigger, event);
43
+ });
44
+ eventUnsubs.push(unsub);
45
+ }
46
+ } else if (trigger.trigger.kind === "cron" && trigger.trigger.expression) {
47
+ if (this.deps.createCron) {
48
+ const handle = this.deps.createCron(trigger.trigger.expression, () => {
49
+ const cronEvent = {
50
+ type: "cron",
51
+ path: `/cron/${programId}/${trigger.jobName}`,
52
+ source: "program-manager",
53
+ timestamp: Date.now()
54
+ };
55
+ this.deps.onTrigger?.(programId, trigger.scriptPath, trigger.jobName, cronEvent);
56
+ this.executeTriggerJob(programId, afs, trigger, cronEvent);
57
+ });
58
+ cronHandles.push(handle);
59
+ }
60
+ }
61
+ this.activated.set(programId, {
62
+ manifest,
63
+ runtimeAFS: afs,
64
+ ownedProviders,
65
+ eventUnsubs,
66
+ cronHandles
67
+ });
68
+ }
69
+ /**
70
+ * Deactivate a program by ID.
71
+ * Cancels subscriptions, stops cron jobs, closes owned providers.
72
+ * No-op if program is not activated.
73
+ */
74
+ async deactivate(programId) {
75
+ const state = this.activated.get(programId);
76
+ if (!state) return;
77
+ this.activated.delete(programId);
78
+ for (const unsub of state.eventUnsubs) try {
79
+ unsub();
80
+ } catch {}
81
+ for (const cron of state.cronHandles) try {
82
+ cron.stop();
83
+ } catch {}
84
+ for (const provider of state.ownedProviders) try {
85
+ await provider.close?.();
86
+ } catch {}
87
+ }
88
+ /**
89
+ * Activate all installed programs that have triggers.
90
+ * Failures on individual programs are skipped.
91
+ */
92
+ async activateAll() {
93
+ const programs = await this.deps.listPrograms();
94
+ for (const program of programs) try {
95
+ await this.activate(program.id);
96
+ } catch (err) {
97
+ console.error(`[PM] Failed to activate "${program.id}":`, err instanceof Error ? err.message : err);
98
+ }
99
+ }
100
+ /**
101
+ * Deactivate all currently activated programs.
102
+ */
103
+ async deactivateAll() {
104
+ const ids = [...this.activated.keys()];
105
+ for (const id of ids) await this.deactivate(id);
106
+ }
107
+ /**
108
+ * Reload all programs: deactivateAll + activateAll.
109
+ * Serialized via lock to prevent concurrent reloads.
110
+ */
111
+ async reload() {
112
+ if (this.reloadLock) await this.reloadLock;
113
+ this.reloadLock = (async () => {
114
+ try {
115
+ await this.deactivateAll();
116
+ await this.activateAll();
117
+ } finally {
118
+ this.reloadLock = null;
119
+ }
120
+ })();
121
+ await this.reloadLock;
122
+ }
123
+ /**
124
+ * Get list of activated program IDs.
125
+ */
126
+ getActivatedPrograms() {
127
+ return [...this.activated.keys()];
128
+ }
129
+ /**
130
+ * Execute a trigger job via the runtime AFS's ASH provider.
131
+ * Reads script source from /program/scripts/..., then calls /ash/.actions/run
132
+ * with job name and event data.
133
+ */
134
+ async executeTriggerJob(programId, runtimeAFS, trigger, event) {
135
+ try {
136
+ const scriptPath = joinURL("/program", trigger.scriptPath);
137
+ if (!runtimeAFS.read) {
138
+ console.error(`[PM] Cannot execute trigger: runtime AFS has no read()`);
139
+ return;
140
+ }
141
+ const readResult = await runtimeAFS.read(scriptPath, {});
142
+ const source = String(readResult.data?.content ?? "");
143
+ if (!source.trim()) {
144
+ console.error(`[PM] Script source empty: ${scriptPath}`);
145
+ return;
146
+ }
147
+ if (!runtimeAFS.exec) {
148
+ console.error(`[PM] Cannot execute trigger: runtime AFS has no exec()`);
149
+ return;
150
+ }
151
+ const result = await runtimeAFS.exec("/ash/.actions/run", {
152
+ source,
153
+ job: trigger.jobName,
154
+ event,
155
+ _runtime_afs: runtimeAFS
156
+ }, {});
157
+ if (!result.success) console.error(`[PM] Job "${programId}/${trigger.jobName}" failed:`, result.data ?? result.error);
158
+ } catch (err) {
159
+ console.error(`[PM] Job "${programId}/${trigger.jobName}" threw:`, err instanceof Error ? err.message : err);
160
+ }
161
+ }
162
+ };
163
+
164
+ //#endregion
165
+ export { ProgramManager };
166
+ //# sourceMappingURL=program-manager.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"program-manager.mjs","names":["defaultCreateProgramAFS"],"sources":["../../src/program/program-manager.ts"],"sourcesContent":["/**\n * ProgramManager — manages activation and deactivation of installed programs.\n *\n * Activation creates a persistent Runtime AFS, registers EventBus subscriptions\n * and cron jobs for trigger-bearing scripts. Deactivation cleans up everything.\n */\n\nimport {\n type AFS,\n type AFSEvent,\n type AFSModule,\n type AFSRoot,\n type AFSUnsubscribe,\n type CreateProgramAFSOptions,\n createProgramAFS as defaultCreateProgramAFS,\n type MountConfig,\n type MountOverride,\n type ProgramManifest,\n} from \"@aigne/afs\";\nimport { joinURL } from \"ufo\";\nimport type { ProgramTriggerInfo, ScriptTriggerInfo } from \"./trigger-scanner.js\";\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface CronHandle {\n stop(): void;\n}\n\nexport interface ProgramManagerDeps {\n /** Global AFS instance — used for EventBus subscriptions */\n globalAFS: AFS;\n /** Optional: factory for creating providers (with credential resolution) */\n createProvider?: (mount: MountConfig) => Promise<AFSModule>;\n /** List installed programs */\n listPrograms: () => Promise<Array<{ id: string; installPath: string; mountPath: string }>>;\n /** Scan triggers in a program directory */\n scanTriggers: (programDir: string) => Promise<ProgramTriggerInfo | null>;\n /** Create Runtime AFS for a program. Default: createProgramAFS from @aigne/afs */\n createProgramAFS?: (\n programPath: string,\n dataPath: string,\n globalAFS: AFS,\n options?: CreateProgramAFSOptions,\n ) => Promise<{ afs: AFSRoot; manifest: ProgramManifest; ownedProviders: AFSModule[] }>;\n /** Get data directory path for a program */\n dataDir: (programId: string) => string;\n /** Optional: create a cron job. Returns handle with stop(). */\n createCron?: (expression: string, callback: () => void) => CronHandle;\n /** Optional: callback when a trigger fires */\n onTrigger?: (\n programId: string,\n scriptPath: string,\n jobName: string,\n event?: AFSEvent | Record<string, unknown>,\n ) => void;\n /** Optional: read user-side mount overrides from mounts.toml */\n readMountOverrides?: (programId: string) => Promise<MountOverride[]>;\n}\n\ninterface ActivatedProgramState {\n manifest: ProgramManifest;\n runtimeAFS: AFSRoot;\n ownedProviders: AFSModule[];\n eventUnsubs: AFSUnsubscribe[];\n cronHandles: CronHandle[];\n}\n\n// ─── Implementation ─────────────────────────────────────────────────────────\n\nexport class ProgramManager {\n private activated = new Map<string, ActivatedProgramState>();\n private reloadLock: Promise<void> | null = null;\n private readonly deps: ProgramManagerDeps;\n\n constructor(deps: ProgramManagerDeps) {\n this.deps = deps;\n }\n\n /**\n * Activate a single program by ID.\n * Creates Runtime AFS, registers event/cron subscriptions.\n * If already activated, deactivates first then re-activates.\n */\n async activate(programId: string): Promise<void> {\n // Find the program\n const programs = await this.deps.listPrograms();\n const program = programs.find((p) => p.id === programId);\n if (!program) {\n throw new Error(`Program \"${programId}\" not found in installed programs`);\n }\n // If already activated, deactivate first\n if (this.activated.has(programId)) {\n await this.deactivate(programId);\n }\n\n // Scan for triggers\n const triggerInfo = await this.deps.scanTriggers(program.installPath);\n if (!triggerInfo || triggerInfo.triggers.length === 0) {\n return; // No triggers — nothing to activate\n }\n\n // Create Runtime AFS\n const createAFS = this.deps.createProgramAFS ?? defaultCreateProgramAFS;\n const dataPath = this.deps.dataDir(programId);\n const mountOverrides = (await this.deps.readMountOverrides?.(programId)) ?? [];\n const createAFSOptions: CreateProgramAFSOptions = {};\n if (this.deps.createProvider) {\n createAFSOptions.createProvider = this.deps.createProvider;\n }\n if (mountOverrides.length > 0) {\n createAFSOptions.mountOverrides = mountOverrides;\n }\n const { afs, manifest, ownedProviders } = await createAFS(\n program.mountPath,\n dataPath,\n this.deps.globalAFS,\n Object.keys(createAFSOptions).length > 0 ? createAFSOptions : undefined,\n );\n\n // Register triggers\n const eventUnsubs: AFSUnsubscribe[] = [];\n const cronHandles: CronHandle[] = [];\n\n for (const trigger of triggerInfo.triggers) {\n if (trigger.trigger.kind === \"event\" && trigger.trigger.path) {\n // Subscribe on the program's runtime AFS — trigger paths are relative\n // to the program's namespace (e.g. \"/telegram\", not \"/modules/telegram\").\n // Requires shared:false mounts for event-emitting providers so that\n // the owned provider instance emits events to the runtime EventBus.\n if (afs.subscribe) {\n const unsub = afs.subscribe({ path: trigger.trigger.path }, (event) => {\n this.deps.onTrigger?.(programId, trigger.scriptPath, trigger.jobName, event);\n this.executeTriggerJob(programId, afs, trigger, event);\n });\n eventUnsubs.push(unsub);\n }\n } else if (trigger.trigger.kind === \"cron\" && trigger.trigger.expression) {\n if (this.deps.createCron) {\n const handle = this.deps.createCron(trigger.trigger.expression, () => {\n const cronEvent = {\n type: \"cron\",\n path: `/cron/${programId}/${trigger.jobName}`,\n source: \"program-manager\",\n timestamp: Date.now(),\n };\n this.deps.onTrigger?.(programId, trigger.scriptPath, trigger.jobName, cronEvent);\n this.executeTriggerJob(programId, afs, trigger, cronEvent);\n });\n cronHandles.push(handle);\n }\n }\n }\n\n // Store activated state\n this.activated.set(programId, {\n manifest,\n runtimeAFS: afs,\n ownedProviders,\n eventUnsubs,\n cronHandles,\n });\n }\n\n /**\n * Deactivate a program by ID.\n * Cancels subscriptions, stops cron jobs, closes owned providers.\n * No-op if program is not activated.\n */\n async deactivate(programId: string): Promise<void> {\n const state = this.activated.get(programId);\n if (!state) return;\n\n // Remove from map first (prevents re-entrant issues)\n this.activated.delete(programId);\n\n // Unsubscribe all event triggers (swallow individual errors)\n for (const unsub of state.eventUnsubs) {\n try {\n unsub();\n } catch {\n // Swallow — best-effort cleanup\n }\n }\n\n // Stop all cron jobs (swallow individual errors)\n for (const cron of state.cronHandles) {\n try {\n cron.stop();\n } catch {\n // Swallow — best-effort cleanup\n }\n }\n\n // Close all owned providers (swallow individual errors)\n for (const provider of state.ownedProviders) {\n try {\n await provider.close?.();\n } catch {\n // Swallow — best-effort cleanup\n }\n }\n }\n\n /**\n * Activate all installed programs that have triggers.\n * Failures on individual programs are skipped.\n */\n async activateAll(): Promise<void> {\n const programs = await this.deps.listPrograms();\n for (const program of programs) {\n try {\n await this.activate(program.id);\n } catch (err) {\n console.error(\n `[PM] Failed to activate \"${program.id}\":`,\n err instanceof Error ? err.message : err,\n );\n }\n }\n }\n\n /**\n * Deactivate all currently activated programs.\n */\n async deactivateAll(): Promise<void> {\n const ids = [...this.activated.keys()];\n for (const id of ids) {\n await this.deactivate(id);\n }\n }\n\n /**\n * Reload all programs: deactivateAll + activateAll.\n * Serialized via lock to prevent concurrent reloads.\n */\n async reload(): Promise<void> {\n // Serialize concurrent reload calls\n if (this.reloadLock) {\n await this.reloadLock;\n }\n\n this.reloadLock = (async () => {\n try {\n await this.deactivateAll();\n await this.activateAll();\n } finally {\n this.reloadLock = null;\n }\n })();\n\n await this.reloadLock;\n }\n\n /**\n * Get list of activated program IDs.\n */\n getActivatedPrograms(): string[] {\n return [...this.activated.keys()];\n }\n\n /**\n * Execute a trigger job via the runtime AFS's ASH provider.\n * Reads script source from /program/scripts/..., then calls /ash/.actions/run\n * with job name and event data.\n */\n private async executeTriggerJob(\n programId: string,\n runtimeAFS: AFSRoot,\n trigger: ScriptTriggerInfo,\n event: AFSEvent | Record<string, unknown>,\n ): Promise<void> {\n try {\n // Read script source from runtime AFS\n const scriptPath = joinURL(\"/program\", trigger.scriptPath);\n if (!runtimeAFS.read) {\n console.error(`[PM] Cannot execute trigger: runtime AFS has no read()`);\n return;\n }\n const readResult = await runtimeAFS.read(scriptPath, {});\n const source = String(readResult.data?.content ?? \"\");\n if (!source.trim()) {\n console.error(`[PM] Script source empty: ${scriptPath}`);\n return;\n }\n\n // Execute via ASH provider's run action with job parameter\n if (!runtimeAFS.exec) {\n console.error(`[PM] Cannot execute trigger: runtime AFS has no exec()`);\n return;\n }\n const result = await runtimeAFS.exec(\n \"/ash/.actions/run\",\n {\n source,\n job: trigger.jobName,\n event,\n _runtime_afs: runtimeAFS,\n },\n {},\n );\n if (!result.success) {\n console.error(\n `[PM] Job \"${programId}/${trigger.jobName}\" failed:`,\n result.data ?? result.error,\n );\n }\n } catch (err) {\n console.error(\n `[PM] Job \"${programId}/${trigger.jobName}\" threw:`,\n err instanceof Error ? err.message : err,\n );\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,148 @@
1
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
2
+ let node_fs_promises = require("node:fs/promises");
3
+ let node_path = require("node:path");
4
+ let _aigne_afs = require("@aigne/afs");
5
+
6
+ //#region src/program/trigger-scanner.ts
7
+ /**
8
+ * Scan a program directory for .ash scripts containing trigger declarations.
9
+ *
10
+ * @param programDir - Absolute filesystem path to the program directory
11
+ * @param compile - Compile function (typically compileSource from @aigne/ash).
12
+ * If null, falls back to regex-based extraction from source.
13
+ * @returns ProgramTriggerInfo if any triggers found, null otherwise
14
+ * @throws If programDir doesn't exist, or program.yaml is missing/invalid
15
+ */
16
+ async function scanProgramTriggers(programDir, compile) {
17
+ const resolvedDir = (0, node_path.resolve)(programDir);
18
+ try {
19
+ if (!(await (0, node_fs_promises.stat)(resolvedDir)).isDirectory()) throw new Error(`Program path is not a directory: ${resolvedDir}`);
20
+ } catch (err) {
21
+ if (err.code === "ENOENT") throw new Error(`Program directory does not exist: ${resolvedDir}`);
22
+ throw err;
23
+ }
24
+ const manifestPath = (0, node_path.join)(resolvedDir, "program.yaml");
25
+ let yamlContent;
26
+ try {
27
+ yamlContent = await (0, node_fs_promises.readFile)(manifestPath, "utf-8");
28
+ } catch (err) {
29
+ if (err.code === "ENOENT") throw new Error("program.yaml not found in program directory");
30
+ throw err;
31
+ }
32
+ const manifest = (0, _aigne_afs.parseProgramManifest)(yamlContent);
33
+ const ashFiles = await findAshFiles(resolvedDir, resolvedDir);
34
+ if (ashFiles.length === 0) return null;
35
+ const triggers = [];
36
+ for (const relPath of ashFiles) {
37
+ const fullPath = (0, node_path.join)(resolvedDir, relPath);
38
+ let source;
39
+ try {
40
+ source = await (0, node_fs_promises.readFile)(fullPath, "utf-8");
41
+ } catch {
42
+ continue;
43
+ }
44
+ if (!source.trim()) continue;
45
+ try {
46
+ if (compile) {
47
+ const result = compile(source);
48
+ if (!result.program) continue;
49
+ for (const unit of result.program.units) {
50
+ if (unit.kind !== "job" || !unit.trigger) continue;
51
+ triggers.push({
52
+ scriptPath: relPath,
53
+ jobName: unit.name,
54
+ trigger: {
55
+ kind: unit.trigger.kind,
56
+ path: unit.trigger.path,
57
+ event: unit.trigger.event,
58
+ expression: unit.trigger.expression
59
+ }
60
+ });
61
+ }
62
+ } else {
63
+ const extracted = extractTriggersFromSource(source, relPath);
64
+ triggers.push(...extracted);
65
+ }
66
+ } catch {}
67
+ }
68
+ if (triggers.length === 0) return null;
69
+ return {
70
+ manifest,
71
+ triggers
72
+ };
73
+ }
74
+ /**
75
+ * Recursively find all .ash files under a directory.
76
+ * Skips symlinks that resolve outside the root directory.
77
+ * Returns paths relative to rootDir.
78
+ */
79
+ async function findAshFiles(dir, rootDir) {
80
+ const results = [];
81
+ let entries;
82
+ try {
83
+ entries = await (0, node_fs_promises.readdir)(dir, { withFileTypes: true });
84
+ } catch {
85
+ return results;
86
+ }
87
+ for (const entry of entries) {
88
+ const fullPath = (0, node_path.join)(dir, entry.name);
89
+ if (entry.isSymbolicLink()) {
90
+ try {
91
+ const realPath = await import("node:fs/promises").then((fs) => fs.realpath(fullPath));
92
+ if (!realPath.startsWith(rootDir)) continue;
93
+ const s = await (0, node_fs_promises.stat)(realPath);
94
+ if (s.isDirectory()) {
95
+ const subResults = await findAshFiles(fullPath, rootDir);
96
+ results.push(...subResults);
97
+ } else if (s.isFile() && entry.name.endsWith(".ash")) results.push((0, node_path.relative)(rootDir, fullPath));
98
+ } catch {
99
+ continue;
100
+ }
101
+ continue;
102
+ }
103
+ if (entry.isDirectory()) {
104
+ const subResults = await findAshFiles(fullPath, rootDir);
105
+ results.push(...subResults);
106
+ } else if (entry.isFile() && entry.name.endsWith(".ash")) results.push((0, node_path.relative)(rootDir, fullPath));
107
+ }
108
+ return results.sort();
109
+ }
110
+ /**
111
+ * Extract triggers from ASH source using regex.
112
+ * Matches ASH trigger syntax:
113
+ * job name on /path:event { ... }
114
+ * job name on cron("expression") { ... }
115
+ */
116
+ function extractTriggersFromSource(source, relPath) {
117
+ const results = [];
118
+ const lines = source.split("\n");
119
+ for (const line of lines) {
120
+ const trimmed = line.trim();
121
+ const eventMatch = trimmed.match(/^job\s+(\w+)\s+on\s+(\/[^\s:]+):(\w+)\s*\{/);
122
+ if (eventMatch) {
123
+ results.push({
124
+ scriptPath: relPath,
125
+ jobName: eventMatch[1],
126
+ trigger: {
127
+ kind: "event",
128
+ path: eventMatch[2],
129
+ event: eventMatch[3]
130
+ }
131
+ });
132
+ continue;
133
+ }
134
+ const cronMatch = trimmed.match(/^job\s+(\w+)\s+on\s+cron\(\s*"([^"]+)"\s*\)\s*\{/);
135
+ if (cronMatch) results.push({
136
+ scriptPath: relPath,
137
+ jobName: cronMatch[1],
138
+ trigger: {
139
+ kind: "cron",
140
+ expression: cronMatch[2]
141
+ }
142
+ });
143
+ }
144
+ return results;
145
+ }
146
+
147
+ //#endregion
148
+ exports.scanProgramTriggers = scanProgramTriggers;
@@ -0,0 +1,148 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { parseProgramManifest } from "@aigne/afs";
4
+
5
+ //#region src/program/trigger-scanner.ts
6
+ /**
7
+ * Scan a program directory for .ash scripts containing trigger declarations.
8
+ *
9
+ * @param programDir - Absolute filesystem path to the program directory
10
+ * @param compile - Compile function (typically compileSource from @aigne/ash).
11
+ * If null, falls back to regex-based extraction from source.
12
+ * @returns ProgramTriggerInfo if any triggers found, null otherwise
13
+ * @throws If programDir doesn't exist, or program.yaml is missing/invalid
14
+ */
15
+ async function scanProgramTriggers(programDir, compile) {
16
+ const resolvedDir = resolve(programDir);
17
+ try {
18
+ if (!(await stat(resolvedDir)).isDirectory()) throw new Error(`Program path is not a directory: ${resolvedDir}`);
19
+ } catch (err) {
20
+ if (err.code === "ENOENT") throw new Error(`Program directory does not exist: ${resolvedDir}`);
21
+ throw err;
22
+ }
23
+ const manifestPath = join(resolvedDir, "program.yaml");
24
+ let yamlContent;
25
+ try {
26
+ yamlContent = await readFile(manifestPath, "utf-8");
27
+ } catch (err) {
28
+ if (err.code === "ENOENT") throw new Error("program.yaml not found in program directory");
29
+ throw err;
30
+ }
31
+ const manifest = parseProgramManifest(yamlContent);
32
+ const ashFiles = await findAshFiles(resolvedDir, resolvedDir);
33
+ if (ashFiles.length === 0) return null;
34
+ const triggers = [];
35
+ for (const relPath of ashFiles) {
36
+ const fullPath = join(resolvedDir, relPath);
37
+ let source;
38
+ try {
39
+ source = await readFile(fullPath, "utf-8");
40
+ } catch {
41
+ continue;
42
+ }
43
+ if (!source.trim()) continue;
44
+ try {
45
+ if (compile) {
46
+ const result = compile(source);
47
+ if (!result.program) continue;
48
+ for (const unit of result.program.units) {
49
+ if (unit.kind !== "job" || !unit.trigger) continue;
50
+ triggers.push({
51
+ scriptPath: relPath,
52
+ jobName: unit.name,
53
+ trigger: {
54
+ kind: unit.trigger.kind,
55
+ path: unit.trigger.path,
56
+ event: unit.trigger.event,
57
+ expression: unit.trigger.expression
58
+ }
59
+ });
60
+ }
61
+ } else {
62
+ const extracted = extractTriggersFromSource(source, relPath);
63
+ triggers.push(...extracted);
64
+ }
65
+ } catch {}
66
+ }
67
+ if (triggers.length === 0) return null;
68
+ return {
69
+ manifest,
70
+ triggers
71
+ };
72
+ }
73
+ /**
74
+ * Recursively find all .ash files under a directory.
75
+ * Skips symlinks that resolve outside the root directory.
76
+ * Returns paths relative to rootDir.
77
+ */
78
+ async function findAshFiles(dir, rootDir) {
79
+ const results = [];
80
+ let entries;
81
+ try {
82
+ entries = await readdir(dir, { withFileTypes: true });
83
+ } catch {
84
+ return results;
85
+ }
86
+ for (const entry of entries) {
87
+ const fullPath = join(dir, entry.name);
88
+ if (entry.isSymbolicLink()) {
89
+ try {
90
+ const realPath = await import("node:fs/promises").then((fs) => fs.realpath(fullPath));
91
+ if (!realPath.startsWith(rootDir)) continue;
92
+ const s = await stat(realPath);
93
+ if (s.isDirectory()) {
94
+ const subResults = await findAshFiles(fullPath, rootDir);
95
+ results.push(...subResults);
96
+ } else if (s.isFile() && entry.name.endsWith(".ash")) results.push(relative(rootDir, fullPath));
97
+ } catch {
98
+ continue;
99
+ }
100
+ continue;
101
+ }
102
+ if (entry.isDirectory()) {
103
+ const subResults = await findAshFiles(fullPath, rootDir);
104
+ results.push(...subResults);
105
+ } else if (entry.isFile() && entry.name.endsWith(".ash")) results.push(relative(rootDir, fullPath));
106
+ }
107
+ return results.sort();
108
+ }
109
+ /**
110
+ * Extract triggers from ASH source using regex.
111
+ * Matches ASH trigger syntax:
112
+ * job name on /path:event { ... }
113
+ * job name on cron("expression") { ... }
114
+ */
115
+ function extractTriggersFromSource(source, relPath) {
116
+ const results = [];
117
+ const lines = source.split("\n");
118
+ for (const line of lines) {
119
+ const trimmed = line.trim();
120
+ const eventMatch = trimmed.match(/^job\s+(\w+)\s+on\s+(\/[^\s:]+):(\w+)\s*\{/);
121
+ if (eventMatch) {
122
+ results.push({
123
+ scriptPath: relPath,
124
+ jobName: eventMatch[1],
125
+ trigger: {
126
+ kind: "event",
127
+ path: eventMatch[2],
128
+ event: eventMatch[3]
129
+ }
130
+ });
131
+ continue;
132
+ }
133
+ const cronMatch = trimmed.match(/^job\s+(\w+)\s+on\s+cron\(\s*"([^"]+)"\s*\)\s*\{/);
134
+ if (cronMatch) results.push({
135
+ scriptPath: relPath,
136
+ jobName: cronMatch[1],
137
+ trigger: {
138
+ kind: "cron",
139
+ expression: cronMatch[2]
140
+ }
141
+ });
142
+ }
143
+ return results;
144
+ }
145
+
146
+ //#endregion
147
+ export { scanProgramTriggers };
148
+ //# sourceMappingURL=trigger-scanner.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trigger-scanner.mjs","names":[],"sources":["../../src/program/trigger-scanner.ts"],"sourcesContent":["/**\n * Trigger Scanner — scans program directories for .ash scripts with\n * @on (event) and @cron trigger declarations.\n *\n * Uses dependency injection for the compile function to avoid direct\n * @aigne/ash dependency. The caller provides compileSource.\n */\n\nimport type { Dirent } from \"node:fs\";\nimport { readdir, readFile, stat } from \"node:fs/promises\";\nimport { join, relative, resolve } from \"node:path\";\nimport { type ProgramManifest, parseProgramManifest } from \"@aigne/afs\";\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface TriggerInfo {\n kind: \"event\" | \"cron\";\n /** For event triggers: the AFS path to watch */\n path?: string;\n /** For event triggers: the event type (e.g., \"created\", \"deleted\") */\n event?: string;\n /** For cron triggers: the cron expression */\n expression?: string;\n}\n\nexport interface ScriptTriggerInfo {\n /** Relative path to the .ash script from program root */\n scriptPath: string;\n /** Job name in the script */\n jobName: string;\n /** Trigger declaration */\n trigger: TriggerInfo;\n}\n\nexport interface ProgramTriggerInfo {\n /** Parsed program manifest */\n manifest: ProgramManifest;\n /** All trigger declarations found across scripts */\n triggers: ScriptTriggerInfo[];\n}\n\n/**\n * Compile function signature — matches @aigne/ash compileSource.\n * Accepts ASH source code, returns compiled program with trigger info.\n */\nexport type CompileFn = (source: string) => {\n program?: {\n units: Array<{\n kind: string;\n name: string;\n trigger?: {\n kind: string;\n path?: string;\n event?: string;\n expression?: string;\n };\n }>;\n };\n diagnostics: Array<{ message?: string }>;\n};\n\n// ─── Implementation ─────────────────────────────────────────────────────────\n\n/**\n * Scan a program directory for .ash scripts containing trigger declarations.\n *\n * @param programDir - Absolute filesystem path to the program directory\n * @param compile - Compile function (typically compileSource from @aigne/ash).\n * If null, falls back to regex-based extraction from source.\n * @returns ProgramTriggerInfo if any triggers found, null otherwise\n * @throws If programDir doesn't exist, or program.yaml is missing/invalid\n */\nexport async function scanProgramTriggers(\n programDir: string,\n compile: CompileFn | null,\n): Promise<ProgramTriggerInfo | null> {\n const resolvedDir = resolve(programDir);\n\n // 1. Verify directory exists\n try {\n const s = await stat(resolvedDir);\n if (!s.isDirectory()) {\n throw new Error(`Program path is not a directory: ${resolvedDir}`);\n }\n } catch (err: any) {\n if (err.code === \"ENOENT\") {\n throw new Error(`Program directory does not exist: ${resolvedDir}`);\n }\n throw err;\n }\n\n // 2. Read and validate program.yaml\n const manifestPath = join(resolvedDir, \"program.yaml\");\n let yamlContent: string;\n try {\n yamlContent = await readFile(manifestPath, \"utf-8\");\n } catch (err: any) {\n if (err.code === \"ENOENT\") {\n throw new Error(\"program.yaml not found in program directory\");\n }\n throw err;\n }\n const manifest = parseProgramManifest(yamlContent);\n\n // 3. Recursively find all .ash files\n const ashFiles = await findAshFiles(resolvedDir, resolvedDir);\n if (ashFiles.length === 0) {\n return null;\n }\n\n // 4. Compile each script and extract triggers\n const triggers: ScriptTriggerInfo[] = [];\n\n for (const relPath of ashFiles) {\n const fullPath = join(resolvedDir, relPath);\n let source: string;\n try {\n source = await readFile(fullPath, \"utf-8\");\n } catch {\n continue; // Skip unreadable files\n }\n\n // Skip empty files\n if (!source.trim()) {\n continue;\n }\n\n // Extract triggers: use compiler if available, otherwise regex fallback\n try {\n if (compile) {\n const result = compile(source);\n if (!result.program) {\n continue; // Compilation failed — skip\n }\n for (const unit of result.program.units) {\n if (unit.kind !== \"job\" || !unit.trigger) continue;\n triggers.push({\n scriptPath: relPath,\n jobName: unit.name,\n trigger: {\n kind: unit.trigger.kind as \"event\" | \"cron\",\n path: unit.trigger.path,\n event: unit.trigger.event,\n expression: unit.trigger.expression,\n },\n });\n }\n } else {\n // Regex fallback: extract @on and @cron from source text\n const extracted = extractTriggersFromSource(source, relPath);\n triggers.push(...extracted);\n }\n } catch {}\n }\n\n if (triggers.length === 0) {\n return null;\n }\n\n return { manifest, triggers };\n}\n\n/**\n * Recursively find all .ash files under a directory.\n * Skips symlinks that resolve outside the root directory.\n * Returns paths relative to rootDir.\n */\nasync function findAshFiles(dir: string, rootDir: string): Promise<string[]> {\n const results: string[] = [];\n let entries: Dirent<string>[];\n try {\n entries = (await readdir(dir, { withFileTypes: true })) as Dirent<string>[];\n } catch {\n return results;\n }\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n\n // Security: check for symlinks that point outside the program directory\n if (entry.isSymbolicLink()) {\n try {\n const realPath = await import(\"node:fs/promises\").then((fs) => fs.realpath(fullPath));\n if (!realPath.startsWith(rootDir)) {\n continue; // Skip symlinks that escape the program directory\n }\n // Check if it's a directory or file after resolving\n const s = await stat(realPath);\n if (s.isDirectory()) {\n const subResults = await findAshFiles(fullPath, rootDir);\n results.push(...subResults);\n } else if (s.isFile() && entry.name.endsWith(\".ash\")) {\n results.push(relative(rootDir, fullPath));\n }\n } catch {\n continue; // Skip broken symlinks\n }\n continue;\n }\n\n if (entry.isDirectory()) {\n const subResults = await findAshFiles(fullPath, rootDir);\n results.push(...subResults);\n } else if (entry.isFile() && entry.name.endsWith(\".ash\")) {\n results.push(relative(rootDir, fullPath));\n }\n }\n\n return results.sort();\n}\n\n// ─── Regex Fallback ────────────────────────────────────────────────────────\n\n/**\n * Extract triggers from ASH source using regex.\n * Matches ASH trigger syntax:\n * job name on /path:event { ... }\n * job name on cron(\"expression\") { ... }\n */\nfunction extractTriggersFromSource(source: string, relPath: string): ScriptTriggerInfo[] {\n const results: ScriptTriggerInfo[] = [];\n const lines = source.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Match: job <name> on /path:event {\n const eventMatch = trimmed.match(/^job\\s+(\\w+)\\s+on\\s+(\\/[^\\s:]+):(\\w+)\\s*\\{/);\n if (eventMatch) {\n results.push({\n scriptPath: relPath,\n jobName: eventMatch[1]!,\n trigger: {\n kind: \"event\",\n path: eventMatch[2],\n event: eventMatch[3],\n },\n });\n continue;\n }\n\n // Match: job <name> on cron(\"expression\") {\n const cronMatch = trimmed.match(/^job\\s+(\\w+)\\s+on\\s+cron\\(\\s*\"([^\"]+)\"\\s*\\)\\s*\\{/);\n if (cronMatch) {\n results.push({\n scriptPath: relPath,\n jobName: cronMatch[1]!,\n trigger: {\n kind: \"cron\",\n expression: cronMatch[2],\n },\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+
2
+ //#region ../../providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs
3
+ function __decorate(decorators, target, key, desc) {
4
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
6
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
7
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
8
+ }
9
+
10
+ //#endregion
11
+ exports.__decorate = __decorate;
@@ -0,0 +1,11 @@
1
+ //#region ../../providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs
2
+ function __decorate(decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ }
8
+
9
+ //#endregion
10
+ export { __decorate };
11
+ //# sourceMappingURL=decorate.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorate.mjs","names":[],"sources":["../../../../../../../../../providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs"],"sourcesContent":["//#region \\0@oxc-project+runtime@0.108.0/helpers/decorate.js\nfunction __decorate(decorators, target, key, desc) {\n\tvar c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n\tif (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\n\telse for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n\treturn c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\n//#endregion\nexport { __decorate };"],"mappings":";AACA,SAAS,WAAW,YAAY,QAAQ,KAAK,MAAM;CAClD,IAAI,IAAI,UAAU,QAAQ,IAAI,IAAI,IAAI,SAAS,SAAS,OAAO,OAAO,OAAO,yBAAyB,QAAQ,IAAI,GAAG,MAAM;AAC3H,KAAI,OAAO,YAAY,YAAY,OAAO,QAAQ,aAAa,WAAY,KAAI,QAAQ,SAAS,YAAY,QAAQ,KAAK,KAAK;KACzH,MAAK,IAAI,IAAI,WAAW,SAAS,GAAG,KAAK,GAAG,IAAK,KAAI,IAAI,WAAW,GAAI,MAAK,IAAI,IAAI,EAAE,EAAE,GAAG,IAAI,IAAI,EAAE,QAAQ,KAAK,EAAE,GAAG,EAAE,QAAQ,IAAI,KAAK;AAChJ,QAAO,IAAI,KAAK,KAAK,OAAO,eAAe,QAAQ,KAAK,EAAE,EAAE"}