@untitled-devs/wasla 1.0.0 → 1.0.2

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.
@@ -10,17 +10,17 @@ export async function readConfiguredScope() {
10
10
  return null;
11
11
  const config = await readJSON(configPath);
12
12
  if (typeof config !== 'object' || config === null || typeof config.scope !== 'string') {
13
- throw new Error(`Invalid scope in ${configPath}. Run: waslagenie config --scope <scope>`);
13
+ throw new Error(`Invalid scope in ${configPath}. Run: wasla config --scope <scope>`);
14
14
  }
15
15
  if (config.scope !== 'user' && config.scope !== 'workspace') {
16
- throw new Error(`Invalid scope in ${configPath}. Run: waslagenie config --scope <scope>`);
16
+ throw new Error(`Invalid scope in ${configPath}. Run: wasla config --scope <scope>`);
17
17
  }
18
18
  return config.scope;
19
19
  }
20
20
  export async function requireConfiguredScope() {
21
21
  const scope = await readConfiguredScope();
22
22
  if (!scope) {
23
- throw new Error('Scope is not configured. Run: waslagenie config --scope <user|workspace>');
23
+ throw new Error('Scope is not configured. Run: wasla config --scope <user|workspace>');
24
24
  }
25
25
  return scope;
26
26
  }
@@ -11,18 +11,18 @@ export function getRegistryPath(scope) {
11
11
  return resolve(`output/tests/${scope}-registry.json`);
12
12
  }
13
13
  if (scope === 'user') {
14
- return expandTilde('~/.waslagenie/registry.json');
14
+ return expandTilde('~/.wasla/registry.json');
15
15
  }
16
- return resolve('.waslagenie/registry.json');
16
+ return resolve('.wasla/registry.json');
17
17
  }
18
18
  export function getRegistryDir(scope) {
19
19
  if (process.env.NODE_ENV === 'test') {
20
20
  return resolve('output/tests');
21
21
  }
22
22
  if (scope === 'user') {
23
- return expandTilde('~/.waslagenie');
23
+ return expandTilde('~/.wasla');
24
24
  }
25
- return resolve('.waslagenie');
25
+ return resolve('.wasla');
26
26
  }
27
27
  export function getToolMarkers(scope = 'user') {
28
28
  if (scope === 'workspace') {
@@ -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'
@@ -284,7 +261,7 @@ export class Syncer {
284
261
  catch {
285
262
  await this.registry.load();
286
263
  }
287
- const candidates = sourceTool === 'waslagenie'
264
+ const candidates = sourceTool === 'wasla'
288
265
  ? (await this.scanner.scanAllTools([type])).filter((item) => item.name === name)
289
266
  : (await this.scanner.scanTool(sourceTool, [type])).filter((item) => item.name === name);
290
267
  const sourceCandidates = candidates.filter((item) => item.tool !== targetTool);
@@ -292,7 +269,7 @@ export class Syncer {
292
269
  if (items.length === 0) {
293
270
  throw new Error(`Cannot find ${type}:${name} in ${sourceTool}`);
294
271
  }
295
- const sourceItems = sourceTool === 'waslagenie' ? items.filter((item) => item.tool === items[0].tool) : items;
272
+ const sourceItems = sourceTool === 'wasla' ? items.filter((item) => item.tool === items[0].tool) : items;
296
273
  const sorted = sourceItems.sort((a, b) => b.modifiedAt - a.modifiedAt);
297
274
  const source = type === 'skill'
298
275
  ? sorted.find((item) => item.relativePath.endsWith(`${sep}SKILL.md`) || item.relativePath === 'SKILL.md') || sorted[0]
@@ -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;
@@ -113,9 +113,9 @@ export class Scanner {
113
113
  if (process.env.DEBUG_SCANNER) {
114
114
  console.log(`[Scanner] Processing: path=${filePath}, rel=${relativePath}, name=${name}, isStub=${isStub}`);
115
115
  }
116
- if (name.toLowerCase() === 'waslagenie') {
116
+ if (name.toLowerCase() === 'wasla') {
117
117
  if (process.env.DEBUG_SCANNER) {
118
- console.log(`[Scanner] Skipping waslagenie asset`);
118
+ console.log(`[Scanner] Skipping wasla asset`);
119
119
  }
120
120
  continue;
121
121
  }
@@ -201,7 +201,7 @@ export class Scanner {
201
201
  return grouped;
202
202
  }
203
203
  extractAssetName(relativePathOrFileName) {
204
- // For nested paths: waslagenie/SKILL.md -> waslagenie
204
+ // For nested paths: wasla/SKILL.md -> wasla
205
205
  // For flat files: researcher.md -> researcher
206
206
  const parts = relativePathOrFileName.split(/[/\\]/);
207
207
  if (parts.length > 1) {