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.
@@ -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
- throw new Error(`Circular import detected: ${importPath}`);
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
- throw new Error(`Import not found: ${imp.path} (resolved to ${importPath})`);
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
- throw new Error(`Anchor '${imp.anchor}' not found in ${imp.path}`);
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
- } finally {
262
- // Remove from visited after processing (allow same doc in different branches)
263
- visited.delete(importPath);
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;
@@ -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>;