@promptbook/cli 0.112.0-36 → 0.112.0-38

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.
Files changed (134) hide show
  1. package/README.md +10 -6
  2. package/esm/index.es.js +950 -424
  3. package/esm/index.es.js.map +1 -1
  4. package/esm/scripts/find-refactor-candidates/RefactorCandidateLevel.d.ts +41 -0
  5. package/esm/scripts/find-refactor-candidates/analyzeSourceFileForRefactorCandidate.d.ts +5 -0
  6. package/esm/scripts/find-refactor-candidates/find-refactor-candidates.constants.d.ts +2 -14
  7. package/esm/scripts/find-refactor-candidates/find-refactor-candidates.d.ts +13 -1
  8. package/esm/scripts/find-refactor-candidates/findRefactorCandidatesInProject.d.ts +18 -1
  9. package/esm/src/_packages/components.index.d.ts +2 -0
  10. package/esm/src/_packages/types.index.d.ts +48 -46
  11. package/esm/src/book-components/Chat/Chat/ChatActionsBar.d.ts +7 -0
  12. package/esm/src/book-components/Chat/Chat/ChatActionsBar.test.d.ts +2 -0
  13. package/esm/src/book-components/Chat/Chat/ChatInputArea.d.ts +4 -0
  14. package/esm/src/book-components/Chat/Chat/ChatMessageItem.d.ts +8 -0
  15. package/esm/src/book-components/Chat/Chat/ChatMessageList.d.ts +2 -0
  16. package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +50 -1
  17. package/esm/src/book-components/Chat/Chat/ChatReplyPreview.d.ts +19 -0
  18. package/esm/src/book-components/Chat/Chat/createProgressCardChecklistMarkdown.d.ts +2 -2
  19. package/esm/src/book-components/Chat/MockedChat/MockedChat.d.ts +1 -1
  20. package/esm/src/book-components/Chat/types/ChatMessage.d.ts +35 -0
  21. package/esm/src/book-components/Chat/utils/resolveChatMessageReplyPreviewText.d.ts +25 -0
  22. package/esm/src/book-components/Chat/utils/resolveChatMessageReplySenderLabel.d.ts +12 -0
  23. package/esm/src/cli/cli-commands/coder/agentCodingFile.d.ts +14 -0
  24. package/esm/src/cli/cli-commands/coder/agentsFile.d.ts +12 -0
  25. package/esm/src/cli/cli-commands/coder/appendBlock.d.ts +6 -0
  26. package/esm/src/cli/cli-commands/coder/boilerplateTemplates.d.ts +10 -0
  27. package/esm/src/cli/cli-commands/coder/ensureCoderEnvFile.d.ts +15 -0
  28. package/esm/src/cli/cli-commands/coder/ensureCoderGitignoreFile.d.ts +7 -0
  29. package/esm/src/cli/cli-commands/coder/ensureCoderMarkdownFile.d.ts +7 -0
  30. package/esm/src/cli/cli-commands/coder/ensureCoderPackageJsonFile.d.ts +7 -0
  31. package/esm/src/cli/cli-commands/coder/ensureCoderVscodeSettingsFile.d.ts +7 -0
  32. package/esm/src/cli/cli-commands/coder/ensureDirectory.d.ts +7 -0
  33. package/esm/src/cli/cli-commands/coder/find-refactor-candidates.d.ts +1 -1
  34. package/esm/src/cli/cli-commands/coder/find-refactor-candidates.test.d.ts +1 -0
  35. package/esm/src/cli/cli-commands/coder/formatDisplayPath.d.ts +6 -0
  36. package/esm/src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.d.ts +6 -0
  37. package/esm/src/cli/cli-commands/coder/getDefaultCoderVscodeSettings.d.ts +6 -0
  38. package/esm/src/cli/cli-commands/coder/init.d.ts +3 -38
  39. package/esm/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +25 -0
  40. package/esm/src/cli/cli-commands/coder/mergeStringRecordJsonFile.d.ts +18 -0
  41. package/esm/src/cli/cli-commands/coder/printInitializationSummary.d.ts +7 -0
  42. package/esm/src/cli/cli-commands/coder/readTextFileIfExists.d.ts +6 -0
  43. package/esm/src/types/string_agent_url.d.ts +7 -0
  44. package/esm/src/types/string_agent_url_private.d.ts +9 -0
  45. package/esm/src/types/string_base64.d.ts +13 -0
  46. package/esm/src/types/string_base64_private.d.ts +2 -2
  47. package/esm/src/types/string_base_url.d.ts +7 -0
  48. package/esm/src/types/string_base_url_private.d.ts +9 -0
  49. package/esm/src/types/string_email.d.ts +13 -0
  50. package/esm/src/types/string_email_private.d.ts +2 -2
  51. package/esm/src/types/string_host.d.ts +42 -0
  52. package/esm/src/types/string_host_private.d.ts +7 -7
  53. package/esm/src/types/string_href.d.ts +19 -0
  54. package/esm/src/types/string_href_private.d.ts +24 -0
  55. package/esm/src/types/string_mime_type.d.ts +15 -0
  56. package/esm/src/types/string_mime_type_private.d.ts +2 -2
  57. package/esm/src/types/string_pipeline_root_url.d.ts +7 -0
  58. package/esm/src/types/string_pipeline_root_url_private.d.ts +9 -0
  59. package/esm/src/types/string_pipeline_url.d.ts +13 -0
  60. package/esm/src/types/string_pipeline_url_private.d.ts +17 -0
  61. package/esm/src/types/string_promptbook_server_url.d.ts +7 -0
  62. package/esm/src/types/string_promptbook_server_url_private.d.ts +9 -0
  63. package/esm/src/types/string_url.d.ts +14 -141
  64. package/esm/src/types/string_url_image.d.ts +7 -0
  65. package/esm/src/types/string_url_image_private.d.ts +9 -0
  66. package/esm/src/types/string_url_private.d.ts +0 -80
  67. package/esm/src/version.d.ts +1 -1
  68. package/package.json +1 -1
  69. package/umd/index.umd.js +949 -423
  70. package/umd/index.umd.js.map +1 -1
  71. package/umd/scripts/find-refactor-candidates/RefactorCandidateLevel.d.ts +41 -0
  72. package/umd/scripts/find-refactor-candidates/analyzeSourceFileForRefactorCandidate.d.ts +5 -0
  73. package/umd/scripts/find-refactor-candidates/find-refactor-candidates.constants.d.ts +2 -14
  74. package/umd/scripts/find-refactor-candidates/find-refactor-candidates.d.ts +13 -1
  75. package/umd/scripts/find-refactor-candidates/findRefactorCandidatesInProject.d.ts +18 -1
  76. package/umd/src/_packages/components.index.d.ts +2 -0
  77. package/umd/src/_packages/types.index.d.ts +48 -46
  78. package/umd/src/book-components/Chat/Chat/ChatActionsBar.d.ts +7 -0
  79. package/umd/src/book-components/Chat/Chat/ChatActionsBar.test.d.ts +2 -0
  80. package/umd/src/book-components/Chat/Chat/ChatInputArea.d.ts +4 -0
  81. package/umd/src/book-components/Chat/Chat/ChatMessageItem.d.ts +8 -0
  82. package/umd/src/book-components/Chat/Chat/ChatMessageList.d.ts +2 -0
  83. package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +50 -1
  84. package/umd/src/book-components/Chat/Chat/ChatReplyPreview.d.ts +19 -0
  85. package/umd/src/book-components/Chat/Chat/createProgressCardChecklistMarkdown.d.ts +2 -2
  86. package/umd/src/book-components/Chat/MockedChat/MockedChat.d.ts +1 -1
  87. package/umd/src/book-components/Chat/types/ChatMessage.d.ts +35 -0
  88. package/umd/src/book-components/Chat/utils/resolveChatMessageReplyPreviewText.d.ts +25 -0
  89. package/umd/src/book-components/Chat/utils/resolveChatMessageReplySenderLabel.d.ts +12 -0
  90. package/umd/src/cli/cli-commands/coder/agentCodingFile.d.ts +14 -0
  91. package/umd/src/cli/cli-commands/coder/agentsFile.d.ts +12 -0
  92. package/umd/src/cli/cli-commands/coder/appendBlock.d.ts +6 -0
  93. package/umd/src/cli/cli-commands/coder/boilerplateTemplates.d.ts +10 -0
  94. package/umd/src/cli/cli-commands/coder/ensureCoderEnvFile.d.ts +15 -0
  95. package/umd/src/cli/cli-commands/coder/ensureCoderGitignoreFile.d.ts +7 -0
  96. package/umd/src/cli/cli-commands/coder/ensureCoderMarkdownFile.d.ts +7 -0
  97. package/umd/src/cli/cli-commands/coder/ensureCoderPackageJsonFile.d.ts +7 -0
  98. package/umd/src/cli/cli-commands/coder/ensureCoderVscodeSettingsFile.d.ts +7 -0
  99. package/umd/src/cli/cli-commands/coder/ensureDirectory.d.ts +7 -0
  100. package/umd/src/cli/cli-commands/coder/find-refactor-candidates.d.ts +1 -1
  101. package/umd/src/cli/cli-commands/coder/find-refactor-candidates.test.d.ts +1 -0
  102. package/umd/src/cli/cli-commands/coder/formatDisplayPath.d.ts +6 -0
  103. package/umd/src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.d.ts +6 -0
  104. package/umd/src/cli/cli-commands/coder/getDefaultCoderVscodeSettings.d.ts +6 -0
  105. package/umd/src/cli/cli-commands/coder/init.d.ts +3 -38
  106. package/umd/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +25 -0
  107. package/umd/src/cli/cli-commands/coder/mergeStringRecordJsonFile.d.ts +18 -0
  108. package/umd/src/cli/cli-commands/coder/printInitializationSummary.d.ts +7 -0
  109. package/umd/src/cli/cli-commands/coder/readTextFileIfExists.d.ts +6 -0
  110. package/umd/src/types/string_agent_url.d.ts +7 -0
  111. package/umd/src/types/string_agent_url_private.d.ts +9 -0
  112. package/umd/src/types/string_base64.d.ts +13 -0
  113. package/umd/src/types/string_base64_private.d.ts +2 -2
  114. package/umd/src/types/string_base_url.d.ts +7 -0
  115. package/umd/src/types/string_base_url_private.d.ts +9 -0
  116. package/umd/src/types/string_email.d.ts +13 -0
  117. package/umd/src/types/string_email_private.d.ts +2 -2
  118. package/umd/src/types/string_host.d.ts +42 -0
  119. package/umd/src/types/string_host_private.d.ts +7 -7
  120. package/umd/src/types/string_href.d.ts +19 -0
  121. package/umd/src/types/string_href_private.d.ts +24 -0
  122. package/umd/src/types/string_mime_type.d.ts +15 -0
  123. package/umd/src/types/string_mime_type_private.d.ts +2 -2
  124. package/umd/src/types/string_pipeline_root_url.d.ts +7 -0
  125. package/umd/src/types/string_pipeline_root_url_private.d.ts +9 -0
  126. package/umd/src/types/string_pipeline_url.d.ts +13 -0
  127. package/umd/src/types/string_pipeline_url_private.d.ts +17 -0
  128. package/umd/src/types/string_promptbook_server_url.d.ts +7 -0
  129. package/umd/src/types/string_promptbook_server_url_private.d.ts +9 -0
  130. package/umd/src/types/string_url.d.ts +14 -141
  131. package/umd/src/types/string_url_image.d.ts +7 -0
  132. package/umd/src/types/string_url_image_private.d.ts +9 -0
  133. package/umd/src/types/string_url_private.d.ts +0 -80
  134. package/umd/src/version.d.ts +1 -1
package/umd/index.umd.js CHANGED
@@ -60,7 +60,7 @@
60
60
  * @generated
61
61
  * @see https://github.com/webgptorg/promptbook
62
62
  */
