@untitled-devs/wasla 1.0.0 → 1.0.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
|
@@ -121,22 +121,21 @@ The same pattern applies across every asset type:
|
|
|
121
121
|
|
|
122
122
|
## 🚀 Installation
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
**Install globally:**
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
npm i -g @untitled-devs/wasla
|
|
128
|
+
waslagenie config --scope workspace
|
|
129
|
+
waslagenie sync
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
Choose `workspace` or `user` once before running operational commands.
|
|
132
|
-
It does not register helper skills inside Claude, Gemini, or other tools.
|
|
132
|
+
Choose `workspace` or `user` once before running operational commands.
|
|
133
133
|
|
|
134
|
-
**Or
|
|
134
|
+
**Or run via `npx` (no global installation required):**
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
waslagenie sync
|
|
137
|
+
npx @untitled-devs/wasla config --scope workspace
|
|
138
|
+
npx @untitled-devs/wasla sync
|
|
140
139
|
```
|
|
141
140
|
|
|
142
141
|
Optional helper registration:
|
|
@@ -197,7 +196,7 @@ Use `npm run ...` while developing because it runs your local code (`dist`) afte
|
|
|
197
196
|
|
|
198
197
|
If you run through `npm run ...` in this repo: **No reinstall needed**. Just run the script again; it rebuilds.
|
|
199
198
|
|
|
200
|
-
If you installed globally with `npm
|
|
199
|
+
If you installed globally with `npm i -g @untitled-devs/wasla`: **Yes**, reinstall (or relink) to test your latest local changes.
|
|
201
200
|
|
|
202
201
|
For local development without repeated global installs:
|
|
203
202
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseAdapter } from './base.js';
|
|
2
2
|
import { fileExists, writeText, ensureDir } from '#shared/fs.js';
|
|
3
|
-
import { join } from 'path';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
4
|
import { getToolMarkers } from '#shared/paths.js';
|
|
5
5
|
export class ClaudeAdapter extends BaseAdapter {
|
|
6
6
|
constructor(scope = 'workspace') {
|
|
@@ -19,13 +19,16 @@ export class ClaudeAdapter extends BaseAdapter {
|
|
|
19
19
|
}
|
|
20
20
|
get paths() {
|
|
21
21
|
const markers = getToolMarkers(this.scope);
|
|
22
|
+
const workspaceRoot = dirname(markers.claude);
|
|
22
23
|
return {
|
|
23
24
|
agent: join(markers.claude, 'agents'),
|
|
24
25
|
skill: join(markers.claude, 'skills'),
|
|
25
26
|
mcp: this.scope === 'workspace'
|
|
26
27
|
? join(markers.claude, 'mcp.json')
|
|
27
28
|
: join(markers.claude, 'settings.json'),
|
|
28
|
-
context:
|
|
29
|
+
context: this.scope === 'workspace'
|
|
30
|
+
? join(workspaceRoot, 'CLAUDE.md')
|
|
31
|
+
: join(markers.claude, 'CLAUDE.md'),
|
|
29
32
|
};
|
|
30
33
|
}
|
|
31
34
|
get skillDirs() {
|
|
@@ -20,6 +20,15 @@ export declare class Syncer {
|
|
|
20
20
|
attachAssetToTool(name: string, type: AssetType, sourceTool: string, targetTool: string): Promise<boolean>;
|
|
21
21
|
detachAssetFromTool(name: string, type: AssetType, tool: string): Promise<boolean>;
|
|
22
22
|
private calculateHash;
|
|
23
|
+
/**
|
|
24
|
+
* Filters discovered files, removing those that cannot be read (ENOENT).
|
|
25
|
+
* Treats read-time ENOENT as a deletion signal: if a file disappears during
|
|
26
|
+
* pruning, it is removed from sync and will be treated as a deletion in
|
|
27
|
+
* reconcileDeletedAssets. This prevents sync failures when files are deleted
|
|
28
|
+
* between scan and read phases.
|
|
29
|
+
*/
|
|
30
|
+
private removeMissingFiles;
|
|
31
|
+
private isMissingFileError;
|
|
23
32
|
private getTargetPath;
|
|
24
33
|
private writeTarget;
|
|
25
34
|
private writeMcpServer;
|
|
@@ -27,6 +27,7 @@ export class Syncer {
|
|
|
27
27
|
await ensureDir(join(registryDir, 'context'));
|
|
28
28
|
const discovered = await this.scanner.scanAllTools();
|
|
29
29
|
const grouped = this.groupByNameAndType(discovered);
|
|
30
|
+
await this.removeMissingFiles(grouped);
|
|
30
31
|
const installedTools = await this.scanner.detectInstalledTools();
|
|
31
32
|
let stubsWritten = 0;
|
|
32
33
|
const stubsDeleted = await this.reconcileDeletedAssets(grouped, installedTools);
|
|
@@ -102,20 +103,7 @@ export class Syncer {
|
|
|
102
103
|
continue;
|
|
103
104
|
}
|
|
104
105
|
// Update stub info in registry
|
|
105
|
-
|
|
106
|
-
if (existingStub) {
|
|
107
|
-
existingStub.path = primaryTargetPath;
|
|
108
|
-
existingStub.written_at = new Date().toISOString();
|
|
109
|
-
existingStub.hash = contentHash;
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
asset.stubs.push({
|
|
113
|
-
tool,
|
|
114
|
-
path: primaryTargetPath,
|
|
115
|
-
written_at: new Date().toISOString(),
|
|
116
|
-
hash: contentHash,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
106
|
+
await this.upsertStub(asset, tool, primaryTargetPath, contentHash);
|
|
119
107
|
}
|
|
120
108
|
// Also save to canonical registry location
|
|
121
109
|
const registrySubdir = type === 'agent'
|
|
@@ -166,11 +154,13 @@ export class Syncer {
|
|
|
166
154
|
const assetTypes = ['agent', 'skill', 'mcp', 'context'];
|
|
167
155
|
const discovered = await this.scanner.scanTool(sourceTool, assetTypes);
|
|
168
156
|
const grouped = this.groupByNameAndType(discovered);
|
|
157
|
+
await this.removeMissingFiles(grouped);
|
|
169
158
|
const deletionScan = [...discovered];
|
|
170
159
|
for (const tool of targetTools.filter((tool) => tool !== sourceTool)) {
|
|
171
160
|
deletionScan.push(...(await this.scanner.scanTool(tool, assetTypes)));
|
|
172
161
|
}
|
|
173
162
|
const deletionGrouped = this.groupByNameAndType(deletionScan);
|
|
163
|
+
await this.removeMissingFiles(deletionGrouped);
|
|
174
164
|
let stubsWritten = 0;
|
|
175
165
|
const stubsDeleted = await this.reconcileDeletedAssets(deletionGrouped, [...new Set([sourceTool, ...targetTools])], sourceTool);
|
|
176
166
|
for (const key of Object.keys(grouped)) {
|
|
@@ -209,7 +199,7 @@ export class Syncer {
|
|
|
209
199
|
last_synced_at: new Date().toISOString(),
|
|
210
200
|
});
|
|
211
201
|
}
|
|
212
|
-
this.upsertStub(asset, sourceTool, source.path, contentHash);
|
|
202
|
+
await this.upsertStub(asset, sourceTool, source.path, contentHash);
|
|
213
203
|
// Write only to target tools
|
|
214
204
|
for (const tool of targetTools) {
|
|
215
205
|
const adapter = getAdapter(tool, this.scope);
|
|
@@ -242,20 +232,7 @@ export class Syncer {
|
|
|
242
232
|
continue;
|
|
243
233
|
}
|
|
244
234
|
// Update stub info in registry
|
|
245
|
-
|
|
246
|
-
if (existingStub) {
|
|
247
|
-
existingStub.path = primaryTargetPath;
|
|
248
|
-
existingStub.written_at = new Date().toISOString();
|
|
249
|
-
existingStub.hash = contentHash;
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
asset.stubs.push({
|
|
253
|
-
tool,
|
|
254
|
-
path: primaryTargetPath,
|
|
255
|
-
written_at: new Date().toISOString(),
|
|
256
|
-
hash: contentHash,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
235
|
+
await this.upsertStub(asset, tool, primaryTargetPath, contentHash);
|
|
259
236
|
}
|
|
260
237
|
// Also save to canonical registry location
|
|
261
238
|
const registrySubdir = type === 'agent'
|
|
@@ -311,7 +288,7 @@ export class Syncer {
|
|
|
311
288
|
};
|
|
312
289
|
this.registry.addAsset(asset);
|
|
313
290
|
}
|
|
314
|
-
this.upsertStub(asset, source.tool, source.path, contentHash);
|
|
291
|
+
await this.upsertStub(asset, source.tool, source.path, contentHash);
|
|
315
292
|
const adapter = getAdapter(targetTool, this.scope);
|
|
316
293
|
const formatsRecord = adapter.formats;
|
|
317
294
|
if (!adapter.paths[type] || !formatsRecord[type]) {
|
|
@@ -374,6 +351,46 @@ export class Syncer {
|
|
|
374
351
|
calculateHash(content) {
|
|
375
352
|
return createHash('sha256').update(content).digest('hex');
|
|
376
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Filters discovered files, removing those that cannot be read (ENOENT).
|
|
356
|
+
* Treats read-time ENOENT as a deletion signal: if a file disappears during
|
|
357
|
+
* pruning, it is removed from sync and will be treated as a deletion in
|
|
358
|
+
* reconcileDeletedAssets. This prevents sync failures when files are deleted
|
|
359
|
+
* between scan and read phases.
|
|
360
|
+
*/
|
|
361
|
+
async removeMissingFiles(grouped) {
|
|
362
|
+
for (const key of Object.keys(grouped)) {
|
|
363
|
+
const readable = [];
|
|
364
|
+
for (const item of grouped[key]) {
|
|
365
|
+
if (item.content !== undefined) {
|
|
366
|
+
readable.push(item);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
item.content = await readText(item.path);
|
|
371
|
+
readable.push(item);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (!this.isMissingFileError(error)) {
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
// ENOENT: file was deleted after scanning, treat as deletion
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (readable.length > 0) {
|
|
381
|
+
grouped[key] = readable;
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
delete grouped[key];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
isMissingFileError(error) {
|
|
389
|
+
return (typeof error === 'object' &&
|
|
390
|
+
error !== null &&
|
|
391
|
+
'code' in error &&
|
|
392
|
+
error.code === 'ENOENT');
|
|
393
|
+
}
|
|
377
394
|
getTargetPath(adapter, type, name, relativePath) {
|
|
378
395
|
const typeDir = adapter.paths[type];
|
|
379
396
|
const format = adapter.formats[type];
|
|
@@ -436,9 +453,15 @@ export class Syncer {
|
|
|
436
453
|
await writeJSON(targetPath, config);
|
|
437
454
|
return true;
|
|
438
455
|
}
|
|
439
|
-
upsertStub(asset, tool, path, hash) {
|
|
456
|
+
async upsertStub(asset, tool, path, hash) {
|
|
440
457
|
const existingStub = asset.stubs.find((stub) => stub.tool === tool);
|
|
441
458
|
if (existingStub) {
|
|
459
|
+
if (asset.type === 'context' &&
|
|
460
|
+
existingStub.path !== path &&
|
|
461
|
+
(await fileExists(existingStub.path)) &&
|
|
462
|
+
this.calculateHash(await readText(existingStub.path)) === existingStub.hash) {
|
|
463
|
+
await removePath(existingStub.path);
|
|
464
|
+
}
|
|
442
465
|
existingStub.path = path;
|
|
443
466
|
existingStub.written_at = new Date().toISOString();
|
|
444
467
|
existingStub.hash = hash;
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@untitled-devs/wasla",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Universal synchronization layer for AI agent orchestrators",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/The-Untitled-Org/wasla-genie.git"
|
|
8
8
|
},
|
|
9
|
+
"homepage": "https://the-untitled-org.github.io/wasla-genie/",
|
|
10
|
+
"preferGlobal": true,
|
|
9
11
|
"type": "module",
|
|
10
12
|
"main": "dist/apps/cli/src/index.js",
|
|
11
13
|
"bin": {
|
|
@@ -79,7 +81,7 @@
|
|
|
79
81
|
"test:integration": "vitest run tests/integration",
|
|
80
82
|
"test:validation": "vitest run tests/validation",
|
|
81
83
|
"test:coverage": "vitest run --coverage",
|
|
82
|
-
"coverage:open": "open output/index.html",
|
|
84
|
+
"coverage:open": "open-cli output/index.html",
|
|
83
85
|
"docs": "cd docs && npm run start",
|
|
84
86
|
"docs:install": "npm --prefix docs install",
|
|
85
87
|
"docs:build": "cd docs && npm run build",
|
|
@@ -127,6 +129,7 @@
|
|
|
127
129
|
"@types/ws": "^8.18.1",
|
|
128
130
|
"@vitest/coverage-v8": "^1.6.1",
|
|
129
131
|
"@vitest/ui": "^1.0.0",
|
|
132
|
+
"open-cli": "^9.0.0",
|
|
130
133
|
"prettier": "^3.1.0",
|
|
131
134
|
"typescript": "^5.3.3",
|
|
132
135
|
"vitest": "^1.6.1"
|