@promptbook/cli 0.112.0-36 → 0.112.0-39

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 (136) hide show
  1. package/README.md +10 -6
  2. package/esm/index.es.js +1079 -437
  3. package/esm/index.es.js.map +1 -1
  4. package/esm/scripts/find-refactor-candidates/RefactorCandidateLevel.d.ts +45 -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 +23 -1
  9. package/esm/scripts/find-refactor-candidates/resolveRefactorCandidateProject.d.ts +23 -0
  10. package/esm/src/_packages/components.index.d.ts +2 -0
  11. package/esm/src/_packages/types.index.d.ts +48 -46
  12. package/esm/src/book-components/Chat/Chat/ChatActionsBar.d.ts +7 -0
  13. package/esm/src/book-components/Chat/Chat/ChatActionsBar.test.d.ts +2 -0
  14. package/esm/src/book-components/Chat/Chat/ChatInputArea.d.ts +4 -0
  15. package/esm/src/book-components/Chat/Chat/ChatMessageItem.d.ts +8 -0
  16. package/esm/src/book-components/Chat/Chat/ChatMessageList.d.ts +2 -0
  17. package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +50 -1
  18. package/esm/src/book-components/Chat/Chat/ChatReplyPreview.d.ts +19 -0
  19. package/esm/src/book-components/Chat/Chat/createProgressCardChecklistMarkdown.d.ts +2 -2
  20. package/esm/src/book-components/Chat/MockedChat/MockedChat.d.ts +1 -1
  21. package/esm/src/book-components/Chat/types/ChatMessage.d.ts +35 -0
  22. package/esm/src/book-components/Chat/utils/resolveChatMessageReplyPreviewText.d.ts +25 -0
  23. package/esm/src/book-components/Chat/utils/resolveChatMessageReplySenderLabel.d.ts +12 -0
  24. package/esm/src/cli/cli-commands/coder/agentCodingFile.d.ts +14 -0
  25. package/esm/src/cli/cli-commands/coder/agentsFile.d.ts +12 -0
  26. package/esm/src/cli/cli-commands/coder/appendBlock.d.ts +6 -0
  27. package/esm/src/cli/cli-commands/coder/boilerplateTemplates.d.ts +10 -0
  28. package/esm/src/cli/cli-commands/coder/ensureCoderEnvFile.d.ts +15 -0
  29. package/esm/src/cli/cli-commands/coder/ensureCoderGitignoreFile.d.ts +7 -0
  30. package/esm/src/cli/cli-commands/coder/ensureCoderMarkdownFile.d.ts +7 -0
  31. package/esm/src/cli/cli-commands/coder/ensureCoderPackageJsonFile.d.ts +7 -0
  32. package/esm/src/cli/cli-commands/coder/ensureCoderVscodeSettingsFile.d.ts +7 -0
  33. package/esm/src/cli/cli-commands/coder/ensureDirectory.d.ts +7 -0
  34. package/esm/src/cli/cli-commands/coder/find-refactor-candidates.d.ts +1 -1
  35. package/esm/src/cli/cli-commands/coder/find-refactor-candidates.test.d.ts +1 -0
  36. package/esm/src/cli/cli-commands/coder/formatDisplayPath.d.ts +6 -0
  37. package/esm/src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.d.ts +6 -0
  38. package/esm/src/cli/cli-commands/coder/getDefaultCoderVscodeSettings.d.ts +6 -0
  39. package/esm/src/cli/cli-commands/coder/init.d.ts +3 -38
  40. package/esm/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +25 -0
  41. package/esm/src/cli/cli-commands/coder/mergeStringRecordJsonFile.d.ts +18 -0
  42. package/esm/src/cli/cli-commands/coder/printInitializationSummary.d.ts +7 -0
  43. package/esm/src/cli/cli-commands/coder/readTextFileIfExists.d.ts +6 -0
  44. package/esm/src/types/string_agent_url.d.ts +7 -0
  45. package/esm/src/types/string_agent_url_private.d.ts +9 -0
  46. package/esm/src/types/string_base64.d.ts +13 -0
  47. package/esm/src/types/string_base64_private.d.ts +2 -2
  48. package/esm/src/types/string_base_url.d.ts +7 -0
  49. package/esm/src/types/string_base_url_private.d.ts +9 -0
  50. package/esm/src/types/string_email.d.ts +13 -0
  51. package/esm/src/types/string_email_private.d.ts +2 -2
  52. package/esm/src/types/string_host.d.ts +42 -0
  53. package/esm/src/types/string_host_private.d.ts +7 -7
  54. package/esm/src/types/string_href.d.ts +19 -0
  55. package/esm/src/types/string_href_private.d.ts +24 -0
  56. package/esm/src/types/string_mime_type.d.ts +15 -0
  57. package/esm/src/types/string_mime_type_private.d.ts +2 -2
  58. package/esm/src/types/string_pipeline_root_url.d.ts +7 -0
  59. package/esm/src/types/string_pipeline_root_url_private.d.ts +9 -0
  60. package/esm/src/types/string_pipeline_url.d.ts +13 -0
  61. package/esm/src/types/string_pipeline_url_private.d.ts +17 -0
  62. package/esm/src/types/string_promptbook_server_url.d.ts +7 -0
  63. package/esm/src/types/string_promptbook_server_url_private.d.ts +9 -0
  64. package/esm/src/types/string_url.d.ts +14 -141
  65. package/esm/src/types/string_url_image.d.ts +7 -0
  66. package/esm/src/types/string_url_image_private.d.ts +9 -0
  67. package/esm/src/types/string_url_private.d.ts +0 -80
  68. package/esm/src/version.d.ts +1 -1
  69. package/package.json +2 -1
  70. package/umd/index.umd.js +1082 -440
  71. package/umd/index.umd.js.map +1 -1
  72. package/umd/scripts/find-refactor-candidates/RefactorCandidateLevel.d.ts +45 -0
  73. package/umd/scripts/find-refactor-candidates/analyzeSourceFileForRefactorCandidate.d.ts +5 -0
  74. package/umd/scripts/find-refactor-candidates/find-refactor-candidates.constants.d.ts +2 -14
  75. package/umd/scripts/find-refactor-candidates/find-refactor-candidates.d.ts +13 -1
  76. package/umd/scripts/find-refactor-candidates/findRefactorCandidatesInProject.d.ts +23 -1
  77. package/umd/scripts/find-refactor-candidates/resolveRefactorCandidateProject.d.ts +23 -0
  78. package/umd/src/_packages/components.index.d.ts +2 -0
  79. package/umd/src/_packages/types.index.d.ts +48 -46
  80. package/umd/src/book-components/Chat/Chat/ChatActionsBar.d.ts +7 -0
  81. package/umd/src/book-components/Chat/Chat/ChatActionsBar.test.d.ts +2 -0
  82. package/umd/src/book-components/Chat/Chat/ChatInputArea.d.ts +4 -0
  83. package/umd/src/book-components/Chat/Chat/ChatMessageItem.d.ts +8 -0
  84. package/umd/src/book-components/Chat/Chat/ChatMessageList.d.ts +2 -0
  85. package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +50 -1
  86. package/umd/src/book-components/Chat/Chat/ChatReplyPreview.d.ts +19 -0
  87. package/umd/src/book-components/Chat/Chat/createProgressCardChecklistMarkdown.d.ts +2 -2
  88. package/umd/src/book-components/Chat/MockedChat/MockedChat.d.ts +1 -1
  89. package/umd/src/book-components/Chat/types/ChatMessage.d.ts +35 -0
  90. package/umd/src/book-components/Chat/utils/resolveChatMessageReplyPreviewText.d.ts +25 -0
  91. package/umd/src/book-components/Chat/utils/resolveChatMessageReplySenderLabel.d.ts +12 -0
  92. package/umd/src/cli/cli-commands/coder/agentCodingFile.d.ts +14 -0
  93. package/umd/src/cli/cli-commands/coder/agentsFile.d.ts +12 -0
  94. package/umd/src/cli/cli-commands/coder/appendBlock.d.ts +6 -0
  95. package/umd/src/cli/cli-commands/coder/boilerplateTemplates.d.ts +10 -0
  96. package/umd/src/cli/cli-commands/coder/ensureCoderEnvFile.d.ts +15 -0
  97. package/umd/src/cli/cli-commands/coder/ensureCoderGitignoreFile.d.ts +7 -0
  98. package/umd/src/cli/cli-commands/coder/ensureCoderMarkdownFile.d.ts +7 -0
  99. package/umd/src/cli/cli-commands/coder/ensureCoderPackageJsonFile.d.ts +7 -0
  100. package/umd/src/cli/cli-commands/coder/ensureCoderVscodeSettingsFile.d.ts +7 -0
  101. package/umd/src/cli/cli-commands/coder/ensureDirectory.d.ts +7 -0
  102. package/umd/src/cli/cli-commands/coder/find-refactor-candidates.d.ts +1 -1
  103. package/umd/src/cli/cli-commands/coder/find-refactor-candidates.test.d.ts +1 -0
  104. package/umd/src/cli/cli-commands/coder/formatDisplayPath.d.ts +6 -0
  105. package/umd/src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.d.ts +6 -0
  106. package/umd/src/cli/cli-commands/coder/getDefaultCoderVscodeSettings.d.ts +6 -0
  107. package/umd/src/cli/cli-commands/coder/init.d.ts +3 -38
  108. package/umd/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +25 -0
  109. package/umd/src/cli/cli-commands/coder/mergeStringRecordJsonFile.d.ts +18 -0
  110. package/umd/src/cli/cli-commands/coder/printInitializationSummary.d.ts +7 -0
  111. package/umd/src/cli/cli-commands/coder/readTextFileIfExists.d.ts +6 -0
  112. package/umd/src/types/string_agent_url.d.ts +7 -0
  113. package/umd/src/types/string_agent_url_private.d.ts +9 -0
  114. package/umd/src/types/string_base64.d.ts +13 -0
  115. package/umd/src/types/string_base64_private.d.ts +2 -2
  116. package/umd/src/types/string_base_url.d.ts +7 -0
  117. package/umd/src/types/string_base_url_private.d.ts +9 -0
  118. package/umd/src/types/string_email.d.ts +13 -0
  119. package/umd/src/types/string_email_private.d.ts +2 -2
  120. package/umd/src/types/string_host.d.ts +42 -0
  121. package/umd/src/types/string_host_private.d.ts +7 -7
  122. package/umd/src/types/string_href.d.ts +19 -0
  123. package/umd/src/types/string_href_private.d.ts +24 -0
  124. package/umd/src/types/string_mime_type.d.ts +15 -0
  125. package/umd/src/types/string_mime_type_private.d.ts +2 -2
  126. package/umd/src/types/string_pipeline_root_url.d.ts +7 -0
  127. package/umd/src/types/string_pipeline_root_url_private.d.ts +9 -0
  128. package/umd/src/types/string_pipeline_url.d.ts +13 -0
  129. package/umd/src/types/string_pipeline_url_private.d.ts +17 -0
  130. package/umd/src/types/string_promptbook_server_url.d.ts +7 -0
  131. package/umd/src/types/string_promptbook_server_url_private.d.ts +9 -0
  132. package/umd/src/types/string_url.d.ts +14 -141
  133. package/umd/src/types/string_url_image.d.ts +7 -0
  134. package/umd/src/types/string_url_image_private.d.ts +9 -0
  135. package/umd/src/types/string_url_private.d.ts +0 -80
  136. package/umd/src/version.d.ts +1 -1
package/esm/index.es.js CHANGED
@@ -5,7 +5,7 @@ import * as fs from 'fs';
5
5
  import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
6
6
  import * as path from 'path';
7
7
  import { join, basename, dirname, isAbsolute, relative, extname, resolve } from 'path';
8
- import { writeFile, readFile, stat, mkdir, access, constants, readdir, watch, unlink, rm, rename, rmdir } from 'fs/promises';
8
+ import { readFile, writeFile, stat, mkdir, access, constants, readdir, watch, unlink, rm, rename, rmdir } from 'fs/promises';
9
9
  import { forTime, forEver } from 'waitasecond';
10
10
  import prompts from 'prompts';
11
11
  import * as dotenv from 'dotenv';
