@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.
- package/dist/cli.cjs +3 -2
- package/dist/cli.mjs +3 -2
- package/dist/cli.mjs.map +1 -1
- package/dist/config/afs-loader.cjs +36 -315
- package/dist/config/afs-loader.d.cts.map +1 -1
- package/dist/config/afs-loader.d.mts +2 -1
- package/dist/config/afs-loader.d.mts.map +1 -1
- package/dist/config/afs-loader.mjs +28 -307
- package/dist/config/afs-loader.mjs.map +1 -1
- package/dist/config/credential-helpers.cjs +303 -0
- package/dist/config/credential-helpers.d.mts +2 -0
- package/dist/config/credential-helpers.mjs +300 -0
- package/dist/config/credential-helpers.mjs.map +1 -0
- package/dist/config/loader.cjs +3 -1
- package/dist/config/loader.mjs +3 -2
- package/dist/config/loader.mjs.map +1 -1
- package/dist/config/program-install.cjs +450 -0
- package/dist/config/program-install.d.mts +1 -0
- package/dist/config/program-install.mjs +444 -0
- package/dist/config/program-install.mjs.map +1 -0
- package/dist/core/commands/connect.cjs +53 -0
- package/dist/core/commands/connect.d.mts +2 -0
- package/dist/core/commands/connect.mjs +55 -0
- package/dist/core/commands/connect.mjs.map +1 -0
- package/dist/core/commands/daemon.cjs +211 -0
- package/dist/core/commands/daemon.d.mts +2 -0
- package/dist/core/commands/daemon.mjs +212 -0
- package/dist/core/commands/daemon.mjs.map +1 -0
- package/dist/core/commands/explain.cjs +3 -1
- package/dist/core/commands/explain.mjs +3 -1
- package/dist/core/commands/explain.mjs.map +1 -1
- package/dist/core/commands/explore.cjs +47 -12
- package/dist/core/commands/explore.mjs +47 -12
- package/dist/core/commands/explore.mjs.map +1 -1
- package/dist/core/commands/gen-agent-md.cjs +126 -0
- package/dist/core/commands/gen-agent-md.d.mts +2 -0
- package/dist/core/commands/gen-agent-md.mjs +125 -0
- package/dist/core/commands/gen-agent-md.mjs.map +1 -0
- package/dist/core/commands/index.cjs +13 -1
- package/dist/core/commands/index.d.cts.map +1 -1
- package/dist/core/commands/index.d.mts +6 -0
- package/dist/core/commands/index.d.mts.map +1 -1
- package/dist/core/commands/index.mjs +13 -1
- package/dist/core/commands/index.mjs.map +1 -1
- package/dist/core/commands/install.cjs +139 -0
- package/dist/core/commands/install.d.mts +2 -0
- package/dist/core/commands/install.mjs +140 -0
- package/dist/core/commands/install.mjs.map +1 -0
- package/dist/core/commands/ls.cjs +14 -2
- package/dist/core/commands/ls.d.cts +2 -0
- package/dist/core/commands/ls.d.cts.map +1 -1
- package/dist/core/commands/ls.d.mts +2 -0
- package/dist/core/commands/ls.d.mts.map +1 -1
- package/dist/core/commands/ls.mjs +14 -2
- package/dist/core/commands/ls.mjs.map +1 -1
- package/dist/core/commands/mcp-bridge.cjs +201 -0
- package/dist/core/commands/mcp-bridge.d.mts +2 -0
- package/dist/core/commands/mcp-bridge.mjs +201 -0
- package/dist/core/commands/mcp-bridge.mjs.map +1 -0
- package/dist/core/commands/read.cjs +20 -7
- package/dist/core/commands/read.d.cts +2 -0
- package/dist/core/commands/read.d.cts.map +1 -1
- package/dist/core/commands/read.d.mts +2 -0
- package/dist/core/commands/read.d.mts.map +1 -1
- package/dist/core/commands/read.mjs +20 -7
- package/dist/core/commands/read.mjs.map +1 -1
- package/dist/core/commands/search.cjs +5 -1
- package/dist/core/commands/search.mjs +5 -1
- package/dist/core/commands/search.mjs.map +1 -1
- package/dist/core/commands/stat.mjs.map +1 -1
- package/dist/core/commands/types.d.cts +2 -0
- package/dist/core/commands/types.d.cts.map +1 -1
- package/dist/core/commands/types.d.mts +2 -0
- package/dist/core/commands/types.d.mts.map +1 -1
- package/dist/core/commands/types.mjs.map +1 -1
- package/dist/core/commands/vault.cjs +289 -0
- package/dist/core/commands/vault.d.mts +2 -0
- package/dist/core/commands/vault.mjs +289 -0
- package/dist/core/commands/vault.mjs.map +1 -0
- package/dist/core/commands/write.cjs +19 -6
- package/dist/core/commands/write.d.cts +2 -1
- package/dist/core/commands/write.d.cts.map +1 -1
- package/dist/core/commands/write.d.mts +2 -1
- package/dist/core/commands/write.d.mts.map +1 -1
- package/dist/core/commands/write.mjs +19 -6
- package/dist/core/commands/write.mjs.map +1 -1
- package/dist/core/executor/index.cjs +95 -19
- package/dist/core/executor/index.d.cts +4 -0
- package/dist/core/executor/index.d.cts.map +1 -1
- package/dist/core/executor/index.d.mts +4 -0
- package/dist/core/executor/index.d.mts.map +1 -1
- package/dist/core/executor/index.mjs +95 -19
- package/dist/core/executor/index.mjs.map +1 -1
- package/dist/core/formatters/index.d.mts +1 -0
- package/dist/core/formatters/install.cjs +40 -0
- package/dist/core/formatters/install.d.mts +1 -0
- package/dist/core/formatters/install.mjs +36 -0
- package/dist/core/formatters/install.mjs.map +1 -0
- package/dist/core/formatters/vault.cjs +36 -0
- package/dist/core/formatters/vault.mjs +32 -0
- package/dist/core/formatters/vault.mjs.map +1 -0
- package/dist/credential/auth-server.cjs +22 -4
- package/dist/credential/auth-server.mjs +22 -4
- package/dist/credential/auth-server.mjs.map +1 -1
- package/dist/credential/index.d.mts +2 -1
- package/dist/credential/mcp-auth-context.cjs +21 -5
- package/dist/credential/mcp-auth-context.mjs +21 -5
- package/dist/credential/mcp-auth-context.mjs.map +1 -1
- package/dist/credential/resolver.cjs +11 -3
- package/dist/credential/resolver.mjs +11 -3
- package/dist/credential/resolver.mjs.map +1 -1
- package/dist/credential/vault-store.d.mts +1 -0
- package/dist/daemon/config-manager.cjs +279 -0
- package/dist/daemon/config-manager.mjs +279 -0
- package/dist/daemon/config-manager.mjs.map +1 -0
- package/dist/daemon/manager.cjs +164 -0
- package/dist/daemon/manager.mjs +157 -0
- package/dist/daemon/manager.mjs.map +1 -0
- package/dist/daemon/server.cjs +220 -0
- package/dist/daemon/server.mjs +220 -0
- package/dist/daemon/server.mjs.map +1 -0
- package/dist/mcp/http-transport.cjs +14 -1
- package/dist/mcp/http-transport.mjs +14 -1
- package/dist/mcp/http-transport.mjs.map +1 -1
- package/dist/mcp/server.cjs +4 -2
- package/dist/mcp/server.mjs +4 -2
- package/dist/mcp/server.mjs.map +1 -1
- package/dist/mcp/tools.cjs +62 -12
- package/dist/mcp/tools.mjs +62 -12
- package/dist/mcp/tools.mjs.map +1 -1
- package/dist/program/daemon-integration.cjs +46 -0
- package/dist/program/daemon-integration.mjs +45 -0
- package/dist/program/daemon-integration.mjs.map +1 -0
- package/dist/program/program-manager.cjs +166 -0
- package/dist/program/program-manager.mjs +166 -0
- package/dist/program/program-manager.mjs.map +1 -0
- package/dist/program/trigger-scanner.cjs +148 -0
- package/dist/program/trigger-scanner.mjs +148 -0
- package/dist/program/trigger-scanner.mjs.map +1 -0
- package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +11 -0
- package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs.map +1 -0
- package/dist/providers/vault/dist/encrypted-file.cjs +158 -0
- package/dist/providers/vault/dist/encrypted-file.mjs +153 -0
- package/dist/providers/vault/dist/encrypted-file.mjs.map +1 -0
- package/dist/providers/vault/dist/index.cjs +405 -0
- package/dist/providers/vault/dist/index.mjs +400 -0
- package/dist/providers/vault/dist/index.mjs.map +1 -0
- package/dist/providers/vault/dist/key-resolver.cjs +181 -0
- package/dist/providers/vault/dist/key-resolver.mjs +180 -0
- package/dist/providers/vault/dist/key-resolver.mjs.map +1 -0
- package/dist/repl.cjs +109 -14
- package/dist/repl.d.cts.map +1 -1
- package/dist/repl.d.mts.map +1 -1
- package/dist/repl.mjs +109 -14
- package/dist/repl.mjs.map +1 -1
- package/package.json +27 -20
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { AFS_USER_CONFIG_DIR_ENV, CONFIG_DIR_NAME } from "./loader.mjs";
|
|
2
|
+
import { persistMount, unpersistMount } from "./mount-commands.mjs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { cp, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { parseProgramManifest } from "@aigne/afs";
|
|
8
|
+
|
|
9
|
+
//#region src/config/program-install.ts
|
|
10
|
+
/**
|
|
11
|
+
* Program Installation
|
|
12
|
+
*
|
|
13
|
+
* Business logic for installing, listing, and removing AFS programs.
|
|
14
|
+
* Programs are installed to ~/.afs-config/programs/<id>/ and mounted
|
|
15
|
+
* at /programs/<id> in the CWD config (same as `mount add`).
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Get the user config directory.
|
|
19
|
+
*/
|
|
20
|
+
function getUserConfigDir(override) {
|
|
21
|
+
return override ?? process.env[AFS_USER_CONFIG_DIR_ENV] ?? join(homedir(), CONFIG_DIR_NAME);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Detect the source type from a source string.
|
|
25
|
+
*/
|
|
26
|
+
function detectSourceType(source) {
|
|
27
|
+
if (source.endsWith(".zip")) return "zip";
|
|
28
|
+
if (source.startsWith("https://github.com/") || source.startsWith("http://github.com/") || source.startsWith("github.com/")) return "github";
|
|
29
|
+
return "local";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse a GitHub URL into owner, repo, ref, and optional subdirectory.
|
|
33
|
+
*/
|
|
34
|
+
function parseGitHubURL(url) {
|
|
35
|
+
let normalized = url;
|
|
36
|
+
if (normalized.startsWith("github.com/")) normalized = `https://${normalized}`;
|
|
37
|
+
const match = normalized.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)\/(.+))?$/);
|
|
38
|
+
if (!match) return { cloneUrl: normalized.replace(/\/+$/, "") };
|
|
39
|
+
const [, owner, repo, _ref, subdir] = match;
|
|
40
|
+
return {
|
|
41
|
+
cloneUrl: `https://github.com/${owner}/${repo}.git`,
|
|
42
|
+
subdir: subdir || void 0
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a source to a local directory path containing program.yaml.
|
|
47
|
+
* For github/zip sources, downloads/extracts to a temp directory.
|
|
48
|
+
*
|
|
49
|
+
* Returns [resolvedPath, tempDir?] — tempDir is set if caller should clean up.
|
|
50
|
+
*/
|
|
51
|
+
async function resolveSource(source, type, cwd) {
|
|
52
|
+
switch (type) {
|
|
53
|
+
case "local": {
|
|
54
|
+
const dirPath = isAbsolute(source) ? source : resolve(cwd, source);
|
|
55
|
+
try {
|
|
56
|
+
await readdir(dirPath);
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(`Source directory does not exist: ${dirPath}`);
|
|
59
|
+
}
|
|
60
|
+
return [dirPath, void 0];
|
|
61
|
+
}
|
|
62
|
+
case "github": {
|
|
63
|
+
const { cloneUrl, subdir } = parseGitHubURL(source);
|
|
64
|
+
const tempDir = join(tmpdir(), `afs-install-${Date.now()}`);
|
|
65
|
+
await mkdir(tempDir, { recursive: true });
|
|
66
|
+
try {
|
|
67
|
+
execSync(`git clone --depth 1 ${cloneUrl} ${join(tempDir, "repo")}`, {
|
|
68
|
+
stdio: "pipe",
|
|
69
|
+
timeout: 6e4
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
await rm(tempDir, {
|
|
73
|
+
recursive: true,
|
|
74
|
+
force: true
|
|
75
|
+
});
|
|
76
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
77
|
+
throw new Error(`Failed to clone GitHub repository: ${msg}`);
|
|
78
|
+
}
|
|
79
|
+
return [subdir ? join(tempDir, "repo", subdir) : join(tempDir, "repo"), tempDir];
|
|
80
|
+
}
|
|
81
|
+
case "zip": {
|
|
82
|
+
const zipPath = isAbsolute(source) ? source : resolve(cwd, source);
|
|
83
|
+
const tempDir = join(tmpdir(), `afs-install-${Date.now()}`);
|
|
84
|
+
const extractDir = join(tempDir, "extracted");
|
|
85
|
+
await mkdir(extractDir, { recursive: true });
|
|
86
|
+
try {
|
|
87
|
+
const lines = execSync(`unzip -l "${zipPath}"`, {
|
|
88
|
+
encoding: "utf-8",
|
|
89
|
+
stdio: [
|
|
90
|
+
"pipe",
|
|
91
|
+
"pipe",
|
|
92
|
+
"pipe"
|
|
93
|
+
]
|
|
94
|
+
}).split("\n");
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (!trimmed || trimmed.startsWith("Archive:") || trimmed.startsWith("Length") || trimmed.startsWith("---")) continue;
|
|
98
|
+
const parts = trimmed.split(/\s+/);
|
|
99
|
+
if (parts.length >= 4) {
|
|
100
|
+
const name = parts.slice(3).join(" ");
|
|
101
|
+
if (name.includes("..") || name.startsWith("/")) {
|
|
102
|
+
await rm(tempDir, {
|
|
103
|
+
recursive: true,
|
|
104
|
+
force: true
|
|
105
|
+
});
|
|
106
|
+
throw new Error(`Zip file contains unsafe path: ${name}. Refusing to extract.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err instanceof Error && err.message.includes("unsafe path")) throw err;
|
|
112
|
+
await rm(tempDir, {
|
|
113
|
+
recursive: true,
|
|
114
|
+
force: true
|
|
115
|
+
});
|
|
116
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
117
|
+
throw new Error(`Failed to read zip file: ${msg}`);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
execSync(`unzip -o "${zipPath}" -d "${extractDir}"`, {
|
|
121
|
+
stdio: "pipe",
|
|
122
|
+
timeout: 3e4
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
await rm(tempDir, {
|
|
126
|
+
recursive: true,
|
|
127
|
+
force: true
|
|
128
|
+
});
|
|
129
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
130
|
+
throw new Error(`Failed to extract zip file: ${msg}`);
|
|
131
|
+
}
|
|
132
|
+
const entries = await readdir(extractDir);
|
|
133
|
+
if (entries.includes("program.yaml")) return [extractDir, tempDir];
|
|
134
|
+
if (entries.length === 1) {
|
|
135
|
+
const subPath = join(extractDir, entries[0]);
|
|
136
|
+
try {
|
|
137
|
+
if ((await readdir(subPath)).includes("program.yaml")) return [subPath, tempDir];
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
await rm(tempDir, {
|
|
141
|
+
recursive: true,
|
|
142
|
+
force: true
|
|
143
|
+
});
|
|
144
|
+
throw new Error("No program.yaml found in zip archive");
|
|
145
|
+
}
|
|
146
|
+
default: throw new Error(`Unsupported source type: ${type}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Validate a directory contains a valid program.yaml.
|
|
151
|
+
* Returns the parsed manifest.
|
|
152
|
+
*/
|
|
153
|
+
async function validateProgramDir(dirPath) {
|
|
154
|
+
const yamlPath = join(dirPath, "program.yaml");
|
|
155
|
+
let content;
|
|
156
|
+
try {
|
|
157
|
+
content = await readFile(yamlPath, "utf-8");
|
|
158
|
+
} catch {
|
|
159
|
+
throw new Error(`No program.yaml found in ${dirPath}`);
|
|
160
|
+
}
|
|
161
|
+
return parseProgramManifest(content);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Install a program from a source (local dir, GitHub URL, or zip file).
|
|
165
|
+
*/
|
|
166
|
+
async function installProgram(source, options) {
|
|
167
|
+
const userConfigDir = getUserConfigDir(options?.userConfigDir);
|
|
168
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
169
|
+
const [resolvedPath, tempDir] = await resolveSource(source, detectSourceType(source), cwd);
|
|
170
|
+
try {
|
|
171
|
+
const manifest = await validateProgramDir(resolvedPath);
|
|
172
|
+
const programsDir = join(userConfigDir, "programs");
|
|
173
|
+
const installPath = join(programsDir, manifest.id);
|
|
174
|
+
const mountPath = `/programs/${manifest.id}`;
|
|
175
|
+
await mkdir(programsDir, { recursive: true });
|
|
176
|
+
const tempInstallPath = `${installPath}.installing`;
|
|
177
|
+
await rm(tempInstallPath, {
|
|
178
|
+
recursive: true,
|
|
179
|
+
force: true
|
|
180
|
+
});
|
|
181
|
+
await cp(resolvedPath, tempInstallPath, { recursive: true });
|
|
182
|
+
await rm(installPath, {
|
|
183
|
+
recursive: true,
|
|
184
|
+
force: true
|
|
185
|
+
});
|
|
186
|
+
await rename(tempInstallPath, installPath);
|
|
187
|
+
await mkdir(join(userConfigDir, "data", manifest.id), { recursive: true });
|
|
188
|
+
await persistMount(cwd, {
|
|
189
|
+
path: mountPath,
|
|
190
|
+
uri: `fs://${installPath}`
|
|
191
|
+
});
|
|
192
|
+
const { notifyDaemonReload } = await import("../program/daemon-integration.mjs");
|
|
193
|
+
const { getDaemonStatus } = await import("../daemon/manager.mjs");
|
|
194
|
+
await notifyDaemonReload({ getDaemonStatus });
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
programId: manifest.id,
|
|
198
|
+
programName: manifest.name,
|
|
199
|
+
installPath,
|
|
200
|
+
mountPath
|
|
201
|
+
};
|
|
202
|
+
} finally {
|
|
203
|
+
if (tempDir) await rm(tempDir, {
|
|
204
|
+
recursive: true,
|
|
205
|
+
force: true
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* List installed programs by scanning ~/.afs-config/programs/.
|
|
211
|
+
*/
|
|
212
|
+
async function listInstalledPrograms(options) {
|
|
213
|
+
const programsDir = join(getUserConfigDir(options?.userConfigDir), "programs");
|
|
214
|
+
let entries;
|
|
215
|
+
try {
|
|
216
|
+
entries = await readdir(programsDir);
|
|
217
|
+
} catch {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
const programs = [];
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
const dirPath = join(programsDir, entry);
|
|
223
|
+
try {
|
|
224
|
+
const manifest = await validateProgramDir(dirPath);
|
|
225
|
+
programs.push({
|
|
226
|
+
id: manifest.id,
|
|
227
|
+
name: manifest.name,
|
|
228
|
+
entrypoint: manifest.entrypoint,
|
|
229
|
+
installPath: dirPath,
|
|
230
|
+
mountPath: `/programs/${manifest.id}`
|
|
231
|
+
});
|
|
232
|
+
} catch {}
|
|
233
|
+
}
|
|
234
|
+
return programs;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Remove an installed program.
|
|
238
|
+
*/
|
|
239
|
+
async function removeProgram(programId, options) {
|
|
240
|
+
const userConfigDir = getUserConfigDir(options?.userConfigDir);
|
|
241
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
242
|
+
const installPath = join(userConfigDir, "programs", programId);
|
|
243
|
+
try {
|
|
244
|
+
await readdir(installPath);
|
|
245
|
+
} catch {
|
|
246
|
+
throw new Error(`Program "${programId}" is not installed`);
|
|
247
|
+
}
|
|
248
|
+
await rm(installPath, {
|
|
249
|
+
recursive: true,
|
|
250
|
+
force: true
|
|
251
|
+
});
|
|
252
|
+
await unpersistMount(cwd, `/programs/${programId}`);
|
|
253
|
+
let purgedData = false;
|
|
254
|
+
if (options?.purge) {
|
|
255
|
+
await rm(join(userConfigDir, "data", programId), {
|
|
256
|
+
recursive: true,
|
|
257
|
+
force: true
|
|
258
|
+
});
|
|
259
|
+
purgedData = true;
|
|
260
|
+
}
|
|
261
|
+
const { notifyDaemonReload } = await import("../program/daemon-integration.mjs");
|
|
262
|
+
const { getDaemonStatus } = await import("../daemon/manager.mjs");
|
|
263
|
+
await notifyDaemonReload({ getDaemonStatus });
|
|
264
|
+
return {
|
|
265
|
+
success: true,
|
|
266
|
+
programId,
|
|
267
|
+
purgedData
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Read per-program mount overrides from mounts.toml.
|
|
272
|
+
*
|
|
273
|
+
* Location: ~/.afs-config/data/{programId}/mounts.toml
|
|
274
|
+
*
|
|
275
|
+
* Returns [] if the file is missing or invalid.
|
|
276
|
+
*/
|
|
277
|
+
async function readProgramMountOverrides(programId, options) {
|
|
278
|
+
const mountsPath = join(getUserConfigDir(options?.userConfigDir), "data", programId, "mounts.toml");
|
|
279
|
+
let content;
|
|
280
|
+
try {
|
|
281
|
+
content = await readFile(mountsPath, "utf-8");
|
|
282
|
+
} catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const { parse } = await import("smol-toml");
|
|
287
|
+
const mounts = parse(content).mounts;
|
|
288
|
+
if (!Array.isArray(mounts)) return [];
|
|
289
|
+
const result = [];
|
|
290
|
+
for (const entry of mounts) {
|
|
291
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
292
|
+
const { path, uri, options: options$1 } = entry;
|
|
293
|
+
if (typeof path !== "string" || typeof uri !== "string") continue;
|
|
294
|
+
const override = {
|
|
295
|
+
target: path,
|
|
296
|
+
uri
|
|
297
|
+
};
|
|
298
|
+
if (options$1 && typeof options$1 === "object" && !Array.isArray(options$1)) {
|
|
299
|
+
const opts = options$1;
|
|
300
|
+
if (Object.keys(opts).length > 0) override.options = opts;
|
|
301
|
+
}
|
|
302
|
+
result.push(override);
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
} catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Interactive configure flow for a program's mount dependencies.
|
|
311
|
+
*
|
|
312
|
+
* Uses the standard credential resolution flow (browser form / OAuth) for each
|
|
313
|
+
* mount. Mounts with already-stored credentials are skipped silently.
|
|
314
|
+
* After resolution, URI templates are rebuilt with collected values and
|
|
315
|
+
* credentials are persisted under the final URI.
|
|
316
|
+
*/
|
|
317
|
+
async function configureProgramMounts(programId, options) {
|
|
318
|
+
const userConfigDir = getUserConfigDir(options?.userConfigDir);
|
|
319
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
320
|
+
const manifest = await validateProgramDir(join(userConfigDir, "programs", programId));
|
|
321
|
+
const existingOverrides = await readProgramMountOverrides(programId, { userConfigDir });
|
|
322
|
+
const existingByTarget = new Map(existingOverrides.map((o) => [o.target, o]));
|
|
323
|
+
const { ProviderRegistry: ProviderRegistry$1 } = await import("@aigne/afs");
|
|
324
|
+
const { resolveCredentialsForMount } = await import("./credential-helpers.mjs");
|
|
325
|
+
const { createCLIAuthContext } = await import("../credential/cli-auth-context.mjs");
|
|
326
|
+
const { createCredentialStore } = await import("../credential/store.mjs");
|
|
327
|
+
const registry = new ProviderRegistry$1();
|
|
328
|
+
const authContext = createCLIAuthContext();
|
|
329
|
+
const credentialStore = createCredentialStore();
|
|
330
|
+
const configuredMounts = [];
|
|
331
|
+
const { getTemplateVariableNames } = await import("@aigne/afs/utils/uri-template");
|
|
332
|
+
for (const mountDecl of manifest.mounts) {
|
|
333
|
+
const existing = existingByTarget.get(mountDecl.target);
|
|
334
|
+
const startUri = existing?.uri || mountDecl.uri;
|
|
335
|
+
try {
|
|
336
|
+
const forceThis = options?.force === true || options?.force === "" || options?.force === mountDecl.target;
|
|
337
|
+
const result = await resolveCredentialsForMount({
|
|
338
|
+
cwd,
|
|
339
|
+
uri: startUri,
|
|
340
|
+
mountPath: mountDecl.target,
|
|
341
|
+
authContext,
|
|
342
|
+
credentialStore,
|
|
343
|
+
registry,
|
|
344
|
+
forceCollect: forceThis || void 0,
|
|
345
|
+
extraOptions: existing?.options
|
|
346
|
+
});
|
|
347
|
+
if (result) {
|
|
348
|
+
await result.persistCredentials();
|
|
349
|
+
const finalUri = result.resolvedUri || result.configUri || startUri;
|
|
350
|
+
const info = await registry.getProviderInfo(startUri);
|
|
351
|
+
const templateVars = new Set(info?.manifest?.uriTemplate ? getTemplateVariableNames(info.manifest.uriTemplate) : []);
|
|
352
|
+
const filteredOptions = {};
|
|
353
|
+
if (existing?.options) {
|
|
354
|
+
for (const [k, v] of Object.entries(existing.options)) if (!templateVars.has(k)) filteredOptions[k] = v;
|
|
355
|
+
}
|
|
356
|
+
if (result.collected) {
|
|
357
|
+
for (const [k, v] of Object.entries(result.nonSensitive)) if (!templateVars.has(k)) filteredOptions[k] = v;
|
|
358
|
+
}
|
|
359
|
+
const options$1 = Object.keys(filteredOptions).length > 0 ? filteredOptions : void 0;
|
|
360
|
+
configuredMounts.push({
|
|
361
|
+
target: mountDecl.target,
|
|
362
|
+
uri: finalUri,
|
|
363
|
+
options: options$1
|
|
364
|
+
});
|
|
365
|
+
} else configuredMounts.push({
|
|
366
|
+
target: mountDecl.target,
|
|
367
|
+
uri: startUri,
|
|
368
|
+
options: existing?.options
|
|
369
|
+
});
|
|
370
|
+
} catch (err) {
|
|
371
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
372
|
+
console.warn(` Skipping ${mountDecl.target}: ${msg}`);
|
|
373
|
+
configuredMounts.push({
|
|
374
|
+
target: mountDecl.target,
|
|
375
|
+
uri: startUri,
|
|
376
|
+
options: existing?.options
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const changedMounts = configuredMounts.filter((m) => {
|
|
381
|
+
const decl = manifest.mounts.find((d) => d.target === m.target);
|
|
382
|
+
return decl && (m.uri !== decl.uri || m.options && Object.keys(m.options).length > 0);
|
|
383
|
+
});
|
|
384
|
+
const dataDir = join(userConfigDir, "data", programId);
|
|
385
|
+
const mountsTomlPath = join(dataDir, "mounts.toml");
|
|
386
|
+
if (changedMounts.length > 0) {
|
|
387
|
+
await mkdir(dataDir, { recursive: true });
|
|
388
|
+
const { stringify } = await import("smol-toml");
|
|
389
|
+
await writeFile(mountsTomlPath, stringify({ mounts: changedMounts.map((m) => ({
|
|
390
|
+
path: m.target,
|
|
391
|
+
uri: m.uri,
|
|
392
|
+
...m.options && Object.keys(m.options).length > 0 ? { options: m.options } : {}
|
|
393
|
+
})) }), "utf-8");
|
|
394
|
+
} else try {
|
|
395
|
+
await rm(mountsTomlPath);
|
|
396
|
+
} catch {}
|
|
397
|
+
try {
|
|
398
|
+
const { notifyDaemonReload } = await import("../program/daemon-integration.mjs");
|
|
399
|
+
const { getDaemonStatus } = await import("../daemon/manager.mjs");
|
|
400
|
+
await notifyDaemonReload({ getDaemonStatus });
|
|
401
|
+
} catch {}
|
|
402
|
+
return {
|
|
403
|
+
success: true,
|
|
404
|
+
programId,
|
|
405
|
+
configuredMounts: changedMounts
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* List configurable mounts and their current status for a program.
|
|
410
|
+
*/
|
|
411
|
+
async function listProgramMountStatus(programId, options) {
|
|
412
|
+
const userConfigDir = getUserConfigDir(options?.userConfigDir);
|
|
413
|
+
const manifest = await validateProgramDir(join(userConfigDir, "programs", programId));
|
|
414
|
+
const overrides = await readProgramMountOverrides(programId, { userConfigDir });
|
|
415
|
+
const overrideByTarget = new Map(overrides.map((o) => [o.target, o]));
|
|
416
|
+
const { createCredentialStore } = await import("../credential/store.mjs");
|
|
417
|
+
const credentialStore = createCredentialStore();
|
|
418
|
+
const mounts = [];
|
|
419
|
+
for (const mountDecl of manifest.mounts) {
|
|
420
|
+
const override = overrideByTarget.get(mountDecl.target);
|
|
421
|
+
const effectiveUri = override?.uri || mountDecl.uri;
|
|
422
|
+
let hasCredentials = false;
|
|
423
|
+
try {
|
|
424
|
+
const stored = await credentialStore.get(effectiveUri);
|
|
425
|
+
hasCredentials = !!stored && Object.keys(stored).length > 0;
|
|
426
|
+
} catch {}
|
|
427
|
+
mounts.push({
|
|
428
|
+
path: mountDecl.target,
|
|
429
|
+
uri: mountDecl.uri,
|
|
430
|
+
configuredUri: override?.uri !== mountDecl.uri ? override?.uri : void 0,
|
|
431
|
+
required: mountDecl.required,
|
|
432
|
+
hasCredentials,
|
|
433
|
+
hasOptions: !!(override?.options && Object.keys(override.options).length > 0)
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
programId,
|
|
438
|
+
mounts
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
//#endregion
|
|
443
|
+
export { configureProgramMounts, getUserConfigDir, installProgram, listInstalledPrograms, listProgramMountStatus, readProgramMountOverrides, removeProgram };
|
|
444
|
+
//# sourceMappingURL=program-install.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"program-install.mjs","names":["options","ProviderRegistry"],"sources":["../../src/config/program-install.ts"],"sourcesContent":["/**\n * Program Installation\n *\n * Business logic for installing, listing, and removing AFS programs.\n * Programs are installed to ~/.afs-config/programs/<id>/ and mounted\n * at /programs/<id> in the CWD config (same as `mount add`).\n */\n\nimport { execSync } from \"node:child_process\";\nimport { cp, mkdir, readdir, readFile, rename, rm, writeFile } from \"node:fs/promises\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { type MountOverride, type ProgramManifest, parseProgramManifest } from \"@aigne/afs\";\nimport { AFS_USER_CONFIG_DIR_ENV, CONFIG_DIR_NAME } from \"./loader.js\";\nimport { persistMount, unpersistMount } from \"./mount-commands.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nexport type SourceType = \"local\" | \"github\" | \"zip\";\n\nexport interface InstallResult {\n success: boolean;\n programId: string;\n programName: string;\n installPath: string;\n mountPath: string;\n}\n\nexport interface InstalledProgram {\n id: string;\n name: string;\n entrypoint: string;\n installPath: string;\n mountPath: string;\n}\n\nexport interface RemoveResult {\n success: boolean;\n programId: string;\n purgedData: boolean;\n}\n\nexport interface InstallOptions {\n /** Override user config dir (for testing) */\n userConfigDir?: string;\n /** CWD for persistMount (for testing) */\n cwd?: string;\n}\n\nexport interface RemoveOptions extends InstallOptions {\n /** Also remove program data directory */\n purge?: boolean;\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────\n\n/**\n * Get the user config directory.\n */\nexport function getUserConfigDir(override?: string): string {\n return override ?? process.env[AFS_USER_CONFIG_DIR_ENV] ?? join(homedir(), CONFIG_DIR_NAME);\n}\n\n/**\n * Detect the source type from a source string.\n */\nexport function detectSourceType(source: string): SourceType {\n // ZIP file\n if (source.endsWith(\".zip\")) {\n return \"zip\";\n }\n\n // GitHub URL patterns\n if (\n source.startsWith(\"https://github.com/\") ||\n source.startsWith(\"http://github.com/\") ||\n source.startsWith(\"github.com/\")\n ) {\n return \"github\";\n }\n\n // Default: local directory\n return \"local\";\n}\n\n/**\n * Parse a GitHub URL into owner, repo, ref, and optional subdirectory.\n */\nexport function parseGitHubURL(url: string): {\n cloneUrl: string;\n subdir?: string;\n} {\n // Normalize: ensure https://\n let normalized = url;\n if (normalized.startsWith(\"github.com/\")) {\n normalized = `https://${normalized}`;\n }\n\n // Parse: https://github.com/owner/repo[/tree/ref/subdir]\n const match = normalized.match(\n /^https?:\\/\\/github\\.com\\/([^/]+)\\/([^/]+?)(?:\\.git)?(?:\\/tree\\/([^/]+)\\/(.+))?$/,\n );\n if (!match) {\n // Simple case: just owner/repo\n return { cloneUrl: normalized.replace(/\\/+$/, \"\") };\n }\n\n const [, owner, repo, _ref, subdir] = match;\n const cloneUrl = `https://github.com/${owner}/${repo}.git`;\n\n return {\n cloneUrl,\n subdir: subdir || undefined,\n };\n}\n\n/**\n * Resolve a source to a local directory path containing program.yaml.\n * For github/zip sources, downloads/extracts to a temp directory.\n *\n * Returns [resolvedPath, tempDir?] — tempDir is set if caller should clean up.\n */\nexport async function resolveSource(\n source: string,\n type: SourceType,\n cwd: string,\n): Promise<[string, string | undefined]> {\n switch (type) {\n case \"local\": {\n const dirPath = isAbsolute(source) ? source : resolve(cwd, source);\n // Verify it's a directory by trying to read it\n try {\n await readdir(dirPath);\n } catch {\n throw new Error(`Source directory does not exist: ${dirPath}`);\n }\n return [dirPath, undefined];\n }\n\n case \"github\": {\n const { cloneUrl, subdir } = parseGitHubURL(source);\n const tempDir = join(tmpdir(), `afs-install-${Date.now()}`);\n await mkdir(tempDir, { recursive: true });\n\n try {\n execSync(`git clone --depth 1 ${cloneUrl} ${join(tempDir, \"repo\")}`, {\n stdio: \"pipe\",\n timeout: 60000,\n });\n } catch (err) {\n await rm(tempDir, { recursive: true, force: true });\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to clone GitHub repository: ${msg}`);\n }\n\n const resolvedPath = subdir ? join(tempDir, \"repo\", subdir) : join(tempDir, \"repo\");\n return [resolvedPath, tempDir];\n }\n\n case \"zip\": {\n const zipPath = isAbsolute(source) ? source : resolve(cwd, source);\n const tempDir = join(tmpdir(), `afs-install-${Date.now()}`);\n const extractDir = join(tempDir, \"extracted\");\n await mkdir(extractDir, { recursive: true });\n\n // Security: check zip contents for path traversal before extracting\n try {\n const listing = execSync(`unzip -l \"${zipPath}\"`, {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n // Each line after header has format: \" length date time name\"\n const lines = listing.split(\"\\n\");\n for (const line of lines) {\n const trimmed = line.trim();\n // Skip header/footer lines\n if (\n !trimmed ||\n trimmed.startsWith(\"Archive:\") ||\n trimmed.startsWith(\"Length\") ||\n trimmed.startsWith(\"---\")\n )\n continue;\n // Extract filename (last column)\n const parts = trimmed.split(/\\s+/);\n if (parts.length >= 4) {\n const name = parts.slice(3).join(\" \");\n if (name.includes(\"..\") || name.startsWith(\"/\")) {\n await rm(tempDir, { recursive: true, force: true });\n throw new Error(`Zip file contains unsafe path: ${name}. Refusing to extract.`);\n }\n }\n }\n } catch (err) {\n if (err instanceof Error && err.message.includes(\"unsafe path\")) {\n throw err;\n }\n await rm(tempDir, { recursive: true, force: true });\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to read zip file: ${msg}`);\n }\n\n // Extract\n try {\n execSync(`unzip -o \"${zipPath}\" -d \"${extractDir}\"`, {\n stdio: \"pipe\",\n timeout: 30000,\n });\n } catch (err) {\n await rm(tempDir, { recursive: true, force: true });\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to extract zip file: ${msg}`);\n }\n\n // Find program.yaml: might be at root or in a single subdirectory\n const entries = await readdir(extractDir);\n // Check if program.yaml is at extract root\n if (entries.includes(\"program.yaml\")) {\n return [extractDir, tempDir];\n }\n // Check single subdirectory\n if (entries.length === 1) {\n const subPath = join(extractDir, entries[0]!);\n try {\n const subEntries = await readdir(subPath);\n if (subEntries.includes(\"program.yaml\")) {\n return [subPath, tempDir];\n }\n } catch {\n // not a directory\n }\n }\n\n await rm(tempDir, { recursive: true, force: true });\n throw new Error(\"No program.yaml found in zip archive\");\n }\n\n default:\n throw new Error(`Unsupported source type: ${type}`);\n }\n}\n\n/**\n * Validate a directory contains a valid program.yaml.\n * Returns the parsed manifest.\n */\nexport async function validateProgramDir(dirPath: string): Promise<ProgramManifest> {\n const yamlPath = join(dirPath, \"program.yaml\");\n let content: string;\n try {\n content = await readFile(yamlPath, \"utf-8\");\n } catch {\n throw new Error(`No program.yaml found in ${dirPath}`);\n }\n\n return parseProgramManifest(content);\n}\n\n// ─── Main Functions ───────────────────────────────────────────────────────\n\n/**\n * Install a program from a source (local dir, GitHub URL, or zip file).\n */\nexport async function installProgram(\n source: string,\n options?: InstallOptions,\n): Promise<InstallResult> {\n const userConfigDir = getUserConfigDir(options?.userConfigDir);\n const cwd = options?.cwd ?? process.cwd();\n const type = detectSourceType(source);\n\n // 1. Resolve source to local directory\n const [resolvedPath, tempDir] = await resolveSource(source, type, cwd);\n\n try {\n // 2. Validate program.yaml\n const manifest = await validateProgramDir(resolvedPath);\n\n // 3. Determine install target\n const programsDir = join(userConfigDir, \"programs\");\n const installPath = join(programsDir, manifest.id);\n const mountPath = `/programs/${manifest.id}`;\n\n // 4. Atomic install: copy to temp location, swap\n await mkdir(programsDir, { recursive: true });\n const tempInstallPath = `${installPath}.installing`;\n\n // Clean up any previous failed install\n await rm(tempInstallPath, { recursive: true, force: true });\n\n // Copy program files\n await cp(resolvedPath, tempInstallPath, { recursive: true });\n\n // Remove old version if exists, then rename\n await rm(installPath, { recursive: true, force: true });\n await rename(tempInstallPath, installPath);\n\n // 5. Ensure data directory exists\n const dataDir = join(userConfigDir, \"data\", manifest.id);\n await mkdir(dataDir, { recursive: true });\n\n // 6. Persist mount to CWD config (same as mount add)\n await persistMount(cwd, {\n path: mountPath,\n uri: `fs://${installPath}`,\n });\n\n // 7. Notify daemon to reload programs (best-effort)\n const { notifyDaemonReload } = await import(\"../program/daemon-integration.js\");\n const { getDaemonStatus } = await import(\"../daemon/manager.js\");\n await notifyDaemonReload({ getDaemonStatus });\n\n return {\n success: true,\n programId: manifest.id,\n programName: manifest.name,\n installPath,\n mountPath,\n };\n } finally {\n // 8. Cleanup temp dir\n if (tempDir) {\n await rm(tempDir, { recursive: true, force: true });\n }\n }\n}\n\n/**\n * List installed programs by scanning ~/.afs-config/programs/.\n */\nexport async function listInstalledPrograms(options?: InstallOptions): Promise<InstalledProgram[]> {\n const userConfigDir = getUserConfigDir(options?.userConfigDir);\n const programsDir = join(userConfigDir, \"programs\");\n\n let entries: string[];\n try {\n entries = await readdir(programsDir);\n } catch {\n return []; // programs/ directory doesn't exist yet\n }\n\n const programs: InstalledProgram[] = [];\n\n for (const entry of entries) {\n const dirPath = join(programsDir, entry);\n try {\n const manifest = await validateProgramDir(dirPath);\n programs.push({\n id: manifest.id,\n name: manifest.name,\n entrypoint: manifest.entrypoint,\n installPath: dirPath,\n mountPath: `/programs/${manifest.id}`,\n });\n } catch {\n // Skip directories without valid program.yaml\n }\n }\n\n return programs;\n}\n\n/**\n * Remove an installed program.\n */\nexport async function removeProgram(\n programId: string,\n options?: RemoveOptions,\n): Promise<RemoveResult> {\n const userConfigDir = getUserConfigDir(options?.userConfigDir);\n const cwd = options?.cwd ?? process.cwd();\n const installPath = join(userConfigDir, \"programs\", programId);\n\n // Verify program exists\n try {\n await readdir(installPath);\n } catch {\n throw new Error(`Program \"${programId}\" is not installed`);\n }\n\n // Remove program directory\n await rm(installPath, { recursive: true, force: true });\n\n // Remove mount from config (searches cwd → project → user)\n await unpersistMount(cwd, `/programs/${programId}`);\n\n // Optionally purge data\n let purgedData = false;\n if (options?.purge) {\n const dataDir = join(userConfigDir, \"data\", programId);\n await rm(dataDir, { recursive: true, force: true });\n purgedData = true;\n }\n\n // Notify daemon to reload programs (best-effort)\n const { notifyDaemonReload } = await import(\"../program/daemon-integration.js\");\n const { getDaemonStatus } = await import(\"../daemon/manager.js\");\n await notifyDaemonReload({ getDaemonStatus });\n\n return {\n success: true,\n programId,\n purgedData,\n };\n}\n\n// ─── Mount Overrides (mounts.toml) ───────────────────────────────────────\n\n/**\n * Read per-program mount overrides from mounts.toml.\n *\n * Location: ~/.afs-config/data/{programId}/mounts.toml\n *\n * Returns [] if the file is missing or invalid.\n */\nexport async function readProgramMountOverrides(\n programId: string,\n options?: { userConfigDir?: string },\n): Promise<MountOverride[]> {\n const userConfigDir = getUserConfigDir(options?.userConfigDir);\n const mountsPath = join(userConfigDir, \"data\", programId, \"mounts.toml\");\n\n let content: string;\n try {\n content = await readFile(mountsPath, \"utf-8\");\n } catch {\n return []; // File doesn't exist yet\n }\n\n try {\n const { parse } = await import(\"smol-toml\");\n const parsed = parse(content);\n const mounts = parsed.mounts;\n if (!Array.isArray(mounts)) return [];\n\n const result: MountOverride[] = [];\n for (const entry of mounts) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const { path, uri, options } = entry as Record<string, unknown>;\n if (typeof path !== \"string\" || typeof uri !== \"string\") continue;\n const override: MountOverride = { target: path, uri };\n if (options && typeof options === \"object\" && !Array.isArray(options)) {\n const opts = options as Record<string, unknown>;\n if (Object.keys(opts).length > 0) {\n override.options = opts;\n }\n }\n result.push(override);\n }\n return result;\n } catch {\n return []; // Invalid TOML\n }\n}\n\n// ─── Configure Flow ──────────────────────────────────────────────────────\n\nexport interface ConfigureOptions {\n userConfigDir?: string;\n cwd?: string;\n /** Force re-collection: true/\"\" = all mounts, string = specific mount path */\n force?: string | boolean;\n}\n\nexport interface ConfigureResult {\n success: boolean;\n programId: string;\n configuredMounts: Array<{ target: string; uri: string }>;\n}\n\n/**\n * Interactive configure flow for a program's mount dependencies.\n *\n * Uses the standard credential resolution flow (browser form / OAuth) for each\n * mount. Mounts with already-stored credentials are skipped silently.\n * After resolution, URI templates are rebuilt with collected values and\n * credentials are persisted under the final URI.\n */\nexport async function configureProgramMounts(\n programId: string,\n options?: ConfigureOptions,\n): Promise<ConfigureResult> {\n const userConfigDir = getUserConfigDir(options?.userConfigDir);\n const cwd = options?.cwd ?? process.cwd();\n\n // 1. Read program.yaml\n const installPath = join(userConfigDir, \"programs\", programId);\n const manifest = await validateProgramDir(installPath);\n\n // 2. Read existing mounts.toml (if any)\n const existingOverrides = await readProgramMountOverrides(programId, { userConfigDir });\n const existingByTarget = new Map(existingOverrides.map((o) => [o.target, o]));\n\n // 3. Lazy imports\n const { ProviderRegistry } = await import(\"@aigne/afs\");\n const { resolveCredentialsForMount } = await import(\"./credential-helpers.js\");\n const { createCLIAuthContext } = await import(\"../credential/cli-auth-context.js\");\n const { createCredentialStore } = await import(\"../credential/store.js\");\n\n const registry = new ProviderRegistry();\n const authContext = createCLIAuthContext();\n const credentialStore = createCredentialStore();\n\n const configuredMounts: Array<{\n target: string;\n uri: string;\n options?: Record<string, unknown>;\n }> = [];\n\n const { getTemplateVariableNames } = await import(\"@aigne/afs/utils/uri-template\");\n\n for (const mountDecl of manifest.mounts) {\n const existing = existingByTarget.get(mountDecl.target);\n const startUri = existing?.uri || mountDecl.uri;\n\n try {\n const forceThis =\n options?.force === true || options?.force === \"\" || options?.force === mountDecl.target;\n\n const result = await resolveCredentialsForMount({\n cwd,\n uri: startUri,\n mountPath: mountDecl.target,\n authContext,\n credentialStore,\n registry,\n forceCollect: forceThis || undefined,\n extraOptions: existing?.options,\n });\n\n if (result) {\n await result.persistCredentials();\n const finalUri = result.resolvedUri || result.configUri || startUri;\n\n // Filter options: exclude sensitive fields and URI template vars (already in URI)\n const info = await registry.getProviderInfo(startUri);\n const templateVars = new Set(\n info?.manifest?.uriTemplate ? getTemplateVariableNames(info.manifest.uriTemplate) : [],\n );\n const filteredOptions: Record<string, unknown> = {};\n // Preserve existing options from mounts.toml\n if (existing?.options) {\n for (const [k, v] of Object.entries(existing.options)) {\n if (!templateVars.has(k)) filteredOptions[k] = v;\n }\n }\n // Merge newly collected non-sensitive options (overrides existing)\n if (result.collected) {\n for (const [k, v] of Object.entries(result.nonSensitive)) {\n if (!templateVars.has(k)) filteredOptions[k] = v;\n }\n }\n\n const options = Object.keys(filteredOptions).length > 0 ? filteredOptions : undefined;\n configuredMounts.push({ target: mountDecl.target, uri: finalUri, options });\n } else {\n // No schema/credentials needed — preserve existing options\n configuredMounts.push({\n target: mountDecl.target,\n uri: startUri,\n options: existing?.options,\n });\n }\n } catch (err) {\n // Credential resolution failed or user cancelled — keep what we have\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(` Skipping ${mountDecl.target}: ${msg}`);\n configuredMounts.push({\n target: mountDecl.target,\n uri: startUri,\n options: existing?.options,\n });\n }\n }\n\n // 4. Write mounts.toml for mounts whose URI or options changed from program.yaml\n const changedMounts = configuredMounts.filter((m) => {\n const decl = manifest.mounts.find((d: { target: string }) => d.target === m.target);\n return decl && (m.uri !== decl.uri || (m.options && Object.keys(m.options).length > 0));\n });\n\n const dataDir = join(userConfigDir, \"data\", programId);\n const mountsTomlPath = join(dataDir, \"mounts.toml\");\n\n if (changedMounts.length > 0) {\n await mkdir(dataDir, { recursive: true });\n\n const { stringify } = await import(\"smol-toml\");\n const tomlContent = stringify({\n mounts: changedMounts.map((m) => ({\n path: m.target,\n uri: m.uri,\n ...(m.options && Object.keys(m.options).length > 0 ? { options: m.options } : {}),\n })),\n });\n await writeFile(mountsTomlPath, tomlContent, \"utf-8\");\n } else {\n // All mounts match program.yaml defaults — remove stale overrides\n try {\n await rm(mountsTomlPath);\n } catch {\n // File doesn't exist — nothing to clean up\n }\n }\n\n // 5. Notify daemon to reload\n try {\n const { notifyDaemonReload } = await import(\"../program/daemon-integration.js\");\n const { getDaemonStatus } = await import(\"../daemon/manager.js\");\n await notifyDaemonReload({ getDaemonStatus });\n } catch {\n // Best-effort\n }\n\n return {\n success: true,\n programId,\n configuredMounts: changedMounts,\n };\n}\n\n// ─── Mount Status Listing ─────────────────────────────────────────────────\n\nexport interface MountStatus {\n /** Mount target path, e.g. \"/telegram\" */\n path: string;\n /** URI from program.yaml */\n uri: string;\n /** Overridden URI from mounts.toml (if different from program.yaml) */\n configuredUri?: string;\n /** Whether this mount is required */\n required: boolean;\n /** Whether credentials are stored for this mount's effective URI */\n hasCredentials: boolean;\n /** Whether mounts.toml has options for this mount */\n hasOptions: boolean;\n}\n\n/**\n * List configurable mounts and their current status for a program.\n */\nexport async function listProgramMountStatus(\n programId: string,\n options?: { userConfigDir?: string },\n): Promise<{ programId: string; mounts: MountStatus[] }> {\n const userConfigDir = getUserConfigDir(options?.userConfigDir);\n const installPath = join(userConfigDir, \"programs\", programId);\n const manifest = await validateProgramDir(installPath);\n\n const overrides = await readProgramMountOverrides(programId, { userConfigDir });\n const overrideByTarget = new Map(overrides.map((o) => [o.target, o]));\n\n const { createCredentialStore } = await import(\"../credential/store.js\");\n const credentialStore = createCredentialStore();\n\n const mounts: MountStatus[] = [];\n for (const mountDecl of manifest.mounts) {\n const override = overrideByTarget.get(mountDecl.target);\n const effectiveUri = override?.uri || mountDecl.uri;\n\n let hasCredentials = false;\n try {\n const stored = await credentialStore.get(effectiveUri);\n hasCredentials = !!stored && Object.keys(stored).length > 0;\n } catch {\n // Non-fatal\n }\n\n mounts.push({\n path: mountDecl.target,\n uri: mountDecl.uri,\n configuredUri: override?.uri !== mountDecl.uri ? override?.uri : undefined,\n required: mountDecl.required,\n hasCredentials,\n hasOptions: !!(override?.options && Object.keys(override.options).length > 0),\n });\n }\n\n return { programId, mounts };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const require_index = require('../../ui/index.cjs');
|
|
2
|
+
|
|
3
|
+
//#region src/core/commands/connect.ts
|
|
4
|
+
/** No-op formatter for lifecycle commands that manage their own output. */
|
|
5
|
+
const noopFormat = () => "";
|
|
6
|
+
function createConnectCommand(options) {
|
|
7
|
+
return {
|
|
8
|
+
command: "connect",
|
|
9
|
+
describe: "Start service and open web explorer",
|
|
10
|
+
builder: { port: {
|
|
11
|
+
type: "number",
|
|
12
|
+
default: 4900,
|
|
13
|
+
description: "Port for service"
|
|
14
|
+
} },
|
|
15
|
+
handler: async (argv) => {
|
|
16
|
+
const { getDaemonStatus, spawnDaemon } = await Promise.resolve().then(() => require("../../daemon/manager.cjs"));
|
|
17
|
+
let info = await getDaemonStatus();
|
|
18
|
+
if (info) console.log(`${require_index.colors.green("Service already running")} on port ${info.port}`);
|
|
19
|
+
else {
|
|
20
|
+
console.log(require_index.colors.dim("Starting AFS service..."));
|
|
21
|
+
try {
|
|
22
|
+
info = await spawnDaemon(argv.port);
|
|
23
|
+
console.log(require_index.colors.green("AFS Service started"));
|
|
24
|
+
console.log(` ${require_index.colors.dim("PID:")} ${info.pid}`);
|
|
25
|
+
console.log(` ${require_index.colors.dim("Port:")} ${info.port}`);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error(require_index.colors.red(`Failed to start service: ${err.message}`));
|
|
28
|
+
options.onResult({
|
|
29
|
+
command: "connect",
|
|
30
|
+
result: null,
|
|
31
|
+
format: noopFormat
|
|
32
|
+
});
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
console.log(` ${require_index.colors.dim("URL:")} ${require_index.colors.brightCyan(info.url)}`);
|
|
38
|
+
openBrowser(info.url);
|
|
39
|
+
options.onResult({
|
|
40
|
+
command: "connect",
|
|
41
|
+
result: null,
|
|
42
|
+
format: noopFormat
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function openBrowser(url) {
|
|
48
|
+
const { exec } = require("node:child_process");
|
|
49
|
+
exec(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} ${url}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
exports.createConnectCommand = createConnectCommand;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { __require } from "../../_virtual/rolldown_runtime.mjs";
|
|
2
|
+
import { colors } from "../../ui/index.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/core/commands/connect.ts
|
|
5
|
+
/** No-op formatter for lifecycle commands that manage their own output. */
|
|
6
|
+
const noopFormat = () => "";
|
|
7
|
+
function createConnectCommand(options) {
|
|
8
|
+
return {
|
|
9
|
+
command: "connect",
|
|
10
|
+
describe: "Start service and open web explorer",
|
|
11
|
+
builder: { port: {
|
|
12
|
+
type: "number",
|
|
13
|
+
default: 4900,
|
|
14
|
+
description: "Port for service"
|
|
15
|
+
} },
|
|
16
|
+
handler: async (argv) => {
|
|
17
|
+
const { getDaemonStatus, spawnDaemon } = await import("../../daemon/manager.mjs");
|
|
18
|
+
let info = await getDaemonStatus();
|
|
19
|
+
if (info) console.log(`${colors.green("Service already running")} on port ${info.port}`);
|
|
20
|
+
else {
|
|
21
|
+
console.log(colors.dim("Starting AFS service..."));
|
|
22
|
+
try {
|
|
23
|
+
info = await spawnDaemon(argv.port);
|
|
24
|
+
console.log(colors.green("AFS Service started"));
|
|
25
|
+
console.log(` ${colors.dim("PID:")} ${info.pid}`);
|
|
26
|
+
console.log(` ${colors.dim("Port:")} ${info.port}`);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(colors.red(`Failed to start service: ${err.message}`));
|
|
29
|
+
options.onResult({
|
|
30
|
+
command: "connect",
|
|
31
|
+
result: null,
|
|
32
|
+
format: noopFormat
|
|
33
|
+
});
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
console.log(` ${colors.dim("URL:")} ${colors.brightCyan(info.url)}`);
|
|
39
|
+
openBrowser(info.url);
|
|
40
|
+
options.onResult({
|
|
41
|
+
command: "connect",
|
|
42
|
+
result: null,
|
|
43
|
+
format: noopFormat
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function openBrowser(url) {
|
|
49
|
+
const { exec } = __require("node:child_process");
|
|
50
|
+
exec(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} ${url}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { createConnectCommand };
|
|
55
|
+
//# sourceMappingURL=connect.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.mjs","names":[],"sources":["../../../src/core/commands/connect.ts"],"sourcesContent":["/**\n * AFS Connect Command\n *\n * One-command experience: starts service (if not running) + opens browser + exits.\n * This is the primary user entry point.\n */\n\nimport type { CommandModule } from \"yargs\";\nimport { colors } from \"../../ui/index.js\";\nimport type { CommandFactoryOptions } from \"./types.js\";\n\nexport interface ConnectArgs {\n port: number;\n}\n\n/** No-op formatter for lifecycle commands that manage their own output. */\nconst noopFormat = () => \"\";\n\nexport function createConnectCommand(\n options: CommandFactoryOptions,\n): CommandModule<unknown, ConnectArgs> {\n return {\n command: \"connect\",\n describe: \"Start service and open web explorer\",\n builder: {\n port: {\n type: \"number\",\n default: 4900,\n description: \"Port for service\",\n },\n },\n handler: async (argv) => {\n const { getDaemonStatus, spawnDaemon } = await import(\"../../daemon/manager.js\");\n\n let info = await getDaemonStatus();\n\n if (info) {\n console.log(`${colors.green(\"Service already running\")} on port ${info.port}`);\n } else {\n console.log(colors.dim(\"Starting AFS service...\"));\n try {\n info = await spawnDaemon(argv.port);\n console.log(colors.green(\"AFS Service started\"));\n console.log(` ${colors.dim(\"PID:\")} ${info.pid}`);\n console.log(` ${colors.dim(\"Port:\")} ${info.port}`);\n } catch (err) {\n console.error(colors.red(`Failed to start service: ${(err as Error).message}`));\n options.onResult({ command: \"connect\", result: null, format: noopFormat });\n process.exitCode = 1;\n return;\n }\n }\n\n console.log(` ${colors.dim(\"URL:\")} ${colors.brightCyan(info.url)}`);\n openBrowser(info.url);\n\n // Signal executor that command ran (output already printed above)\n options.onResult({ command: \"connect\", result: null, format: noopFormat });\n },\n };\n}\n\nfunction openBrowser(url: string): void {\n const { exec } = require(\"node:child_process\") as typeof import(\"node:child_process\");\n const cmd =\n process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n exec(`${cmd} ${url}`);\n}\n"],"mappings":";;;;;AAgBA,MAAM,mBAAmB;AAEzB,SAAgB,qBACd,SACqC;AACrC,QAAO;EACL,SAAS;EACT,UAAU;EACV,SAAS,EACP,MAAM;GACJ,MAAM;GACN,SAAS;GACT,aAAa;GACd,EACF;EACD,SAAS,OAAO,SAAS;GACvB,MAAM,EAAE,iBAAiB,gBAAgB,MAAM,OAAO;GAEtD,IAAI,OAAO,MAAM,iBAAiB;AAElC,OAAI,KACF,SAAQ,IAAI,GAAG,OAAO,MAAM,0BAA0B,CAAC,WAAW,KAAK,OAAO;QACzE;AACL,YAAQ,IAAI,OAAO,IAAI,0BAA0B,CAAC;AAClD,QAAI;AACF,YAAO,MAAM,YAAY,KAAK,KAAK;AACnC,aAAQ,IAAI,OAAO,MAAM,sBAAsB,CAAC;AAChD,aAAQ,IAAI,KAAK,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;AACnD,aAAQ,IAAI,KAAK,OAAO,IAAI,QAAQ,CAAC,GAAG,KAAK,OAAO;aAC7C,KAAK;AACZ,aAAQ,MAAM,OAAO,IAAI,4BAA6B,IAAc,UAAU,CAAC;AAC/E,aAAQ,SAAS;MAAE,SAAS;MAAW,QAAQ;MAAM,QAAQ;MAAY,CAAC;AAC1E,aAAQ,WAAW;AACnB;;;AAIJ,WAAQ,IAAI,KAAK,OAAO,IAAI,OAAO,CAAC,IAAI,OAAO,WAAW,KAAK,IAAI,GAAG;AACtE,eAAY,KAAK,IAAI;AAGrB,WAAQ,SAAS;IAAE,SAAS;IAAW,QAAQ;IAAM,QAAQ;IAAY,CAAC;;EAE7E;;AAGH,SAAS,YAAY,KAAmB;CACtC,MAAM,EAAE,mBAAiB,qBAAqB;AAG9C,MAAK,GADH,QAAQ,aAAa,WAAW,SAAS,QAAQ,aAAa,UAAU,UAAU,WACxE,GAAG,MAAM"}
|