@enactprotocol/cli 1.2.13 → 2.0.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.
- package/README.md +88 -0
- package/package.json +34 -38
- package/src/commands/auth/index.ts +940 -0
- package/src/commands/cache/index.ts +361 -0
- package/src/commands/config/README.md +239 -0
- package/src/commands/config/index.ts +164 -0
- package/src/commands/env/README.md +197 -0
- package/src/commands/env/index.ts +392 -0
- package/src/commands/exec/README.md +110 -0
- package/src/commands/exec/index.ts +195 -0
- package/src/commands/get/index.ts +198 -0
- package/src/commands/index.ts +30 -0
- package/src/commands/inspect/index.ts +264 -0
- package/src/commands/install/README.md +146 -0
- package/src/commands/install/index.ts +682 -0
- package/src/commands/list/README.md +115 -0
- package/src/commands/list/index.ts +138 -0
- package/src/commands/publish/index.ts +350 -0
- package/src/commands/report/index.ts +366 -0
- package/src/commands/run/README.md +124 -0
- package/src/commands/run/index.ts +686 -0
- package/src/commands/search/index.ts +368 -0
- package/src/commands/setup/index.ts +274 -0
- package/src/commands/sign/index.ts +652 -0
- package/src/commands/trust/README.md +214 -0
- package/src/commands/trust/index.ts +453 -0
- package/src/commands/unyank/index.ts +107 -0
- package/src/commands/yank/index.ts +143 -0
- package/src/index.ts +96 -0
- package/src/types.ts +81 -0
- package/src/utils/errors.ts +409 -0
- package/src/utils/exit-codes.ts +159 -0
- package/src/utils/ignore.ts +147 -0
- package/src/utils/index.ts +107 -0
- package/src/utils/output.ts +242 -0
- package/src/utils/spinner.ts +214 -0
- package/tests/commands/auth.test.ts +217 -0
- package/tests/commands/cache.test.ts +286 -0
- package/tests/commands/config.test.ts +277 -0
- package/tests/commands/env.test.ts +293 -0
- package/tests/commands/exec.test.ts +112 -0
- package/tests/commands/get.test.ts +179 -0
- package/tests/commands/inspect.test.ts +201 -0
- package/tests/commands/install-integration.test.ts +343 -0
- package/tests/commands/install.test.ts +288 -0
- package/tests/commands/list.test.ts +160 -0
- package/tests/commands/publish.test.ts +186 -0
- package/tests/commands/report.test.ts +194 -0
- package/tests/commands/run.test.ts +231 -0
- package/tests/commands/search.test.ts +131 -0
- package/tests/commands/sign.test.ts +164 -0
- package/tests/commands/trust.test.ts +236 -0
- package/tests/commands/unyank.test.ts +114 -0
- package/tests/commands/yank.test.ts +154 -0
- package/tests/e2e.test.ts +554 -0
- package/tests/fixtures/calculator/enact.yaml +34 -0
- package/tests/fixtures/echo-tool/enact.md +31 -0
- package/tests/fixtures/env-tool/enact.yaml +19 -0
- package/tests/fixtures/greeter/enact.yaml +18 -0
- package/tests/fixtures/invalid-tool/enact.yaml +4 -0
- package/tests/index.test.ts +8 -0
- package/tests/types.test.ts +84 -0
- package/tests/utils/errors.test.ts +303 -0
- package/tests/utils/exit-codes.test.ts +189 -0
- package/tests/utils/ignore.test.ts +461 -0
- package/tests/utils/output.test.ts +126 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/index.js +0 -231612
- package/dist/index.js.bak +0 -231611
- package/dist/web/static/app.js +0 -663
- package/dist/web/static/index.html +0 -117
- package/dist/web/static/style.css +0 -291
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enact search command
|
|
3
|
+
*
|
|
4
|
+
* Search the Enact registry for tools.
|
|
5
|
+
* With --local or -g, search installed tools instead.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { type SearchResult, createApiClient, searchTools } from "@enactprotocol/api";
|
|
11
|
+
import {
|
|
12
|
+
getProjectEnactDir,
|
|
13
|
+
listInstalledTools,
|
|
14
|
+
loadConfig,
|
|
15
|
+
tryLoadManifestFromDir,
|
|
16
|
+
} from "@enactprotocol/shared";
|
|
17
|
+
import type { Command } from "commander";
|
|
18
|
+
import type { CommandContext, GlobalOptions } from "../../types";
|
|
19
|
+
import {
|
|
20
|
+
type TableColumn,
|
|
21
|
+
dim,
|
|
22
|
+
error,
|
|
23
|
+
formatError,
|
|
24
|
+
header,
|
|
25
|
+
info,
|
|
26
|
+
json,
|
|
27
|
+
newline,
|
|
28
|
+
table,
|
|
29
|
+
} from "../../utils";
|
|
30
|
+
|
|
31
|
+
interface SearchOptions extends GlobalOptions {
|
|
32
|
+
tags?: string;
|
|
33
|
+
limit?: string;
|
|
34
|
+
offset?: string;
|
|
35
|
+
local?: boolean;
|
|
36
|
+
global?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SearchResultRow {
|
|
40
|
+
name: string;
|
|
41
|
+
version: string;
|
|
42
|
+
description: string;
|
|
43
|
+
rating: string;
|
|
44
|
+
downloads: string;
|
|
45
|
+
[key: string]: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format download count for display
|
|
50
|
+
*/
|
|
51
|
+
function formatDownloads(count: number): string {
|
|
52
|
+
if (count >= 1000000) {
|
|
53
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
54
|
+
}
|
|
55
|
+
if (count >= 1000) {
|
|
56
|
+
return `${(count / 1000).toFixed(1)}K`;
|
|
57
|
+
}
|
|
58
|
+
return String(count);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Truncate description to fit table
|
|
63
|
+
*/
|
|
64
|
+
function truncateDescription(desc: string, maxLen: number): string {
|
|
65
|
+
if (desc.length <= maxLen) return desc;
|
|
66
|
+
return `${desc.substring(0, maxLen - 3)}...`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Tool info from local search
|
|
71
|
+
*/
|
|
72
|
+
interface LocalToolInfo {
|
|
73
|
+
name: string;
|
|
74
|
+
version: string;
|
|
75
|
+
description: string;
|
|
76
|
+
location: string;
|
|
77
|
+
scope: "project" | "global";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Search installed tools locally
|
|
82
|
+
*/
|
|
83
|
+
function searchLocalTools(
|
|
84
|
+
query: string,
|
|
85
|
+
scope: "project" | "global",
|
|
86
|
+
cwd: string
|
|
87
|
+
): LocalToolInfo[] {
|
|
88
|
+
const tools: LocalToolInfo[] = [];
|
|
89
|
+
const queryLower = query.toLowerCase();
|
|
90
|
+
|
|
91
|
+
if (scope === "global") {
|
|
92
|
+
// Search global tools via tools.json
|
|
93
|
+
const installedTools = listInstalledTools("global");
|
|
94
|
+
|
|
95
|
+
for (const tool of installedTools) {
|
|
96
|
+
// Load manifest from cache to get description
|
|
97
|
+
const loaded = tryLoadManifestFromDir(tool.cachePath);
|
|
98
|
+
const name = tool.name.toLowerCase();
|
|
99
|
+
const desc = (loaded?.manifest.description ?? "").toLowerCase();
|
|
100
|
+
|
|
101
|
+
// Simple fuzzy matching: check if query terms appear in name or description
|
|
102
|
+
const queryTerms = queryLower.split(/\s+/);
|
|
103
|
+
const matches = queryTerms.every((term) => name.includes(term) || desc.includes(term));
|
|
104
|
+
|
|
105
|
+
if (matches) {
|
|
106
|
+
tools.push({
|
|
107
|
+
name: tool.name,
|
|
108
|
+
version: tool.version,
|
|
109
|
+
description: loaded?.manifest.description ?? "-",
|
|
110
|
+
location: tool.cachePath,
|
|
111
|
+
scope: "global",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return tools;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Search project tools by walking directory
|
|
119
|
+
const projectDir = getProjectEnactDir(cwd);
|
|
120
|
+
const baseDir = projectDir ? join(projectDir, "tools") : null;
|
|
121
|
+
|
|
122
|
+
if (!baseDir || !existsSync(baseDir)) {
|
|
123
|
+
return tools;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Walk the tools directory structure
|
|
127
|
+
function walkDir(dir: string): void {
|
|
128
|
+
try {
|
|
129
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
130
|
+
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (!entry.isDirectory()) continue;
|
|
133
|
+
|
|
134
|
+
const entryPath = join(dir, entry.name);
|
|
135
|
+
|
|
136
|
+
// Try to load manifest from this directory
|
|
137
|
+
const loaded = tryLoadManifestFromDir(entryPath);
|
|
138
|
+
if (loaded) {
|
|
139
|
+
const manifest = loaded.manifest;
|
|
140
|
+
const name = manifest.name.toLowerCase();
|
|
141
|
+
const desc = (manifest.description ?? "").toLowerCase();
|
|
142
|
+
|
|
143
|
+
// Simple fuzzy matching: check if query terms appear in name or description
|
|
144
|
+
const queryTerms = queryLower.split(/\s+/);
|
|
145
|
+
const matches = queryTerms.every((term) => name.includes(term) || desc.includes(term));
|
|
146
|
+
|
|
147
|
+
if (matches) {
|
|
148
|
+
tools.push({
|
|
149
|
+
name: manifest.name,
|
|
150
|
+
version: manifest.version ?? "-",
|
|
151
|
+
description: manifest.description ?? "-",
|
|
152
|
+
location: entryPath,
|
|
153
|
+
scope: "project",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Recurse into subdirectories (for nested namespaces)
|
|
158
|
+
walkDir(entryPath);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// Ignore errors reading directories
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
walkDir(baseDir);
|
|
167
|
+
return tools;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Search command handler
|
|
172
|
+
*/
|
|
173
|
+
async function searchHandler(
|
|
174
|
+
query: string,
|
|
175
|
+
options: SearchOptions,
|
|
176
|
+
ctx: CommandContext
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
// Handle local search (--local or -g)
|
|
179
|
+
if (options.local || options.global) {
|
|
180
|
+
const scope = options.global ? "global" : "project";
|
|
181
|
+
const results = searchLocalTools(query, scope, ctx.cwd);
|
|
182
|
+
|
|
183
|
+
// JSON output
|
|
184
|
+
if (options.json) {
|
|
185
|
+
json({ query, scope, results, total: results.length });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// No results
|
|
190
|
+
if (results.length === 0) {
|
|
191
|
+
info(`No ${scope} tools found matching "${query}"`);
|
|
192
|
+
dim(
|
|
193
|
+
scope === "project"
|
|
194
|
+
? "Try 'enact search -g' to search global tools, or search the registry without flags"
|
|
195
|
+
: "Try searching the registry without the -g flag"
|
|
196
|
+
);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
header(`${scope === "global" ? "Global" : "Project"} Tools matching "${query}"`);
|
|
201
|
+
newline();
|
|
202
|
+
|
|
203
|
+
const columns: TableColumn[] = [
|
|
204
|
+
{ key: "name", header: "Name", width: 28 },
|
|
205
|
+
{ key: "description", header: "Description", width: 50 },
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
if (ctx.options.verbose) {
|
|
209
|
+
columns.push({ key: "version", header: "Version", width: 10 });
|
|
210
|
+
columns.push({ key: "location", header: "Location", width: 40 });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const rows = results.map((r) => ({
|
|
214
|
+
name: r.name,
|
|
215
|
+
description: truncateDescription(r.description, 48),
|
|
216
|
+
version: r.version,
|
|
217
|
+
location: r.location,
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
table(rows, columns);
|
|
221
|
+
newline();
|
|
222
|
+
dim(`Found ${results.length} matching tool(s)`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Default: Registry search
|
|
227
|
+
const config = loadConfig();
|
|
228
|
+
const registryUrl =
|
|
229
|
+
process.env.ENACT_REGISTRY_URL ??
|
|
230
|
+
config.registry?.url ??
|
|
231
|
+
"https://siikwkfgsmouioodghho.supabase.co/functions/v1";
|
|
232
|
+
const authToken = config.registry?.authToken;
|
|
233
|
+
const client = createApiClient({
|
|
234
|
+
baseUrl: registryUrl,
|
|
235
|
+
authToken: authToken,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const limit = options.limit ? Number.parseInt(options.limit, 10) : 20;
|
|
239
|
+
const offset = options.offset ? Number.parseInt(options.offset, 10) : 0;
|
|
240
|
+
|
|
241
|
+
if (ctx.options.verbose) {
|
|
242
|
+
info(`Searching for: "${query}"`);
|
|
243
|
+
if (options.tags) {
|
|
244
|
+
info(`Tags: ${options.tags}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const response = await searchTools(client, {
|
|
250
|
+
query,
|
|
251
|
+
tags: options.tags,
|
|
252
|
+
limit,
|
|
253
|
+
offset,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// JSON output
|
|
257
|
+
if (options.json) {
|
|
258
|
+
json({
|
|
259
|
+
query,
|
|
260
|
+
results: response.results,
|
|
261
|
+
total: response.total,
|
|
262
|
+
limit: response.limit,
|
|
263
|
+
offset: response.offset,
|
|
264
|
+
hasMore: response.hasMore,
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// No results
|
|
270
|
+
if (response.results.length === 0) {
|
|
271
|
+
info(`No tools found matching "${query}"`);
|
|
272
|
+
dim("Try a different search term or remove tag filters");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Format results for table
|
|
277
|
+
const rows: SearchResultRow[] = response.results.map((result: SearchResult) => ({
|
|
278
|
+
name: result.name,
|
|
279
|
+
version: result.version,
|
|
280
|
+
description: truncateDescription(result.description, 40),
|
|
281
|
+
rating: result.trustStatus ? `${result.trustStatus.auditorCount} ✓` : "-",
|
|
282
|
+
downloads: formatDownloads(result.downloads),
|
|
283
|
+
}));
|
|
284
|
+
|
|
285
|
+
header(`Search Results for "${query}"`);
|
|
286
|
+
newline();
|
|
287
|
+
|
|
288
|
+
const columns: TableColumn[] = [
|
|
289
|
+
{ key: "name", header: "Name", width: 30 },
|
|
290
|
+
{ key: "version", header: "Version", width: 10 },
|
|
291
|
+
{ key: "description", header: "Description", width: 42 },
|
|
292
|
+
{ key: "rating", header: "Rating", width: 8 },
|
|
293
|
+
{ key: "downloads", header: "↓", width: 8 },
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
table(rows, columns);
|
|
297
|
+
newline();
|
|
298
|
+
|
|
299
|
+
// Pagination info
|
|
300
|
+
const showing = offset + response.results.length;
|
|
301
|
+
dim(`Showing ${offset + 1}-${showing} of ${response.total} results`);
|
|
302
|
+
|
|
303
|
+
if (response.hasMore) {
|
|
304
|
+
dim(`Use --offset ${showing} to see more results`);
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
// Handle specific error types
|
|
308
|
+
if (err instanceof Error) {
|
|
309
|
+
const message = err.message.toLowerCase();
|
|
310
|
+
|
|
311
|
+
// Connection errors
|
|
312
|
+
if (
|
|
313
|
+
message.includes("fetch") ||
|
|
314
|
+
message.includes("econnrefused") ||
|
|
315
|
+
message.includes("network") ||
|
|
316
|
+
message.includes("timeout") ||
|
|
317
|
+
err.name === "AbortError"
|
|
318
|
+
) {
|
|
319
|
+
error("Unable to connect to registry");
|
|
320
|
+
dim("Check your internet connection or try again later");
|
|
321
|
+
dim(`Registry URL: ${registryUrl}`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// JSON parsing errors (server returned non-JSON)
|
|
326
|
+
if (message.includes("json") || message.includes("unexpected token")) {
|
|
327
|
+
error("Registry returned an invalid response");
|
|
328
|
+
dim("The server may be down or experiencing issues");
|
|
329
|
+
dim(`Registry URL: ${registryUrl}`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Re-throw other errors
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Configure the search command
|
|
341
|
+
*/
|
|
342
|
+
export function configureSearchCommand(program: Command): void {
|
|
343
|
+
program
|
|
344
|
+
.command("search <query>")
|
|
345
|
+
.description("Search the Enact registry for tools")
|
|
346
|
+
.option("--local", "Search project tools (.enact/tools/) instead of registry")
|
|
347
|
+
.option("-g, --global", "Search global tools (~/.enact/tools/) instead of registry")
|
|
348
|
+
.option("-t, --tags <tags>", "Filter by tags (comma-separated, registry only)")
|
|
349
|
+
.option("-l, --limit <number>", "Maximum results to return (default: 20, registry only)")
|
|
350
|
+
.option("-o, --offset <number>", "Pagination offset (default: 0, registry only)")
|
|
351
|
+
.option("-v, --verbose", "Show detailed output")
|
|
352
|
+
.option("--json", "Output as JSON")
|
|
353
|
+
.action(async (query: string, options: SearchOptions) => {
|
|
354
|
+
const ctx: CommandContext = {
|
|
355
|
+
cwd: process.cwd(),
|
|
356
|
+
options,
|
|
357
|
+
isCI: Boolean(process.env.CI),
|
|
358
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
await searchHandler(query, options, ctx);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
error(formatError(err));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enact setup command
|
|
3
|
+
*
|
|
4
|
+
* Set up Enact configuration interactively
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
import * as clack from "@clack/prompts";
|
|
10
|
+
import { type EnactConfig, getConfigPath, loadConfig } from "@enactprotocol/shared";
|
|
11
|
+
import type { Command } from "commander";
|
|
12
|
+
import type { CommandContext, GlobalOptions } from "../../types";
|
|
13
|
+
import { dim, error, formatError, info } from "../../utils";
|
|
14
|
+
|
|
15
|
+
interface SetupOptions extends GlobalOptions {
|
|
16
|
+
force?: boolean;
|
|
17
|
+
global?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default configuration values
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_CONFIG: Partial<EnactConfig> = {
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
trust: {
|
|
26
|
+
minimum_attestations: 1,
|
|
27
|
+
},
|
|
28
|
+
cache: {
|
|
29
|
+
maxSizeMb: 1024,
|
|
30
|
+
ttlSeconds: 604800, // 7 days
|
|
31
|
+
},
|
|
32
|
+
execution: {
|
|
33
|
+
defaultTimeout: "30s",
|
|
34
|
+
verbose: false,
|
|
35
|
+
},
|
|
36
|
+
registry: {
|
|
37
|
+
url: "https://siikwkfgsmouioodghho.supabase.co/functions/v1",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Setup command handler
|
|
43
|
+
*/
|
|
44
|
+
async function setupHandler(options: SetupOptions, _ctx: CommandContext): Promise<void> {
|
|
45
|
+
const actualScope = options.global ? "global" : "project";
|
|
46
|
+
const configPath = getConfigPath();
|
|
47
|
+
|
|
48
|
+
// Check if config already exists
|
|
49
|
+
if (existsSync(configPath) && !options.force) {
|
|
50
|
+
clack.log.warn(`Configuration already exists at: ${configPath}`);
|
|
51
|
+
const overwrite = await clack.confirm({
|
|
52
|
+
message: "Overwrite existing configuration?",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (clack.isCancel(overwrite) || !overwrite) {
|
|
56
|
+
clack.cancel("Setup cancelled");
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clack.intro(`Setting up Enact ${actualScope} configuration`);
|
|
62
|
+
|
|
63
|
+
// Load existing config if available
|
|
64
|
+
let existingConfig: EnactConfig = {};
|
|
65
|
+
try {
|
|
66
|
+
if (existsSync(configPath)) {
|
|
67
|
+
existingConfig = loadConfig();
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Ignore errors loading existing config
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Prompt for configuration
|
|
74
|
+
const registryUrl = await clack.text({
|
|
75
|
+
message: "Registry URL (recommended: https://siikwkfgsmouioodghho.supabase.co/functions/v1)",
|
|
76
|
+
placeholder: "https://siikwkfgsmouioodghho.supabase.co/functions/v1",
|
|
77
|
+
defaultValue: "https://siikwkfgsmouioodghho.supabase.co/functions/v1",
|
|
78
|
+
validate: (value) => {
|
|
79
|
+
// Allow empty to use default
|
|
80
|
+
if (!value || value.trim() === "") return undefined;
|
|
81
|
+
try {
|
|
82
|
+
new URL(value);
|
|
83
|
+
return undefined;
|
|
84
|
+
} catch {
|
|
85
|
+
return "Invalid URL format";
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (clack.isCancel(registryUrl)) {
|
|
91
|
+
clack.cancel("Setup cancelled");
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Use default if empty
|
|
96
|
+
const finalRegistryUrl =
|
|
97
|
+
(registryUrl as string).trim() || "https://siikwkfgsmouioodghho.supabase.co/functions/v1";
|
|
98
|
+
|
|
99
|
+
const isLocalDev =
|
|
100
|
+
finalRegistryUrl.includes("localhost") || finalRegistryUrl.includes("127.0.0.1");
|
|
101
|
+
const isOfficialRegistry = finalRegistryUrl.includes("siikwkfgsmouioodghho.supabase.co");
|
|
102
|
+
|
|
103
|
+
let authToken: string | undefined;
|
|
104
|
+
if (isLocalDev) {
|
|
105
|
+
const useAnonKey = await clack.confirm({
|
|
106
|
+
message: "Use local development anon key?",
|
|
107
|
+
initialValue: true,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (clack.isCancel(useAnonKey)) {
|
|
111
|
+
clack.cancel("Setup cancelled");
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (useAnonKey) {
|
|
116
|
+
authToken =
|
|
117
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0";
|
|
118
|
+
}
|
|
119
|
+
} else if (isOfficialRegistry) {
|
|
120
|
+
// Use official registry anon key
|
|
121
|
+
authToken =
|
|
122
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaWt3a2Znc21vdWlvb2RnaGhvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ2MTkzMzksImV4cCI6MjA4MDE5NTMzOX0.kxnx6-IPFhmGx6rzNx36vbyhFMFZKP_jFqaDbKnJ_E0";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const minimumAttestations = await clack.text({
|
|
126
|
+
message: "Minimum attestations required for trust",
|
|
127
|
+
placeholder: "1",
|
|
128
|
+
defaultValue: String(
|
|
129
|
+
existingConfig.trust?.minimum_attestations || DEFAULT_CONFIG.trust?.minimum_attestations
|
|
130
|
+
),
|
|
131
|
+
validate: (value) => {
|
|
132
|
+
if (!value || value.trim() === "") return undefined; // Allow empty for default
|
|
133
|
+
const num = Number(value);
|
|
134
|
+
if (Number.isNaN(num) || num < 0) return "Must be a positive number";
|
|
135
|
+
return undefined;
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (clack.isCancel(minimumAttestations)) {
|
|
140
|
+
clack.cancel("Setup cancelled");
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const finalMinAttestations = (minimumAttestations as string).trim()
|
|
145
|
+
? Number(minimumAttestations)
|
|
146
|
+
: existingConfig.trust?.minimum_attestations || DEFAULT_CONFIG.trust?.minimum_attestations || 1;
|
|
147
|
+
|
|
148
|
+
const cacheMaxSize = await clack.text({
|
|
149
|
+
message: "Maximum cache size (MB)",
|
|
150
|
+
placeholder: "1024",
|
|
151
|
+
defaultValue: String(existingConfig.cache?.maxSizeMb || DEFAULT_CONFIG.cache?.maxSizeMb),
|
|
152
|
+
validate: (value) => {
|
|
153
|
+
if (!value || value.trim() === "") return undefined; // Allow empty for default
|
|
154
|
+
const num = Number(value);
|
|
155
|
+
if (Number.isNaN(num) || num <= 0) return "Must be a positive number";
|
|
156
|
+
return undefined;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (clack.isCancel(cacheMaxSize)) {
|
|
161
|
+
clack.cancel("Setup cancelled");
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const finalCacheMaxSize = (cacheMaxSize as string).trim()
|
|
166
|
+
? Number(cacheMaxSize)
|
|
167
|
+
: existingConfig.cache?.maxSizeMb || DEFAULT_CONFIG.cache?.maxSizeMb || 1024;
|
|
168
|
+
|
|
169
|
+
const defaultTimeout = await clack.text({
|
|
170
|
+
message: "Default execution timeout",
|
|
171
|
+
placeholder: "30s",
|
|
172
|
+
defaultValue:
|
|
173
|
+
existingConfig.execution?.defaultTimeout || DEFAULT_CONFIG.execution?.defaultTimeout || "30s",
|
|
174
|
+
validate: (value) => {
|
|
175
|
+
if (!value || value.trim() === "") return undefined; // Allow empty for default
|
|
176
|
+
if (!/^\d+[smh]$/.test(value)) {
|
|
177
|
+
return "Must be in format: 30s, 5m, or 1h";
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (clack.isCancel(defaultTimeout)) {
|
|
184
|
+
clack.cancel("Setup cancelled");
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const finalDefaultTimeout =
|
|
189
|
+
(defaultTimeout as string).trim() ||
|
|
190
|
+
existingConfig.execution?.defaultTimeout ||
|
|
191
|
+
DEFAULT_CONFIG.execution?.defaultTimeout ||
|
|
192
|
+
"30s";
|
|
193
|
+
|
|
194
|
+
// Build configuration
|
|
195
|
+
const config: EnactConfig = {
|
|
196
|
+
version: "1.0.0",
|
|
197
|
+
trust: {
|
|
198
|
+
minimum_attestations: finalMinAttestations,
|
|
199
|
+
},
|
|
200
|
+
cache: {
|
|
201
|
+
maxSizeMb: finalCacheMaxSize,
|
|
202
|
+
ttlSeconds: existingConfig.cache?.ttlSeconds || DEFAULT_CONFIG.cache?.ttlSeconds || 604800,
|
|
203
|
+
},
|
|
204
|
+
execution: {
|
|
205
|
+
defaultTimeout: finalDefaultTimeout,
|
|
206
|
+
verbose: existingConfig.execution?.verbose || DEFAULT_CONFIG.execution?.verbose || false,
|
|
207
|
+
},
|
|
208
|
+
registry: {
|
|
209
|
+
url: finalRegistryUrl,
|
|
210
|
+
...(authToken && { authToken }),
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Save configuration
|
|
215
|
+
const configDir = dirname(configPath);
|
|
216
|
+
if (!existsSync(configDir)) {
|
|
217
|
+
mkdirSync(configDir, { recursive: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Convert to YAML format
|
|
221
|
+
const yaml = [
|
|
222
|
+
`version: ${config.version}`,
|
|
223
|
+
"trust:",
|
|
224
|
+
` minimum_attestations: ${config.trust?.minimum_attestations}`,
|
|
225
|
+
"cache:",
|
|
226
|
+
` maxSizeMb: ${config.cache?.maxSizeMb}`,
|
|
227
|
+
` ttlSeconds: ${config.cache?.ttlSeconds}`,
|
|
228
|
+
"execution:",
|
|
229
|
+
` defaultTimeout: ${config.execution?.defaultTimeout}`,
|
|
230
|
+
` verbose: ${config.execution?.verbose}`,
|
|
231
|
+
"registry:",
|
|
232
|
+
` url: ${config.registry?.url}`,
|
|
233
|
+
...(config.registry?.authToken ? [` authToken: ${config.registry.authToken}`] : []),
|
|
234
|
+
].join("\n");
|
|
235
|
+
|
|
236
|
+
writeFileSync(configPath, `${yaml}\n`, "utf-8");
|
|
237
|
+
|
|
238
|
+
clack.outro(`Configuration saved to ${configPath}`);
|
|
239
|
+
|
|
240
|
+
if (options.verbose) {
|
|
241
|
+
info("\nConfiguration:");
|
|
242
|
+
dim(yaml);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Configure the setup command
|
|
248
|
+
*/
|
|
249
|
+
export function configureSetupCommand(program: Command): void {
|
|
250
|
+
program
|
|
251
|
+
.command("setup")
|
|
252
|
+
.description("Set up Enact configuration")
|
|
253
|
+
.option("-g, --global", "Initialize global configuration (~/.enact/config.yaml)")
|
|
254
|
+
.option("-f, --force", "Overwrite existing configuration without prompting")
|
|
255
|
+
.option("-v, --verbose", "Show detailed output")
|
|
256
|
+
.action(async (options: SetupOptions) => {
|
|
257
|
+
const ctx: CommandContext = {
|
|
258
|
+
cwd: process.cwd(),
|
|
259
|
+
options,
|
|
260
|
+
isCI: Boolean(process.env.CI),
|
|
261
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await setupHandler(options, ctx);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
error(formatError(err));
|
|
268
|
+
if (options.verbose && err instanceof Error && err.stack) {
|
|
269
|
+
dim(err.stack);
|
|
270
|
+
}
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|