@chvor/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { mkdirSync } from "node:fs";
4
+ let currentInstance = null;
5
+ export function setInstance(name) {
6
+ currentInstance = name;
7
+ }
8
+ export function getInstance() {
9
+ return currentInstance;
10
+ }
11
+ export function getChvorHome() {
12
+ if (currentInstance) {
13
+ return join(homedir(), `.chvor-${currentInstance}`);
14
+ }
15
+ return join(homedir(), ".chvor");
16
+ }
17
+ export function getConfigPath() {
18
+ return join(getChvorHome(), "config.json");
19
+ }
20
+ export function getPidPath() {
21
+ return join(getChvorHome(), "chvor.pid");
22
+ }
23
+ export function getAppDir() {
24
+ // App binaries are shared across all instances
25
+ return join(homedir(), ".chvor", "app");
26
+ }
27
+ export function getDataDir() {
28
+ return join(getChvorHome(), "data");
29
+ }
30
+ export function getLogsDir() {
31
+ return join(getChvorHome(), "logs");
32
+ }
33
+ export function getDownloadsDir() {
34
+ // Downloads are shared across all instances
35
+ return join(homedir(), ".chvor", "downloads");
36
+ }
37
+ export function getSkillsDir() {
38
+ return join(getChvorHome(), "skills");
39
+ }
40
+ export function getToolsDir() {
41
+ return join(getChvorHome(), "tools");
42
+ }
43
+ export function ensureDir(dir) {
44
+ mkdirSync(dir, { recursive: true });
45
+ }
@@ -0,0 +1,28 @@
1
+ export function getPlatform() {
2
+ switch (process.platform) {
3
+ case "linux":
4
+ return "linux";
5
+ case "darwin":
6
+ return "darwin";
7
+ case "win32":
8
+ return "win";
9
+ default:
10
+ throw new Error(`Unsupported platform: "${process.platform}". Chvor supports linux, darwin (macOS), and win32 (Windows).`);
11
+ }
12
+ }
13
+ export function getArch() {
14
+ switch (process.arch) {
15
+ case "x64":
16
+ return "x64";
17
+ case "arm64":
18
+ return "arm64";
19
+ default:
20
+ throw new Error(`Unsupported architecture: "${process.arch}". Chvor supports x64 and arm64.`);
21
+ }
22
+ }
23
+ export function getAssetName(version) {
24
+ const os = getPlatform();
25
+ const arch = getArch();
26
+ const ext = os === "win" ? "zip" : "tar.gz";
27
+ return `chvor-v${version}-${os}-${arch}.${ext}`;
28
+ }
@@ -0,0 +1,220 @@
1
+ import { spawn, execFileSync } from "node:child_process";
2
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, openSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { getAppDir, getDataDir, getLogsDir, getPidPath, getSkillsDir, getToolsDir, ensureDir, } from "./paths.js";
5
+ import { readConfig } from "./config.js";
6
+ function isProcessAlive(pid) {
7
+ if (process.platform === "win32") {
8
+ try {
9
+ const output = execFileSync("tasklist", ["/FI", `PID eq ${pid}`], {
10
+ encoding: "utf-8",
11
+ stdio: ["pipe", "pipe", "pipe"],
12
+ });
13
+ return output.includes(String(pid));
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ else {
20
+ try {
21
+ process.kill(pid, 0);
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ }
29
+ export function isServerRunning() {
30
+ const pidPath = getPidPath();
31
+ if (!existsSync(pidPath)) {
32
+ return { running: false };
33
+ }
34
+ try {
35
+ const raw = readFileSync(pidPath, "utf-8").trim();
36
+ const pid = parseInt(raw, 10);
37
+ if (isNaN(pid)) {
38
+ return { running: false };
39
+ }
40
+ if (isProcessAlive(pid)) {
41
+ return { running: true, pid };
42
+ }
43
+ // Stale PID file — clean up
44
+ unlinkSync(pidPath);
45
+ return { running: false };
46
+ }
47
+ catch {
48
+ return { running: false };
49
+ }
50
+ }
51
+ export async function spawnServer(opts = {}) {
52
+ const config = readConfig();
53
+ const port = opts.port ?? config.port;
54
+ const token = config.token;
55
+ const status = isServerRunning();
56
+ if (status.running) {
57
+ console.log(`Chvor is already running (PID ${status.pid}).`);
58
+ console.log(` http://localhost:${port}`);
59
+ return;
60
+ }
61
+ const serverEntry = join(getAppDir(), "apps", "server", "src", "index.ts");
62
+ if (!existsSync(serverEntry)) {
63
+ throw new Error(`Server entry point not found: ${serverEntry}\nRun "chvor install" first.`);
64
+ }
65
+ const dataDir = getDataDir();
66
+ ensureDir(dataDir);
67
+ // Build environment variables
68
+ const env = {
69
+ ...filterEnv(process.env),
70
+ PORT: port,
71
+ CHVOR_DATA_DIR: dataDir,
72
+ CHVOR_SKILLS_DIR: getSkillsDir(),
73
+ CHVOR_TOOLS_DIR: getToolsDir(),
74
+ NODE_ENV: "production",
75
+ };
76
+ if (token) {
77
+ env.CHVOR_TOKEN = token;
78
+ }
79
+ // Pass through common LLM API key env vars
80
+ const llmKeyVars = [
81
+ "OPENAI_API_KEY",
82
+ "ANTHROPIC_API_KEY",
83
+ "GOOGLE_AI_API_KEY",
84
+ "MISTRAL_API_KEY",
85
+ "GROQ_API_KEY",
86
+ "TOGETHER_API_KEY",
87
+ "OPENROUTER_API_KEY",
88
+ "AZURE_OPENAI_API_KEY",
89
+ "AWS_ACCESS_KEY_ID",
90
+ "AWS_SECRET_ACCESS_KEY",
91
+ "AWS_REGION",
92
+ ];
93
+ for (const key of llmKeyVars) {
94
+ if (process.env[key] && !env[key]) {
95
+ env[key] = process.env[key];
96
+ }
97
+ }
98
+ if (opts.foreground) {
99
+ console.log(`Starting Chvor on port ${port} (foreground)...`);
100
+ const child = spawn("node", [serverEntry], {
101
+ env,
102
+ stdio: "inherit",
103
+ });
104
+ const pidPath = getPidPath();
105
+ writeFileSync(pidPath, String(child.pid), "utf-8");
106
+ child.on("exit", (code) => {
107
+ try {
108
+ unlinkSync(pidPath);
109
+ }
110
+ catch {
111
+ // ignore
112
+ }
113
+ if (code !== 0) {
114
+ console.error(`Chvor exited with code ${code}`);
115
+ }
116
+ });
117
+ // Wait for health
118
+ const healthy = await pollHealth(port, token);
119
+ if (healthy) {
120
+ console.log(`Chvor is ready at http://localhost:${port}`);
121
+ }
122
+ else {
123
+ console.warn("Chvor started but health check did not pass within timeout.");
124
+ }
125
+ }
126
+ else {
127
+ console.log(`Starting Chvor on port ${port} (background)...`);
128
+ const logsDir = getLogsDir();
129
+ ensureDir(logsDir);
130
+ const logPath = join(logsDir, "server.log");
131
+ const logFd = openSync(logPath, "a");
132
+ const child = spawn("node", [serverEntry], {
133
+ env,
134
+ stdio: ["ignore", logFd, logFd],
135
+ detached: true,
136
+ });
137
+ child.unref();
138
+ const pidPath = getPidPath();
139
+ writeFileSync(pidPath, String(child.pid), "utf-8");
140
+ console.log(`Chvor started (PID ${child.pid}). Logs: ${logPath}`);
141
+ const healthy = await pollHealth(port, token);
142
+ if (healthy) {
143
+ console.log(`Chvor is ready at http://localhost:${port}`);
144
+ }
145
+ else {
146
+ console.warn("Chvor started but health check did not pass within timeout.\n" +
147
+ `Check logs at ${logPath}`);
148
+ }
149
+ }
150
+ }
151
+ export async function stopServer() {
152
+ const pidPath = getPidPath();
153
+ if (!existsSync(pidPath)) {
154
+ console.log("Chvor is not running.");
155
+ return;
156
+ }
157
+ const raw = readFileSync(pidPath, "utf-8").trim();
158
+ const pid = parseInt(raw, 10);
159
+ if (isNaN(pid)) {
160
+ console.log("Invalid PID file. Cleaning up.");
161
+ unlinkSync(pidPath);
162
+ return;
163
+ }
164
+ try {
165
+ if (process.platform === "win32") {
166
+ execFileSync("taskkill", ["/PID", String(pid), "/T", "/F"], {
167
+ stdio: ["pipe", "pipe", "pipe"],
168
+ });
169
+ }
170
+ else {
171
+ process.kill(pid, "SIGTERM");
172
+ }
173
+ }
174
+ catch {
175
+ // Process may already be gone
176
+ }
177
+ try {
178
+ unlinkSync(pidPath);
179
+ }
180
+ catch {
181
+ // ignore
182
+ }
183
+ console.log("Chvor stopped.");
184
+ }
185
+ export async function pollHealth(port, token, timeoutMs = 15000, intervalMs = 500) {
186
+ const start = Date.now();
187
+ const url = `http://localhost:${port}/api/health`;
188
+ const headers = {};
189
+ if (token) {
190
+ headers["Authorization"] = `Bearer ${token}`;
191
+ }
192
+ while (Date.now() - start < timeoutMs) {
193
+ try {
194
+ const res = await fetch(url, { headers });
195
+ if (res.ok) {
196
+ const body = (await res.json());
197
+ if (body.ok === true) {
198
+ return true;
199
+ }
200
+ }
201
+ }
202
+ catch {
203
+ // Connection refused or other error — server not ready yet
204
+ }
205
+ await sleep(intervalMs);
206
+ }
207
+ return false;
208
+ }
209
+ function sleep(ms) {
210
+ return new Promise((resolve) => setTimeout(resolve, ms));
211
+ }
212
+ function filterEnv(env) {
213
+ const result = {};
214
+ for (const [key, value] of Object.entries(env)) {
215
+ if (value !== undefined) {
216
+ result[key] = value;
217
+ }
218
+ }
219
+ return result;
220
+ }
@@ -0,0 +1,130 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { getChvorHome } from "./paths.js";
5
+ function getAuthPath() {
6
+ return join(getChvorHome(), "data", "registry-auth.json");
7
+ }
8
+ function readAuth() {
9
+ const authPath = getAuthPath();
10
+ if (!existsSync(authPath))
11
+ return null;
12
+ try {
13
+ const data = JSON.parse(readFileSync(authPath, "utf8"));
14
+ // Check expiry
15
+ if (new Date(data.expiresAt) < new Date())
16
+ return null;
17
+ return data;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function writeAuth(auth) {
24
+ const authPath = getAuthPath();
25
+ mkdirSync(dirname(authPath), { recursive: true });
26
+ writeFileSync(authPath, JSON.stringify(auth, null, 2), { encoding: "utf8", mode: 0o600 });
27
+ }
28
+ export function getRegistryToken(registryUrl) {
29
+ const auth = readAuth();
30
+ if (!auth)
31
+ return null;
32
+ if (auth.registryUrl !== registryUrl)
33
+ return null;
34
+ return auth.token;
35
+ }
36
+ export function isAuthenticated(registryUrl) {
37
+ return getRegistryToken(registryUrl) !== null;
38
+ }
39
+ export function getUsername(registryUrl) {
40
+ const auth = readAuth();
41
+ if (!auth || auth.registryUrl !== registryUrl)
42
+ return null;
43
+ return auth.username;
44
+ }
45
+ export function logout() {
46
+ const authPath = getAuthPath();
47
+ if (existsSync(authPath)) {
48
+ writeFileSync(authPath, "{}", "utf8");
49
+ }
50
+ }
51
+ function openBrowser(url) {
52
+ try {
53
+ const platform = process.platform;
54
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
55
+ execFile(cmd, [url], () => { });
56
+ }
57
+ catch {
58
+ // Non-critical — user can open manually
59
+ }
60
+ }
61
+ /**
62
+ * Authenticate with the registry using GitHub device flow.
63
+ * Opens the user's browser to GitHub where they enter a code.
64
+ */
65
+ export async function authenticate(registryUrl) {
66
+ // Step 1: Initiate device flow
67
+ console.log("Authenticating with the registry...\n");
68
+ const codeRes = await fetch(`${registryUrl}/auth/device/code`, {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ });
72
+ if (!codeRes.ok) {
73
+ const body = await codeRes.text();
74
+ throw new Error(`Failed to initiate authentication: ${body}`);
75
+ }
76
+ const codeData = await codeRes.json();
77
+ // Step 2: Display instructions to user
78
+ console.log("To authenticate, visit:");
79
+ console.log(`\n ${codeData.verification_uri}\n`);
80
+ console.log(`Enter code: ${codeData.user_code}\n`);
81
+ console.log("Waiting for authorization...");
82
+ // Try to open browser
83
+ openBrowser(codeData.verification_uri);
84
+ // Step 3: Poll for token
85
+ let interval = (codeData.interval || 5) * 1000;
86
+ const deadline = Date.now() + (codeData.expires_in || 900) * 1000;
87
+ while (Date.now() < deadline) {
88
+ await new Promise((resolve) => setTimeout(resolve, interval));
89
+ const tokenRes = await fetch(`${registryUrl}/auth/device/token`, {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify({ device_code: codeData.device_code }),
93
+ });
94
+ if (!tokenRes.ok) {
95
+ throw new Error(`Token exchange failed: HTTP ${tokenRes.status}`);
96
+ }
97
+ const tokenData = await tokenRes.json();
98
+ switch (tokenData.status) {
99
+ case "complete": {
100
+ if (!tokenData.token || !tokenData.username) {
101
+ throw new Error("Received complete status but missing token/username");
102
+ }
103
+ // Store auth
104
+ const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
105
+ writeAuth({
106
+ token: tokenData.token,
107
+ username: tokenData.username,
108
+ registryUrl,
109
+ expiresAt,
110
+ });
111
+ console.log(`\nAuthenticated as ${tokenData.username}`);
112
+ return { token: tokenData.token, username: tokenData.username };
113
+ }
114
+ case "slow_down":
115
+ if (tokenData.interval)
116
+ interval = tokenData.interval * 1000;
117
+ else
118
+ interval += 5000;
119
+ break;
120
+ case "expired":
121
+ throw new Error("Device code expired. Please try again.");
122
+ case "pending":
123
+ // Keep polling
124
+ break;
125
+ default:
126
+ throw new Error(`Unexpected status: ${tokenData.status}`);
127
+ }
128
+ }
129
+ throw new Error("Authentication timed out. Please try again.");
130
+ }
@@ -0,0 +1,190 @@
1
+ import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { getAppDir } from "./paths.js";
6
+ const DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/chvor-community/skill-registry/main";
7
+ const FETCH_TIMEOUT_MS = 8_000;
8
+ const SAFE_ENTRY_ID_RE = /^[a-z0-9][a-z0-9_-]*$/;
9
+ /** Validates that a registry URL uses HTTPS (or localhost for development). */
10
+ function assertValidRegistryUrl(url) {
11
+ let parsed;
12
+ try {
13
+ parsed = new URL(url);
14
+ }
15
+ catch {
16
+ throw new Error(`Invalid registry URL: "${url}"`);
17
+ }
18
+ const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
19
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLocalhost)) {
20
+ throw new Error(`Registry URL must use HTTPS (got ${parsed.protocol}). HTTP is only allowed for localhost.`);
21
+ }
22
+ }
23
+ /** Validates that an entry ID is safe for use as a filename (no path traversal). */
24
+ function assertSafeEntryId(id) {
25
+ if (!id || id.length > 100 || !SAFE_ENTRY_ID_RE.test(id)) {
26
+ throw new Error(`Invalid entry ID: "${id}" — must be lowercase alphanumeric with hyphens/underscores only`);
27
+ }
28
+ }
29
+ function getBundledTemplatesDir() {
30
+ return join(getAppDir(), "apps", "server", "data", "bundled-templates");
31
+ }
32
+ export function validateTemplate(manifest) {
33
+ if (!manifest || typeof manifest !== "object")
34
+ return false;
35
+ const m = manifest;
36
+ return (typeof m.name === "string" &&
37
+ typeof m.description === "string" &&
38
+ typeof m.version === "string");
39
+ }
40
+ export function loadTemplateFromPath(templateDir) {
41
+ const manifestPath = join(templateDir, "template.yaml");
42
+ if (!existsSync(manifestPath)) {
43
+ throw new Error(`No template.yaml found in ${templateDir}`);
44
+ }
45
+ const raw = readFileSync(manifestPath, "utf-8");
46
+ const manifest = parseYaml(raw);
47
+ if (!validateTemplate(manifest)) {
48
+ throw new Error(`Invalid template manifest in ${manifestPath}. Required fields: name, description, version`);
49
+ }
50
+ return manifest;
51
+ }
52
+ export function listBundledTemplates() {
53
+ const dir = getBundledTemplatesDir();
54
+ if (!existsSync(dir))
55
+ return [];
56
+ const entries = [];
57
+ const dirs = readdirSync(dir, { withFileTypes: true })
58
+ .filter((d) => d.isDirectory());
59
+ for (const d of dirs) {
60
+ const templateDir = join(dir, d.name);
61
+ try {
62
+ const manifest = loadTemplateFromPath(templateDir);
63
+ entries.push({
64
+ id: d.name,
65
+ name: manifest.name,
66
+ description: manifest.description,
67
+ version: manifest.version,
68
+ author: manifest.author,
69
+ icon: manifest.icon,
70
+ tags: manifest.tags,
71
+ path: templateDir,
72
+ });
73
+ }
74
+ catch (err) {
75
+ console.warn(` Warning: skipping invalid template in ${d.name}: ${err instanceof Error ? err.message : err}`);
76
+ }
77
+ }
78
+ return entries;
79
+ }
80
+ export function resolveBundledTemplate(nameOrId) {
81
+ const dir = getBundledTemplatesDir();
82
+ if (!existsSync(dir))
83
+ return null;
84
+ // Direct match by directory name
85
+ const directPath = join(dir, nameOrId);
86
+ if (existsSync(join(directPath, "template.yaml"))) {
87
+ return directPath;
88
+ }
89
+ // Search by manifest name
90
+ const dirs = readdirSync(dir, { withFileTypes: true })
91
+ .filter((d) => d.isDirectory());
92
+ for (const d of dirs) {
93
+ const templateDir = join(dir, d.name);
94
+ try {
95
+ const manifest = loadTemplateFromPath(templateDir);
96
+ if (manifest.name === nameOrId)
97
+ return templateDir;
98
+ }
99
+ catch {
100
+ // Skip — template may be invalid while searching by name
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ export function resolveTemplate(source) {
106
+ // Try as local path first
107
+ if (existsSync(join(source, "template.yaml"))) {
108
+ return { path: source, manifest: loadTemplateFromPath(source) };
109
+ }
110
+ // Try as bundled template name
111
+ const bundledPath = resolveBundledTemplate(source);
112
+ if (bundledPath) {
113
+ return { path: bundledPath, manifest: loadTemplateFromPath(bundledPath) };
114
+ }
115
+ throw new Error(`Template "${source}" not found. Use "chvor init" to see available templates.`);
116
+ }
117
+ export function getTemplateSkillsDir(templatePath) {
118
+ const dir = join(templatePath, "skills");
119
+ return existsSync(dir) ? dir : null;
120
+ }
121
+ export function getTemplateToolsDir(templatePath) {
122
+ const dir = join(templatePath, "tools");
123
+ return existsSync(dir) ? dir : null;
124
+ }
125
+ /**
126
+ * Resolve a template from the community registry.
127
+ * Fetches the template.yaml and any included skills/tools into a temp directory.
128
+ */
129
+ export async function resolveRegistryTemplate(id) {
130
+ assertSafeEntryId(id);
131
+ const registryUrl = process.env.CHVOR_REGISTRY_URL || DEFAULT_REGISTRY_URL;
132
+ assertValidRegistryUrl(registryUrl);
133
+ // Fetch registry index
134
+ const indexRes = await fetch(`${registryUrl}/index.json`, {
135
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
136
+ });
137
+ if (!indexRes.ok) {
138
+ throw new Error(`Failed to fetch registry index (${indexRes.status})`);
139
+ }
140
+ const index = (await indexRes.json());
141
+ const entry = index.entries.find((e) => e.id === id && e.kind === "template");
142
+ if (!entry) {
143
+ throw new Error(`Template "${id}" not found in registry. Check the ID and try again.`);
144
+ }
145
+ // Fetch template.yaml
146
+ const yamlRes = await fetch(`${registryUrl}/templates/${encodeURIComponent(id)}/template.yaml`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
147
+ if (!yamlRes.ok) {
148
+ throw new Error(`Failed to fetch template "${id}" (${yamlRes.status})`);
149
+ }
150
+ const yamlContent = await yamlRes.text();
151
+ // Create temp directory and write manifest
152
+ const tempDir = join(tmpdir(), `chvor-template-${id}-${Date.now()}`);
153
+ mkdirSync(tempDir, { recursive: true });
154
+ writeFileSync(join(tempDir, "template.yaml"), yamlContent, "utf-8");
155
+ const manifest = parseYaml(yamlContent);
156
+ if (!validateTemplate(manifest)) {
157
+ throw new Error(`Invalid template manifest from registry for "${id}". Required fields: name, description, version`);
158
+ }
159
+ // Fetch included skills/tools
160
+ if (entry.includes?.length) {
161
+ for (const includedId of entry.includes) {
162
+ try {
163
+ assertSafeEntryId(includedId);
164
+ }
165
+ catch {
166
+ console.warn(` Warning: included entry "${includedId}" has invalid ID, skipping.`);
167
+ continue;
168
+ }
169
+ const included = index.entries.find((e) => e.id === includedId);
170
+ if (!included || (included.kind !== "skill" && included.kind !== "tool")) {
171
+ console.warn(` Warning: included entry "${includedId}" not found or unsupported kind in registry, skipping.`);
172
+ continue;
173
+ }
174
+ const contentUrl = `${registryUrl}/${included.kind}s/${encodeURIComponent(includedId)}/${included.kind}.md`;
175
+ const contentRes = await fetch(contentUrl, {
176
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
177
+ });
178
+ if (!contentRes.ok) {
179
+ console.warn(` Warning: failed to fetch included ${included.kind} "${includedId}" (${contentRes.status}), skipping.`);
180
+ continue;
181
+ }
182
+ const content = await contentRes.text();
183
+ const destDir = join(tempDir, included.kind === "skill" ? "skills" : "tools");
184
+ mkdirSync(destDir, { recursive: true });
185
+ writeFileSync(join(destDir, `${includedId}.md`), content, "utf-8");
186
+ }
187
+ }
188
+ // Note: caller is responsible for cleaning up tempDir after use
189
+ return { path: tempDir, manifest };
190
+ }