63
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-36';
63
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-38';
64
64
  /**
65
65
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
66
66
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -1664,6 +1664,142 @@
1664
1664
  // Note: [🟡] Code for CLI command [find-fresh-emoji-tags](src/cli/cli-commands/coder/find-fresh-emoji-tags.ts) should never be published outside of `@promptbook/cli`
1665
1665
  // Note: [💞] Ignore a discrepancy between file name and entity name
1666
1666
 
1667
+ /**
1668
+ * Root folders that contain source-like files for scanning.
1669
+ */
1670
+ const SOURCE_ROOTS = ['src', 'apps', 'scripts', 'examples', 'agents', 'other'];
1671
+ /**
1672
+ * File extensions treated as source code.
1673
+ */
1674
+ const SOURCE_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
1675
+ /**
1676
+ * Glob patterns that should be ignored when scanning for source files.
1677
+ */
1678
+ const SOURCE_FILE_IGNORE_GLOBS = [
1679
+ '**/node_modules/**',
1680
+ '**/packages/**',
1681
+ '**/.*/**',
1682
+ '**/.git/**',
1683
+ '**/.idea/**',
1684
+ '**/.vscode/**',
1685
+ '**/.promptbook/**',
1686
+ '**/.next/**',
1687
+ '**/.tmp/**',
1688
+ '**/tmp/**',
1689
+ '**/coverage/**',
1690
+ '**/dist/**',
1691
+ '**/build/**',
1692
+ '**/out/**',
1693
+ '**/prompts/**',
1694
+ '**/changelog/**',
1695
+ ];
1696
+ /**
1697
+ * Glob patterns that are exempt from line-count checks.
1698
+ */
1699
+ const LINE_COUNT_EXEMPT_GLOBS = ['other/cspell-dictionaries/**/*.txt'];
1700
+ /**
1701
+ * File extensions eligible for structural AST analysis.
1702
+ */
1703
+ const STRUCTURAL_ANALYSIS_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
1704
+ /**
1705
+ * Markers that identify generated files which should be skipped.
1706
+ */
1707
+ const GENERATED_CODE_MARKERS = [
1708
+ 'WARNING: This code has been generated',
1709
+ 'This code has been generated so that any manual changes will be overwritten',
1710
+ ];
1711
+ /**
1712
+ * Name of the prompts directory.
1713
+ */
1714
+ const PROMPTS_DIR_NAME = 'prompts';
1715
+ /**
1716
+ * Step size used for prompt numbering.
1717
+ */
1718
+ const PROMPT_NUMBER_STEP = 10;
1719
+ /**
1720
+ * Prefix used for generated prompt slugs.
1721
+ */
1722
+ const PROMPT_SLUG_PREFIX = 'refactor';
1723
+ /**
1724
+ * Label used to mark the target file in generated prompts.
1725
+ */
1726
+ const PROMPT_TARGET_LABEL = 'Target file';
1727
+ /**
1728
+ * Maximum length for generated prompt slugs.
1729
+ */
1730
+ const PROMPT_SLUG_MAX_LENGTH = 80;
1731
+ /**
1732
+ * Note: [?] Code in this file should never be published in any package
1733
+ */
1734
+
1735
+ /**
1736
+ * Supported aggressiveness levels for refactor-candidate scanning.
1737
+ */
1738
+ const REFACTOR_CANDIDATE_LEVEL_VALUES = ['low', 'medium', 'high', 'xhigh'];
1739
+ /**
1740
+ * Default aggressiveness level for refactor-candidate scanning.
1741
+ */
1742
+ const DEFAULT_REFACTOR_CANDIDATE_LEVEL = 'medium';
1743
+ /**
1744
+ * Threshold table for each supported refactor-candidate scanning level.
1745
+ */
1746
+ const REFACTOR_CANDIDATE_LEVEL_CONFIGURATION_BY_LEVEL = {
1747
+ low: createRefactorCandidateLevelConfiguration({
1748
+ maxLineCount: 2800,
1749
+ maxEntityCountPerFile: 28,
1750
+ maxFunctionCountPerFile: 18,
1751
+ maxFunctionComplexity: 20,
1752
+ }),
1753
+ medium: createRefactorCandidateLevelConfiguration({
1754
+ maxLineCount: 2000,
1755
+ maxEntityCountPerFile: 20,
1756
+ maxFunctionCountPerFile: 14,
1757
+ maxFunctionComplexity: 16,
1758
+ }),
1759
+ high: createRefactorCandidateLevelConfiguration({
1760
+ maxLineCount: 1500,
1761
+ maxEntityCountPerFile: 16,
1762
+ maxFunctionCountPerFile: 10,
1763
+ maxFunctionComplexity: 12,
1764
+ }),
1765
+ xhigh: createRefactorCandidateLevelConfiguration({
1766
+ maxLineCount: 1000,
1767
+ maxEntityCountPerFile: 12,
1768
+ maxFunctionCountPerFile: 8,
1769
+ maxFunctionComplexity: 8,
1770
+ }),
1771
+ };
1772
+ /**
1773
+ * Resolves the thresholds for a selected refactor-candidate scanning level.
1774
+ */
1775
+ function getRefactorCandidateLevelConfiguration(level = DEFAULT_REFACTOR_CANDIDATE_LEVEL) {
1776
+ return REFACTOR_CANDIDATE_LEVEL_CONFIGURATION_BY_LEVEL[level];
1777
+ }
1778
+ /**
1779
+ * Builds one normalized refactor-candidate level configuration entry.
1780
+ */
1781
+ function createRefactorCandidateLevelConfiguration(options) {
1782
+ const { maxLineCount, maxEntityCountPerFile, maxFunctionCountPerFile, maxFunctionComplexity } = options;
1783
+ return {
1784
+ maxDefaultLineCount: maxLineCount,
1785
+ maxLineCountByExtension: createLineCountLimits(maxLineCount),
1786
+ maxEntityCountPerFile,
1787
+ maxFunctionCountPerFile,
1788
+ maxFunctionComplexity,
1789
+ };
1790
+ }
1791
+ /**
1792
+ * Creates a per-extension line-count table using one shared threshold.
1793
+ */
1794
+ function createLineCountLimits(maxLineCount) {
1795
+ const maxLineCountByExtension = {};
1796
+ for (const extension of SOURCE_FILE_EXTENSIONS) {
1797
+ maxLineCountByExtension[extension] = maxLineCount;
1798
+ }
1799
+ return maxLineCountByExtension;
1800
+ }
1801
+ // Note: [🟡] Code for repository script [RefactorCandidateLevel](scripts/find-refactor-candidates/RefactorCandidateLevel.ts) should never be published outside of `@promptbook/cli`
1802
+
1667
1803
  /**
1668
1804
  * Initializes `coder find-refactor-candidates` command for Promptbook CLI utilities
1669
1805
  *
@@ -1676,17 +1812,23 @@
1676
1812
  command.description(_spaceTrim.spaceTrim(`
1677
1813
  Scan source files to identify refactoring candidates
1678
1814
 
1679
- Flags files that exceed:
1680
- - Line count limits (500 lines per file by extension)
1681
- - Entity count limits (max 4 entities per file)
1815
+ Levels:
1816
+ - low: Conservative scan for only the most obvious refactor targets
1817
+ - medium: Default scan using the current standard thresholds
1818
+ - high: Stricter scan that finds more crowded or complex files
1819
+ - xhigh: Most aggressive scan for denser and more complex candidates
1682
1820
 
1683
1821
  Generates refactor prompts with guidance for identified candidates.
1684
1822
  `));
1685
- command.action(handleActionErrors(async () => {
1823
+ command.addOption(new commander.Option('--level <level>', `Set scan aggressiveness (${REFACTOR_CANDIDATE_LEVEL_VALUES.join(', ')})`)
1824
+ .choices([...REFACTOR_CANDIDATE_LEVEL_VALUES])
1825
+ .default(DEFAULT_REFACTOR_CANDIDATE_LEVEL));
1826
+ command.action(handleActionErrors(async (cliOptions) => {
1827
+ const { level = DEFAULT_REFACTOR_CANDIDATE_LEVEL } = cliOptions;
1686
1828
  // Note: Import the function dynamically to avoid loading heavy dependencies until needed
1687
1829
  const { findRefactorCandidates } = await Promise.resolve().then(function () { return findRefactorCandidates$1; });
1688
1830
  try {
1689
- await findRefactorCandidates();
1831
+ await findRefactorCandidates({ level });
1690
1832
  }
1691
1833
  catch (error) {
1692
1834
  assertsError(error);
@@ -1732,7 +1874,9 @@
1732
1874
  */
1733
1875
  const PROMPTS_TEMPLATES_DIRECTORY_PATH = path.join(PROMPTS_DIRECTORY_PATH, 'templates');
1734
1876
  /**
1735
- * Built-in boilerplate templates shared by `coder init` and `coder generate-boilerplates`.
1877
+ * Built-in boilerplate templates available to `coder generate-boilerplates`.
1878
+ *
1879
+ * Only the project-agnostic subset is materialized by `coder init`.
1736
1880
  */
1737
1881
  const DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS = [
1738
1882
  {
@@ -1741,10 +1885,11 @@
1741
1885
  slugPrefix: null,
1742
1886
  content: buildCoderPromptTemplateContent([
1743
1887
  '- @@@',
1744
- '- Keep in mind the DRY _(don\'t repeat yourself)_ principle.',
1888
+ "- Keep in mind the DRY _(don't repeat yourself)_ principle.",
1745
1889
  '- Do a proper analysis of the current functionality before you start implementing.',
1746
1890
  '- Add the changes into the [changelog](./changelog/_current-preversion.md)',
1747
1891
  ]),
1892
+ isDefaultProjectTemplate: true,
1748
1893
  },
1749
1894
  {
1750
1895
  id: 'agents-server',
@@ -1752,14 +1897,19 @@
1752
1897
  slugPrefix: 'agents-server',
1753
1898
  content: buildCoderPromptTemplateContent([
1754
1899
  '- @@@',
1755
- '- Keep in mind the DRY _(don\'t repeat yourself)_ principle.',
1900
+ "- Keep in mind the DRY _(don't repeat yourself)_ principle.",
1756
1901
  '- Do a proper analysis of the current functionality before you start implementing.',
1757
1902
  '- You are working with the [Agents Server](apps/agents-server)',
1758
1903
  '- If you need to do the database migration, do it',
1759
1904
  '- Add the changes into the [changelog](changelog/_current-preversion.md)',
1760
1905
  ]),
1906
+ isDefaultProjectTemplate: false,
1761
1907
  },
1762
1908
  ];
1909
+ /**
1910
+ * Project-agnostic coder templates that `ptbk coder init` should materialize in any repository.
1911
+ */
1912
+ const DEFAULT_CODER_PROJECT_PROMPT_TEMPLATE_DEFINITIONS = DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS.filter(({ isDefaultProjectTemplate }) => isDefaultProjectTemplate);
1763
1913
  /**
1764
1914
  * Lists the built-in coder boilerplate templates.
1765
1915
  *
@@ -1768,6 +1918,14 @@
1768
1918
  function getDefaultCoderPromptTemplateDefinitions() {
1769
1919
  return DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS;
1770
1920
  }
1921
+ /**
1922
+ * Lists the built-in coder prompt templates that are safe to initialize in any project.
1923
+ *
1924
+ * @private internal utility of `ptbk coder`
1925
+ */
1926
+ function getDefaultCoderProjectPromptTemplateDefinitions() {
1927
+ return DEFAULT_CODER_PROJECT_PROMPT_TEMPLATE_DEFINITIONS;
1928
+ }
1771
1929
  /**
1772
1930
  * Resolves one built-in coder boilerplate template definition by its stable identifier.
1773
1931
  *
@@ -1787,7 +1945,7 @@
1787
1945
  */
