planmode 0.3.0 → 0.4.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/index.js CHANGED
@@ -1377,18 +1377,240 @@ var init_doctor = __esm({
1377
1377
  }
1378
1378
  });
1379
1379
 
1380
+ // src/lib/context.ts
1381
+ import fs15 from "fs";
1382
+ import path15 from "path";
1383
+ import { parse as parse4, stringify as stringify6 } from "yaml";
1384
+ function getContextPath(projectDir) {
1385
+ return path15.join(projectDir, CONTEXT_DIR, CONTEXT_FILE);
1386
+ }
1387
+ function emptyIndex() {
1388
+ return { version: 1, repos: [] };
1389
+ }
1390
+ function readContextIndex(projectDir = process.cwd()) {
1391
+ const filePath = getContextPath(projectDir);
1392
+ try {
1393
+ const raw = fs15.readFileSync(filePath, "utf-8");
1394
+ const data = parse4(raw);
1395
+ return data ?? emptyIndex();
1396
+ } catch {
1397
+ return emptyIndex();
1398
+ }
1399
+ }
1400
+ function writeContextIndex(index, projectDir = process.cwd()) {
1401
+ const dirPath = path15.join(projectDir, CONTEXT_DIR);
1402
+ fs15.mkdirSync(dirPath, { recursive: true });
1403
+ const filePath = getContextPath(projectDir);
1404
+ fs15.writeFileSync(filePath, stringify6(index), "utf-8");
1405
+ }
1406
+ function walkDirectory(dirPath) {
1407
+ const files = [];
1408
+ function walk(currentPath) {
1409
+ let entries;
1410
+ try {
1411
+ entries = fs15.readdirSync(currentPath, { withFileTypes: true });
1412
+ } catch {
1413
+ return;
1414
+ }
1415
+ for (const entry of entries) {
1416
+ if (entry.name.startsWith(".") && IGNORED_DIRS.has(entry.name)) continue;
1417
+ if (IGNORED_DIRS.has(entry.name)) continue;
1418
+ const fullPath = path15.join(currentPath, entry.name);
1419
+ if (entry.isDirectory()) {
1420
+ walk(fullPath);
1421
+ } else if (entry.isFile()) {
1422
+ const ext = path15.extname(entry.name).toLowerCase();
1423
+ if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
1424
+ try {
1425
+ const stat = fs15.statSync(fullPath);
1426
+ const relativePath = path15.relative(dirPath, fullPath);
1427
+ files.push({
1428
+ path: relativePath,
1429
+ extension: ext,
1430
+ size: stat.size,
1431
+ modified_at: stat.mtime.toISOString()
1432
+ });
1433
+ } catch {
1434
+ }
1435
+ }
1436
+ }
1437
+ }
1438
+ walk(dirPath);
1439
+ return files;
1440
+ }
1441
+ function addContextRepo(repoPath, options = {}) {
1442
+ const projectDir = options.projectDir ?? process.cwd();
1443
+ const absolutePath = path15.resolve(projectDir, repoPath);
1444
+ if (!fs15.existsSync(absolutePath)) {
1445
+ throw new Error(`Directory not found: ${repoPath}`);
1446
+ }
1447
+ if (!fs15.statSync(absolutePath).isDirectory()) {
1448
+ throw new Error(`Not a directory: ${repoPath}`);
1449
+ }
1450
+ const index = readContextIndex(projectDir);
1451
+ const relative = path15.relative(projectDir, absolutePath);
1452
+ const isInsideProject = !relative.startsWith("..") && !path15.isAbsolute(relative);
1453
+ const storedPath = isInsideProject ? relative : absolutePath;
1454
+ const existing = index.repos.find(
1455
+ (r) => r.repo.path === storedPath || r.repo.name === options.name
1456
+ );
1457
+ if (existing) {
1458
+ throw new Error(
1459
+ `Context repo already exists: ${existing.repo.name ?? existing.repo.path}. Use \`planmode context reindex\` to refresh.`
1460
+ );
1461
+ }
1462
+ logger.info(`Scanning ${absolutePath}...`);
1463
+ const files = walkDirectory(absolutePath);
1464
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1465
+ const repoIndex = {
1466
+ repo: {
1467
+ path: storedPath,
1468
+ name: options.name,
1469
+ added_at: now
1470
+ },
1471
+ files,
1472
+ indexed_at: now,
1473
+ file_count: files.length,
1474
+ total_size: files.reduce((sum, f) => sum + f.size, 0)
1475
+ };
1476
+ index.repos.push(repoIndex);
1477
+ writeContextIndex(index, projectDir);
1478
+ logger.success(`Added "${options.name ?? storedPath}" \u2014 ${files.length} file(s), ${formatSize(repoIndex.total_size)}`);
1479
+ const breakdown = getTypeBreakdown(files);
1480
+ if (breakdown.length > 0) {
1481
+ logger.dim(` ${breakdown.join(", ")}`);
1482
+ }
1483
+ return repoIndex;
1484
+ }
1485
+ function removeContextRepo(pathOrName, projectDir = process.cwd()) {
1486
+ const index = readContextIndex(projectDir);
1487
+ const idx = index.repos.findIndex(
1488
+ (r) => r.repo.path === pathOrName || r.repo.name === pathOrName
1489
+ );
1490
+ if (idx === -1) {
1491
+ throw new Error(`Context repo not found: ${pathOrName}`);
1492
+ }
1493
+ const removed = index.repos[idx];
1494
+ index.repos.splice(idx, 1);
1495
+ writeContextIndex(index, projectDir);
1496
+ logger.success(`Removed "${removed.repo.name ?? removed.repo.path}"`);
1497
+ }
1498
+ function reindexContext(pathOrName, projectDir = process.cwd()) {
1499
+ const index = readContextIndex(projectDir);
1500
+ if (index.repos.length === 0) {
1501
+ throw new Error("No context repos configured. Use `planmode context add <path>` first.");
1502
+ }
1503
+ const targets = pathOrName ? index.repos.filter(
1504
+ (r) => r.repo.path === pathOrName || r.repo.name === pathOrName
1505
+ ) : index.repos;
1506
+ if (pathOrName && targets.length === 0) {
1507
+ throw new Error(`Context repo not found: ${pathOrName}`);
1508
+ }
1509
+ for (const repo of targets) {
1510
+ const absolutePath = path15.resolve(projectDir, repo.repo.path);
1511
+ if (!fs15.existsSync(absolutePath)) {
1512
+ logger.warn(`Directory not found, skipping: ${repo.repo.path}`);
1513
+ continue;
1514
+ }
1515
+ logger.info(`Re-scanning ${repo.repo.name ?? repo.repo.path}...`);
1516
+ const files = walkDirectory(absolutePath);
1517
+ repo.files = files;
1518
+ repo.indexed_at = (/* @__PURE__ */ new Date()).toISOString();
1519
+ repo.file_count = files.length;
1520
+ repo.total_size = files.reduce((sum, f) => sum + f.size, 0);
1521
+ logger.success(`Reindexed "${repo.repo.name ?? repo.repo.path}" \u2014 ${files.length} file(s), ${formatSize(repo.total_size)}`);
1522
+ }
1523
+ writeContextIndex(index, projectDir);
1524
+ }
1525
+ function getContextSummary(projectDir = process.cwd()) {
1526
+ const index = readContextIndex(projectDir);
1527
+ return {
1528
+ totalRepos: index.repos.length,
1529
+ totalFiles: index.repos.reduce((sum, r) => sum + r.file_count, 0),
1530
+ totalSize: index.repos.reduce((sum, r) => sum + r.total_size, 0),
1531
+ repos: index.repos.map((r) => ({
1532
+ name: r.repo.name ?? r.repo.path,
1533
+ path: r.repo.path,
1534
+ fileCount: r.file_count,
1535
+ totalSize: r.total_size,
1536
+ typeBreakdown: getTypeBreakdown(r.files),
1537
+ indexedAt: r.indexed_at
1538
+ }))
1539
+ };
1540
+ }
1541
+ function getTypeBreakdown(files) {
1542
+ const counts = /* @__PURE__ */ new Map();
1543
+ for (const file of files) {
1544
+ counts.set(file.extension, (counts.get(file.extension) ?? 0) + 1);
1545
+ }
1546
+ return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]).map(([ext, count]) => `${ext}: ${count}`);
1547
+ }
1548
+ function formatSize(bytes) {
1549
+ if (bytes < 1024) return `${bytes} B`;
1550
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1551
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1552
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
1553
+ }
1554
+ var CONTEXT_DIR, CONTEXT_FILE, SUPPORTED_EXTENSIONS, IGNORED_DIRS;
1555
+ var init_context = __esm({
1556
+ "src/lib/context.ts"() {
1557
+ "use strict";
1558
+ init_logger();
1559
+ CONTEXT_DIR = ".planmode";
1560
+ CONTEXT_FILE = "context.yaml";
1561
+ SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
1562
+ ".txt",
1563
+ ".md",
1564
+ ".markdown",
1565
+ ".pdf",
1566
+ ".rtf",
1567
+ ".doc",
1568
+ ".docx",
1569
+ ".csv",
1570
+ ".tsv",
1571
+ ".json",
1572
+ ".yaml",
1573
+ ".yml",
1574
+ ".xml",
1575
+ ".html",
1576
+ ".htm",
1577
+ ".rst",
1578
+ ".org",
1579
+ ".tex",
1580
+ ".log"
1581
+ ]);
1582
+ IGNORED_DIRS = /* @__PURE__ */ new Set([
1583
+ "node_modules",
1584
+ ".git",
1585
+ "dist",
1586
+ "build",
1587
+ ".next",
1588
+ "__pycache__",
1589
+ ".venv",
1590
+ "venv",
1591
+ ".tox",
1592
+ "target",
1593
+ "out",
1594
+ ".cache",
1595
+ ".turbo",
1596
+ "coverage",
1597
+ ".nyc_output"
1598
+ ]);
1599
+ }
1600
+ });
1601
+
1380
1602
  // src/commands/interactive.ts
