busy-cli 0.2.0 → 0.3.0
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/dist/cli/index.js +4 -3
- package/dist/commands/package.d.ts +5 -2
- package/dist/commands/package.d.ts.map +1 -1
- package/dist/commands/package.js +31 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +50 -30
- package/dist/package/manifest.d.ts +12 -0
- package/dist/package/manifest.d.ts.map +1 -1
- package/dist/package/manifest.js +118 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +12 -8
- package/dist/types/schema.d.ts +1650 -76
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/types/schema.js +12 -2
- package/package.json +1 -1
- package/src/__tests__/view-config.test.ts +277 -0
- package/src/cli/index.ts +4 -3
- package/src/commands/package.ts +32 -3
- package/src/index.ts +3 -0
- package/src/loader.ts +66 -38
- package/src/package/manifest.ts +138 -0
- package/src/parser.ts +12 -8
- package/src/types/schema.ts +17 -3
package/src/package/manifest.ts
CHANGED
|
@@ -347,3 +347,141 @@ export async function fetchPackageFromManifest(
|
|
|
347
347
|
integrity,
|
|
348
348
|
};
|
|
349
349
|
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Recursively discover all files in a directory.
|
|
353
|
+
* Skips hidden directories (e.g. .git, .libraries) and node_modules.
|
|
354
|
+
*/
|
|
355
|
+
export async function discoverFiles(dirPath: string): Promise<string[]> {
|
|
356
|
+
const results: string[] = [];
|
|
357
|
+
|
|
358
|
+
async function walk(dir: string) {
|
|
359
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
360
|
+
for (const entry of entries) {
|
|
361
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const fullPath = path.join(dir, entry.name);
|
|
365
|
+
if (entry.isDirectory()) {
|
|
366
|
+
await walk(fullPath);
|
|
367
|
+
} else if (entry.isFile()) {
|
|
368
|
+
results.push(fullPath);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await walk(dirPath);
|
|
374
|
+
return results;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Fetch a package from a local folder, copying all files.
|
|
379
|
+
*
|
|
380
|
+
* If a package.busy.md exists, its metadata (name, version, description) is used.
|
|
381
|
+
* Otherwise, metadata is derived from the folder name.
|
|
382
|
+
*/
|
|
383
|
+
export async function fetchPackageFromLocalFolder(
|
|
384
|
+
workspaceRoot: string,
|
|
385
|
+
folderPath: string,
|
|
386
|
+
): Promise<FetchPackageResult> {
|
|
387
|
+
const absolutePath = path.isAbsolute(folderPath)
|
|
388
|
+
? folderPath
|
|
389
|
+
: path.resolve(process.cwd(), folderPath);
|
|
390
|
+
|
|
391
|
+
// Verify it's a directory
|
|
392
|
+
const stat = await fs.stat(absolutePath);
|
|
393
|
+
if (!stat.isDirectory()) {
|
|
394
|
+
throw new Error(`Not a directory: ${absolutePath}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Try to read manifest for metadata
|
|
398
|
+
const manifestPath = path.join(absolutePath, 'package.busy.md');
|
|
399
|
+
let manifest: PackageManifest | null = null;
|
|
400
|
+
let manifestContent: string | null = null;
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
404
|
+
manifest = parsePackageManifest(manifestContent);
|
|
405
|
+
} catch {
|
|
406
|
+
// No manifest - that's fine
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const packageName = manifest?.name || path.basename(absolutePath);
|
|
410
|
+
|
|
411
|
+
// Discover all files in the folder
|
|
412
|
+
const discoveredFiles = await discoverFiles(absolutePath);
|
|
413
|
+
|
|
414
|
+
// Filter out package.busy.md itself (handled separately)
|
|
415
|
+
const filesToCopy = discoveredFiles
|
|
416
|
+
.filter(f => path.basename(f) !== 'package.busy.md')
|
|
417
|
+
.map(f => ({
|
|
418
|
+
relativePath: path.relative(absolutePath, f),
|
|
419
|
+
absolutePath: f,
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
// Initialize cache
|
|
423
|
+
const cache = new CacheManager(workspaceRoot);
|
|
424
|
+
await cache.init();
|
|
425
|
+
|
|
426
|
+
// Copy files
|
|
427
|
+
let combinedContent = '';
|
|
428
|
+
const documents: PackageDocument[] = [];
|
|
429
|
+
|
|
430
|
+
for (const file of filesToCopy) {
|
|
431
|
+
try {
|
|
432
|
+
const content = await fs.readFile(file.absolutePath, 'utf-8');
|
|
433
|
+
combinedContent += content;
|
|
434
|
+
|
|
435
|
+
const cachePath = path.join(packageName, file.relativePath);
|
|
436
|
+
await cache.save(cachePath, content);
|
|
437
|
+
|
|
438
|
+
documents.push({
|
|
439
|
+
name: path.basename(file.relativePath, path.extname(file.relativePath)),
|
|
440
|
+
relativePath: './' + file.relativePath,
|
|
441
|
+
});
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.warn(`Warning: Failed to read ${file.absolutePath}: ${error}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Save manifest if it exists
|
|
448
|
+
if (manifestContent) {
|
|
449
|
+
await cache.save(path.join(packageName, 'package.busy.md'), manifestContent);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Calculate integrity
|
|
453
|
+
const integrity = calculateIntegrity(combinedContent);
|
|
454
|
+
|
|
455
|
+
// Add to registry
|
|
456
|
+
const registry = new PackageRegistry(workspaceRoot);
|
|
457
|
+
try {
|
|
458
|
+
await registry.load();
|
|
459
|
+
} catch {
|
|
460
|
+
await registry.init();
|
|
461
|
+
await registry.load();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const entry: PackageEntry = {
|
|
465
|
+
id: packageName,
|
|
466
|
+
description: manifest?.description || '',
|
|
467
|
+
source: absolutePath,
|
|
468
|
+
provider: 'local',
|
|
469
|
+
cached: `.libraries/${packageName}`,
|
|
470
|
+
version: manifest?.version || 'latest',
|
|
471
|
+
fetched: new Date().toISOString(),
|
|
472
|
+
integrity,
|
|
473
|
+
category: 'Packages',
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
registry.addPackage(entry);
|
|
477
|
+
await registry.save();
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
name: packageName,
|
|
481
|
+
version: manifest?.version || 'latest',
|
|
482
|
+
description: manifest?.description || '',
|
|
483
|
+
documents,
|
|
484
|
+
cached: `.libraries/${packageName}`,
|
|
485
|
+
integrity,
|
|
486
|
+
};
|
|
487
|
+
}
|
package/src/parser.ts
CHANGED
|
@@ -217,14 +217,16 @@ export function resolveImports(
|
|
|
217
217
|
// Resolve the import path
|
|
218
218
|
const importPath = resolve(dirname(basePath), imp.path);
|
|
219
219
|
|
|
220
|
-
// Check for circular imports
|
|
220
|
+
// Check for circular imports — warn and skip instead of crashing
|
|
221
221
|
if (visited.has(importPath)) {
|
|
222
|
-
|
|
222
|
+
console.warn(`⚠ Circular import skipped: ${imp.path} (from ${basePath})`);
|
|
223
|
+
continue;
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
// Check if file exists
|
|
226
|
+
// Check if file exists — warn and skip instead of crashing
|
|
226
227
|
if (!existsSync(importPath)) {
|
|
227
|
-
|
|
228
|
+
console.warn(`⚠ Import not found: ${imp.path} (resolved to ${importPath})`);
|
|
229
|
+
continue;
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
// Mark as visited
|
|
@@ -248,7 +250,7 @@ export function resolveImports(
|
|
|
248
250
|
);
|
|
249
251
|
|
|
250
252
|
if (!hasOperation && !hasDefinition) {
|
|
251
|
-
|
|
253
|
+
console.warn(`⚠ Anchor '${imp.anchor}' not found in ${imp.path}`);
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
|
|
@@ -258,10 +260,12 @@ export function resolveImports(
|
|
|
258
260
|
// Recursively resolve imports in the imported document
|
|
259
261
|
const nestedResolved = resolveImports(importedDoc, importPath, visited);
|
|
260
262
|
Object.assign(resolved, nestedResolved);
|
|
261
|
-
}
|
|
262
|
-
//
|
|
263
|
-
|
|
263
|
+
} catch (e) {
|
|
264
|
+
// Don't crash on nested resolution failures
|
|
265
|
+
console.warn(`⚠ Failed to resolve nested imports in ${imp.path}: ${e}`);
|
|
264
266
|
}
|
|
267
|
+
// NOTE: we intentionally keep importPath in `visited` — once resolved,
|
|
268
|
+
// don't re-resolve in other branches (prevents exponential recursion)
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
return resolved;
|
package/src/types/schema.ts
CHANGED
|
@@ -160,7 +160,7 @@ type Section = {
|
|
|
160
160
|
};
|
|
161
161
|
|
|
162
162
|
type ConceptBase = {
|
|
163
|
-
kind: 'concept' | 'document' | 'operation' | 'checklist' | 'tool' | 'playbook' | 'localdef' | 'importdef' | 'setup';
|
|
163
|
+
kind: 'concept' | 'document' | 'operation' | 'checklist' | 'tool' | 'playbook' | 'view' | 'config' | 'localdef' | 'importdef' | 'setup';
|
|
164
164
|
id: string;
|
|
165
165
|
docId: string;
|
|
166
166
|
slug: string;
|
|
@@ -194,7 +194,7 @@ export type { Section };
|
|
|
194
194
|
|
|
195
195
|
// ConceptBase schema - need to keep as regular object schema to allow .extend()
|
|
196
196
|
const ConceptBaseSchemaObject = z.object({
|
|
197
|
-
kind: z.enum(['concept', 'document', 'operation', 'checklist', 'tool', 'playbook', 'localdef', 'importdef', 'setup']),
|
|
197
|
+
kind: z.enum(['concept', 'document', 'operation', 'checklist', 'tool', 'playbook', 'view', 'config', 'localdef', 'importdef', 'setup']),
|
|
198
198
|
id: ConceptIdSchema,
|
|
199
199
|
docId: DocIdSchema,
|
|
200
200
|
slug: z.string(),
|
|
@@ -261,6 +261,18 @@ export const PlaybookSchema = LegacyBusyDocumentSchema.extend({
|
|
|
261
261
|
sequence: z.array(ConceptIdSchema), // Ordered array of operation references
|
|
262
262
|
})
|
|
263
263
|
|
|
264
|
+
// View schema - extends LegacyBusyDocument with display section
|
|
265
|
+
// Views follow MVC: imports=Model, localDefs=ViewModel, template=View, operations=Controller
|
|
266
|
+
export const ViewSchema = LegacyBusyDocumentSchema.extend({
|
|
267
|
+
kind: z.literal('view'),
|
|
268
|
+
display: z.string().optional(), // Markdown template (optional — LORE can generate)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// Config schema - extends LegacyBusyDocument, semantically a singleton Model
|
|
272
|
+
export const ConfigSchema = LegacyBusyDocumentSchema.extend({
|
|
273
|
+
kind: z.literal('config'),
|
|
274
|
+
})
|
|
275
|
+
|
|
264
276
|
// Edge schema
|
|
265
277
|
export const EdgeRoleSchema = z.enum(['ref', 'calls', 'extends', 'imports']);
|
|
266
278
|
export const EdgeSchema = z.object({
|
|
@@ -287,7 +299,7 @@ export const RepoSchema = z.object({
|
|
|
287
299
|
byId: z.record(z.union([SectionSchema, LocalDefSchema, LegacyOperationSchema, ConceptBaseSchema])),
|
|
288
300
|
byFile: z.record( // Renamed from byDoc for clarity
|
|
289
301
|
z.object({
|
|
290
|
-
concept: z.union([LegacyBusyDocumentSchema, PlaybookSchema]), // The concept defined in this file
|
|
302
|
+
concept: z.union([LegacyBusyDocumentSchema, PlaybookSchema, ViewSchema, ConfigSchema]), // The concept defined in this file
|
|
291
303
|
bySlug: z.record(SectionSchema),
|
|
292
304
|
})
|
|
293
305
|
),
|
|
@@ -319,6 +331,8 @@ export type Slug = z.infer<typeof SlugSchema>;
|
|
|
319
331
|
// Section and ConceptBase types defined above to avoid circular references
|
|
320
332
|
export type LegacyBusyDocument = z.infer<typeof LegacyBusyDocumentSchema>;
|
|
321
333
|
export type Playbook = z.infer<typeof PlaybookSchema>;
|
|
334
|
+
export type View = z.infer<typeof ViewSchema>;
|
|
335
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
322
336
|
export type LocalDef = z.infer<typeof LocalDefSchema>;
|
|
323
337
|
export type LegacyOperation = z.infer<typeof LegacyOperationSchema>;
|
|
324
338
|
export type ImportDef = z.infer<typeof ImportDefSchema>;
|