@@ -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-36';
61
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-39';
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
@@ -1661,6 +1662,181 @@ function $initializeCoderFindFreshEmojiTagCommand(program) {
1661
1662
  // 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`
1662
1663
  // Note: [💞] Ignore a discrepancy between file name and entity name
1663
1664
 
1665
+ /**
1666
+ * Root folders that contain source-like files for scanning.
1667
+ */
1668
+ const SOURCE_ROOTS = ['src', 'apps', 'scripts', 'examples', 'agents', 'other'];
1669
+ /**
1670
+ * File extensions treated as source code.
1671
+ */
1672
+ const SOURCE_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
1673
+ /**
1674
+ * Glob patterns that should be ignored when scanning for source files.
1675
+ */
1676
+ const SOURCE_FILE_IGNORE_GLOBS = [
1677
+ '**/node_modules/**',
1678
+ '**/packages/**',
1679
+ '**/.*/**',
1680
+ '**/.git/**',
1681
+ '**/.idea/**',
1682
+ '**/.vscode/**',
1683
+ '**/.promptbook/**',
1684
+ '**/.next/**',
1685
+ '**/.tmp/**',
1686
+ '**/tmp/**',
1687
+ '**/coverage/**',
1688
+ '**/dist/**',
1689
+ '**/build/**',
1690
+ '**/out/**',
1691
+ '**/prompts/**',
1692
+ '**/changelog/**',
1693
+ ];
1694
+ /**
1695
+ * Glob patterns that are exempt from line-count checks.
1696
+ */
1697
+ const LINE_COUNT_EXEMPT_GLOBS = ['other/cspell-dictionaries/**/*.txt'];
1698
+ /**
1699
+ * File extensions eligible for structural AST analysis.
1700
+ */
1701
+ const STRUCTURAL_ANALYSIS_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
1702
+ /**
1703
+ * Markers that identify generated files which should be skipped.
1704
+ */
1705
+ const GENERATED_CODE_MARKERS = [
1706
+ 'WARNING: This code has been generated',
1707
+ 'This code has been generated so that any manual changes will be overwritten',
1708
+ ];
1709
+ /**
1710
+ * Name of the prompts directory.
1711
+ */
1712
+ const PROMPTS_DIR_NAME = 'prompts';
1713
+ /**
1714
+ * Step size used for prompt numbering.
1715
+ */
1716
+ const PROMPT_NUMBER_STEP = 10;
1717
+ /**
1718
+ * Prefix used for generated prompt slugs.
1719
+ */
1720
+ const PROMPT_SLUG_PREFIX = 'refactor';
1721
+ /**
1722
+ * Label used to mark the target file in generated prompts.
1723
+ */
1724
+ const PROMPT_TARGET_LABEL = 'Target file';
1725
+ /**
1726
+ * Maximum length for generated prompt slugs.
1727
+ */
1728
+ const PROMPT_SLUG_MAX_LENGTH = 80;
1729
+ /**
1730
+ * Note: [?] Code in this file should never be published in any package
1731
+ */
1732
+
1733
+ /**
1734
+ * Supported aggressiveness levels for refactor-candidate scanning.
1735
+ */
1736
+ const REFACTOR_CANDIDATE_LEVEL_VALUES = ['xlow', 'low', 'medium', 'high', 'xhigh', 'extreme'];
1737
+ /**
1738
+ * Default aggressiveness level for refactor-candidate scanning.
1739
+ */
1740
+ const DEFAULT_REFACTOR_CANDIDATE_LEVEL = 'medium';
1741
+ /**
1742
+ * Threshold table for each supported refactor-candidate scanning level.
1743
+ */
1744
+ const REFACTOR_CANDIDATE_LEVEL_DETAILS_BY_LEVEL = {
1745
+ xlow: createRefactorCandidateLevelDetails({
1746
+ description: 'Very benevolent scan that only flags the most obvious refactor targets.',
1747
+ maxLineCount: 4200,
1748
+ maxEntityCountPerFile: 36,
1749
+ maxFunctionCountPerFile: 24,
1750
+ maxFunctionComplexity: 24,
1751
+ }),
1752
+ low: createRefactorCandidateLevelDetails({
1753
+ description: 'Conservative scan for only the most obvious refactor targets.',
1754
+ maxLineCount: 2800,
1755
+ maxEntityCountPerFile: 28,
1756
+ maxFunctionCountPerFile: 18,
1757
+ maxFunctionComplexity: 20,
1758
+ }),
1759
+ medium: createRefactorCandidateLevelDetails({
1760
+ description: 'Default scan using the current standard thresholds.',
1761
+ maxLineCount: 2000,
1762
+ maxEntityCountPerFile: 20,
1763
+ maxFunctionCountPerFile: 14,
1764
+ maxFunctionComplexity: 16,
1765
+ }),
1766
+ high: createRefactorCandidateLevelDetails({
1767
+ description: 'Strict scan that finds more crowded or complex files.',
1768
+ maxLineCount: 1400,
1769
+ maxEntityCountPerFile: 15,
1770
+ maxFunctionCountPerFile: 10,
1771
+ maxFunctionComplexity: 12,
1772
+ }),
1773
+ xhigh: createRefactorCandidateLevelDetails({
1774
+ description: 'Very strict scan for denser and more complex candidates.',
1775
+ maxLineCount: 900,
1776
+ maxEntityCountPerFile: 10,
1777
+ maxFunctionCountPerFile: 7,
1778
+ maxFunctionComplexity: 8,
1779
+ }),
1780
+ extreme: createRefactorCandidateLevelDetails({
1781
+ description: 'Most aggressive scan that also surfaces weaker refactor opportunities.',
1782
+ maxLineCount: 600,
1783
+ maxEntityCountPerFile: 6,
1784
+ maxFunctionCountPerFile: 4,
1785
+ maxFunctionComplexity: 6,
1786
+ }),
1787
+ };
1788
+ /**
1789
+ * Resolves the thresholds for a selected refactor-candidate scanning level.
1790
+ */
1791
+ function getRefactorCandidateLevelConfiguration(level = DEFAULT_REFACTOR_CANDIDATE_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
+ };
1814
+ }
1815
+ /**
1816
+ * Builds one normalized refactor-candidate level configuration entry.
1817
+ */
1818
+ function createRefactorCandidateLevelConfiguration(options) {
1819
+ const { maxLineCount, maxEntityCountPerFile, maxFunctionCountPerFile, maxFunctionComplexity } = options;
1820
+ return {
1821
+ maxDefaultLineCount: maxLineCount,
1822
+ maxLineCountByExtension: createLineCountLimits(maxLineCount),
1823
+ maxEntityCountPerFile,
1824
+ maxFunctionCountPerFile,
1825
+ maxFunctionComplexity,
1826
+ };
1827
+ }
1828
+ /**
1829
+ * Creates a per-extension line-count table using one shared threshold.
1830
+ */
1831
+ function createLineCountLimits(maxLineCount) {
1832
+ const maxLineCountByExtension = {};
1833
+ for (const extension of SOURCE_FILE_EXTENSIONS) {
1834
+ maxLineCountByExtension[extension] = maxLineCount;
1835
+ }
1836
+ return maxLineCountByExtension;
1837
+ }
1838
+ // Note: [🟡] Code for repository script [RefactorCandidateLevel](scripts/find-refactor-candidates/RefactorCandidateLevel.ts) should never be published outside of `@promptbook/cli`
1839
+
1664
1840
  /**
1665
1841
  * Initializes `coder find-refactor-candidates` command for Promptbook CLI utilities
1666
1842
  *
@@ -1670,20 +1846,23 @@ function $initializeCoderFindFreshEmojiTagCommand(program) {
1670
1846
  */
