gaunt-sloth-assistant 0.1.5 → 0.2.2

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 (128) hide show
  1. package/.prettierrc.json +9 -0
  2. package/README.md +177 -158
  3. package/ROADMAP.md +1 -1
  4. package/dist/commands/askCommand.d.ts +6 -0
  5. package/dist/commands/askCommand.js +26 -0
  6. package/dist/commands/askCommand.js.map +1 -0
  7. package/dist/commands/initCommand.d.ts +6 -0
  8. package/dist/commands/initCommand.js +16 -0
  9. package/dist/commands/initCommand.js.map +1 -0
  10. package/dist/commands/reviewCommand.d.ts +3 -0
  11. package/dist/commands/reviewCommand.js +128 -0
  12. package/dist/commands/reviewCommand.js.map +1 -0
  13. package/dist/config.d.ts +80 -0
  14. package/dist/config.js +178 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/configs/anthropic.d.ts +5 -0
  17. package/{src → dist}/configs/anthropic.js +45 -48
  18. package/dist/configs/anthropic.js.map +1 -0
  19. package/dist/configs/fake.d.ts +3 -0
  20. package/{src → dist}/configs/fake.js +11 -14
  21. package/dist/configs/fake.js.map +1 -0
  22. package/dist/configs/groq.d.ts +4 -0
  23. package/{src → dist}/configs/groq.js +10 -13
  24. package/dist/configs/groq.js.map +1 -0
  25. package/dist/configs/types.d.ts +14 -0
  26. package/dist/configs/types.js +2 -0
  27. package/dist/configs/types.js.map +1 -0
  28. package/dist/configs/vertexai.d.ts +4 -0
  29. package/{src → dist}/configs/vertexai.js +44 -47
  30. package/dist/configs/vertexai.js.map +1 -0
  31. package/dist/consoleUtils.d.ts +6 -0
  32. package/{src → dist}/consoleUtils.js +10 -15
  33. package/dist/consoleUtils.js.map +1 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +17 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/modules/questionAnsweringModule.d.ts +18 -0
  38. package/{src → dist}/modules/questionAnsweringModule.js +72 -82
  39. package/dist/modules/questionAnsweringModule.js.map +1 -0
  40. package/dist/modules/reviewModule.d.ts +4 -0
  41. package/{src → dist}/modules/reviewModule.js +25 -35
  42. package/dist/modules/reviewModule.js.map +1 -0
  43. package/dist/modules/types.d.ts +18 -0
  44. package/dist/modules/types.js +2 -0
  45. package/dist/modules/types.js.map +1 -0
  46. package/dist/prompt.d.ts +7 -0
  47. package/dist/prompt.js +32 -0
  48. package/dist/prompt.js.map +1 -0
  49. package/dist/providers/file.d.ts +8 -0
  50. package/dist/providers/file.js +20 -0
  51. package/dist/providers/file.js.map +1 -0
  52. package/dist/providers/ghPrDiffProvider.d.ts +8 -0
  53. package/dist/providers/ghPrDiffProvider.js +16 -0
  54. package/dist/providers/ghPrDiffProvider.js.map +1 -0
  55. package/dist/providers/jiraIssueLegacyAccessTokenProvider.d.ts +8 -0
  56. package/dist/providers/jiraIssueLegacyAccessTokenProvider.js +62 -0
  57. package/dist/providers/jiraIssueLegacyAccessTokenProvider.js.map +1 -0
  58. package/dist/providers/jiraIssueLegacyProvider.d.ts +8 -0
  59. package/dist/providers/jiraIssueLegacyProvider.js +74 -0
  60. package/dist/providers/jiraIssueLegacyProvider.js.map +1 -0
  61. package/dist/providers/jiraIssueProvider.d.ts +11 -0
  62. package/dist/providers/jiraIssueProvider.js +96 -0
  63. package/dist/providers/jiraIssueProvider.js.map +1 -0
  64. package/dist/providers/text.d.ts +8 -0
  65. package/dist/providers/text.js +10 -0
  66. package/dist/providers/text.js.map +1 -0
  67. package/dist/providers/types.d.ts +21 -0
  68. package/dist/providers/types.js +2 -0
  69. package/dist/providers/types.js.map +1 -0
  70. package/dist/systemUtils.d.ts +22 -0
  71. package/dist/systemUtils.js +36 -0
  72. package/dist/systemUtils.js.map +1 -0
  73. package/dist/utils.d.ts +49 -0
  74. package/{src → dist}/utils.js +73 -60
  75. package/dist/utils.js.map +1 -0
  76. package/docs/CONFIGURATION.md +95 -6
  77. package/docs/RELEASE-HOWTO.md +1 -1
  78. package/eslint.config.js +99 -21
  79. package/index.js +10 -27
  80. package/package.json +26 -15
  81. package/src/commands/askCommand.ts +34 -0
  82. package/src/commands/initCommand.ts +19 -0
  83. package/src/commands/reviewCommand.ts +209 -0
  84. package/src/config.ts +266 -0
  85. package/src/configs/anthropic.ts +55 -0
  86. package/src/configs/fake.ts +15 -0
  87. package/src/configs/groq.ts +54 -0
  88. package/src/configs/vertexai.ts +53 -0
  89. package/src/consoleUtils.ts +33 -0
  90. package/src/index.ts +21 -0
  91. package/src/modules/questionAnsweringModule.ts +97 -0
  92. package/src/modules/reviewModule.ts +81 -0
  93. package/src/modules/types.ts +23 -0
  94. package/src/prompt.ts +39 -0
  95. package/src/providers/file.ts +24 -0
  96. package/src/providers/ghPrDiffProvider.ts +20 -0
  97. package/src/providers/jiraIssueLegacyProvider.ts +103 -0
  98. package/src/providers/jiraIssueProvider.ts +133 -0
  99. package/src/providers/text.ts +14 -0
  100. package/src/providers/types.ts +24 -0
  101. package/src/systemUtils.ts +52 -0
  102. package/src/utils.ts +225 -0
  103. package/tsconfig.json +24 -0
  104. package/vitest.config.ts +13 -0
  105. package/.eslint.config.mjs +0 -72
  106. package/.github/dependabot.yml +0 -11
  107. package/.github/workflows/ci.yml +0 -33
  108. package/spec/.gsloth.config.js +0 -22
  109. package/spec/.gsloth.config.json +0 -25
  110. package/spec/askCommand.spec.js +0 -92
  111. package/spec/config.spec.js +0 -421
  112. package/spec/initCommand.spec.js +0 -55
  113. package/spec/predefinedConfigs.spec.js +0 -100
  114. package/spec/questionAnsweringModule.spec.js +0 -137
  115. package/spec/reviewCommand.spec.js +0 -222
  116. package/spec/reviewModule.spec.js +0 -28
  117. package/spec/support/jasmine.mjs +0 -14
  118. package/src/commands/askCommand.js +0 -27
  119. package/src/commands/initCommand.js +0 -17
  120. package/src/commands/reviewCommand.js +0 -154
  121. package/src/config.js +0 -177
  122. package/src/prompt.js +0 -34
  123. package/src/providers/file.js +0 -19
  124. package/src/providers/ghPrDiffProvider.js +0 -11
  125. package/src/providers/jiraIssueLegacyAccessTokenProvider.js +0 -84
  126. package/src/providers/text.js +0 -6
  127. package/src/systemUtils.js +0 -32
  128. /package/{.gsloth.preamble.internal.md → .gsloth.backstory.md} +0 -0
