busy-cli 0.2.0 → 0.2.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/dist/cli/index.js CHANGED
@@ -310,14 +310,15 @@ const packageCmd = program
310
310
  // Package add
311
311
  packageCmd
312
312
  .command('add')
313
- .description('Add a package from URL')
314
- .argument('<url>', 'URL to the BUSY document')
313
+ .description('Add a package from URL or local folder')
314
+ .argument('<url>', 'URL or local path to the BUSY document or folder')
315
315
  .option('-d, --dir <directory>', 'Workspace directory', '.')
316
+ .option('-r, --recursive', 'Recursively add all files from a local folder')
316
317
  .action(async (url, options) => {
317
318
  try {
318
319
  const workspaceRoot = resolve(options.dir);
319
320
  console.log(`\nAdding package from: ${url}\n`);
320
- const result = await addPackage(workspaceRoot, url);
321
+ const result = await addPackage(workspaceRoot, url, { recursive: options.recursive });
321
322
  console.log(` ID: ${result.id}`);
322
323
  console.log(` Provider: ${result.provider}`);
323
324
  console.log(` Version: ${result.version}`);
@@ -71,12 +71,15 @@ export declare function checkWorkspace(workspaceRoot: string, options?: {
71
71
  skipExternal?: boolean;
72
72
  }): Promise<CheckResult>;
73
73
  /**
74
- * Add a package from URL
74
+ * Add a package from URL or local folder
75
75
  *
76
76
  * If the URL points to a package.busy.md manifest, fetches the entire package.
77
+ * If it's a local directory with --recursive (or without a manifest), copies all files.
77
78
  * Otherwise, fetches a single file.
78
79
  */
79
- export declare function addPackage(workspaceRoot: string, url: string): Promise<AddResult>;
80
+ export declare function addPackage(workspaceRoot: string, url: string, options?: {
81
+ recursive?: boolean;
82
+ }): Promise<AddResult>;
80
83
  /**
81
84
  * Remove a package
82
85
  */
@@ -1 +1 @@
1
- {"version":3,"file":"package.d.ts","sourceRoot":"","sources":["../../src/commands/package.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAmB,YAAY,EAAiC,MAAM,sBAAsB,CAAC;AAKpG,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,wBAAwB,CAAC;AAChC,OAAO,qBAAqB,CAAC;AAE7B;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkC9E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CA8CtH;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAoFvF;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAwBnG;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ7E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAM3G;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA4ErG"}
1
+ {"version":3,"file":"package.d.ts","sourceRoot":"","sources":["../../src/commands/package.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAmB,YAAY,EAAiC,MAAM,sBAAsB,CAAC;AAKpG,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,wBAAwB,CAAC;AAChC,OAAO,qBAAqB,CAAC;AAE7B;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkC9E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CA8CtH;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAgH1H;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAwBnG;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ7E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAM3G;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA4ErG"}
@@ -8,7 +8,7 @@ import * as path from 'node:path';
8
8
  import { CacheManager, deriveCachePath } from '../cache/index.js';
9
9
  import { PackageRegistry, deriveEntryId, deriveCategory } from '../registry/index.js';
10
10
  import { providerRegistry } from '../providers/index.js';
11
- import { isPackageManifestUrl, fetchPackageFromManifest } from '../package/manifest.js';
11
+ import { isPackageManifestUrl, fetchPackageFromManifest, fetchPackageFromLocalFolder } from '../package/manifest.js';
12
12
  // Ensure providers are registered
13
13
  import '../providers/local.js';
14
14
  import '../providers/github.js';
@@ -94,12 +94,40 @@ export async function checkWorkspace(workspaceRoot, options) {
94
94
  };
95
95
  }
96
96
  /**
97
- * Add a package from URL
97
+ * Add a package from URL or local folder
98
98
  *
99
99
  * If the URL points to a package.busy.md manifest, fetches the entire package.
100
+ * If it's a local directory with --recursive (or without a manifest), copies all files.
100
101
  * Otherwise, fetches a single file.
101
102
  */
102
- export async function addPackage(workspaceRoot, url) {
103
+ export async function addPackage(workspaceRoot, url, options) {
104
+ // Check if this is a local directory that should use folder-based discovery
105
+ if (url.startsWith('./') || url.startsWith('../') || url.startsWith('/') || (!url.includes('://') && !url.startsWith('http'))) {
106
+ const resolvedPath = path.isAbsolute(url) ? url : path.resolve(process.cwd(), url);
107
+ try {
108
+ const stat = await fs.stat(resolvedPath);
109
+ if (stat.isDirectory()) {
110
+ const manifestExists = await fs.stat(path.join(resolvedPath, 'package.busy.md'))
111
+ .then(() => true).catch(() => false);
112
+ if (options?.recursive || !manifestExists) {
113
+ // Use folder-based discovery: --recursive flag or no manifest available
114
+ const result = await fetchPackageFromLocalFolder(workspaceRoot, resolvedPath);
115
+ return {
116
+ id: result.name,
117
+ source: resolvedPath,
118
+ provider: 'local',
119
+ cached: result.cached,
120
+ version: result.version,
121
+ integrity: result.integrity,
122
+ };
123
+ }
124
+ // Has manifest and not --recursive: fall through to manifest-based flow
125
+ }
126
+ }
127
+ catch {
128
+ // Not a directory or doesn't exist - fall through to normal handling
129
+ }
130
+ }
103
131
  // Check if this is a package manifest URL
104
132
  if (isPackageManifestUrl(url)) {
105
133
  // Use manifest-based package installation
@@ -56,4 +56,16 @@ export declare function parsePackageManifest(content: string): PackageManifest;
56
56
  * Fetch a package from its manifest URL or local path
57
57
  */
58
58
  export declare function fetchPackageFromManifest(workspaceRoot: string, manifestUrl: string): Promise<FetchPackageResult>;
59
+ /**
60
+ * Recursively discover all files in a directory.
61
+ * Skips hidden directories (e.g. .git, .libraries) and node_modules.
62
+ */
63
+ export declare function discoverFiles(dirPath: string): Promise<string[]>;
64
+ /**
65
+ * Fetch a package from a local folder, copying all files.
66
+ *
67
+ * If a package.busy.md exists, its metadata (name, version, description) is used.
68
+ * Otherwise, metadata is derived from the folder name.
69
+ */
70
+ export declare function fetchPackageFromLocalFolder(workspaceRoot: string, folderPath: string): Promise<FetchPackageResult>;
59
71
  //# sourceMappingURL=manifest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/package/manifest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,wBAAwB,CAAC;AAChC,OAAO,qBAAqB,CAAC;AAE7B;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,eAAe,EAAE,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAazD;AAyBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAiHrE;AAkBD;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,kBAAkB,CAAC,CA4G7B"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/package/manifest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,wBAAwB,CAAC;AAChC,OAAO,qBAAqB,CAAC;AAE7B;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,eAAe,EAAE,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAazD;AAyBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAiHrE;AAkBD;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,kBAAkB,CAAC,CA4G7B;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAoBtE;AAED;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAC/C,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,kBAAkB,CAAC,CAqG7B"}
@@ -262,4 +262,122 @@ export async function fetchPackageFromManifest(workspaceRoot, manifestUrl) {
262
262
  integrity,
263
263
  };
264
264
  }
265
+ /**
266
+ * Recursively discover all files in a directory.
267
+ * Skips hidden directories (e.g. .git, .libraries) and node_modules.
268
+ */
269
+ export async function discoverFiles(dirPath) {
270
+ const results = [];
271
+ async function walk(dir) {
272
+ const entries = await fs.readdir(dir, { withFileTypes: true });
273
+ for (const entry of entries) {
274
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
275
+ continue;
276
+ }
277
+ const fullPath = path.join(dir, entry.name);
278
+ if (entry.isDirectory()) {
279
+ await walk(fullPath);
280
+ }
281
+ else if (entry.isFile()) {
282
+ results.push(fullPath);
283
+ }
284
+ }
285
+ }
286
+ await walk(dirPath);
287
+ return results;
288
+ }
289
+ /**
290
+ * Fetch a package from a local folder, copying all files.
291
+ *
292
+ * If a package.busy.md exists, its metadata (name, version, description) is used.
293
+ * Otherwise, metadata is derived from the folder name.
294
+ */
295
+ export async function fetchPackageFromLocalFolder(workspaceRoot, folderPath) {
296
+ const absolutePath = path.isAbsolute(folderPath)
297
+ ? folderPath
298
+ : path.resolve(process.cwd(), folderPath);
299
+ // Verify it's a directory
300
+ const stat = await fs.stat(absolutePath);
301
+ if (!stat.isDirectory()) {
302
+ throw new Error(`Not a directory: ${absolutePath}`);
303
+ }
304
+ // Try to read manifest for metadata
305
+ const manifestPath = path.join(absolutePath, 'package.busy.md');
306
+ let manifest = null;
307
+ let manifestContent = null;
308
+ try {
309
+ manifestContent = await fs.readFile(manifestPath, 'utf-8');
310
+ manifest = parsePackageManifest(manifestContent);
311
+ }
312
+ catch {
313
+ // No manifest - that's fine
314
+ }
315
+ const packageName = manifest?.name || path.basename(absolutePath);
316
+ // Discover all files in the folder
317
+ const discoveredFiles = await discoverFiles(absolutePath);
318
+ // Filter out package.busy.md itself (handled separately)
319
+ const filesToCopy = discoveredFiles
320
+ .filter(f => path.basename(f) !== 'package.busy.md')
321
+ .map(f => ({
322
+ relativePath: path.relative(absolutePath, f),
323
+ absolutePath: f,
324
+ }));
325
+ // Initialize cache
326
+ const cache = new CacheManager(workspaceRoot);
327
+ await cache.init();
328
+ // Copy files
329
+ let combinedContent = '';
330
+ const documents = [];
331
+ for (const file of filesToCopy) {
332
+ try {
333
+ const content = await fs.readFile(file.absolutePath, 'utf-8');
334
+ combinedContent += content;
335
+ const cachePath = path.join(packageName, file.relativePath);
336
+ await cache.save(cachePath, content);
337
+ documents.push({
338
+ name: path.basename(file.relativePath, path.extname(file.relativePath)),
339
+ relativePath: './' + file.relativePath,
340
+ });
341
+ }
342
+ catch (error) {
343
+ console.warn(`Warning: Failed to read ${file.absolutePath}: ${error}`);
344
+ }
345
+ }
346
+ // Save manifest if it exists
347
+ if (manifestContent) {
348
+ await cache.save(path.join(packageName, 'package.busy.md'), manifestContent);
349
+ }
350
+ // Calculate integrity
351
+ const integrity = calculateIntegrity(combinedContent);
352
+ // Add to registry
353
+ const registry = new PackageRegistry(workspaceRoot);
354
+ try {
355
+ await registry.load();
356
+ }
357
+ catch {
358
+ await registry.init();
359
+ await registry.load();
360
+ }
361
+ const entry = {
362
+ id: packageName,
363
+ description: manifest?.description || '',
364
+ source: absolutePath,
365
+ provider: 'local',
366
+ cached: `.libraries/${packageName}`,
367
+ version: manifest?.version || 'latest',
368
+ fetched: new Date().toISOString(),
369
+ integrity,
370
+ category: 'Packages',
371
+ };
372
+ registry.addPackage(entry);
373
+ await registry.save();
374
+ return {
375
+ name: packageName,
376
+ version: manifest?.version || 'latest',
377
+ description: manifest?.description || '',
378
+ documents,
379
+ cached: `.libraries/${packageName}`,
380
+ integrity,
381
+ };
382
+ }
265
383
  //# sourceMappingURL=manifest.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busy-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for BUSY document framework - parse, validate, and manage BUSY workspaces",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/cli/index.ts CHANGED
@@ -352,16 +352,17 @@ const packageCmd = program
352
352
  // Package add
353
353
  packageCmd
354
354
  .command('add')
355
- .description('Add a package from URL')
356
- .argument('<url>', 'URL to the BUSY document')
355
+ .description('Add a package from URL or local folder')
356
+ .argument('<url>', 'URL or local path to the BUSY document or folder')
357
357
  .option('-d, --dir <directory>', 'Workspace directory', '.')
358
+ .option('-r, --recursive', 'Recursively add all files from a local folder')
358
359
  .action(async (url: string, options) => {
359
360
  try {
360
361
  const workspaceRoot = resolve(options.dir);
361
362
 
362
363
  console.log(`\nAdding package from: ${url}\n`);
363
364
 
364
- const result = await addPackage(workspaceRoot, url);
365
+ const result = await addPackage(workspaceRoot, url, { recursive: options.recursive });
365
366
 
366
367
  console.log(` ID: ${result.id}`);
367
368
  console.log(` Provider: ${result.provider}`);
@@ -9,7 +9,7 @@ import * as path from 'node:path';
9
9
  import { CacheManager, calculateIntegrity, deriveCachePath } from '../cache/index.js';
10
10
  import { PackageRegistry, PackageEntry, deriveEntryId, deriveCategory } from '../registry/index.js';
11
11
  import { providerRegistry } from '../providers/index.js';
12
- import { isPackageManifestUrl, fetchPackageFromManifest } from '../package/manifest.js';
12
+ import { isPackageManifestUrl, fetchPackageFromManifest, fetchPackageFromLocalFolder } from '../package/manifest.js';
13
13
 
14
14
  // Ensure providers are registered
15
15
  import '../providers/local.js';
@@ -166,12 +166,41 @@ export async function checkWorkspace(workspaceRoot: string, options?: { skipExte
166
166
  }
167
167
 
168
168
  /**
169
- * Add a package from URL
169
+ * Add a package from URL or local folder
170
170
  *
171
171
  * If the URL points to a package.busy.md manifest, fetches the entire package.
172
+ * If it's a local directory with --recursive (or without a manifest), copies all files.
172
173
  * Otherwise, fetches a single file.
173
174
  */
174
- export async function addPackage(workspaceRoot: string, url: string): Promise<AddResult> {
175
+ export async function addPackage(workspaceRoot: string, url: string, options?: { recursive?: boolean }): Promise<AddResult> {
176
+ // Check if this is a local directory that should use folder-based discovery
177
+ if (url.startsWith('./') || url.startsWith('../') || url.startsWith('/') || (!url.includes('://') && !url.startsWith('http'))) {
178
+ const resolvedPath = path.isAbsolute(url) ? url : path.resolve(process.cwd(), url);
179
+ try {
180
+ const stat = await fs.stat(resolvedPath);
181
+ if (stat.isDirectory()) {
182
+ const manifestExists = await fs.stat(path.join(resolvedPath, 'package.busy.md'))
183
+ .then(() => true).catch(() => false);
184
+
185
+ if (options?.recursive || !manifestExists) {
186
+ // Use folder-based discovery: --recursive flag or no manifest available
187
+ const result = await fetchPackageFromLocalFolder(workspaceRoot, resolvedPath);
188
+ return {
189
+ id: result.name,
190
+ source: resolvedPath,
191
+ provider: 'local',
192
+ cached: result.cached,
193
+ version: result.version,
194
+ integrity: result.integrity,
195
+ };
196
+ }
197
+ // Has manifest and not --recursive: fall through to manifest-based flow
198
+ }
199
+ } catch {
200
+ // Not a directory or doesn't exist - fall through to normal handling
201
+ }
202
+ }
203
+
175
204
  // Check if this is a package manifest URL
176
205
  if (isPackageManifestUrl(url)) {
177
206
  // Use manifest-based package installation
@@ -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
+ }