1381
1603
  var interactive_exports = {};
1382
1604
  __export(interactive_exports, {
1383
1605
  runInteractiveMenu: () => runInteractiveMenu
1384
1606
  });
1385
- import fs15 from "fs";
1386
- import path15 from "path";
1607
+ import fs16 from "fs";
1608
+ import path16 from "path";
1387
1609
  import os3 from "os";
1388
1610
  import * as p12 from "@clack/prompts";
1389
1611
  function isFirstRun() {
1390
- const configPath = path15.join(os3.homedir(), ".planmode", "config");
1391
- const hasConfig = fs15.existsSync(configPath);
1612
+ const configPath = path16.join(os3.homedir(), ".planmode", "config");
1613
+ const hasConfig = fs16.existsSync(configPath);
1392
1614
  const lockfile = readLockfile();
1393
1615
  const hasPackages = Object.keys(lockfile.packages).length > 0;
1394
1616
  return !hasConfig && !hasPackages;
@@ -1517,6 +1739,7 @@ async function mainMenu() {
1517
1739
  { value: "install", label: "Install a package", hint: "install by name" },
1518
1740
  { value: "create", label: "Create a new package" },
1519
1741
  { value: "list", label: "My installed packages" },
1742
+ { value: "context", label: "Manage context", hint: "document directories for AI" },
1520
1743
  { value: "doctor", label: "Health check" },
1521
1744
  { value: "exit", label: "Exit" }
1522
1745
  ]
@@ -1540,6 +1763,9 @@ async function mainMenu() {
1540
1763
  case "list":
1541
1764
  listFlow();
1542
1765
  break;
1766
+ case "context":
1767
+ await contextFlow();
1768
+ break;
1543
1769
  case "doctor":
1544
1770
  doctorFlow();
1545
1771
  break;
@@ -1697,6 +1923,94 @@ async function installFlow() {
1697
1923
  p12.log.error(err.message);
1698
1924
  }
1699
1925
  }
1926
+ async function contextFlow() {
1927
+ const summary = getContextSummary();
1928
+ if (summary.totalRepos === 0) {
1929
+ p12.log.info("No context repos yet.");
1930
+ } else {
1931
+ const lines = summary.repos.map(
1932
+ (r) => `${r.name} \u2014 ${r.fileCount} file(s), ${formatSize(r.totalSize)}`
1933
+ );
1934
+ p12.note(lines.join("\n"), `${summary.totalRepos} context repo(s)`);
1935
+ }
1936
+ const action = handleCancel(
1937
+ await p12.select({
1938
+ message: "What would you like to do?",
1939
+ options: [
1940
+ { value: "add", label: "Add a directory" },
1941
+ { value: "remove", label: "Remove a directory" },
1942
+ { value: "reindex", label: "Re-index all" },
1943
+ { value: "back", label: "Back" }
1944
+ ]
1945
+ })
1946
+ );
1947
+ if (action === "back") return;
1948
+ if (action === "add") {
1949
+ const dirPath = handleCancel(
1950
+ await p12.text({
1951
+ message: "Path to document directory:",
1952
+ placeholder: "e.g. docs, specs, ./reference",
1953
+ validate(input) {
1954
+ if (!input) return "Please enter a directory path";
1955
+ }
1956
+ })
1957
+ );
1958
+ const name = handleCancel(
1959
+ await p12.text({
1960
+ message: "Label (optional):",
1961
+ placeholder: "e.g. Project Documentation"
1962
+ })
1963
+ );
1964
+ try {
1965
+ await withSpinner(
1966
+ "Indexing documents...",
1967
+ async () => addContextRepo(dirPath, { name: name || void 0 }),
1968
+ "Indexing complete"
1969
+ );
1970
+ } catch (err) {
1971
+ p12.log.error(err.message);
1972
+ }
1973
+ } else if (action === "remove") {
1974
+ if (summary.totalRepos === 0) {
1975
+ p12.log.warn("No context repos to remove.");
1976
+ return;
1977
+ }
1978
+ const selected = handleCancel(
1979
+ await p12.select({
1980
+ message: "Select a repo to remove:",
1981
+ options: [
1982
+ ...summary.repos.map((r) => ({
1983
+ value: r.name,
1984
+ label: r.name,
1985
+ hint: `${r.fileCount} files, ${formatSize(r.totalSize)}`
1986
+ })),
1987
+ { value: "__back__", label: "Back" }
1988
+ ]
1989
+ })
1990
+ );
1991
+ if (selected === "__back__") return;
1992
+ try {
1993
+ removeContextRepo(selected);
1994
+ p12.log.success(`Removed "${selected}"`);
1995
+ } catch (err) {
1996
+ p12.log.error(err.message);
1997
+ }
1998
+ } else if (action === "reindex") {
1999
+ if (summary.totalRepos === 0) {
2000
+ p12.log.warn("No context repos to reindex.");
2001
+ return;
2002
+ }
2003
+ try {
2004
+ await withSpinner(
2005
+ "Re-scanning documents...",
2006
+ async () => reindexContext(),
2007
+ "Reindex complete"
2008
+ );
2009
+ } catch (err) {
2010
+ p12.log.error(err.message);
2011
+ }
2012
+ }
2013
+ }
1700
2014
  function listFlow() {
1701
2015
  const lockfile = readLockfile();
1702
2016
  const entries = Object.entries(lockfile.packages);
@@ -1738,6 +2052,7 @@ var init_interactive = __esm({
1738
2052
  init_installer();
1739
2053
  init_lockfile();
1740
2054
  init_doctor();
2055
+ init_context();
1741
2056
  CATEGORIES2 = [
1742
2057
  "frontend",
1743
2058
  "backend",
@@ -1754,7 +2069,7 @@ var init_interactive = __esm({
1754
2069
  });
1755
2070
 
1756
2071
  // src/index.ts
1757
- import { Command as Command16 } from "commander";
2072
+ import { Command as Command17 } from "commander";
1758
2073
 
1759
2074
  // src/commands/install.ts
1760
2075
  init_installer();
@@ -3243,10 +3558,95 @@ var snapshotCommand = new Command15("snapshot").description("Analyze the current
3243
3558
  }
3244
3559
  });
3245
3560
 
3561
+ // src/commands/context.ts
3562
+ init_context();
3563
+ init_logger();
3564
+ init_prompts();
3565
+ import { Command as Command16 } from "commander";
3566
+ var contextCommand = new Command16("context").description("Manage project document context for AI");
3567
+ contextCommand.command("add <path>").description("Add a document directory to the project context").option("--name <name>", "Human-readable label for this directory").action(async (dirPath, options) => {
3568
+ try {
3569
+ const interactive = isInteractive();
3570
+ if (interactive) {
3571
+ await withSpinner(
3572
+ "Indexing documents...",
3573
+ async () => addContextRepo(dirPath, { name: options.name }),
3574
+ "Indexing complete"
3575
+ );
3576
+ } else {
3577
+ logger.blank();
3578
+ addContextRepo(dirPath, { name: options.name });
3579
+ logger.blank();
3580
+ }
3581
+ } catch (err) {
3582
+ logger.error(err.message);
3583
+ process.exit(1);
3584
+ }
3585
+ });
3586
+ contextCommand.command("remove <path-or-name>").description("Remove a directory from the project context").action((pathOrName) => {
3587
+ try {
3588
+ logger.blank();
3589
+ removeContextRepo(pathOrName);
3590
+ logger.blank();
3591
+ } catch (err) {
3592
+ logger.error(err.message);
3593
+ process.exit(1);
3594
+ }
3595
+ });
3596
+ contextCommand.command("list").description("Show all directories in the project context").option("--json", "Output as JSON").action((options) => {
3597
+ try {
3598
+ const summary = getContextSummary();
3599
+ if (options.json) {
3600
+ console.log(JSON.stringify(summary, null, 2));
3601
+ return;
3602
+ }
3603
+ logger.blank();
3604
+ if (summary.totalRepos === 0) {
3605
+ logger.info("No context repos configured. Run `planmode context add <path>` to add one.");
3606
+ logger.blank();
3607
+ return;
3608
+ }
3609
+ logger.bold(`${summary.totalRepos} context repo(s) \u2014 ${summary.totalFiles} file(s), ${formatSize(summary.totalSize)}`);
3610
+ logger.blank();
3611
+ for (const repo of summary.repos) {
3612
+ logger.info(`${repo.name}`);
3613
+ logger.dim(` Path: ${repo.path}`);
3614
+ logger.dim(` Files: ${repo.fileCount} (${formatSize(repo.totalSize)})`);
3615
+ if (repo.typeBreakdown.length > 0) {
3616
+ logger.dim(` Types: ${repo.typeBreakdown.join(", ")}`);
3617
+ }
3618
+ logger.dim(` Indexed: ${repo.indexedAt}`);
3619
+ logger.blank();
3620
+ }
3621
+ } catch (err) {
3622
+ logger.error(err.message);
3623
+ process.exit(1);
3624
+ }
3625
+ });
3626
+ contextCommand.command("reindex [path-or-name]").description("Re-scan files in one or all context directories").action(async (pathOrName) => {
3627
+ try {
3628
+ const interactive = isInteractive();
3629
+ if (interactive) {
3630
+ await withSpinner(
3631
+ "Re-scanning documents...",
3632
+ async () => reindexContext(pathOrName),
3633
+ "Reindex complete"
3634
+ );
3635
+ } else {
3636
+ logger.blank();
3637
+ reindexContext(pathOrName);
3638
+ logger.blank();
3639
+ }
3640
+ } catch (err) {
3641
+ logger.error(err.message);
3642
+ process.exit(1);
3643
+ }
3644
+ });
3645
+
3246
3646
  // src/index.ts
3247
3647
  init_prompts();
3248
- var program = new Command16();
3249
- program.name("planmode").description("The open source package manager for AI plans, rules, and prompts.").version("0.3.0");
3648
+ var program = new Command17();
3649
+ program.name("planmode").description("The open source package manager for AI plans, rules, and prompts.").version("0.4.0");
3250
3650
  program.addCommand(installCommand);
3251
3651
  program.addCommand(uninstallCommand);
3252
3652
  program.addCommand(searchCommand);
@@ -3262,6 +3662,7 @@ program.addCommand(doctorCommand);
3262
3662
  program.addCommand(testCommand);
3263
3663
  program.addCommand(recordCommand);
3264
3664
  program.addCommand(snapshotCommand);
3665
+ program.addCommand(contextCommand);
3265
3666
  if (process.argv.length <= 2 && isInteractive()) {
3266
3667
  const { runInteractiveMenu: runInteractiveMenu2 } = await Promise.resolve().then(() => (init_interactive(), interactive_exports));
3267
3668
  runInteractiveMenu2();