@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
- WaslaGenie is cross-platform via `npx` — no global install required:
124
+ **Install globally:**
125
125
 
126
126
  ```bash
127
- npx @untitled-devs/wasla config --scope workspace
128
- npx @untitled-devs/wasla sync
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. This runs the CLI directly.
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 install globally:**
134
+ **Or run via `npx` (no global installation required):**
135
135
 
136
136
  ```bash
137
- npm install -g @untitled-devs/wasla
138
- waslagenie config --scope workspace
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 install -g wasla-genie`: **Yes**, reinstall (or relink) to test your latest local changes.
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: join(markers.claude, 'CLAUDE.md'),
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
- const existingStub = asset.stubs.find((s) => s.tool === tool);
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
- const existingStub = asset.stubs.find((s) => s.tool === tool);
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.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"