@finchip.ai/foga 0.6.0-alpha.6 → 0.6.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 +35 -14
- package/dist/a2a/card.js +54 -0
- package/dist/a2a/server.js +276 -0
- package/dist/a2a/types.js +20 -0
- package/dist/build/bundle.js +2 -1
- package/dist/cli-cell.js +176 -16
- package/dist/cli.js +150 -5
- package/dist/core/world.js +17 -0
- package/dist/demo/birth.js +11 -5
- package/dist/demo/loop-d.js +39 -1
- package/dist/demo/quickstart.js +97 -11
- package/dist/genesis/operator-scope.js +1 -0
- package/dist/genesis/probe.js +23 -7
- package/dist/http/access.js +105 -0
- package/dist/http/server.js +84 -43
- package/dist/http/signed.js +136 -0
- package/dist/mcp/server.js +48 -1
- package/dist/node/advise.js +134 -0
- package/dist/node/config.js +55 -1
- package/dist/node/documents.js +298 -0
- package/dist/node/preflight.js +208 -0
- package/dist/node/propagation.js +95 -0
- package/dist/node/wallet.js +132 -0
- package/dist/tools/documents.js +74 -0
- package/dist/tools/index.js +3 -1
- package/dist/tools/propagation.js +35 -0
- package/dist/tools/tenders.js +22 -6
- package/dist/util/version.js +2 -0
- package/dist/world/sourcify.js +165 -0
- package/docs/a2a.md +31 -0
- package/docs/design-memo.md +23 -6
- package/docs/documents.md +25 -0
- package/docs/examples/README.md +2 -2
- package/docs/extending.md +49 -0
- package/docs/loop-d.md +11 -0
- package/docs/mcp.md +18 -11
- package/docs/node.md +35 -11
- package/docs/og-deployment.md +131 -0
- package/docs/propagation.md +41 -0
- package/docs/quickstart.md +36 -21
- package/docs/wallet.md +62 -0
- package/docs/world.md +12 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/cli-cell.js
CHANGED
|
@@ -6,6 +6,7 @@ import { cellInit } from "./node/init.js";
|
|
|
6
6
|
import { KeyIsolationError, forbiddenVariables, loadOperatorKey, loadRoleKey } from "./node/keys.js";
|
|
7
7
|
import { cellMigrate } from "./node/migrate.js";
|
|
8
8
|
import { Node } from "./node/node.js";
|
|
9
|
+
import { loadWallet, walletKey } from "./node/wallet.js";
|
|
9
10
|
import { Watchers } from "./node/watchers.js";
|
|
10
11
|
import { defaultCatalog, OPERATOR_CALLER, PUBLIC_CALLER } from "./tools/index.js";
|
|
11
12
|
import { exists, repoRoot } from "./util/fs.js";
|
|
@@ -20,23 +21,40 @@ import { fail } from "./util/log.js";
|
|
|
20
21
|
export const CELL_USAGE = `
|
|
21
22
|
foga cell list [--public] [--as-json] the capabilities (action, visibility, principal, modes) as the operator or as the public sees them
|
|
22
23
|
foga cell schema <capability> the input and output JSON Schema of a capability
|
|
23
|
-
foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --
|
|
24
|
-
foga cell init --config <cell.config.json> [--rpc <url>] [--capital-wei <n>]
|
|
25
|
-
the cell's birth (the owner's key: FOGA_OWNER_KEY[_FILE]); writes the account, agent id, binding, record and unit into the config
|
|
24
|
+
foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --out <cell.config.json> (--wallet <dir> | --owner <addr> --operator <addr> --guardian <addr>) [--rpc <url>] [--skills <dir>] [--input-dir <dir>]
|
|
25
|
+
foga cell init --config <cell.config.json> [--wallet <dir>] [--rpc <url>] [--capital-wei <n>]
|
|
26
|
+
the cell's birth (the owner's key: the wallet's, or FOGA_OWNER_KEY[_FILE]); writes the account, agent id, binding, record and unit into the config
|
|
26
27
|
foga cell migrate --config <cell.config.json> --to <new.config.json> [--store <new ops.sqlite>] [--operator <addr>] [--capital-wei <n>] [--no-sweep]
|
|
27
28
|
move the cell to a new revision-2 account (the owner's key); the new config and store are written beside
|
|
28
29
|
foga cell status --config <cell.config.json> cell_status as the operator
|
|
29
|
-
foga cell
|
|
30
|
-
|
|
30
|
+
foga cell advise --config <cell.config.json> [--brief] [--as-json]
|
|
31
|
+
what a running cell should do next — separate the roles, tune the policy, standing, a public face, a supervisor,
|
|
32
|
+
backups, a dedicated RPC — only what applies to this cell; advice, never required; no chain read, no key
|
|
33
|
+
foga cell docs build --config <cell.config.json> [--rpc <url>] [--out <dir>]
|
|
34
|
+
the cell's documents from its configuration and the chain (registration, AID document, ARD descriptor, AUF unit,
|
|
35
|
+
agent card, server.json, manifest) under runtime.http.documentsDir — what the node serves at /documents and /.well-known
|
|
36
|
+
foga cell docs publish --config <cell.config.json> [--wallet <dir>] [--rpc <url>]
|
|
37
|
+
build, then commit on chain what differs (setAgentURI, setDocumentURI, updateDescriptor, publishVersion; the operator
|
|
38
|
+
key, one keyed job), then read every commitment back
|
|
39
|
+
foga cell docs status --config <cell.config.json> [--rpc <url>] what is built, what the chain holds, what a publish would commit
|
|
40
|
+
foga cell <capability> --config <cell.config.json> [--wallet <dir>] [--mode simulate|build|act] [--json '<input json>' | --json-file <f>] [--<field> <value> ...]
|
|
41
|
+
any capability; an act loads the operator key, an admin capability the owner's or the guardian's — from --wallet, or from
|
|
42
|
+
FOGA_OPERATOR_KEY[_FILE], FOGA_OWNER_KEY[_FILE], FOGA_GUARDIAN_KEY[_FILE]
|
|
31
43
|
[--caller <id> --level read|build|act] call as another caller (what the public or an API-key holder would get)
|
|
32
44
|
foga cell owner <capability> … | foga cell guardian <capability> … the same, said explicitly
|
|
45
|
+
foga node <command> … [--wallet <dir>] every node command takes the operator key from the wallet's operator.key (= FOGA_OPERATOR_KEY_FILE)
|
|
46
|
+
foga node preflight --config <cell.config.json> [--wallet <dir>] [--rpc <url>] [--no-executors] [--no-network] [--out <report.json>] [--strict]
|
|
47
|
+
the readiness reconciliation before the public is served: the account and its allow-list, the operator key and its
|
|
48
|
+
gas, the standing, the skills' packages and executors, the documents against the chain, the public URL, the model;
|
|
49
|
+
advisory (exit 5 on a Fail only with --strict)
|
|
33
50
|
foga node tick --config <cell.config.json> run the watchers once (heartbeat, discovery, settlement, safety) in a node environment (operator key alone)
|
|
34
51
|
foga node watch --config <cell.config.json> [--interval <s>] run the watchers on the poll interval until stopped
|
|
35
52
|
foga node mcp --config <cell.config.json> MCP over stdio for a local client (Claude Desktop, Claude Code, Cursor): the operator's full level with
|
|
36
53
|
FOGA_OPERATOR_KEY[_FILE] in the environment, read and build without it; logs go to stderr
|
|
37
54
|
foga node serve --config <cell.config.json> [--listen <host:port>] [--no-watch]
|
|
38
|
-
|
|
39
|
-
a Bearer key acts as its grant says;
|
|
55
|
+
host mode: POST /api/<capability>, /mcp (Streamable HTTP), /a2a (A2A 1.0), /.well-known/agent-card.json, /documents/*,
|
|
56
|
+
/healthz, / — the public reads and builds, a Bearer key or a wallet signature (FOGA-EIP712) acts as its grant says;
|
|
57
|
+
the watchers run beside it; no model
|
|
40
58
|
foga node key new --config <cell.config.json> --id <id> [--level read|build|act] [--visibility public|caller|operator] [--note <text>]
|
|
41
59
|
mint an API key for the HTTP entrance: printed once, kept only as its hash in the config
|
|
42
60
|
foga node run --config <cell.config.json> [--model stub|anthropic] [--listen <host:port> | --no-http] [--interval <s>] [--once | --ticks <n>] [--transcripts <dir>]
|
|
@@ -44,7 +62,7 @@ export const CELL_USAGE = `
|
|
|
44
62
|
judges) on the poll interval; the operator key alone; anthropic needs ANTHROPIC_API_KEY and runtime.model.model
|
|
45
63
|
`;
|
|
46
64
|
const str = (flags, key) => (typeof flags[key] === "string" ? flags[key] : undefined);
|
|
47
|
-
const RESERVED = new Set(["config", "mode", "json", "json-file", "caller", "level", "rpc", "public", "help"]);
|
|
65
|
+
const RESERVED = new Set(["config", "mode", "json", "json-file", "caller", "level", "rpc", "public", "help", "wallet"]);
|
|
48
66
|
function inputFromFlags(flags) {
|
|
49
67
|
let input = {};
|
|
50
68
|
const inline = str(flags, "json");
|
|
@@ -81,7 +99,26 @@ function callerFromFlags(flags) {
|
|
|
81
99
|
fail(`--level must be read, build, act or admin`);
|
|
82
100
|
return { id: id ?? "caller", entrance: "cli", level: lvl, visibility: lvl === "admin" ? "operator" : id ? "caller" : "public" };
|
|
83
101
|
}
|
|
84
|
-
|
|
102
|
+
/** The wallet `--wallet <dir>` names, or null without the flag. */
|
|
103
|
+
function walletFromFlags(flags) {
|
|
104
|
+
const dir = str(flags, "wallet");
|
|
105
|
+
if (!dir)
|
|
106
|
+
return null;
|
|
107
|
+
const w = loadWallet(resolve(dir));
|
|
108
|
+
if (!w)
|
|
109
|
+
fail(`${resolve(dir)} holds no wallet (no wallet.json); foga wallet new --dir ${resolve(dir)} makes one`);
|
|
110
|
+
return w;
|
|
111
|
+
}
|
|
112
|
+
/** A role's key: from the wallet when --wallet is given, from the environment otherwise. */
|
|
113
|
+
function keysFor(role, strict = false, wallet = null) {
|
|
114
|
+
if (wallet) {
|
|
115
|
+
try {
|
|
116
|
+
return walletKey(wallet, role);
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
fail(e.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
85
122
|
try {
|
|
86
123
|
return (role === "operator" && strict ? loadOperatorKey() : loadRoleKey(role)).privateKey;
|
|
87
124
|
}
|
|
@@ -120,7 +157,8 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
120
157
|
return print({ name: cap.name, title: cap.title, description: cap.description, action: cap.action, visibility: cap.visibility, principal: cap.principal, ercs: cap.ercs, input: z.toJSONSchema(cap.input, { target: "draft-2020-12", io: "input" }), output: z.toJSONSchema(cap.output, { target: "draft-2020-12", io: "output" }) });
|
|
121
158
|
}
|
|
122
159
|
if (cmd === "new") {
|
|
123
|
-
const
|
|
160
|
+
const wallet = walletFromFlags(flags);
|
|
161
|
+
const need = (k) => str(flags, k) ?? (wallet && wallet.addresses[k] ? wallet.addresses[k] : fail(`--${k} is required (or --wallet <dir> with a ${k} key)`));
|
|
124
162
|
const out = resolve(need("out"));
|
|
125
163
|
if (exists(out))
|
|
126
164
|
fail(`${out} exists; choose another --out (a config is never overwritten)`);
|
|
@@ -137,13 +175,33 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
137
175
|
const configPath = str(flags, "config") ?? fail("--config <cell.config.json> is required");
|
|
138
176
|
const rpc = str(flags, "rpc");
|
|
139
177
|
const log = (line) => console.error(line);
|
|
178
|
+
const wallet = walletFromFlags(flags);
|
|
179
|
+
if (cmd === "advise") {
|
|
180
|
+
const { adviseCell, formatAdvice } = await import("./node/advise.js");
|
|
181
|
+
const node = await Node.open({ configPath, rpc, keys: {}, allowUnborn: true, log: () => { } });
|
|
182
|
+
try {
|
|
183
|
+
const advice = adviseCell(node);
|
|
184
|
+
if (flags["as-json"])
|
|
185
|
+
return print(advice);
|
|
186
|
+
console.log(formatAdvice(advice, { brief: Boolean(flags.brief), config: configPath }));
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
node.close();
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
140
193
|
if (cmd === "init") {
|
|
141
|
-
const owner = keysFor("owner");
|
|
194
|
+
const owner = keysFor("owner", false, wallet);
|
|
142
195
|
const node = await Node.open({ configPath, rpc, keys: { owner }, allowUnborn: true, log });
|
|
143
196
|
try {
|
|
144
197
|
const capital = str(flags, "capital-wei");
|
|
145
198
|
const result = await cellInit(node, { capitalWei: capital ? BigInt(capital) : undefined, log });
|
|
146
199
|
print(result);
|
|
200
|
+
// Born: say what a cell should do next, once, briefly — the first run stays minimal, the hardening is advice.
|
|
201
|
+
const { adviseCell, formatAdvice } = await import("./node/advise.js");
|
|
202
|
+
const next = formatAdvice(adviseCell(node), { brief: true, config: configPath });
|
|
203
|
+
if (next)
|
|
204
|
+
console.error(`\n${next}`);
|
|
147
205
|
}
|
|
148
206
|
finally {
|
|
149
207
|
node.close();
|
|
@@ -151,7 +209,7 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
151
209
|
return;
|
|
152
210
|
}
|
|
153
211
|
if (cmd === "migrate") {
|
|
154
|
-
const owner = keysFor("owner");
|
|
212
|
+
const owner = keysFor("owner", false, wallet);
|
|
155
213
|
const to = resolve(str(flags, "to") ?? fail("--to <new.config.json> is required"));
|
|
156
214
|
if (exists(to))
|
|
157
215
|
fail(`${to} exists; choose another --to`);
|
|
@@ -166,6 +224,72 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
166
224
|
}
|
|
167
225
|
return;
|
|
168
226
|
}
|
|
227
|
+
if (cmd === "docs") {
|
|
228
|
+
const sub = rest[0];
|
|
229
|
+
const { buildDocuments, readBack, documentsDir, onChainState, readBuilt, publishSteps } = await import("./node/documents.js");
|
|
230
|
+
const { version } = await import("./util/version.js");
|
|
231
|
+
if (sub === "build") {
|
|
232
|
+
const node = await Node.open({ configPath, rpc, keys: {}, log });
|
|
233
|
+
try {
|
|
234
|
+
const built = await buildDocuments(node, { version, dir: str(flags, "out") });
|
|
235
|
+
console.error(`documents v${built.manifest.version} written to ${built.dir}:`);
|
|
236
|
+
for (const [file, d] of Object.entries(built.manifest.documents))
|
|
237
|
+
console.error(` ${file.padEnd(20)} ${d.standard.padEnd(40)} ${d.contentHash}`);
|
|
238
|
+
const { steps, unchanged } = await publishSteps(node, built);
|
|
239
|
+
console.error(steps.length ? `a publish would commit: ${steps.map((x) => x.label).join("; ")}` : "every commitment on chain matches what was built");
|
|
240
|
+
if (unchanged.length)
|
|
241
|
+
console.error(`already as built: ${unchanged.join(", ")}`);
|
|
242
|
+
print(built.manifest);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
node.close();
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (sub === "status") {
|
|
250
|
+
const node = await Node.open({ configPath, rpc, keys: {}, log });
|
|
251
|
+
try {
|
|
252
|
+
const dir = documentsDir(node);
|
|
253
|
+
const built = readBuilt(dir);
|
|
254
|
+
const rows = built ? await readBack(node, built) : [];
|
|
255
|
+
print({ dir, manifest: built?.manifest ?? null, onChain: await onChainState(node), readBack: rows, pending: built ? (await publishSteps(node, built)).steps.map((x) => x.label) : [] });
|
|
256
|
+
if (rows.some((r) => r.ok === false))
|
|
257
|
+
process.exitCode = 2;
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
node.close();
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (sub === "publish") {
|
|
265
|
+
const node = await Node.open({ configPath, rpc, keys: { operator: keysFor("operator", false, wallet) }, log });
|
|
266
|
+
try {
|
|
267
|
+
const result = await catalog.invoke(node, "docs_publish", {}, OPERATOR_CALLER, { mode: "act" });
|
|
268
|
+
if (!result.ok) {
|
|
269
|
+
print(result);
|
|
270
|
+
process.exitCode = 2;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const out = result.output;
|
|
274
|
+
console.error(`documents v${out.version} in ${out.dir}: ${out.committed.length ? `committed ${out.committed.join(", ")}` : "nothing to commit"}${out.unchanged.length ? `; already as built: ${out.unchanged.join(", ")}` : ""} (job ${out.jobId}, ${out.operations.length} operation(s))`);
|
|
275
|
+
// the read-back from here fetches the URLs too, when the node serves them
|
|
276
|
+
const built = readBuilt(out.dir);
|
|
277
|
+
const rows = await readBack(node, built, { fetchUrls: true });
|
|
278
|
+
for (const r of rows)
|
|
279
|
+
console.error(` ${r.ok === true ? "ok " : r.ok === false ? "FAIL " : "unread "} ${r.commitment.padEnd(26)} ${r.detail}`);
|
|
280
|
+
print({ ...out, readBack: rows });
|
|
281
|
+
if (rows.some((r) => r.ok === false)) {
|
|
282
|
+
console.error("a commitment read back from the chain does not match what was built: the publish is not complete");
|
|
283
|
+
process.exitCode = 2;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
node.close();
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
fail("foga cell docs build | publish | status");
|
|
292
|
+
}
|
|
169
293
|
// a capability, possibly said as "owner <capability>" / "guardian <capability>"
|
|
170
294
|
let name = cmd;
|
|
171
295
|
let explicitRole = null;
|
|
@@ -186,10 +310,10 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
186
310
|
if (cap.action !== "read" && mode === "act") {
|
|
187
311
|
if (cap.action === "admin") {
|
|
188
312
|
const role = cap.signer ?? explicitRole ?? "owner";
|
|
189
|
-
keys[role] = keysFor(role);
|
|
313
|
+
keys[role] = keysFor(role, false, wallet);
|
|
190
314
|
}
|
|
191
315
|
else
|
|
192
|
-
keys.operator = keysFor("operator");
|
|
316
|
+
keys.operator = keysFor("operator", false, wallet);
|
|
193
317
|
}
|
|
194
318
|
const node = await Node.open({ configPath, rpc, keys, log });
|
|
195
319
|
try {
|
|
@@ -230,6 +354,32 @@ export async function nodeCommand(cmd, rest, flags) {
|
|
|
230
354
|
}
|
|
231
355
|
const configPath = str(flags, "config") ?? fail("--config <cell.config.json> is required");
|
|
232
356
|
const log = (line) => console.error(line);
|
|
357
|
+
// --wallet <dir>: the operator key file of the wallet, as FOGA_OPERATOR_KEY_FILE would name it; never beside another key source
|
|
358
|
+
const wallet = walletFromFlags(flags);
|
|
359
|
+
if (wallet) {
|
|
360
|
+
if (process.env.FOGA_OPERATOR_KEY || process.env.FOGA_OPERATOR_KEY_FILE)
|
|
361
|
+
fail("--wallet and FOGA_OPERATOR_KEY[_FILE] are both set; a node takes its operator key from one place");
|
|
362
|
+
if (!wallet.files.operator)
|
|
363
|
+
fail(`wallet ${wallet.dir} has no operator key`);
|
|
364
|
+
process.env.FOGA_OPERATOR_KEY_FILE = wallet.files.operator;
|
|
365
|
+
}
|
|
366
|
+
if (cmd === "preflight") {
|
|
367
|
+
const { preflight, printPreflight } = await import("./node/preflight.js");
|
|
368
|
+
const { writeJson } = await import("./util/fs.js");
|
|
369
|
+
const node = await Node.open({ configPath, rpc: str(flags, "rpc"), keys: {}, log });
|
|
370
|
+
try {
|
|
371
|
+
const report = await preflight(node, { executors: !flags["no-executors"], network: !flags["no-network"] });
|
|
372
|
+
printPreflight(report, (line) => console.log(line));
|
|
373
|
+
if (str(flags, "out"))
|
|
374
|
+
writeJson(resolve(str(flags, "out")), report);
|
|
375
|
+
if (flags.strict && report.summary.Fail > 0)
|
|
376
|
+
process.exitCode = 5;
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
node.close();
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
233
383
|
if (cmd === "key") {
|
|
234
384
|
if (rest[0] !== "new")
|
|
235
385
|
fail("foga node key new --config <cfg> --id <id> [--level act] [--visibility caller]");
|
|
@@ -281,7 +431,17 @@ export async function nodeCommand(cmd, rest, flags) {
|
|
|
281
431
|
const { version } = await import("./util/version.js");
|
|
282
432
|
const listen = str(flags, "listen") ?? node.config.runtime.http.listen;
|
|
283
433
|
const running = await createHttpServer({ node, catalog, version, log }).listen(listen);
|
|
284
|
-
|
|
434
|
+
const { documentsDir, readBuilt } = await import("./node/documents.js");
|
|
435
|
+
const built = readBuilt(documentsDir(node));
|
|
436
|
+
const signers = node.config.access.signers;
|
|
437
|
+
log(`foga ${version}: serving cell ${node.config.name}${node.cell ? ` (${node.cell.address})` : ""} at ${running.url} — /mcp, /api/<capability>, /a2a, /.well-known/agent-card.json, /documents/*, /healthz; ${keys.operator ? "acting with the operator key" : "no operator key: reads and builds only"}; ${node.config.access.keys.length} API key(s) configured; signatures: ${signers.default} by default, ${signers.allow.length} allowed${signers.standing ? `, standing level ${signers.standing.minLevel}+ granted ${signers.standing.level}` : ""}; documents ${built ? `v${built.manifest.version}` : "not built (foga cell docs build)"}`);
|
|
438
|
+
{
|
|
439
|
+
// One line of advice at start-up (the full list is `foga cell advise`); nothing here is a condition of serving.
|
|
440
|
+
const { adviseCell } = await import("./node/advise.js");
|
|
441
|
+
const recommended = adviseCell(node).filter((a) => a.level === "recommended");
|
|
442
|
+
if (recommended.length)
|
|
443
|
+
log(`advice: ${recommended.map((a) => a.id).join(", ")} — foga cell advise --config ${configPath} explains; nothing required`);
|
|
444
|
+
}
|
|
285
445
|
const stopWatch = !flags["no-watch"] && keys.operator && node.cell ? startWatchLoop(new Watchers(node, catalog), node.config.runtime.pollSeconds * 1000, log) : () => { };
|
|
286
446
|
const stop = () => {
|
|
287
447
|
stopWatch();
|
|
@@ -363,7 +523,7 @@ export async function nodeCommand(cmd, rest, flags) {
|
|
|
363
523
|
return;
|
|
364
524
|
}
|
|
365
525
|
if (cmd !== "tick" && cmd !== "watch")
|
|
366
|
-
fail(`unknown node command ${cmd}; tick, watch, mcp, serve, run or key`);
|
|
526
|
+
fail(`unknown node command ${cmd}; tick, watch, mcp, serve, run, preflight or key`);
|
|
367
527
|
// a node environment: the operator key alone, nothing that derives another role
|
|
368
528
|
const operator = keysFor("operator", true);
|
|
369
529
|
const node = await Node.open({ configPath, rpc: str(flags, "rpc"), keys: { operator }, log });
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { cpus, homedir, loadavg } from "node:os";
|
|
4
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { buildBundle } from "./build/bundle.js";
|
|
@@ -21,6 +21,17 @@ usage:
|
|
|
21
21
|
deploy-mode config: the whole stack; attach-mode config: attach what it
|
|
22
22
|
lists and deploy the reference contracts it does not (deployer = W)
|
|
23
23
|
foga world attach --world <config.json> [--out <file>] address book only, no transactions (every contract listed)
|
|
24
|
+
foga world verify --world <world.json> --sourcify [--dry-run] [--only <id,id>] [--server <url>] [--out <dir>] [--build-info <dir>]
|
|
25
|
+
submit every contract the world deployed from the bundle to Sourcify, with the standard JSON input of its
|
|
26
|
+
compiler run (.foga-build/out/<repo>/build-info, written by npm run bundle:verify); a local chain is a dry run
|
|
27
|
+
foga wallet new [--name <n> | --dir <dir>] [--cell] the first step: a key of your own — one signer in a 0600 file, your identity, which fills
|
|
28
|
+
every role a cell needs (default ~/.foga/wallet; --name under ~/.foga/wallets/<n>; --cell: the
|
|
29
|
+
separated owner/operator/guardian set for a guarded cell account). Never overwrites; prints the addresses
|
|
30
|
+
foga wallet show [--name <n> | --dir <dir>] [--rpc <url>] the wallet's addresses (and balances with an RPC)
|
|
31
|
+
foga wallet sign (--config <cell.config.json> | --url <node url> | --cell <account> --chain-id <n>) [--name <n> | --dir <dir>]
|
|
32
|
+
[--role signer|operator|owner|guardian] [--ttl <s>] [--agent-id <id>] [--method <METHOD> --path <path> [--body '<json>' | --body-file <f>]]
|
|
33
|
+
an Authorization header (FOGA-EIP712) proving the wallet to that cell — named by its configuration, by its node's
|
|
34
|
+
URL (/healthz says which account and chain) or by hand: a session envelope, or one bound to a request
|
|
24
35
|
foga keys new [--out <file>] write a fresh 12-word phrase (default ~/.foga/genesis.mnemonic) and print the role addresses
|
|
25
36
|
foga keys show print the role addresses the environment derives (FOGA_MNEMONIC[_FILE], FOGA_KEY_*)
|
|
26
37
|
foga keys export --role <role> [--out <file>] write one role's key to a 0600 file for a node that holds that key alone
|
|
@@ -38,9 +49,10 @@ usage:
|
|
|
38
49
|
[--zk auto|require|skip] [--crash-after <opKey>] [--crash-before <opKey>]
|
|
39
50
|
run the genesis scenario (idempotent; re-run to resume)
|
|
40
51
|
foga genesis status --world <world.json> [--store <file.sqlite>] list the operations the store holds
|
|
41
|
-
foga quickstart [--dir <dir>] [--port <n>] [--listen <host:port> | --no-serve] [--name <cell>] [--no-tenders] [--exit]
|
|
42
|
-
one command to a live cell: anvil, the local world, a cell born with
|
|
43
|
-
the node serving HTTP and MCP, and the MCP client configurations
|
|
52
|
+
foga quickstart [--dir <dir>] [--wallet <dir>] [--port <n>] [--listen <host:port> | --no-serve] [--name <cell>] [--no-tenders] [--exit]
|
|
53
|
+
one command to a live cell: the wallet (made on the first run, reused after), anvil, the local world, a cell born with
|
|
54
|
+
the wallet's keys and its two skills, two tenders, the node serving HTTP and MCP, and the MCP client configurations
|
|
55
|
+
printed with every path filled in (~/.foga/quickstart)
|
|
44
56
|
foga demo [--port <n>] [--keep] [--genesis <T0..T9>] start anvil, deploy the local world, run the doctor (and the genesis)
|
|
45
57
|
foga demo --loop-d [--model stub|anthropic|external] [--model-name <name>] [--seed <n>] [--no-recovery] [--port <n>] [--keep]
|
|
46
58
|
the above, then Loop D: a new cell born, a demander minting cases a-g, the node with a model (or served for an
|
|
@@ -149,6 +161,21 @@ async function main() {
|
|
|
149
161
|
}
|
|
150
162
|
return;
|
|
151
163
|
}
|
|
164
|
+
if (group === "world" && cmd === "verify") {
|
|
165
|
+
if (!flags.sourcify)
|
|
166
|
+
fail("foga world verify --sourcify --world <world.json> (Sourcify is the one verifier wired so far)");
|
|
167
|
+
const worldPath = str(flags, "world") ?? fail("--world <world.json> is required (a deployed world)");
|
|
168
|
+
const { verifyWorld } = await import("./world/sourcify.js");
|
|
169
|
+
const { report, outDir } = await verifyWorld({ worldPath: resolve(worldPath), server: str(flags, "server"), dryRun: flags["dry-run"] ? true : undefined, only: str(flags, "only")?.split(","), outDir: str(flags, "out") ? resolve(str(flags, "out")) : undefined, buildInfoRoot: str(flags, "build-info") ? resolve(str(flags, "build-info")) : undefined, log: (line) => console.error(line) });
|
|
170
|
+
for (const r of report.results)
|
|
171
|
+
console.log(` ${r.outcome.padEnd(12)} ${r.id.padEnd(32)} ${r.address} ${r.contractIdentifier} (${r.compilerVersion})${r.outcome === "dry-run" ? "" : ` ${r.detail}`}`);
|
|
172
|
+
for (const m of report.missing)
|
|
173
|
+
console.log(` missing ${m.id.padEnd(32)} ${m.reason}`);
|
|
174
|
+
console.log(`sourcify: ${Object.entries(report.summary).map(([k, v]) => `${v} ${k}`).join(", ") || "nothing to do"}${report.missing.length ? `, ${report.missing.length} missing` : ""}; report ${relative(root, join(outDir, "report.json"))}`);
|
|
175
|
+
if (report.missing.length || report.results.some((r) => r.outcome === "failed" || r.outcome === "error"))
|
|
176
|
+
process.exitCode = 3;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
152
179
|
if (group === "world" && (cmd === "deploy" || cmd === "attach")) {
|
|
153
180
|
const worldConfigPath = str(flags, "world");
|
|
154
181
|
if (!worldConfigPath)
|
|
@@ -217,6 +244,118 @@ async function main() {
|
|
|
217
244
|
process.stdout.write(RELAY_SCRIPT);
|
|
218
245
|
return;
|
|
219
246
|
}
|
|
247
|
+
if (group === "wallet") {
|
|
248
|
+
const { CELL_ROLES, createWallet, loadWallet, walletDir, walletFileProblems } = await import("./node/wallet.js");
|
|
249
|
+
const dir = resolve(str(flags, "dir") ?? walletDir(str(flags, "name")));
|
|
250
|
+
const printWallet = async (w, made) => {
|
|
251
|
+
console.log(`${made ? "wallet made at" : "wallet"} ${w.dir}${w.name !== "default" ? ` (${w.name})` : ""}, ${w.createdAt}`);
|
|
252
|
+
const rpc = str(flags, "rpc") ?? process.env.FOGA_RPC_URL;
|
|
253
|
+
const provider = rpc ? new (await import("ethers")).JsonRpcProvider(rpc) : null;
|
|
254
|
+
for (const role of w.roles) {
|
|
255
|
+
let balance = "";
|
|
256
|
+
if (provider) {
|
|
257
|
+
try {
|
|
258
|
+
balance = ` ${(await import("ethers")).formatEther(await provider.getBalance(w.addresses[role]))} ETH`;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
balance = " (balance: RPC did not answer)";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
console.log(` ${role.padEnd(9)} ${w.addresses[role]}${balance} ${w.files[role]}`);
|
|
265
|
+
}
|
|
266
|
+
provider?.destroy();
|
|
267
|
+
for (const p of walletFileProblems(w))
|
|
268
|
+
log.warn(p);
|
|
269
|
+
const single = w.roles.length === 1 && w.roles[0] === "signer";
|
|
270
|
+
if (single)
|
|
271
|
+
console.log(`one key, your identity; it fills every role a cell needs. For a cell's birth, fund this address (capital and gas), then: foga cell new --wallet ${dir} … and foga cell init --config <cfg> --wallet ${dir}`);
|
|
272
|
+
else if (w.roles.includes("owner"))
|
|
273
|
+
console.log(`a separated cell wallet; fund the owner for a cell's birth (capital and gas) and the operator for its gas, then: foga cell new --wallet ${dir} … and foga cell init --config <cfg> --wallet ${dir}`);
|
|
274
|
+
};
|
|
275
|
+
if (cmd === "new") {
|
|
276
|
+
const existing = loadWallet(dir);
|
|
277
|
+
if (existing) {
|
|
278
|
+
await printWallet(existing, false);
|
|
279
|
+
console.log("(already there; a wallet is never overwritten — foga wallet new --name <n> makes another)");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// the base is one key (--single, now the default); --cell makes the separated owner/operator/guardian set (the guarded FogaCellAccount profile)
|
|
283
|
+
const roles = flags.cell ? [...CELL_ROLES] : undefined;
|
|
284
|
+
try {
|
|
285
|
+
await printWallet(createWallet(dir, { name: str(flags, "name") ?? "default", roles }), true);
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
fail(e.message);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (cmd === "show") {
|
|
293
|
+
const w = loadWallet(dir);
|
|
294
|
+
if (!w)
|
|
295
|
+
fail(`no wallet at ${dir}; foga wallet new makes one`);
|
|
296
|
+
await printWallet(w, false);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (cmd === "sign") {
|
|
300
|
+
const w = loadWallet(dir);
|
|
301
|
+
if (!w)
|
|
302
|
+
fail(`no wallet at ${dir}; foga wallet new makes one`);
|
|
303
|
+
const { walletKey } = await import("./node/wallet.js");
|
|
304
|
+
const { loadCellConfig, configPath } = await import("./node/config.js");
|
|
305
|
+
const { signAuthorization } = await import("./http/signed.js");
|
|
306
|
+
const { Wallet } = await import("ethers");
|
|
307
|
+
// the cell the signature is for: its own configuration (the operator's shell), its node's URL (anyone's: /healthz names the account and the chain), or both values by hand
|
|
308
|
+
let cellAddress;
|
|
309
|
+
let chainId;
|
|
310
|
+
let cellName;
|
|
311
|
+
if (str(flags, "config")) {
|
|
312
|
+
const loaded = loadCellConfig(str(flags, "config"));
|
|
313
|
+
cellAddress = loaded.config.identity.account ?? fail("the config names no account yet (the cell is not born)");
|
|
314
|
+
chainId = readJson(configPath(loaded, loaded.config.world)).chainId;
|
|
315
|
+
cellName = loaded.config.name;
|
|
316
|
+
}
|
|
317
|
+
else if (str(flags, "url")) {
|
|
318
|
+
const base = str(flags, "url").replace(/\/+$/, "");
|
|
319
|
+
let health;
|
|
320
|
+
try {
|
|
321
|
+
health = (await (await fetch(`${base}/healthz`, { signal: AbortSignal.timeout(10_000) })).json());
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
fail(`${base}/healthz did not answer: ${e.message}`);
|
|
325
|
+
}
|
|
326
|
+
if (!health.account || !health.chainId)
|
|
327
|
+
fail(`${base}/healthz names no account or chain (${JSON.stringify(health).slice(0, 200)}); the cell is not born or this is not a FOGA node`);
|
|
328
|
+
cellAddress = health.account;
|
|
329
|
+
chainId = health.chainId;
|
|
330
|
+
cellName = health.cell ?? base;
|
|
331
|
+
}
|
|
332
|
+
else if (str(flags, "cell") && str(flags, "chain-id")) {
|
|
333
|
+
cellAddress = str(flags, "cell");
|
|
334
|
+
chainId = Number(str(flags, "chain-id"));
|
|
335
|
+
cellName = cellAddress;
|
|
336
|
+
}
|
|
337
|
+
else
|
|
338
|
+
fail("the signature is made for one cell: --config <cell.config.json>, or --url <its node's URL>, or --cell <account> --chain-id <n>");
|
|
339
|
+
const role = (str(flags, "role") ?? (w.roles.includes("signer") ? "signer" : "operator"));
|
|
340
|
+
let key;
|
|
341
|
+
try {
|
|
342
|
+
key = walletKey(w, role);
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
fail(e.message);
|
|
346
|
+
}
|
|
347
|
+
const method = str(flags, "method");
|
|
348
|
+
const path = str(flags, "path");
|
|
349
|
+
if ((method && !path) || (!method && path))
|
|
350
|
+
fail("--method and --path go together");
|
|
351
|
+
const bodyText = str(flags, "body") ?? (str(flags, "body-file") ? readFileSync(resolve(str(flags, "body-file")), "utf8") : undefined);
|
|
352
|
+
const header = await signAuthorization(new Wallet(key), { chainId, cell: cellAddress, ttlSeconds: str(flags, "ttl") ? Number(str(flags, "ttl")) : undefined, agentId: str(flags, "agent-id"), request: method ? { method, path: path, body: bodyText ?? null } : undefined });
|
|
353
|
+
console.error(`${role} ${new Wallet(key).address} signed ${method ? `a ${method} ${path} envelope` : "a session envelope"} for cell ${cellName} (${cellAddress}, chain ${chainId}); the header value follows (one line):`);
|
|
354
|
+
console.log(header);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
fail("foga wallet new | show | sign");
|
|
358
|
+
}
|
|
220
359
|
if (group === "keys" && cmd === "export") {
|
|
221
360
|
const { ROLE_ORDER, envKeys, roleAddresses } = await import("./genesis/roles.js");
|
|
222
361
|
const role = str(flags, "role");
|
|
@@ -362,7 +501,7 @@ async function main() {
|
|
|
362
501
|
const { runQuickstart } = await import("./demo/quickstart.js");
|
|
363
502
|
let result;
|
|
364
503
|
try {
|
|
365
|
-
result = await runQuickstart({ dir: str(flags, "dir"), port: str(flags, "port") ? Number(str(flags, "port")) : undefined, listen: flags["no-serve"] ? null : str(flags, "listen"), name: str(flags, "name"), tenders: !flags["no-tenders"], exit: Boolean(flags.exit) });
|
|
504
|
+
result = await runQuickstart({ dir: str(flags, "dir"), wallet: str(flags, "wallet"), port: str(flags, "port") ? Number(str(flags, "port")) : undefined, listen: flags["no-serve"] ? null : str(flags, "listen"), name: str(flags, "name"), tenders: !flags["no-tenders"], exit: Boolean(flags.exit) });
|
|
366
505
|
}
|
|
367
506
|
catch (e) {
|
|
368
507
|
fail(e.message);
|
|
@@ -371,13 +510,19 @@ async function main() {
|
|
|
371
510
|
await result.stop();
|
|
372
511
|
return;
|
|
373
512
|
}
|
|
513
|
+
let asked = false;
|
|
374
514
|
const stopping = () => {
|
|
515
|
+
if (asked)
|
|
516
|
+
return;
|
|
517
|
+
asked = true;
|
|
375
518
|
console.error("quickstart: stopping the node and the chain");
|
|
376
519
|
result.stop().finally(() => process.exit(0));
|
|
377
520
|
};
|
|
378
521
|
process.on("SIGINT", stopping);
|
|
379
522
|
process.on("SIGTERM", stopping);
|
|
380
523
|
const why = await result.ended;
|
|
524
|
+
if (why === "stopped")
|
|
525
|
+
return; // a stop that was asked for: both children are gone
|
|
381
526
|
fail(`${why}; the quickstart is over`);
|
|
382
527
|
}
|
|
383
528
|
if (group === "demo") {
|
package/dist/core/world.js
CHANGED
|
@@ -39,6 +39,23 @@ export class World {
|
|
|
39
39
|
get chainId() {
|
|
40
40
|
return this.file.chainId;
|
|
41
41
|
}
|
|
42
|
+
get network() {
|
|
43
|
+
return this.file.network;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The covenants this world has contracts for, as ERC ids ("ERC-8414"), read off the contract keys ("8414/TaskToken";
|
|
47
|
+
* "identity8004/…" is the ERC-8004 registry). Data, not a constant: a world that attaches an eleventh covenant under
|
|
48
|
+
* a new key advertises it here with no code change (docs/extending.md), and a world that lacks one does not claim it.
|
|
49
|
+
*/
|
|
50
|
+
covenants() {
|
|
51
|
+
const ids = new Set();
|
|
52
|
+
for (const key of [...Object.keys(this.file.contracts), ...this.dynamic.keys()]) {
|
|
53
|
+
const m = /^(?:identity)?(\d{4,5})$/.exec(key.split("/")[0]);
|
|
54
|
+
if (m)
|
|
55
|
+
ids.add(`ERC-${m[1]}`);
|
|
56
|
+
}
|
|
57
|
+
return [...ids].sort((a, b) => Number(a.slice(4)) - Number(b.slice(4)));
|
|
58
|
+
}
|
|
42
59
|
has(id) {
|
|
43
60
|
return Boolean(this.file.contracts[id] || this.dynamic.get(id));
|
|
44
61
|
}
|
package/dist/demo/birth.js
CHANGED
|
@@ -12,12 +12,18 @@ export async function birthCell(o) {
|
|
|
12
12
|
const log = o.log ?? (() => { });
|
|
13
13
|
const root = repoRoot();
|
|
14
14
|
const W = k.address("W");
|
|
15
|
-
const owner = Wallet.createRandom();
|
|
16
|
-
const operator = Wallet.createRandom();
|
|
17
|
-
const guardian = Wallet.createRandom();
|
|
18
|
-
log(`cell ${o.name}: owner ${owner.address}, operator ${operator.address}, guardian ${guardian.address}`);
|
|
15
|
+
const owner = o.keys ? new Wallet(o.keys.owner) : Wallet.createRandom();
|
|
16
|
+
const operator = o.keys ? new Wallet(o.keys.operator) : Wallet.createRandom();
|
|
17
|
+
const guardian = o.keys ? new Wallet(o.keys.guardian) : Wallet.createRandom();
|
|
18
|
+
log(`cell ${o.name}: owner ${owner.address}, operator ${operator.address}, guardian ${guardian.address}${o.keys ? " (the wallet's keys)" : " (fresh keys)"}`);
|
|
19
19
|
// --- funding by W, the standing scheme registered by W
|
|
20
|
-
|
|
20
|
+
// The reserve is priced at the chain's gas price, floored at 1 gwei. A public testnet can quote a price near zero
|
|
21
|
+
// (Sepolia has sat at a few wei), which would fund the roles with the flat amounts alone: the operator below the
|
|
22
|
+
// cell policy's gas floor (a quarter of the daily gas budget — the budget gate then refuses every tender) and the
|
|
23
|
+
// guardian with next to nothing for its suspend and resume. The floor keeps the funding meaningful on any chain;
|
|
24
|
+
// on a local chain, whose price is above it, nothing changes.
|
|
25
|
+
const quoted = (await k.world.provider.getFeeData()).gasPrice ?? 1000000000n;
|
|
26
|
+
const price = quoted > 1000000000n ? quoted : 1000000000n;
|
|
21
27
|
const gas = (units) => units * price * 2n;
|
|
22
28
|
await k.transfer("fund.owner", "W", owner.address, parseEther("0.05") + gas(12000000n));
|
|
23
29
|
await k.transfer("fund.operator", "W", operator.address, gas(20000000n) + parseEther("0.003"));
|
package/dist/demo/loop-d.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -106,6 +106,14 @@ export async function runLoopD(opts) {
|
|
|
106
106
|
const root = repoRoot();
|
|
107
107
|
const dir = resolve(opts.dir);
|
|
108
108
|
mkdirSync(dir, { recursive: true });
|
|
109
|
+
// One working directory holds one run: the cell's configuration and store, the orchestrator's and the demander's
|
|
110
|
+
// stores, the cases, the brief, the transcripts. A second run in the same directory would find the first run's
|
|
111
|
+
// keyed operations and replay them (a resumable kernel resumes) against a cell with fresh keys — the birth then
|
|
112
|
+
// fails on the first replayed answer. So a previous run's files are cleared first; the evidence (`--out`) lives
|
|
113
|
+
// elsewhere and is kept.
|
|
114
|
+
const cleared = cleanLoopDDir(dir);
|
|
115
|
+
if (cleared.length)
|
|
116
|
+
log(`loop D: cleared the previous run's files in ${dir} (${cleared.join(", ")})`);
|
|
109
117
|
const worldPath = resolve(opts.worldPath);
|
|
110
118
|
const seed = opts.seed ?? 7;
|
|
111
119
|
const cases = opts.cases ?? ALL_CASES;
|
|
@@ -563,3 +571,33 @@ export function defaultLoopDDir() {
|
|
|
563
571
|
mkdirSync(dir, { recursive: true });
|
|
564
572
|
return dir;
|
|
565
573
|
}
|
|
574
|
+
/** The files a Loop D run leaves in its working directory — its own and nothing else. */
|
|
575
|
+
const LOOP_D_FILES = ["D.json", "D.ops.sqlite", "D.ops.sqlite-wal", "D.ops.sqlite-shm", "orchestrator.sqlite", "orchestrator.sqlite-wal", "orchestrator.sqlite-shm", "demander.sqlite", "demander.sqlite-wal", "demander.sqlite-shm", "agent-brief.md"];
|
|
576
|
+
const LOOP_D_DIRS = ["transcripts", "documents"];
|
|
577
|
+
/** Remove what a previous Loop D run left in `dir` (only the run's own files); returns the names removed. */
|
|
578
|
+
export function cleanLoopDDir(dir) {
|
|
579
|
+
if (!existsSync(dir))
|
|
580
|
+
return [];
|
|
581
|
+
const removed = [];
|
|
582
|
+
for (const name of LOOP_D_FILES) {
|
|
583
|
+
const p = join(dir, name);
|
|
584
|
+
if (existsSync(p)) {
|
|
585
|
+
rmSync(p, { force: true });
|
|
586
|
+
removed.push(name);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
for (const name of readdirSync(dir)) {
|
|
590
|
+
if (/^cases-.*\.json$/.test(name)) {
|
|
591
|
+
rmSync(join(dir, name), { force: true });
|
|
592
|
+
removed.push(name);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
for (const name of LOOP_D_DIRS) {
|
|
596
|
+
const p = join(dir, name);
|
|
597
|
+
if (existsSync(p)) {
|
|
598
|
+
rmSync(p, { recursive: true, force: true });
|
|
599
|
+
removed.push(`${name}/`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return removed;
|
|
603
|
+
}
|