@promptbook/cli 0.112.0-38 → 0.112.0-40

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 CHANGED
@@ -602,7 +602,7 @@ npx ptbk coder verify
602
602
  | `ptbk coder init` | Creates `prompts/`, `prompts/done/`, the project-generic template files materialized in `prompts/templates/` (currently `common.md`), and a starter `AGENTS.md`; ensures `.env` contains `CODING_AGENT_GIT_NAME`, `CODING_AGENT_GIT_EMAIL`, and `CODING_AGENT_GIT_SIGNING_KEY`; adds helper coder scripts to `package.json`; ensures `.gitignore` contains `/.tmp`; and configures `.vscode/settings.json` to save pasted prompt images into `prompts/screenshots/`. |
603
603
  | `ptbk coder generate-boilerplates` | Creates new prompt markdown files with fresh emoji tags so you can quickly fill in coding tasks; `--template` accepts either a built-in alias or a markdown file path relative to the project root. |
604
604
  | `ptbk coder run` | Picks the next ready prompt, appends optional context, runs it through the selected coding agent, marks success or failure, then commits and pushes the result. |
605
- | `ptbk coder find-refactor-candidates` | Scans the repository for oversized or overpacked files and writes prompt files for likely refactors; `--level <low|medium|high|xhigh>` makes the scan more or less aggressive. |
605
+ | `ptbk coder find-refactor-candidates` | Scans the repository for oversized or overpacked files and writes prompt files for likely refactors; `--level <xlow|low|medium|high|xhigh|extreme>` ranges from a very benevolent scan to a very aggressive sweep. |
606
606
  | `ptbk coder verify` | Walks through completed prompts, archives truly finished work, and adds follow-up repair prompts for unfinished results. |
607
607
 
608
608
  #### Most useful `ptbk coder run` flags
package/esm/index.es.js CHANGED
@@ -31,6 +31,7 @@ import Anthropic from '@anthropic-ai/sdk';
31
31
  import Bottleneck from 'bottleneck';
32
32
  import { OpenAIClient, AzureKeyCredential } from '@azure/openai';
33
33
  import * as ts from 'typescript';
34
+ import ignore from 'ignore';
34
35
  import * as readline from 'readline';
35
36
  import { cursorTo, clearLine, createInterface } from 'readline';
36
37
  import { spawn } from 'child_process';
@@ -57,7 +58,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
57
58
  * @generated
58
59
  * @see https://github.com/webgptorg/promptbook
59
60
  */
60
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-38';
61
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-40';
61
62
  /**
62
63
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
63
64
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -1732,7 +1733,7 @@ const PROMPT_SLUG_MAX_LENGTH = 80;
1732
1733
  /**
1733
1734
  * Supported aggressiveness levels for refactor-candidate scanning.
1734
1735
  */
1735
- const REFACTOR_CANDIDATE_LEVEL_VALUES = ['low', 'medium', 'high', 'xhigh'];
1736
+ const REFACTOR_CANDIDATE_LEVEL_VALUES = ['xlow', 'low', 'medium', 'high', 'xhigh', 'extreme'];
1736
1737
  /**
1737
1738
  * Default aggressiveness level for refactor-candidate scanning.
1738
1739
  */
@@ -1740,37 +1741,76 @@ const DEFAULT_REFACTOR_CANDIDATE_LEVEL = 'medium';
1740
1741
  /**
1741
1742
  * Threshold table for each supported refactor-candidate scanning level.
1742
1743
  */
