agentwheel 0.2.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/index.js +1402 -0
  4. package/package.json +57 -0
package/dist/index.js ADDED
@@ -0,0 +1,1402 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { mkdir as mkdir6, rm as rm6, writeFile as writeFile3 } from "fs/promises";
5
+ import { join as join12 } from "path";
6
+ import { Command } from "commander";
7
+
8
+ // src/adapters/claude.ts
9
+ var claudeAdapter = {
10
+ name: "claude",
11
+ displayName: "Claude",
12
+ targets: {}
13
+ };
14
+
15
+ // src/adapters/copilot.ts
16
+ var copilotAdapter = {
17
+ name: "copilot",
18
+ displayName: "GitHub Copilot",
19
+ targets: {
20
+ instructions: { enabled: true, dest: ".github/copilot-instructions.md" },
21
+ rules: { enabled: true, dest: ".github/instructions" },
22
+ commands: { enabled: true, dest: ".github/prompts" },
23
+ // GitHub Copilot has no native SKILL.md runtime surface; keep skills disabled
24
+ // until a conversion target is introduced.
25
+ skills: { enabled: false, dest: ".github/skills" }
26
+ }
27
+ };
28
+
29
+ // src/adapters/codex.ts
30
+ var codexAdapter = {
31
+ name: "codex",
32
+ displayName: "Codex",
33
+ targets: {}
34
+ };
35
+
36
+ // src/adapters/hermes.ts
37
+ var hermesAdapter = {
38
+ name: "hermes",
39
+ displayName: "Hermes",
40
+ targets: {
41
+ instructions: { enabled: true, dest: ".hermes/AGENTS.md" },
42
+ rules: { enabled: true, dest: ".hermes/rules" },
43
+ skills: { enabled: true, dest: ".hermes/skills" },
44
+ commands: { enabled: true, dest: ".hermes/commands" },
45
+ mcp: { enabled: true, dest: ".hermes/mcp" },
46
+ hooks: { enabled: true, dest: ".hermes/hooks" }
47
+ }
48
+ };
49
+
50
+ // src/adapters/openclaw.ts
51
+ var openClawAdapter = {
52
+ name: "openclaw",
53
+ displayName: "OpenClaw",
54
+ targets: {
55
+ instructions: { enabled: true, dest: ".openclaw/AGENTS.md" },
56
+ rules: { enabled: true, dest: ".openclaw/rules" },
57
+ skills: { enabled: true, dest: ".openclaw/skills" },
58
+ commands: { enabled: true, dest: ".openclaw/commands" },
59
+ mcp: { enabled: true, dest: ".openclaw/mcp" },
60
+ hooks: { enabled: true, dest: ".openclaw/hooks" },
61
+ plugins: { enabled: true, dest: ".openclaw/plugins", semantic: "openclaw-plugin" }
62
+ }
63
+ };
64
+
65
+ // src/adapters/index.ts
66
+ var adapters = [openClawAdapter, claudeAdapter, codexAdapter, hermesAdapter, copilotAdapter];
67
+ function getAdapter(name) {
68
+ const adapter = adapters.find((candidate) => candidate.name === name);
69
+ if (!adapter) {
70
+ throw new Error(`Unknown adapter: ${name}`);
71
+ }
72
+ return adapter;
73
+ }
74
+
75
+ // src/model/adapter.ts
76
+ import { readFile } from "fs/promises";
77
+ import { parse, printParseErrorCode } from "jsonc-parser";
78
+ import { z as z2 } from "zod";
79
+
80
+ // src/model/artifact.ts
81
+ import { z } from "zod";
82
+ var artifactTypeSchema = z.enum([
83
+ "instructions",
84
+ "rules",
85
+ "skills",
86
+ "commands",
87
+ "subagents",
88
+ "mcp",
89
+ "hooks",
90
+ "plugins"
91
+ ]);
92
+ var fileKindSchema = z.enum(["file", "dir"]);
93
+ var artifactSchema = z.object({
94
+ type: artifactTypeSchema,
95
+ name: z.string().min(1),
96
+ sourcePath: z.string().min(1),
97
+ stagedPath: z.string().min(1).optional(),
98
+ relativePath: z.string().min(1),
99
+ kind: fileKindSchema,
100
+ hash: z.string().min(16),
101
+ packageName: z.string().min(1).optional(),
102
+ channel: z.enum(["managed", "overlay", "addition", "override", "ejected"]).default("managed")
103
+ });
104
+
105
+ // src/model/adapter.ts
106
+ var targetMappingSchema = z2.object({
107
+ dest: z2.string().min(1),
108
+ enabled: z2.boolean().default(true),
109
+ semantic: z2.enum(["openclaw-plugin"]).optional()
110
+ });
111
+ var adapterSchema = z2.object({
112
+ name: z2.string().min(1),
113
+ displayName: z2.string().min(1).optional(),
114
+ targets: z2.partialRecord(
115
+ artifactTypeSchema,
116
+ targetMappingSchema
117
+ ).default({})
118
+ });
119
+ async function loadAdapterConfig(path) {
120
+ const content = await readFile(path, "utf8");
121
+ const errors = [];
122
+ const parsed = parse(content, errors, {
123
+ allowTrailingComma: true,
124
+ disallowComments: false
125
+ });
126
+ if (errors.length > 0) {
127
+ const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
128
+ throw new Error(`Invalid adapter config ${path}: ${details}`);
129
+ }
130
+ return adapterSchema.parse(parsed);
131
+ }
132
+
133
+ // src/install/apply.ts
134
+ import { execFile } from "child_process";
135
+ import { rm as rm3 } from "fs/promises";
136
+ import { promisify } from "util";
137
+
138
+ // src/utils/fs.ts
139
+ import { createHash } from "crypto";
140
+ import {
141
+ copyFile,
142
+ cp,
143
+ mkdir,
144
+ readdir,
145
+ readFile as readFile2,
146
+ rename,
147
+ rm,
148
+ stat,
149
+ writeFile
150
+ } from "fs/promises";
151
+ import { dirname, join, relative } from "path";
152
+ async function pathExists(path) {
153
+ try {
154
+ await stat(path);
155
+ return true;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+ async function hashPath(path) {
161
+ const stats = await stat(path);
162
+ if (stats.isFile()) {
163
+ const content = await readFile2(path);
164
+ return createHash("sha256").update("file\0").update(content).digest("hex");
165
+ }
166
+ if (!stats.isDirectory()) {
167
+ throw new Error(`Unsupported path kind: ${path}`);
168
+ }
169
+ const hash = createHash("sha256").update("dir\0");
170
+ const files = await listFiles(path);
171
+ for (const file of files) {
172
+ hash.update(relative(path, file).replaceAll("\\", "/")).update("\0");
173
+ hash.update(await hashPath(file)).update("\0");
174
+ }
175
+ return hash.digest("hex");
176
+ }
177
+ async function listFiles(root) {
178
+ const out = [];
179
+ async function walk(dir) {
180
+ const entries = await readdir(dir, { withFileTypes: true });
181
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
182
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
183
+ const full = join(dir, entry.name);
184
+ if (entry.isDirectory()) {
185
+ await walk(full);
186
+ } else if (entry.isFile()) {
187
+ out.push(full);
188
+ }
189
+ }
190
+ }
191
+ await walk(root);
192
+ return out;
193
+ }
194
+ async function atomicCopy(source, dest, kind) {
195
+ await mkdir(dirname(dest), { recursive: true });
196
+ const temp = `${dest}.agentwheel-tmp-${process.pid}-${Date.now()}`;
197
+ await rm(temp, { recursive: true, force: true });
198
+ if (kind === "file") {
199
+ await copyFile(source, temp);
200
+ } else {
201
+ await cp(source, temp, { recursive: true, dereference: true });
202
+ }
203
+ await rm(dest, { recursive: true, force: true });
204
+ await rename(temp, dest);
205
+ }
206
+ async function writeJsonAtomic(path, data) {
207
+ await mkdir(dirname(path), { recursive: true });
208
+ const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
209
+ await writeFile(temp, `${JSON.stringify(data, null, 2)}
210
+ `, "utf8");
211
+ await rename(temp, path);
212
+ }
213
+
214
+ // src/install/manifest.ts
215
+ import { readFile as readFile3, rm as rm2 } from "fs/promises";
216
+ import { resolve } from "path";
217
+
218
+ // src/model/manifest.ts
219
+ import { z as z3 } from "zod";
220
+ var manifestEntrySchema = z3.object({
221
+ path: z3.string().min(1),
222
+ artifactType: artifactTypeSchema,
223
+ artifactName: z3.string().min(1),
224
+ kind: fileKindSchema,
225
+ hash: z3.string().min(16),
226
+ sourceHash: z3.string().min(16),
227
+ updatedAt: z3.string().datetime(),
228
+ channel: z3.enum(["managed", "overlay", "addition", "override", "ejected"]).default("managed"),
229
+ packageName: z3.string().min(1).optional(),
230
+ semanticCommand: z3.array(z3.string()).optional(),
231
+ executed: z3.boolean().optional()
232
+ });
233
+ var installManifestSchema = z3.object({
234
+ version: z3.literal(1),
235
+ adapter: z3.string().min(1),
236
+ targetRoot: z3.string().min(1),
237
+ generatedAt: z3.string().datetime(),
238
+ entries: z3.array(manifestEntrySchema)
239
+ });
240
+ var sourceLockSchema = z3.object({
241
+ version: z3.literal(1),
242
+ driver: z3.string().min(1),
243
+ source: z3.string().min(1),
244
+ resolvedPath: z3.string().min(1),
245
+ packageName: z3.string().min(1).optional(),
246
+ packageVersion: z3.string().min(1).optional(),
247
+ mode: z3.enum(["pinned", "tracking"]).default("pinned"),
248
+ requestedRef: z3.string().min(1).optional(),
249
+ resolvedCommit: z3.string().min(1).optional(),
250
+ sourceHash: z3.string().min(16).optional(),
251
+ generatedAt: z3.string().datetime(),
252
+ artifacts: z3.array(
253
+ z3.object({
254
+ type: artifactTypeSchema,
255
+ name: z3.string().min(1),
256
+ relativePath: z3.string().min(1),
257
+ kind: fileKindSchema,
258
+ hash: z3.string().min(16)
259
+ })
260
+ )
261
+ });
262
+
263
+ // src/install/paths.ts
264
+ import { join as join2 } from "path";
265
+ function metadataDir(targetRoot) {
266
+ return join2(targetRoot, ".agentwheel");
267
+ }
268
+ function installManifestPath(targetRoot, adapter) {
269
+ return join2(metadataDir(targetRoot), `${adapter}.install-manifest.json`);
270
+ }
271
+ function sourceLockPath(targetRoot, adapter) {
272
+ return join2(metadataDir(targetRoot), `${adapter}.source-lock.json`);
273
+ }
274
+
275
+ // src/install/manifest.ts
276
+ async function readInstallManifest(targetRoot, adapter) {
277
+ const path = installManifestPath(targetRoot, adapter);
278
+ if (!await pathExists(path)) return void 0;
279
+ return installManifestSchema.parse(JSON.parse(await readFile3(path, "utf8")));
280
+ }
281
+ async function writeInstallManifest(manifest) {
282
+ await writeJsonAtomic(installManifestPath(manifest.targetRoot, manifest.adapter), manifest);
283
+ }
284
+ async function writeSourceLock(targetRoot, adapter, lock) {
285
+ await writeJsonAtomic(sourceLockPath(targetRoot, adapter), lock);
286
+ }
287
+ async function readSourceLock(targetRoot, adapter) {
288
+ const path = sourceLockPath(targetRoot, adapter);
289
+ if (!await pathExists(path)) return void 0;
290
+ return sourceLockSchema.parse(JSON.parse(await readFile3(path, "utf8")));
291
+ }
292
+ async function removeStateFiles(targetRoot, adapter) {
293
+ await rm2(installManifestPath(targetRoot, adapter), { force: true });
294
+ await rm2(sourceLockPath(targetRoot, adapter), { force: true });
295
+ }
296
+ function normalizeTargetRoot(path) {
297
+ return resolve(path);
298
+ }
299
+
300
+ // src/install/apply.ts
301
+ var execFileAsync = promisify(execFile);
302
+ async function applyInstallPlan(plan, sourceLock, options = {}) {
303
+ if (plan.hasBlockingChanges) {
304
+ const blockers = plan.operations.filter((operation) => operation.action === "drift" || operation.action === "conflict");
305
+ throw new Error(`Refusing to apply with blocking changes: ${blockers.map((item) => item.relativeDestPath).join(", ")}`);
306
+ }
307
+ const entries = [];
308
+ const now = (/* @__PURE__ */ new Date()).toISOString();
309
+ for (const operation of plan.operations) {
310
+ if (operation.action === "plugin") {
311
+ if (!operation.desiredHash) {
312
+ throw new Error(`Invalid plugin operation missing hash: ${operation.relativeDestPath}`);
313
+ }
314
+ if (options.executePlugins) {
315
+ if (!operation.semanticCommand || operation.semanticCommand.length === 0) {
316
+ throw new Error(`Invalid plugin operation missing command: ${operation.relativeDestPath}`);
317
+ }
318
+ const command = operation.semanticCommand[0];
319
+ const args = operation.semanticCommand.slice(1);
320
+ if (!command) {
321
+ throw new Error(`Invalid plugin operation missing command: ${operation.relativeDestPath}`);
322
+ }
323
+ await execFileAsync(command, args);
324
+ }
325
+ entries.push({
326
+ path: operation.relativeDestPath,
327
+ artifactType: operation.artifactType,
328
+ artifactName: operation.artifactName,
329
+ kind: operation.kind,
330
+ hash: operation.desiredHash,
331
+ sourceHash: operation.desiredHash,
332
+ updatedAt: now,
333
+ channel: operation.channel,
334
+ packageName: operation.packageName,
335
+ semanticCommand: operation.semanticCommand,
336
+ executed: options.executePlugins === true
337
+ });
338
+ } else if (operation.action === "create" || operation.action === "update") {
339
+ if (!operation.sourcePath || !operation.desiredHash) {
340
+ throw new Error(`Invalid operation missing source/hash: ${operation.relativeDestPath}`);
341
+ }
342
+ await atomicCopy(operation.sourcePath, operation.destPath, operation.kind);
343
+ entries.push({
344
+ path: operation.relativeDestPath,
345
+ artifactType: operation.artifactType,
346
+ artifactName: operation.artifactName,
347
+ kind: operation.kind,
348
+ hash: await hashPath(operation.destPath),
349
+ sourceHash: operation.desiredHash,
350
+ updatedAt: now,
351
+ channel: operation.channel,
352
+ packageName: operation.packageName,
353
+ semanticCommand: operation.semanticCommand
354
+ });
355
+ } else if (operation.action === "skip") {
356
+ if (!operation.desiredHash) {
357
+ throw new Error(`Invalid skip operation missing hash: ${operation.relativeDestPath}`);
358
+ }
359
+ entries.push({
360
+ path: operation.relativeDestPath,
361
+ artifactType: operation.artifactType,
362
+ artifactName: operation.artifactName,
363
+ kind: operation.kind,
364
+ hash: operation.desiredHash,
365
+ sourceHash: operation.desiredHash,
366
+ updatedAt: now,
367
+ channel: operation.channel,
368
+ packageName: operation.packageName,
369
+ semanticCommand: operation.semanticCommand
370
+ });
371
+ } else if (operation.action === "remove") {
372
+ await rm3(operation.destPath, { recursive: true, force: true });
373
+ }
374
+ }
375
+ const manifest = {
376
+ version: 1,
377
+ adapter: plan.adapter,
378
+ targetRoot: plan.targetRoot,
379
+ generatedAt: now,
380
+ entries: entries.sort((a, b) => a.path.localeCompare(b.path))
381
+ };
382
+ await writeInstallManifest(manifest);
383
+ await writeSourceLock(plan.targetRoot, plan.adapter, sourceLock);
384
+ return manifest;
385
+ }
386
+ async function uninstall(plan, dryRun) {
387
+ if (plan.hasBlockingChanges) {
388
+ const blockers = plan.operations.filter((operation) => operation.action === "drift" || operation.action === "conflict");
389
+ throw new Error(`Refusing to uninstall with drift: ${blockers.map((item) => item.relativeDestPath).join(", ")}`);
390
+ }
391
+ if (dryRun) return;
392
+ for (const operation of plan.operations) {
393
+ await rm3(operation.destPath, { recursive: true, force: true });
394
+ }
395
+ await removeStateFiles(plan.targetRoot, plan.adapter);
396
+ }
397
+
398
+ // src/install/plan.ts
399
+ import { join as join3, relative as relative2 } from "path";
400
+
401
+ // src/targets/plugins/openclaw.ts
402
+ function openClawPluginInstallCommand(request) {
403
+ return ["openclaw", "plugins", "install", "--link", request.path];
404
+ }
405
+
406
+ // src/install/plan.ts
407
+ async function createInstallPlan(bundle, adapter, targetRoot, manifest) {
408
+ const desired = /* @__PURE__ */ new Map();
409
+ for (const artifact of bundle.artifacts) {
410
+ const op = operationForArtifact(artifact, adapter, targetRoot);
411
+ if (op) {
412
+ desired.set(op.relativeDestPath, op);
413
+ }
414
+ }
415
+ const manifestByPath = new Map((manifest?.entries ?? []).map((entry) => [entry.path, entry]));
416
+ const operations = [];
417
+ for (const op of desired.values()) {
418
+ if (op.action === "plugin") {
419
+ const existing2 = manifestByPath.get(op.relativeDestPath);
420
+ if (existing2 && existing2.hash === op.desiredHash) {
421
+ operations.push({ ...op, action: "skip", manifestHash: existing2.hash, reason: "plugin already planned" });
422
+ } else {
423
+ operations.push(op);
424
+ }
425
+ continue;
426
+ }
427
+ const existing = manifestByPath.get(op.relativeDestPath);
428
+ const exists = await pathExists(op.destPath);
429
+ if (!exists) {
430
+ operations.push({ ...op, action: "create", reason: "destination missing" });
431
+ continue;
432
+ }
433
+ const currentHash = await hashPath(op.destPath);
434
+ if (!existing) {
435
+ operations.push({ ...op, action: "conflict", currentHash, reason: "destination exists but is not managed" });
436
+ continue;
437
+ }
438
+ if (currentHash !== existing.hash) {
439
+ operations.push({
440
+ ...op,
441
+ action: "drift",
442
+ currentHash,
443
+ manifestHash: existing.hash,
444
+ reason: "managed destination changed outside agentwheel"
445
+ });
446
+ continue;
447
+ }
448
+ if (currentHash === op.desiredHash) {
449
+ operations.push({ ...op, action: "skip", currentHash, manifestHash: existing.hash, reason: "already up to date" });
450
+ } else {
451
+ operations.push({ ...op, action: "update", currentHash, manifestHash: existing.hash, reason: "source changed" });
452
+ }
453
+ }
454
+ for (const entry of manifest?.entries ?? []) {
455
+ if (desired.has(entry.path)) continue;
456
+ const destPath = join3(targetRoot, entry.path);
457
+ if (!await pathExists(destPath)) continue;
458
+ const currentHash = await hashPath(destPath);
459
+ if (currentHash !== entry.hash) {
460
+ operations.push({
461
+ action: "drift",
462
+ artifactType: entry.artifactType,
463
+ artifactName: entry.artifactName,
464
+ kind: entry.kind,
465
+ destPath,
466
+ relativeDestPath: entry.path,
467
+ currentHash,
468
+ manifestHash: entry.hash,
469
+ reason: "managed stale destination changed outside agentwheel",
470
+ channel: entry.channel,
471
+ packageName: entry.packageName
472
+ });
473
+ } else {
474
+ operations.push({
475
+ action: "remove",
476
+ artifactType: entry.artifactType,
477
+ artifactName: entry.artifactName,
478
+ kind: entry.kind,
479
+ destPath,
480
+ relativeDestPath: entry.path,
481
+ currentHash,
482
+ manifestHash: entry.hash,
483
+ reason: "artifact removed from source",
484
+ channel: entry.channel,
485
+ packageName: entry.packageName
486
+ });
487
+ }
488
+ }
489
+ operations.sort((a, b) => a.relativeDestPath.localeCompare(b.relativeDestPath));
490
+ return {
491
+ adapter: adapter.name,
492
+ targetRoot,
493
+ operations,
494
+ hasBlockingChanges: operations.some((op) => op.action === "drift" || op.action === "conflict")
495
+ };
496
+ }
497
+ function operationForArtifact(artifact, adapter, targetRoot) {
498
+ const target = adapter.targets[artifact.type];
499
+ if (!target?.enabled) return void 0;
500
+ if (artifact.type === "plugins" && target.semantic === "openclaw-plugin") {
501
+ const sourcePath = artifact.stagedPath ?? artifact.sourcePath;
502
+ return {
503
+ action: "plugin",
504
+ artifactType: artifact.type,
505
+ artifactName: artifact.name,
506
+ kind: artifact.kind,
507
+ sourcePath,
508
+ destPath: targetRoot,
509
+ relativeDestPath: `plugins/${artifact.name}`,
510
+ desiredHash: artifact.hash,
511
+ reason: "semantic plugin install planned",
512
+ channel: artifact.channel ?? "managed",
513
+ packageName: artifact.packageName,
514
+ semanticCommand: openClawPluginInstallCommand({ path: sourcePath, dryRun: true })
515
+ };
516
+ }
517
+ const destPath = artifact.type === "instructions" ? join3(targetRoot, target.dest) : join3(targetRoot, target.dest, artifact.name);
518
+ return {
519
+ action: "create",
520
+ artifactType: artifact.type,
521
+ artifactName: artifact.name,
522
+ kind: artifact.kind,
523
+ sourcePath: artifact.stagedPath ?? artifact.sourcePath,
524
+ destPath,
525
+ relativeDestPath: relative2(targetRoot, destPath).replaceAll("\\", "/"),
526
+ desiredHash: artifact.hash,
527
+ reason: "destination missing",
528
+ channel: artifact.channel ?? "managed",
529
+ packageName: artifact.packageName
530
+ };
531
+ }
532
+ function summarizePlan(plan) {
533
+ const summary = {
534
+ create: 0,
535
+ update: 0,
536
+ skip: 0,
537
+ remove: 0,
538
+ drift: 0,
539
+ conflict: 0,
540
+ plugin: 0
541
+ };
542
+ for (const operation of plan.operations) {
543
+ summary[operation.action]++;
544
+ }
545
+ return summary;
546
+ }
547
+
548
+ // src/install/uninstall.ts
549
+ import { join as join4 } from "path";
550
+ async function createUninstallPlan(manifest) {
551
+ const operations = [];
552
+ for (const entry of manifest.entries) {
553
+ const destPath = join4(manifest.targetRoot, entry.path);
554
+ if (!await pathExists(destPath)) continue;
555
+ const currentHash = await hashPath(destPath);
556
+ if (currentHash !== entry.hash) {
557
+ operations.push({
558
+ action: "drift",
559
+ artifactType: entry.artifactType,
560
+ artifactName: entry.artifactName,
561
+ kind: entry.kind,
562
+ destPath,
563
+ relativeDestPath: entry.path,
564
+ currentHash,
565
+ manifestHash: entry.hash,
566
+ reason: "managed destination changed outside agentwheel",
567
+ channel: entry.channel,
568
+ packageName: entry.packageName
569
+ });
570
+ } else {
571
+ operations.push({
572
+ action: "remove",
573
+ artifactType: entry.artifactType,
574
+ artifactName: entry.artifactName,
575
+ kind: entry.kind,
576
+ destPath,
577
+ relativeDestPath: entry.path,
578
+ currentHash,
579
+ manifestHash: entry.hash,
580
+ reason: "uninstall managed artifact",
581
+ channel: entry.channel,
582
+ packageName: entry.packageName
583
+ });
584
+ }
585
+ }
586
+ operations.sort((a, b) => a.relativeDestPath.localeCompare(b.relativeDestPath));
587
+ return {
588
+ adapter: manifest.adapter,
589
+ targetRoot: manifest.targetRoot,
590
+ operations,
591
+ hasBlockingChanges: operations.some((operation) => operation.action === "drift" || operation.action === "conflict")
592
+ };
593
+ }
594
+
595
+ // src/cli/format.ts
596
+ var labels = {
597
+ create: "CREATE",
598
+ update: "UPDATE",
599
+ skip: "SKIP",
600
+ remove: "REMOVE",
601
+ drift: "DRIFT",
602
+ conflict: "CONFLICT",
603
+ plugin: "PLUGIN"
604
+ };
605
+ var channelLabels = {
606
+ managed: "MANAGED",
607
+ overlay: "OVERLAY",
608
+ addition: "ADDITION",
609
+ override: "OVERRIDE",
610
+ ejected: "EJECTED"
611
+ };
612
+ function formatPlan(plan) {
613
+ const lines = [`Plan for ${plan.adapter} at ${plan.targetRoot}`];
614
+ for (const operation of plan.operations) {
615
+ const source = operation.sourcePath ? `${operation.sourcePath} -> ` : "";
616
+ const command = operation.semanticCommand ? ` :: ${operation.semanticCommand.join(" ")}` : "";
617
+ lines.push(`${labels[operation.action].padEnd(8)} ${channelLabels[operation.channel].padEnd(8)} ${operation.artifactType}/${operation.artifactName} ${source}${operation.relativeDestPath} (${operation.reason})${command}`);
618
+ }
619
+ const summary = summarizePlan(plan);
620
+ lines.push(
621
+ `Summary: create ${summary.create}, update ${summary.update}, skip ${summary.skip}, remove ${summary.remove}, drift ${summary.drift}, conflict ${summary.conflict}, plugin ${summary.plugin}`
622
+ );
623
+ return lines.join("\n");
624
+ }
625
+
626
+ // src/source/git.ts
627
+ import { execFile as execFile2 } from "child_process";
628
+ import { mkdir as mkdir2, rm as rm4 } from "fs/promises";
629
+ import { homedir } from "os";
630
+ import { basename as basename2, join as join7, resolve as resolve3 } from "path";
631
+ import { promisify as promisify2 } from "util";
632
+
633
+ // src/model/package.ts
634
+ import { readFile as readFile4 } from "fs/promises";
635
+ import { join as join5 } from "path";
636
+ import { parse as parse2, printParseErrorCode as printParseErrorCode2 } from "jsonc-parser";
637
+ import { z as z4 } from "zod";
638
+ var packageProvideSchema = z4.object({
639
+ type: artifactTypeSchema,
640
+ path: z4.string().min(1)
641
+ });
642
+ var packageManifestSchema = z4.object({
643
+ schemaVersion: z4.literal(1),
644
+ name: z4.string().min(1),
645
+ version: z4.string().min(1),
646
+ provides: z4.array(packageProvideSchema).min(1)
647
+ });
648
+ async function findPackageManifestPath(root) {
649
+ for (const name of ["agentwheel.json", "agentwheel.jsonc"]) {
650
+ const candidate = join5(root, name);
651
+ if (await pathExists(candidate)) return candidate;
652
+ }
653
+ return void 0;
654
+ }
655
+ async function readPackageManifest(root) {
656
+ const path = await findPackageManifestPath(root);
657
+ if (!path) return void 0;
658
+ const content = await readFile4(path, "utf8");
659
+ const errors = [];
660
+ const parsed = parse2(content, errors, { allowTrailingComma: true, disallowComments: false });
661
+ if (errors.length > 0) {
662
+ const details = errors.map((error) => `${printParseErrorCode2(error.error)} at offset ${error.offset}`).join(", ");
663
+ throw new Error(`Invalid package manifest ${path}: ${details}`);
664
+ }
665
+ return packageManifestSchema.parse(parsed);
666
+ }
667
+
668
+ // src/source/local.ts
669
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
670
+ import { basename, join as join6, resolve as resolve2 } from "path";
671
+ var LocalSourceDriver = class {
672
+ name = "local";
673
+ async resolve(source) {
674
+ const resolvedPath = resolve2(source);
675
+ if (!await pathExists(resolvedPath)) {
676
+ throw new Error(`Local source not found: ${resolvedPath}`);
677
+ }
678
+ const stats = await stat2(resolvedPath);
679
+ if (!stats.isDirectory()) {
680
+ throw new Error(`Local source must be a directory: ${resolvedPath}`);
681
+ }
682
+ const manifest = await readPackageManifest(resolvedPath);
683
+ return {
684
+ driver: this.name,
685
+ source,
686
+ resolvedPath,
687
+ packageName: manifest?.name,
688
+ packageVersion: manifest?.version,
689
+ mode: "pinned",
690
+ sourceHash: await hashPath(resolvedPath)
691
+ };
692
+ }
693
+ async list(resolved) {
694
+ const manifest = await readPackageManifest(resolved.resolvedPath);
695
+ if (manifest) {
696
+ return listFromManifest(resolved.resolvedPath, manifest.name);
697
+ }
698
+ const artifacts = [];
699
+ const root = resolved.resolvedPath;
700
+ const instructions = await firstExisting([join6(root, "instructions.md"), join6(root, "AGENTS.md")]);
701
+ if (instructions) {
702
+ artifacts.push({
703
+ type: "instructions",
704
+ name: basename(instructions),
705
+ sourcePath: instructions,
706
+ relativePath: basename(instructions),
707
+ kind: "file",
708
+ hash: await hashPath(instructions),
709
+ packageName: resolved.packageName,
710
+ channel: "managed"
711
+ });
712
+ }
713
+ const rulesDir = join6(root, "rules");
714
+ if (await pathExists(rulesDir)) {
715
+ for (const entry of await sortedDirEntries(rulesDir)) {
716
+ const full = join6(rulesDir, entry.name);
717
+ if (entry.isFile()) {
718
+ artifacts.push({
719
+ type: "rules",
720
+ name: entry.name,
721
+ sourcePath: full,
722
+ relativePath: join6("rules", entry.name),
723
+ kind: "file",
724
+ hash: await hashPath(full),
725
+ packageName: resolved.packageName,
726
+ channel: "managed"
727
+ });
728
+ }
729
+ }
730
+ }
731
+ const skillsDir = join6(root, "skills");
732
+ if (await pathExists(skillsDir)) {
733
+ for (const entry of await sortedDirEntries(skillsDir)) {
734
+ const full = join6(skillsDir, entry.name);
735
+ if (entry.isDirectory()) {
736
+ artifacts.push({
737
+ type: "skills",
738
+ name: entry.name,
739
+ sourcePath: full,
740
+ relativePath: join6("skills", entry.name),
741
+ kind: "dir",
742
+ hash: await hashPath(full),
743
+ packageName: resolved.packageName,
744
+ channel: "managed"
745
+ });
746
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
747
+ artifacts.push({
748
+ type: "skills",
749
+ name: entry.name.replace(/\.md$/, ""),
750
+ sourcePath: full,
751
+ relativePath: join6("skills", entry.name),
752
+ kind: "file",
753
+ hash: await hashPath(full),
754
+ packageName: resolved.packageName,
755
+ channel: "managed"
756
+ });
757
+ }
758
+ }
759
+ }
760
+ for (const type of ["commands", "mcp", "hooks", "plugins"]) {
761
+ const dir = join6(root, type);
762
+ if (!await pathExists(dir)) continue;
763
+ artifacts.push(...await listGenericArtifacts(type, dir, type, resolved.packageName));
764
+ }
765
+ return artifacts;
766
+ }
767
+ async fetch(resolved) {
768
+ return resolved;
769
+ }
770
+ async scan(resolved) {
771
+ const artifacts = await this.list(resolved);
772
+ const findings = [];
773
+ if (!artifacts.some((artifact) => artifact.type === "instructions")) {
774
+ findings.push({ level: "warning", message: "No instructions.md or AGENTS.md found", path: resolved.resolvedPath });
775
+ }
776
+ for (const artifact of artifacts.filter((item) => item.type === "skills" && item.kind === "dir")) {
777
+ if (!await pathExists(join6(artifact.sourcePath, "SKILL.md"))) {
778
+ findings.push({ level: "warning", message: `Skill directory has no SKILL.md: ${artifact.name}`, path: artifact.sourcePath });
779
+ }
780
+ }
781
+ return { ok: !findings.some((finding) => finding.level === "error"), findings };
782
+ }
783
+ async translate(resolved) {
784
+ return resolved;
785
+ }
786
+ async export(resolved) {
787
+ return resolved;
788
+ }
789
+ };
790
+ async function firstExisting(paths) {
791
+ for (const path of paths) {
792
+ if (await pathExists(path)) return path;
793
+ }
794
+ return void 0;
795
+ }
796
+ async function sortedDirEntries(path) {
797
+ return (await readdir2(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
798
+ }
799
+ async function listFromManifest(root, packageName) {
800
+ const manifest = await readPackageManifest(root);
801
+ if (!manifest) return [];
802
+ const artifacts = [];
803
+ for (const provide of manifest.provides) {
804
+ const full = join6(root, provide.path);
805
+ if (!await pathExists(full)) continue;
806
+ const stats = await stat2(full);
807
+ if (provide.type === "instructions") {
808
+ if (stats.isFile()) {
809
+ artifacts.push(await artifactForFile(provide.type, basename(full), full, provide.path, packageName));
810
+ }
811
+ continue;
812
+ }
813
+ if (stats.isDirectory()) {
814
+ for (const entry of await sortedDirEntries(full)) {
815
+ const child = join6(full, entry.name);
816
+ if (provide.type === "skills" && entry.isDirectory()) {
817
+ artifacts.push(await artifactForDir(provide.type, entry.name, child, join6(provide.path, entry.name), packageName));
818
+ } else if (provide.type === "plugins" && entry.isDirectory()) {
819
+ artifacts.push(await artifactForDir(provide.type, entry.name, child, join6(provide.path, entry.name), packageName));
820
+ } else if (entry.isFile()) {
821
+ const name = provide.type === "rules" && entry.name.endsWith(".md") ? entry.name : entry.name;
822
+ artifacts.push(await artifactForFile(provide.type, name, child, join6(provide.path, entry.name), packageName));
823
+ }
824
+ }
825
+ } else if (stats.isFile()) {
826
+ artifacts.push(await artifactForFile(provide.type, basename(full), full, provide.path, packageName));
827
+ }
828
+ }
829
+ return artifacts;
830
+ }
831
+ async function listGenericArtifacts(type, dir, relativeRoot, packageName) {
832
+ const artifacts = [];
833
+ for (const entry of await sortedDirEntries(dir)) {
834
+ const full = join6(dir, entry.name);
835
+ if (entry.isDirectory()) {
836
+ artifacts.push(await artifactForDir(type, entry.name, full, join6(relativeRoot, entry.name), packageName));
837
+ } else if (entry.isFile()) {
838
+ artifacts.push(await artifactForFile(type, entry.name, full, join6(relativeRoot, entry.name), packageName));
839
+ }
840
+ }
841
+ return artifacts;
842
+ }
843
+ async function artifactForFile(type, name, sourcePath, relativePath, packageName) {
844
+ return {
845
+ type,
846
+ name,
847
+ sourcePath,
848
+ relativePath,
849
+ kind: "file",
850
+ hash: await hashPath(sourcePath),
851
+ packageName,
852
+ channel: "managed"
853
+ };
854
+ }
855
+ async function artifactForDir(type, name, sourcePath, relativePath, packageName) {
856
+ return {
857
+ type,
858
+ name,
859
+ sourcePath,
860
+ relativePath,
861
+ kind: "dir",
862
+ hash: await hashPath(sourcePath),
863
+ packageName,
864
+ channel: "managed"
865
+ };
866
+ }
867
+
868
+ // src/source/git.ts
869
+ var execFileAsync2 = promisify2(execFile2);
870
+ var GitSourceDriver = class {
871
+ name = "git";
872
+ local = new LocalSourceDriver();
873
+ async resolve(source, options = {}) {
874
+ const parsed = parseGitSource(source);
875
+ const requestedRef = options.ref ?? parsed.ref ?? "HEAD";
876
+ const mode = options.mode ?? (parsed.ref ? "pinned" : "tracking");
877
+ return {
878
+ driver: this.name,
879
+ source,
880
+ resolvedPath: cachePathFor(parsed.url, options.cacheRoot),
881
+ mode,
882
+ requestedRef
883
+ };
884
+ }
885
+ async fetch(resolved) {
886
+ const parsed = parseGitSource(resolved.source);
887
+ await mkdir2(resolve3(resolved.resolvedPath, ".."), { recursive: true });
888
+ if (!await pathExists(join7(resolved.resolvedPath, ".git"))) {
889
+ await rm4(resolved.resolvedPath, { recursive: true, force: true });
890
+ await git(["clone", "--no-tags", parsed.url, resolved.resolvedPath]);
891
+ } else {
892
+ await git(["-C", resolved.resolvedPath, "fetch", "--prune", "origin"]);
893
+ }
894
+ const ref = resolved.requestedRef ?? parsed.ref ?? "HEAD";
895
+ if (ref === "HEAD") {
896
+ await git(["-C", resolved.resolvedPath, "checkout", "--detach", "origin/HEAD"]);
897
+ } else if (/^[0-9a-f]{7,40}$/i.test(ref)) {
898
+ await git(["-C", resolved.resolvedPath, "checkout", "--detach", ref]);
899
+ } else {
900
+ try {
901
+ await git(["-C", resolved.resolvedPath, "checkout", ref]);
902
+ await git(["-C", resolved.resolvedPath, "reset", "--hard", `origin/${ref}`]);
903
+ } catch {
904
+ await git(["-C", resolved.resolvedPath, "checkout", "--detach", ref]);
905
+ }
906
+ }
907
+ const { stdout } = await git(["-C", resolved.resolvedPath, "rev-parse", "HEAD"]);
908
+ const resolvedCommit = stdout.trim();
909
+ const manifest = await readPackageManifest(resolved.resolvedPath);
910
+ return {
911
+ ...resolved,
912
+ packageName: manifest?.name,
913
+ packageVersion: manifest?.version,
914
+ resolvedCommit,
915
+ sourceHash: await hashPath(resolved.resolvedPath)
916
+ };
917
+ }
918
+ async list(resolved) {
919
+ return this.local.list({ ...resolved, driver: "local" });
920
+ }
921
+ async scan(resolved) {
922
+ return this.local.scan({ ...resolved, driver: "local" });
923
+ }
924
+ async translate(resolved) {
925
+ return resolved;
926
+ }
927
+ async export(resolved) {
928
+ return resolved;
929
+ }
930
+ };
931
+ function parseGitSource(source) {
932
+ if (source.startsWith("github:")) {
933
+ const rest = source.slice("github:".length);
934
+ const [repo, ref] = rest.split("#", 2);
935
+ if (!repo.includes("/")) throw new Error(`Invalid GitHub source: ${source}`);
936
+ return { url: `https://github.com/${repo}.git`, ref };
937
+ }
938
+ if (source.startsWith("git:")) {
939
+ const rest = source.slice("git:".length);
940
+ const hashIndex = rest.lastIndexOf("#");
941
+ if (hashIndex >= 0) {
942
+ return { url: rest.slice(0, hashIndex), ref: rest.slice(hashIndex + 1) };
943
+ }
944
+ return { url: rest };
945
+ }
946
+ throw new Error(`Invalid git source: ${source}`);
947
+ }
948
+ function cachePathFor(url, cacheRoot) {
949
+ const root = cacheRoot ? resolve3(cacheRoot) : join7(homedir(), ".agentwheel", "cache");
950
+ const slug = url.replace(/^[a-z]+:\/\//i, "").replace(/\.git$/i, "").replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "");
951
+ return join7(root, slug || basename2(url));
952
+ }
953
+ async function git(args) {
954
+ return execFileAsync2("git", args, { maxBuffer: 1024 * 1024 * 10 });
955
+ }
956
+
957
+ // src/source/index.ts
958
+ var drivers = [new LocalSourceDriver(), new GitSourceDriver()];
959
+ function getSourceDriver(name = "local") {
960
+ const driver = drivers.find((candidate) => candidate.name === name);
961
+ if (!driver) {
962
+ throw new Error(`Unknown source driver: ${name}`);
963
+ }
964
+ return driver;
965
+ }
966
+
967
+ // src/staging/staging.ts
968
+ import { cp as cp3, mkdir as mkdir4, mkdtemp } from "fs/promises";
969
+ import { dirname as dirname3, join as join9 } from "path";
970
+ import { tmpdir } from "os";
971
+
972
+ // src/staging/customize.ts
973
+ import { cp as cp2, mkdir as mkdir3, readdir as readdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
974
+ import { dirname as dirname2, join as join8 } from "path";
975
+ async function applyCustomizations(artifacts, options) {
976
+ let next = [...artifacts];
977
+ next = await applyReplacements(next, options, "override");
978
+ next = await applyReplacements(next, options, "ejected");
979
+ next = await applyAdditions(next, options);
980
+ next = await applyInstructionOverlay(next, options);
981
+ return next.sort((a, b) => `${a.type}:${a.name}:${a.channel}`.localeCompare(`${b.type}:${b.name}:${b.channel}`));
982
+ }
983
+ async function applyInstructionOverlay(artifacts, options) {
984
+ const overlayPath = join8(options.workspaceRoot, ".agentwheel", "overlays", options.adapter.name, "instructions.local.md");
985
+ if (!await pathExists(overlayPath)) return artifacts;
986
+ const index = artifacts.findIndex((artifact2) => artifact2.type === "instructions");
987
+ if (index < 0) return artifacts;
988
+ const artifact = artifacts[index];
989
+ const managed = await readFile5(artifact.stagedPath ?? artifact.sourcePath, "utf8");
990
+ const local = await readFile5(overlayPath, "utf8");
991
+ const composedPath = join8(options.stageRoot, ".agentwheel-composed", "instructions", "AGENTS.md");
992
+ await mkdir3(dirname2(composedPath), { recursive: true });
993
+ await writeFile2(
994
+ composedPath,
995
+ [
996
+ "<!-- BEGIN agentwheel managed: upstream -->",
997
+ managed.trimEnd(),
998
+ "<!-- END agentwheel managed: upstream -->",
999
+ "",
1000
+ "<!-- BEGIN agentwheel local: editable -->",
1001
+ local.trimEnd(),
1002
+ "<!-- END agentwheel local: editable -->",
1003
+ ""
1004
+ ].join("\n"),
1005
+ "utf8"
1006
+ );
1007
+ const updated = {
1008
+ ...artifact,
1009
+ sourcePath: composedPath,
1010
+ stagedPath: composedPath,
1011
+ relativePath: "instructions/AGENTS.md",
1012
+ name: "AGENTS.md",
1013
+ hash: await hashPath(composedPath),
1014
+ channel: "overlay"
1015
+ };
1016
+ return [...artifacts.slice(0, index), updated, ...artifacts.slice(index + 1)];
1017
+ }
1018
+ async function applyAdditions(artifacts, options) {
1019
+ const additionsRoot = join8(options.workspaceRoot, ".agentwheel", "additions");
1020
+ const rulesRoot = join8(additionsRoot, "rules");
1021
+ if (!await pathExists(rulesRoot)) return artifacts;
1022
+ const additions = [];
1023
+ for (const entry of await sortedDirEntries2(rulesRoot)) {
1024
+ const full = join8(rulesRoot, entry.name);
1025
+ if (!entry.isFile()) continue;
1026
+ additions.push({
1027
+ type: "rules",
1028
+ name: entry.name,
1029
+ sourcePath: full,
1030
+ stagedPath: full,
1031
+ relativePath: join8("additions", "rules", entry.name),
1032
+ kind: "file",
1033
+ hash: await hashPath(full),
1034
+ packageName: options.packageName,
1035
+ channel: "addition"
1036
+ });
1037
+ }
1038
+ return [...artifacts, ...additions];
1039
+ }
1040
+ async function applyReplacements(artifacts, options, channel) {
1041
+ const packageName = options.packageName;
1042
+ if (!packageName) return artifacts;
1043
+ const root = join8(options.workspaceRoot, ".agentwheel", channel === "override" ? "overrides" : "ejected", ...packageName.split("/"));
1044
+ if (!await pathExists(root)) return artifacts;
1045
+ const byKey = new Map(artifacts.map((artifact) => [artifactKey(artifact), artifact]));
1046
+ for (const type of ["instructions", "rules", "skills", "commands", "subagents", "mcp", "hooks", "plugins"]) {
1047
+ const typeRoot = join8(root, type);
1048
+ if (!await pathExists(typeRoot)) continue;
1049
+ for (const entry of await sortedDirEntries2(typeRoot)) {
1050
+ const full = join8(typeRoot, entry.name);
1051
+ const kind = entry.isDirectory() ? "dir" : "file";
1052
+ const existing = byKey.get(`${type}:${entry.name}`);
1053
+ const stagedPath = join8(options.stageRoot, ".agentwheel-composed", channel, type, entry.name);
1054
+ await mkdir3(dirname2(stagedPath), { recursive: true });
1055
+ await cp2(full, stagedPath, { recursive: kind === "dir", dereference: true });
1056
+ byKey.set(`${type}:${entry.name}`, {
1057
+ type,
1058
+ name: entry.name,
1059
+ sourcePath: full,
1060
+ stagedPath,
1061
+ relativePath: existing?.relativePath ?? join8(type, entry.name),
1062
+ kind,
1063
+ hash: await hashPath(stagedPath),
1064
+ packageName,
1065
+ channel
1066
+ });
1067
+ }
1068
+ }
1069
+ return [...byKey.values()];
1070
+ }
1071
+ function artifactKey(artifact) {
1072
+ return `${artifact.type}:${artifact.name}`;
1073
+ }
1074
+ async function sortedDirEntries2(path) {
1075
+ return (await readdir3(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
1076
+ }
1077
+
1078
+ // src/staging/staging.ts
1079
+ async function stageSource(driver, source, options = {}) {
1080
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(source, options))));
1081
+ const artifacts = await driver.list(resolved);
1082
+ const root = await mkdtemp(join9(tmpdir(), "agentwheel-stage-"));
1083
+ const stagedArtifacts = [];
1084
+ for (const artifact of artifacts) {
1085
+ const stagedPath = join9(root, artifact.relativePath);
1086
+ await mkdir4(dirname3(stagedPath), { recursive: true });
1087
+ await cp3(artifact.sourcePath, stagedPath, { recursive: artifact.kind === "dir", dereference: true });
1088
+ stagedArtifacts.push({
1089
+ ...artifact,
1090
+ stagedPath,
1091
+ hash: await hashPath(stagedPath),
1092
+ channel: artifact.channel ?? "managed"
1093
+ });
1094
+ }
1095
+ const finalArtifacts = options.workspaceRoot && options.adapter ? await applyCustomizations(stagedArtifacts, {
1096
+ workspaceRoot: options.workspaceRoot,
1097
+ adapter: options.adapter,
1098
+ stageRoot: root,
1099
+ packageName: resolved.packageName
1100
+ }) : stagedArtifacts;
1101
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1102
+ return {
1103
+ root,
1104
+ source: resolved,
1105
+ artifacts: finalArtifacts,
1106
+ sourceLock: {
1107
+ version: 1,
1108
+ driver: resolved.driver,
1109
+ source: resolved.source,
1110
+ resolvedPath: resolved.resolvedPath,
1111
+ packageName: resolved.packageName,
1112
+ packageVersion: resolved.packageVersion,
1113
+ mode: resolved.mode ?? "pinned",
1114
+ requestedRef: resolved.requestedRef,
1115
+ resolvedCommit: resolved.resolvedCommit,
1116
+ sourceHash: resolved.sourceHash,
1117
+ generatedAt,
1118
+ artifacts: finalArtifacts.map((artifact) => ({
1119
+ type: artifact.type,
1120
+ name: artifact.name,
1121
+ relativePath: artifact.relativePath,
1122
+ kind: artifact.kind,
1123
+ hash: artifact.hash
1124
+ }))
1125
+ }
1126
+ };
1127
+ }
1128
+
1129
+ // src/model/workspace.ts
1130
+ import { readFile as readFile6 } from "fs/promises";
1131
+ import { join as join10 } from "path";
1132
+ import { z as z5 } from "zod";
1133
+ var workspacePackageSchema = z5.object({
1134
+ name: z5.string().min(1),
1135
+ source: z5.string().min(1),
1136
+ driver: z5.enum(["local", "git"]).default("local"),
1137
+ adapter: z5.string().min(1).default("openclaw"),
1138
+ adapterConfig: z5.string().min(1).optional(),
1139
+ mode: z5.enum(["pinned", "tracking"]).default("pinned"),
1140
+ requestedRef: z5.string().min(1).optional()
1141
+ });
1142
+ var workspaceConfigSchema = z5.object({
1143
+ schemaVersion: z5.literal(1),
1144
+ packages: z5.array(workspacePackageSchema).default([])
1145
+ });
1146
+ function workspaceConfigPath(workspaceRoot) {
1147
+ return join10(workspaceRoot, ".agentwheel", "config.json");
1148
+ }
1149
+ async function readWorkspaceConfig(workspaceRoot) {
1150
+ const path = workspaceConfigPath(workspaceRoot);
1151
+ if (!await pathExists(path)) return { schemaVersion: 1, packages: [] };
1152
+ return workspaceConfigSchema.parse(JSON.parse(await readFile6(path, "utf8")));
1153
+ }
1154
+ async function writeWorkspaceConfig(workspaceRoot, config) {
1155
+ await writeJsonAtomic(workspaceConfigPath(workspaceRoot), workspaceConfigSchema.parse(config));
1156
+ }
1157
+ function upsertPackage(config, entry) {
1158
+ const packages = config.packages.filter((candidate) => candidate.name !== entry.name);
1159
+ packages.push(entry);
1160
+ packages.sort((a, b) => a.name.localeCompare(b.name));
1161
+ return { schemaVersion: 1, packages };
1162
+ }
1163
+
1164
+ // src/lifecycle/customization.ts
1165
+ import { appendFile, cp as cp4, mkdir as mkdir5, rm as rm5 } from "fs/promises";
1166
+ import { dirname as dirname4, join as join11 } from "path";
1167
+ async function remember(workspaceRoot, runtime, text) {
1168
+ const overlayPath = join11(workspaceRoot, ".agentwheel", "overlays", runtime, "instructions.local.md");
1169
+ await mkdir5(dirname4(overlayPath), { recursive: true });
1170
+ await appendFile(overlayPath, `${text.trim()}
1171
+ `, "utf8");
1172
+ return { overlayPath };
1173
+ }
1174
+ async function ejectArtifact(workspaceRoot, item) {
1175
+ const parsed = parseEjectItem(item);
1176
+ const config = await readWorkspaceConfig(workspaceRoot);
1177
+ const pkg = config.packages.find((candidate) => candidate.name === parsed.packageName);
1178
+ if (!pkg) {
1179
+ throw new Error(`Package not configured: ${parsed.packageName}`);
1180
+ }
1181
+ const driver = getSourceDriver(pkg.driver);
1182
+ const adapter = pkg.adapterConfig ? await loadAdapterConfig(pkg.adapterConfig) : getAdapter(pkg.adapter);
1183
+ const bundle = await stageSource(driver, pkg.source, {
1184
+ adapter,
1185
+ cacheRoot: join11(workspaceRoot, ".agentwheel", "cache"),
1186
+ mode: pkg.mode
1187
+ });
1188
+ try {
1189
+ const artifact = bundle.artifacts.find((candidate) => candidate.type === parsed.type && candidate.name === parsed.name);
1190
+ if (!artifact) {
1191
+ throw new Error(`Artifact not found: ${item}`);
1192
+ }
1193
+ const ejectedPath = join11(workspaceRoot, ".agentwheel", "ejected", ...parsed.packageName.split("/"), parsed.type, parsed.name);
1194
+ await mkdir5(dirname4(ejectedPath), { recursive: true });
1195
+ await rm5(ejectedPath, { recursive: true, force: true });
1196
+ await cp4(artifact.stagedPath ?? artifact.sourcePath, ejectedPath, { recursive: artifact.kind === "dir", dereference: true });
1197
+ return { ...parsed, ejectedPath };
1198
+ } finally {
1199
+ await rm5(bundle.root, { recursive: true, force: true });
1200
+ }
1201
+ }
1202
+ function parseEjectItem(item) {
1203
+ const parts = item.split("/").filter(Boolean);
1204
+ if (parts.length < 3) {
1205
+ throw new Error("Eject item must be <package>/<type>/<name>");
1206
+ }
1207
+ const name = parts.pop();
1208
+ const type = artifactTypeSchema.parse(parts.pop());
1209
+ const packageName = parts.join("/");
1210
+ return { packageName, type, name };
1211
+ }
1212
+
1213
+ // src/lifecycle/update.ts
1214
+ function shouldUpdatePackage(pkg, lock) {
1215
+ if (!lock) {
1216
+ return { shouldUpdate: true, reason: "no source lock" };
1217
+ }
1218
+ if (pkg.mode === "tracking") {
1219
+ return { shouldUpdate: true, reason: "tracking source" };
1220
+ }
1221
+ if (lock.source !== pkg.source) {
1222
+ return { shouldUpdate: true, reason: "pinned source changed" };
1223
+ }
1224
+ if (lock.requestedRef !== pkg.requestedRef) {
1225
+ return { shouldUpdate: true, reason: "pinned ref changed" };
1226
+ }
1227
+ return { shouldUpdate: false, reason: "pinned source unchanged" };
1228
+ }
1229
+
1230
+ // src/cli/index.ts
1231
+ var program = new Command();
1232
+ program.name("agentwheel").description("Multi-runtime agent artifact orchestrator").version("0.2.0");
1233
+ program.command("init").argument("[kind]", "workspace or package", "workspace").option("--target-root <path>", "workspace root", process.cwd()).action(async (kind, options) => {
1234
+ const root = normalizeTargetRoot(options.targetRoot);
1235
+ if (kind === "package") {
1236
+ await initPackage(root);
1237
+ console.log("Initialized agentwheel package.");
1238
+ return;
1239
+ }
1240
+ if (kind !== "workspace") {
1241
+ throw new Error(`Unknown init kind: ${kind}`);
1242
+ }
1243
+ await writeWorkspaceConfig(root, await readWorkspaceConfig(root));
1244
+ console.log("Initialized .agentwheel/config.json.");
1245
+ });
1246
+ program.command("add").argument("<source>", "package source").option("--driver <driver>", "source driver (local or git)").option("--adapter <adapter>", "built-in adapter", "openclaw").option("--adapter-config <path>", "adapter JSON/JSONC file").option("--target-root <path>", "workspace root", process.cwd()).option("--mode <mode>", "pinned or tracking", "pinned").option("--name <name>", "package alias").action(async (source, options) => {
1247
+ const targetRoot = normalizeTargetRoot(options.targetRoot);
1248
+ const driverName = options.driver ?? inferDriver(source);
1249
+ const driver = getSourceDriver(driverName);
1250
+ const adapter = options.adapterConfig ? await loadAdapterConfig(options.adapterConfig) : getAdapter(options.adapter);
1251
+ const bundle = await stageSource(driver, source, {
1252
+ workspaceRoot: targetRoot,
1253
+ adapter,
1254
+ cacheRoot: join12(targetRoot, ".agentwheel", "cache"),
1255
+ mode: options.mode
1256
+ });
1257
+ const name = options.name ?? bundle.source.packageName ?? source;
1258
+ const entry = {
1259
+ name,
1260
+ source,
1261
+ driver: driverName,
1262
+ adapter: adapter.name,
1263
+ adapterConfig: options.adapterConfig,
1264
+ mode: options.mode,
1265
+ requestedRef: bundle.source.requestedRef
1266
+ };
1267
+ await writeWorkspaceConfig(targetRoot, upsertPackage(await readWorkspaceConfig(targetRoot), entry));
1268
+ await rm6(bundle.root, { recursive: true, force: true });
1269
+ console.log(`Added ${name}.`);
1270
+ });
1271
+ program.command("list").argument("<source>", "local source directory").option("--driver <driver>", "source driver", "local").action(async (source, options) => {
1272
+ const driver = getSourceDriver(options.driver);
1273
+ const resolved = await driver.resolve(source);
1274
+ const artifacts = await driver.list(resolved);
1275
+ for (const artifact of artifacts) {
1276
+ console.log(`${artifact.type} ${artifact.name} ${artifact.relativePath}`);
1277
+ }
1278
+ });
1279
+ program.command("scan").argument("<source>", "local source directory").option("--driver <driver>", "source driver", "local").action(async (source, options) => {
1280
+ const driver = getSourceDriver(options.driver);
1281
+ const resolved = await driver.resolve(source);
1282
+ const result = await driver.scan(resolved);
1283
+ if (result.findings.length === 0) {
1284
+ console.log("Scan ok: no findings");
1285
+ } else {
1286
+ for (const finding of result.findings) {
1287
+ console.log(`${finding.level.toUpperCase()}: ${finding.message}${finding.path ? ` (${finding.path})` : ""}`);
1288
+ }
1289
+ }
1290
+ if (!result.ok) process.exitCode = 1;
1291
+ });
1292
+ program.command("plan").argument("<source>", "source directory").option("--driver <driver>", "source driver", "local").option("--adapter <adapter>", "built-in adapter", "openclaw").option("--adapter-config <path>", "adapter JSON/JSONC file").option("--target-root <path>", "runtime/project root", process.cwd()).option("--mode <mode>", "pinned or tracking").action(async (source, options) => {
1293
+ const { plan, bundle } = await buildPlan(source, options);
1294
+ console.log(formatPlan(plan));
1295
+ await rm6(bundle.root, { recursive: true, force: true });
1296
+ if (plan.hasBlockingChanges) process.exitCode = 1;
1297
+ });
1298
+ program.command("sync").argument("<source>", "source directory").option("--driver <driver>", "source driver", "local").option("--adapter <adapter>", "built-in adapter", "openclaw").option("--adapter-config <path>", "adapter JSON/JSONC file").option("--target-root <path>", "runtime/project root", process.cwd()).option("--mode <mode>", "pinned or tracking").option("--dry-run", "show plan without writing", false).option("--execute-plugins", "execute semantic plugin installs", false).action(async (source, options) => {
1299
+ const { plan, bundle } = await buildPlan(source, options);
1300
+ console.log(formatPlan(plan));
1301
+ if (!options.dryRun) {
1302
+ await applyInstallPlan(plan, bundle.sourceLock, { executePlugins: options.executePlugins });
1303
+ console.log("Applied.");
1304
+ }
1305
+ await rm6(bundle.root, { recursive: true, force: true });
1306
+ if (plan.hasBlockingChanges) process.exitCode = 1;
1307
+ });
1308
+ program.command("update").option("--target-root <path>", "workspace root", process.cwd()).option("--dry-run", "show plans without writing", false).option("--execute-plugins", "execute semantic plugin installs", false).action(async (options) => {
1309
+ const targetRoot = normalizeTargetRoot(options.targetRoot);
1310
+ const config = await readWorkspaceConfig(targetRoot);
1311
+ if (config.packages.length === 0) {
1312
+ console.log("No packages configured.");
1313
+ return;
1314
+ }
1315
+ for (const pkg of config.packages) {
1316
+ const adapter = pkg.adapterConfig ? await loadAdapterConfig(pkg.adapterConfig) : getAdapter(pkg.adapter);
1317
+ const lock = await readSourceLock(targetRoot, adapter.name);
1318
+ const decision = shouldUpdatePackage(pkg, lock);
1319
+ if (!decision.shouldUpdate) {
1320
+ console.log(`Skipping ${pkg.name}: ${decision.reason}.`);
1321
+ continue;
1322
+ }
1323
+ const { plan, bundle } = await buildPlan(pkg.source, {
1324
+ driver: pkg.driver,
1325
+ adapter: pkg.adapter,
1326
+ adapterConfig: pkg.adapterConfig,
1327
+ targetRoot,
1328
+ mode: pkg.mode
1329
+ });
1330
+ console.log(`Update ${pkg.name}:`);
1331
+ console.log(formatPlan(plan));
1332
+ if (!options.dryRun) {
1333
+ await applyInstallPlan(plan, bundle.sourceLock, { executePlugins: options.executePlugins });
1334
+ console.log(`Applied ${pkg.name}.`);
1335
+ }
1336
+ await rm6(bundle.root, { recursive: true, force: true });
1337
+ if (plan.hasBlockingChanges) process.exitCode = 1;
1338
+ }
1339
+ });
1340
+ program.command("remember").requiredOption("--runtime <runtime>", "runtime/adapter name").option("--target-root <path>", "workspace root", process.cwd()).argument("<text>", "text to append to the local instructions overlay").action(async (text, options) => {
1341
+ const targetRoot = normalizeTargetRoot(options.targetRoot);
1342
+ const result = await remember(targetRoot, options.runtime, text);
1343
+ console.log(`Remembered in ${result.overlayPath}. Run: agentwheel sync <source> --adapter ${options.runtime}`);
1344
+ });
1345
+ program.command("eject").argument("<item>", "package/type/name").option("--target-root <path>", "workspace root", process.cwd()).action(async (item, options) => {
1346
+ const targetRoot = normalizeTargetRoot(options.targetRoot);
1347
+ const result = await ejectArtifact(targetRoot, item);
1348
+ console.log(`Ejected ${item} to ${result.ejectedPath}.`);
1349
+ });
1350
+ program.command("uninstall").option("--adapter <adapter>", "adapter", "openclaw").option("--target-root <path>", "runtime/project root", process.cwd()).option("--dry-run", "show removals without writing", false).action(async (options) => {
1351
+ const targetRoot = normalizeTargetRoot(options.targetRoot);
1352
+ const manifest = await readInstallManifest(targetRoot, options.adapter);
1353
+ if (!manifest) {
1354
+ console.log(`No install manifest for ${options.adapter} at ${targetRoot}`);
1355
+ return;
1356
+ }
1357
+ const plan = await createUninstallPlan(manifest);
1358
+ console.log(formatPlan(plan));
1359
+ await uninstall(plan, options.dryRun);
1360
+ if (!options.dryRun) console.log("Uninstalled.");
1361
+ if (plan.hasBlockingChanges) process.exitCode = 1;
1362
+ });
1363
+ async function buildPlan(source, options) {
1364
+ const driver = getSourceDriver(options.driver ?? inferDriver(source));
1365
+ const adapter = options.adapterConfig ? await loadAdapterConfig(options.adapterConfig) : getAdapter(options.adapter);
1366
+ const targetRoot = normalizeTargetRoot(options.targetRoot);
1367
+ const bundle = await stageSource(driver, source, {
1368
+ workspaceRoot: targetRoot,
1369
+ adapter,
1370
+ cacheRoot: join12(targetRoot, ".agentwheel", "cache"),
1371
+ mode: options.mode
1372
+ });
1373
+ const manifest = await readInstallManifest(targetRoot, adapter.name);
1374
+ const plan = await createInstallPlan(bundle, adapter, targetRoot, manifest);
1375
+ return { plan, bundle };
1376
+ }
1377
+ async function initPackage(root) {
1378
+ await mkdir6(join12(root, "instructions"), { recursive: true });
1379
+ await mkdir6(join12(root, "rules"), { recursive: true });
1380
+ await mkdir6(join12(root, "skills"), { recursive: true });
1381
+ const manifestPath = join12(root, "agentwheel.json");
1382
+ const manifest = {
1383
+ schemaVersion: 1,
1384
+ name: "example/agentwheel-package",
1385
+ version: "0.1.0",
1386
+ provides: [
1387
+ { type: "instructions", path: "instructions/AGENTS.md" },
1388
+ { type: "rules", path: "rules" },
1389
+ { type: "skills", path: "skills" }
1390
+ ]
1391
+ };
1392
+ await writeFile3(manifestPath, `${JSON.stringify(manifest, null, 2)}
1393
+ `, "utf8");
1394
+ await writeFile3(join12(root, "instructions", "AGENTS.md"), "# Agent Instructions\n", "utf8");
1395
+ }
1396
+ function inferDriver(source) {
1397
+ return source.startsWith("github:") || source.startsWith("git:") ? "git" : "local";
1398
+ }
1399
+ program.parseAsync().catch((error) => {
1400
+ console.error(error instanceof Error ? error.message : String(error));
1401
+ process.exitCode = 1;
1402
+ });