@@ -0,0 +1,23 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+
3
+ export type Message = BaseMessage;
4
+
5
+ export interface State {
6
+ messages: Message[];
7
+ }
8
+
9
+ export interface ProgressCallback {
10
+ (): void;
11
+ }
12
+
13
+ export interface ReviewOptions {
14
+ source: string;
15
+ preamble: string;
16
+ diff: string;
17
+ }
18
+
19
+ export interface QuestionOptions {
20
+ source: string;
21
+ preamble: string;
22
+ content: string;
23
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { resolve } from 'node:path';
2
+ import { GSLOTH_BACKSTORY } from '#src/config.js';
3
+ import { readFileSyncWithMessages, spawnCommand } from '#src/utils.js';
4
+ import { displayError } from '#src/consoleUtils.js';
5
+ import { exit, getCurrentDir, getInstallDir } from '#src/systemUtils.js';
6
+
7
+ export function readInternalPreamble(): string {
8
+ const installDir = getInstallDir();
9
+ const filePath = resolve(installDir, GSLOTH_BACKSTORY);
10
+ return readFileSyncWithMessages(filePath, 'Error reading internal preamble file at:') || '';
11
+ }
12
+
13
+ export function readPreamble(preambleFilename: string): string {
14
+ const currentDir = getCurrentDir();
15
+ const filePath = resolve(currentDir, preambleFilename);
16
+ return (
17
+ readFileSyncWithMessages(
18
+ filePath,
19
+ 'Error reading preamble file at:',
20
+ 'Consider running `gsloth init` to set up your project. Check `gsloth init --help` to see options.'
21
+ ) || ''
22
+ );
23
+ }
24
+
25
+ /**
26
+ * This function expects https://cli.github.com/ to be installed and authenticated.
27
+ * It does something like `gh pr diff 42`
28
+ */
29
+ export async function getPrDiff(pr: string): Promise<string> {
30
+ // TODO makes sense to check if gh is available and authenticated
31
+ try {
32
+ return await spawnCommand('gh', ['pr', 'diff', pr], 'Loading PR diff...', 'Loaded PR diff.');
33
+ } catch (e) {
34
+ displayError(e instanceof Error ? e.toString() : String(e));
35
+ displayError(`Failed to call "gh pr diff ${pr}", see message above for details.`);
36
+ exit();
37
+ return ''; // This line will never be reached due to exit()
38
+ }
39
+ }
@@ -0,0 +1,24 @@
1
+ import { resolve } from 'node:path';
2
+ import { display } from '#src/consoleUtils.js';
3
+ import { readFileSyncWithMessages } from '#src/utils.js';
4
+ import { getCurrentDir } from '#src/systemUtils.js';
5
+ import type { ProviderConfig } from './types.js';
6
+
7
+ /**
8
+ * Reads the text file from current dir
9
+ * @param _ config (unused in this provider)
10
+ * @param fileName
11
+ * @returns file contents
12
+ */
13
+ export async function get(
14
+ _: ProviderConfig | null,
15
+ fileName: string | undefined
16
+ ): Promise<string | null> {
17
+ if (!fileName) {
18
+ return null;
19
+ }
20
+ const currentDir = getCurrentDir();
21
+ const filePath = resolve(currentDir, fileName);
22
+ display(`Reading file ${fileName}...`);
23
+ return readFileSyncWithMessages(filePath);
24
+ }
@@ -0,0 +1,20 @@
1
+ import { displayWarning } from '#src/consoleUtils.js';
2
+ import { execAsync } from '#src/utils.js';
3
+ import type { ProviderConfig } from './types.js';
4
+
5
+ /**
6
+ * Gets PR diff using gh command line tool
7
+ * @param _ config (unused in this provider)
8
+ * @param pr PR number
9
+ * @returns PR diff
10
+ */
11
+ export async function get(
12
+ _: ProviderConfig | null,
13
+ pr: string | undefined
14
+ ): Promise<string | null> {
15
+ if (!pr) {
16
+ displayWarning('No PR provided');
17
+ return null;
18
+ }
19
+ return execAsync(`gh pr diff ${pr}`);
20
+ }
@@ -0,0 +1,103 @@
1
+ import { display, displayError, displayWarning } from '#src/consoleUtils.js';
2
+ import { env } from '#src/systemUtils.js';
3
+ import type { JiraLegacyConfig } from '#src/providers/types.js';
4
+
5
+ interface JiraIssueResponse {
6
+ fields: {
7
+ summary: string;
8
+ description: string;
9
+ [key: string]: unknown;
10
+ };
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ /**
15
+ * Gets Jira issue using Atlassian REST API v2 with unscoped API token (aka Legacy Token)
16
+ * @param config Jira configuration
17
+ * @param issueId Jira issue ID
18
+ * @returns Jira issue content
19
+ */
20
+ export async function get(
21
+ config: Partial<JiraLegacyConfig>,
22
+ issueId: string | undefined
23
+ ): Promise<string | null> {
24
+ if (!config) {
25
+ displayWarning('No Jira config provided');
26
+ return null;
27
+ }
28
+ if (!issueId) {
29
+ displayWarning('No issue ID provided');
30
+ return null;
31
+ }
32
+ if (!config.baseUrl) {
33
+ displayWarning('No Jira base URL provided');
34
+ return null;
35
+ }
36
+
37
+ // Get username from environment variable or config
38
+ const username = env.JIRA_USERNAME || config.username;
39
+ if (!username) {
40
+ throw new Error(
41
+ 'Missing JIRA username. The username can be defined as JIRA_USERNAME environment variable or as "username" in config.'
42
+ );
43
+ }
44
+
45
+ // Get token from environment variable or config
46
+ const token = env.JIRA_LEGACY_API_TOKEN || config.token;
47
+ if (!token) {
48
+ throw new Error(
49
+ 'Missing JIRA Legacy API token. The legacy token can be defined as JIRA_LEGACY_API_TOKEN environment variable or as "token" in config.'
50
+ );
51
+ }
52
+
53
+ try {
54
+ const issue = await getJiraIssue(
55
+ {
56
+ ...(config as JiraLegacyConfig),
57
+ username,
58
+ token,
59
+ },
60
+ issueId
61
+ );
62
+ if (!issue) {
63
+ return null;
64
+ }
65
+
66
+ const summary = issue.fields.summary;
67
+ const description = issue.fields.description;
68
+
69
+ return `Jira Issue: ${issueId}\nSummary: ${summary}\n\nDescription:\n${description}`;
70
+ } catch (error) {
71
+ displayError(
72
+ `Failed to get Jira issue: ${error instanceof Error ? error.message : String(error)}`
73
+ );
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Helper function to get Jira issue details
80
+ * @param config Jira configuration
81
+ * @param issueId Jira issue ID
82
+ * @returns Jira issue response
83
+ */
84
+ async function getJiraIssue(config: JiraLegacyConfig, issueId: string): Promise<JiraIssueResponse> {
85
+ const auth = Buffer.from(`${config.username}:${config.token}`).toString('base64');
86
+ const url = `${config.baseUrl}${issueId}`;
87
+ if (config.displayUrl) {
88
+ display(`Loading Jira issue ${config.displayUrl}${issueId}`);
89
+ }
90
+ display(`Retrieving jira from api ${url.replace(/^https?:\/\//, '')}`);
91
+ const response = await fetch(url, {
92
+ headers: {
93
+ Authorization: `Basic ${auth}`,
94
+ Accept: 'application/json',
95
+ },
96
+ });
97
+
98
+ if (!response.ok) {
99
+ throw new Error(`Failed to fetch Jira issue from ${url} with status: ${response.statusText}`);
100
+ }
101
+
102
+ return response.json();
103
+ }
@@ -0,0 +1,133 @@
1
+ import { display, displayError, displayWarning } from '#src/consoleUtils.js';
2
+ import { env } from '#src/systemUtils.js';
3
+ import type { JiraConfig } from './types.js';
4
+
5
+ interface JiraIssueResponse {
6
+ fields: {
7
+ summary: string;
8
+ description: string;
9
+ [key: string]: unknown;
10
+ };
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ /**
15
+ * Gets Jira issue using Atlassian REST API v3 with Personal Access Token
16
+ *
17
+ * TODO we need to figure out how would this work with public jira.
18
+ *
19
+ * @param config Jira configuration
20
+ * @param issueId Jira issue ID
21
+ * @returns Jira issue content
22
+ */
23
+ export async function get(
24
+ config: Partial<JiraConfig> | null,
25
+ issueId: string | undefined
26
+ ): Promise<string | null> {
27
+ if (!config) {
28
+ displayWarning('No Jira config provided');
29
+ return null;
30
+ }
31
+ if (!issueId) {
32
+ displayWarning('No issue ID provided');
33
+ return null;
34
+ }
35
+
36
+ // Get username from environment variable or config
37
+ const username = env.JIRA_USERNAME || config.username;
38
+ if (!username) {
39
+ throw new Error(
40
+ 'Missing JIRA username. The username can be defined as JIRA_USERNAME environment variable or as "username" in config.'
41
+ );
42
+ }
43
+
44
+ // Get token from environment variable or config
45
+ const token = env.JIRA_API_PAT_TOKEN || config.token;
46
+ if (!token) {
47
+ throw new Error(
48
+ 'Missing JIRA PAT token. The token can be defined as JIRA_API_PAT_TOKEN environment variable or as "token" in config.'
49
+ );
50
+ }
51
+
52
+ // Get cloud ID from environment variable or config
53
+ const cloudId = env.JIRA_CLOUD_ID || config.cloudId;
54
+ if (!cloudId) {
55
+ throw new Error(
56
+ 'Missing JIRA Cloud ID. The Cloud ID can be defined as JIRA_CLOUD_ID environment variable or as "cloudId" in config.'
57
+ );
58
+ }
59
+
60
+ try {
61
+ const issue = await getJiraIssue(
62
+ {
63
+ ...config,
64
+ username,
65
+ token,
66
+ cloudId,
67
+ },
68
+ issueId
69
+ );
70
+ if (!issue) {
71
+ return null;
72
+ }
73
+
74
+ const summary = issue.fields.summary;
75
+ const description = issue.fields.description;
76
+
77
+ return `Jira Issue: ${issueId}\nSummary: ${summary}\n\nDescription:\n${description}`;
78
+ } catch (error) {
79
+ displayError(
80
+ `Failed to get Jira issue: ${error instanceof Error ? error.message : String(error)}`
81
+ );
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Helper function to get Jira issue details using Atlassian REST API v3
88
+ * @param config Jira configuration
89
+ * @param jiraKey Jira issue ID
90
+ * @returns Jira issue response
91
+ */
92
+ async function getJiraIssue(config: JiraConfig, jiraKey: string): Promise<JiraIssueResponse> {
93
+ // Jira Cloud ID can be found by authenticated user at https://company.atlassian.net/_edge/tenant_info
94
+
95
+ // According to doc https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-get permissions to read this resource:
96
+ // either Classic (RECOMMENDED) read:jira-work
97
+ // or Granular read:issue-meta:jira, read:issue-security-level:jira, read:issue.vote:jira, read:issue.changelog:jira, read:avatar:jira, read:issue:jira, read:status:jira, read:user:jira, read:field-configuration:jira
98
+ const apiUrl = `https://api.atlassian.com/ex/jira/${config.cloudId}/rest/api/3/issue/${jiraKey}`;
99
+ if (config.displayUrl) {
100
+ display(`Loading Jira issue ${config.displayUrl}${jiraKey}`);
101
+ }
102
+ display(`Retrieving jira from api ${apiUrl.replace(/^https?:\/\//, '')}`);
103
+
104
+ // Encode credentials for Basic Authentication header
105
+ const credentials = `${config.username}:${config.token}`;
106
+ const encodedCredentials = Buffer.from(credentials).toString('base64');
107
+ const authHeader = `Basic ${encodedCredentials}`;
108
+
109
+ // Define request headers
110
+ const headers = {
111
+ Authorization: authHeader,
112
+ Accept: 'application/json; charset=utf-8',
113
+ 'Accept-Language': 'en-US,en;q=0.9', // Prevents errors in other languages
114
+ };
115
+
116
+ const response = await fetch(apiUrl, {
117
+ method: 'GET',
118
+ headers: headers,
119
+ });
120
+
121
+ if (!response?.ok) {
122
+ try {
123
+ const errorData = await response.json();
124
+ throw new Error(
125
+ `Failed to fetch Jira issue: ${response.statusText} - ${JSON.stringify(errorData)}`
126
+ );
127
+ } catch (_e) {
128
+ throw new Error(`Failed to fetch Jira issue: ${response?.statusText}`);
129
+ }
130
+ }
131
+
132
+ return response.json();
133
+ }
@@ -0,0 +1,14 @@
1
+ import type { ProviderConfig } from './types.js';
2
+
3
+ /**
4
+ * Returns the provided text as is
5
+ * @param _ config (unused in this provider)
6
+ * @param text Text to return
7
+ * @returns The provided text
8
+ */
9
+ export async function get(
10
+ _: ProviderConfig | null,
11
+ text: string | undefined
12
+ ): Promise<string | null> {
13
+ return text ?? null;
14
+ }
@@ -0,0 +1,24 @@
1
+ export interface ProviderConfig {
2
+ username?: string;
3
+ token?: string;
4
+ baseUrl?: string;
5
+ [key: string]: unknown;
6
+ }
7
+
8
+ export interface JiraLegacyConfig extends ProviderConfig {
9
+ username: string;
10
+ baseUrl: string;
11
+ displayUrl?: string;
12
+ token: string;
13
+ }
14
+
15
+ export interface JiraConfig extends ProviderConfig {
16
+ cloudId: string;
17
+ username: string;
18
+ displayUrl?: string;
19
+ token: string;
20
+ }
21
+
22
+ export interface Provider {
23
+ get: (config: ProviderConfig | null, id: string | undefined) => Promise<string | null>;
24
+ }
@@ -0,0 +1,52 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ /**
5
+ * This file contains all system functions and objects that are globally available
6
+ * but not imported directly, such as process.stdin, process.stdout, process.argv,
7
+ * process.env, process.cwd(), process.exit(), etc.
8
+ *
9
+ * By centralizing these in one file, we improve testability and make it easier
10
+ * to mock these dependencies in tests.
11
+ */
12
+
13
+ interface InnerState {
14
+ installDir: string | null | undefined;
15
+ }
16
+
17
+ const innerState: InnerState = {
18
+ installDir: undefined,
19
+ };
20
+
21
+ // Process-related functions and objects
22
+ export const getCurrentDir = (): string => process.cwd();
23
+ export const getInstallDir = (): string => {
24
+ if (innerState.installDir) {
25
+ return innerState.installDir;
26
+ }
27
+ throw new Error('Install directory not set');
28
+ };
29
+ export const exit = (code?: number): never => process.exit(code || 0);
30
+ export const stdin = process.stdin;
31
+ export const stdout = process.stdout;
32
+ export const argv = process.argv;
33
+ export const env = process.env;
34
+
35
+ // noinspection JSUnusedGlobalSymbols
36
+ /**
37
+ * Provide the path to the entry point of the application.
38
+ * This is used to set the install directory.
39
+ * This is called from index.js root entry point.
40
+ */
41
+ export const setEntryPoint = (indexJs: string): void => {
42
+ const filePath = fileURLToPath(indexJs);
43
+ const dirPath = dirname(filePath);
44
+ innerState.installDir = resolve(dirPath);
45
+ };
46
+
47
+ // Console-related functions
48
+ export const log = (message: string): void => console.log(message);
49
+ export const error = (message: string): void => console.error(message);
50
+ export const warn = (message: string): void => console.warn(message);
51
+ export const info = (message: string): void => console.info(message);
52
+ export const debug = (message: string): void => console.debug(message);
package/src/utils.ts ADDED
@@ -0,0 +1,225 @@
1
+ import { display, displayError, displaySuccess, displayWarning } from '#src/consoleUtils.js';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { SlothConfig, slothContext } from '#src/config.js';
4
+ import { resolve } from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { exit, stdin, stdout, argv, getCurrentDir, getInstallDir } from '#src/systemUtils.js';
7
+ import url from 'node:url';
8
+ import { Command } from 'commander';
9
+
10
+ export function toFileSafeString(string: string): string {
11
+ return string.replace(/[^A-Za-z0-9]/g, '-');
12
+ }
13
+
14
+ export function fileSafeLocalDate(): string {
15
+ const date = new Date();
16
+ const offsetMs = date.getTimezoneOffset() * 60 * 1000;
17
+ const msLocal = date.getTime() - offsetMs;
18
+ const dateLocal = new Date(msLocal);
19
+ const iso = dateLocal.toISOString();
20
+ const isoLocal = iso.slice(0, 19);
21
+ return toFileSafeString(isoLocal);
22
+ }
23
+
24
+ export function readFileFromCurrentDir(fileName: string): string {
25
+ const currentDir = getCurrentDir();
26
+ const filePath = resolve(currentDir, fileName);
27
+ display(`Reading file ${fileName}...`);
28
+ return readFileSyncWithMessages(filePath);
29
+ }
30
+
31
+ export function writeFileIfNotExistsWithMessages(filePath: string, content: string): void {
32
+ display(`checking ${filePath} existence`);
33
+ if (!existsSync(filePath)) {
34
+ writeFileSync(filePath, content);
35
+ displaySuccess(`Created ${filePath}`);
36
+ } else {
37
+ displayWarning(`${filePath} already exists`);
38
+ }
39
+ }
40
+
41
+ export function readFileSyncWithMessages(
42
+ filePath: string,
43
+ errorMessageIn?: string,
44
+ noFileMessage?: string
45
+ ): string {
46
+ const errorMessage = errorMessageIn ?? 'Error reading file at: ';
47
+ try {
48
+ return readFileSync(filePath, { encoding: 'utf8' });
49
+ } catch (error) {
50
+ displayError(errorMessage + filePath);
51
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
52
+ displayWarning(noFileMessage ?? 'Please ensure the file exists.');
53
+ } else {
54
+ displayError((error as Error).message);
55
+ }
56
+ exit(1); // Exit gracefully after error
57
+ throw error; // This line will never be reached due to exit(1), but satisfies TypeScript
58
+ }
59
+ }
60
+
61
+ export function readStdin(program: Command): Promise<void> {
62
+ return new Promise((resolvePromise) => {
63
+ if (stdin.isTTY) {
64
+ program.parseAsync().then(() => resolvePromise());
65
+ } else {
66
+ // Support piping diff into gsloth
67
+ const progressIndicator = new ProgressIndicator('reading STDIN');
68
+ progressIndicator.indicate();
69
+
70
+ stdin.on('readable', function (this: NodeJS.ReadStream) {
71
+ const chunk = this.read();
72
+ progressIndicator.indicate();
73
+ if (chunk !== null) {
74
+ const chunkStr = chunk.toString('utf8');
75
+ (slothContext as { stdin: string }).stdin = slothContext.stdin + chunkStr;
76
+ }
77
+ });
78
+
79
+ stdin.on('end', function () {
80
+ program.parseAsync(argv).then(() => resolvePromise());
81
+ });
82
+ }
83
+ });
84
+ }
85
+
86
+ interface SpawnOutput {
87
+ stdout: string;
88
+ stderr: string;
89
+ }
90
+
91
+ export async function spawnCommand(
92
+ command: string,
93
+ args: string[],
94
+ progressMessage: string,
95
+ successMessage: string
96
+ ): Promise<string> {
97
+ return new Promise((resolve, reject) => {
98
+ const progressIndicator = new ProgressIndicator(progressMessage);
99
+ const out: SpawnOutput = { stdout: '', stderr: '' };
100
+ const spawned = spawn(command, args);
101
+
102
+ spawned.stdout.on('data', async (stdoutChunk) => {
103
+ progressIndicator.indicate();
104
+ out.stdout += stdoutChunk.toString();
105
+ });
106
+
107
+ spawned.stderr.on('data', (err) => {
108
+ progressIndicator.indicate();
109
+ out.stderr += err.toString();
110
+ });
111
+
112
+ spawned.on('error', (err) => {
113
+ reject(err.toString());
114
+ });
115
+
116
+ spawned.on('close', (code) => {
117
+ if (code === 0) {
118
+ display(successMessage);
119
+ resolve(out.stdout);
120
+ } else {
121
+ displayError(`Failed to spawn command with code ${code}`);
122
+ reject(out.stdout + ' ' + out.stderr);
123
+ }
124
+ });
125
+ });
126
+ }
127
+
128
+ export function getSlothVersion(): string {
129
+ // TODO figure out if this can be injected with TS
130
+ const installDir = getInstallDir();
131
+ const jsonPath = resolve(installDir, 'package.json');
132
+ const projectJson = readFileSync(jsonPath, { encoding: 'utf8' });
133
+ return JSON.parse(projectJson).version;
134
+ }
135
+
136
+ export class ProgressIndicator {
137
+ private hasBeenCalled: boolean;
138
+ private initialMessage: string;
139
+
140
+ constructor(initialMessage: string) {
141
+ this.hasBeenCalled = false;
142
+ this.initialMessage = initialMessage;
143
+ }
144
+
145
+ indicate(): void {
146
+ if (this.hasBeenCalled) {
147
+ stdout.write('.');
148
+ } else {
149
+ this.hasBeenCalled = true;
150
+ stdout.write(this.initialMessage);
151
+ }
152
+ }
153
+ }
154
+
155
+ interface LLMOutput {
156
+ messages: Array<{
157
+ content: string;
158
+ }>;
159
+ }
160
+
161
+ /**
162
+ * Extracts the content of the last message from an LLM response
163
+ * @param output - The output from the LLM containing messages
164
+ * @returns The content of the last message
165
+ */
166
+ export function extractLastMessageContent(output: LLMOutput): string {
167
+ if (!output || !output.messages || !output.messages.length) {
168
+ return '';
169
+ }
170
+ return output.messages[output.messages.length - 1].content;
171
+ }
172
+
173
+ /**
174
+ * Dynamically imports a module from a file path from the outside of the installation dir
175
+ * @param filePath - The path to the file to import
176
+ * @returns A promise that resolves to the imported module
177
+ */
178
+ export function importExternalFile(
179
+ filePath: string
180
+ ): Promise<{ configure: (module: string) => Promise<Partial<SlothConfig>> }> {
181
+ const configFileUrl = url.pathToFileURL(filePath).toString();
182
+ return import(configFileUrl);
183
+ }
184
+
185
+ /**
186
+ * Alias for importExternalFile for backward compatibility with tests
187
+ * @param filePath - The path to the file to import
188
+ * @returns A promise that resolves to the imported module
189
+ */
190
+ export const importFromFilePath = importExternalFile;
191
+
192
+ /**
193
+ * Reads multiple files from the current directory and returns their contents
194
+ * @param fileNames - Array of file names to read
195
+ * @returns Combined content of all files with proper formatting
196
+ */
197
+ export function readMultipleFilesFromCurrentDir(fileNames: string | string[]): string {
198
+ if (!Array.isArray(fileNames)) {
199
+ return readFileFromCurrentDir(fileNames);
200
+ }
201
+
202
+ return fileNames
203
+ .map((fileName) => {
204
+ const content = readFileFromCurrentDir(fileName);
205
+ return `${fileName}:\n\`\`\`\n${content}\n\`\`\``;
206
+ })
207
+ .join('\n\n');
208
+ }
209
+
210
+ export async function execAsync(command: string): Promise<string> {
211
+ const { exec } = await import('node:child_process');
212
+ return new Promise((resolve, reject) => {
213
+ exec(command, (error, stdout, stderr) => {
214
+ if (error) {
215
+ reject(error);
216
+ return;
217
+ }
218
+ if (stderr) {
219
+ reject(new Error(stderr));
220
+ return;
221
+ }
222
+ resolve(stdout.trim());
223
+ });
224
+ });
225
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "declaration": true,
13
+ "sourceMap": true,
14
+ "typeRoots": ["./node_modules/@types", "./src/types"],
15
+ "allowJs": true,
16
+ "checkJs": false,
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "#src/*": ["./src/*"]
20
+ }
21
+ },
22
+ "include": ["src/**/*"],
23
+ "exclude": ["node_modules", "dist", "spec"]
24
+ }