1743
- const REFACTOR_CANDIDATE_LEVEL_CONFIGURATION_BY_LEVEL = {
1744
- low: createRefactorCandidateLevelConfiguration({
1745
- maxLineCount: 2800,
1746
- maxEntityCountPerFile: 28,
1747
- maxFunctionCountPerFile: 18,
1748
- maxFunctionComplexity: 20,
1744
+ const REFACTOR_CANDIDATE_LEVEL_DETAILS_BY_LEVEL = {
1745
+ xlow: createRefactorCandidateLevelDetails({
1746
+ description: 'Extremely benevolent scan that flags only very obvious refactor targets.',
1747
+ maxLineCount: 9600,
1748
+ maxEntityCountPerFile: 72,
1749
+ maxFunctionCountPerFile: 48,
1750
+ maxFunctionComplexity: 40,
1749
1751
  }),
1750
- medium: createRefactorCandidateLevelConfiguration({
1751
- maxLineCount: 2000,
1752
- maxEntityCountPerFile: 20,
1753
- maxFunctionCountPerFile: 14,
1754
- maxFunctionComplexity: 16,
1752
+ low: createRefactorCandidateLevelDetails({
1753
+ description: 'Conservative scan for only the most obvious refactor targets.',
1754
+ maxLineCount: 3600,
1755
+ maxEntityCountPerFile: 30,
1756
+ maxFunctionCountPerFile: 20,
1757
+ maxFunctionComplexity: 24,
1755
1758
  }),
1756
- high: createRefactorCandidateLevelConfiguration({
1757
- maxLineCount: 1500,
1759
+ medium: createRefactorCandidateLevelDetails({
1760
+ description: 'Default scan using the current standard thresholds.',
1761
+ maxLineCount: 1800,
1758
1762
  maxEntityCountPerFile: 16,
1759
- maxFunctionCountPerFile: 10,
1760
- maxFunctionComplexity: 12,
1763
+ maxFunctionCountPerFile: 12,
1764
+ maxFunctionComplexity: 16,
1761
1765
  }),
1762
- xhigh: createRefactorCandidateLevelConfiguration({
1763
- maxLineCount: 1000,
1764
- maxEntityCountPerFile: 12,
1766
+ high: createRefactorCandidateLevelDetails({
1767
+ description: 'Strict scan that finds more crowded or complex files.',
1768
+ maxLineCount: 900,
1769
+ maxEntityCountPerFile: 9,
1765
1770
  maxFunctionCountPerFile: 8,
1766
- maxFunctionComplexity: 8,
1771
+ maxFunctionComplexity: 11,
1772
+ }),
1773
+ xhigh: createRefactorCandidateLevelDetails({
1774
+ description: 'Very strict scan for denser and more complex candidates.',
1775
+ maxLineCount: 450,
1776
+ maxEntityCountPerFile: 5,
1777
+ maxFunctionCountPerFile: 5,
1778
+ maxFunctionComplexity: 7,
1779
+ }),
1780
+ extreme: createRefactorCandidateLevelDetails({
1781
+ description: 'Most aggressive scan that surfaces even weak refactor opportunities.',
1782
+ maxLineCount: 180,
1783
+ maxEntityCountPerFile: 2,
1784
+ maxFunctionCountPerFile: 2,
1785
+ maxFunctionComplexity: 4,
1767
1786
  }),
1768
1787
  };
1769
1788
  /**
1770
1789
  * Resolves the thresholds for a selected refactor-candidate scanning level.
1771
1790
  */
1772
1791
  function getRefactorCandidateLevelConfiguration(level = DEFAULT_REFACTOR_CANDIDATE_LEVEL) {
1773
- return REFACTOR_CANDIDATE_LEVEL_CONFIGURATION_BY_LEVEL[level];
1792
+ return REFACTOR_CANDIDATE_LEVEL_DETAILS_BY_LEVEL[level].configuration;
1793
+ }
1794
+ /**
1795
+ * Resolves the user-facing description for a selected refactor-candidate scanning level.
1796
+ */
1797
+ function getRefactorCandidateLevelDescription(level) {
1798
+ return REFACTOR_CANDIDATE_LEVEL_DETAILS_BY_LEVEL[level].description;
1799
+ }
1800
+ /**
1801
+ * Builds one normalized refactor-candidate level entry.
1802
+ */
1803
+ function createRefactorCandidateLevelDetails(options) {
1804
+ const { description, maxLineCount, maxEntityCountPerFile, maxFunctionCountPerFile, maxFunctionComplexity } = options;
1805
+ return {
1806
+ description,
1807
+ configuration: createRefactorCandidateLevelConfiguration({
1808
+ maxLineCount,
1809
+ maxEntityCountPerFile,
1810
+ maxFunctionCountPerFile,
1811
+ maxFunctionComplexity,
1812
+ }),
1813
+ };
1774
1814
  }
1775
1815
  /**
1776
1816
  * Builds one normalized refactor-candidate level configuration entry.
@@ -1806,17 +1846,14 @@ function createLineCountLimits(maxLineCount) {
1806
1846
  */
1807
1847
  function $initializeCoderFindRefactorCandidatesCommand(program) {
1808
1848
  const command = program.command('find-refactor-candidates');
1809
- command.description(spaceTrim$1(`
1810
- Scan source files to identify refactoring candidates
1811
-
1812
- Levels:
1813
- - low: Conservative scan for only the most obvious refactor targets
1814
- - medium: Default scan using the current standard thresholds
1815
- - high: Stricter scan that finds more crowded or complex files
1816
- - xhigh: Most aggressive scan for denser and more complex candidates
1817
-
1818
- Generates refactor prompts with guidance for identified candidates.
1819
- `));
1849
+ command.description([
1850
+ 'Scan source files to identify refactoring candidates',
1851
+ '',
1852
+ 'Levels:',
1853
+ ...REFACTOR_CANDIDATE_LEVEL_VALUES.map((level) => `- ${level}: ${getRefactorCandidateLevelDescription(level)}`),
1854
+ '',
1855
+ 'Generates refactor prompts with guidance for identified candidates.',
1856
+ ].join('\n'));
1820
1857
  command.addOption(new Option('--level <level>', `Set scan aggressiveness (${REFACTOR_CANDIDATE_LEVEL_VALUES.join(', ')})`)
1821
1858
  .choices([...REFACTOR_CANDIDATE_LEVEL_VALUES])
1822
1859
  .default(DEFAULT_REFACTOR_CANDIDATE_LEVEL));
@@ -1944,7 +1981,7 @@ async function ensureDefaultCoderPromptTemplateFiles(projectPath) {
1944
1981
  const ensuredTemplateFiles = [];
1945
1982
  for (const definition of DEFAULT_CODER_PROJECT_PROMPT_TEMPLATE_DEFINITIONS) {
1946
1983
  const absoluteTemplatePath = join(projectPath, definition.relativeFilePath);
1947
- if (await isExistingFile$1(absoluteTemplatePath)) {
1984
+ if (await isExistingFile$2(absoluteTemplatePath)) {
1948
1985
  ensuredTemplateFiles.push({
1949
1986
  id: definition.id,
1950
1987
  relativeFilePath: definition.relativeFilePath,
@@ -2058,7 +2095,7 @@ function isNodeJsErrorWithCode(error, code) {
2058
2095
  /**
2059
2096
  * Checks whether a path exists and is a file.
2060
2097
  */
2061
- async function isExistingFile$1(path) {
2098
+ async function isExistingFile$2(path) {
2062
2099
  try {
2063
2100
  return (await stat(path)).isFile();
2064
2101
  }
@@ -2492,7 +2529,7 @@ function hasTmpGitignoreRule(gitignoreContent) {
2492
2529
  */
2493
2530
  async function ensureCoderMarkdownFile(projectPath, relativeFilePath, fileContent) {
2494
2531
  const absoluteFilePath = join(projectPath, relativeFilePath);
2495
- if (await isExistingFile(absoluteFilePath)) {
2532
+ if (await isExistingFile$1(absoluteFilePath)) {
2496
2533
  return 'unchanged';
2497
2534
  }
2498
2535
  await writeFile(absoluteFilePath, `${fileContent}\n`, 'utf-8');
@@ -2501,7 +2538,7 @@ async function ensureCoderMarkdownFile(projectPath, relativeFilePath, fileConten
2501
2538
  /**
2502
2539
  * Checks whether a path exists and is a file.
2503
2540
  */
2504
- async function isExistingFile(path) {
2541
+ async function isExistingFile$1(path) {
2505
2542
  try {
2506
2543
  return (await stat(path)).isFile();
2507
2544
  }
@@ -41457,9 +41494,9 @@ function normalizeAbsolutePath$1(pathValue) {
41457
41494
  * @private function of findRefactorCandidates
41458
41495
  */
41459
41496
  async function findRefactorCandidatesInProject(options) {
41460
- const { heuristics, rootDir } = options;
41461
- const lineCountExemptPaths = await buildExemptPathSet(rootDir, LINE_COUNT_EXEMPT_GLOBS);
41462
- const sourceFiles = await listSourceFiles(rootDir);
41497
+ const { heuristics, isIgnoredRelativePath = () => false, rootDir } = options;
41498
+ const lineCountExemptPaths = await buildExemptPathSet(rootDir, LINE_COUNT_EXEMPT_GLOBS, isIgnoredRelativePath);
41499
+ const sourceFiles = await listSourceFiles(rootDir, isIgnoredRelativePath);
41463
41500
  const candidates = [];
41464
41501
  for (const filePath of sourceFiles) {
41465
41502
  const candidate = await analyzeSourceFileForRefactorCandidate({
@@ -41479,7 +41516,7 @@ async function findRefactorCandidatesInProject(options) {
41479
41516
  *
41480
41517
  * @private function of findRefactorCandidatesInProject
41481
41518
  */
41482
- async function listSourceFiles(rootDir) {
41519
+ async function listSourceFiles(rootDir, isIgnoredRelativePath) {
41483
41520
  const extensions = SOURCE_FILE_EXTENSIONS.map((extension) => extension.replace(/^\./, '')).join(',');
41484
41521
  const extensionGlob = `{${extensions}}`;
41485
41522
  const patterns = [...SOURCE_ROOTS.map((root) => `${root}/**/*.${extensionGlob}`), `*.${extensionGlob}`];
@@ -41492,6 +41529,9 @@ async function listSourceFiles(rootDir) {
41492
41529
  absolute: true,
41493
41530
  });
41494
41531
  for (const match of matches) {
41532
+ if (shouldIgnoreAbsolutePath(rootDir, match, isIgnoredRelativePath)) {
41533
+ continue;
41534
+ }
41495
41535
  files.add(match);
41496
41536
  }
41497
41537
  }
@@ -41502,7 +41542,7 @@ async function listSourceFiles(rootDir) {
41502
41542
  *
41503
41543
  * @private function of findRefactorCandidatesInProject
41504
41544
  */
41505
- async function buildExemptPathSet(rootDir, patterns) {
41545
+ async function buildExemptPathSet(rootDir, patterns, isIgnoredRelativePath) {
41506
41546
  const exemptPaths = new Set();
41507
41547
  for (const pattern of patterns) {
41508
41548
  const matches = await glob(pattern, {
@@ -41512,11 +41552,23 @@ async function buildExemptPathSet(rootDir, patterns) {
41512
41552
  absolute: true,
41513
41553
  });
41514
41554
  for (const match of matches) {
41555
+ if (shouldIgnoreAbsolutePath(rootDir, match, isIgnoredRelativePath)) {
41556
+ continue;
41557
+ }
41515
41558
  exemptPaths.add(normalizeAbsolutePath(match));
41516
41559
  }
41517
41560
  }
41518
41561
  return exemptPaths;
41519
41562
  }
41563
+ /**
41564
+ * Resolves whether an absolute path falls under the project `.gitignore` rules.
41565
+ *
41566
+ * @private function of findRefactorCandidatesInProject
41567
+ */
41568
+ function shouldIgnoreAbsolutePath(rootDir, absolutePath, isIgnoredRelativePath) {
41569
+ const relativePath = normalizeRefactorCandidatePath(relative(rootDir, absolutePath));
41570
+ return isIgnoredRelativePath(relativePath);
41571
+ }
41520
41572
  /**
41521
41573
  * Normalizes an absolute path for consistent comparisons.
41522
41574
  *
@@ -41565,6 +41617,69 @@ function escapeRegExp(value) {
41565
41617
  }
41566
41618
  // Note: [🟡] Code for repository script [loadExistingPromptTargets](scripts/find-refactor-candidates/loadExistingPromptTargets.ts) should never be published outside of `@promptbook/cli`
41567
41619
 
41620
+ /**
41621
+ * Filename used to discover the project root for refactor-candidate scanning.
41622
+ */
41623
+ const GITIGNORE_FILE_NAME = '.gitignore';
41624
+ /**
41625
+ * Resolves the project root and `.gitignore` matcher for refactor-candidate scanning.
41626
+ *
41627
+ * @private function of findRefactorCandidates
41628
+ */
41629
+ async function resolveRefactorCandidateProject(startDir) {
41630
+ const absoluteStartDir = resolve(startDir);
41631
+ const gitignorePath = await findNearestGitignorePath(absoluteStartDir);
41632
+ if (!gitignorePath) {
41633
+ return {
41634
+ rootDir: absoluteStartDir,
41635
+ isIgnoredRelativePath: () => false,
41636
+ };
41637
+ }
41638
+ const rootDir = dirname(gitignorePath);
41639
+ const gitignoreMatcher = ignore().add(await readFile(gitignorePath, 'utf-8'));
41640
+ return {
41641
+ rootDir,
41642
+ isIgnoredRelativePath(relativePath) {
41643
+ return gitignoreMatcher.ignores(normalizeRefactorCandidatePath(relativePath));
41644
+ },
41645
+ };
41646
+ }
41647
+ /**
41648
+ * Finds the nearest ancestor `.gitignore` so scans work from any project subdirectory.
41649
+ *
41650
+ * @private function of resolveRefactorCandidateProject
41651
+ */
41652
+ async function findNearestGitignorePath(startDir) {
41653
+ let currentDir = resolve(startDir);
41654
+ while (true) {
41655
+ const gitignorePath = join(currentDir, GITIGNORE_FILE_NAME);
41656
+ if (await isExistingFile(gitignorePath)) {
41657
+ return gitignorePath;
41658
+ }
41659
+ const parentDir = dirname(currentDir);
41660
+ if (parentDir === currentDir) {
41661
+ return null;
41662
+ }
41663
+ currentDir = parentDir;
41664
+ }
41665
+ }
41666
+ /**
41667
+ * Detects whether a file exists without swallowing unexpected filesystem failures.
41668
+ *
41669
+ * @private function of resolveRefactorCandidateProject
41670
+ */
41671
+ async function isExistingFile(filePath) {
41672
+ var _a;
41673
+ const fileStats = await stat(filePath).catch((error) => {
41674
+ if (error.code === 'ENOENT') {
41675
+ return undefined;
41676
+ }
41677
+ throw error;
41678
+ });
41679
+ return (_a = fileStats === null || fileStats === void 0 ? void 0 : fileStats.isFile()) !== null && _a !== void 0 ? _a : false;
41680
+ }
41681
+ // Note: [🟡] Code for repository script [resolveRefactorCandidateProject](scripts/find-refactor-candidates/resolveRefactorCandidateProject.ts) should never be published outside of `@promptbook/cli`
41682
+
41568
41683
  /**
41569
41684
  * Calculates the next available prompt numbering sequence for a month.
41570
41685
  */
@@ -41922,11 +42037,12 @@ async function findRefactorCandidates(options = {}) {
41922
42037
  initializeFindRefactorCandidatesRun();
41923
42038
  console.info(colors.cyan('⚡🏭 Find refactor candidates'));
41924
42039
  console.info(colors.gray(`Using \`${level}\` scan level.`));
41925
- const rootDir = process.cwd();
42040
+ const { isIgnoredRelativePath, rootDir } = await resolveRefactorCandidateProject(process.cwd());
41926
42041
  const promptsDir = join(rootDir, PROMPTS_DIR_NAME);
41927
42042
  const existingTargets = await loadExistingPromptTargets(promptsDir);
41928
42043
  const candidates = await findRefactorCandidatesInProject({
41929
42044
  heuristics,
42045
+ isIgnoredRelativePath,
41930
42046
  rootDir,
41931
42047
  });
41932
42048
  if (candidates.length === 0) {