@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.
- package/README.md +65 -74
- package/dist/apps/cli/src/cli-output.js +5 -5
- package/dist/apps/cli/src/commands/config.js +1 -1
- package/dist/apps/cli/src/commands/install.js +6 -6
- package/dist/apps/cli/src/commands/register.js +1 -1
- package/dist/apps/cli/src/commands/status.js +1 -1
- package/dist/apps/cli/src/commands/sync-to.js +2 -2
- package/dist/apps/cli/src/commands/watch.js +1 -1
- package/dist/apps/cli/src/index.js +4 -4
- package/dist/apps/cli/src/server/visualizer-server.js +6 -7
- package/dist/packages/adapters/src/base.d.ts +2 -2
- package/dist/packages/adapters/src/claude.js +14 -11
- package/dist/packages/adapters/src/factory.d.ts +4 -4
- package/dist/packages/adapters/src/gemini.js +10 -10
- package/dist/packages/core/src/types.d.ts +1 -1
- package/dist/packages/shared/src/config.js +3 -3
- package/dist/packages/shared/src/paths.js +4 -4
- package/dist/packages/sync/src/index.d.ts +9 -0
- package/dist/packages/sync/src/index.js +56 -33
- package/dist/packages/sync/src/scanner.js +3 -3
- package/dist/visualizer/assets/{index-cU_xphSj.js → index-DF8-6vb2.js} +1 -1
- package/dist/visualizer/favicon.png +0 -0
- package/dist/visualizer/index.html +3 -2
- package/dist/visualizer/logo.png +0 -0
- package/package.json +7 -4
|
@@ -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:
|
|
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:
|
|
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:
|
|
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('~/.
|
|
14
|
+
return expandTilde('~/.wasla/registry.json');
|
|
15
15
|
}
|
|
16
|
-
return resolve('.
|
|
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('~/.
|
|
23
|
+
return expandTilde('~/.wasla');
|
|
24
24
|
}
|
|
25
|
-
return resolve('.
|
|
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
|
-
|
|
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'
|
|
@@ -284,7 +261,7 @@ export class Syncer {
|
|
|
284
261
|
catch {
|
|
285
262
|
await this.registry.load();
|
|
286
263
|
}
|
|
287
|
-
const candidates = sourceTool === '
|
|
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 === '
|
|
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() === '
|
|
116
|
+
if (name.toLowerCase() === 'wasla') {
|
|
117
117
|
if (process.env.DEBUG_SCANNER) {
|
|
118
|
-
console.log(`[Scanner] Skipping
|
|
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:
|
|
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) {
|