@xnetjs/cli 0.0.2 → 0.0.4
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.js +1028 -160
- package/package.json +10 -3
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,793 @@ import {
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
import { program } from "commander";
|
|
10
10
|
|
|
11
|
+
// src/commands/agent.ts
|
|
12
|
+
import { readFile, rename, mkdir } from "fs/promises";
|
|
13
|
+
import { dirname, join, resolve } from "path";
|
|
14
|
+
import {
|
|
15
|
+
ScriptSandbox,
|
|
16
|
+
XNET_AGENT_SKILL_MD,
|
|
17
|
+
createAgentScriptContext,
|
|
18
|
+
createAiSurfaceService,
|
|
19
|
+
createAiWorkspaceExporter,
|
|
20
|
+
createAiWorkspaceWatcher,
|
|
21
|
+
flattenRowForTsv,
|
|
22
|
+
toTsv
|
|
23
|
+
} from "@xnetjs/plugins/node";
|
|
24
|
+
|
|
25
|
+
// src/utils/agent-remote.ts
|
|
26
|
+
var RemoteApiError = class extends Error {
|
|
27
|
+
constructor(message, status) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.name = "RemoteApiError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
async function createRemoteAgentBackend(options = {}) {
|
|
34
|
+
const baseUrl = (options.apiUrl ?? process.env.XNET_API_URL ?? "http://127.0.0.1:31415").replace(
|
|
35
|
+
/\/$/,
|
|
36
|
+
""
|
|
37
|
+
);
|
|
38
|
+
const token = options.token ?? process.env.XNET_API_TOKEN;
|
|
39
|
+
const request = async (method, path, body) => {
|
|
40
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
41
|
+
method,
|
|
42
|
+
headers: {
|
|
43
|
+
"content-type": "application/json",
|
|
44
|
+
...token ? { authorization: `Bearer ${token}` } : {}
|
|
45
|
+
},
|
|
46
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
47
|
+
});
|
|
48
|
+
const text2 = await response.text();
|
|
49
|
+
const parsed = text2 ? JSON.parse(text2) : null;
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const message = isRecord(parsed) && typeof parsed.error === "string" ? parsed.error : `${method} ${path} failed with ${response.status}`;
|
|
52
|
+
throw new RemoteApiError(message, response.status);
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
55
|
+
};
|
|
56
|
+
const store = {
|
|
57
|
+
get: async (id) => {
|
|
58
|
+
try {
|
|
59
|
+
return await request("GET", `/api/v1/nodes/${encodeURIComponent(id)}`);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err instanceof RemoteApiError && err.status === 404) return null;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
list: async (listOptions) => {
|
|
66
|
+
const params = new URLSearchParams();
|
|
67
|
+
if (listOptions?.schemaId) params.set("schema", listOptions.schemaId);
|
|
68
|
+
if (listOptions?.limit !== void 0) params.set("limit", String(listOptions.limit));
|
|
69
|
+
if (listOptions?.offset !== void 0) params.set("offset", String(listOptions.offset));
|
|
70
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
71
|
+
const result = await request("GET", `/api/v1/nodes${query}`);
|
|
72
|
+
return isRecord(result) && Array.isArray(result.nodes) ? result.nodes : [];
|
|
73
|
+
},
|
|
74
|
+
create: async ({ schemaId, properties }) => await request("POST", "/api/v1/nodes", { schema: schemaId, properties }),
|
|
75
|
+
update: async (id, { properties }) => await request("PATCH", `/api/v1/nodes/${encodeURIComponent(id)}`, properties),
|
|
76
|
+
delete: async (id) => {
|
|
77
|
+
await request("DELETE", `/api/v1/nodes/${encodeURIComponent(id)}`);
|
|
78
|
+
},
|
|
79
|
+
subscribe: () => () => {
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const schemaCache = /* @__PURE__ */ new Map();
|
|
83
|
+
const listed = await request("GET", "/api/v1/schemas");
|
|
84
|
+
if (isRecord(listed) && Array.isArray(listed.schemas)) {
|
|
85
|
+
for (const schema of listed.schemas) {
|
|
86
|
+
if (isRecord(schema) && typeof schema.iri === "string") {
|
|
87
|
+
schemaCache.set(schema.iri, schema);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const schemas = {
|
|
92
|
+
getAllIRIs: () => Array.from(schemaCache.keys()),
|
|
93
|
+
get: async (iri) => {
|
|
94
|
+
const cached = schemaCache.get(iri);
|
|
95
|
+
if (cached) return cached;
|
|
96
|
+
try {
|
|
97
|
+
const fetched = await request(
|
|
98
|
+
"GET",
|
|
99
|
+
`/api/v1/schemas/${encodeURIComponent(iri)}`
|
|
100
|
+
);
|
|
101
|
+
schemaCache.set(iri, fetched);
|
|
102
|
+
return fetched;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err instanceof RemoteApiError && err.status === 404) return null;
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
return { store, schemas };
|
|
110
|
+
}
|
|
111
|
+
function isRecord(value) {
|
|
112
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/commands/agent.ts
|
|
116
|
+
function createAgentServices(backend) {
|
|
117
|
+
const aiSurface = createAiSurfaceService({ store: backend.store, schemas: backend.schemas });
|
|
118
|
+
return {
|
|
119
|
+
store: backend.store,
|
|
120
|
+
schemas: backend.schemas,
|
|
121
|
+
aiSurface,
|
|
122
|
+
exporter: createAiWorkspaceExporter({ ...backend, aiSurface }),
|
|
123
|
+
watcher: createAiWorkspaceWatcher({ ...backend, aiSurface })
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
var defaultServicesFactory = async ({ apiUrl }) => createAgentServices(await createRemoteAgentBackend({ apiUrl }));
|
|
127
|
+
async function runCheckout(services, options) {
|
|
128
|
+
const result = await services.exporter.checkout({
|
|
129
|
+
rootDir: resolve(options.dir),
|
|
130
|
+
...options.name ? { workspaceName: options.name } : {},
|
|
131
|
+
scope: {
|
|
132
|
+
...options.query ? { query: options.query } : {},
|
|
133
|
+
...options.schema?.length ? { schemaIds: options.schema } : {},
|
|
134
|
+
...options.node?.length ? { nodeIds: options.node } : {},
|
|
135
|
+
...options.kind?.length ? { kinds: options.kind } : {},
|
|
136
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const lines = result.manifestEntries.map((entry) => `${entry.path} ${entry.id}`);
|
|
140
|
+
return [
|
|
141
|
+
`checked out ${result.manifestEntries.length} file(s) into ${options.dir}`,
|
|
142
|
+
...lines
|
|
143
|
+
].join("\n");
|
|
144
|
+
}
|
|
145
|
+
async function runStatus(services, options) {
|
|
146
|
+
const scan = await services.watcher.scanChangedFiles({
|
|
147
|
+
rootDir: resolve(options.dir),
|
|
148
|
+
writePendingPlans: false,
|
|
149
|
+
writeConflicts: false,
|
|
150
|
+
writeReviewIndex: false
|
|
151
|
+
});
|
|
152
|
+
if (options.format === "json") {
|
|
153
|
+
return JSON.stringify({
|
|
154
|
+
pendingPlans: scan.pendingPlans.map((pending) => ({
|
|
155
|
+
path: pending.path,
|
|
156
|
+
planId: pending.plan.id,
|
|
157
|
+
intent: pending.plan.intent
|
|
158
|
+
})),
|
|
159
|
+
conflicts: scan.conflicts
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const lines = [
|
|
163
|
+
...scan.pendingPlans.map(
|
|
164
|
+
(pending) => `pending ${pending.path} ${pending.plan.id} ${pending.plan.intent}`
|
|
165
|
+
),
|
|
166
|
+
...scan.conflicts.map(
|
|
167
|
+
(conflict) => `conflict ${conflict.path} ${conflict.kind} ${conflict.message}`
|
|
168
|
+
)
|
|
169
|
+
];
|
|
170
|
+
return lines.length > 0 ? lines.join("\n") : "clean";
|
|
171
|
+
}
|
|
172
|
+
async function runCommit(services, options) {
|
|
173
|
+
const rootDir = resolve(options.dir);
|
|
174
|
+
const scan = await services.watcher.scanChangedFiles({
|
|
175
|
+
rootDir,
|
|
176
|
+
actor: options.actor ?? "xnet-cli"
|
|
177
|
+
});
|
|
178
|
+
const lines = [];
|
|
179
|
+
for (const conflict of scan.conflicts) {
|
|
180
|
+
lines.push(`conflict ${conflict.path} ${conflict.kind} see ${conflict.notePath ?? ""}`);
|
|
181
|
+
}
|
|
182
|
+
if (!options.apply) {
|
|
183
|
+
for (const pending of scan.pendingPlans) {
|
|
184
|
+
lines.push(`planned ${pending.path} ${pending.plan.id}`);
|
|
185
|
+
}
|
|
186
|
+
if (scan.pendingPlans.length === 0 && scan.conflicts.length === 0) return "clean";
|
|
187
|
+
lines.push(`${scan.pendingPlans.length} plan(s) pending; re-run with --apply to apply`);
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
const appliedNodeIds = [];
|
|
191
|
+
for (const pending of scan.pendingPlans) {
|
|
192
|
+
const outcome = await applyPendingPlan(services, pending.plan);
|
|
193
|
+
lines.push(`${outcome.status} ${pending.path} ${pending.plan.id} ${outcome.detail}`);
|
|
194
|
+
if (outcome.status === "applied") {
|
|
195
|
+
appliedNodeIds.push(...pending.plan.changes.map((change) => change.targetId));
|
|
196
|
+
await archivePendingPlan(rootDir, pending.planPath);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (appliedNodeIds.length > 0) {
|
|
200
|
+
await services.exporter.checkout({ rootDir, scope: { nodeIds: appliedNodeIds } });
|
|
201
|
+
}
|
|
202
|
+
return lines.length > 0 ? lines.join("\n") : "clean";
|
|
203
|
+
}
|
|
204
|
+
var APPLY_TOOL_BY_TARGET_KIND = {
|
|
205
|
+
page: "xnet_apply_page_markdown",
|
|
206
|
+
database: "xnet_apply_database_mutation",
|
|
207
|
+
databaseRows: "xnet_apply_database_mutation"
|
|
208
|
+
};
|
|
209
|
+
async function applyPendingPlan(services, plan) {
|
|
210
|
+
const targetKind = plan.changes[0]?.targetKind ?? "unknown";
|
|
211
|
+
const tool = APPLY_TOOL_BY_TARGET_KIND[targetKind];
|
|
212
|
+
if (!tool) {
|
|
213
|
+
return { status: "skipped", detail: `${targetKind} plans need review in the xNet app` };
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const result = await services.aiSurface.callTool(tool, { plan, confirmApply: true });
|
|
217
|
+
return result.applied ? { status: "applied", detail: `${targetKind} plan applied` } : { status: "failed", detail: result.validation?.errors?.join("; ") ?? "not applied" };
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return { status: "failed", detail: err instanceof Error ? err.message : String(err) };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function archivePendingPlan(rootDir, planPath) {
|
|
223
|
+
const appliedPath = planPath.replace(".xnet/pending/", ".xnet/applied/");
|
|
224
|
+
try {
|
|
225
|
+
await mkdir(dirname(join(rootDir, appliedPath)), { recursive: true });
|
|
226
|
+
await rename(join(rootDir, planPath), join(rootDir, appliedPath));
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function runSearch(services, options) {
|
|
231
|
+
const result = await services.aiSurface.search({
|
|
232
|
+
query: options.text,
|
|
233
|
+
schemaId: options.schema,
|
|
234
|
+
limit: options.limit
|
|
235
|
+
});
|
|
236
|
+
const results = Array.isArray(result.results) ? result.results : [];
|
|
237
|
+
if (options.format === "json") return JSON.stringify(result);
|
|
238
|
+
if (options.format === "jsonl") return results.map((row) => JSON.stringify(row)).join("\n");
|
|
239
|
+
const compact = results.map((row) => ({
|
|
240
|
+
id: row.id,
|
|
241
|
+
schemaId: row.schemaId,
|
|
242
|
+
title: row.title,
|
|
243
|
+
snippet: row.snippet
|
|
244
|
+
}));
|
|
245
|
+
if (results.length === 0) return "no results";
|
|
246
|
+
if (options.format === "md") return toMarkdownTable(compact);
|
|
247
|
+
return toTsv(compact).trimEnd();
|
|
248
|
+
}
|
|
249
|
+
async function runQuery(services, options) {
|
|
250
|
+
const where = parseAssignments(options.where ?? []);
|
|
251
|
+
const result = await services.aiSurface.callTool("xnet_database_query", {
|
|
252
|
+
databaseId: options.databaseId,
|
|
253
|
+
...Object.keys(where).length > 0 ? { where } : {},
|
|
254
|
+
...options.limit !== void 0 ? { limit: options.limit } : {},
|
|
255
|
+
...options.offset !== void 0 ? { offset: options.offset } : {}
|
|
256
|
+
});
|
|
257
|
+
const rows = Array.isArray(result.rows) ? result.rows : [];
|
|
258
|
+
if (options.format === "json") {
|
|
259
|
+
return JSON.stringify(
|
|
260
|
+
options.detailed ? result : {
|
|
261
|
+
databaseId: result.databaseId,
|
|
262
|
+
count: result.count,
|
|
263
|
+
totalCount: result.totalCount,
|
|
264
|
+
rows
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (options.format === "jsonl") return rows.map((row) => JSON.stringify(row)).join("\n");
|
|
269
|
+
if (rows.length === 0) return "no rows";
|
|
270
|
+
if (options.format === "md") {
|
|
271
|
+
return toMarkdownTable(rows.map((row) => flattenRowForTsv(row)));
|
|
272
|
+
}
|
|
273
|
+
return toTsv(rows).trimEnd();
|
|
274
|
+
}
|
|
275
|
+
function toMarkdownTable(rows) {
|
|
276
|
+
const columns = [];
|
|
277
|
+
for (const row of rows) {
|
|
278
|
+
for (const key of Object.keys(row)) {
|
|
279
|
+
if (!columns.includes(key)) columns.push(key);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const cell = (value) => {
|
|
283
|
+
if (value === null || value === void 0) return "";
|
|
284
|
+
const text2 = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
285
|
+
return text2.replace(/[|\n\r\t]+/g, " ");
|
|
286
|
+
};
|
|
287
|
+
return [
|
|
288
|
+
`| ${columns.join(" | ")} |`,
|
|
289
|
+
`| ${columns.map(() => "---").join(" | ")} |`,
|
|
290
|
+
...rows.map((row) => `| ${columns.map((column) => cell(row[column])).join(" | ")} |`)
|
|
291
|
+
].join("\n");
|
|
292
|
+
}
|
|
293
|
+
async function runDbGet(services, options) {
|
|
294
|
+
const node = await services.store.get(options.nodeId);
|
|
295
|
+
if (!node) throw new Error(`Node not found: ${options.nodeId}`);
|
|
296
|
+
if (options.detailed) return JSON.stringify(node);
|
|
297
|
+
return JSON.stringify({
|
|
298
|
+
id: node.id,
|
|
299
|
+
schemaId: node.schemaId,
|
|
300
|
+
properties: node.properties,
|
|
301
|
+
revision: `updatedAt:${node.updatedAt}`
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async function runDbSet(services, options) {
|
|
305
|
+
const properties = parseAssignments(options.assignments);
|
|
306
|
+
if (Object.keys(properties).length === 0) {
|
|
307
|
+
throw new Error("db set requires at least one field=value assignment");
|
|
308
|
+
}
|
|
309
|
+
const plan = await services.aiSurface.callTool("xnet_plan_database_mutation", {
|
|
310
|
+
databaseId: options.databaseId,
|
|
311
|
+
actor: options.actor ?? "xnet-cli",
|
|
312
|
+
intent: `Update row ${options.rowId} via xnet db set`,
|
|
313
|
+
operations: [{ op: "updateRow", args: { rowId: options.rowId, properties } }]
|
|
314
|
+
});
|
|
315
|
+
if (!plan.validation.valid) {
|
|
316
|
+
throw new Error(`plan invalid: ${plan.validation.errors.join("; ")}`);
|
|
317
|
+
}
|
|
318
|
+
if (options.planOnly) return JSON.stringify(plan);
|
|
319
|
+
const result = await services.aiSurface.callTool("xnet_apply_database_mutation", {
|
|
320
|
+
plan,
|
|
321
|
+
confirmApply: true
|
|
322
|
+
});
|
|
323
|
+
if (!result.applied) {
|
|
324
|
+
throw new Error(`apply failed: ${result.validation?.errors?.join("; ") ?? "unknown error"}`);
|
|
325
|
+
}
|
|
326
|
+
return `applied ${options.rowId} ${plan.id}`;
|
|
327
|
+
}
|
|
328
|
+
async function runScript(services, options) {
|
|
329
|
+
const code = await readFile(resolve(options.file), "utf8");
|
|
330
|
+
const nodes = await services.store.list({
|
|
331
|
+
...options.schema ? { schemaId: options.schema } : {},
|
|
332
|
+
limit: options.limit ?? 200,
|
|
333
|
+
offset: 0
|
|
334
|
+
});
|
|
335
|
+
const flatNodes = nodes.filter((node) => !node.deleted).map(toFlatNode);
|
|
336
|
+
const currentNode = options.node ? flatNodes.find((node) => node.id === options.node) : void 0;
|
|
337
|
+
const session = createAgentScriptContext({
|
|
338
|
+
nodes: flatNodes,
|
|
339
|
+
...currentNode ? { node: currentNode } : {}
|
|
340
|
+
});
|
|
341
|
+
const sandbox = new ScriptSandbox({ timeoutMs: options.timeoutMs ?? 5e3 });
|
|
342
|
+
const result = await sandbox.execute(code, session.context);
|
|
343
|
+
const plan = session.toMutationPlan({ actor: options.actor ?? "xnet-cli-script" });
|
|
344
|
+
const output = { result };
|
|
345
|
+
if (plan) {
|
|
346
|
+
output.plan = { id: plan.id, changes: plan.changes.length, valid: plan.validation.valid };
|
|
347
|
+
if (options.dir) {
|
|
348
|
+
const planPath = `.xnet/pending/${plan.id}.plan.json`;
|
|
349
|
+
const fullPath = join(resolve(options.dir), planPath);
|
|
350
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
351
|
+
await (await import("fs/promises")).writeFile(fullPath, `${JSON.stringify(plan, null, 2)}
|
|
352
|
+
`, "utf8");
|
|
353
|
+
output.planPath = planPath;
|
|
354
|
+
} else {
|
|
355
|
+
output.planDetail = plan;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return JSON.stringify(output);
|
|
359
|
+
}
|
|
360
|
+
function toFlatNode(node) {
|
|
361
|
+
return {
|
|
362
|
+
id: node.id,
|
|
363
|
+
schemaIRI: node.schemaId,
|
|
364
|
+
...node.properties,
|
|
365
|
+
updatedAt: node.updatedAt
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function startDaemon(services, options) {
|
|
369
|
+
const rootDir = resolve(options.dir);
|
|
370
|
+
const report = options.onScan ?? ((summary) => console.log(summary));
|
|
371
|
+
const handleScan = async (scan) => {
|
|
372
|
+
if (scan.pendingPlans.length === 0 && scan.conflicts.length === 0) return;
|
|
373
|
+
if (options.apply && scan.pendingPlans.length > 0) {
|
|
374
|
+
const summary = await runCommit(services, {
|
|
375
|
+
dir: rootDir,
|
|
376
|
+
apply: true,
|
|
377
|
+
actor: options.actor
|
|
378
|
+
});
|
|
379
|
+
report(summary);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
report(
|
|
383
|
+
[
|
|
384
|
+
...scan.pendingPlans.map((pending) => `planned ${pending.path} ${pending.plan.id}`),
|
|
385
|
+
...scan.conflicts.map((conflict) => `conflict ${conflict.path} ${conflict.kind}`)
|
|
386
|
+
].join("\n")
|
|
387
|
+
);
|
|
388
|
+
};
|
|
389
|
+
const handle = services.watcher.watchWorkspace(
|
|
390
|
+
{
|
|
391
|
+
rootDir,
|
|
392
|
+
actor: options.actor ?? "xnet-daemon",
|
|
393
|
+
usePolling: options.poll,
|
|
394
|
+
...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {}
|
|
395
|
+
},
|
|
396
|
+
(scan) => void handleScan(scan)
|
|
397
|
+
);
|
|
398
|
+
return handle;
|
|
399
|
+
}
|
|
400
|
+
function parseAssignments(pairs) {
|
|
401
|
+
const record = {};
|
|
402
|
+
for (const pair of pairs) {
|
|
403
|
+
const separator = pair.indexOf("=");
|
|
404
|
+
if (separator <= 0) {
|
|
405
|
+
throw new Error(`Invalid assignment "${pair}"; expected field=value`);
|
|
406
|
+
}
|
|
407
|
+
const field = pair.slice(0, separator).trim();
|
|
408
|
+
const raw = pair.slice(separator + 1);
|
|
409
|
+
try {
|
|
410
|
+
record[field] = JSON.parse(raw);
|
|
411
|
+
} catch {
|
|
412
|
+
record[field] = raw;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return record;
|
|
416
|
+
}
|
|
417
|
+
function registerAgentCommands(program2, createServices = defaultServicesFactory) {
|
|
418
|
+
const services = (options) => createServices(options);
|
|
419
|
+
const print = (text2) => {
|
|
420
|
+
console.log(text2);
|
|
421
|
+
};
|
|
422
|
+
program2.command("checkout").description("Materialize a scoped slice of the workspace into a vault folder").option("-q, --query <text>", "Search query scope").option("-s, --schema <iri...>", "Schema IRI scope").option("-n, --node <id...>", "Node id scope").option("-k, --kind <kind...>", "Kind folder scope: page|database|canvas").option("-l, --limit <n>", "Max nodes to materialize", parseIntOption).option("-d, --dir <path>", "Checkout directory", ".").option("--name <name>", "Workspace display name").option("--api-url <url>", "xNet local API URL").action(async (options) => {
|
|
423
|
+
print(await runCheckout(await services(options), options));
|
|
424
|
+
});
|
|
425
|
+
program2.command("status").description("List pending plans and conflicts for a checkout").option("-d, --dir <path>", "Checkout directory", ".").option("--format <format>", "Output format: tsv|json", "tsv").option("--api-url <url>", "xNet local API URL").action(async (options) => {
|
|
426
|
+
print(await runStatus(await services(options), options));
|
|
427
|
+
});
|
|
428
|
+
program2.command("commit").description("Lift file edits into mutation plans; --apply applies them").option("-d, --dir <path>", "Checkout directory", ".").option("--apply", "Apply valid plans through the plan pipeline").option("--actor <actor>", "Actor recorded on plans", "xnet-cli").option("--api-url <url>", "xNet local API URL").action(async (options) => {
|
|
429
|
+
print(await runCommit(await services(options), options));
|
|
430
|
+
});
|
|
431
|
+
program2.command("search <text>").description("Ranked workspace search (TSV: id, schema, title, snippet)").option("-s, --schema <iri>", "Schema IRI filter").option("-l, --limit <n>", "Max results", parseIntOption).option("--format <format>", "Output format: tsv|jsonl|json", "tsv").option("--api-url <url>", "xNet local API URL").action(async (text2, options) => {
|
|
432
|
+
print(await runSearch(await services(options), { ...options, text: text2 }));
|
|
433
|
+
});
|
|
434
|
+
program2.command("query <databaseId>").description("Query database rows (TSV by default)").option("-w, --where <expr...>", "Filters as field=value").option("-l, --limit <n>", "Max rows", parseIntOption).option("-o, --offset <n>", "Row offset", parseIntOption).option("--format <format>", "Output format: tsv|jsonl|json", "tsv").option("--detailed", "Include descriptor and query plan in json output").option("--api-url <url>", "xNet local API URL").action(async (databaseId, options) => {
|
|
435
|
+
print(await runQuery(await services(options), { ...options, databaseId }));
|
|
436
|
+
});
|
|
437
|
+
const db = program2.command("db").description("Direct node and row access");
|
|
438
|
+
db.command("get <nodeId>").description("Read a node as compact JSON").option("--detailed", "Include full node record").option("--api-url <url>", "xNet local API URL").action(async (nodeId, options) => {
|
|
439
|
+
print(await runDbGet(await services(options), { ...options, nodeId }));
|
|
440
|
+
});
|
|
441
|
+
db.command("set <databaseId> <rowId> <assignments...>").description("Update row properties through the plan/apply pipeline").option("--plan-only", "Print the mutation plan without applying").option("--actor <actor>", "Actor recorded on the plan", "xnet-cli").option("--api-url <url>", "xNet local API URL").action(async (databaseId, rowId, assignments, options) => {
|
|
442
|
+
print(
|
|
443
|
+
await runDbSet(await services(options), {
|
|
444
|
+
databaseId,
|
|
445
|
+
rowId,
|
|
446
|
+
assignments,
|
|
447
|
+
actor: options.actor,
|
|
448
|
+
planOnly: options.planOnly
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
});
|
|
452
|
+
program2.command("run <file>").description("Run a sandboxed agent script with the @xnet/agent-api surface").option("-s, --schema <iri>", "Preload nodes of this schema").option("-l, --limit <n>", "Max nodes to preload", parseIntOption).option("-n, --node <id>", "Current node id").option("-d, --dir <path>", "Checkout directory for proposal plans").option("--actor <actor>", "Actor recorded on proposal plans", "xnet-cli-script").option("--api-url <url>", "xNet local API URL").action(async (file, options) => {
|
|
453
|
+
print(await runScript(await services(options), { ...options, file }));
|
|
454
|
+
});
|
|
455
|
+
program2.command("daemon").description("Watch a checkout and lift saves into mutation plans").option("-d, --dir <path>", "Checkout directory", ".").option("--poll", "Use interval polling instead of fs.watch").option("--apply", "Auto-apply valid plans (watcher autocommit)").option("--actor <actor>", "Actor recorded on plans", "xnet-daemon").option("--api-url <url>", "xNet local API URL").action(async (options) => {
|
|
456
|
+
const handle = startDaemon(await services(options), options);
|
|
457
|
+
console.log(`watching ${resolve(options.dir)} (ctrl-c to stop)`);
|
|
458
|
+
process.on("SIGINT", () => {
|
|
459
|
+
handle.close();
|
|
460
|
+
process.exit(0);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
program2.command("skill").description("Print the cross-harness xNet SKILL.md").action(() => {
|
|
464
|
+
print(XNET_AGENT_SKILL_MD);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function parseIntOption(value) {
|
|
468
|
+
const parsed = Number.parseInt(value, 10);
|
|
469
|
+
if (!Number.isFinite(parsed)) throw new Error(`Invalid number: ${value}`);
|
|
470
|
+
return parsed;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/commands/bridge.ts
|
|
474
|
+
import { writeFileSync } from "fs";
|
|
475
|
+
import { tmpdir } from "os";
|
|
476
|
+
import { join as join2 } from "path";
|
|
477
|
+
import {
|
|
478
|
+
buildAgentArgs,
|
|
479
|
+
cliAgentRunner,
|
|
480
|
+
cliChatAgent,
|
|
481
|
+
createBridgeServer,
|
|
482
|
+
DEFAULT_BRIDGE_PORT,
|
|
483
|
+
defaultXnetGate,
|
|
484
|
+
Git,
|
|
485
|
+
handleBridgeRun,
|
|
486
|
+
mcpConfigFor,
|
|
487
|
+
NodeCommandRunner
|
|
488
|
+
} from "@xnetjs/devkit";
|
|
489
|
+
function buildBridgeServer(options, runner = new NodeCommandRunner()) {
|
|
490
|
+
const command = options.agent ?? "claude";
|
|
491
|
+
const cwd = options.cwd ?? process.cwd();
|
|
492
|
+
const args = buildAgentArgs(command, {
|
|
493
|
+
...options.mcpConfigPath ? { mcpConfigPath: options.mcpConfigPath } : {}
|
|
494
|
+
});
|
|
495
|
+
const agent = cliChatAgent(runner, { command, cwd, args });
|
|
496
|
+
const run = options.code ? (request) => handleBridgeRun(
|
|
497
|
+
{
|
|
498
|
+
git: new Git(runner, cwd),
|
|
499
|
+
runner,
|
|
500
|
+
agent: cliAgentRunner(runner, { command }),
|
|
501
|
+
gate: defaultXnetGate(),
|
|
502
|
+
worktreeRoot: join2(cwd, ".xnet", "agent-worktrees")
|
|
503
|
+
},
|
|
504
|
+
request
|
|
505
|
+
) : void 0;
|
|
506
|
+
return createBridgeServer({
|
|
507
|
+
agent,
|
|
508
|
+
agentName: command,
|
|
509
|
+
...run ? { run } : {},
|
|
510
|
+
...options.host ? { host: options.host } : {},
|
|
511
|
+
...options.port !== void 0 ? { port: options.port } : {},
|
|
512
|
+
...options.allowOrigin ? { allowedOrigins: options.allowOrigin } : {}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function registerBridgeCommand(program2) {
|
|
516
|
+
const bridge = program2.command("bridge").description("Run the local agent bridge for XNet's AI chat panel");
|
|
517
|
+
bridge.command("serve").description("Serve the agent bridge on loopback (default :31416), driving your own agent CLI").option("--agent <command>", "Agent CLI to drive (claude, codex, \u2026)", "claude").option("--host <host>", "Loopback host (default 127.0.0.1)").option("--port <n>", `Port (default ${DEFAULT_BRIDGE_PORT})`, parseIntOption2).option(
|
|
518
|
+
"--allow-origin <origin...>",
|
|
519
|
+
"Browser origins permitted (e.g. https://user.github.io for the web deployment)"
|
|
520
|
+
).option("--cwd <dir>", "Working directory the agent runs in (default current dir)").option("--code", "Enable POST /run agentic code tasks (worktree \u2192 gate \u2192 checkpoint/PR)").option("--mcp", "Give the agent XNet's workspace tools via `xnet mcp serve`").option(
|
|
521
|
+
"--mcp-api-url <url>",
|
|
522
|
+
"xNet local API URL the MCP server talks to (default http://127.0.0.1:31415)"
|
|
523
|
+
).action(async (options) => {
|
|
524
|
+
const resolved = { ...options };
|
|
525
|
+
if (options.mcp) {
|
|
526
|
+
const spec = {
|
|
527
|
+
command: process.execPath,
|
|
528
|
+
args: [
|
|
529
|
+
process.argv[1],
|
|
530
|
+
"mcp",
|
|
531
|
+
"serve",
|
|
532
|
+
"--api-url",
|
|
533
|
+
options.mcpApiUrl ?? "http://127.0.0.1:31415"
|
|
534
|
+
]
|
|
535
|
+
};
|
|
536
|
+
const mcpConfigPath = join2(tmpdir(), `xnet-bridge-mcp-${process.pid}.json`);
|
|
537
|
+
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfigFor(spec)));
|
|
538
|
+
resolved.mcpConfigPath = mcpConfigPath;
|
|
539
|
+
}
|
|
540
|
+
const handle = buildBridgeServer(resolved);
|
|
541
|
+
await handle.start();
|
|
542
|
+
console.error(
|
|
543
|
+
`xNet agent bridge listening on ${handle.url} (agent: ${options.agent ?? "claude"}${options.mcp ? ", workspace tools enabled" : ""})`
|
|
544
|
+
);
|
|
545
|
+
console.error('In XNet, open the AI panel and select "Local bridge".');
|
|
546
|
+
const shutdown = () => {
|
|
547
|
+
void handle.stop().then(() => process.exit(0));
|
|
548
|
+
};
|
|
549
|
+
process.on("SIGINT", shutdown);
|
|
550
|
+
process.on("SIGTERM", shutdown);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
function parseIntOption2(value) {
|
|
554
|
+
const parsed = Number.parseInt(value, 10);
|
|
555
|
+
if (!Number.isFinite(parsed)) throw new Error(`Invalid number: ${value}`);
|
|
556
|
+
return parsed;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/commands/code.ts
|
|
560
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
561
|
+
import {
|
|
562
|
+
cliAgentRunner as cliAgentRunner2,
|
|
563
|
+
defaultXnetGate as defaultXnetGate2,
|
|
564
|
+
Git as Git2,
|
|
565
|
+
NodeCommandRunner as NodeCommandRunner2,
|
|
566
|
+
openPullRequest,
|
|
567
|
+
runAgentTask
|
|
568
|
+
} from "@xnetjs/devkit";
|
|
569
|
+
function slug(text2) {
|
|
570
|
+
return text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
|
|
571
|
+
}
|
|
572
|
+
function resolveCodeRunConfig(prompt, options, repoRoot, now = Date.now) {
|
|
573
|
+
const id = options.id ?? `code-${slug(prompt)}-${now().toString(36)}`;
|
|
574
|
+
const branch = options.branch ?? `agent/${id}`;
|
|
575
|
+
const base = options.base ?? "origin/main";
|
|
576
|
+
const worktreePath = options.worktree ? resolve2(options.worktree) : join3(repoRoot, ".xnet", "agent-worktrees", id);
|
|
577
|
+
return {
|
|
578
|
+
id,
|
|
579
|
+
branch,
|
|
580
|
+
base,
|
|
581
|
+
worktreePath,
|
|
582
|
+
gate: defaultXnetGate2({ changedSince: base }),
|
|
583
|
+
keepWorktree: Boolean(options.pr || options.keep)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function summarizeAgentTaskResult(result) {
|
|
587
|
+
if (!result.ok) {
|
|
588
|
+
if (result.rolledBack && result.gate.failedStep) {
|
|
589
|
+
return `\u2717 gate failed at "${result.gate.failedStep}" \u2014 edits rolled back on ${result.branch}.`;
|
|
590
|
+
}
|
|
591
|
+
return `\u2717 agent run failed on ${result.branch}.`;
|
|
592
|
+
}
|
|
593
|
+
if (result.noChanges) return `\u2022 agent made no changes on ${result.branch}.`;
|
|
594
|
+
return `\u2713 ${result.branch} passed the gate and was checkpointed.`;
|
|
595
|
+
}
|
|
596
|
+
function registerCodeCommand(program2) {
|
|
597
|
+
program2.command("code").argument("<intent>", "What the coding agent should do").description("Run a coding agent on this repo in an isolated worktree (gate \u2192 checkpoint/PR)").option("--agent <command>", "Coding agent CLI (claude, codex, aider, \u2026)", "claude").option("--id <id>", "Task id (also the default branch + worktree name)").option("--branch <name>", "Branch for the worktree (default agent/<id>)").option("--base <ref>", "Base ref for the gate + PR (default origin/main)").option("--worktree <dir>", "Worktree directory (default .xnet/agent-worktrees/<id>)").option("--pr", "Open a pull request when the gate passes").option("--keep", "Keep the worktree after a successful run").option("--repo <dir>", "Repo root (default current dir)").action(async (intent, options) => {
|
|
598
|
+
const runner = new NodeCommandRunner2();
|
|
599
|
+
const repoRoot = resolve2(options.repo ?? process.cwd());
|
|
600
|
+
const config = resolveCodeRunConfig(intent, options, repoRoot);
|
|
601
|
+
const result = await runAgentTask({
|
|
602
|
+
git: new Git2(runner, repoRoot),
|
|
603
|
+
runner,
|
|
604
|
+
agent: cliAgentRunner2(runner, { command: options.agent ?? "claude" }),
|
|
605
|
+
task: { id: config.id, prompt: intent },
|
|
606
|
+
worktreePath: config.worktreePath,
|
|
607
|
+
branch: config.branch,
|
|
608
|
+
gate: config.gate,
|
|
609
|
+
keepWorktree: config.keepWorktree
|
|
610
|
+
});
|
|
611
|
+
console.error(summarizeAgentTaskResult(result));
|
|
612
|
+
if (result.ok && !result.noChanges && options.pr) {
|
|
613
|
+
try {
|
|
614
|
+
const pr = await openPullRequest(runner, config.worktreePath, config.branch, {
|
|
615
|
+
base: config.base,
|
|
616
|
+
title: intent
|
|
617
|
+
});
|
|
618
|
+
console.error(`\u2192 PR: ${pr.url}`);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
console.error(`\u2192 PR failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/commands/connector.ts
|
|
628
|
+
import { resolve as resolve4 } from "path";
|
|
629
|
+
import { scaffoldPlugin as scaffoldPlugin2 } from "@xnetjs/plugins";
|
|
630
|
+
|
|
631
|
+
// src/commands/plugin.ts
|
|
632
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
633
|
+
import { resolve as resolve3, dirname as dirname2, join as join4 } from "path";
|
|
634
|
+
import { scaffoldPlugin } from "@xnetjs/plugins";
|
|
635
|
+
var TEMPLATES = ["client", "two-sided", "ai-script"];
|
|
636
|
+
var nodeIO = {
|
|
637
|
+
mkdir: (path) => mkdirSync(path, { recursive: true }),
|
|
638
|
+
writeFile: (path, content) => writeFileSync2(path, content, "utf-8")
|
|
639
|
+
};
|
|
640
|
+
function writeScaffoldFiles(files, targetDir, io = nodeIO) {
|
|
641
|
+
const written = [];
|
|
642
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
643
|
+
const full = join4(targetDir, rel);
|
|
644
|
+
io.mkdir(dirname2(full));
|
|
645
|
+
io.writeFile(full, content);
|
|
646
|
+
written.push(rel);
|
|
647
|
+
}
|
|
648
|
+
return written;
|
|
649
|
+
}
|
|
650
|
+
function resolveTemplate(value) {
|
|
651
|
+
const template = value ?? "client";
|
|
652
|
+
if (!TEMPLATES.includes(template)) {
|
|
653
|
+
throw new Error(`Unknown template "${value}". Use one of: ${TEMPLATES.join(", ")}`);
|
|
654
|
+
}
|
|
655
|
+
return template;
|
|
656
|
+
}
|
|
657
|
+
function scaffoldCommand(id, options) {
|
|
658
|
+
const template = resolveTemplate(options.template);
|
|
659
|
+
const { files } = scaffoldPlugin({
|
|
660
|
+
id,
|
|
661
|
+
name: options.name ?? id,
|
|
662
|
+
template,
|
|
663
|
+
author: options.author,
|
|
664
|
+
description: options.description
|
|
665
|
+
});
|
|
666
|
+
const targetDir = resolve3(process.cwd(), options.out ?? id);
|
|
667
|
+
const written = writeScaffoldFiles(files, targetDir);
|
|
668
|
+
console.log(`Scaffolded ${template} plugin "${id}" at ${targetDir}`);
|
|
669
|
+
for (const rel of written) console.log(` + ${rel}`);
|
|
670
|
+
console.log("\nNext: cd into the project, `npm install`, then `npm test`.");
|
|
671
|
+
}
|
|
672
|
+
function registerPluginCommand(program2) {
|
|
673
|
+
const plugin = program2.command("plugin").description("Author and manage xNet plugins");
|
|
674
|
+
plugin.command("scaffold <id>").description("Scaffold a new plugin project (id is reverse-domain, e.g. com.acme.kanban)").option("--name <name>", "Human-readable plugin name").option("--template <template>", `Template: ${TEMPLATES.join(" | ")}`, "client").option("--author <author>", "Author name").option("--description <text>", "Short description").option("--out <dir>", "Output directory (default: the plugin id)").action((id, opts) => {
|
|
675
|
+
try {
|
|
676
|
+
scaffoldCommand(id, opts);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/commands/connector.ts
|
|
685
|
+
function scaffoldConnector(id, options) {
|
|
686
|
+
const { files } = scaffoldPlugin2({
|
|
687
|
+
id,
|
|
688
|
+
name: options.name ?? id,
|
|
689
|
+
template: "connector",
|
|
690
|
+
author: options.author,
|
|
691
|
+
description: options.description
|
|
692
|
+
});
|
|
693
|
+
const targetDir = resolve4(process.cwd(), options.out ?? id);
|
|
694
|
+
const written = writeScaffoldFiles(files, targetDir);
|
|
695
|
+
console.log(`Scaffolded connector "${id}" at ${targetDir}`);
|
|
696
|
+
for (const rel of written) console.log(` + ${rel}`);
|
|
697
|
+
console.log(
|
|
698
|
+
"\nNext: cd in, `npm install`, `npm test`. Then fill in the sync `pull` and register the hub half with `connectorSyncFeature` (see @xnetjs/hub)."
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
function registerConnectorCommand(program2) {
|
|
702
|
+
const connector = program2.command("connector").description("Author agent-native Connectors (sync an external service into governed nodes)");
|
|
703
|
+
connector.command("scaffold <id>").description(
|
|
704
|
+
"Scaffold a Connector project (id is reverse-domain, e.g. dev.acme.connector.slack)"
|
|
705
|
+
).option("--name <name>", "Human-readable connector name").option("--author <author>", "Author name").option("--description <text>", "Short description").option("--out <dir>", "Output directory (default: the connector id)").action((id, opts) => {
|
|
706
|
+
try {
|
|
707
|
+
scaffoldConnector(id, opts);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/commands/data.ts
|
|
716
|
+
import { generateSigningKeyPair, getSigningPublicKeyFromPrivate } from "@xnetjs/crypto";
|
|
717
|
+
import { defineSchema, SQLiteNodeStorageAdapter, text } from "@xnetjs/data";
|
|
718
|
+
import { createDID } from "@xnetjs/identity";
|
|
719
|
+
import { createXNetClient } from "@xnetjs/runtime";
|
|
720
|
+
import chalk from "chalk";
|
|
721
|
+
var NoteSchema = defineSchema({
|
|
722
|
+
name: "Note",
|
|
723
|
+
namespace: "xnet://cli/",
|
|
724
|
+
properties: {
|
|
725
|
+
title: text({ required: true }),
|
|
726
|
+
body: text({})
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
function hexToBytes(hex) {
|
|
730
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
731
|
+
return new Uint8Array(Buffer.from(clean, "hex"));
|
|
732
|
+
}
|
|
733
|
+
function resolveSigningKey(key) {
|
|
734
|
+
const provided = key ?? process.env.XNET_SIGNING_KEY;
|
|
735
|
+
if (provided) return { signingKey: hexToBytes(provided), ephemeral: false };
|
|
736
|
+
return { signingKey: generateSigningKeyPair().privateKey, ephemeral: true };
|
|
737
|
+
}
|
|
738
|
+
async function resolveStorage(db) {
|
|
739
|
+
if (db) {
|
|
740
|
+
const { createElectronSQLiteAdapter } = await import("@xnetjs/sqlite/electron");
|
|
741
|
+
const adapter2 = await createElectronSQLiteAdapter({
|
|
742
|
+
path: db,
|
|
743
|
+
busyTimeout: 5e3,
|
|
744
|
+
foreignKeys: true,
|
|
745
|
+
walMode: true
|
|
746
|
+
});
|
|
747
|
+
return new SQLiteNodeStorageAdapter(adapter2);
|
|
748
|
+
}
|
|
749
|
+
const { createMemorySQLiteAdapter } = await import("@xnetjs/sqlite/memory");
|
|
750
|
+
const adapter = await createMemorySQLiteAdapter();
|
|
751
|
+
return new SQLiteNodeStorageAdapter(adapter);
|
|
752
|
+
}
|
|
753
|
+
async function buildDataClient(options = {}) {
|
|
754
|
+
const { signingKey } = resolveSigningKey(options.key);
|
|
755
|
+
const authorDID = createDID(getSigningPublicKeyFromPrivate(signingKey));
|
|
756
|
+
const nodeStorage = await resolveStorage(options.db);
|
|
757
|
+
return createXNetClient({ nodeStorage, authorDID, signingKey });
|
|
758
|
+
}
|
|
759
|
+
async function runCreateNote(client, input) {
|
|
760
|
+
return client.mutate.create(NoteSchema, { title: input.title, body: input.body ?? "" });
|
|
761
|
+
}
|
|
762
|
+
async function runListNotes(client) {
|
|
763
|
+
return client.fetch(NoteSchema);
|
|
764
|
+
}
|
|
765
|
+
function registerDataCommand(program2) {
|
|
766
|
+
const data = program2.command("data").description("Read and write live nodes (runtime client)");
|
|
767
|
+
data.command("add").description("Create a Note node").requiredOption("--title <title>", "Note title").option("--body <body>", "Note body", "").option("--db <path>", "SQLite file path (default: in-memory)").option("--key <hex>", "Ed25519 signing key (hex); falls back to $XNET_SIGNING_KEY").action(async (opts) => {
|
|
768
|
+
const client = await buildDataClient({ db: opts.db, key: opts.key });
|
|
769
|
+
try {
|
|
770
|
+
const node = await runCreateNote(client, { title: opts.title, body: opts.body });
|
|
771
|
+
console.log(chalk.green(`\u2713 created ${node.id}`));
|
|
772
|
+
console.log(chalk.dim(` author: ${client.authorDID}`));
|
|
773
|
+
} finally {
|
|
774
|
+
await client.destroy();
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
data.command("list").description("List Note nodes").option("--db <path>", "SQLite file path (default: in-memory)").option("--key <hex>", "Ed25519 signing key (hex); falls back to $XNET_SIGNING_KEY").action(async (opts) => {
|
|
778
|
+
const client = await buildDataClient({ db: opts.db, key: opts.key });
|
|
779
|
+
try {
|
|
780
|
+
const notes = await runListNotes(client);
|
|
781
|
+
if (notes.length === 0) {
|
|
782
|
+
console.log(chalk.dim("(no notes)"));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
for (const note of notes) {
|
|
786
|
+
const title = String(note.properties.title ?? "");
|
|
787
|
+
console.log(`${chalk.cyan(note.id)} ${title}`);
|
|
788
|
+
}
|
|
789
|
+
} finally {
|
|
790
|
+
await client.destroy();
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
11
795
|
// src/commands/doctor.ts
|
|
12
|
-
import { writeFileSync, readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
13
|
-
import { resolve, join } from "path";
|
|
796
|
+
import { writeFileSync as writeFileSync3, readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
797
|
+
import { resolve as resolve5, join as join5 } from "path";
|
|
14
798
|
import {
|
|
15
799
|
verifyIntegrity,
|
|
16
800
|
quickIntegrityCheck,
|
|
@@ -22,8 +806,8 @@ import {
|
|
|
22
806
|
} from "@xnetjs/sync";
|
|
23
807
|
async function getChalk() {
|
|
24
808
|
try {
|
|
25
|
-
const
|
|
26
|
-
return
|
|
809
|
+
const chalk2 = await import("chalk");
|
|
810
|
+
return chalk2.default;
|
|
27
811
|
} catch {
|
|
28
812
|
const identity = (s) => s;
|
|
29
813
|
return {
|
|
@@ -40,15 +824,15 @@ async function getChalk() {
|
|
|
40
824
|
}
|
|
41
825
|
function findDataDir(providedDir) {
|
|
42
826
|
if (providedDir) {
|
|
43
|
-
const resolved =
|
|
827
|
+
const resolved = resolve5(process.cwd(), providedDir);
|
|
44
828
|
if (existsSync(resolved)) {
|
|
45
829
|
return resolved;
|
|
46
830
|
}
|
|
47
831
|
throw new Error(`Data directory not found: ${resolved}`);
|
|
48
832
|
}
|
|
49
|
-
const commonPaths = [".xnet/data", "data", ".data",
|
|
833
|
+
const commonPaths = [".xnet/data", "data", ".data", resolve5(process.env.HOME ?? "", ".xnet/data")];
|
|
50
834
|
for (const path of commonPaths) {
|
|
51
|
-
const resolved =
|
|
835
|
+
const resolved = resolve5(process.cwd(), path);
|
|
52
836
|
if (existsSync(resolved)) {
|
|
53
837
|
return resolved;
|
|
54
838
|
}
|
|
@@ -59,7 +843,7 @@ function loadChangesFromDir(dataDir) {
|
|
|
59
843
|
const changes = [];
|
|
60
844
|
const patterns = ["changes.json", "changes/*.json", "*.changes.json"];
|
|
61
845
|
for (const pattern of patterns) {
|
|
62
|
-
const changesFile =
|
|
846
|
+
const changesFile = join5(dataDir, pattern.split("/")[0]);
|
|
63
847
|
if (existsSync(changesFile) && statSync(changesFile).isFile()) {
|
|
64
848
|
try {
|
|
65
849
|
const content = readFileSync(changesFile, "utf-8");
|
|
@@ -73,12 +857,12 @@ function loadChangesFromDir(dataDir) {
|
|
|
73
857
|
}
|
|
74
858
|
}
|
|
75
859
|
}
|
|
76
|
-
const changesDir =
|
|
860
|
+
const changesDir = join5(dataDir, "changes");
|
|
77
861
|
if (existsSync(changesDir) && statSync(changesDir).isDirectory()) {
|
|
78
862
|
const files = readdirSync(changesDir).filter((f) => f.endsWith(".json"));
|
|
79
863
|
for (const file of files) {
|
|
80
864
|
try {
|
|
81
|
-
const content = readFileSync(
|
|
865
|
+
const content = readFileSync(join5(changesDir, file), "utf-8");
|
|
82
866
|
const change = JSON.parse(content);
|
|
83
867
|
changes.push(change);
|
|
84
868
|
} catch {
|
|
@@ -92,114 +876,114 @@ function formatDuration(ms) {
|
|
|
92
876
|
return `${(ms / 1e3).toFixed(2)}s`;
|
|
93
877
|
}
|
|
94
878
|
async function doctorCommand(options) {
|
|
95
|
-
const
|
|
96
|
-
console.log(
|
|
97
|
-
console.log(
|
|
879
|
+
const chalk2 = await getChalk();
|
|
880
|
+
console.log(chalk2.bold("\nxNet Health Check\n"));
|
|
881
|
+
console.log(chalk2.dim("\u2500".repeat(50)));
|
|
98
882
|
try {
|
|
99
883
|
let dataDir;
|
|
100
884
|
let changes = [];
|
|
101
885
|
try {
|
|
102
886
|
dataDir = findDataDir(options.dataDir);
|
|
103
|
-
console.log(
|
|
887
|
+
console.log(chalk2.gray(`Data directory: ${dataDir}`));
|
|
104
888
|
changes = loadChangesFromDir(dataDir);
|
|
105
|
-
console.log(
|
|
889
|
+
console.log(chalk2.gray(`Found ${changes.length} changes`));
|
|
106
890
|
} catch {
|
|
107
|
-
console.log(
|
|
108
|
-
console.log(
|
|
109
|
-
console.log(
|
|
891
|
+
console.log(chalk2.yellow("\nNote: No data directory found."));
|
|
892
|
+
console.log(chalk2.gray("Use --data-dir to specify a data directory."));
|
|
893
|
+
console.log(chalk2.gray("Running with demo data...\n"));
|
|
110
894
|
changes = generateDemoChanges();
|
|
111
895
|
}
|
|
112
896
|
console.log();
|
|
113
|
-
console.log(
|
|
897
|
+
console.log(chalk2.bold("Checking data integrity..."));
|
|
114
898
|
const report = options.quick ? await quickIntegrityCheck(changes) : await verifyIntegrity(changes);
|
|
115
899
|
if (options.json) {
|
|
116
900
|
console.log(JSON.stringify(report, null, 2));
|
|
117
901
|
return;
|
|
118
902
|
}
|
|
119
|
-
printIntegrityResults(report,
|
|
903
|
+
printIntegrityResults(report, chalk2, options.verbose);
|
|
120
904
|
console.log();
|
|
121
|
-
console.log(
|
|
905
|
+
console.log(chalk2.bold("Analyzing change chains..."));
|
|
122
906
|
const orphans = findOrphans(changes);
|
|
123
907
|
const roots = findRoots(changes);
|
|
124
908
|
const heads = findHeads(changes);
|
|
125
909
|
const depth = getChainDepth(changes);
|
|
126
|
-
console.log(` ${
|
|
127
|
-
console.log(` ${
|
|
128
|
-
console.log(` ${
|
|
910
|
+
console.log(` ${chalk2.green("\u2713")} Roots: ${roots.length}`);
|
|
911
|
+
console.log(` ${chalk2.green("\u2713")} Heads: ${heads.length}`);
|
|
912
|
+
console.log(` ${chalk2.green("\u2713")} Depth: ${depth}`);
|
|
129
913
|
if (orphans.length > 0) {
|
|
130
|
-
console.log(` ${
|
|
914
|
+
console.log(` ${chalk2.yellow("\u26A0")} Orphans: ${orphans.length}`);
|
|
131
915
|
} else {
|
|
132
|
-
console.log(` ${
|
|
916
|
+
console.log(` ${chalk2.green("\u2713")} Orphans: 0`);
|
|
133
917
|
}
|
|
134
918
|
console.log();
|
|
135
|
-
console.log(
|
|
136
|
-
console.log(
|
|
919
|
+
console.log(chalk2.bold("Checking schema compatibility..."));
|
|
920
|
+
console.log(chalk2.gray(" (Schema analysis requires @xnetjs/data integration)"));
|
|
137
921
|
console.log();
|
|
138
|
-
console.log(
|
|
139
|
-
console.log(
|
|
922
|
+
console.log(chalk2.bold("Checking sync state..."));
|
|
923
|
+
console.log(chalk2.gray(" (Sync analysis requires active SyncProvider)"));
|
|
140
924
|
console.log();
|
|
141
|
-
console.log(
|
|
925
|
+
console.log(chalk2.dim("\u2500".repeat(50)));
|
|
142
926
|
const hasErrors = report.summary.errors > 0;
|
|
143
927
|
const hasWarnings = report.summary.warnings > 0 || orphans.length > 0;
|
|
144
928
|
if (hasErrors) {
|
|
145
|
-
console.log(
|
|
146
|
-
console.log(
|
|
929
|
+
console.log(chalk2.red(chalk2.bold("Status: UNHEALTHY")));
|
|
930
|
+
console.log(chalk2.red(" Data integrity issues detected. Run `xnet repair` to fix."));
|
|
147
931
|
} else if (hasWarnings) {
|
|
148
|
-
console.log(
|
|
149
|
-
console.log(
|
|
932
|
+
console.log(chalk2.yellow(chalk2.bold("Status: HEALTHY with warnings")));
|
|
933
|
+
console.log(chalk2.gray(" Some issues detected but data is intact."));
|
|
150
934
|
} else {
|
|
151
|
-
console.log(
|
|
152
|
-
console.log(
|
|
935
|
+
console.log(chalk2.green(chalk2.bold("Status: HEALTHY")));
|
|
936
|
+
console.log(chalk2.green(" All checks passed."));
|
|
153
937
|
}
|
|
154
938
|
console.log();
|
|
155
939
|
} catch (error) {
|
|
156
|
-
console.error(
|
|
940
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
157
941
|
process.exit(1);
|
|
158
942
|
}
|
|
159
943
|
}
|
|
160
|
-
function printIntegrityResults(report,
|
|
944
|
+
function printIntegrityResults(report, chalk2, verbose) {
|
|
161
945
|
const { checked, valid, issues, summary, durationMs } = report;
|
|
162
946
|
const percentage = checked > 0 ? Math.round(valid / checked * 100) : 100;
|
|
163
947
|
if (issues.length === 0) {
|
|
164
|
-
console.log(` ${
|
|
165
|
-
console.log(` ${
|
|
166
|
-
console.log(` ${
|
|
167
|
-
console.log(
|
|
948
|
+
console.log(` ${chalk2.green("\u2713")} ${checked} changes verified (${percentage}% valid)`);
|
|
949
|
+
console.log(` ${chalk2.green("\u2713")} Hash chains intact`);
|
|
950
|
+
console.log(` ${chalk2.green("\u2713")} No issues detected`);
|
|
951
|
+
console.log(chalk2.gray(` Completed in ${formatDuration(durationMs)}`));
|
|
168
952
|
} else {
|
|
169
953
|
console.log(
|
|
170
|
-
` ${
|
|
954
|
+
` ${chalk2.yellow("!")} ${checked} changes checked (${valid} valid, ${checked - valid} issues)`
|
|
171
955
|
);
|
|
172
956
|
if (summary.errors > 0) {
|
|
173
|
-
console.log(` ${
|
|
957
|
+
console.log(` ${chalk2.red("\u2717")} ${summary.errors} errors`);
|
|
174
958
|
}
|
|
175
959
|
if (summary.warnings > 0) {
|
|
176
|
-
console.log(` ${
|
|
960
|
+
console.log(` ${chalk2.yellow("\u26A0")} ${summary.warnings} warnings`);
|
|
177
961
|
}
|
|
178
|
-
console.log(
|
|
962
|
+
console.log(chalk2.gray(` Completed in ${formatDuration(durationMs)}`));
|
|
179
963
|
if (verbose) {
|
|
180
964
|
console.log();
|
|
181
|
-
console.log(
|
|
965
|
+
console.log(chalk2.bold("Issues by type:"));
|
|
182
966
|
for (const [type, count] of Object.entries(summary.byType)) {
|
|
183
967
|
if (count > 0) {
|
|
184
968
|
console.log(` - ${type}: ${count}`);
|
|
185
969
|
}
|
|
186
970
|
}
|
|
187
971
|
console.log();
|
|
188
|
-
console.log(
|
|
972
|
+
console.log(chalk2.bold("Details:"));
|
|
189
973
|
for (const issue of issues.slice(0, 10)) {
|
|
190
|
-
const icon = issue.severity === "error" ?
|
|
974
|
+
const icon = issue.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("\u26A0");
|
|
191
975
|
console.log(` ${icon} [${issue.type}] ${issue.details}`);
|
|
192
976
|
if (issue.repairAction) {
|
|
193
|
-
console.log(
|
|
977
|
+
console.log(chalk2.gray(` \u2192 ${issue.repairAction.description}`));
|
|
194
978
|
}
|
|
195
979
|
}
|
|
196
980
|
if (issues.length > 10) {
|
|
197
|
-
console.log(
|
|
981
|
+
console.log(chalk2.gray(` ... and ${issues.length - 10} more`));
|
|
198
982
|
}
|
|
199
983
|
}
|
|
200
984
|
if (report.repairable) {
|
|
201
985
|
console.log();
|
|
202
|
-
console.log(
|
|
986
|
+
console.log(chalk2.cyan(` Run \`xnet repair\` to fix these issues.`));
|
|
203
987
|
}
|
|
204
988
|
}
|
|
205
989
|
}
|
|
@@ -219,7 +1003,7 @@ function generateDemoChanges() {
|
|
|
219
1003
|
authorDID: demoAuthor,
|
|
220
1004
|
signature: new Uint8Array([1, 2, 3, 4]),
|
|
221
1005
|
wallTime: now - 1e4,
|
|
222
|
-
lamport:
|
|
1006
|
+
lamport: 1
|
|
223
1007
|
},
|
|
224
1008
|
{
|
|
225
1009
|
id: "change-2",
|
|
@@ -231,13 +1015,13 @@ function generateDemoChanges() {
|
|
|
231
1015
|
authorDID: demoAuthor,
|
|
232
1016
|
signature: new Uint8Array([5, 6, 7, 8]),
|
|
233
1017
|
wallTime: now - 5e3,
|
|
234
|
-
lamport:
|
|
1018
|
+
lamport: 2
|
|
235
1019
|
}
|
|
236
1020
|
];
|
|
237
1021
|
}
|
|
238
1022
|
async function repairCommand(options) {
|
|
239
|
-
const
|
|
240
|
-
console.log(
|
|
1023
|
+
const chalk2 = await getChalk();
|
|
1024
|
+
console.log(chalk2.bold("\nxNet Data Repair\n"));
|
|
241
1025
|
try {
|
|
242
1026
|
let dataDir;
|
|
243
1027
|
let changes = [];
|
|
@@ -245,15 +1029,15 @@ async function repairCommand(options) {
|
|
|
245
1029
|
dataDir = findDataDir(options.dataDir);
|
|
246
1030
|
changes = loadChangesFromDir(dataDir);
|
|
247
1031
|
} catch {
|
|
248
|
-
console.log(
|
|
249
|
-
console.log(
|
|
1032
|
+
console.log(chalk2.yellow("Note: No data directory found."));
|
|
1033
|
+
console.log(chalk2.gray("Using demo data for illustration.\n"));
|
|
250
1034
|
dataDir = "";
|
|
251
1035
|
changes = generateDemoChanges();
|
|
252
1036
|
}
|
|
253
|
-
console.log(
|
|
1037
|
+
console.log(chalk2.gray("Running integrity check..."));
|
|
254
1038
|
const report = await verifyIntegrity(changes);
|
|
255
1039
|
if (report.issues.length === 0) {
|
|
256
|
-
console.log(
|
|
1040
|
+
console.log(chalk2.green("\n\u2713 No issues found. Nothing to repair."));
|
|
257
1041
|
return;
|
|
258
1042
|
}
|
|
259
1043
|
console.log(`
|
|
@@ -262,7 +1046,7 @@ Found ${report.issues.length} issues:`);
|
|
|
262
1046
|
console.log(` - ${report.summary.warnings} warnings`);
|
|
263
1047
|
console.log();
|
|
264
1048
|
if (options.dryRun) {
|
|
265
|
-
console.log(
|
|
1049
|
+
console.log(chalk2.yellow("DRY RUN - No changes will be made\n"));
|
|
266
1050
|
}
|
|
267
1051
|
const { remainingIssues, repairCount } = await attemptRepair(changes, report.issues);
|
|
268
1052
|
if (options.json) {
|
|
@@ -280,33 +1064,33 @@ Found ${report.issues.length} issues:`);
|
|
|
280
1064
|
);
|
|
281
1065
|
return;
|
|
282
1066
|
}
|
|
283
|
-
console.log(
|
|
284
|
-
console.log(` ${
|
|
285
|
-
console.log(` ${
|
|
1067
|
+
console.log(chalk2.bold("Repair results:"));
|
|
1068
|
+
console.log(` ${chalk2.green("\u2713")} Repaired: ${repairCount} issues`);
|
|
1069
|
+
console.log(` ${chalk2.yellow("!")} Remaining: ${remainingIssues.length} issues`);
|
|
286
1070
|
if (remainingIssues.length > 0) {
|
|
287
1071
|
console.log();
|
|
288
|
-
console.log(
|
|
1072
|
+
console.log(chalk2.yellow("Issues that require manual intervention:"));
|
|
289
1073
|
for (const issue of remainingIssues.slice(0, 5)) {
|
|
290
1074
|
console.log(` - [${issue.type}] ${issue.details}`);
|
|
291
1075
|
}
|
|
292
1076
|
if (remainingIssues.length > 5) {
|
|
293
|
-
console.log(
|
|
1077
|
+
console.log(chalk2.gray(` ... and ${remainingIssues.length - 5} more`));
|
|
294
1078
|
}
|
|
295
1079
|
}
|
|
296
1080
|
if (!options.dryRun && dataDir && repairCount > 0) {
|
|
297
1081
|
console.log();
|
|
298
|
-
console.log(
|
|
299
|
-
console.log(
|
|
1082
|
+
console.log(chalk2.gray("Note: Writing repaired changes requires NodeStore integration."));
|
|
1083
|
+
console.log(chalk2.gray("Changes are prepared but not written to disk."));
|
|
300
1084
|
}
|
|
301
1085
|
console.log();
|
|
302
1086
|
} catch (error) {
|
|
303
|
-
console.error(
|
|
1087
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
304
1088
|
process.exit(1);
|
|
305
1089
|
}
|
|
306
1090
|
}
|
|
307
1091
|
async function exportCommand(options) {
|
|
308
|
-
const
|
|
309
|
-
console.log(
|
|
1092
|
+
const chalk2 = await getChalk();
|
|
1093
|
+
console.log(chalk2.bold("\nxNet Data Export\n"));
|
|
310
1094
|
try {
|
|
311
1095
|
let dataDir;
|
|
312
1096
|
let changes = [];
|
|
@@ -314,14 +1098,14 @@ async function exportCommand(options) {
|
|
|
314
1098
|
dataDir = findDataDir(options.dataDir);
|
|
315
1099
|
changes = loadChangesFromDir(dataDir);
|
|
316
1100
|
} catch {
|
|
317
|
-
console.log(
|
|
318
|
-
console.log(
|
|
1101
|
+
console.log(chalk2.yellow("Note: No data directory found."));
|
|
1102
|
+
console.log(chalk2.gray("Using demo data for illustration.\n"));
|
|
319
1103
|
dataDir = "";
|
|
320
1104
|
changes = generateDemoChanges();
|
|
321
1105
|
}
|
|
322
|
-
const outputPath =
|
|
323
|
-
console.log(
|
|
324
|
-
console.log(
|
|
1106
|
+
const outputPath = resolve5(process.cwd(), options.output);
|
|
1107
|
+
console.log(chalk2.gray(`Source: ${dataDir || "demo data"}`));
|
|
1108
|
+
console.log(chalk2.gray(`Output: ${outputPath}`));
|
|
325
1109
|
console.log();
|
|
326
1110
|
const exportData = {
|
|
327
1111
|
version: 1,
|
|
@@ -342,30 +1126,30 @@ async function exportCommand(options) {
|
|
|
342
1126
|
const content = options.pretty ? JSON.stringify(exportData, null, 2) : JSON.stringify(exportData);
|
|
343
1127
|
if (options.format === "jsonl") {
|
|
344
1128
|
const lines = exportData.changes.map((c) => JSON.stringify(c)).join("\n");
|
|
345
|
-
|
|
1129
|
+
writeFileSync3(outputPath, lines, "utf-8");
|
|
346
1130
|
} else {
|
|
347
|
-
|
|
1131
|
+
writeFileSync3(outputPath, content, "utf-8");
|
|
348
1132
|
}
|
|
349
|
-
console.log(
|
|
350
|
-
console.log(
|
|
1133
|
+
console.log(chalk2.green(`\u2713 Exported ${changes.length} changes`));
|
|
1134
|
+
console.log(chalk2.gray(` File size: ${Math.round(content.length / 1024)}KB`));
|
|
351
1135
|
console.log();
|
|
352
1136
|
} catch (error) {
|
|
353
|
-
console.error(
|
|
1137
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
354
1138
|
process.exit(1);
|
|
355
1139
|
}
|
|
356
1140
|
}
|
|
357
1141
|
async function importCommand(options) {
|
|
358
|
-
const
|
|
359
|
-
console.log(
|
|
1142
|
+
const chalk2 = await getChalk();
|
|
1143
|
+
console.log(chalk2.bold("\nxNet Data Import\n"));
|
|
360
1144
|
try {
|
|
361
|
-
const inputPath =
|
|
1145
|
+
const inputPath = resolve5(process.cwd(), options.input);
|
|
362
1146
|
if (!existsSync(inputPath)) {
|
|
363
|
-
console.error(
|
|
1147
|
+
console.error(chalk2.red(`Error: Input file not found: ${inputPath}`));
|
|
364
1148
|
process.exit(1);
|
|
365
1149
|
}
|
|
366
|
-
console.log(
|
|
1150
|
+
console.log(chalk2.gray(`Input: ${inputPath}`));
|
|
367
1151
|
if (options.dryRun) {
|
|
368
|
-
console.log(
|
|
1152
|
+
console.log(chalk2.yellow("\nDRY RUN - No changes will be applied\n"));
|
|
369
1153
|
}
|
|
370
1154
|
const content = readFileSync(inputPath, "utf-8");
|
|
371
1155
|
let importData;
|
|
@@ -380,43 +1164,43 @@ async function importCommand(options) {
|
|
|
380
1164
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
381
1165
|
importData = { changes: lines.map((l) => JSON.parse(l)) };
|
|
382
1166
|
}
|
|
383
|
-
console.log(
|
|
1167
|
+
console.log(chalk2.gray(`Found ${importData.changes.length} changes to import`));
|
|
384
1168
|
console.log();
|
|
385
1169
|
const changes = importData.changes.map((c) => ({
|
|
386
1170
|
...c,
|
|
387
1171
|
signature: typeof c.signature === "string" ? new Uint8Array(Buffer.from(c.signature, "base64")) : c.signature
|
|
388
1172
|
}));
|
|
389
|
-
console.log(
|
|
1173
|
+
console.log(chalk2.bold("Verifying import data..."));
|
|
390
1174
|
const report = await quickIntegrityCheck(changes);
|
|
391
1175
|
if (report.issues.length > 0) {
|
|
392
|
-
console.log(
|
|
1176
|
+
console.log(chalk2.yellow(` \u26A0 ${report.issues.length} integrity issues found`));
|
|
393
1177
|
if (report.summary.errors > 0) {
|
|
394
|
-
console.log(
|
|
1178
|
+
console.log(chalk2.red(` ${report.summary.errors} errors may prevent import`));
|
|
395
1179
|
}
|
|
396
1180
|
} else {
|
|
397
|
-
console.log(
|
|
1181
|
+
console.log(chalk2.green(" \u2713 All changes verified"));
|
|
398
1182
|
}
|
|
399
1183
|
if (options.applyMigrations) {
|
|
400
1184
|
console.log();
|
|
401
|
-
console.log(
|
|
402
|
-
console.log(
|
|
1185
|
+
console.log(chalk2.bold("Checking for required migrations..."));
|
|
1186
|
+
console.log(chalk2.gray(" (Migration detection requires schema registry)"));
|
|
403
1187
|
}
|
|
404
1188
|
console.log();
|
|
405
|
-
console.log(
|
|
1189
|
+
console.log(chalk2.bold("Import summary:"));
|
|
406
1190
|
console.log(` Changes: ${changes.length}`);
|
|
407
1191
|
console.log(` Roots: ${findRoots(changes).length}`);
|
|
408
1192
|
console.log(` Heads: ${findHeads(changes).length}`);
|
|
409
1193
|
console.log(` Chain depth: ${getChainDepth(changes)}`);
|
|
410
1194
|
if (!options.dryRun) {
|
|
411
1195
|
console.log();
|
|
412
|
-
console.log(
|
|
413
|
-
console.log(
|
|
1196
|
+
console.log(chalk2.gray("Note: Writing imported changes requires NodeStore integration."));
|
|
1197
|
+
console.log(chalk2.gray("Changes are validated but not written to storage."));
|
|
414
1198
|
}
|
|
415
1199
|
console.log();
|
|
416
|
-
console.log(
|
|
1200
|
+
console.log(chalk2.green("\u2713 Import validation complete"));
|
|
417
1201
|
console.log();
|
|
418
1202
|
} catch (error) {
|
|
419
|
-
console.error(
|
|
1203
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
420
1204
|
process.exit(1);
|
|
421
1205
|
}
|
|
422
1206
|
}
|
|
@@ -435,13 +1219,90 @@ function registerDoctorCommand(program2) {
|
|
|
435
1219
|
});
|
|
436
1220
|
}
|
|
437
1221
|
|
|
1222
|
+
// src/commands/mcp.ts
|
|
1223
|
+
import {
|
|
1224
|
+
createMCPServer,
|
|
1225
|
+
createMcpHttpServer
|
|
1226
|
+
} from "@xnetjs/plugins/node";
|
|
1227
|
+
var defaultBackendFactory = (options) => createRemoteAgentBackend(options);
|
|
1228
|
+
function buildMcpServer(backend) {
|
|
1229
|
+
return createMCPServer({ store: backend.store, schemas: backend.schemas });
|
|
1230
|
+
}
|
|
1231
|
+
async function startMcpServe(backendFactory, options) {
|
|
1232
|
+
const backend = await backendFactory({
|
|
1233
|
+
...options.apiUrl ? { apiUrl: options.apiUrl } : {}
|
|
1234
|
+
});
|
|
1235
|
+
const server = buildMcpServer(backend);
|
|
1236
|
+
if (options.http) {
|
|
1237
|
+
const http = createMcpHttpServer({
|
|
1238
|
+
server,
|
|
1239
|
+
...options.pairingToken ? { pairingToken: options.pairingToken } : {},
|
|
1240
|
+
...options.allowOrigin && options.allowOrigin.length > 0 ? { allowedOrigins: options.allowOrigin } : {},
|
|
1241
|
+
...options.host ? { host: options.host } : {},
|
|
1242
|
+
...options.port !== void 0 ? { port: options.port } : {}
|
|
1243
|
+
});
|
|
1244
|
+
await http.start();
|
|
1245
|
+
return { mode: "http", server, http, stop: () => http.stop() };
|
|
1246
|
+
}
|
|
1247
|
+
return {
|
|
1248
|
+
mode: "stdio",
|
|
1249
|
+
server,
|
|
1250
|
+
stop: async () => {
|
|
1251
|
+
server.stop();
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function openClawHttpConfigSnippet(handle) {
|
|
1256
|
+
return JSON.stringify(
|
|
1257
|
+
{
|
|
1258
|
+
mcp: {
|
|
1259
|
+
servers: {
|
|
1260
|
+
xnet: {
|
|
1261
|
+
url: `${handle.url}${handle.path}`,
|
|
1262
|
+
transport: "streamable-http",
|
|
1263
|
+
headers: { "x-xnet-pairing": handle.pairingToken }
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
null,
|
|
1269
|
+
2
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
function registerMcpCommand(program2, backendFactory = defaultBackendFactory) {
|
|
1273
|
+
const mcp = program2.command("mcp").description("Expose the workspace to MCP clients");
|
|
1274
|
+
mcp.command("serve").description("Start an MCP server (stdio by default; --http for browser/OpenClaw clients)").option("--http", "Serve over hardened loopback HTTP instead of stdio").option("--host <host>", "Loopback host for --http (default 127.0.0.1)").option("--port <n>", "Port for --http (default 31416)", parseIntOption3).option(
|
|
1275
|
+
"--allow-origin <origin...>",
|
|
1276
|
+
"Browser origins permitted for --http (e.g. https://user.github.io)"
|
|
1277
|
+
).option("--pairing-token <token>", "Shared secret for --http (generated if omitted)").option("--api-url <url>", "xNet local API URL (default http://127.0.0.1:31415)").action(async (options) => {
|
|
1278
|
+
const handle = await startMcpServe(backendFactory, options);
|
|
1279
|
+
if (handle.mode === "http" && handle.http) {
|
|
1280
|
+
console.error(`xNet MCP server listening on ${handle.http.url}${handle.http.path}`);
|
|
1281
|
+
console.error(`pairing token: ${handle.http.pairingToken}`);
|
|
1282
|
+
console.error("OpenClaw config:\n" + openClawHttpConfigSnippet(handle.http));
|
|
1283
|
+
const shutdown = () => {
|
|
1284
|
+
void handle.stop().then(() => process.exit(0));
|
|
1285
|
+
};
|
|
1286
|
+
process.on("SIGINT", shutdown);
|
|
1287
|
+
process.on("SIGTERM", shutdown);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
await handle.server.startStdio();
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
function parseIntOption3(value) {
|
|
1294
|
+
const parsed = Number.parseInt(value, 10);
|
|
1295
|
+
if (!Number.isFinite(parsed)) throw new Error(`Invalid number: ${value}`);
|
|
1296
|
+
return parsed;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
438
1299
|
// src/commands/migrate.ts
|
|
439
|
-
import { writeFileSync as
|
|
440
|
-
import { resolve as
|
|
1300
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
1301
|
+
import { resolve as resolve6 } from "path";
|
|
441
1302
|
async function getChalk2() {
|
|
442
1303
|
try {
|
|
443
|
-
const
|
|
444
|
-
return
|
|
1304
|
+
const chalk2 = await import("chalk");
|
|
1305
|
+
return chalk2.default;
|
|
445
1306
|
} catch {
|
|
446
1307
|
const identity = (s) => s;
|
|
447
1308
|
return {
|
|
@@ -490,7 +1351,7 @@ function findSchema(schemaIRI, schemas) {
|
|
|
490
1351
|
return null;
|
|
491
1352
|
}
|
|
492
1353
|
async function analyzeCommand(options) {
|
|
493
|
-
const
|
|
1354
|
+
const chalk2 = await getChalk2();
|
|
494
1355
|
try {
|
|
495
1356
|
const fromParsed = parseSchemaIRI(options.from);
|
|
496
1357
|
const toParsed = parseSchemaIRI(options.to);
|
|
@@ -501,8 +1362,8 @@ async function analyzeCommand(options) {
|
|
|
501
1362
|
const fromSchema = findSchema(options.from, schemas);
|
|
502
1363
|
const toSchema = findSchema(options.to, schemas);
|
|
503
1364
|
if (!fromSchema || !toSchema) {
|
|
504
|
-
console.log(
|
|
505
|
-
console.log(
|
|
1365
|
+
console.log(chalk2.yellow("\nNote: Schema file not provided. Showing example output.\n"));
|
|
1366
|
+
console.log(chalk2.gray("Use --schema-file to load actual schemas from a JSON file.\n"));
|
|
506
1367
|
const mockFromSchema = {
|
|
507
1368
|
"@id": `xnet://xnet.fyi/${fromParsed.name}@${fromParsed.version}`,
|
|
508
1369
|
"@type": "xnet://xnet.fyi/Schema",
|
|
@@ -531,72 +1392,72 @@ async function analyzeCommand(options) {
|
|
|
531
1392
|
{ "@id": `#priority`, name: "priority", type: "text", required: true }
|
|
532
1393
|
]
|
|
533
1394
|
};
|
|
534
|
-
printAnalysisResult(diffSchemas(mockFromSchema, mockToSchema), options.json,
|
|
1395
|
+
printAnalysisResult(diffSchemas(mockFromSchema, mockToSchema), options.json, chalk2);
|
|
535
1396
|
return;
|
|
536
1397
|
}
|
|
537
1398
|
const diff = diffSchemas(fromSchema, toSchema);
|
|
538
|
-
printAnalysisResult(diff, options.json,
|
|
1399
|
+
printAnalysisResult(diff, options.json, chalk2);
|
|
539
1400
|
} catch (error) {
|
|
540
|
-
console.error(
|
|
1401
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
541
1402
|
process.exit(1);
|
|
542
1403
|
}
|
|
543
1404
|
}
|
|
544
|
-
function printAnalysisResult(diff, json,
|
|
1405
|
+
function printAnalysisResult(diff, json, chalk2) {
|
|
545
1406
|
if (json) {
|
|
546
1407
|
console.log(JSON.stringify(diff, null, 2));
|
|
547
1408
|
return;
|
|
548
1409
|
}
|
|
549
|
-
console.log(
|
|
1410
|
+
console.log(chalk2.bold(`
|
|
550
1411
|
Schema changes: ${diff.fromVersion} \u2192 ${diff.toVersion}
|
|
551
1412
|
`));
|
|
552
1413
|
if (diff.changes.length === 0) {
|
|
553
|
-
console.log(
|
|
1414
|
+
console.log(chalk2.green("No changes detected."));
|
|
554
1415
|
return;
|
|
555
1416
|
}
|
|
556
1417
|
const breaking = diff.changes.filter((c) => c.risk === "breaking");
|
|
557
1418
|
const caution = diff.changes.filter((c) => c.risk === "caution");
|
|
558
1419
|
const safe = diff.changes.filter((c) => c.risk === "safe");
|
|
559
1420
|
if (breaking.length > 0) {
|
|
560
|
-
console.log(
|
|
1421
|
+
console.log(chalk2.red(chalk2.bold("BREAKING CHANGES:")));
|
|
561
1422
|
for (const change of breaking) {
|
|
562
|
-
console.log(
|
|
1423
|
+
console.log(chalk2.red(` - ${change.type.toUpperCase()}: ${change.description}`));
|
|
563
1424
|
}
|
|
564
1425
|
console.log();
|
|
565
1426
|
}
|
|
566
1427
|
if (caution.length > 0) {
|
|
567
|
-
console.log(
|
|
1428
|
+
console.log(chalk2.yellow(chalk2.bold("CAUTION:")));
|
|
568
1429
|
for (const change of caution) {
|
|
569
|
-
console.log(
|
|
1430
|
+
console.log(chalk2.yellow(` - ${change.type.toUpperCase()}: ${change.description}`));
|
|
570
1431
|
}
|
|
571
1432
|
console.log();
|
|
572
1433
|
}
|
|
573
1434
|
if (safe.length > 0) {
|
|
574
|
-
console.log(
|
|
1435
|
+
console.log(chalk2.green(chalk2.bold("SAFE:")));
|
|
575
1436
|
for (const change of safe) {
|
|
576
|
-
console.log(
|
|
1437
|
+
console.log(chalk2.green(` - ${change.type.toUpperCase()}: ${change.description}`));
|
|
577
1438
|
}
|
|
578
1439
|
console.log();
|
|
579
1440
|
}
|
|
580
|
-
console.log(
|
|
1441
|
+
console.log(chalk2.bold("Summary:"));
|
|
581
1442
|
console.log(
|
|
582
|
-
` ${
|
|
1443
|
+
` ${chalk2.red(`${diff.summary.breaking} breaking`)} | ${chalk2.yellow(`${diff.summary.caution} caution`)} | ${chalk2.green(`${diff.summary.safe} safe`)}`
|
|
583
1444
|
);
|
|
584
1445
|
console.log();
|
|
585
1446
|
if (diff.changes.some((c) => c.suggestedLens)) {
|
|
586
|
-
console.log(
|
|
587
|
-
console.log(
|
|
1447
|
+
console.log(chalk2.bold("Suggested lens:"));
|
|
1448
|
+
console.log(chalk2.dim("\u2500".repeat(40)));
|
|
588
1449
|
console.log(generateLensSnippet(diff));
|
|
589
|
-
console.log(
|
|
1450
|
+
console.log(chalk2.dim("\u2500".repeat(40)));
|
|
590
1451
|
console.log();
|
|
591
1452
|
}
|
|
592
1453
|
if (diff.autoMigratable) {
|
|
593
|
-
console.log(
|
|
1454
|
+
console.log(chalk2.green("\u2713 Automatic migration possible"));
|
|
594
1455
|
} else {
|
|
595
|
-
console.log(
|
|
1456
|
+
console.log(chalk2.yellow("\u26A0 Manual intervention required for some changes"));
|
|
596
1457
|
}
|
|
597
1458
|
}
|
|
598
1459
|
async function generateCommand(options) {
|
|
599
|
-
const
|
|
1460
|
+
const chalk2 = await getChalk2();
|
|
600
1461
|
try {
|
|
601
1462
|
const fromParsed = parseSchemaIRI(options.from);
|
|
602
1463
|
const toParsed = parseSchemaIRI(options.to);
|
|
@@ -609,7 +1470,7 @@ async function generateCommand(options) {
|
|
|
609
1470
|
const from = fromSchema ?? createMockSchema(fromParsed.name, fromParsed.version, "from");
|
|
610
1471
|
const to = toSchema ?? createMockSchema(toParsed.name, toParsed.version, "to");
|
|
611
1472
|
if (!fromSchema || !toSchema) {
|
|
612
|
-
console.log(
|
|
1473
|
+
console.log(chalk2.yellow("\nNote: Using mock schemas for demonstration.\n"));
|
|
613
1474
|
}
|
|
614
1475
|
const diff = diffSchemas(from, to);
|
|
615
1476
|
const sourceIRI = from["@id"];
|
|
@@ -622,26 +1483,26 @@ async function generateCommand(options) {
|
|
|
622
1483
|
includeTodos: true
|
|
623
1484
|
});
|
|
624
1485
|
if (options.output) {
|
|
625
|
-
const outputPath =
|
|
1486
|
+
const outputPath = resolve6(process.cwd(), options.output);
|
|
626
1487
|
if (existsSync2(outputPath) && !options.force) {
|
|
627
|
-
console.error(
|
|
628
|
-
console.error(
|
|
1488
|
+
console.error(chalk2.red(`Error: File already exists: ${outputPath}`));
|
|
1489
|
+
console.error(chalk2.gray("Use --force to overwrite."));
|
|
629
1490
|
process.exit(1);
|
|
630
1491
|
}
|
|
631
|
-
|
|
632
|
-
console.log(
|
|
1492
|
+
writeFileSync4(outputPath, generated.code, "utf-8");
|
|
1493
|
+
console.log(chalk2.green(`
|
|
633
1494
|
\u2713 Generated lens written to: ${outputPath}`));
|
|
634
1495
|
} else {
|
|
635
1496
|
console.log(generated.code);
|
|
636
1497
|
}
|
|
637
1498
|
if (!generated.isComplete) {
|
|
638
|
-
console.log(
|
|
1499
|
+
console.log(chalk2.yellow("\n\u26A0 Manual completion required:"));
|
|
639
1500
|
for (const item of generated.manualItems) {
|
|
640
|
-
console.log(
|
|
1501
|
+
console.log(chalk2.yellow(` - ${item}`));
|
|
641
1502
|
}
|
|
642
1503
|
}
|
|
643
1504
|
} catch (error) {
|
|
644
|
-
console.error(
|
|
1505
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
645
1506
|
process.exit(1);
|
|
646
1507
|
}
|
|
647
1508
|
}
|
|
@@ -679,43 +1540,43 @@ function createMockSchema(name, version, type) {
|
|
|
679
1540
|
}
|
|
680
1541
|
}
|
|
681
1542
|
async function runCommand(options) {
|
|
682
|
-
const
|
|
1543
|
+
const chalk2 = await getChalk2();
|
|
683
1544
|
if (!options.dryRun && !options.apply) {
|
|
684
|
-
console.error(
|
|
1545
|
+
console.error(chalk2.red("Error: Must specify either --dry-run or --apply"));
|
|
685
1546
|
process.exit(1);
|
|
686
1547
|
}
|
|
687
1548
|
if (options.dryRun && options.apply) {
|
|
688
|
-
console.error(
|
|
1549
|
+
console.error(chalk2.red("Error: Cannot specify both --dry-run and --apply"));
|
|
689
1550
|
process.exit(1);
|
|
690
1551
|
}
|
|
691
1552
|
try {
|
|
692
|
-
console.log(
|
|
1553
|
+
console.log(chalk2.blue(`
|
|
693
1554
|
Migration: ${options.from} \u2192 ${options.to}
|
|
694
1555
|
`));
|
|
695
1556
|
if (options.dryRun) {
|
|
696
|
-
console.log(
|
|
697
|
-
console.log(
|
|
698
|
-
console.log(
|
|
699
|
-
console.log(
|
|
700
|
-
console.log(
|
|
1557
|
+
console.log(chalk2.yellow("DRY RUN - No changes will be applied\n"));
|
|
1558
|
+
console.log(chalk2.gray("Would migrate nodes:"));
|
|
1559
|
+
console.log(chalk2.gray(" - Scan data directory for nodes with schema " + options.from));
|
|
1560
|
+
console.log(chalk2.gray(" - Apply migration lens to each node"));
|
|
1561
|
+
console.log(chalk2.gray(" - Update schema version to " + options.to));
|
|
701
1562
|
console.log();
|
|
702
|
-
console.log(
|
|
703
|
-
console.log(
|
|
1563
|
+
console.log(chalk2.dim("Note: Full migration requires --data-dir and --lens-file options"));
|
|
1564
|
+
console.log(chalk2.dim(" or a configured NodeStore with registered lenses."));
|
|
704
1565
|
} else {
|
|
705
|
-
console.log(
|
|
706
|
-
console.log(
|
|
707
|
-
console.log(
|
|
708
|
-
console.log(
|
|
709
|
-
console.log(
|
|
1566
|
+
console.log(chalk2.yellow("APPLY MODE - Changes will be written\n"));
|
|
1567
|
+
console.log(chalk2.gray("Would apply migration:"));
|
|
1568
|
+
console.log(chalk2.gray(" - Load nodes with schema " + options.from));
|
|
1569
|
+
console.log(chalk2.gray(" - Transform using migration lens"));
|
|
1570
|
+
console.log(chalk2.gray(" - Write updated nodes with schema " + options.to));
|
|
710
1571
|
console.log();
|
|
711
|
-
console.log(
|
|
712
|
-
console.log(
|
|
1572
|
+
console.log(chalk2.dim("Note: Full migration requires --data-dir and --lens-file options"));
|
|
1573
|
+
console.log(chalk2.dim(" or a configured NodeStore with registered lenses."));
|
|
713
1574
|
}
|
|
714
1575
|
console.log();
|
|
715
|
-
console.log(
|
|
716
|
-
console.log(
|
|
1576
|
+
console.log(chalk2.green("\u2713 Migration command parsed successfully"));
|
|
1577
|
+
console.log(chalk2.gray(" Full implementation requires NodeStore integration."));
|
|
717
1578
|
} catch (error) {
|
|
718
|
-
console.error(
|
|
1579
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
719
1580
|
process.exit(1);
|
|
720
1581
|
}
|
|
721
1582
|
}
|
|
@@ -733,7 +1594,7 @@ function registerMigrateCommand(program2) {
|
|
|
733
1594
|
}
|
|
734
1595
|
|
|
735
1596
|
// src/commands/schema.ts
|
|
736
|
-
import { writeFileSync as
|
|
1597
|
+
import { writeFileSync as writeFileSync5, readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
737
1598
|
function extractSchemas() {
|
|
738
1599
|
const schemaExportPath = process.env.XNET_SCHEMA_EXPORT || "./schemas-export.json";
|
|
739
1600
|
if (existsSync3(schemaExportPath)) {
|
|
@@ -758,7 +1619,7 @@ function registerSchemaCommand(program2) {
|
|
|
758
1619
|
schemas
|
|
759
1620
|
};
|
|
760
1621
|
const json = options.pretty ? JSON.stringify(output, null, 2) : JSON.stringify(output);
|
|
761
|
-
|
|
1622
|
+
writeFileSync5(options.output, json, "utf-8");
|
|
762
1623
|
console.log(`Extracted ${schemas.length} schemas to ${options.output}`);
|
|
763
1624
|
});
|
|
764
1625
|
schema.command("diff").description("Compare two schema files and report changes").argument("<old-file>", "Path to the old schemas JSON file").argument("<new-file>", "Path to the new schemas JSON file").option("-o, --output <file>", "Output diff to JSON file").option("--fail-on-breaking", "Exit with code 1 if breaking changes found", false).option("--quiet", "Only output JSON, no console messages", false).action(
|
|
@@ -850,7 +1711,7 @@ function registerSchemaCommand(program2) {
|
|
|
850
1711
|
}
|
|
851
1712
|
};
|
|
852
1713
|
if (options.output) {
|
|
853
|
-
|
|
1714
|
+
writeFileSync5(options.output, JSON.stringify(output, null, 2), "utf-8");
|
|
854
1715
|
}
|
|
855
1716
|
if (!options.quiet) {
|
|
856
1717
|
if (diffs.length === 0) {
|
|
@@ -887,4 +1748,11 @@ program.name("xnet").description("xNet CLI - Schema migrations, diagnostics, and
|
|
|
887
1748
|
registerMigrateCommand(program);
|
|
888
1749
|
registerSchemaCommand(program);
|
|
889
1750
|
registerDoctorCommand(program);
|
|
1751
|
+
registerAgentCommands(program);
|
|
1752
|
+
registerMcpCommand(program);
|
|
1753
|
+
registerBridgeCommand(program);
|
|
1754
|
+
registerCodeCommand(program);
|
|
1755
|
+
registerDataCommand(program);
|
|
1756
|
+
registerPluginCommand(program);
|
|
1757
|
+
registerConnectorCommand(program);
|
|
890
1758
|
program.parse();
|