1671
1847
  function $initializeCoderFindRefactorCandidatesCommand(program) {
1672
1848
  const command = program.command('find-refactor-candidates');
1673
- command.description(spaceTrim$1(`
1674
- Scan source files to identify refactoring candidates
1675
-
1676
- Flags files that exceed:
1677
- - Line count limits (500 lines per file by extension)
1678
- - Entity count limits (max 4 entities per file)
1679
-
1680
- Generates refactor prompts with guidance for identified candidates.
1681
- `));
1682
- command.action(handleActionErrors(async () => {
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'));
1857
+ command.addOption(new Option('--level <level>', `Set scan aggressiveness (${REFACTOR_CANDIDATE_LEVEL_VALUES.join(', ')})`)
1858
+ .choices([...REFACTOR_CANDIDATE_LEVEL_VALUES])
1859
+ .default(DEFAULT_REFACTOR_CANDIDATE_LEVEL));
1860
+ command.action(handleActionErrors(async (cliOptions) => {
1861
+ const { level = DEFAULT_REFACTOR_CANDIDATE_LEVEL } = cliOptions;
1683
1862
  // Note: Import the function dynamically to avoid loading heavy dependencies until needed
1684
1863
  const { findRefactorCandidates } = await Promise.resolve().then(function () { return findRefactorCandidates$1; });
1685
1864
  try {
1686
- await findRefactorCandidates();
1865
+ await findRefactorCandidates({ level });
1687
1866
  }
1688
1867
  catch (error) {
1689
1868
  assertsError(error);
@@ -1729,7 +1908,9 @@ const PROMPTS_DONE_DIRECTORY_PATH = join(PROMPTS_DIRECTORY_PATH, 'done');
1729
1908
  */
1730
1909
  const PROMPTS_TEMPLATES_DIRECTORY_PATH = join(PROMPTS_DIRECTORY_PATH, 'templates');
1731
1910
  /**
1732
- * Built-in boilerplate templates shared by `coder init` and `coder generate-boilerplates`.
1911
+ * Built-in boilerplate templates available to `coder generate-boilerplates`.
1912
+ *
1913
+ * Only the project-agnostic subset is materialized by `coder init`.
1733
1914
  */
1734
1915
  const DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS = [
1735
1916
  {
@@ -1738,10 +1919,11 @@ const DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS = [
1738
1919
  slugPrefix: null,
1739
1920
  content: buildCoderPromptTemplateContent([
1740
1921
  '- @@@',
1741
- '- Keep in mind the DRY _(don\'t repeat yourself)_ principle.',
1922
+ "- Keep in mind the DRY _(don't repeat yourself)_ principle.",
1742
1923
  '- Do a proper analysis of the current functionality before you start implementing.',
1743
1924
  '- Add the changes into the [changelog](./changelog/_current-preversion.md)',
1744
1925
  ]),
1926
+ isDefaultProjectTemplate: true,
1745
1927
  },
1746
1928
  {
1747
1929
  id: 'agents-server',
@@ -1749,14 +1931,19 @@ const DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS = [
1749
1931
  slugPrefix: 'agents-server',
1750
1932
  content: buildCoderPromptTemplateContent([
1751
1933
  '- @@@',
1752
- '- Keep in mind the DRY _(don\'t repeat yourself)_ principle.',
1934
+ "- Keep in mind the DRY _(don't repeat yourself)_ principle.",
1753
1935
  '- Do a proper analysis of the current functionality before you start implementing.',
1754
1936
  '- You are working with the [Agents Server](apps/agents-server)',
1755
1937
  '- If you need to do the database migration, do it',
1756
1938
  '- Add the changes into the [changelog](changelog/_current-preversion.md)',
1757
1939
  ]),
1940
+ isDefaultProjectTemplate: false,
1758
1941
  },
1759
1942
  ];
1943
+ /**
1944
+ * Project-agnostic coder templates that `ptbk coder init` should materialize in any repository.
1945
+ */
1946
+ const DEFAULT_CODER_PROJECT_PROMPT_TEMPLATE_DEFINITIONS = DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS.filter(({ isDefaultProjectTemplate }) => isDefaultProjectTemplate);
1760
1947
  /**
1761
1948
  * Lists the built-in coder boilerplate templates.
1762
1949
  *
@@ -1765,6 +1952,14 @@ const DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS = [
1765
1952
  function getDefaultCoderPromptTemplateDefinitions() {
1766
1953
  return DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS;
1767
1954
  }
1955
+ /**
1956
+ * Lists the built-in coder prompt templates that are safe to initialize in any project.
1957
+ *
1958
+ * @private internal utility of `ptbk coder`
1959
+ */
1960
+ function getDefaultCoderProjectPromptTemplateDefinitions() {
1961
+ return DEFAULT_CODER_PROJECT_PROMPT_TEMPLATE_DEFINITIONS;
1962
+ }
1768
1963
  /**
1769
1964
  * Resolves one built-in coder boilerplate template definition by its stable identifier.
1770
1965
  *
@@ -1784,9 +1979,9 @@ function getDefaultCoderPromptTemplateDefinition(template) {
1784
1979
  */
1785
1980
  async function ensureDefaultCoderPromptTemplateFiles(projectPath) {
1786
1981
  const ensuredTemplateFiles = [];
1787
- for (const definition of DEFAULT_CODER_PROMPT_TEMPLATE_DEFINITIONS) {
1982
+ for (const definition of DEFAULT_CODER_PROJECT_PROMPT_TEMPLATE_DEFINITIONS) {
1788
1983
  const absoluteTemplatePath = join(projectPath, definition.relativeFilePath);
1789
- if (await isExistingFile$1(absoluteTemplatePath)) {
1984
+ if (await isExistingFile$2(absoluteTemplatePath)) {
1790
1985
  ensuredTemplateFiles.push({
1791
1986
  id: definition.id,
1792
1987
  relativeFilePath: definition.relativeFilePath,
@@ -1900,7 +2095,7 @@ function isNodeJsErrorWithCode(error, code) {
1900
2095
  /**
1901
2096
  * Checks whether a path exists and is a file.
1902
2097
  */
1903
- async function isExistingFile$1(path) {
2098
+ async function isExistingFile$2(path) {
1904
2099
  try {
1905
2100
  return (await stat(path)).isFile();
1906
2101
  }
@@ -2050,178 +2245,202 @@ function buildPromptSlug$1(templateSlugPrefix, title) {
2050
2245
  // Note: [💞] Ignore a discrepancy between file name and entity name
2051
2246
 
2052
2247
  /**
2053
- * This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
2248
+ * Relative path to the shared coder context file initialized in project roots.
2054
2249
  *
2055
- * @public exported from `@promptbook/core`
2250
+ * @private internal utility of `ptbk coder`
2056
2251
  */
2057
- class ParseError extends Error {
2058
- constructor(message) {
2059
- super(message);
2060
- this.name = 'ParseError';
2061
- Object.setPrototypeOf(this, ParseError.prototype);
2062
- }
2063
- }
2064
- // TODO: Maybe split `ParseError` and `ApplyError`
2065
-
2252
+ const AGENTS_FILE_PATH = 'AGENTS.md';
2066
2253
  /**
2067
- * Required environment variables for coding-agent git identity.
2254
+ * Stable boilerplate instructions written into newly initialized `AGENTS.md` files.
2068
2255
  */
2069
- const REQUIRED_CODER_ENV_VARIABLES = [
2070
- {
2071
- name: 'CODING_AGENT_GIT_NAME',
2072
- value: 'Promptbook Coding Agent',
2073
- },
2074
- {
2075
- name: 'CODING_AGENT_GIT_EMAIL',
2076
- value: 'coding-agent@promptbook.studio',
2077
- },
2078
- {
2079
- name: 'CODING_AGENT_GIT_SIGNING_KEY',
2080
- value: '13406525ED912F938FEA85AB4046C687298B2382',
2081
- },
2256
+ const DEFAULT_CODER_AGENTS_FILE_LINES = [
2257
+ '<!-- TODO: Write instructions for the Promptbook AI Coder here -->',
2082
2258
  ];
2083
2259
  /**
2084
- * Default npm scripts initialized by `ptbk coder init`.
2260
+ * Shared markdown boilerplate written into new `AGENTS.md` files.
2085
2261
  */
2086
- const DEFAULT_CODER_PACKAGE_JSON_SCRIPTS = {
2087
- 'coder:generate-boilerplates': 'npx ptbk coder generate-boilerplates',
2088
- 'coder:run': 'npx ptbk coder run --agent github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --no-wait',
2089
- 'coder:find-refactor-candidates': 'npx ptbk coder find-refactor-candidates',
2090
- 'coder:verify': 'npx ptbk coder verify',
2091
- };
2262
+ const DEFAULT_CODER_AGENTS_FILE_CONTENT = DEFAULT_CODER_AGENTS_FILE_LINES.join('\n');
2092
2263
  /**
2093
- * Relative path to `.gitignore` in the initialized project.
2094
- */
2095
- const GITIGNORE_FILE_PATH = '.gitignore';
2096
- /**
2097
- * Relative path to `package.json` in the initialized project.
2098
- */
2099
- const PACKAGE_JSON_FILE_PATH = 'package.json';
2100
- /**
2101
- * Relative path to the VS Code settings file initialized by `ptbk coder init`.
2264
+ * Returns the default coder `AGENTS.md` boilerplate instructions.
2265
+ *
2266
+ * @private internal utility of `ptbk coder`
2102
2267
  */
2103
- const VSCODE_SETTINGS_FILE_PATH = '.vscode/settings.json';
2268
+ function getDefaultCoderAgentsFileContent() {
2269
+ return DEFAULT_CODER_AGENTS_FILE_CONTENT;
2270
+ }
2271
+ // Note: [🟡] Code for coder AGENTS file boilerplate [agentsFile](src/cli/cli-commands/coder/agentsFile.ts) should never be published outside of `@promptbook/cli`
2272
+ // Note: [💞] Ignore a discrepancy between file name and exported helper names
2273
+
2104
2274
  /**
2105
- * Relative path to the VS Code directory initialized by `ptbk coder init`.
2275
+ * Normalizes one project-relative path for human-readable CLI output and markdown.
2276
+ *
2277
+ * @private internal utility of `ptbk coder`
2106
2278
  */
2107
- const VSCODE_DIRECTORY_PATH = '.vscode';
2279
+ function formatDisplayPath(relativePath) {
2280
+ return relativePath.replace(/\\/gu, '/');
2281
+ }
2282
+ // Note: [🟡] Code for coder path formatting [formatDisplayPath](src/cli/cli-commands/coder/formatDisplayPath.ts) should never be published outside of `@promptbook/cli`
2283
+
2108
2284
  /**
2109
- * VS Code setting key used to route pasted markdown images into prompt-specific screenshots.
2285
+ * Relative path to the Promptbook Coder quick-reference file initialized in project roots.
2286
+ *
2287
+ * @private internal utility of `ptbk coder`
2110
2288
  */
2111
- const MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY = 'markdown.copyFiles.destination';
2289
+ const AGENT_CODING_FILE_PATH = 'AGENT_CODING.md';
2112
2290
  /**
2113
- * Markdown glob used for coder prompt files inside VS Code settings.
2291
+ * Returns the default coder `AGENT_CODING.md` quick-reference content.
2292
+ *
2293
+ * @private internal utility of `ptbk coder`
2114
2294
  */
2115
- const PROMPTS_MARKDOWN_FILE_GLOB = 'prompts/*md';
2295
+ function getDefaultCoderAgentCodingFileContent({ packageJsonScripts, }) {
2296
+ return [
2297
+ '# Promptbook Coder quick reference',
2298
+ '',
2299
+ `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)}/\`.`,
2300
+ '',
2301
+ '## Workflow',
2302
+ `1. Put repository-wide coding rules into \`${AGENTS_FILE_PATH}\`. The default \`npm run coder:run\` script already passes \`--context ${AGENTS_FILE_PATH}\`.`,
2303
+ `2. Create or customize prompt templates in \`${formatDisplayPath(PROMPTS_TEMPLATES_DIRECTORY_PATH)}/\`. ${buildStarterTemplateSentence()}`,
2304
+ '3. Generate prompt files with `npm run coder:generate-boilerplates` or `npx ptbk coder generate-boilerplates --template <template> --count <count>`.',
2305
+ '4. Replace every `@@@`, keep drafts as `[-]`, and switch prompts to `[ ]` when they are ready to run. Completed prompts are marked `[x]`.',
2306
+ '5. Run `npm run coder:run` to execute the next ready prompt with the configured coding agent.',
2307
+ `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.`,
2308
+ '7. Use `npm run coder:find-refactor-candidates` when you want Promptbook to suggest refactor prompts automatically.',
2309
+ '',
2310
+ '## Templates',
2311
+ `- Project-owned templates created by \`ptbk coder init\`: ${formatInlineCodeList(getDefaultCoderProjectPromptTemplateDefinitions().map(({ relativeFilePath }) => formatDisplayPath(relativeFilePath)))}`,
2312
+ `- Built-in \`--template\` aliases: ${formatInlineCodeList(getDefaultCoderPromptTemplateDefinitions().map(({ id }) => id))}`,
2313
+ `- To add a custom template, create a markdown file such as \`${formatDisplayPath(PROMPTS_TEMPLATES_DIRECTORY_PATH)}/backend.md\`.`,
2314
+ `- To use a project template, run \`npx ptbk coder generate-boilerplates --template ${formatDisplayPath(PROMPTS_TEMPLATES_DIRECTORY_PATH)}/backend.md\`.`,
2315
+ `- 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.`,
2316
+ '',
2317
+ '## Created npm scripts',
2318
+ '| Script | Purpose |',
2319
+ '| --- | --- |',
2320
+ ...buildPackageJsonScriptTableLines(packageJsonScripts),
2321
+ '',
2322
+ '## Customizing the workflow',
2323
+ '- Edit `package.json` if you want `npm run coder:run` to use another coding agent, model, thinking level, context file, or wait mode.',
2324
+ '- Use direct CLI commands when you need one-off flags such as `--priority`, `--ignore-git-changes`, `--dry-run`, `--allow-credits`, or `--auto-migrate`.',
2325
+ '- Use `npx ptbk coder --help` and `npx ptbk coder <command> --help` for the full CLI reference.',
2326
+ ].join('\n');
2327
+ }
2116
2328
  /**
2117
- * Screenshot destination used for pasted prompt images inside VS Code settings.
2329
+ * Builds the sentence describing the starter templates created during initialization.
2118
2330
  */
2119
- const PROMPTS_SCREENSHOT_DESTINATION = './prompts/screenshots/${documentBaseName}.png';
2331
+ function buildStarterTemplateSentence() {
2332
+ const starterTemplatePaths = getDefaultCoderProjectPromptTemplateDefinitions().map(({ relativeFilePath }) => formatDisplayPath(relativeFilePath));
2333
+ if (starterTemplatePaths.length === 1) {
2334
+ return `The starter project template created by \`ptbk coder init\` is \`${starterTemplatePaths[0]}\`.`;
2335
+ }
2336
+ return `The starter project templates created by \`ptbk coder init\` are ${formatInlineCodeList(starterTemplatePaths)}.`;
2337
+ }
2120
2338
  /**
2121
- * Default indentation used when creating new JSON configuration files.
2339
+ * Builds the markdown table rows describing the initialized npm scripts.
2122
2340
  */
2123
- const DEFAULT_JSON_FILE_INDENTATION = ' ';
2341
+ function buildPackageJsonScriptTableLines(packageJsonScripts) {
2342
+ return Object.entries(packageJsonScripts).map(([scriptName, scriptCommand]) => `| \`npm run ${scriptName}\` | ${describeDefaultCoderPackageJsonScript(scriptName, scriptCommand)} |`);
2343
+ }
2124
2344
  /**
2125
- * Default newline used when creating new JSON configuration files.
2345
+ * Describes one initialized npm script in human-readable terms.
2126
2346
  */
2127
- const DEFAULT_JSON_FILE_NEWLINE = '\n';
2347
+ function describeDefaultCoderPackageJsonScript(scriptName, scriptCommand) {
2348
+ if (scriptName === 'coder:generate-boilerplates') {
2349
+ return `Runs \`${scriptCommand}\` to create new prompt files in \`${formatDisplayPath(PROMPTS_DIRECTORY_PATH)}/\`.`;
2350
+ }
2351
+ if (scriptName === 'coder:run') {
2352
+ return `Runs \`${scriptCommand}\` to execute the next ready prompt with shared repository context from \`${AGENTS_FILE_PATH}\`.`;
2353
+ }
2354
+ if (scriptName === 'coder:find-refactor-candidates') {
2355
+ return `Runs \`${scriptCommand}\` to generate prompt candidates for large or crowded files.`;
2356
+ }
2357
+ if (scriptName === 'coder:verify') {
2358
+ return `Runs \`${scriptCommand}\` to archive verified prompts into \`${formatDisplayPath(PROMPTS_DONE_DIRECTORY_PATH)}/\` and append repair prompts when needed.`;
2359
+ }
2360
+ return `Runs \`${scriptCommand}\`.`;
2361
+ }
2128
2362
  /**
2129
- * `.gitignore` block required by standalone Promptbook coder projects.
2363
+ * Formats one inline code list for human-readable markdown.
2130
2364
  */
2131
- const CODER_GITIGNORE_BLOCK = spaceTrim$1(`
2132
- # Promptbook Coder
2133
- /.tmp
2134
- `);
2365
+ function formatInlineCodeList(values) {
2366
+ return values.map((value) => `\`${value}\``).join(', ');
2367
+ }
2368
+ // Note: [🟡] Code for coder AGENT_CODING file boilerplate [agentCodingFile](src/cli/cli-commands/coder/agentCodingFile.ts) should never be published outside of `@promptbook/cli`
2369
+ // Note: [💞] Ignore a discrepancy between file name and exported helper names
2370
+
2135
2371
  /**
2136
- * Initializes `coder init` command for Promptbook CLI utilities.
2137
- *
2138
- * Note: `$` is used to indicate that this function is not a pure function - it registers a command in the CLI.
2372
+ * Appends one text block to existing file content while preserving readable newlines.
2139
2373
  *
2140
- * @private internal function of `promptbookCli`
2374
+ * @private function of `initializeCoderProjectConfiguration`
2141
2375
  */
2142
- function $initializeCoderInitCommand(program) {
2143
- const command = program.command('init');
2144
- command.alias('initialize');
2145
- command.description(spaceTrim$1(`
2146
- Initialize Promptbook coder configuration for current project
2147
-
2148
- Creates or updates:
2149
- - prompts/
2150
- - prompts/done/
2151
- - prompts/templates/common.md
2152
- - prompts/templates/agents-server.md
2153
- - .gitignore
2154
- - package.json
2155
- - .vscode/settings.json
2156
-
2157
- Ensures required coding-agent environment variables in .env:
2158
- - CODING_AGENT_GIT_NAME
2159
- - CODING_AGENT_GIT_EMAIL
2160
- - CODING_AGENT_GIT_SIGNING_KEY
2161
- `));
2162
- command.action(handleActionErrors(async () => {
2163
- const summary = await initializeCoderProjectConfiguration(process.cwd());
2164
- printInitializationSummary(summary);
2165
- }));
2376
+ function appendBlock(currentContent, blockToAppend) {
2377
+ if (currentContent.trim() === '') {
2378
+ return `${blockToAppend}\n`;
2379
+ }
2380
+ const normalizedCurrentContent = currentContent.endsWith('\n') ? currentContent : `${currentContent}\n`;
2381
+ return `${normalizedCurrentContent}\n${blockToAppend}\n`;
2166
2382
  }
2383
+ // Note: [🟡] Code for coder init text appending [appendBlock](src/cli/cli-commands/coder/appendBlock.ts) should never be published outside of `@promptbook/cli`
2384
+
2167
2385
  /**
2168
- * Lists the default npm scripts initialized by `ptbk coder init`.
2386
+ * Reads one text file when it exists, otherwise returns `undefined`.
2169
2387
  *
2170
- * @private internal utility of `coder init` command
2388
+ * @private function of `initializeCoderProjectConfiguration`
2171
2389
  */
2172
- function getDefaultCoderPackageJsonScripts() {
2173
- return DEFAULT_CODER_PACKAGE_JSON_SCRIPTS;
2390
+ async function readTextFileIfExists(path) {
2391
+ try {
2392
+ const fileStats = await stat(path);
2393
+ if (!fileStats.isFile()) {
2394
+ return undefined;
2395
+ }
2396
+ }
2397
+ catch (_a) {
2398
+ return undefined;
2399
+ }
2400
+ return readFile(path, 'utf-8');
2174
2401
  }
2402
+ // Note: [🟡] Code for coder init text-file reading [readTextFileIfExists](src/cli/cli-commands/coder/readTextFileIfExists.ts) should never be published outside of `@promptbook/cli`
2403
+
2175
2404
  /**
2176
- * Creates or updates all coder configuration artifacts required in the current project.
2177
- *
2178
- * @private internal utility of `coder init` command
2405
+ * Relative path to `.env` in the initialized project.
2179
2406
  */
2180
- async function initializeCoderProjectConfiguration(projectPath) {
2181
- const promptsDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_DIRECTORY_PATH);
2182
- const promptsDoneDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_DONE_DIRECTORY_PATH);
2183
- const promptsTemplatesDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_TEMPLATES_DIRECTORY_PATH);
2184
- const promptTemplateFileStatuses = await ensureDefaultCoderPromptTemplateFiles(projectPath);
2185
- const { envFileStatus, initializedEnvVariableNames } = await ensureCoderEnvFile(projectPath);
2186
- const gitignoreFileStatus = await ensureCoderGitignoreFile(projectPath);
2187
- const packageJsonFileStatus = await ensureCoderPackageJsonFile(projectPath);
2188
- const vscodeSettingsFileStatus = await ensureCoderVscodeSettingsFile(projectPath);
2189
- return {
2190
- promptsDirectoryStatus,
2191
- promptsDoneDirectoryStatus,
2192
- promptsTemplatesDirectoryStatus,
2193
- promptTemplateFileStatuses,
2194
- envFileStatus,
2195
- gitignoreFileStatus,
2196
- packageJsonFileStatus,
2197
- vscodeSettingsFileStatus,
2198
- initializedEnvVariableNames,
2199
- };
2200
- }
2407
+ const ENV_FILE_PATH = '.env';
2201
2408
  /**
2202
- * Ensures a relative directory exists in the project root.
2409
+ * Fallback `.env` content used when no required variables need to be appended.
2203
2410
  */
2204
- async function ensureDirectory(projectPath, relativeDirectoryPath) {
2205
- const directoryPath = join(projectPath, relativeDirectoryPath);
2206
- const existedBefore = await isExistingDirectory(directoryPath);
2207
- if (!existedBefore) {
2208
- await mkdir(directoryPath, { recursive: true });
2209
- return 'created';
2210
- }
2211
- return 'unchanged';
2212
- }
2411
+ const EMPTY_CODER_ENV_FILE_CONTENT = '# Environment variables for Promptbook coder\n';
2412
+ /**
2413
+ * Required environment variables for coding-agent git identity.
2414
+ */
2415
+ const REQUIRED_CODER_ENV_VARIABLES = [
2416
+ {
2417
+ name: 'CODING_AGENT_GIT_NAME',
2418
+ value: 'Promptbook Coding Agent',
2419
+ },
2420
+ {
2421
+ name: 'CODING_AGENT_GIT_EMAIL',
2422
+ value: 'coding-agent@promptbook.studio',
2423
+ },
2424
+ {
2425
+ name: 'CODING_AGENT_GIT_SIGNING_KEY',
2426
+ value: '13406525ED912F938FEA85AB4046C687298B2382',
2427
+ },
2428
+ ];
2213
2429
  /**
2214
2430
  * Ensures `.env` exists and contains all required coder environment variables.
2431
+ *
2432
+ * @private function of `initializeCoderProjectConfiguration`
2215
2433
  */
2216
2434
  async function ensureCoderEnvFile(projectPath) {
2217
- const envFilePath = join(projectPath, '.env');
2218
- const envFileExistedBefore = await isExistingFile(envFilePath);
2219
- const currentEnvContent = envFileExistedBefore ? await readFile(envFilePath, 'utf-8') : '';
2220
- const existingEnvVariables = parseEnvVariableNames(currentEnvContent);
2221
- const missingEnvVariables = REQUIRED_CODER_ENV_VARIABLES.filter(({ name }) => !existingEnvVariables.has(name));
2435
+ const envFilePath = join(projectPath, ENV_FILE_PATH);
2436
+ const existingEnvContent = await readTextFileIfExists(envFilePath);
2437
+ const isEnvFileExisting = existingEnvContent !== undefined;
2438
+ const currentEnvContent = existingEnvContent || '';
2439
+ const existingEnvVariableNames = parseEnvVariableNames(currentEnvContent);
2440
+ const missingEnvVariables = REQUIRED_CODER_ENV_VARIABLES.filter(({ name }) => !existingEnvVariableNames.has(name));
2222
2441
  if (missingEnvVariables.length === 0) {
2223
- if (!envFileExistedBefore) {
2224
- await writeFile(envFilePath, '# Environment variables for Promptbook coder\n', 'utf-8');
2442
+ if (!isEnvFileExisting) {
2443
+ await writeFile(envFilePath, EMPTY_CODER_ENV_FILE_CONTENT, 'utf-8');
2225
2444
  return {
2226
2445
  envFileStatus: 'created',
2227
2446
  initializedEnvVariableNames: [],
@@ -2236,12 +2455,54 @@ async function ensureCoderEnvFile(projectPath) {
2236
2455
  const nextEnvContent = appendBlock(currentEnvContent, envBlockToAppend);
2237
2456
  await writeFile(envFilePath, nextEnvContent, 'utf-8');
2238
2457
  return {
2239
- envFileStatus: envFileExistedBefore ? 'updated' : 'created',
2458
+ envFileStatus: isEnvFileExisting ? 'updated' : 'created',
2240
2459
  initializedEnvVariableNames: missingEnvVariables.map(({ name }) => name),
2241
2460
  };
2242
2461
  }
2462
+ /**
2463
+ * Parses variable names currently defined in `.env` style content.
2464
+ */
2465
+ function parseEnvVariableNames(envContent) {
2466
+ const variableNames = new Set();
2467
+ for (const line of envContent.split(/\r?\n/)) {
2468
+ const trimmedLine = line.trim();
2469
+ if (trimmedLine === '' || trimmedLine.startsWith('#')) {
2470
+ continue;
2471
+ }
2472
+ const match = trimmedLine.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
2473
+ if (!match || !match[1]) {
2474
+ continue;
2475
+ }
2476
+ variableNames.add(match[1]);
2477
+ }
2478
+ return variableNames;
2479
+ }
2480
+ /**
2481
+ * Builds a `.env` block containing missing coder environment variables.
2482
+ */
2483
+ function buildMissingEnvVariablesBlock(variables) {
2484
+ return spaceTrim$1(`
2485
+ # Promptbook coder identity (initialized by \`ptbk coder init\`)
2486
+ ${variables.map(({ name, value }) => `${name}=${JSON.stringify(value)}`).join('\n')}
2487
+ `);
2488
+ }
2489
+ // Note: [🟡] Code for coder init environment bootstrapping [ensureCoderEnvFile](src/cli/cli-commands/coder/ensureCoderEnvFile.ts) should never be published outside of `@promptbook/cli`
2490
+
2491
+ /**
2492
+ * Relative path to `.gitignore` in the initialized project.
2493
+ */
2494
+ const GITIGNORE_FILE_PATH = '.gitignore';
2495
+ /**
2496
+ * `.gitignore` block required by standalone Promptbook coder projects.
2497
+ */
2498
+ const CODER_GITIGNORE_BLOCK = spaceTrim$1(`
2499
+ # Promptbook Coder
2500
+ /.tmp
2501
+ `);
2243
2502
  /**
2244
2503
  * Ensures `.gitignore` contains the standalone Promptbook coder cache entry.
2504
+ *
2505
+ * @private function of `initializeCoderProjectConfiguration`
2245
2506
  */
2246
2507
  async function ensureCoderGitignoreFile(projectPath) {
2247
2508
  const gitignorePath = join(projectPath, GITIGNORE_FILE_PATH);
@@ -2254,95 +2515,339 @@ async function ensureCoderGitignoreFile(projectPath) {
2254
2515
  return currentGitignoreContent === undefined ? 'created' : 'updated';
2255
2516
  }
2256
2517
  /**
2257
- * Ensures `package.json` contains the standalone Promptbook coder helper scripts.
2518
+ * Detects whether `.gitignore` already covers the standalone coder temp directory.
2258
2519
  */
2259
- async function ensureCoderPackageJsonFile(projectPath) {
2260
- const packageJsonPath = join(projectPath, PACKAGE_JSON_FILE_PATH);
2261
- const packageJsonContent = await readTextFileIfExists(packageJsonPath);
2262
- const formatting = detectJsonFileFormatting(packageJsonContent);
2263
- const packageJson = packageJsonContent === undefined ? {} : await parseJsonObjectFile(PACKAGE_JSON_FILE_PATH, packageJsonContent);
2264
- const scripts = getStringRecordOrDefault(packageJson['scripts'], PACKAGE_JSON_FILE_PATH, 'scripts');
2265
- let hasChanges = packageJsonContent === undefined;
2266
- const nextScripts = { ...scripts };
2267
- for (const [scriptName, scriptCommand] of Object.entries(getDefaultCoderPackageJsonScripts())) {
2268
- if (nextScripts[scriptName] !== scriptCommand) {
2269
- nextScripts[scriptName] = scriptCommand;
2520
+ function hasTmpGitignoreRule(gitignoreContent) {
2521
+ return /(^|[\r\n])\/?\.tmp(?:[\r\n]|$)/u.test(gitignoreContent);
2522
+ }
2523
+ // Note: [🟡] Code for coder init gitignore bootstrapping [ensureCoderGitignoreFile](src/cli/cli-commands/coder/ensureCoderGitignoreFile.ts) should never be published outside of `@promptbook/cli`
2524
+
2525
+ /**
2526
+ * Ensures one coder markdown file exists with the provided default boilerplate.
2527
+ *
2528
+ * @private function of `initializeCoderProjectConfiguration`
2529
+ */
2530
+ async function ensureCoderMarkdownFile(projectPath, relativeFilePath, fileContent) {
2531
+ const absoluteFilePath = join(projectPath, relativeFilePath);
2532
+ if (await isExistingFile$1(absoluteFilePath)) {
2533
+ return 'unchanged';
2534
+ }
2535
+ await writeFile(absoluteFilePath, `${fileContent}\n`, 'utf-8');
2536
+ return 'created';
2537
+ }
2538
+ /**
2539
+ * Checks whether a path exists and is a file.
2540
+ */
2541
+ async function isExistingFile$1(path) {
2542
+ try {
2543
+ return (await stat(path)).isFile();
2544
+ }
2545
+ catch (_a) {
2546
+ return false;
2547
+ }
2548
+ }
2549
+ // Note: [🟡] Code for coder init markdown bootstrapping [ensureCoderMarkdownFile](src/cli/cli-commands/coder/ensureCoderMarkdownFile.ts) should never be published outside of `@promptbook/cli`
2550
+
2551
+ /**
2552
+ * Default npm scripts initialized by `ptbk coder init`.
2553
+ */
2554
+ const DEFAULT_CODER_PACKAGE_JSON_SCRIPTS = {
2555
+ 'coder:generate-boilerplates': 'npx ptbk coder generate-boilerplates',
2556
+ 'coder:run': 'npx ptbk coder run --agent github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --no-wait',
2557
+ 'coder:find-refactor-candidates': 'npx ptbk coder find-refactor-candidates',
2558
+ 'coder:verify': 'npx ptbk coder verify',
2559
+ };
2560
+ /**
2561
+ * Lists the default npm scripts initialized by `ptbk coder init`.
2562
+ *
2563
+ * @private internal utility of `coder init` command
2564
+ */
2565
+ function getDefaultCoderPackageJsonScripts() {
2566
+ return DEFAULT_CODER_PACKAGE_JSON_SCRIPTS;
2567
+ }
2568
+ // Note: [🟡] Code for coder init package scripts [getDefaultCoderPackageJsonScripts](src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.ts) should never be published outside of `@promptbook/cli`
2569
+
2570
+ /**
2571
+ * This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
2572
+ *
2573
+ * @public exported from `@promptbook/core`
2574
+ */
2575
+ class ParseError extends Error {
2576
+ constructor(message) {
2577
+ super(message);
2578
+ this.name = 'ParseError';
2579
+ Object.setPrototypeOf(this, ParseError.prototype);
2580
+ }
2581
+ }
2582
+ // TODO: Maybe split `ParseError` and `ApplyError`
2583
+
2584
+ /**
2585
+ * Default indentation used when creating new JSON configuration files.
2586
+ */
2587
+ const DEFAULT_JSON_FILE_INDENTATION = ' ';
2588
+ /**
2589
+ * Default newline used when creating new JSON configuration files.
2590
+ */
2591
+ const DEFAULT_JSON_FILE_NEWLINE = '\n';
2592
+ /**
2593
+ * Ensures one JSON object field contains the provided string-record entries.
2594
+ *
2595
+ * @private function of `initializeCoderProjectConfiguration`
2596
+ */
2597
+ async function mergeStringRecordJsonFile({ projectPath, relativeFilePath, fieldPath, nextEntries, ensureParentDirectoryPath, }) {
2598
+ if (ensureParentDirectoryPath) {
2599
+ await mkdir(join(projectPath, ensureParentDirectoryPath), { recursive: true });
2600
+ }
2601
+ const absoluteFilePath = join(projectPath, relativeFilePath);
2602
+ const fileContent = await readTextFileIfExists(absoluteFilePath);
2603
+ const formatting = detectJsonFileFormatting(fileContent);
2604
+ const jsonObject = fileContent === undefined ? {} : await parseJsonObjectFile(relativeFilePath, fileContent);
2605
+ const existingEntries = getStringRecordOrDefault(jsonObject[fieldPath], relativeFilePath, fieldPath);
2606
+ let hasChanges = fileContent === undefined;
2607
+ const mergedEntries = { ...existingEntries };
2608
+ for (const [entryKey, entryValue] of Object.entries(nextEntries)) {
2609
+ if (mergedEntries[entryKey] !== entryValue) {
2610
+ mergedEntries[entryKey] = entryValue;
2270
2611
  hasChanges = true;
2271
2612
  }
2272
2613
  }
2273
2614
  if (!hasChanges) {
2274
2615
  return 'unchanged';
2275
2616
  }
2276
- const nextPackageJson = { ...packageJson };
2277
- nextPackageJson['scripts'] = nextScripts;
2278
- await writeFile(packageJsonPath, serializeJsonObject(nextPackageJson, formatting), 'utf-8');
2279
- return packageJsonContent === undefined ? 'created' : 'updated';
2617
+ const nextJsonObject = { ...jsonObject };
2618
+ nextJsonObject[fieldPath] = mergedEntries;
2619
+ await writeFile(absoluteFilePath, serializeJsonObject(nextJsonObject, formatting), 'utf-8');
2620
+ return fileContent === undefined ? 'created' : 'updated';
2280
2621
  }
2281
2622
  /**
2282
- * Ensures VS Code routes pasted prompt images into `prompts/screenshots`.
2623
+ * Parses one JSON object file while accepting VS Code style comments and trailing commas.
2283
2624
  */
2284
- async function ensureCoderVscodeSettingsFile(projectPath) {
2285
- await mkdir(join(projectPath, VSCODE_DIRECTORY_PATH), { recursive: true });
2286
- const vscodeSettingsPath = join(projectPath, VSCODE_SETTINGS_FILE_PATH);
2287
- const vscodeSettingsContent = await readTextFileIfExists(vscodeSettingsPath);
2288
- const formatting = detectJsonFileFormatting(vscodeSettingsContent);
2289
- const vscodeSettings = vscodeSettingsContent === undefined
2290
- ? {}
2291
- : await parseJsonObjectFile(VSCODE_SETTINGS_FILE_PATH, vscodeSettingsContent);
2292
- const markdownCopyFilesDestinations = getStringRecordOrDefault(vscodeSettings[MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY], VSCODE_SETTINGS_FILE_PATH, MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY);
2293
- let hasChanges = vscodeSettingsContent === undefined;
2294
- const nextMarkdownCopyFilesDestinations = { ...markdownCopyFilesDestinations };
2295
- if (nextMarkdownCopyFilesDestinations[PROMPTS_MARKDOWN_FILE_GLOB] !== PROMPTS_SCREENSHOT_DESTINATION) {
2296
- nextMarkdownCopyFilesDestinations[PROMPTS_MARKDOWN_FILE_GLOB] = PROMPTS_SCREENSHOT_DESTINATION;
2297
- hasChanges = true;
2625
+ async function parseJsonObjectFile(relativeFilePath, fileContent) {
2626
+ if (fileContent.trim() === '') {
2627
+ return {};
2298
2628
  }
2299
- if (!hasChanges) {
2300
- return 'unchanged';
2629
+ const typescript = await import('typescript');
2630
+ const parsedFile = typescript.parseConfigFileTextToJson(relativeFilePath, fileContent);
2631
+ if (parsedFile.error) {
2632
+ throw new ParseError(spaceTrim$1(`
2633
+ Cannot parse \`${relativeFilePath}\` as JSON.
2634
+
2635
+ ${typescript.flattenDiagnosticMessageText(parsedFile.error.messageText, '\n')}
2636
+ `));
2301
2637
  }
2302
- const nextVscodeSettings = { ...vscodeSettings };
2303
- nextVscodeSettings[MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY] = nextMarkdownCopyFilesDestinations;
2304
- await writeFile(vscodeSettingsPath, serializeJsonObject(nextVscodeSettings, formatting), 'utf-8');
2305
- return vscodeSettingsContent === undefined ? 'created' : 'updated';
2638
+ if (!isPlainObject(parsedFile.config)) {
2639
+ throw new ParseError(spaceTrim$1(`
2640
+ File \`${relativeFilePath}\` must contain one top-level JSON object.
2641
+ `));
2642
+ }
2643
+ return parsedFile.config;
2306
2644
  }
2307
2645
  /**
2308
- * Parses variable names currently defined in `.env` style content.
2646
+ * Reads one JSON object field as a string-to-string record.
2309
2647
  */
2310
- function parseEnvVariableNames(envContent) {
2311
- const variableNames = new Set();
2312
- for (const line of envContent.split(/\r?\n/)) {
2313
- const trimmedLine = line.trim();
2314
- if (trimmedLine === '' || trimmedLine.startsWith('#')) {
2315
- continue;
2316
- }
2317
- const match = trimmedLine.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
2318
- if (!match || !match[1]) {
2319
- continue;
2648
+ function getStringRecordOrDefault(value, relativeFilePath, fieldPath) {
2649
+ if (value === undefined) {
2650
+ return {};
2651
+ }
2652
+ if (!isPlainObject(value)) {
2653
+ throw new ParseError(spaceTrim$1(`
2654
+ File \`${relativeFilePath}\` contains invalid \`${fieldPath}\`.
2655
+
2656
+ Expected \`${fieldPath}\` to be an object with string values.
2657
+ `));
2658
+ }
2659
+ const stringRecord = {};
2660
+ for (const [key, itemValue] of Object.entries(value)) {
2661
+ if (typeof itemValue !== 'string') {
2662
+ throw new ParseError(spaceTrim$1(`
2663
+ File \`${relativeFilePath}\` contains invalid \`${fieldPath}.${key}\`.
2664
+
2665
+ Expected \`${fieldPath}\` to be an object with string values.
2666
+ `));
2320
2667
  }
2321
- variableNames.add(match[1]);
2668
+ stringRecord[key] = itemValue;
2322
2669
  }
2323
- return variableNames;
2670
+ return stringRecord;
2324
2671
  }
2325
2672
  /**
2326
- * Builds a `.env` block containing missing coder environment variables.
2673
+ * Serializes one JSON object using detected or default formatting.
2327
2674
  */
2328
- function buildMissingEnvVariablesBlock(variables) {
2329
- return spaceTrim$1(`
2330
- # Promptbook coder identity (initialized by \`ptbk coder init\`)
2331
- ${variables.map(({ name, value }) => `${name}=${JSON.stringify(value)}`).join('\n')}
2332
- `);
2675
+ function serializeJsonObject(value, formatting) {
2676
+ return `${JSON.stringify(value, null, formatting.indentation)}${formatting.newline}`;
2333
2677
  }
2334
2678
  /**
2335
- * Appends one text block to existing file content while preserving readable newlines.
2679
+ * Detects indentation and newline formatting from an existing JSON file.
2336
2680
  */
2337
- function appendBlock(currentContent, blockToAppend) {
2338
- if (currentContent.trim() === '') {
2339
- return `${blockToAppend}\n`;
2681
+ function detectJsonFileFormatting(fileContent) {
2682
+ if (!fileContent) {
2683
+ return {
2684
+ indentation: DEFAULT_JSON_FILE_INDENTATION,
2685
+ newline: DEFAULT_JSON_FILE_NEWLINE,
2686
+ };
2340
2687
  }
2341
- const normalizedCurrentContent = currentContent.endsWith('\n') ? currentContent : `${currentContent}\n`;
2342
- return `${normalizedCurrentContent}\n${blockToAppend}\n`;
2688
+ const indentationMatch = fileContent.match(/^[ \t]+(?=")/mu);
2689
+ return {
2690
+ indentation: (indentationMatch === null || indentationMatch === void 0 ? void 0 : indentationMatch[0]) || DEFAULT_JSON_FILE_INDENTATION,
2691
+ newline: fileContent.includes('\r\n') ? '\r\n' : '\n',
2692
+ };
2693
+ }
2694
+ /**
2695
+ * Checks whether one parsed JSON value is a plain object.
2696
+ */
2697
+ function isPlainObject(value) {
2698
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
2699
+ }
2700
+ // Note: [🟡] Code for coder init JSON merging [mergeStringRecordJsonFile](src/cli/cli-commands/coder/mergeStringRecordJsonFile.ts) should never be published outside of `@promptbook/cli`
2701
+
2702
+ /**
2703
+ * Relative path to `package.json` in the initialized project.
2704
+ */
2705
+ const PACKAGE_JSON_FILE_PATH = 'package.json';
2706
+ /**
2707
+ * Ensures `package.json` contains the standalone Promptbook coder helper scripts.
2708
+ *
2709
+ * @private function of `initializeCoderProjectConfiguration`
2710
+ */
2711
+ async function ensureCoderPackageJsonFile(projectPath) {
2712
+ return mergeStringRecordJsonFile({
2713
+ projectPath,
2714
+ relativeFilePath: PACKAGE_JSON_FILE_PATH,
2715
+ fieldPath: 'scripts',
2716
+ nextEntries: getDefaultCoderPackageJsonScripts(),
2717
+ });
2718
+ }
2719
+ // Note: [🟡] Code for coder init package.json bootstrapping [ensureCoderPackageJsonFile](src/cli/cli-commands/coder/ensureCoderPackageJsonFile.ts) should never be published outside of `@promptbook/cli`
2720
+
2721
+ /**
2722
+ * VS Code setting key used to route pasted markdown images into prompt-specific screenshots.
2723
+ */
2724
+ const MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY = 'markdown.copyFiles.destination';
2725
+ /**
2726
+ * Markdown glob used for coder prompt files inside VS Code settings.
2727
+ */
2728
+ const PROMPTS_MARKDOWN_FILE_GLOB = 'prompts/*md';
2729
+ /**
2730
+ * Screenshot destination used for pasted prompt images inside VS Code settings.
2731
+ */
2732
+ const PROMPTS_SCREENSHOT_DESTINATION = './prompts/screenshots/${documentBaseName}.png';
2733
+ /**
2734
+ * Default VS Code settings initialized by `ptbk coder init`.
2735
+ */
2736
+ const DEFAULT_CODER_VSCODE_SETTINGS = {
2737
+ [MARKDOWN_COPY_FILES_DESTINATION_SETTING_KEY]: {
2738
+ [PROMPTS_MARKDOWN_FILE_GLOB]: PROMPTS_SCREENSHOT_DESTINATION,
2739
+ },
2740
+ };
2741
+ /**
2742
+ * Lists the default VS Code settings initialized by `ptbk coder init`.
2743
+ *
2744
+ * @private internal utility of `coder init` command
2745
+ */
2746
+ function getDefaultCoderVscodeSettings() {
2747
+ return DEFAULT_CODER_VSCODE_SETTINGS;
2748
+ }
2749
+ // Note: [🟡] Code for coder init VS Code settings [getDefaultCoderVscodeSettings](src/cli/cli-commands/coder/getDefaultCoderVscodeSettings.ts) should never be published outside of `@promptbook/cli`
2750
+
2751
+ /**
2752
+ * Relative path to the VS Code settings file initialized by `ptbk coder init`.
2753
+ */
2754
+ const VSCODE_SETTINGS_FILE_PATH = '.vscode/settings.json';
2755
+ /**
2756
+ * Relative path to the VS Code directory initialized by `ptbk coder init`.
2757
+ */
2758
+ const VSCODE_DIRECTORY_PATH = '.vscode';
2759
+ /**
2760
+ * Ensures VS Code routes pasted prompt images into `prompts/screenshots`.
2761
+ *
2762
+ * @private function of `initializeCoderProjectConfiguration`
2763
+ */
2764
+ async function ensureCoderVscodeSettingsFile(projectPath) {
2765
+ const [fieldPath, nextEntries] = resolveDefaultCoderVscodeSettingsEntry();
2766
+ return mergeStringRecordJsonFile({
2767
+ projectPath,
2768
+ relativeFilePath: VSCODE_SETTINGS_FILE_PATH,
2769
+ fieldPath,
2770
+ nextEntries,
2771
+ ensureParentDirectoryPath: VSCODE_DIRECTORY_PATH,
2772
+ });
2773
+ }
2774
+ /**
2775
+ * Resolves the default string-record entry that `coder init` merges into VS Code settings.
2776
+ */
2777
+ function resolveDefaultCoderVscodeSettingsEntry() {
2778
+ const [defaultVscodeSettingsEntry] = Object.entries(getDefaultCoderVscodeSettings());
2779
+ if (!defaultVscodeSettingsEntry) {
2780
+ throw new Error('Default coder VS Code settings must define at least one string-record entry.');
2781
+ }
2782
+ return defaultVscodeSettingsEntry;
2783
+ }
2784
+ // Note: [🟡] Code for coder init VS Code bootstrapping [ensureCoderVscodeSettingsFile](src/cli/cli-commands/coder/ensureCoderVscodeSettingsFile.ts) should never be published outside of `@promptbook/cli`
2785
+
2786
+ /**
2787
+ * Ensures a relative directory exists in the project root.
2788
+ *
2789
+ * @private function of `initializeCoderProjectConfiguration`
2790
+ */
2791
+ async function ensureDirectory(projectPath, relativeDirectoryPath) {
2792
+ const directoryPath = join(projectPath, relativeDirectoryPath);
2793
+ const isDirectoryExisting = await isExistingDirectory(directoryPath);
2794
+ if (!isDirectoryExisting) {
2795
+ await mkdir(directoryPath, { recursive: true });
2796
+ return 'created';
2797
+ }
2798
+ return 'unchanged';
2799
+ }
2800
+ /**
2801
+ * Checks whether a path exists and is a directory.
2802
+ */
2803
+ async function isExistingDirectory(path) {
2804
+ try {
2805
+ return (await stat(path)).isDirectory();
2806
+ }
2807
+ catch (_a) {
2808
+ return false;
2809
+ }
2810
+ }
2811
+ // Note: [🟡] Code for coder init directory creation [ensureDirectory](src/cli/cli-commands/coder/ensureDirectory.ts) should never be published outside of `@promptbook/cli`
2812
+
2813
+ /**
2814
+ * Creates or updates all coder configuration artifacts required in the current project.
2815
+ *
2816
+ * @private internal utility of `coder init` command
2817
+ */
2818
+ async function initializeCoderProjectConfiguration(projectPath) {
2819
+ const promptsDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_DIRECTORY_PATH);
2820
+ const promptsDoneDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_DONE_DIRECTORY_PATH);
2821
+ const promptsTemplatesDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_TEMPLATES_DIRECTORY_PATH);
2822
+ const promptTemplateFileStatuses = await ensureDefaultCoderPromptTemplateFiles(projectPath);
2823
+ const agentsFileStatus = await ensureCoderMarkdownFile(projectPath, AGENTS_FILE_PATH, getDefaultCoderAgentsFileContent());
2824
+ const agentCodingFileStatus = await ensureCoderMarkdownFile(projectPath, AGENT_CODING_FILE_PATH, getDefaultCoderAgentCodingFileContent({
2825
+ packageJsonScripts: getDefaultCoderPackageJsonScripts(),
2826
+ }));
2827
+ const { envFileStatus, initializedEnvVariableNames } = await ensureCoderEnvFile(projectPath);
2828
+ const gitignoreFileStatus = await ensureCoderGitignoreFile(projectPath);
2829
+ const packageJsonFileStatus = await ensureCoderPackageJsonFile(projectPath);
2830
+ const vscodeSettingsFileStatus = await ensureCoderVscodeSettingsFile(projectPath);
2831
+ return {
2832
+ promptsDirectoryStatus,
2833
+ promptsDoneDirectoryStatus,
2834
+ promptsTemplatesDirectoryStatus,
2835
+ promptTemplateFileStatuses,
2836
+ agentsFileStatus,
2837
+ agentCodingFileStatus,
2838
+ envFileStatus,
2839
+ gitignoreFileStatus,
2840
+ packageJsonFileStatus,
2841
+ vscodeSettingsFileStatus,
2842
+ initializedEnvVariableNames,
2843
+ };
2343
2844
  }
2845
+ // Note: [🟡] Code for coder init project bootstrapping [initializeCoderProjectConfiguration](src/cli/cli-commands/coder/initializeCoderProjectConfiguration.ts) should never be published outside of `@promptbook/cli`
2846
+
2344
2847
  /**
2345
2848
  * Prints a readable summary of what was initialized for the user.
2849
+ *
2850
+ * @private function of `coder init` command
2346
2851
  */
2347
2852
  function printInitializationSummary(summary) {
2348
2853
  console.info(colors.green('Promptbook coder configuration initialized.'));
@@ -2352,6 +2857,8 @@ function printInitializationSummary(summary) {
2352
2857
  for (const templateFileStatus of summary.promptTemplateFileStatuses) {
2353
2858
  printInitializationStatusLine(formatDisplayPath(templateFileStatus.relativeFilePath), templateFileStatus.status);
2354
2859
  }
2860
+ printInitializationStatusLine(AGENTS_FILE_PATH, summary.agentsFileStatus);
2861
+ printInitializationStatusLine(AGENT_CODING_FILE_PATH, summary.agentCodingFileStatus);
2355
2862
  printInitializationStatusLine('.env', summary.envFileStatus);
2356
2863
  printInitializationStatusLine('.gitignore', summary.gitignoreFileStatus);
2357
2864
  printInitializationStatusLine('package.json', summary.packageJsonFileStatus);
@@ -2387,126 +2894,48 @@ function printInitializationStatusLine(relativePath, status) {
2387
2894
  function printInitializationNote(message, colorize) {
2388
2895
  console.info(colorize(`✔ ${message}`));
2389
2896
  }
2390
- /**
2391
- * Normalizes one project-relative path for human-readable CLI output.
2392
- */
2393
- function formatDisplayPath(relativePath) {
2394
- return relativePath.replace(/\\/gu, '/');
2395
- }
2396
- /**
2397
- * Detects whether `.gitignore` already covers the standalone coder temp directory.
2398
- */
2399
- function hasTmpGitignoreRule(gitignoreContent) {
2400
- return /(^|[\r\n])\/?\.tmp(?:[\r\n]|$)/u.test(gitignoreContent);
2401
- }
2402
- /**
2403
- * Reads one text file when it exists, otherwise returns `undefined`.
2404
- */
2405
- async function readTextFileIfExists(path) {
2406
- if (!(await isExistingFile(path))) {
2407
- return undefined;
2408
- }
2409
- return readFile(path, 'utf-8');
2410
- }
2411
- /**
2412
- * Parses one JSON object file while accepting VS Code style comments and trailing commas.
2413
- */
2414
- async function parseJsonObjectFile(relativeFilePath, fileContent) {
2415
- if (fileContent.trim() === '') {
2416
- return {};
2417
- }
2418
- const typescript = await import('typescript');
2419
- const parsedFile = typescript.parseConfigFileTextToJson(relativeFilePath, fileContent);
2420
- if (parsedFile.error) {
2421
- throw new ParseError(spaceTrim$1(`
2422
- Cannot parse \`${relativeFilePath}\` as JSON.
2897
+ // Note: [🟡] Code for coder init summary printing [printInitializationSummary](src/cli/cli-commands/coder/printInitializationSummary.ts) should never be published outside of `@promptbook/cli`
2423
2898
 
2424
- ${typescript.flattenDiagnosticMessageText(parsedFile.error.messageText, '\n')}
2425
- `));
2426
- }
2427
- if (!isPlainObject(parsedFile.config)) {
2428
- throw new ParseError(spaceTrim$1(`
2429
- File \`${relativeFilePath}\` must contain one top-level JSON object.
2430
- `));
2431
- }
2432
- return parsedFile.config;
2433
- }
2434
2899
  /**
2435
- * Reads one JSON object field as a string-to-string record.
2900
+ * Initializes `coder init` command for Promptbook CLI utilities.
2901
+ *
2902
+ * Note: `$` is used to indicate that this function is not a pure function - it registers a command in the CLI.
2903
+ *
2904
+ * @private internal function of `promptbookCli`
2436
2905
  */
2437
- function getStringRecordOrDefault(value, relativeFilePath, fieldPath) {
2438
- if (value === undefined) {
2439
- return {};
2440
- }
2441
- if (!isPlainObject(value)) {
2442
- throw new ParseError(spaceTrim$1(`
2443
- File \`${relativeFilePath}\` contains invalid \`${fieldPath}\`.
2906
+ function $initializeCoderInitCommand(program) {
2907
+ const command = program.command('init');
2908
+ command.alias('initialize');
2909
+ command.description(spaceTrim$1(`
2910
+ Initialize Promptbook coder configuration for current project
2444
2911
 
2445
- Expected \`${fieldPath}\` to be an object with string values.
2446
- `));
2447
- }
2448
- const stringRecord = {};
2449
- for (const [key, itemValue] of Object.entries(value)) {
2450
- if (typeof itemValue !== 'string') {
2451
- throw new ParseError(spaceTrim$1(`
2452
- File \`${relativeFilePath}\` contains invalid \`${fieldPath}.${key}\`.
2912
+ Creates or updates:
2913
+ - prompts/
2914
+ - prompts/done/
2915
+ ${listDefaultCoderProjectPromptTemplateDisplayPaths()}
2916
+ - ${AGENTS_FILE_PATH}
2917
+ - ${AGENT_CODING_FILE_PATH}
2918
+ - .gitignore
2919
+ - package.json
2920
+ - .vscode/settings.json
2453
2921
 
2454
- Expected \`${fieldPath}\` to be an object with string values.
2455
- `));
2456
- }
2457
- stringRecord[key] = itemValue;
2458
- }
2459
- return stringRecord;
2460
- }
2461
- /**
2462
- * Serializes one JSON object using detected or default formatting.
2463
- */
2464
- function serializeJsonObject(value, formatting) {
2465
- return `${JSON.stringify(value, null, formatting.indentation)}${formatting.newline}`;
2466
- }
2467
- /**
2468
- * Detects indentation and newline formatting from an existing JSON file.
2469
- */
2470
- function detectJsonFileFormatting(fileContent) {
2471
- if (!fileContent) {
2472
- return {
2473
- indentation: DEFAULT_JSON_FILE_INDENTATION,
2474
- newline: DEFAULT_JSON_FILE_NEWLINE,
2475
- };
2476
- }
2477
- const indentationMatch = fileContent.match(/^[ \t]+(?=")/mu);
2478
- return {
2479
- indentation: (indentationMatch === null || indentationMatch === void 0 ? void 0 : indentationMatch[0]) || DEFAULT_JSON_FILE_INDENTATION,
2480
- newline: fileContent.includes('\r\n') ? '\r\n' : '\n',
2481
- };
2482
- }
2483
- /**
2484
- * Checks whether one parsed JSON value is a plain object.
2485
- */
2486
- function isPlainObject(value) {
2487
- return typeof value === 'object' && value !== null && !Array.isArray(value);
2488
- }
2489
- /**
2490
- * Checks whether a path exists and is a file.
2491
- */
2492
- async function isExistingFile(path) {
2493
- try {
2494
- return (await stat(path)).isFile();
2495
- }
2496
- catch (_a) {
2497
- return false;
2498
- }
2922
+ Ensures required coding-agent environment variables in .env:
2923
+ - CODING_AGENT_GIT_NAME
2924
+ - CODING_AGENT_GIT_EMAIL
2925
+ - CODING_AGENT_GIT_SIGNING_KEY
2926
+ `));
2927
+ command.action(handleActionErrors(async () => {
2928
+ const summary = await initializeCoderProjectConfiguration(process.cwd());
2929
+ printInitializationSummary(summary);
2930
+ }));
2499
2931
  }
2500
2932
  /**
2501
- * Checks whether a path exists and is a directory.
2933
+ * Lists the project-owned template file paths created by `ptbk coder init`.
2502
2934
  */
2503
- async function isExistingDirectory(path) {
2504
- try {
2505
- return (await stat(path)).isDirectory();
2506
- }
2507
- catch (_a) {
2508
- return false;
2509
- }
2935
+ function listDefaultCoderProjectPromptTemplateDisplayPaths() {
2936
+ return getDefaultCoderProjectPromptTemplateDefinitions()
2937
+ .map(({ relativeFilePath }) => `- ${formatDisplayPath(relativeFilePath)}`)
2938
+ .join('\n');
2510
2939
  }
2511
2940
  // Note: [🟡] Code for CLI command [init](src/cli/cli-commands/coder/init.ts) should never be published outside of `@promptbook/cli`
2512
2941
  // Note: [💞] Ignore a discrepancy between file name and entity name
@@ -40740,91 +41169,6 @@ var findFreshEmojiTags = /*#__PURE__*/Object.freeze({
40740
41169
  findFreshEmojiTag: findFreshEmojiTag
40741
41170
  });
40742
41171
 
40743
- /**
40744
- * Root folders that contain source-like files for scanning.
40745
- */
40746
- const SOURCE_ROOTS = ['src', 'apps', 'scripts', 'examples', 'agents', 'other'];
40747
- /**
40748
- * File extensions treated as source code.
40749
- */
40750
- const SOURCE_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
40751
- /**
40752
- * Glob patterns that should be ignored when scanning for source files.
40753
- */
40754
- const SOURCE_FILE_IGNORE_GLOBS = [
40755
- '**/node_modules/**',
40756
- '**/packages/**',
40757
- '**/.*/**',
40758
- '**/.git/**',
40759
- '**/.idea/**',
40760
- '**/.vscode/**',
40761
- '**/.promptbook/**',
40762
- '**/.next/**',
40763
- '**/.tmp/**',
40764
- '**/tmp/**',
40765
- '**/coverage/**',
40766
- '**/dist/**',
40767
- '**/build/**',
40768
- '**/out/**',
40769
- '**/prompts/**',
40770
- '**/changelog/**',
40771
- ];
40772
- /**
40773
- * Default maximum line count for source files.
40774
- */
40775
- const DEFAULT_MAX_LINE_COUNT = 2000;
40776
- /**
40777
- * Per-extension line count limits.
40778
- */
40779
- const LINE_COUNT_LIMITS_BY_EXTENSION = {
40780
- '.ts': 2000,
40781
- '.tsx': 2000,
40782
- '.js': 2000,
40783
- '.jsx': 2000,
40784
- };
40785
- /**
40786
- * Glob patterns that are exempt from line-count checks.
40787
- */
40788
- const LINE_COUNT_EXEMPT_GLOBS = ['other/cspell-dictionaries/**/*.txt'];
40789
- /**
40790
- * Maximum number of entities before a file is flagged.
40791
- */
40792
- const MAX_ENTITIES_PER_FILE = 20;
40793
- /**
40794
- * File extensions eligible for entity counting.
40795
- */
40796
- const ENTITY_COUNT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
40797
- /**
40798
- * Markers that identify generated files which should be skipped.
40799
- */
40800
- const GENERATED_CODE_MARKERS = [
40801
- 'WARNING: This code has been generated',
40802
- 'This code has been generated so that any manual changes will be overwritten',
40803
- ];
40804
- /**
40805
- * Name of the prompts directory.
40806
- */
40807
- const PROMPTS_DIR_NAME = 'prompts';
40808
- /**
40809
- * Step size used for prompt numbering.
40810
- */
40811
- const PROMPT_NUMBER_STEP = 10;
40812
- /**
40813
- * Prefix used for generated prompt slugs.
40814
- */
40815
- const PROMPT_SLUG_PREFIX = 'refactor';
40816
- /**
40817
- * Label used to mark the target file in generated prompts.
40818
- */
40819
- const PROMPT_TARGET_LABEL = 'Target file';
40820
- /**
40821
- * Maximum length for generated prompt slugs.
40822
- */
40823
- const PROMPT_SLUG_MAX_LENGTH = 80;
40824
- /**
40825
- * Note: [?] Code in this file should never be published in any package
40826
- */
40827
-
40828
41172
  /**
40829
41173
  * Normalizes a repo-relative path to use forward slashes.
40830
41174
  *
@@ -40842,7 +41186,7 @@ function normalizeRefactorCandidatePath(pathValue) {
40842
41186
  * @private function of findRefactorCandidates
40843
41187
  */
40844
41188
  async function analyzeSourceFileForRefactorCandidate(options) {
40845
- const { filePath, lineCountExemptPaths, rootDir } = options;
41189
+ const { filePath, heuristics, lineCountExemptPaths, rootDir } = options;
40846
41190
  const normalizedAbsolutePath = normalizeAbsolutePath$1(filePath);
40847
41191
  const content = await readFile(filePath, 'utf-8');
40848
41192
  if (isGeneratedFile(content)) {
@@ -40853,15 +41197,21 @@ async function analyzeSourceFileForRefactorCandidate(options) {
40853
41197
  const reasons = [];
40854
41198
  if (!lineCountExemptPaths.has(normalizedAbsolutePath)) {
40855
41199
  const lineCount = countLines(content);
40856
- const maxLines = getMaxLinesForExtension(extension);
41200
+ const maxLines = getMaxLinesForExtension(extension, heuristics);
40857
41201
  if (lineCount > maxLines) {
40858
41202
  reasons.push(`lines ${lineCount}/${maxLines}`);
40859
41203
  }
40860
41204
  }
40861
- if (ENTITY_COUNT_EXTENSIONS.includes(extension)) {
40862
- const entityCount = countEntities(content, extension, filePath);
40863
- if (entityCount > MAX_ENTITIES_PER_FILE) {
40864
- reasons.push(`entities ${entityCount}/${MAX_ENTITIES_PER_FILE}`);
41205
+ if (STRUCTURAL_ANALYSIS_EXTENSIONS.includes(extension)) {
41206
+ const structureSummary = summarizeSourceFileStructure(content, extension, filePath);
41207
+ if (structureSummary.entityCount > heuristics.maxEntityCountPerFile) {
41208
+ reasons.push(`entities ${structureSummary.entityCount}/${heuristics.maxEntityCountPerFile}`);
41209
+ }
41210
+ if (structureSummary.functionCount > heuristics.maxFunctionCountPerFile) {
41211
+ reasons.push(`functions ${structureSummary.functionCount}/${heuristics.maxFunctionCountPerFile}`);
41212
+ }
41213
+ if (structureSummary.maxFunctionComplexity > heuristics.maxFunctionComplexity) {
41214
+ reasons.push(buildComplexityReason(structureSummary, heuristics.maxFunctionComplexity));
40865
41215
  }
40866
41216
  }
40867
41217
  if (reasons.length === 0) {
@@ -40886,9 +41236,9 @@ function isGeneratedFile(content) {
40886
41236
  *
40887
41237
  * @private function of analyzeSourceFileForRefactorCandidate
40888
41238
  */
40889
- function getMaxLinesForExtension(extension) {
41239
+ function getMaxLinesForExtension(extension, heuristics) {
40890
41240
  var _a;
40891
- return (_a = LINE_COUNT_LIMITS_BY_EXTENSION[extension]) !== null && _a !== void 0 ? _a : DEFAULT_MAX_LINE_COUNT;
41241
+ return (_a = heuristics.maxLineCountByExtension[extension]) !== null && _a !== void 0 ? _a : heuristics.maxDefaultLineCount;
40892
41242
  }
40893
41243
  /**
40894
41244
  * Counts lines while ignoring a trailing newline.
@@ -40903,14 +41253,17 @@ function countLines(content) {
40903
41253
  return lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
40904
41254
  }
40905
41255
  /**
40906
- * Counts top-level entities in a source file.
41256
+ * Summarizes the structural metrics used to score one source file.
40907
41257
  *
40908
41258
  * @private function of analyzeSourceFileForRefactorCandidate
40909
41259
  */
40910
- function countEntities(content, extension, filePath) {
41260
+ function summarizeSourceFileStructure(content, extension, filePath) {
40911
41261
  const scriptKind = getScriptKindForExtension(extension);
40912
- const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, false, scriptKind);
40913
- return countEntitiesInSourceFile(sourceFile);
41262
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
41263
+ return {
41264
+ entityCount: countEntitiesInSourceFile(sourceFile),
41265
+ ...summarizeFunctionsInSourceFile(sourceFile),
41266
+ };
40914
41267
  }
40915
41268
  /**
40916
41269
  * Counts top-level entities in a parsed TypeScript source file.
@@ -40944,6 +41297,169 @@ function countEntitiesInSourceFile(sourceFile) {
40944
41297
  }
40945
41298
  return count;
40946
41299
  }
41300
+ /**
41301
+ * Summarizes named functions and methods in a parsed source file.
41302
+ *
41303
+ * @private function of analyzeSourceFileForRefactorCandidate
41304
+ */
41305
+ function summarizeFunctionsInSourceFile(sourceFile) {
41306
+ let functionCount = 0;
41307
+ let maxFunctionComplexity = 0;
41308
+ let mostComplexFunctionName = null;
41309
+ const visitNode = (node) => {
41310
+ if (isCountedFunctionLikeDeclaration(node)) {
41311
+ functionCount += 1;
41312
+ const functionComplexity = calculateFunctionComplexity(node);
41313
+ if (functionComplexity > maxFunctionComplexity) {
41314
+ maxFunctionComplexity = functionComplexity;
41315
+ mostComplexFunctionName = getFunctionDisplayName(node);
41316
+ }
41317
+ }
41318
+ ts.forEachChild(node, visitNode);
41319
+ };
41320
+ visitNode(sourceFile);
41321
+ return {
41322
+ functionCount,
41323
+ maxFunctionComplexity,
41324
+ mostComplexFunctionName,
41325
+ };
41326
+ }
41327
+ /**
41328
+ * Determines whether a node counts as a named function or method for density checks.
41329
+ *
41330
+ * @private function of analyzeSourceFileForRefactorCandidate
41331
+ */
41332
+ function isCountedFunctionLikeDeclaration(node) {
41333
+ if (ts.isFunctionDeclaration(node) ||
41334
+ ts.isMethodDeclaration(node) ||
41335
+ ts.isConstructorDeclaration(node) ||
41336
+ ts.isGetAccessorDeclaration(node) ||
41337
+ ts.isSetAccessorDeclaration(node)) {
41338
+ return true;
41339
+ }
41340
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
41341
+ return isNamedFunctionExpression(node);
41342
+ }
41343
+ return false;
41344
+ }
41345
+ /**
41346
+ * Determines whether a function expression is attached to a named variable or property.
41347
+ *
41348
+ * @private function of analyzeSourceFileForRefactorCandidate
41349
+ */
41350
+ function isNamedFunctionExpression(node) {
41351
+ const parent = node.parent;
41352
+ return (ts.isVariableDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isPropertyAssignment(parent));
41353
+ }
41354
+ /**
41355
+ * Calculates a lightweight cyclomatic-complexity score for one function.
41356
+ *
41357
+ * @private function of analyzeSourceFileForRefactorCandidate
41358
+ */
41359
+ function calculateFunctionComplexity(functionNode) {
41360
+ if (!functionNode.body) {
41361
+ return 1;
41362
+ }
41363
+ let complexity = 1;
41364
+ const visitNode = (node) => {
41365
+ if (node !== functionNode.body && isCountedFunctionLikeDeclaration(node)) {
41366
+ return;
41367
+ }
41368
+ if (isComplexityDecisionNode(node)) {
41369
+ complexity += 1;
41370
+ }
41371
+ ts.forEachChild(node, visitNode);
41372
+ };
41373
+ visitNode(functionNode.body);
41374
+ return complexity;
41375
+ }
41376
+ /**
41377
+ * Determines whether a node should increase the complexity score.
41378
+ *
41379
+ * @private function of analyzeSourceFileForRefactorCandidate
41380
+ */
41381
+ function isComplexityDecisionNode(node) {
41382
+ if (ts.isIfStatement(node) ||
41383
+ ts.isConditionalExpression(node) ||
41384
+ ts.isCatchClause(node) ||
41385
+ ts.isForStatement(node) ||
41386
+ ts.isForInStatement(node) ||
41387
+ ts.isForOfStatement(node) ||
41388
+ ts.isWhileStatement(node) ||
41389
+ ts.isDoStatement(node) ||
41390
+ ts.isCaseClause(node)) {
41391
+ return true;
41392
+ }
41393
+ if (ts.isBinaryExpression(node)) {
41394
+ const operatorKind = node.operatorToken.kind;
41395
+ return (operatorKind === ts.SyntaxKind.AmpersandAmpersandToken ||
41396
+ operatorKind === ts.SyntaxKind.BarBarToken ||
41397
+ operatorKind === ts.SyntaxKind.QuestionQuestionToken);
41398
+ }
41399
+ return false;
41400
+ }
41401
+ /**
41402
+ * Resolves a readable display name for a counted function-like declaration.
41403
+ *
41404
+ * @private function of analyzeSourceFileForRefactorCandidate
41405
+ */
41406
+ function getFunctionDisplayName(functionNode) {
41407
+ if (ts.isConstructorDeclaration(functionNode)) {
41408
+ return 'constructor';
41409
+ }
41410
+ if (ts.isFunctionDeclaration(functionNode) ||
41411
+ ts.isMethodDeclaration(functionNode) ||
41412
+ ts.isGetAccessorDeclaration(functionNode) ||
41413
+ ts.isSetAccessorDeclaration(functionNode)) {
41414
+ if (!functionNode.name) {
41415
+ return null;
41416
+ }
41417
+ return getPropertyNameText(functionNode.name);
41418
+ }
41419
+ if (ts.isArrowFunction(functionNode) || ts.isFunctionExpression(functionNode)) {
41420
+ if (functionNode.name) {
41421
+ return functionNode.name.text;
41422
+ }
41423
+ const parent = functionNode.parent;
41424
+ if (ts.isVariableDeclaration(parent)) {
41425
+ return getBindingNameText(parent.name);
41426
+ }
41427
+ if (ts.isPropertyDeclaration(parent) || ts.isPropertyAssignment(parent)) {
41428
+ return getPropertyNameText(parent.name);
41429
+ }
41430
+ }
41431
+ return null;
41432
+ }
41433
+ /**
41434
+ * Resolves text for a binding name when it is a simple identifier.
41435
+ *
41436
+ * @private function of analyzeSourceFileForRefactorCandidate
41437
+ */
41438
+ function getBindingNameText(name) {
41439
+ return ts.isIdentifier(name) ? name.text : null;
41440
+ }
41441
+ /**
41442
+ * Resolves text for a property name while preserving computed names when necessary.
41443
+ *
41444
+ * @private function of analyzeSourceFileForRefactorCandidate
41445
+ */
41446
+ function getPropertyNameText(name) {
41447
+ if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
41448
+ return name.text;
41449
+ }
41450
+ return name.getText();
41451
+ }
41452
+ /**
41453
+ * Formats the reason emitted when a function in the file exceeds the complexity threshold.
41454
+ *
41455
+ * @private function of analyzeSourceFileForRefactorCandidate
41456
+ */
41457
+ function buildComplexityReason(structureSummary, maxAllowedFunctionComplexity) {
41458
+ const functionSuffix = structureSummary.mostComplexFunctionName
41459
+ ? ` in \`${structureSummary.mostComplexFunctionName}\``
41460
+ : '';
41461
+ return `complexity ${structureSummary.maxFunctionComplexity}/${maxAllowedFunctionComplexity}${functionSuffix}`;
41462
+ }
40947
41463
  /**
40948
41464
  * Resolves the script kind for a source file extension.
40949
41465
  *
@@ -40977,13 +41493,15 @@ function normalizeAbsolutePath$1(pathValue) {
40977
41493
  *
40978
41494
  * @private function of findRefactorCandidates
40979
41495
  */
40980
- async function findRefactorCandidatesInProject(rootDir) {
40981
- const lineCountExemptPaths = await buildExemptPathSet(rootDir, LINE_COUNT_EXEMPT_GLOBS);
40982
- const sourceFiles = await listSourceFiles(rootDir);
41496
+ async function findRefactorCandidatesInProject(options) {
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);
40983
41500
  const candidates = [];
40984
41501
  for (const filePath of sourceFiles) {
40985
41502
  const candidate = await analyzeSourceFileForRefactorCandidate({
40986
41503
  filePath,
41504
+ heuristics,
40987
41505
  lineCountExemptPaths,
40988
41506
  rootDir,
40989
41507
  });
@@ -40998,7 +41516,7 @@ async function findRefactorCandidatesInProject(rootDir) {
40998
41516
  *
40999
41517
  * @private function of findRefactorCandidatesInProject
41000
41518
  */
41001
- async function listSourceFiles(rootDir) {
41519
+ async function listSourceFiles(rootDir, isIgnoredRelativePath) {
41002
41520
  const extensions = SOURCE_FILE_EXTENSIONS.map((extension) => extension.replace(/^\./, '')).join(',');
41003
41521
  const extensionGlob = `{${extensions}}`;
41004
41522
  const patterns = [...SOURCE_ROOTS.map((root) => `${root}/**/*.${extensionGlob}`), `*.${extensionGlob}`];
@@ -41011,6 +41529,9 @@ async function listSourceFiles(rootDir) {
41011
41529
  absolute: true,
41012
41530
  });
41013
41531
  for (const match of matches) {
41532
+ if (shouldIgnoreAbsolutePath(rootDir, match, isIgnoredRelativePath)) {
41533
+ continue;
41534
+ }
41014
41535
  files.add(match);
41015
41536
  }
41016
41537
  }
@@ -41021,7 +41542,7 @@ async function listSourceFiles(rootDir) {
41021
41542
  *
41022
41543
  * @private function of findRefactorCandidatesInProject
41023
41544
  */
41024
- async function buildExemptPathSet(rootDir, patterns) {
41545
+ async function buildExemptPathSet(rootDir, patterns, isIgnoredRelativePath) {
41025
41546
  const exemptPaths = new Set();
41026
41547
  for (const pattern of patterns) {
41027
41548
  const matches = await glob(pattern, {
@@ -41031,11 +41552,23 @@ async function buildExemptPathSet(rootDir, patterns) {
41031
41552
  absolute: true,
41032
41553
  });
41033
41554
  for (const match of matches) {
41555
+ if (shouldIgnoreAbsolutePath(rootDir, match, isIgnoredRelativePath)) {
41556
+ continue;
41557
+ }
41034
41558
  exemptPaths.add(normalizeAbsolutePath(match));
41035
41559
  }
41036
41560
  }
41037
41561
  return exemptPaths;
41038
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
+ }
41039
41572
  /**
41040
41573
  * Normalizes an absolute path for consistent comparisons.
41041
41574
  *
@@ -41084,6 +41617,69 @@ function escapeRegExp(value) {
41084
41617
  }
41085
41618
  // Note: [🟡] Code for repository script [loadExistingPromptTargets](scripts/find-refactor-candidates/loadExistingPromptTargets.ts) should never be published outside of `@promptbook/cli`
41086
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
+
41087
41683
  /**
41088
41684
  * Calculates the next available prompt numbering sequence for a month.
41089
41685
  */
@@ -41256,11 +41852,18 @@ function buildPromptGuidance(candidate) {
41256
41852
  if (counts.entityCount !== null && counts.maxEntities !== null) {
41257
41853
  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.`);
41258
41854
  }
41855
+ if (counts.functionCount !== null && counts.maxFunctions !== null) {
41856
+ 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.`);
41857
+ }
41858
+ if (counts.functionComplexity !== null && counts.maxFunctionComplexity !== null) {
41859
+ const functionSuffix = counts.mostComplexFunctionName ? ` in \`${counts.mostComplexFunctionName}\`` : '';
41860
+ 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.`);
41861
+ }
41259
41862
  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.');
41260
41863
  return guidance;
41261
41864
  }
41262
41865
  /**
41263
- * Extracts line and entity counts from refactor reasons.
41866
+ * Extracts structural counts from refactor reasons.
41264
41867
  *
41265
41868
  * @private function of buildPromptContent
41266
41869
  */
@@ -41269,6 +41872,11 @@ function extractReasonCounts(reasons) {
41269
41872
  let maxLines = null;
41270
41873
  let entityCount = null;
41271
41874
  let maxEntities = null;
41875
+ let functionCount = null;
41876
+ let maxFunctions = null;
41877
+ let functionComplexity = null;
41878
+ let maxFunctionComplexity = null;
41879
+ let mostComplexFunctionName = null;
41272
41880
  for (const reason of reasons) {
41273
41881
  const lineMatch = reason.match(/lines\s+(?<count>\d+)\/(?<max>\d+)/i);
41274
41882
  if (lineMatch === null || lineMatch === void 0 ? void 0 : lineMatch.groups) {
@@ -41280,6 +41888,19 @@ function extractReasonCounts(reasons) {
41280
41888
  if (entityMatch === null || entityMatch === void 0 ? void 0 : entityMatch.groups) {
41281
41889
  entityCount = Number(entityMatch.groups.count);
41282
41890
  maxEntities = Number(entityMatch.groups.max);
41891
+ continue;
41892
+ }
41893
+ const functionMatch = reason.match(/functions\s+(?<count>\d+)\/(?<max>\d+)/i);
41894
+ if (functionMatch === null || functionMatch === void 0 ? void 0 : functionMatch.groups) {
41895
+ functionCount = Number(functionMatch.groups.count);
41896
+ maxFunctions = Number(functionMatch.groups.max);
41897
+ continue;
41898
+ }
41899
+ const complexityMatch = reason.match(/complexity\s+(?<count>\d+)\/(?<max>\d+)(?:\s+in\s+`(?<functionName>[^`]+)`)?/i);
41900
+ if (complexityMatch === null || complexityMatch === void 0 ? void 0 : complexityMatch.groups) {
41901
+ functionComplexity = Number(complexityMatch.groups.count);
41902
+ maxFunctionComplexity = Number(complexityMatch.groups.max);
41903
+ mostComplexFunctionName = complexityMatch.groups.functionName || null;
41283
41904
  }
41284
41905
  }
41285
41906
  return {
@@ -41287,6 +41908,11 @@ function extractReasonCounts(reasons) {
41287
41908
  maxLines,
41288
41909
  entityCount,
41289
41910
  maxEntities,
41911
+ functionCount,
41912
+ maxFunctions,
41913
+ functionComplexity,
41914
+ maxFunctionComplexity,
41915
+ mostComplexFunctionName,
41290
41916
  };
41291
41917
  }
41292
41918
  /**
@@ -41295,14 +41921,23 @@ function extractReasonCounts(reasons) {
41295
41921
  * @private function of buildPromptContent
41296
41922
  */
41297
41923
  function buildDensityNote(counts) {
41298
- if (counts.lineCount !== null && counts.entityCount !== null) {
41299
- return 'The file mixes multiple concerns, making it harder to follow.';
41924
+ const activeSignalsCount = [
41925
+ counts.lineCount !== null,
41926
+ counts.entityCount !== null,
41927
+ counts.functionCount !== null,
41928
+ counts.functionComplexity !== null,
41929
+ ].filter(Boolean).length;
41930
+ if (activeSignalsCount > 1) {
41931
+ return 'The file mixes multiple concerns and dense logic, making it harder to follow.';
41300
41932
  }
41301
41933
  if (counts.lineCount !== null) {
41302
41934
  return 'The file is large enough that it is hard to follow.';
41303
41935
  }
41304
- if (counts.entityCount !== null) {
41305
- return 'The file is dense enough that it is hard to follow.';
41936
+ if (counts.entityCount !== null || counts.functionCount !== null) {
41937
+ return 'The file packs too many responsibilities into one place.';
41938
+ }
41939
+ if (counts.functionComplexity !== null) {
41940
+ return 'The file contains logic that is too complex to follow comfortably.';
41306
41941
  }
41307
41942
  return null;
41308
41943
  }
@@ -41396,13 +42031,20 @@ function initializeFindRefactorCandidatesRun() {
41396
42031
  *
41397
42032
  * @public exported from `@promptbook/cli`
41398
42033
  */
41399
- async function findRefactorCandidates() {
42034
+ async function findRefactorCandidates(options = {}) {
42035
+ const { level = DEFAULT_REFACTOR_CANDIDATE_LEVEL } = options;
42036
+ const heuristics = getRefactorCandidateLevelConfiguration(level);
41400
42037
  initializeFindRefactorCandidatesRun();
41401
- console.info(colors.cyan('?? Find refactor candidates'));
41402
- const rootDir = process.cwd();
42038
+ console.info(colors.cyan('⚡🏭 Find refactor candidates'));
42039
+ console.info(colors.gray(`Using \`${level}\` scan level.`));
42040
+ const { isIgnoredRelativePath, rootDir } = await resolveRefactorCandidateProject(process.cwd());
41403
42041
  const promptsDir = join(rootDir, PROMPTS_DIR_NAME);
41404
42042
  const existingTargets = await loadExistingPromptTargets(promptsDir);
41405
- const candidates = await findRefactorCandidatesInProject(rootDir);
42043
+ const candidates = await findRefactorCandidatesInProject({
42044
+ heuristics,
42045
+ isIgnoredRelativePath,
42046
+ rootDir,
42047
+ });
41406
42048
  if (candidates.length === 0) {
41407
42049
  console.info(colors.green('No refactor candidates found.'));
41408
42050
  return;