1788
1946
  async function ensureDefaultCoderPromptTemplateFiles(projectPath) {
1789
1947
  const ensuredTemplateFiles = [];
1790
- for (const definition of DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS) {
1948
+ for (const definition of DEFAULT_CODER_PROJECT_PROMPT_TEMPLATE_DEFINITIONS) {
1791
1949
  const absoluteTemplatePath = path.join(projectPath, definition.relativeFilePath);
1792
1950
  if (await isExistingFile$1(absoluteTemplatePath)) {
1793
1951
  ensuredTemplateFiles.push({
@@ -2053,178 +2211,202 @@
2053
2211
  // Note: [💞] Ignore a discrepancy between file name and entity name
2054
2212
 
2055
2213
  /**
2056
- * This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
2214
+ * Relative path to the shared coder context file initialized in project roots.
2057
2215
  *
2058
- * @public exported from `@promptbook/core`
2216
+ * @private internal utility of `ptbk coder`
2059
2217
  */
2060
- class ParseError extends Error {
2061
- constructor(message) {
2062
- super(message);
2063
- this.name = 'ParseError';
2064
- Object.setPrototypeOf(this, ParseError.prototype);
2065
- }
2066
- }
2067
- // TODO: Maybe split `ParseError` and `ApplyError`
2068
-
2218
+ const AGENTS_FILE_PATH = 'AGENTS.md';
2069
2219
  /**
2070
- * Required environment variables for coding-agent git identity.
2220
+ * Stable boilerplate instructions written into newly initialized `AGENTS.md` files.
2071
2221
  */
2072
- const REQUIRED_CODER_ENV_VARIABLES = [
2073
- {
2074
- name: 'CODING_AGENT_GIT_NAME',
2075
- value: 'Promptbook Coding Agent',
2076
- },
2077
- {
2078
- name: 'CODING_AGENT_GIT_EMAIL',
2079
- value: 'coding-agent@promptbook.studio',
2080
- },
2081
- {
2082
- name: 'CODING_AGENT_GIT_SIGNING_KEY',
2083
- value: '13406525ED912F938FEA85AB4046C687298B2382',
2084
- },
2222
+ const DEFAULT_CODER_AGENTS_FILE_LINES = [
2223
+ '<!-- TODO: Write instructions for the Promptbook AI Coder here -->',
2085
2224
  ];
2086
2225
  /**
2087
- * Default npm scripts initialized by `ptbk coder init`.
2226
+ * Shared markdown boilerplate written into new `AGENTS.md` files.
2088
2227
  */
2089
- const DEFAULT_CODER_PACKAGE_JSON_SCRIPTS = {
2090
- 'coder:generate-boilerplates': 'npx ptbk coder generate-boilerplates',
2091
- 'coder:run': 'npx ptbk coder run --agent github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --no-wait',
2092
- 'coder:find-refactor-candidates': 'npx ptbk coder find-refactor-candidates',
2093
- 'coder:verify': 'npx ptbk coder verify',
2094
- };
2228
+ const DEFAULT_CODER_AGENTS_FILE_CONTENT = DEFAULT_CODER_AGENTS_FILE_LINES.join('\n');
2095
2229
  /**
2096
- * Relative path to `.gitignore` in the initialized project.
2097
- */
2098
- const GITIGNORE_FILE_PATH = '.gitignore';
2099
- /**
2100
- * Relative path to `package.json` in the initialized project.
2101
- */
2102
- const PACKAGE_JSON_FILE_PATH = 'package.json';
2103
- /**
2104
- * Relative path to the VS Code settings file initialized by `ptbk coder init`.
2230
+ * Returns the default coder `AGENTS.md` boilerplate instructions.
2231
+ *
2232
+ * @private internal utility of `ptbk coder`
2105
2233
  */
2106
- const VSCODE_SETTINGS_FILE_PATH = '.vscode/settings.json';
2234
+ function getDefaultCoderAgentsFileContent() {
2235
+ return DEFAULT_CODER_AGENTS_FILE_CONTENT;
2236
+ }
2237
+ // Note: [🟡] Code for coder AGENTS file boilerplate [agentsFile](src/cli/cli-commands/coder/agentsFile.ts) should never be published outside of `@promptbook/cli`
2238
+ // Note: [💞] Ignore a discrepancy between file name and exported helper names
2239
+
2107
2240
  /**
2108
- * Relative path to the VS Code directory initialized by `ptbk coder init`.
2241
+ * Normalizes one project-relative path for human-readable CLI output and markdown.
2242
+ *
2243
+ * @private internal utility of `ptbk coder`
2109
2244
  */
2110
- const VSCODE_DIRECTORY_PATH = '.vscode';
2245
+ function formatDisplayPath(relativePath) {
2246
+ return relativePath.replace(/\\/gu, '/');
2247
+ }
2248
+ // Note: [🟡] Code for coder path formatting [formatDisplayPath](src/cli/cli-commands/coder/formatDisplayPath.ts) should never be published outside of `@promptbook/cli`
2249
+
2111
2250
  /**
2112
- * VS Code setting key used to route pasted markdown images into prompt-specific screenshots.
2251
+ * Relative path to the Promptbook Coder quick-reference file initialized in project roots.
2252
+ *
2253
+ * @private internal utility of `ptbk coder`
2113
2254
  */
2114
- const MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY = 'markdown.copyFiles.destination';
2255
+ const AGENT_CODING_FILE_PATH = 'AGENT_CODING.md';
2115
2256
  /**
2116
- * Markdown glob used for coder prompt files inside VS Code settings.
2257
+ * Returns the default coder `AGENT_CODING.md` quick-reference content.
2258
+ *
2259
+ * @private internal utility of `ptbk coder`
2117
2260
  */
2118
- const PROMPTS_MARKDOWN_FILE_GLOB = 'prompts/*md';
2261
+ function getDefaultCoderAgentCodingFileContent({ packageJsonScripts, }) {
2262
+ return [
2263
+ '# Promptbook Coder quick reference',
2264
+ '',
2265
+ `This project is prepared for the \`ptbk coder\` workflow. Promptbook Coder does not create a new model on its own; it orchestrates coding agents such as GitHub Copilot, OpenAI Codex, Claude Code, Opencode, Cline, and Gemini CLI through prompt files in \`${formatDisplayPath(PROMPTS_DIRECTORY_PATH)}/\`.`,
2266
+ '',
2267
+ '## Workflow',
2268
+ `1. Put repository-wide coding rules into \`${AGENTS_FILE_PATH}\`. The default \`npm run coder:run\` script already passes \`--context ${AGENTS_FILE_PATH}\`.`,
2269
+ `2. Create or customize prompt templates in \`${formatDisplayPath(PROMPTS_TEMPLATES_DIRECTORY_PATH)}/\`. ${buildStarterTemplateSentence()}`,
2270
+ '3. Generate prompt files with `npm run coder:generate-boilerplates` or `npx ptbk coder generate-boilerplates --template <template> --count <count>`.',
2271
+ '4. Replace every `@@@`, keep drafts as `[-]`, and switch prompts to `[ ]` when they are ready to run. Completed prompts are marked `[x]`.',
2272
+ '5. Run `npm run coder:run` to execute the next ready prompt with the configured coding agent.',
2273
+ `6. Use \`npm run coder:verify\` to archive finished prompts into \`${formatDisplayPath(PROMPTS_DONE_DIRECTORY_PATH)}/\` and append repair follow-up prompts when more work is needed.`,
2274
+ '7. Use `npm run coder:find-refactor-candidates` when you want Promptbook to suggest refactor prompts automatically.',
2275
+ '',
2276
+ '## Templates',
2277
+ `- Project-owned templates created by \`ptbk coder init\`: ${formatInlineCodeList(getDefaultCoderProjectPromptTemplateDefinitions().map(({ relativeFilePath }) => formatDisplayPath(relativeFilePath)))}`,
2278
+ `- Built-in \`--template\` aliases: ${formatInlineCodeList(getDefaultCoderPromptTemplateDefinitions().map(({ id }) => id))}`,
2279
+ `- To add a custom template, create a markdown file such as \`${formatDisplayPath(PROMPTS_TEMPLATES_DIRECTORY_PATH)}/backend.md\`.`,
2280
+ `- To use a project template, run \`npx ptbk coder generate-boilerplates --template ${formatDisplayPath(PROMPTS_TEMPLATES_DIRECTORY_PATH)}/backend.md\`.`,
2281
+ `- Keep shared repository rules in \`${AGENTS_FILE_PATH}\` and recurring task-family rules in template files so individual prompt files stay focused on the actual task.`,
2282
+ '',
2283
+ '## Created npm scripts',
2284
+ '| Script | Purpose |',
2285
+ '| --- | --- |',
2286
+ ...buildPackageJsonScriptTableLines(packageJsonScripts),
2287
+ '',
2288
+ '## Customizing the workflow',
2289
+ '- Edit `package.json` if you want `npm run coder:run` to use another coding agent, model, thinking level, context file, or wait mode.',
2290
+ '- Use direct CLI commands when you need one-off flags such as `--priority`, `--ignore-git-changes`, `--dry-run`, `--allow-credits`, or `--auto-migrate`.',
2291
+ '- Use `npx ptbk coder --help` and `npx ptbk coder <command> --help` for the full CLI reference.',
2292
+ ].join('\n');
2293
+ }
2119
2294
  /**
2120
- * Screenshot destination used for pasted prompt images inside VS Code settings.
2295
+ * Builds the sentence describing the starter templates created during initialization.
2121
2296
  */
2122
- const PROMPTS_SCREENSHOT_DESTINATION = './prompts/screenshots/${documentBaseName}.png';
2297
+ function buildStarterTemplateSentence() {
2298
+ const starterTemplatePaths = getDefaultCoderProjectPromptTemplateDefinitions().map(({ relativeFilePath }) => formatDisplayPath(relativeFilePath));
2299
+ if (starterTemplatePaths.length === 1) {
2300
+ return `The starter project template created by \`ptbk coder init\` is \`${starterTemplatePaths[0]}\`.`;
2301
+ }
2302
+ return `The starter project templates created by \`ptbk coder init\` are ${formatInlineCodeList(starterTemplatePaths)}.`;
2303
+ }
2123
2304
  /**
2124
- * Default indentation used when creating new JSON configuration files.
2305
+ * Builds the markdown table rows describing the initialized npm scripts.
2125
2306
  */
2126
- const DEFAULT_JSON_FILE_INDENTATION = ' ';
2307
+ function buildPackageJsonScriptTableLines(packageJsonScripts) {
2308
+ return Object.entries(packageJsonScripts).map(([scriptName, scriptCommand]) => `| \`npm run ${scriptName}\` | ${describeDefaultCoderPackageJsonScript(scriptName, scriptCommand)} |`);
2309
+ }
2127
2310
  /**
2128
- * Default newline used when creating new JSON configuration files.
2311
+ * Describes one initialized npm script in human-readable terms.
2129
2312
  */
2130
- const DEFAULT_JSON_FILE_NEWLINE = '\n';
2313
+ function describeDefaultCoderPackageJsonScript(scriptName, scriptCommand) {
2314
+ if (scriptName === 'coder:generate-boilerplates') {
2315
+ return `Runs \`${scriptCommand}\` to create new prompt files in \`${formatDisplayPath(PROMPTS_DIRECTORY_PATH)}/\`.`;
2316
+ }
2317
+ if (scriptName === 'coder:run') {
2318
+ return `Runs \`${scriptCommand}\` to execute the next ready prompt with shared repository context from \`${AGENTS_FILE_PATH}\`.`;
2319
+ }
2320
+ if (scriptName === 'coder:find-refactor-candidates') {
2321
+ return `Runs \`${scriptCommand}\` to generate prompt candidates for large or crowded files.`;
2322
+ }
2323
+ if (scriptName === 'coder:verify') {
2324
+ return `Runs \`${scriptCommand}\` to archive verified prompts into \`${formatDisplayPath(PROMPTS_DONE_DIRECTORY_PATH)}/\` and append repair prompts when needed.`;
2325
+ }
2326
+ return `Runs \`${scriptCommand}\`.`;
2327
+ }
2131
2328
  /**
2132
- * `.gitignore` block required by standalone Promptbook coder projects.
2329
+ * Formats one inline code list for human-readable markdown.
2133
2330
  */
2134
- const CODER_GITIGNORE_BLOCK = _spaceTrim.spaceTrim(`
2135
- # Promptbook Coder
2136
- /.tmp
2137
- `);
2331
+ function formatInlineCodeList(values) {
2332
+ return values.map((value) => `\`${value}\``).join(', ');
2333
+ }
2334
+ // Note: [🟡] Code for coder AGENT_CODING file boilerplate [agentCodingFile](src/cli/cli-commands/coder/agentCodingFile.ts) should never be published outside of `@promptbook/cli`
2335
+ // Note: [💞] Ignore a discrepancy between file name and exported helper names
2336
+
2138
2337
  /**
2139
- * Initializes `coder init` command for Promptbook CLI utilities.
2140
- *
2141
- * Note: `$` is used to indicate that this function is not a pure function - it registers a command in the CLI.
2338
+ * Appends one text block to existing file content while preserving readable newlines.
2142
2339
  *
2143
- * @private internal function of `promptbookCli`
2340
+ * @private function of `initializeCoderProjectConfiguration`
2144
2341
  */
2145
- function $initializeCoderInitCommand(program) {
2146
- const command = program.command('init');
2147
- command.alias('initialize');
2148
- command.description(_spaceTrim.spaceTrim(`
2149
- Initialize Promptbook coder configuration for current project
2150
-
2151
- Creates or updates:
2152
- - prompts/
2153
- - prompts/done/
2154
- - prompts/templates/common.md
2155
- - prompts/templates/agents-server.md
2156
- - .gitignore
2157
- - package.json
2158
- - .vscode/settings.json
2159
-
2160
- Ensures required coding-agent environment variables in .env:
2161
- - CODING_AGENT_GIT_NAME
2162
- - CODING_AGENT_GIT_EMAIL
2163
- - CODING_AGENT_GIT_SIGNING_KEY
2164
- `));
2165
- command.action(handleActionErrors(async () => {
2166
- const summary = await initializeCoderProjectConfiguration(process.cwd());
2167
- printInitializationSummary(summary);
2168
- }));
2342
+ function appendBlock(currentContent, blockToAppend) {
2343
+ if (currentContent.trim() === '') {
2344
+ return `${blockToAppend}\n`;
2345
+ }
2346
+ const normalizedCurrentContent = currentContent.endsWith('\n') ? currentContent : `${currentContent}\n`;
2347
+ return `${normalizedCurrentContent}\n${blockToAppend}\n`;
2169
2348
  }
2349
+ // Note: [🟡] Code for coder init text appending [appendBlock](src/cli/cli-commands/coder/appendBlock.ts) should never be published outside of `@promptbook/cli`
2350
+
2170
2351
  /**
2171
- * Lists the default npm scripts initialized by `ptbk coder init`.
2352
+ * Reads one text file when it exists, otherwise returns `undefined`.
2172
2353
  *
2173
- * @private internal utility of `coder init` command
2354
+ * @private function of `initializeCoderProjectConfiguration`
2174
2355
  */
2175
- function getDefaultCoderPackageJsonScripts() {
2176
- return DEFAULT_CODER_PACKAGE_JSON_SCRIPTS;
2356
+ async function readTextFileIfExists(path) {
2357
+ try {
2358
+ const fileStats = await promises.stat(path);
2359
+ if (!fileStats.isFile()) {
2360
+ return undefined;
2361
+ }
2362
+ }
2363
+ catch (_a) {
2364
+ return undefined;
2365
+ }
2366
+ return promises.readFile(path, 'utf-8');
2177
2367
  }
2368
+ // Note: [🟡] Code for coder init text-file reading [readTextFileIfExists](src/cli/cli-commands/coder/readTextFileIfExists.ts) should never be published outside of `@promptbook/cli`
2369
+
2178
2370
  /**
2179
- * Creates or updates all coder configuration artifacts required in the current project.
2180
- *
2181
- * @private internal utility of `coder init` command
2371
+ * Relative path to `.env` in the initialized project.
2182
2372
  */
2183
- async function initializeCoderProjectConfiguration(projectPath) {
2184
- const promptsDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_DIRECTORY_PATH);
2185
- const promptsDoneDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_DONE_DIRECTORY_PATH);
2186
- const promptsTemplatesDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_TEMPLATES_DIRECTORY_PATH);
2187
- const promptTemplateFileStatuses = await ensureDefaultCoderPromptTemplateFiles(projectPath);
2188
- const { envFileStatus, initializedEnvVariableNames } = await ensureCoderEnvFile(projectPath);
2189
- const gitignoreFileStatus = await ensureCoderGitignoreFile(projectPath);
2190
- const packageJsonFileStatus = await ensureCoderPackageJsonFile(projectPath);
2191
- const vscodeSettingsFileStatus = await ensureCoderVscodeSettingsFile(projectPath);
2192
- return {
2193
- promptsDirectoryStatus,
2194
- promptsDoneDirectoryStatus,
2195
- promptsTemplatesDirectoryStatus,
2196
- promptTemplateFileStatuses,
2197
- envFileStatus,
2198
- gitignoreFileStatus,
2199
- packageJsonFileStatus,
2200
- vscodeSettingsFileStatus,
2201
- initializedEnvVariableNames,
2202
- };
2203
- }
2373
+ const ENV_FILE_PATH = '.env';
2204
2374
  /**
2205
- * Ensures a relative directory exists in the project root.
2375
+ * Fallback `.env` content used when no required variables need to be appended.
2206
2376
  */
2207
- async function ensureDirectory(projectPath, relativeDirectoryPath) {
2208
- const directoryPath = path.join(projectPath, relativeDirectoryPath);
2209
- const existedBefore = await isExistingDirectory(directoryPath);
2210
- if (!existedBefore) {
2211
- await promises.mkdir(directoryPath, { recursive: true });
2212
- return 'created';
2213
- }
2214
- return 'unchanged';
2215
- }
2377
+ const EMPTY_CODER_ENV_FILE_CONTENT = '# Environment variables for Promptbook coder\n';
2378
+ /**
2379
+ * Required environment variables for coding-agent git identity.
2380
+ */
2381
+ const REQUIRED_CODER_ENV_VARIABLES = [
2382
+ {
2383
+ name: 'CODING_AGENT_GIT_NAME',
2384
+ value: 'Promptbook Coding Agent',
2385
+ },
2386
+ {
2387
+ name: 'CODING_AGENT_GIT_EMAIL',
2388
+ value: 'coding-agent@promptbook.studio',
2389
+ },
2390
+ {
2391
+ name: 'CODING_AGENT_GIT_SIGNING_KEY',
2392
+ value: '13406525ED912F938FEA85AB4046C687298B2382',
2393
+ },
2394
+ ];
2216
2395
  /**
2217
2396
  * Ensures `.env` exists and contains all required coder environment variables.
2397
+ *
2398
+ * @private function of `initializeCoderProjectConfiguration`
2218
2399
  */
2219
2400
  async function ensureCoderEnvFile(projectPath) {
2220
- const envFilePath = path.join(projectPath, '.env');
2221
- const envFileExistedBefore = await isExistingFile(envFilePath);
2222
- const currentEnvContent = envFileExistedBefore ? await promises.readFile(envFilePath, 'utf-8') : '';
2223
- const existingEnvVariables = parseEnvVariableNames(currentEnvContent);
2224
- const missingEnvVariables = REQUIRED_CODER_ENV_VARIABLES.filter(({ name }) => !existingEnvVariables.has(name));
2401
+ const envFilePath = path.join(projectPath, ENV_FILE_PATH);
2402
+ const existingEnvContent = await readTextFileIfExists(envFilePath);
2403
+ const isEnvFileExisting = existingEnvContent !== undefined;
2404
+ const currentEnvContent = existingEnvContent || '';
2405
+ const existingEnvVariableNames = parseEnvVariableNames(currentEnvContent);
2406
+ const missingEnvVariables = REQUIRED_CODER_ENV_VARIABLES.filter(({ name }) => !existingEnvVariableNames.has(name));
2225
2407
  if (missingEnvVariables.length === 0) {
2226
- if (!envFileExistedBefore) {
2227
- await promises.writeFile(envFilePath, '# Environment variables for Promptbook coder\n', 'utf-8');
2408
+ if (!isEnvFileExisting) {
2409
+ await promises.writeFile(envFilePath, EMPTY_CODER_ENV_FILE_CONTENT, 'utf-8');
2228
2410
  return {
2229
2411
  envFileStatus: 'created',
2230
2412
  initializedEnvVariableNames: [],
@@ -2239,12 +2421,54 @@
2239
2421
  const nextEnvContent = appendBlock(currentEnvContent, envBlockToAppend);
2240
2422
  await promises.writeFile(envFilePath, nextEnvContent, 'utf-8');
2241
2423
  return {
2242
- envFileStatus: envFileExistedBefore ? 'updated' : 'created',
2424
+ envFileStatus: isEnvFileExisting ? 'updated' : 'created',
2243
2425
  initializedEnvVariableNames: missingEnvVariables.map(({ name }) => name),
2244
2426
  };
2245
2427
  }
2428
+ /**
2429
+ * Parses variable names currently defined in `.env` style content.
2430
+ */
2431
+ function parseEnvVariableNames(envContent) {
2432
+ const variableNames = new Set();
2433
+ for (const line of envContent.split(/\r?\n/)) {
2434
+ const trimmedLine = line.trim();
2435
+ if (trimmedLine === '' || trimmedLine.startsWith('#')) {
2436
+ continue;
2437
+ }
2438
+ const match = trimmedLine.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
2439
+ if (!match || !match[1]) {
2440
+ continue;
2441
+ }
2442
+ variableNames.add(match[1]);
2443
+ }
2444
+ return variableNames;
2445
+ }
2446
+ /**
2447
+ * Builds a `.env` block containing missing coder environment variables.
2448
+ */
2449
+ function buildMissingEnvVariablesBlock(variables) {
2450
+ return _spaceTrim.spaceTrim(`
2451
+ # Promptbook coder identity (initialized by \`ptbk coder init\`)
2452
+ ${variables.map(({ name, value }) => `${name}=${JSON.stringify(value)}`).join('\n')}
2453
+ `);
2454
+ }
2455
+ // Note: [🟡] Code for coder init environment bootstrapping [ensureCoderEnvFile](src/cli/cli-commands/coder/ensureCoderEnvFile.ts) should never be published outside of `@promptbook/cli`
2456
+
2457
+ /**
2458
+ * Relative path to `.gitignore` in the initialized project.
2459
+ */
2460
+ const GITIGNORE_FILE_PATH = '.gitignore';
2461
+ /**
2462
+ * `.gitignore` block required by standalone Promptbook coder projects.
2463
+ */
2464
+ const CODER_GITIGNORE_BLOCK = _spaceTrim.spaceTrim(`
2465
+ # Promptbook Coder
2466
+ /.tmp
2467
+ `);
2246
2468
  /**
2247
2469
  * Ensures `.gitignore` contains the standalone Promptbook coder cache entry.
2470
+ *
2471
+ * @private function of `initializeCoderProjectConfiguration`
2248
2472
  */
2249
2473
  async function ensureCoderGitignoreFile(projectPath) {
2250
2474
  const gitignorePath = path.join(projectPath, GITIGNORE_FILE_PATH);
@@ -2257,95 +2481,339 @@
2257
2481
  return currentGitignoreContent === undefined ? 'created' : 'updated';
2258
2482
  }
2259
2483
  /**
2260
- * Ensures `package.json` contains the standalone Promptbook coder helper scripts.
2484
+ * Detects whether `.gitignore` already covers the standalone coder temp directory.
2261
2485
  */
2262
- async function ensureCoderPackageJsonFile(projectPath) {
2263
- const packageJsonPath = path.join(projectPath, PACKAGE_JSON_FILE_PATH);
2264
- const packageJsonContent = await readTextFileIfExists(packageJsonPath);
2265
- const formatting = detectJsonFileFormatting(packageJsonContent);
2266
- const packageJson = packageJsonContent === undefined ? {} : await parseJsonObjectFile(PACKAGE_JSON_FILE_PATH, packageJsonContent);
2267
- const scripts = getStringRecordOrDefault(packageJson['scripts'], PACKAGE_JSON_FILE_PATH, 'scripts');
2268
- let hasChanges = packageJsonContent === undefined;
2269
- const nextScripts = { ...scripts };
2270
- for (const [scriptName, scriptCommand] of Object.entries(getDefaultCoderPackageJsonScripts())) {
2271
- if (nextScripts[scriptName] !== scriptCommand) {
2272
- nextScripts[scriptName] = scriptCommand;
2486
+ function hasTmpGitignoreRule(gitignoreContent) {
2487
+ return /(^|[\r\n])\/?\.tmp(?:[\r\n]|$)/u.test(gitignoreContent);
2488
+ }
2489
+ // Note: [🟡] Code for coder init gitignore bootstrapping [ensureCoderGitignoreFile](src/cli/cli-commands/coder/ensureCoderGitignoreFile.ts) should never be published outside of `@promptbook/cli`
2490
+
2491
+ /**
2492
+ * Ensures one coder markdown file exists with the provided default boilerplate.
2493
+ *
2494
+ * @private function of `initializeCoderProjectConfiguration`
2495
+ */
2496
+ async function ensureCoderMarkdownFile(projectPath, relativeFilePath, fileContent) {
2497
+ const absoluteFilePath = path.join(projectPath, relativeFilePath);
2498
+ if (await isExistingFile(absoluteFilePath)) {
2499
+ return 'unchanged';
2500
+ }
2501
+ await promises.writeFile(absoluteFilePath, `${fileContent}\n`, 'utf-8');
2502
+ return 'created';
2503
+ }
2504
+ /**
2505
+ * Checks whether a path exists and is a file.
2506
+ */
2507
+ async function isExistingFile(path) {
2508
+ try {
2509
+ return (await promises.stat(path)).isFile();
2510
+ }
2511
+ catch (_a) {
2512
+ return false;
2513
+ }
2514
+ }
2515
+ // Note: [🟡] Code for coder init markdown bootstrapping [ensureCoderMarkdownFile](src/cli/cli-commands/coder/ensureCoderMarkdownFile.ts) should never be published outside of `@promptbook/cli`
2516
+
2517
+ /**
2518
+ * Default npm scripts initialized by `ptbk coder init`.
2519
+ */
2520
+ const DEFAULT_CODER_PACKAGE_JSON_SCRIPTS = {
2521
+ 'coder:generate-boilerplates': 'npx ptbk coder generate-boilerplates',
2522
+ 'coder:run': 'npx ptbk coder run --agent github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --no-wait',
2523
+ 'coder:find-refactor-candidates': 'npx ptbk coder find-refactor-candidates',
2524
+ 'coder:verify': 'npx ptbk coder verify',
2525
+ };
2526
+ /**
2527
+ * Lists the default npm scripts initialized by `ptbk coder init`.
2528
+ *
2529
+ * @private internal utility of `coder init` command
2530
+ */
2531
+ function getDefaultCoderPackageJsonScripts() {
2532
+ return DEFAULT_CODER_PACKAGE_JSON_SCRIPTS;
2533
+ }
2534
+ // Note: [🟡] Code for coder init package scripts [getDefaultCoderPackageJsonScripts](src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.ts) should never be published outside of `@promptbook/cli`
2535
+
2536
+ /**
2537
+ * This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
2538
+ *
2539
+ * @public exported from `@promptbook/core`
2540
+ */
2541
+ class ParseError extends Error {
2542
+ constructor(message) {
2543
+ super(message);
2544
+ this.name = 'ParseError';
2545
+ Object.setPrototypeOf(this, ParseError.prototype);
2546
+ }
2547
+ }
2548
+ // TODO: Maybe split `ParseError` and `ApplyError`
2549
+
2550
+ /**
2551
+ * Default indentation used when creating new JSON configuration files.
2552
+ */
2553
+ const DEFAULT_JSON_FILE_INDENTATION = ' ';
2554
+ /**
2555
+ * Default newline used when creating new JSON configuration files.
2556
+ */
2557
+ const DEFAULT_JSON_FILE_NEWLINE = '\n';
2558
+ /**
2559
+ * Ensures one JSON object field contains the provided string-record entries.
2560
+ *
2561
+ * @private function of `initializeCoderProjectConfiguration`
2562
+ */
2563
+ async function mergeStringRecordJsonFile({ projectPath, relativeFilePath, fieldPath, nextEntries, ensureParentDirectoryPath, }) {
2564
+ if (ensureParentDirectoryPath) {
2565
+ await promises.mkdir(path.join(projectPath, ensureParentDirectoryPath), { recursive: true });
2566
+ }
2567
+ const absoluteFilePath = path.join(projectPath, relativeFilePath);
2568
+ const fileContent = await readTextFileIfExists(absoluteFilePath);
2569
+ const formatting = detectJsonFileFormatting(fileContent);
2570
+ const jsonObject = fileContent === undefined ? {} : await parseJsonObjectFile(relativeFilePath, fileContent);
2571
+ const existingEntries = getStringRecordOrDefault(jsonObject[fieldPath], relativeFilePath, fieldPath);
2572
+ let hasChanges = fileContent === undefined;
2573
+ const mergedEntries = { ...existingEntries };
2574
+ for (const [entryKey, entryValue] of Object.entries(nextEntries)) {
2575
+ if (mergedEntries[entryKey] !== entryValue) {
2576
+ mergedEntries[entryKey] = entryValue;
2273
2577
  hasChanges = true;
2274
2578
  }
2275
2579
  }
2276
2580
  if (!hasChanges) {
2277
2581
  return 'unchanged';
2278
2582
  }
2279
- const nextPackageJson = { ...packageJson };
2280
- nextPackageJson['scripts'] = nextScripts;
2281
- await promises.writeFile(packageJsonPath, serializeJsonObject(nextPackageJson, formatting), 'utf-8');
2282
- return packageJsonContent === undefined ? 'created' : 'updated';
2583
+ const nextJsonObject = { ...jsonObject };
2584
+ nextJsonObject[fieldPath] = mergedEntries;
2585
+ await promises.writeFile(absoluteFilePath, serializeJsonObject(nextJsonObject, formatting), 'utf-8');
2586
+ return fileContent === undefined ? 'created' : 'updated';
2283
2587
  }
2284
2588
  /**
2285
- * Ensures VS Code routes pasted prompt images into `prompts/screenshots`.
2589
+ * Parses one JSON object file while accepting VS Code style comments and trailing commas.
2286
2590
  */
2287
- async function ensureCoderVscodeSettingsFile(projectPath) {
2288
- await promises.mkdir(path.join(projectPath, VSCODE_DIRECTORY_PATH), { recursive: true });
2289
- const vscodeSettingsPath = path.join(projectPath, VSCODE_SETTINGS_FILE_PATH);
2290
- const vscodeSettingsContent = await readTextFileIfExists(vscodeSettingsPath);
2291
- const formatting = detectJsonFileFormatting(vscodeSettingsContent);
2292
- const vscodeSettings = vscodeSettingsContent === undefined
2293
- ? {}
2294
- : await parseJsonObjectFile(VSCODE_SETTINGS_FILE_PATH, vscodeSettingsContent);
2295
- const markdownCopyFilesDestinations = getStringRecordOrDefault(vscodeSettings[MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY], VSCODE_SETTINGS_FILE_PATH, MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY);
2296
- let hasChanges = vscodeSettingsContent === undefined;
2297
- const nextMarkdownCopyFilesDestinations = { ...markdownCopyFilesDestinations };
2298
- if (nextMarkdownCopyFilesDestinations[PROMPTS_MARKDOWN_FILE_GLOB] !== PROMPTS_SCREENSHOT_DESTINATION) {
2299
- nextMarkdownCopyFilesDestinations[PROMPTS_MARKDOWN_FILE_GLOB] = PROMPTS_SCREENSHOT_DESTINATION;
2300
- hasChanges = true;
2591
+ async function parseJsonObjectFile(relativeFilePath, fileContent) {
2592
+ if (fileContent.trim() === '') {
2593
+ return {};
2301
2594
  }
2302
- if (!hasChanges) {
2303
- return 'unchanged';
2595
+ const typescript = await import('typescript');
2596
+ const parsedFile = typescript.parseConfigFileTextToJson(relativeFilePath, fileContent);
2597
+ if (parsedFile.error) {
2598
+ throw new ParseError(_spaceTrim.spaceTrim(`
2599
+ Cannot parse \`${relativeFilePath}\` as JSON.
2600
+
2601
+ ${typescript.flattenDiagnosticMessageText(parsedFile.error.messageText, '\n')}
2602
+ `));
2603
+ }
2604
+ if (!isPlainObject(parsedFile.config)) {
2605
+ throw new ParseError(_spaceTrim.spaceTrim(`
2606
+ File \`${relativeFilePath}\` must contain one top-level JSON object.
2607
+ `));
2304
2608
  }
2305
- const nextVscodeSettings = { ...vscodeSettings };
2306
- nextVscodeSettings[MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY] = nextMarkdownCopyFilesDestinations;
2307
- await promises.writeFile(vscodeSettingsPath, serializeJsonObject(nextVscodeSettings, formatting), 'utf-8');
2308
- return vscodeSettingsContent === undefined ? 'created' : 'updated';
2609
+ return parsedFile.config;
2309
2610
  }
2310
2611
  /**
2311
- * Parses variable names currently defined in `.env` style content.
2612
+ * Reads one JSON object field as a string-to-string record.
2312
2613
  */
2313
- function parseEnvVariableNames(envContent) {
2314
- const variableNames = new Set();
2315
- for (const line of envContent.split(/\r?\n/)) {
2316
- const trimmedLine = line.trim();
2317
- if (trimmedLine === '' || trimmedLine.startsWith('#')) {
2318
- continue;
2319
- }
2320
- const match = trimmedLine.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
2321
- if (!match || !match[1]) {
2322
- continue;
2614
+ function getStringRecordOrDefault(value, relativeFilePath, fieldPath) {
2615
+ if (value === undefined) {
2616
+ return {};
2617
+ }
2618
+ if (!isPlainObject(value)) {
2619
+ throw new ParseError(_spaceTrim.spaceTrim(`
2620
+ File \`${relativeFilePath}\` contains invalid \`${fieldPath}\`.
2621
+
2622
+ Expected \`${fieldPath}\` to be an object with string values.
2623
+ `));
2624
+ }
2625
+ const stringRecord = {};
2626
+ for (const [key, itemValue] of Object.entries(value)) {
2627
+ if (typeof itemValue !== 'string') {
2628
+ throw new ParseError(_spaceTrim.spaceTrim(`
2629
+ File \`${relativeFilePath}\` contains invalid \`${fieldPath}.${key}\`.
2630
+
2631
+ Expected \`${fieldPath}\` to be an object with string values.
2632
+ `));
2323
2633
  }
2324
- variableNames.add(match[1]);
2634
+ stringRecord[key] = itemValue;
2325
2635
  }
2326
- return variableNames;
2636
+ return stringRecord;
2327
2637
  }
2328
2638
  /**
2329
- * Builds a `.env` block containing missing coder environment variables.
2639
+ * Serializes one JSON object using detected or default formatting.
2330
2640
  */
2331
- function buildMissingEnvVariablesBlock(variables) {
2332
- return _spaceTrim.spaceTrim(`
2333
- # Promptbook coder identity (initialized by \`ptbk coder init\`)
2334
- ${variables.map(({ name, value }) => `${name}=${JSON.stringify(value)}`).join('\n')}
2335
- `);
2641
+ function serializeJsonObject(value, formatting) {
2642
+ return `${JSON.stringify(value, null, formatting.indentation)}${formatting.newline}`;
2336
2643
  }
2337
2644
  /**
2338
- * Appends one text block to existing file content while preserving readable newlines.
2645
+ * Detects indentation and newline formatting from an existing JSON file.
2339
2646
  */
2340
- function appendBlock(currentContent, blockToAppend) {
2341
- if (currentContent.trim() === '') {
2342
- return `${blockToAppend}\n`;
2647
+ function detectJsonFileFormatting(fileContent) {
2648
+ if (!fileContent) {
2649
+ return {
2650
+ indentation: DEFAULT_JSON_FILE_INDENTATION,
2651
+ newline: DEFAULT_JSON_FILE_NEWLINE,
2652
+ };
2653
+ }
2654
+ const indentationMatch = fileContent.match(/^[ \t]+(?=")/mu);
2655
+ return {
2656
+ indentation: (indentationMatch === null || indentationMatch === void 0 ? void 0 : indentationMatch[0]) || DEFAULT_JSON_FILE_INDENTATION,
2657
+ newline: fileContent.includes('\r\n') ? '\r\n' : '\n',
2658
+ };
2659
+ }
2660
+ /**
2661
+ * Checks whether one parsed JSON value is a plain object.
2662
+ */
2663
+ function isPlainObject(value) {
2664
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
2665
+ }
2666
+ // Note: [🟡] Code for coder init JSON merging [mergeStringRecordJsonFile](src/cli/cli-commands/coder/mergeStringRecordJsonFile.ts) should never be published outside of `@promptbook/cli`
2667
+
2668
+ /**
2669
+ * Relative path to `package.json` in the initialized project.
2670
+ */
2671
+ const PACKAGE_JSON_FILE_PATH = 'package.json';
2672
+ /**
2673
+ * Ensures `package.json` contains the standalone Promptbook coder helper scripts.
2674
+ *
2675
+ * @private function of `initializeCoderProjectConfiguration`
2676
+ */
2677
+ async function ensureCoderPackageJsonFile(projectPath) {
2678
+ return mergeStringRecordJsonFile({
2679
+ projectPath,
2680
+ relativeFilePath: PACKAGE_JSON_FILE_PATH,
2681
+ fieldPath: 'scripts',
2682
+ nextEntries: getDefaultCoderPackageJsonScripts(),
2683
+ });
2684
+ }
2685
+ // Note: [🟡] Code for coder init package.json bootstrapping [ensureCoderPackageJsonFile](src/cli/cli-commands/coder/ensureCoderPackageJsonFile.ts) should never be published outside of `@promptbook/cli`
2686
+
2687
+ /**
2688
+ * VS Code setting key used to route pasted markdown images into prompt-specific screenshots.
2689
+ */
2690
+ const MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY = 'markdown.copyFiles.destination';
2691
+ /**
2692
+ * Markdown glob used for coder prompt files inside VS Code settings.
2693
+ */
2694
+ const PROMPTS_MARKDOWN_FILE_GLOB = 'prompts/*md';
2695
+ /**
2696
+ * Screenshot destination used for pasted prompt images inside VS Code settings.
2697
+ */
2698
+ const PROMPTS_SCREENSHOT_DESTINATION = './prompts/screenshots/${documentBaseName}.png';
2699
+ /**
2700
+ * Default VS Code settings initialized by `ptbk coder init`.
2701
+ */
2702
+ const DEFAULT_CODER_VSCODE_SETTINGS = {
2703
+ [MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY]: {
2704
+ [PROMPTS_MARKDOWN_FILE_GLOB]: PROMPTS_SCREENSHOT_DESTINATION,
2705
+ },
2706
+ };
2707
+ /**
2708
+ * Lists the default VS Code settings initialized by `ptbk coder init`.
2709
+ *
2710
+ * @private internal utility of `coder init` command
2711
+ */
2712
+ function getDefaultCoderVscodeSettings() {
2713
+ return DEFAULT_CODER_VSCODE_SETTINGS;
2714
+ }
2715
+ // Note: [🟡] Code for coder init VS Code settings [getDefaultCoderVscodeSettings](src/cli/cli-commands/coder/getDefaultCoderVscodeSettings.ts) should never be published outside of `@promptbook/cli`
2716
+
2717
+ /**
2718
+ * Relative path to the VS Code settings file initialized by `ptbk coder init`.
2719
+ */
2720
+ const VSCODE_SETTINGS_FILE_PATH = '.vscode/settings.json';
2721
+ /**
2722
+ * Relative path to the VS Code directory initialized by `ptbk coder init`.
2723
+ */
2724
+ const VSCODE_DIRECTORY_PATH = '.vscode';
2725
+ /**
2726
+ * Ensures VS Code routes pasted prompt images into `prompts/screenshots`.
2727
+ *
2728
+ * @private function of `initializeCoderProjectConfiguration`
2729
+ */
2730
+ async function ensureCoderVscodeSettingsFile(projectPath) {
2731
+ const [fieldPath, nextEntries] = resolveDefaultCoderVscodeSettingsEntry();
2732
+ return mergeStringRecordJsonFile({
2733
+ projectPath,
2734
+ relativeFilePath: VSCODE_SETTINGS_FILE_PATH,
2735
+ fieldPath,
2736
+ nextEntries,
2737
+ ensureParentDirectoryPath: VSCODE_DIRECTORY_PATH,
2738
+ });
2739
+ }
2740
+ /**
2741
+ * Resolves the default string-record entry that `coder init` merges into VS Code settings.
2742
+ */
2743
+ function resolveDefaultCoderVscodeSettingsEntry() {
2744
+ const [defaultVscodeSettingsEntry] = Object.entries(getDefaultCoderVscodeSettings());
2745
+ if (!defaultVscodeSettingsEntry) {
2746
+ throw new Error('Default coder VS Code settings must define at least one string-record entry.');
2747
+ }
2748
+ return defaultVscodeSettingsEntry;
2749
+ }
2750
+ // Note: [🟡] Code for coder init VS Code bootstrapping [ensureCoderVscodeSettingsFile](src/cli/cli-commands/coder/ensureCoderVscodeSettingsFile.ts) should never be published outside of `@promptbook/cli`
2751
+
2752
+ /**
2753
+ * Ensures a relative directory exists in the project root.
2754
+ *
2755
+ * @private function of `initializeCoderProjectConfiguration`
2756
+ */
2757
+ async function ensureDirectory(projectPath, relativeDirectoryPath) {
2758
+ const directoryPath = path.join(projectPath, relativeDirectoryPath);
2759
+ const isDirectoryExisting = await isExistingDirectory(directoryPath);
2760
+ if (!isDirectoryExisting) {
2761
+ await promises.mkdir(directoryPath, { recursive: true });
2762
+ return 'created';
2763
+ }
2764
+ return 'unchanged';
2765
+ }
2766
+ /**
2767
+ * Checks whether a path exists and is a directory.
2768
+ */
2769
+ async function isExistingDirectory(path) {
2770
+ try {
2771
+ return (await promises.stat(path)).isDirectory();
2772
+ }
2773
+ catch (_a) {
2774
+ return false;
2343
2775
  }
2344
- const normalizedCurrentContent = currentContent.endsWith('\n') ? currentContent : `${currentContent}\n`;
2345
- return `${normalizedCurrentContent}\n${blockToAppend}\n`;
2346
2776
  }
2777
+ // Note: [🟡] Code for coder init directory creation [ensureDirectory](src/cli/cli-commands/coder/ensureDirectory.ts) should never be published outside of `@promptbook/cli`
2778
+
2779
+ /**
2780
+ * Creates or updates all coder configuration artifacts required in the current project.
2781
+ *
2782
+ * @private internal utility of `coder init` command
2783
+ */
2784
+ async function initializeCoderProjectConfiguration(projectPath) {
2785
+ const promptsDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_DIRECTORY_PATH);
2786
+ const promptsDoneDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_DONE_DIRECTORY_PATH);
2787
+ const promptsTemplatesDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_TEMPLATES_DIRECTORY_PATH);
2788
+ const promptTemplateFileStatuses = await ensureDefaultCoderPromptTemplateFiles(projectPath);
2789
+ const agentsFileStatus = await ensureCoderMarkdownFile(projectPath, AGENTS_FILE_PATH, getDefaultCoderAgentsFileContent());
2790
+ const agentCodingFileStatus = await ensureCoderMarkdownFile(projectPath, AGENT_CODING_FILE_PATH, getDefaultCoderAgentCodingFileContent({
2791
+ packageJsonScripts: getDefaultCoderPackageJsonScripts(),
2792
+ }));
2793
+ const { envFileStatus, initializedEnvVariableNames } = await ensureCoderEnvFile(projectPath);
2794
+ const gitignoreFileStatus = await ensureCoderGitignoreFile(projectPath);
2795
+ const packageJsonFileStatus = await ensureCoderPackageJsonFile(projectPath);
2796
+ const vscodeSettingsFileStatus = await ensureCoderVscodeSettingsFile(projectPath);
2797
+ return {
2798
+ promptsDirectoryStatus,
2799
+ promptsDoneDirectoryStatus,
2800
+ promptsTemplatesDirectoryStatus,
2801
+ promptTemplateFileStatuses,
2802
+ agentsFileStatus,
2803
+ agentCodingFileStatus,
2804
+ envFileStatus,
2805
+ gitignoreFileStatus,
2806
+ packageJsonFileStatus,
2807
+ vscodeSettingsFileStatus,
2808
+ initializedEnvVariableNames,
2809
+ };
2810
+ }
2811
+ // Note: [🟡] Code for coder init project bootstrapping [initializeCoderProjectConfiguration](src/cli/cli-commands/coder/initializeCoderProjectConfiguration.ts) should never be published outside of `@promptbook/cli`
2812
+
2347
2813
  /**
2348
2814
  * Prints a readable summary of what was initialized for the user.
2815
+ *
2816
+ * @private function of `coder init` command
2349
2817
  */
2350
2818
  function printInitializationSummary(summary) {
2351
2819
  console.info(colors__default["default"].green('Promptbook coder configuration initialized.'));
@@ -2355,6 +2823,8 @@
2355
2823
  for (const templateFileStatus of summary.promptTemplateFileStatuses) {
2356
2824
  printInitializationStatusLine(formatDisplayPath(templateFileStatus.relativeFilePath), templateFileStatus.status);
2357
2825
  }
2826
+ printInitializationStatusLine(AGENTS_FILE_PATH, summary.agentsFileStatus);
2827
+ printInitializationStatusLine(AGENT_CODING_FILE_PATH, summary.agentCodingFileStatus);
2358
2828
  printInitializationStatusLine('.env', summary.envFileStatus);
2359
2829
  printInitializationStatusLine('.gitignore', summary.gitignoreFileStatus);
2360
2830
  printInitializationStatusLine('package.json', summary.packageJsonFileStatus);
@@ -2390,126 +2860,48 @@
2390
2860
  function printInitializationNote(message, colorize) {
2391
2861
  console.info(colorize(`✔ ${message}`));
2392
2862
  }
2393
- /**
2394
- * Normalizes one project-relative path for human-readable CLI output.
2395
- */
2396
- function formatDisplayPath(relativePath) {
2397
- return relativePath.replace(/\\/gu, '/');
2398
- }
2399
- /**
2400
- * Detects whether `.gitignore` already covers the standalone coder temp directory.
2401
- */
2402
- function hasTmpGitignoreRule(gitignoreContent) {
2403
- return /(^|[\r\n])\/?\.tmp(?:[\r\n]|$)/u.test(gitignoreContent);
2404
- }
2405
- /**
2406
- * Reads one text file when it exists, otherwise returns `undefined`.
2407
- */
2408
- async function readTextFileIfExists(path) {
2409
- if (!(await isExistingFile(path))) {
2410
- return undefined;
2411
- }
2412
- return promises.readFile(path, 'utf-8');
2413
- }
2414
- /**
2415
- * Parses one JSON object file while accepting VS Code style comments and trailing commas.
2416
- */
2417
- async function parseJsonObjectFile(relativeFilePath, fileContent) {
2418
- if (fileContent.trim() === '') {
2419
- return {};
2420
- }
2421
- const typescript = await import('typescript');
2422
- const parsedFile = typescript.parseConfigFileTextToJson(relativeFilePath, fileContent);
2423
- if (parsedFile.error) {
2424
- throw new ParseError(_spaceTrim.spaceTrim(`
2425
- Cannot parse \`${relativeFilePath}\` as JSON.
2863
+ // Note: [🟡] Code for coder init summary printing [printInitializationSummary](src/cli/cli-commands/coder/printInitializationSummary.ts) should never be published outside of `@promptbook/cli`
2426
2864
 
2427
- ${typescript.flattenDiagnosticMessageText(parsedFile.error.messageText, '\n')}
2428
- `));
2429
- }
2430
- if (!isPlainObject(parsedFile.config)) {
2431
- throw new ParseError(_spaceTrim.spaceTrim(`
2432
- File \`${relativeFilePath}\` must contain one top-level JSON object.
2433
- `));
2434
- }
2435
- return parsedFile.config;
2436
- }
2437
2865
  /**
2438
- * Reads one JSON object field as a string-to-string record.
2866
+ * Initializes `coder init` command for Promptbook CLI utilities.
2867
+ *
2868
+ * Note: `$` is used to indicate that this function is not a pure function - it registers a command in the CLI.
2869
+ *
2870
+ * @private internal function of `promptbookCli`
2439
2871
  */
2440
- function getStringRecordOrDefault(value, relativeFilePath, fieldPath) {
2441
- if (value === undefined) {
2442
- return {};
2443
- }
2444
- if (!isPlainObject(value)) {
2445
- throw new ParseError(_spaceTrim.spaceTrim(`
2446
- File \`${relativeFilePath}\` contains invalid \`${fieldPath}\`.
2872
+ function $initializeCoderInitCommand(program) {
2873
+ const command = program.command('init');
2874
+ command.alias('initialize');
2875
+ command.description(_spaceTrim.spaceTrim(`
2876
+ Initialize Promptbook coder configuration for current project
2447
2877
 
2448
- Expected \`${fieldPath}\` to be an object with string values.
2449
- `));
2450
- }
2451
- const stringRecord = {};
2452
- for (const [key, itemValue] of Object.entries(value)) {
2453
- if (typeof itemValue !== 'string') {
2454
- throw new ParseError(_spaceTrim.spaceTrim(`
2455
- File \`${relativeFilePath}\` contains invalid \`${fieldPath}.${key}\`.
2878
+ Creates or updates:
2879
+ - prompts/
2880
+ - prompts/done/
2881
+ ${listDefaultCoderProjectPromptTemplateDisplayPaths()}
2882
+ - ${AGENTS_FILE_PATH}
2883
+ - ${AGENT_CODING_FILE_PATH}
2884
+ - .gitignore
2885
+ - package.json
2886
+ - .vscode/settings.json
2456
2887
 
2457
- Expected \`${fieldPath}\` to be an object with string values.
2458
- `));
2459
- }
2460
- stringRecord[key] = itemValue;
2461
- }
2462
- return stringRecord;
2463
- }
2464
- /**
2465
- * Serializes one JSON object using detected or default formatting.
2466
- */
2467
- function serializeJsonObject(value, formatting) {
2468
- return `${JSON.stringify(value, null, formatting.indentation)}${formatting.newline}`;
2469
- }
2470
- /**
2471
- * Detects indentation and newline formatting from an existing JSON file.
2472
- */
2473
- function detectJsonFileFormatting(fileContent) {
2474
- if (!fileContent) {
2475
- return {
2476
- indentation: DEFAULT_JSON_FILE_INDENTATION,
2477
- newline: DEFAULT_JSON_FILE_NEWLINE,
2478
- };
2479
- }
2480
- const indentationMatch = fileContent.match(/^[ \t]+(?=")/mu);
2481
- return {
2482
- indentation: (indentationMatch === null || indentationMatch === void 0 ? void 0 : indentationMatch[0]) || DEFAULT_JSON_FILE_INDENTATION,
2483
- newline: fileContent.includes('\r\n') ? '\r\n' : '\n',
2484
- };
2485
- }
2486
- /**
2487
- * Checks whether one parsed JSON value is a plain object.
2488
- */
2489
- function isPlainObject(value) {
2490
- return typeof value === 'object' && value !== null && !Array.isArray(value);
2491
- }
2492
- /**
2493
- * Checks whether a path exists and is a file.
2494
- */
2495
- async function isExistingFile(path) {
2496
- try {
2497
- return (await promises.stat(path)).isFile();
2498
- }
2499
- catch (_a) {
2500
- return false;
2501
- }
2888
+ Ensures required coding-agent environment variables in .env:
2889
+ - CODING_AGENT_GIT_NAME
2890
+ - CODING_AGENT_GIT_EMAIL
2891
+ - CODING_AGENT_GIT_SIGNING_KEY
2892
+ `));
2893
+ command.action(handleActionErrors(async () => {
2894
+ const summary = await initializeCoderProjectConfiguration(process.cwd());
2895
+ printInitializationSummary(summary);
2896
+ }));
2502
2897
  }
2503
2898
  /**
2504
- * Checks whether a path exists and is a directory.
2899
+ * Lists the project-owned template file paths created by `ptbk coder init`.
2505
2900
  */
2506
- async function isExistingDirectory(path) {
2507
- try {
2508
- return (await promises.stat(path)).isDirectory();
2509
- }
2510
- catch (_a) {
2511
- return false;
2512
- }
2901
+ function listDefaultCoderProjectPromptTemplateDisplayPaths() {
2902
+ return getDefaultCoderProjectPromptTemplateDefinitions()
2903
+ .map(({ relativeFilePath }) => `- ${formatDisplayPath(relativeFilePath)}`)
2904
+ .join('\n');
2513
2905
  }
2514
2906
  // Note: [🟡] Code for CLI command [init](src/cli/cli-commands/coder/init.ts) should never be published outside of `@promptbook/cli`
2515
2907
  // Note: [💞] Ignore a discrepancy between file name and entity name
@@ -40743,91 +41135,6 @@
40743
41135
  findFreshEmojiTag: findFreshEmojiTag
40744
41136
  });
40745
41137
 
40746
- /**
40747
- * Root folders that contain source-like files for scanning.
40748
- */
40749
- const SOURCE_ROOTS = ['src', 'apps', 'scripts', 'examples', 'agents', 'other'];
40750
- /**
40751
- * File extensions treated as source code.
40752
- */
40753
- const SOURCE_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
40754
- /**
40755
- * Glob patterns that should be ignored when scanning for source files.
40756
- */
40757
- const SOURCE_FILE_IGNORE_GLOBS = [
40758
- '**/node_modules/**',
40759
- '**/packages/**',
40760
- '**/.*/**',
40761
- '**/.git/**',
40762
- '**/.idea/**',
40763
- '**/.vscode/**',
40764
- '**/.promptbook/**',
40765
- '**/.next/**',
40766
- '**/.tmp/**',
40767
- '**/tmp/**',
40768
- '**/coverage/**',
40769
- '**/dist/**',
40770
- '**/build/**',
40771
- '**/out/**',
40772
- '**/prompts/**',
40773
- '**/changelog/**',
40774
- ];
40775
- /**
40776
- * Default maximum line count for source files.
40777
- */
40778
- const DEFAULT_MAX_LINE_COUNT = 2000;
40779
- /**
40780
- * Per-extension line count limits.
40781
- */
40782
- const LINE_COUNT_LIMITS_BY_EXTENSION = {
40783
- '.ts': 2000,
40784
- '.tsx': 2000,
40785
- '.js': 2000,
40786
- '.jsx': 2000,
40787
- };
40788
- /**
40789
- * Glob patterns that are exempt from line-count checks.
40790
- */
40791
- const LINE_COUNT_EXEMPT_GLOBS = ['other/cspell-dictionaries/**/*.txt'];
40792
- /**
40793
- * Maximum number of entities before a file is flagged.
40794
- */
40795
- const MAX_ENTITIES_PER_FILE = 20;
40796
- /**
40797
- * File extensions eligible for entity counting.
40798
- */
40799
- const ENTITY_COUNT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
40800
- /**
40801
- * Markers that identify generated files which should be skipped.
40802
- */
40803
- const GENERATED_CODE_MARKERS = [
40804
- 'WARNING: This code has been generated',
40805
- 'This code has been generated so that any manual changes will be overwritten',
40806
- ];
40807
- /**
40808
- * Name of the prompts directory.
40809
- */
40810
- const PROMPTS_DIR_NAME = 'prompts';
40811
- /**
40812
- * Step size used for prompt numbering.
40813
- */
40814
- const PROMPT_NUMBER_STEP = 10;
40815
- /**
40816
- * Prefix used for generated prompt slugs.
40817
- */
40818
- const PROMPT_SLUG_PREFIX = 'refactor';
40819
- /**
40820
- * Label used to mark the target file in generated prompts.
40821
- */
40822
- const PROMPT_TARGET_LABEL = 'Target file';
40823
- /**
40824
- * Maximum length for generated prompt slugs.
40825
- */
40826
- const PROMPT_SLUG_MAX_LENGTH = 80;
40827
- /**
40828
- * Note: [?] Code in this file should never be published in any package
40829
- */
40830
-
40831
41138
  /**
40832
41139
  * Normalizes a repo-relative path to use forward slashes.
40833
41140
  *
@@ -40845,7 +41152,7 @@
40845
41152
  * @private function of findRefactorCandidates
40846
41153
  */
40847
41154
  async function analyzeSourceFileForRefactorCandidate(options) {
40848
- const { filePath, lineCountExemptPaths, rootDir } = options;
41155
+ const { filePath, heuristics, lineCountExemptPaths, rootDir } = options;
40849
41156
  const normalizedAbsolutePath = normalizeAbsolutePath$1(filePath);
40850
41157
  const content = await promises.readFile(filePath, 'utf-8');
40851
41158
  if (isGeneratedFile(content)) {
@@ -40856,15 +41163,21 @@
40856
41163
  const reasons = [];
40857
41164
  if (!lineCountExemptPaths.has(normalizedAbsolutePath)) {
40858
41165
  const lineCount = countLines(content);
40859
- const maxLines = getMaxLinesForExtension(extension);
41166
+ const maxLines = getMaxLinesForExtension(extension, heuristics);
40860
41167
  if (lineCount > maxLines) {
40861
41168
  reasons.push(`lines ${lineCount}/${maxLines}`);
40862
41169
  }
40863
41170
  }
40864
- if (ENTITY_COUNT_EXTENSIONS.includes(extension)) {
40865
- const entityCount = countEntities(content, extension, filePath);
40866
- if (entityCount > MAX_ENTITIES_PER_FILE) {
40867
- reasons.push(`entities ${entityCount}/${MAX_ENTITIES_PER_FILE}`);
41171
+ if (STRUCTURAL_ANALYSIS_EXTENSIONS.includes(extension)) {
41172
+ const structureSummary = summarizeSourceFileStructure(content, extension, filePath);
41173
+ if (structureSummary.entityCount > heuristics.maxEntityCountPerFile) {
41174
+ reasons.push(`entities ${structureSummary.entityCount}/${heuristics.maxEntityCountPerFile}`);
41175
+ }
41176
+ if (structureSummary.functionCount > heuristics.maxFunctionCountPerFile) {
41177
+ reasons.push(`functions ${structureSummary.functionCount}/${heuristics.maxFunctionCountPerFile}`);
41178
+ }
41179
+ if (structureSummary.maxFunctionComplexity > heuristics.maxFunctionComplexity) {
41180
+ reasons.push(buildComplexityReason(structureSummary, heuristics.maxFunctionComplexity));
40868
41181
  }
40869
41182
  }
40870
41183
  if (reasons.length === 0) {
@@ -40889,9 +41202,9 @@
40889
41202
  *
40890
41203
  * @private function of analyzeSourceFileForRefactorCandidate
40891
41204
  */
40892
- function getMaxLinesForExtension(extension) {
41205
+ function getMaxLinesForExtension(extension, heuristics) {
40893
41206
  var _a;
40894
- return (_a = LINE_COUNT_LIMITS_BY_EXTENSION[extension]) !== null && _a !== void 0 ? _a : DEFAULT_MAX_LINE_COUNT;
41207
+ return (_a = heuristics.maxLineCountByExtension[extension]) !== null && _a !== void 0 ? _a : heuristics.maxDefaultLineCount;
40895
41208
  }
40896
41209
  /**
40897
41210
  * Counts lines while ignoring a trailing newline.
@@ -40906,14 +41219,17 @@
40906
41219
  return lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
40907
41220
  }
40908
41221
  /**
40909
- * Counts top-level entities in a source file.
41222
+ * Summarizes the structural metrics used to score one source file.
40910
41223
  *
40911
41224
  * @private function of analyzeSourceFileForRefactorCandidate
40912
41225
  */
40913
- function countEntities(content, extension, filePath) {
41226
+ function summarizeSourceFileStructure(content, extension, filePath) {
40914
41227
  const scriptKind = getScriptKindForExtension(extension);
40915
- const sourceFile = ts__namespace.createSourceFile(filePath, content, ts__namespace.ScriptTarget.Latest, false, scriptKind);
40916
- return countEntitiesInSourceFile(sourceFile);
41228
+ const sourceFile = ts__namespace.createSourceFile(filePath, content, ts__namespace.ScriptTarget.Latest, true, scriptKind);
41229
+ return {
41230
+ entityCount: countEntitiesInSourceFile(sourceFile),
41231
+ ...summarizeFunctionsInSourceFile(sourceFile),
41232
+ };
40917
41233
  }
40918
41234
  /**
40919
41235
  * Counts top-level entities in a parsed TypeScript source file.
@@ -40947,6 +41263,169 @@
40947
41263
  }
40948
41264
  return count;
40949
41265
  }
41266
+ /**
41267
+ * Summarizes named functions and methods in a parsed source file.
41268
+ *
41269
+ * @private function of analyzeSourceFileForRefactorCandidate
41270
+ */
41271
+ function summarizeFunctionsInSourceFile(sourceFile) {
41272
+ let functionCount = 0;
41273
+ let maxFunctionComplexity = 0;
41274
+ let mostComplexFunctionName = null;
41275
+ const visitNode = (node) => {
41276
+ if (isCountedFunctionLikeDeclaration(node)) {
41277
+ functionCount += 1;
41278
+ const functionComplexity = calculateFunctionComplexity(node);
41279
+ if (functionComplexity > maxFunctionComplexity) {
41280
+ maxFunctionComplexity = functionComplexity;
41281
+ mostComplexFunctionName = getFunctionDisplayName(node);
41282
+ }
41283
+ }
41284
+ ts__namespace.forEachChild(node, visitNode);
41285
+ };
41286
+ visitNode(sourceFile);
41287
+ return {
41288
+ functionCount,
41289
+ maxFunctionComplexity,
41290
+ mostComplexFunctionName,
41291
+ };
41292
+ }
41293
+ /**
41294
+ * Determines whether a node counts as a named function or method for density checks.
41295
+ *
41296
+ * @private function of analyzeSourceFileForRefactorCandidate
41297
+ */
41298
+ function isCountedFunctionLikeDeclaration(node) {
41299
+ if (ts__namespace.isFunctionDeclaration(node) ||
41300
+ ts__namespace.isMethodDeclaration(node) ||
41301
+ ts__namespace.isConstructorDeclaration(node) ||
41302
+ ts__namespace.isGetAccessorDeclaration(node) ||
41303
+ ts__namespace.isSetAccessorDeclaration(node)) {
41304
+ return true;
41305
+ }
41306
+ if (ts__namespace.isArrowFunction(node) || ts__namespace.isFunctionExpression(node)) {
41307
+ return isNamedFunctionExpression(node);
41308
+ }
41309
+ return false;
41310
+ }
41311
+ /**
41312
+ * Determines whether a function expression is attached to a named variable or property.
41313
+ *
41314
+ * @private function of analyzeSourceFileForRefactorCandidate
41315
+ */
41316
+ function isNamedFunctionExpression(node) {
41317
+ const parent = node.parent;
41318
+ return (ts__namespace.isVariableDeclaration(parent) || ts__namespace.isPropertyDeclaration(parent) || ts__namespace.isPropertyAssignment(parent));
41319
+ }
41320
+ /**
41321
+ * Calculates a lightweight cyclomatic-complexity score for one function.
41322
+ *
41323
+ * @private function of analyzeSourceFileForRefactorCandidate
41324
+ */
41325
+ function calculateFunctionComplexity(functionNode) {
41326
+ if (!functionNode.body) {
41327
+ return 1;
41328
+ }
41329
+ let complexity = 1;
41330
+ const visitNode = (node) => {
41331
+ if (node !== functionNode.body && isCountedFunctionLikeDeclaration(node)) {
41332
+ return;
41333
+ }
41334
+ if (isComplexityDecisionNode(node)) {
41335
+ complexity += 1;
41336
+ }
41337
+ ts__namespace.forEachChild(node, visitNode);
41338
+ };
41339
+ visitNode(functionNode.body);
41340
+ return complexity;
41341
+ }
41342
+ /**
41343
+ * Determines whether a node should increase the complexity score.
41344
+ *
41345
+ * @private function of analyzeSourceFileForRefactorCandidate
41346
+ */
41347
+ function isComplexityDecisionNode(node) {
41348
+ if (ts__namespace.isIfStatement(node) ||
41349
+ ts__namespace.isConditionalExpression(node) ||
41350
+ ts__namespace.isCatchClause(node) ||
41351
+ ts__namespace.isForStatement(node) ||
41352
+ ts__namespace.isForInStatement(node) ||
41353
+ ts__namespace.isForOfStatement(node) ||
41354
+ ts__namespace.isWhileStatement(node) ||
41355
+ ts__namespace.isDoStatement(node) ||
41356
+ ts__namespace.isCaseClause(node)) {
41357
+ return true;
41358
+ }
41359
+ if (ts__namespace.isBinaryExpression(node)) {
41360
+ const operatorKind = node.operatorToken.kind;
41361
+ return (operatorKind === ts__namespace.SyntaxKind.AmpersandAmpersandToken ||
41362
+ operatorKind === ts__namespace.SyntaxKind.BarBarToken ||
41363
+ operatorKind === ts__namespace.SyntaxKind.QuestionQuestionToken);
41364
+ }
41365
+ return false;
41366
+ }
41367
+ /**
41368
+ * Resolves a readable display name for a counted function-like declaration.
41369
+ *
41370
+ * @private function of analyzeSourceFileForRefactorCandidate
41371
+ */
41372
+ function getFunctionDisplayName(functionNode) {
41373
+ if (ts__namespace.isConstructorDeclaration(functionNode)) {
41374
+ return 'constructor';
41375
+ }
41376
+ if (ts__namespace.isFunctionDeclaration(functionNode) ||
41377
+ ts__namespace.isMethodDeclaration(functionNode) ||
41378
+ ts__namespace.isGetAccessorDeclaration(functionNode) ||
41379
+ ts__namespace.isSetAccessorDeclaration(functionNode)) {
41380
+ if (!functionNode.name) {
41381
+ return null;
41382
+ }
41383
+ return getPropertyNameText(functionNode.name);
41384
+ }
41385
+ if (ts__namespace.isArrowFunction(functionNode) || ts__namespace.isFunctionExpression(functionNode)) {
41386
+ if (functionNode.name) {
41387
+ return functionNode.name.text;
41388
+ }
41389
+ const parent = functionNode.parent;
41390
+ if (ts__namespace.isVariableDeclaration(parent)) {
41391
+ return getBindingNameText(parent.name);
41392
+ }
41393
+ if (ts__namespace.isPropertyDeclaration(parent) || ts__namespace.isPropertyAssignment(parent)) {
41394
+ return getPropertyNameText(parent.name);
41395
+ }
41396
+ }
41397
+ return null;
41398
+ }
41399
+ /**
41400
+ * Resolves text for a binding name when it is a simple identifier.
41401
+ *
41402
+ * @private function of analyzeSourceFileForRefactorCandidate
41403
+ */
41404
+ function getBindingNameText(name) {
41405
+ return ts__namespace.isIdentifier(name) ? name.text : null;
41406
+ }
41407
+ /**
41408
+ * Resolves text for a property name while preserving computed names when necessary.
41409
+ *
41410
+ * @private function of analyzeSourceFileForRefactorCandidate
41411
+ */
41412
+ function getPropertyNameText(name) {
41413
+ if (ts__namespace.isIdentifier(name) || ts__namespace.isPrivateIdentifier(name) || ts__namespace.isStringLiteral(name) || ts__namespace.isNumericLiteral(name)) {
41414
+ return name.text;
41415
+ }
41416
+ return name.getText();
41417
+ }
41418
+ /**
41419
+ * Formats the reason emitted when a function in the file exceeds the complexity threshold.
41420
+ *
41421
+ * @private function of analyzeSourceFileForRefactorCandidate
41422
+ */
41423
+ function buildComplexityReason(structureSummary, maxAllowedFunctionComplexity) {
41424
+ const functionSuffix = structureSummary.mostComplexFunctionName
41425
+ ? ` in \`${structureSummary.mostComplexFunctionName}\``
41426
+ : '';
41427
+ return `complexity ${structureSummary.maxFunctionComplexity}/${maxAllowedFunctionComplexity}${functionSuffix}`;
41428
+ }
40950
41429
  /**
40951
41430
  * Resolves the script kind for a source file extension.
40952
41431
  *
@@ -40980,13 +41459,15 @@
40980
41459
  *
40981
41460
  * @private function of findRefactorCandidates
40982
41461
  */
40983
- async function findRefactorCandidatesInProject(rootDir) {
41462
+ async function findRefactorCandidatesInProject(options) {
41463
+ const { heuristics, rootDir } = options;
40984
41464
  const lineCountExemptPaths = await buildExemptPathSet(rootDir, LINE_COUNT_EXEMPT_GLOBS);
40985
41465
  const sourceFiles = await listSourceFiles(rootDir);
40986
41466
  const candidates = [];
40987
41467
  for (const filePath of sourceFiles) {
40988
41468
  const candidate = await analyzeSourceFileForRefactorCandidate({
40989
41469
  filePath,
41470
+ heuristics,
40990
41471
  lineCountExemptPaths,
40991
41472
  rootDir,
40992
41473
  });
@@ -41259,11 +41740,18 @@
41259
41740
  if (counts.entityCount !== null && counts.maxEntities !== null) {
41260
41741
  guidance.push(`- The file defines too many responsibilities (${counts.entityCount} in single file)`, ` - Keep in mind the Single Responsibility Principle (SRP)`, ` - Consider breaking it down into smaller, focused modules or components.`);
41261
41742
  }
41743
+ if (counts.functionCount !== null && counts.maxFunctions !== null) {
41744
+ guidance.push(`- The file contains too many functions (${counts.functionCount}/${counts.maxFunctions})`, ` - Keep related responsibilities grouped behind small facades or focused modules.`, ` - Consider extracting private helpers or splitting independent concerns into dedicated files.`);
41745
+ }
41746
+ if (counts.functionComplexity !== null && counts.maxFunctionComplexity !== null) {
41747
+ const functionSuffix = counts.mostComplexFunctionName ? ` in \`${counts.mostComplexFunctionName}\`` : '';
41748
+ guidance.push(`- The file contains overly complex logic${functionSuffix} (${counts.functionComplexity}/${counts.maxFunctionComplexity})`, ` - Break branching logic into smaller, focused helper functions.`, ` - Keep each function responsible for one clear step or decision.`);
41749
+ }
41262
41750
  guidance.push('- Purpose of this refactoring is to improve code maintainability and readability.', '- Look at the internal structure, the usage and also surrounding code to understand how to best refactor this file.', '- Consider breaking down large functions into smaller, more manageable ones, removing any redundant code, and ensuring that the file adheres to the project coding standards.', '- After the refactoring, ensure that (1) `npm run test-name-discrepancies` and (2) `npm run test-package-generation` are passing successfully.', ' 1. All the things you have moved to new files should correspond the thing in the file with the file name, for example `MyComponent.tsx` should export `MyComponent`.', ' 2. All the things you have moved to new files but are private things to the outside world should have `@private function of TheMainThing` JSDoc comment.', '- Keep in mind DRY *(Do not repeat yourself)* and SOLID principles while refactoring.', '- **Do not change the external behavior** of the code. Focus solely on improving the internal structure and organization of the code.', '- Before you start refactoring, make sure to read the code carefully and understand its current structure and functionality. Do a analysis of the current functionality before you start.');
41263
41751
  return guidance;
41264
41752
  }
41265
41753
  /**
41266
- * Extracts line and entity counts from refactor reasons.
41754
+ * Extracts structural counts from refactor reasons.
41267
41755
  *
41268
41756
  * @private function of buildPromptContent
41269
41757
  */
@@ -41272,6 +41760,11 @@
41272
41760
  let maxLines = null;
41273
41761
  let entityCount = null;
41274
41762
  let maxEntities = null;
41763
+ let functionCount = null;
41764
+ let maxFunctions = null;
41765
+ let functionComplexity = null;
41766
+ let maxFunctionComplexity = null;
41767
+ let mostComplexFunctionName = null;
41275
41768
  for (const reason of reasons) {
41276
41769
  const lineMatch = reason.match(/lines\s+(?<count>\d+)\/(?<max>\d+)/i);
41277
41770
  if (lineMatch === null || lineMatch === void 0 ? void 0 : lineMatch.groups) {
@@ -41283,6 +41776,19 @@
41283
41776
  if (entityMatch === null || entityMatch === void 0 ? void 0 : entityMatch.groups) {
41284
41777
  entityCount = Number(entityMatch.groups.count);
41285
41778
  maxEntities = Number(entityMatch.groups.max);
41779
+ continue;
41780
+ }
41781
+ const functionMatch = reason.match(/functions\s+(?<count>\d+)\/(?<max>\d+)/i);
41782
+ if (functionMatch === null || functionMatch === void 0 ? void 0 : functionMatch.groups) {
41783
+ functionCount = Number(functionMatch.groups.count);
41784
+ maxFunctions = Number(functionMatch.groups.max);
41785
+ continue;
41786
+ }
41787
+ const complexityMatch = reason.match(/complexity\s+(?<count>\d+)\/(?<max>\d+)(?:\s+in\s+`(?<functionName>[^`]+)`)?/i);
41788
+ if (complexityMatch === null || complexityMatch === void 0 ? void 0 : complexityMatch.groups) {
41789
+ functionComplexity = Number(complexityMatch.groups.count);
41790
+ maxFunctionComplexity = Number(complexityMatch.groups.max);
41791
+ mostComplexFunctionName = complexityMatch.groups.functionName || null;
41286
41792
  }
41287
41793
  }
41288
41794
  return {
@@ -41290,6 +41796,11 @@
41290
41796
  maxLines,
41291
41797
  entityCount,
41292
41798
  maxEntities,
41799
+ functionCount,
41800
+ maxFunctions,
41801
+ functionComplexity,
41802
+ maxFunctionComplexity,
41803
+ mostComplexFunctionName,
41293
41804
  };
41294
41805
  }
41295
41806
  /**
@@ -41298,14 +41809,23 @@
41298
41809
  * @private function of buildPromptContent
41299
41810
  */
41300
41811
  function buildDensityNote(counts) {
41301
- if (counts.lineCount !== null && counts.entityCount !== null) {
41302
- return 'The file mixes multiple concerns, making it harder to follow.';
41812
+ const activeSignalsCount = [
41813
+ counts.lineCount !== null,
41814
+ counts.entityCount !== null,
41815
+ counts.functionCount !== null,
41816
+ counts.functionComplexity !== null,
41817
+ ].filter(Boolean).length;
41818
+ if (activeSignalsCount > 1) {
41819
+ return 'The file mixes multiple concerns and dense logic, making it harder to follow.';
41303
41820
  }
41304
41821
  if (counts.lineCount !== null) {
41305
41822
  return 'The file is large enough that it is hard to follow.';
41306
41823
  }
41307
- if (counts.entityCount !== null) {
41308
- return 'The file is dense enough that it is hard to follow.';
41824
+ if (counts.entityCount !== null || counts.functionCount !== null) {
41825
+ return 'The file packs too many responsibilities into one place.';
41826
+ }
41827
+ if (counts.functionComplexity !== null) {
41828
+ return 'The file contains logic that is too complex to follow comfortably.';
41309
41829
  }
41310
41830
  return null;
41311
41831
  }
@@ -41399,13 +41919,19 @@
41399
41919
  *
41400
41920
  * @public exported from `@promptbook/cli`
41401
41921
  */
41402
- async function findRefactorCandidates() {
41922
+ async function findRefactorCandidates(options = {}) {
41923
+ const { level = DEFAULT_REFACTOR_CANDIDATE_LEVEL } = options;
41924
+ const heuristics = getRefactorCandidateLevelConfiguration(level);
41403
41925
  initializeFindRefactorCandidatesRun();
41404
- console.info(colors__default["default"].cyan('?? Find refactor candidates'));
41926
+ console.info(colors__default["default"].cyan('⚡🏭 Find refactor candidates'));
41927
+ console.info(colors__default["default"].gray(`Using \`${level}\` scan level.`));
41405
41928
  const rootDir = process.cwd();
41406
41929
  const promptsDir = path.join(rootDir, PROMPTS_DIR_NAME);
41407
41930
  const existingTargets = await loadExistingPromptTargets(promptsDir);
41408
- const candidates = await findRefactorCandidatesInProject(rootDir);
41931
+ const candidates = await findRefactorCandidatesInProject({
41932
+ heuristics,
41933
+ rootDir,
41934
+ });
41409
41935
  if (candidates.length === 0) {
41410
41936
  console.info(colors__default["default"].green('No refactor candidates found.'));
41411
41937
  return;