@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,279 @@
1
+ import { ConfigLoader } from "../config/loader.mjs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { parse, stringify } from "smol-toml";
5
+ import { ProviderRegistry } from "@aigne/afs";
6
+ import { watch } from "node:fs";
7
+
8
+ //#region src/daemon/config-manager.ts
9
+ /**
10
+ * ConfigManager Implementation
11
+ *
12
+ * Manages config.toml read/write operations for the daemon.
13
+ * Implements the ConfigManager interface from @aigne/afs-explorer.
14
+ */
15
+ var DaemonConfigManager = class {
16
+ cwd;
17
+ afs;
18
+ onConfigChanged;
19
+ watcher;
20
+ _selfWriteTimestamp = 0;
21
+ _debounceTimer;
22
+ _failures = [];
23
+ /** Tracks mount paths that originated from config.toml. Only these are eligible for unmount on reload. */
24
+ configManagedPaths;
25
+ registry;
26
+ constructor(options) {
27
+ this.cwd = options.cwd;
28
+ this.afs = options.afs;
29
+ this.registry = options.registry;
30
+ this.onConfigChanged = options.onConfigChanged;
31
+ this.configManagedPaths = new Set(options.configMountPaths ?? []);
32
+ if (options.failures) this._failures = [...options.failures];
33
+ }
34
+ async getConfig() {
35
+ return new ConfigLoader().load(this.cwd);
36
+ }
37
+ async getMountList() {
38
+ const mounts = this.afs.getMounts();
39
+ return {
40
+ mounts: await Promise.all(mounts.map(async (m) => {
41
+ const ctor = m.module.constructor;
42
+ const manifest = typeof ctor.manifest === "function" ? ctor.manifest() : null;
43
+ let url;
44
+ try {
45
+ if (typeof m.module.stat === "function") url = (await m.module.stat("/", {}))?.data?.meta?.url || void 0;
46
+ } catch {}
47
+ return {
48
+ namespace: m.namespace,
49
+ path: m.path,
50
+ name: m.module.name,
51
+ description: m.module.description,
52
+ accessMode: m.module.accessMode,
53
+ category: manifest?.category || void 0,
54
+ tags: manifest?.tags || void 0,
55
+ uri: manifest?.uriTemplate || void 0,
56
+ url
57
+ };
58
+ })),
59
+ failures: this._failures
60
+ };
61
+ }
62
+ async addMount(mount) {
63
+ const uri = mount.uri;
64
+ const path = mount.path;
65
+ if (!uri || !path) throw new Error("uri and path are required");
66
+ const mountConfig = await this.buildMountWithCredentials(uri, path, mount.auth, {
67
+ description: mount.description,
68
+ access_mode: mount.accessMode,
69
+ namespace: mount.namespace,
70
+ options: mount.options
71
+ });
72
+ const provider = await (this.registry ?? new ProviderRegistry()).createProvider(mountConfig);
73
+ await this.afs.mount(provider, path, { namespace: mountConfig.namespace ?? null });
74
+ await this.persistAddMount(mountConfig);
75
+ this.configManagedPaths.add(path);
76
+ }
77
+ async removeMount(path) {
78
+ this.afs.unmount(path);
79
+ await this.persistRemoveMount(path);
80
+ this.configManagedPaths.delete(path);
81
+ }
82
+ async updateMount(path, updates) {
83
+ const config = await this.readConfigFile();
84
+ const entry = (config.mounts ?? []).find((m) => m.path === path);
85
+ if (!entry) throw new Error(`Mount "${path}" not found in config`);
86
+ if (updates.description !== void 0) entry.description = updates.description;
87
+ if (updates.accessMode !== void 0) entry.access_mode = updates.accessMode;
88
+ if (updates.auth !== void 0) entry.auth = updates.auth;
89
+ await this.writeConfigFile(config);
90
+ try {
91
+ this.afs.unmount(path);
92
+ } catch {}
93
+ const mount = await this.buildMountWithCredentials(entry.uri, entry.path, entry.auth, {
94
+ description: entry.description,
95
+ access_mode: entry.access_mode,
96
+ namespace: entry.namespace,
97
+ options: entry.options
98
+ });
99
+ const provider = await new ProviderRegistry().createProvider(mount);
100
+ await this.afs.mount(provider, path, { namespace: entry.namespace ?? null });
101
+ }
102
+ async testMount(uri, auth) {
103
+ try {
104
+ const registry = new ProviderRegistry();
105
+ const mount = await this.buildMountWithCredentials(uri, "/test", auth);
106
+ const provider = await registry.createProvider(mount);
107
+ await provider.list("/", {});
108
+ const providerName = provider.name;
109
+ try {
110
+ await provider.close?.();
111
+ } catch {}
112
+ return {
113
+ success: true,
114
+ providerName
115
+ };
116
+ } catch (err) {
117
+ return {
118
+ success: false,
119
+ error: err instanceof Error ? err.message : String(err)
120
+ };
121
+ }
122
+ }
123
+ async reloadConfig() {
124
+ const newConfig = await new ConfigLoader().load(this.cwd);
125
+ const currentMounts = new Set(this.afs.getMounts().map((m) => m.path));
126
+ const newMountPaths = new Set(newConfig.mounts.map((m) => m.path));
127
+ const removed = [];
128
+ for (const path of this.configManagedPaths) if (!newMountPaths.has(path)) {
129
+ try {
130
+ this.afs.unmount(path);
131
+ removed.push(path);
132
+ } catch {}
133
+ this.configManagedPaths.delete(path);
134
+ }
135
+ const added = [];
136
+ const registry = new ProviderRegistry();
137
+ for (const mount of newConfig.mounts) if (!currentMounts.has(mount.path)) try {
138
+ const provider = await registry.createProvider(mount);
139
+ await this.afs.mount(provider, mount.path, { namespace: mount.namespace ?? null });
140
+ added.push(mount.path);
141
+ this.configManagedPaths.add(mount.path);
142
+ } catch (err) {
143
+ console.warn(`[reload] Failed to mount ${mount.path}: ${err instanceof Error ? err.message : String(err)}`);
144
+ }
145
+ if (this.onConfigChanged && (added.length > 0 || removed.length > 0)) this.onConfigChanged(added, removed);
146
+ }
147
+ /**
148
+ * Start watching config file for external changes.
149
+ */
150
+ startWatching() {
151
+ const configPath = this.getConfigPath();
152
+ try {
153
+ this.watcher = watch(dirname(configPath), (_event, filename) => {
154
+ if (filename !== "config.toml") return;
155
+ if (Date.now() - this._selfWriteTimestamp < 500) return;
156
+ if (this._debounceTimer) clearTimeout(this._debounceTimer);
157
+ this._debounceTimer = setTimeout(() => {
158
+ this.reloadConfig().catch((err) => {
159
+ console.warn(`[watch] Reload failed: ${err instanceof Error ? err.message : String(err)}`);
160
+ });
161
+ }, 300);
162
+ });
163
+ } catch {}
164
+ }
165
+ async getRegistry() {
166
+ try {
167
+ const { scanInstalledProviders } = await import("@aigne/afs-registry");
168
+ return (await scanInstalledProviders()).map((m) => ({
169
+ name: m.name,
170
+ description: m.description,
171
+ category: m.category,
172
+ uriTemplate: m.uriTemplate,
173
+ tags: m.tags
174
+ }));
175
+ } catch {
176
+ return [];
177
+ }
178
+ }
179
+ stopWatching() {
180
+ if (this.watcher) {
181
+ this.watcher.close();
182
+ this.watcher = void 0;
183
+ }
184
+ if (this._debounceTimer) {
185
+ clearTimeout(this._debounceTimer);
186
+ this._debounceTimer = void 0;
187
+ }
188
+ }
189
+ /**
190
+ * Build a MountConfig with proper credential mapping.
191
+ *
192
+ * Uses the provider schema to:
193
+ * 1. Resolve credentials from env vars (e.g. AIGNE_HUB_API_KEY)
194
+ * 2. Resolve credentials from the credential store (vault)
195
+ * 3. Map generic `auth` to the provider's specific credential field (e.g. apiKey)
196
+ */
197
+ async buildMountWithCredentials(uri, path, auth, extra) {
198
+ const mount = {
199
+ uri,
200
+ path,
201
+ auth,
202
+ description: extra?.description,
203
+ access_mode: extra?.access_mode,
204
+ namespace: extra?.namespace,
205
+ options: extra?.options ? { ...extra.options } : void 0
206
+ };
207
+ try {
208
+ const info = await new ProviderRegistry().getProviderInfo(uri);
209
+ if (!info?.schema) return mount;
210
+ const { getSensitiveFields, resolveEnvFromSchema } = await import("@aigne/afs/utils/schema");
211
+ const opts = mount.options ?? {};
212
+ const envResolved = resolveEnvFromSchema(info.schema);
213
+ for (const [field, value] of Object.entries(envResolved)) if (opts[field] === void 0) opts[field] = value;
214
+ try {
215
+ const { createCredentialStore } = await import("../credential/store.mjs");
216
+ const stored = await createCredentialStore().get(uri);
217
+ if (stored) {
218
+ for (const [field, value] of Object.entries(stored)) if (!field.startsWith("env:") && opts[field] === void 0) opts[field] = value;
219
+ }
220
+ } catch {}
221
+ if (auth) {
222
+ const sensitiveFields = getSensitiveFields(info.schema);
223
+ for (const field of sensitiveFields) if (field !== "auth" && field !== "token" && opts[field] === void 0) {
224
+ opts[field] = auth;
225
+ break;
226
+ }
227
+ }
228
+ if (Object.keys(opts).length > 0) mount.options = opts;
229
+ } catch {}
230
+ return mount;
231
+ }
232
+ getConfigPath() {
233
+ return join(this.cwd, ".afs-config", "config.toml");
234
+ }
235
+ async readConfigFile() {
236
+ const configPath = this.getConfigPath();
237
+ try {
238
+ return parse(await readFile(configPath, "utf-8"));
239
+ } catch {
240
+ return { mounts: [] };
241
+ }
242
+ }
243
+ async writeConfigFile(config) {
244
+ const configPath = this.getConfigPath();
245
+ const configDir = dirname(configPath);
246
+ try {
247
+ await mkdir(configDir, { recursive: true });
248
+ } catch {}
249
+ this._selfWriteTimestamp = Date.now();
250
+ await writeFile(configPath, stringify(config), "utf-8");
251
+ }
252
+ async persistAddMount(mount) {
253
+ const config = await this.readConfigFile();
254
+ const mounts = config.mounts ?? [];
255
+ const entry = {
256
+ path: mount.path,
257
+ uri: mount.uri
258
+ };
259
+ if (mount.description) entry.description = mount.description;
260
+ if (mount.access_mode) entry.access_mode = mount.access_mode;
261
+ if (mount.auth) entry.auth = mount.auth;
262
+ if (mount.namespace) entry.namespace = mount.namespace;
263
+ if (mount.options) entry.options = mount.options;
264
+ const existing = mounts.findIndex((m) => m.path === mount.path);
265
+ if (existing >= 0) mounts[existing] = entry;
266
+ else mounts.push(entry);
267
+ config.mounts = mounts;
268
+ await this.writeConfigFile(config);
269
+ }
270
+ async persistRemoveMount(path) {
271
+ const config = await this.readConfigFile();
272
+ config.mounts = (config.mounts ?? []).filter((m) => m.path !== path);
273
+ await this.writeConfigFile(config);
274
+ }
275
+ };
276
+
277
+ //#endregion
278
+ export { DaemonConfigManager };
279
+ //# sourceMappingURL=config-manager.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-manager.mjs","names":[],"sources":["../../src/daemon/config-manager.ts"],"sourcesContent":["/**\n * ConfigManager Implementation\n *\n * Manages config.toml read/write operations for the daemon.\n * Implements the ConfigManager interface from @aigne/afs-explorer.\n */\n\nimport { type FSWatcher, watch } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { AFS } from \"@aigne/afs\";\nimport { ProviderRegistry } from \"@aigne/afs\";\nimport type { ConfigManager } from \"@aigne/afs-explorer\";\nimport { parse, stringify } from \"smol-toml\";\nimport { ConfigLoader } from \"../config/loader.js\";\nimport type { MountConfig } from \"../config/schema.js\";\n\nexport interface MountFailure {\n path: string;\n uri: string;\n reason: string;\n}\n\nexport interface ConfigManagerOptions {\n /** Working directory for config resolution. */\n cwd: string;\n /** AFS instance to mount/unmount providers on. */\n afs: AFS;\n /** Provider registry for creating providers at runtime (e.g. add-mount). */\n registry?: ProviderRegistry;\n /** Mount paths that came from config.toml at startup (only these are unmounted on reload). */\n configMountPaths?: string[];\n /** Initial mount failures from startup. */\n failures?: MountFailure[];\n /** Callback when config changes are detected (for WS broadcast). */\n onConfigChanged?: (added: string[], removed: string[]) => void;\n}\n\ninterface ConfigMountEntry {\n path: string;\n uri: string;\n namespace?: string;\n description?: string;\n access_mode?: \"readonly\" | \"readwrite\";\n auth?: string;\n token?: string;\n options?: Record<string, unknown>;\n}\n\nexport class DaemonConfigManager implements ConfigManager {\n private cwd: string;\n private afs: AFS;\n private onConfigChanged?: (added: string[], removed: string[]) => void;\n private watcher?: FSWatcher;\n private _selfWriteTimestamp = 0;\n private _debounceTimer?: ReturnType<typeof setTimeout>;\n private _failures: MountFailure[] = [];\n /** Tracks mount paths that originated from config.toml. Only these are eligible for unmount on reload. */\n private configManagedPaths: Set<string>;\n private registry?: ProviderRegistry;\n\n constructor(options: ConfigManagerOptions) {\n this.cwd = options.cwd;\n this.afs = options.afs;\n this.registry = options.registry;\n this.onConfigChanged = options.onConfigChanged;\n this.configManagedPaths = new Set(options.configMountPaths ?? []);\n if (options.failures) this._failures = [...options.failures];\n }\n\n async getConfig(): Promise<unknown> {\n const loader = new ConfigLoader();\n return loader.load(this.cwd);\n }\n\n async getMountList(): Promise<{ mounts: unknown[]; failures: unknown[] }> {\n const mounts = this.afs.getMounts();\n const mountInfos = await Promise.all(\n mounts.map(async (m) => {\n const ctor = m.module.constructor as unknown as Record<string, unknown>;\n const manifest =\n typeof ctor.manifest === \"function\" ? (ctor.manifest() as Record<string, unknown>) : null;\n\n // Try to get URL from provider meta (e.g. UI providers with web servers)\n let url: string | undefined;\n try {\n if (typeof m.module.stat === \"function\") {\n const statResult = await m.module.stat(\"/\", {});\n url = (statResult?.data?.meta?.url as string) || undefined;\n }\n } catch {\n // Provider may not support stat — ignore\n }\n\n return {\n namespace: m.namespace,\n path: m.path,\n name: m.module.name,\n description: m.module.description,\n accessMode: m.module.accessMode,\n category: (manifest?.category as string) || undefined,\n tags: (manifest?.tags as string[]) || undefined,\n uri: (manifest?.uriTemplate as string) || undefined,\n url,\n };\n }),\n );\n return { mounts: mountInfos, failures: this._failures };\n }\n\n async addMount(mount: Record<string, unknown>): Promise<void> {\n const uri = mount.uri as string;\n const path = mount.path as string;\n if (!uri || !path) throw new Error(\"uri and path are required\");\n\n // Build mount config with credential resolution (env, store, auth→field mapping)\n const mountConfig = await this.buildMountWithCredentials(\n uri,\n path,\n mount.auth as string | undefined,\n {\n description: mount.description as string | undefined,\n access_mode: mount.accessMode as \"readonly\" | \"readwrite\" | undefined,\n namespace: mount.namespace as string | undefined,\n options: mount.options as Record<string, unknown> | undefined,\n },\n );\n\n const registry = this.registry ?? new ProviderRegistry();\n const provider = await registry.createProvider(mountConfig);\n await this.afs.mount(provider, path, {\n namespace: mountConfig.namespace ?? null,\n });\n\n // Persist to config.toml and track as config-managed\n await this.persistAddMount(mountConfig);\n this.configManagedPaths.add(path);\n }\n\n async removeMount(path: string): Promise<void> {\n // Unmount from AFS\n this.afs.unmount(path);\n\n // Remove from config.toml and stop tracking\n await this.persistRemoveMount(path);\n this.configManagedPaths.delete(path);\n }\n\n async updateMount(path: string, updates: Record<string, unknown>): Promise<void> {\n // Update config.toml\n const config = await this.readConfigFile();\n const mounts = (config.mounts as ConfigMountEntry[]) ?? [];\n const entry = mounts.find((m) => m.path === path);\n if (!entry) throw new Error(`Mount \"${path}\" not found in config`);\n\n if (updates.description !== undefined) entry.description = updates.description as string;\n if (updates.accessMode !== undefined)\n entry.access_mode = updates.accessMode as \"readonly\" | \"readwrite\";\n if (updates.auth !== undefined) entry.auth = updates.auth as string;\n\n await this.writeConfigFile(config);\n\n // Re-mount with updated config: unmount + remount\n try {\n this.afs.unmount(path);\n } catch {\n // May not be mounted\n }\n\n const mount = await this.buildMountWithCredentials(entry.uri, entry.path, entry.auth, {\n description: entry.description,\n access_mode: entry.access_mode,\n namespace: entry.namespace,\n options: entry.options,\n });\n\n const registry = new ProviderRegistry();\n const provider = await registry.createProvider(mount);\n await this.afs.mount(provider, path, {\n namespace: entry.namespace ?? null,\n });\n }\n\n async testMount(\n uri: string,\n auth?: string,\n ): Promise<{ success: boolean; error?: string; providerName?: string }> {\n try {\n const registry = new ProviderRegistry();\n const mount = await this.buildMountWithCredentials(uri, \"/test\", auth);\n const provider = await registry.createProvider(mount);\n\n // Try listing root to verify connection\n await provider.list!(\"/\", {});\n const providerName = provider.name;\n\n // Clean up\n try {\n await provider.close?.();\n } catch {\n // ignore\n }\n\n return { success: true, providerName };\n } catch (err) {\n return {\n success: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n async reloadConfig(): Promise<void> {\n const loader = new ConfigLoader();\n const newConfig = await loader.load(this.cwd);\n\n const currentMounts = new Set(this.afs.getMounts().map((m) => m.path));\n const newMountPaths = new Set(newConfig.mounts.map((m) => m.path));\n\n // Find removed mounts — only check config-managed paths (leave code-managed providers untouched)\n const removed: string[] = [];\n for (const path of this.configManagedPaths) {\n if (!newMountPaths.has(path)) {\n try {\n this.afs.unmount(path);\n removed.push(path);\n } catch {\n // ignore\n }\n this.configManagedPaths.delete(path);\n }\n }\n\n // Find added mounts\n const added: string[] = [];\n const registry = new ProviderRegistry();\n for (const mount of newConfig.mounts) {\n if (!currentMounts.has(mount.path)) {\n try {\n const provider = await registry.createProvider(mount);\n await this.afs.mount(provider, mount.path, {\n namespace: mount.namespace ?? null,\n });\n added.push(mount.path);\n this.configManagedPaths.add(mount.path);\n } catch (err) {\n console.warn(\n `[reload] Failed to mount ${mount.path}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n if (this.onConfigChanged && (added.length > 0 || removed.length > 0)) {\n this.onConfigChanged(added, removed);\n }\n }\n\n /**\n * Start watching config file for external changes.\n */\n startWatching(): void {\n const configPath = this.getConfigPath();\n try {\n this.watcher = watch(dirname(configPath), (_event, filename) => {\n if (filename !== \"config.toml\") return;\n\n // Skip self-write events\n if (Date.now() - this._selfWriteTimestamp < 500) return;\n\n // Debounce\n if (this._debounceTimer) clearTimeout(this._debounceTimer);\n this._debounceTimer = setTimeout(() => {\n this.reloadConfig().catch((err) => {\n console.warn(\n `[watch] Reload failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n });\n }, 300);\n });\n } catch {\n // fs.watch may not be supported\n }\n }\n\n async getRegistry(): Promise<\n Array<{\n name: string;\n description: string;\n category: string;\n uriTemplate: string;\n tags?: string[];\n packageName?: string;\n }>\n > {\n try {\n const { scanInstalledProviders } = await import(\"@aigne/afs-registry\");\n const manifests = await scanInstalledProviders();\n return manifests.map((m) => ({\n name: m.name,\n description: m.description,\n category: m.category,\n uriTemplate: m.uriTemplate,\n tags: m.tags,\n }));\n } catch {\n return [];\n }\n }\n\n stopWatching(): void {\n if (this.watcher) {\n this.watcher.close();\n this.watcher = undefined;\n }\n if (this._debounceTimer) {\n clearTimeout(this._debounceTimer);\n this._debounceTimer = undefined;\n }\n }\n\n // ── Private helpers ──\n\n /**\n * Build a MountConfig with proper credential mapping.\n *\n * Uses the provider schema to:\n * 1. Resolve credentials from env vars (e.g. AIGNE_HUB_API_KEY)\n * 2. Resolve credentials from the credential store (vault)\n * 3. Map generic `auth` to the provider's specific credential field (e.g. apiKey)\n */\n private async buildMountWithCredentials(\n uri: string,\n path: string,\n auth?: string,\n extra?: {\n description?: string;\n access_mode?: \"readonly\" | \"readwrite\";\n namespace?: string;\n options?: Record<string, unknown>;\n },\n ): Promise<MountConfig> {\n const mount: MountConfig = {\n uri,\n path,\n auth,\n description: extra?.description,\n access_mode: extra?.access_mode,\n namespace: extra?.namespace,\n options: extra?.options ? { ...extra.options } : undefined,\n };\n\n try {\n const registry = new ProviderRegistry();\n const info = await registry.getProviderInfo(uri);\n if (!info?.schema) return mount;\n\n const { getSensitiveFields, resolveEnvFromSchema } = await import(\"@aigne/afs/utils/schema\");\n const opts = mount.options ?? {};\n\n // Step 1: Resolve env vars declared in schema (e.g. AIGNE_HUB_API_KEY → apiKey)\n const envResolved = resolveEnvFromSchema(info.schema);\n for (const [field, value] of Object.entries(envResolved)) {\n if (opts[field] === undefined) {\n opts[field] = value;\n }\n }\n\n // Step 2: Resolve from credential store\n try {\n const { createCredentialStore } = await import(\"../credential/store.js\");\n const store = createCredentialStore();\n const stored = await store.get(uri);\n if (stored) {\n for (const [field, value] of Object.entries(stored)) {\n if (!field.startsWith(\"env:\") && opts[field] === undefined) {\n opts[field] = value;\n }\n }\n }\n } catch {\n // Credential store unavailable — continue\n }\n\n // Step 3: Map generic auth → provider's sensitive credential field\n // e.g. auth token → apiKey for aignehub, token for github\n if (auth) {\n const sensitiveFields = getSensitiveFields(info.schema);\n for (const field of sensitiveFields) {\n if (field !== \"auth\" && field !== \"token\" && opts[field] === undefined) {\n opts[field] = auth;\n break;\n }\n }\n }\n\n if (Object.keys(opts).length > 0) {\n mount.options = opts;\n }\n } catch {\n // Schema resolution failed — return mount as-is\n }\n\n return mount;\n }\n\n private getConfigPath(): string {\n return join(this.cwd, \".afs-config\", \"config.toml\");\n }\n\n private async readConfigFile(): Promise<Record<string, unknown>> {\n const configPath = this.getConfigPath();\n try {\n const content = await readFile(configPath, \"utf-8\");\n return parse(content) as Record<string, unknown>;\n } catch {\n return { mounts: [] };\n }\n }\n\n private async writeConfigFile(config: Record<string, unknown>): Promise<void> {\n const configPath = this.getConfigPath();\n const configDir = dirname(configPath);\n try {\n await mkdir(configDir, { recursive: true });\n } catch {\n // may exist\n }\n this._selfWriteTimestamp = Date.now();\n await writeFile(configPath, stringify(config), \"utf-8\");\n }\n\n private async persistAddMount(mount: MountConfig): Promise<void> {\n const config = await this.readConfigFile();\n const mounts = (config.mounts as ConfigMountEntry[]) ?? [];\n\n const entry: ConfigMountEntry = {\n path: mount.path,\n uri: mount.uri,\n };\n if (mount.description) entry.description = mount.description;\n if (mount.access_mode) entry.access_mode = mount.access_mode;\n if (mount.auth) entry.auth = mount.auth;\n if (mount.namespace) entry.namespace = mount.namespace;\n if (mount.options) entry.options = mount.options;\n\n // Upsert\n const existing = mounts.findIndex((m) => m.path === mount.path);\n if (existing >= 0) {\n mounts[existing] = entry;\n } else {\n mounts.push(entry);\n }\n config.mounts = mounts;\n\n await this.writeConfigFile(config);\n }\n\n private async persistRemoveMount(path: string): Promise<void> {\n const config = await this.readConfigFile();\n const mounts = (config.mounts as ConfigMountEntry[]) ?? [];\n config.mounts = mounts.filter((m) => m.path !== path);\n await this.writeConfigFile(config);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,164 @@
1
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
2
+ let node_child_process = require("node:child_process");
3
+ let node_fs_promises = require("node:fs/promises");
4
+ let node_os = require("node:os");
5
+ let node_path = require("node:path");
6
+
7
+ //#region src/daemon/manager.ts
8
+ /**
9
+ * Daemon Process Manager
10
+ *
11
+ * Manages PID/port files in ~/.afs/ and provides daemon lifecycle operations.
12
+ */
13
+ const DAEMON_DIR = (0, node_path.join)((0, node_os.homedir)(), ".afs");
14
+ const PID_FILE = (0, node_path.join)(DAEMON_DIR, "daemon.pid");
15
+ const PORT_FILE = (0, node_path.join)(DAEMON_DIR, "daemon.port");
16
+ const LOG_FILE = (0, node_path.join)(DAEMON_DIR, "daemon.log");
17
+ function getLogFile() {
18
+ return LOG_FILE;
19
+ }
20
+ async function ensureDaemonDir() {
21
+ await (0, node_fs_promises.mkdir)(DAEMON_DIR, { recursive: true });
22
+ }
23
+ async function writePidFile(pid) {
24
+ await ensureDaemonDir();
25
+ await (0, node_fs_promises.writeFile)(PID_FILE, String(pid), "utf-8");
26
+ }
27
+ async function writePortFile(port) {
28
+ await ensureDaemonDir();
29
+ await (0, node_fs_promises.writeFile)(PORT_FILE, String(port), "utf-8");
30
+ }
31
+ async function readPidFile() {
32
+ try {
33
+ const content = await (0, node_fs_promises.readFile)(PID_FILE, "utf-8");
34
+ const pid = Number.parseInt(content.trim(), 10);
35
+ return Number.isNaN(pid) ? null : pid;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ async function readPortFile() {
41
+ try {
42
+ const content = await (0, node_fs_promises.readFile)(PORT_FILE, "utf-8");
43
+ const port = Number.parseInt(content.trim(), 10);
44
+ return Number.isNaN(port) ? null : port;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+ async function cleanPidFiles() {
50
+ try {
51
+ await (0, node_fs_promises.rm)(PID_FILE, { force: true });
52
+ } catch {}
53
+ try {
54
+ await (0, node_fs_promises.rm)(PORT_FILE, { force: true });
55
+ } catch {}
56
+ }
57
+ /**
58
+ * Check if a process is alive by sending signal 0.
59
+ */
60
+ function isProcessAlive(pid) {
61
+ try {
62
+ process.kill(pid, 0);
63
+ return true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ /**
69
+ * Check if daemon is currently running.
70
+ * Returns DaemonInfo if alive, null if not running (cleans stale PID files).
71
+ */
72
+ async function getDaemonStatus() {
73
+ const pid = await readPidFile();
74
+ if (pid === null) return null;
75
+ if (!isProcessAlive(pid)) {
76
+ await cleanPidFiles();
77
+ return null;
78
+ }
79
+ const port = await readPortFile();
80
+ if (port === null) return null;
81
+ return {
82
+ pid,
83
+ port,
84
+ url: `http://localhost:${port}`,
85
+ mcpUrl: `http://localhost:${port}/mcp`
86
+ };
87
+ }
88
+ /**
89
+ * Stop the daemon by sending SIGTERM to the PID.
90
+ */
91
+ async function stopDaemon() {
92
+ const pid = await readPidFile();
93
+ if (pid === null) return false;
94
+ if (!isProcessAlive(pid)) {
95
+ await cleanPidFiles();
96
+ return false;
97
+ }
98
+ try {
99
+ process.kill(pid, "SIGTERM");
100
+ } catch {}
101
+ for (let i = 0; i < 50; i++) {
102
+ await new Promise((r) => setTimeout(r, 100));
103
+ if (!isProcessAlive(pid)) {
104
+ await cleanPidFiles();
105
+ return true;
106
+ }
107
+ }
108
+ try {
109
+ process.kill(pid, "SIGKILL");
110
+ } catch {}
111
+ await cleanPidFiles();
112
+ return true;
113
+ }
114
+ /**
115
+ * Spawn a detached daemon child process (`afs daemon _run`).
116
+ *
117
+ * The parent opens a log file, spawns the child detached, then polls for
118
+ * the port file the child writes on successful startup.
119
+ *
120
+ * Returns DaemonInfo once the child is confirmed running, or throws on timeout.
121
+ */
122
+ async function spawnDaemon(port) {
123
+ await ensureDaemonDir();
124
+ const logFd = await (0, node_fs_promises.open)(LOG_FILE, "w");
125
+ const fd = logFd.fd;
126
+ const args = [
127
+ ...process.execArgv,
128
+ process.argv[1],
129
+ "service",
130
+ "_run",
131
+ "--port",
132
+ String(port)
133
+ ];
134
+ const opts = {
135
+ detached: true,
136
+ stdio: [
137
+ "ignore",
138
+ fd,
139
+ fd
140
+ ],
141
+ env: process.env
142
+ };
143
+ (0, node_child_process.spawn)(process.execPath, args, opts).unref();
144
+ await logFd.close();
145
+ const maxWait = 15e3;
146
+ const interval = 100;
147
+ const deadline = Date.now() + maxWait;
148
+ while (Date.now() < deadline) {
149
+ const info = await getDaemonStatus();
150
+ if (info) return info;
151
+ await new Promise((r) => setTimeout(r, interval));
152
+ }
153
+ throw new Error(`Daemon did not start within ${maxWait / 1e3}s — check ${LOG_FILE}`);
154
+ }
155
+
156
+ //#endregion
157
+ exports.cleanPidFiles = cleanPidFiles;
158
+ exports.ensureDaemonDir = ensureDaemonDir;
159
+ exports.getDaemonStatus = getDaemonStatus;
160
+ exports.getLogFile = getLogFile;
161
+ exports.spawnDaemon = spawnDaemon;
162
+ exports.stopDaemon = stopDaemon;
163
+ exports.writePidFile = writePidFile;
164
+ exports.writePortFile = writePortFile;
@@ -0,0 +1,157 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, open, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ //#region src/daemon/manager.ts
7
+ /**
8
+ * Daemon Process Manager
9
+ *
10
+ * Manages PID/port files in ~/.afs/ and provides daemon lifecycle operations.
11
+ */
12
+ const DAEMON_DIR = join(homedir(), ".afs");
13
+ const PID_FILE = join(DAEMON_DIR, "daemon.pid");
14
+ const PORT_FILE = join(DAEMON_DIR, "daemon.port");
15
+ const LOG_FILE = join(DAEMON_DIR, "daemon.log");
16
+ function getLogFile() {
17
+ return LOG_FILE;
18
+ }
19
+ async function ensureDaemonDir() {
20
+ await mkdir(DAEMON_DIR, { recursive: true });
21
+ }
22
+ async function writePidFile(pid) {
23
+ await ensureDaemonDir();
24
+ await writeFile(PID_FILE, String(pid), "utf-8");
25
+ }
26
+ async function writePortFile(port) {
27
+ await ensureDaemonDir();
28
+ await writeFile(PORT_FILE, String(port), "utf-8");
29
+ }
30
+ async function readPidFile() {
31
+ try {
32
+ const content = await readFile(PID_FILE, "utf-8");
33
+ const pid = Number.parseInt(content.trim(), 10);
34
+ return Number.isNaN(pid) ? null : pid;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ async function readPortFile() {
40
+ try {
41
+ const content = await readFile(PORT_FILE, "utf-8");
42
+ const port = Number.parseInt(content.trim(), 10);
43
+ return Number.isNaN(port) ? null : port;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ async function cleanPidFiles() {
49
+ try {
50
+ await rm(PID_FILE, { force: true });
51
+ } catch {}
52
+ try {
53
+ await rm(PORT_FILE, { force: true });
54
+ } catch {}
55
+ }
56
+ /**
57
+ * Check if a process is alive by sending signal 0.
58
+ */
59
+ function isProcessAlive(pid) {
60
+ try {
61
+ process.kill(pid, 0);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ /**
68
+ * Check if daemon is currently running.
69
+ * Returns DaemonInfo if alive, null if not running (cleans stale PID files).
70
+ */
71
+ async function getDaemonStatus() {
72
+ const pid = await readPidFile();
73
+ if (pid === null) return null;
74
+ if (!isProcessAlive(pid)) {
75
+ await cleanPidFiles();
76
+ return null;
77
+ }
78
+ const port = await readPortFile();
79
+ if (port === null) return null;
80
+ return {
81
+ pid,
82
+ port,
83
+ url: `http://localhost:${port}`,
84
+ mcpUrl: `http://localhost:${port}/mcp`
85
+ };
86
+ }
87
+ /**
88
+ * Stop the daemon by sending SIGTERM to the PID.
89
+ */
90
+ async function stopDaemon() {
91
+ const pid = await readPidFile();
92
+ if (pid === null) return false;
93
+ if (!isProcessAlive(pid)) {
94
+ await cleanPidFiles();
95
+ return false;
96
+ }
97
+ try {
98
+ process.kill(pid, "SIGTERM");
99
+ } catch {}
100
+ for (let i = 0; i < 50; i++) {
101
+ await new Promise((r) => setTimeout(r, 100));
102
+ if (!isProcessAlive(pid)) {
103
+ await cleanPidFiles();
104
+ return true;
105
+ }
106
+ }
107
+ try {
108
+ process.kill(pid, "SIGKILL");
109
+ } catch {}
110
+ await cleanPidFiles();
111
+ return true;
112
+ }
113
+ /**
114
+ * Spawn a detached daemon child process (`afs daemon _run`).
115
+ *
116
+ * The parent opens a log file, spawns the child detached, then polls for
117
+ * the port file the child writes on successful startup.
118
+ *
119
+ * Returns DaemonInfo once the child is confirmed running, or throws on timeout.
120
+ */
121
+ async function spawnDaemon(port) {
122
+ await ensureDaemonDir();
123
+ const logFd = await open(LOG_FILE, "w");
124
+ const fd = logFd.fd;
125
+ const args = [
126
+ ...process.execArgv,
127
+ process.argv[1],
128
+ "service",
129
+ "_run",
130
+ "--port",
131
+ String(port)
132
+ ];
133
+ const opts = {
134
+ detached: true,
135
+ stdio: [
136
+ "ignore",
137
+ fd,
138
+ fd
139
+ ],
140
+ env: process.env
141
+ };
142
+ spawn(process.execPath, args, opts).unref();
143
+ await logFd.close();
144
+ const maxWait = 15e3;
145
+ const interval = 100;
146
+ const deadline = Date.now() + maxWait;
147
+ while (Date.now() < deadline) {
148
+ const info = await getDaemonStatus();
149
+ if (info) return info;
150
+ await new Promise((r) => setTimeout(r, interval));
151
+ }
152
+ throw new Error(`Daemon did not start within ${maxWait / 1e3}s — check ${LOG_FILE}`);
153
+ }
154
+
155
+ //#endregion
156
+ export { cleanPidFiles, ensureDaemonDir, getDaemonStatus, getLogFile, spawnDaemon, stopDaemon, writePidFile, writePortFile };
157
+ //# sourceMappingURL=manager.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.mjs","names":[],"sources":["../../src/daemon/manager.ts"],"sourcesContent":["/**\n * Daemon Process Manager\n *\n * Manages PID/port files in ~/.afs/ and provides daemon lifecycle operations.\n */\n\nimport { type SpawnOptions, spawn } from \"node:child_process\";\nimport { mkdir, open, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport interface DaemonInfo {\n pid: number;\n port: number;\n url: string;\n mcpUrl: string;\n}\n\nconst DAEMON_DIR = join(homedir(), \".afs\");\nconst PID_FILE = join(DAEMON_DIR, \"daemon.pid\");\nconst PORT_FILE = join(DAEMON_DIR, \"daemon.port\");\nconst LOG_FILE = join(DAEMON_DIR, \"daemon.log\");\n\nexport function getDaemonDir(): string {\n return DAEMON_DIR;\n}\n\nexport function getLogFile(): string {\n return LOG_FILE;\n}\n\nexport async function ensureDaemonDir(): Promise<void> {\n await mkdir(DAEMON_DIR, { recursive: true });\n}\n\nexport async function writePidFile(pid: number): Promise<void> {\n await ensureDaemonDir();\n await writeFile(PID_FILE, String(pid), \"utf-8\");\n}\n\nexport async function writePortFile(port: number): Promise<void> {\n await ensureDaemonDir();\n await writeFile(PORT_FILE, String(port), \"utf-8\");\n}\n\nexport async function readPidFile(): Promise<number | null> {\n try {\n const content = await readFile(PID_FILE, \"utf-8\");\n const pid = Number.parseInt(content.trim(), 10);\n return Number.isNaN(pid) ? null : pid;\n } catch {\n return null;\n }\n}\n\nexport async function readPortFile(): Promise<number | null> {\n try {\n const content = await readFile(PORT_FILE, \"utf-8\");\n const port = Number.parseInt(content.trim(), 10);\n return Number.isNaN(port) ? null : port;\n } catch {\n return null;\n }\n}\n\nexport async function cleanPidFiles(): Promise<void> {\n try {\n await rm(PID_FILE, { force: true });\n } catch {\n // ignore\n }\n try {\n await rm(PORT_FILE, { force: true });\n } catch {\n // ignore\n }\n}\n\n/**\n * Check if a process is alive by sending signal 0.\n */\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if daemon is currently running.\n * Returns DaemonInfo if alive, null if not running (cleans stale PID files).\n */\nexport async function getDaemonStatus(): Promise<DaemonInfo | null> {\n const pid = await readPidFile();\n if (pid === null) return null;\n\n if (!isProcessAlive(pid)) {\n // Stale PID file — clean up\n await cleanPidFiles();\n return null;\n }\n\n const port = await readPortFile();\n if (port === null) return null;\n\n return {\n pid,\n port,\n url: `http://localhost:${port}`,\n mcpUrl: `http://localhost:${port}/mcp`,\n };\n}\n\n/**\n * Stop the daemon by sending SIGTERM to the PID.\n */\nexport async function stopDaemon(): Promise<boolean> {\n const pid = await readPidFile();\n if (pid === null) return false;\n\n if (!isProcessAlive(pid)) {\n await cleanPidFiles();\n return false;\n }\n\n try {\n process.kill(pid, \"SIGTERM\");\n } catch {\n // Process may have already exited\n }\n\n // Wait for process to exit (up to 5 seconds)\n for (let i = 0; i < 50; i++) {\n await new Promise((r) => setTimeout(r, 100));\n if (!isProcessAlive(pid)) {\n await cleanPidFiles();\n return true;\n }\n }\n\n // Force kill if still alive\n try {\n process.kill(pid, \"SIGKILL\");\n } catch {\n // ignore\n }\n await cleanPidFiles();\n return true;\n}\n\n/**\n * Spawn a detached daemon child process (`afs daemon _run`).\n *\n * The parent opens a log file, spawns the child detached, then polls for\n * the port file the child writes on successful startup.\n *\n * Returns DaemonInfo once the child is confirmed running, or throws on timeout.\n */\nexport async function spawnDaemon(port: number): Promise<DaemonInfo> {\n await ensureDaemonDir();\n\n // Truncate / create log file\n const logFd = await open(LOG_FILE, \"w\");\n const fd = logFd.fd;\n\n // No --cwd: _run defaults to homedir() for config discovery.\n // The daemon serves a global AFS instance, independent of caller's cwd.\n const args = [...process.execArgv, process.argv[1]!, \"service\", \"_run\", \"--port\", String(port)];\n const opts: SpawnOptions = {\n detached: true,\n stdio: [\"ignore\", fd, fd],\n env: process.env,\n };\n\n const child = spawn(process.execPath, args, opts);\n child.unref();\n\n // Close our handle — the child inherited the fd\n await logFd.close();\n\n // Poll for port file (child writes it on successful startup)\n const maxWait = 15_000;\n const interval = 100;\n const deadline = Date.now() + maxWait;\n\n while (Date.now() < deadline) {\n const info = await getDaemonStatus();\n if (info) return info;\n await new Promise((r) => setTimeout(r, interval));\n }\n\n throw new Error(`Daemon did not start within ${maxWait / 1000}s — check ${LOG_FILE}`);\n}\n"],"mappings":"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"}