agentwheel 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,9 +32,9 @@ No lock-in. No central gatekeeper. Your packages live in plain git repos, your c
32
32
 
33
33
  ---
34
34
 
35
- > **Status: early (v0.2).** The install spine and lifecycle core are real and tested — local/git
36
- > sources, plan/sync/update/drift/uninstall, overlays, eject/remember, and pluggable adapters.
37
- > Expect sharp edges.
35
+ > **Status: early (v0.3).** The lifecycle core is real and tested — local/git/skillkit/vercel
36
+ > sources, optional registry discovery, plan/sync/update/drift/uninstall, overlays, eject/remember,
37
+ > profiles, rich JSON merge, and pluggable adapters. Expect sharp edges.
38
38
 
39
39
  ## What it does
40
40
 
@@ -102,6 +102,34 @@ A package is a git repo (or folder) with a JSON manifest and a canonical layout:
102
102
  Publish by pushing to any git host. A registry exists only for short names and discovery — it's
103
103
  optional, and `agentwheel add <url|path>` always works without it.
104
104
 
105
+ ## How to add a package
106
+
107
+ The public package registry lives at
108
+ [`NestDevLab/agentwheel-registry`](https://github.com/NestDevLab/agentwheel-registry).
109
+
110
+ 1. Create a public repo with `agentwheel.json` and a standard layout such as `instructions/`, `rules/`, `skills/`, `commands/`, `mcp/`, or `hooks/`.
111
+ 2. Open a pull request to `agentwheel-registry` that adds an entry to `index.json`.
112
+ 3. Users install by short name:
113
+
114
+ ```bash
115
+ agentwheel registry update
116
+ agentwheel add your-package-name --adapter openclaw
117
+ agentwheel update --dry-run
118
+ agentwheel update
119
+ ```
120
+
121
+ Example registry entry:
122
+
123
+ ```json
124
+ {
125
+ "name": "your-package-name",
126
+ "source": "github:your-org/your-agent-package",
127
+ "type": "package",
128
+ "description": "Reusable skills, rules, and instructions for agentwheel.",
129
+ "tags": ["skills", "rules", "instructions"]
130
+ }
131
+ ```
132
+
105
133
  ## Customizing without getting overwritten
106
134
 
107
135
  Drift detection blocks *accidental* edits to generated files. *Intentional* changes have four channels,
@@ -133,7 +161,7 @@ agentwheel sync ./my-pack --adapter-config ./myco-internal.jsonc
133
161
  ```
134
162
 
135
163
  Built-in adapters ship for common runtimes; declarative adapters need no code and stay private.
136
- (Programmatic adapters, for logic beyond file placement, are planned behind an explicit opt-in.)
164
+ Programmatic adapters, for private runtime logic beyond file placement, require explicit `--allow-adapter-code`.
137
165
 
138
166
  Copilot support is intentionally file-drop only: instructions, rules, and prompt/command files are
139
167
  placed in GitHub-native locations, while raw `SKILL.md` directories stay disabled until there is a
@@ -143,7 +171,7 @@ clear conversion format.
143
171
 
144
172
  - [x] **v0.1** — install spine: local sources; openclaw/claude/codex adapters; skills/rules/instructions; `plan` · `sync` · `--dry-run` · `uninstall`; manifest + drift + idempotency.
145
173
  - [x] **v0.2** — git source driver; `update` (pinned & tracking); overlays/additive/override/eject; `init`; hermes + copilot adapters; commands/mcp/hooks artifacts; OpenClaw semantic plugin planning.
146
- - [ ] **v0.3** — registry & federation (skill ecosystems, MCP); profiles; programmatic adapters.
174
+ - [x] **v0.3** — skillkit/vercel source drivers; optional registry & federation; programmatic adapters behind `--allow-adapter-code`; rich JSON merge for mcp/hooks/settings; profiles.
147
175
 
148
176
  ## Design docs
149
177
 
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/source/identify.ts
4
+ import { homedir } from "os";
5
+ import { resolve } from "path";
6
+
7
+ // src/utils/fs.ts
8
+ import { createHash } from "crypto";
9
+ import {
10
+ copyFile,
11
+ cp,
12
+ mkdir,
13
+ readdir,
14
+ readFile,
15
+ rename,
16
+ rm,
17
+ stat,
18
+ writeFile
19
+ } from "fs/promises";
20
+ import { dirname, join, relative } from "path";
21
+ async function pathExists(path) {
22
+ try {
23
+ await stat(path);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ async function hashPath(path) {
30
+ const stats = await stat(path);
31
+ if (stats.isFile()) {
32
+ const content = await readFile(path);
33
+ return createHash("sha256").update("file\0").update(content).digest("hex");
34
+ }
35
+ if (!stats.isDirectory()) {
36
+ throw new Error(`Unsupported path kind: ${path}`);
37
+ }
38
+ const hash = createHash("sha256").update("dir\0");
39
+ const files = await listFiles(path);
40
+ for (const file of files) {
41
+ hash.update(relative(path, file).replaceAll("\\", "/")).update("\0");
42
+ hash.update(await hashPath(file)).update("\0");
43
+ }
44
+ return hash.digest("hex");
45
+ }
46
+ async function listFiles(root) {
47
+ const out = [];
48
+ async function walk(dir) {
49
+ const entries = await readdir(dir, { withFileTypes: true });
50
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
51
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
52
+ const full = join(dir, entry.name);
53
+ if (entry.isDirectory()) {
54
+ await walk(full);
55
+ } else if (entry.isFile()) {
56
+ out.push(full);
57
+ }
58
+ }
59
+ }
60
+ await walk(root);
61
+ return out;
62
+ }
63
+ async function atomicCopy(source, dest, kind) {
64
+ await mkdir(dirname(dest), { recursive: true });
65
+ const temp = `${dest}.agentwheel-tmp-${process.pid}-${Date.now()}`;
66
+ await rm(temp, { recursive: true, force: true });
67
+ if (kind === "file") {
68
+ await copyFile(source, temp);
69
+ } else {
70
+ await cp(source, temp, { recursive: true, dereference: true });
71
+ }
72
+ await rm(dest, { recursive: true, force: true });
73
+ await rename(temp, dest);
74
+ }
75
+ async function writeJsonAtomic(path, data) {
76
+ await mkdir(dirname(path), { recursive: true });
77
+ const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
78
+ await writeFile(temp, `${JSON.stringify(data, null, 2)}
79
+ `, "utf8");
80
+ await rename(temp, path);
81
+ }
82
+
83
+ // src/source/identify.ts
84
+ function inferSourceDriverName(source) {
85
+ if (source.startsWith("skillkit:")) return "skillkit";
86
+ if (source.startsWith("vercel:")) return "vercel-skills";
87
+ return source.startsWith("github:") || source.startsWith("git:") ? "git" : "local";
88
+ }
89
+ async function isExplicitSource(source) {
90
+ if (source.startsWith("github:") || source.startsWith("git:") || source.startsWith("skillkit:") || source.startsWith("vercel:")) {
91
+ return true;
92
+ }
93
+ if (source.startsWith("./") || source.startsWith("../") || source.startsWith("/") || source.startsWith("~/")) {
94
+ return true;
95
+ }
96
+ return pathExists(resolveLocalPath(source));
97
+ }
98
+ function resolveLocalPath(source) {
99
+ if (source === "~") return homedir();
100
+ if (source.startsWith("~/")) return resolve(homedir(), source.slice(2));
101
+ return resolve(source);
102
+ }
103
+
104
+ export {
105
+ pathExists,
106
+ hashPath,
107
+ atomicCopy,
108
+ writeJsonAtomic,
109
+ inferSourceDriverName,
110
+ isExplicitSource
111
+ };
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ inferSourceDriverName,
4
+ isExplicitSource
5
+ } from "./chunk-N2LZY7LO.js";
6
+ export {
7
+ inferSourceDriverName,
8
+ isExplicitSource
9
+ };