disunday 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/ai-tool-to-genai.js +208 -0
  2. package/dist/ai-tool-to-genai.test.js +267 -0
  3. package/dist/channel-management.js +96 -0
  4. package/dist/cli.js +1674 -0
  5. package/dist/commands/abort.js +89 -0
  6. package/dist/commands/add-project.js +117 -0
  7. package/dist/commands/agent.js +250 -0
  8. package/dist/commands/ask-question.js +219 -0
  9. package/dist/commands/compact.js +126 -0
  10. package/dist/commands/context-menu.js +171 -0
  11. package/dist/commands/context.js +89 -0
  12. package/dist/commands/cost.js +93 -0
  13. package/dist/commands/create-new-project.js +111 -0
  14. package/dist/commands/diff.js +77 -0
  15. package/dist/commands/export.js +100 -0
  16. package/dist/commands/files.js +73 -0
  17. package/dist/commands/fork.js +199 -0
  18. package/dist/commands/help.js +54 -0
  19. package/dist/commands/login.js +488 -0
  20. package/dist/commands/merge-worktree.js +165 -0
  21. package/dist/commands/model.js +325 -0
  22. package/dist/commands/permissions.js +140 -0
  23. package/dist/commands/ping.js +13 -0
  24. package/dist/commands/queue.js +133 -0
  25. package/dist/commands/remove-project.js +119 -0
  26. package/dist/commands/rename.js +70 -0
  27. package/dist/commands/restart-opencode-server.js +77 -0
  28. package/dist/commands/resume.js +276 -0
  29. package/dist/commands/run-config.js +79 -0
  30. package/dist/commands/run.js +240 -0
  31. package/dist/commands/schedule.js +170 -0
  32. package/dist/commands/session-info.js +58 -0
  33. package/dist/commands/session.js +191 -0
  34. package/dist/commands/settings.js +84 -0
  35. package/dist/commands/share.js +89 -0
  36. package/dist/commands/status.js +79 -0
  37. package/dist/commands/sync.js +119 -0
  38. package/dist/commands/theme.js +53 -0
  39. package/dist/commands/types.js +2 -0
  40. package/dist/commands/undo-redo.js +170 -0
  41. package/dist/commands/user-command.js +135 -0
  42. package/dist/commands/verbosity.js +59 -0
  43. package/dist/commands/worktree-settings.js +50 -0
  44. package/dist/commands/worktree.js +288 -0
  45. package/dist/config.js +139 -0
  46. package/dist/database.js +585 -0
  47. package/dist/discord-bot.js +700 -0
  48. package/dist/discord-utils.js +336 -0
  49. package/dist/discord-utils.test.js +20 -0
  50. package/dist/errors.js +193 -0
  51. package/dist/escape-backticks.test.js +429 -0
  52. package/dist/format-tables.js +96 -0
  53. package/dist/format-tables.test.js +418 -0
  54. package/dist/genai-worker-wrapper.js +109 -0
  55. package/dist/genai-worker.js +299 -0
  56. package/dist/genai.js +230 -0
  57. package/dist/image-utils.js +107 -0
  58. package/dist/interaction-handler.js +289 -0
  59. package/dist/limit-heading-depth.js +25 -0
  60. package/dist/limit-heading-depth.test.js +105 -0
  61. package/dist/logger.js +111 -0
  62. package/dist/markdown.js +323 -0
  63. package/dist/markdown.test.js +269 -0
  64. package/dist/message-formatting.js +447 -0
  65. package/dist/message-formatting.test.js +73 -0
  66. package/dist/openai-realtime.js +226 -0
  67. package/dist/opencode.js +224 -0
  68. package/dist/reaction-handler.js +128 -0
  69. package/dist/scheduler.js +93 -0
  70. package/dist/security.js +200 -0
  71. package/dist/session-handler.js +1436 -0
  72. package/dist/system-message.js +138 -0
  73. package/dist/tools.js +354 -0
  74. package/dist/unnest-code-blocks.js +117 -0
  75. package/dist/unnest-code-blocks.test.js +432 -0
  76. package/dist/utils.js +95 -0
  77. package/dist/voice-handler.js +569 -0
  78. package/dist/voice.js +344 -0
  79. package/dist/worker-types.js +4 -0
  80. package/dist/worktree-utils.js +134 -0
  81. package/dist/xml.js +90 -0
  82. package/dist/xml.test.js +32 -0
  83. package/package.json +84 -0
@@ -0,0 +1,289 @@
1
+ // Discord slash command and interaction handler.
2
+ // Processes all slash commands (/session, /resume, /fork, /model, /abort, etc.)
3
+ // and manages autocomplete, select menu interactions for the bot.
4
+ import { Events } from 'discord.js';
5
+ import { handleSessionCommand, handleSessionAutocomplete, } from './commands/session.js';
6
+ import { handleNewWorktreeCommand } from './commands/worktree.js';
7
+ import { handleMergeWorktreeCommand } from './commands/merge-worktree.js';
8
+ import { handleToggleWorktreesCommand } from './commands/worktree-settings.js';
9
+ import { handleResumeCommand, handleResumeAutocomplete, } from './commands/resume.js';
10
+ import { handleAddProjectCommand, handleAddProjectAutocomplete, } from './commands/add-project.js';
11
+ import { handleRemoveProjectCommand, handleRemoveProjectAutocomplete, } from './commands/remove-project.js';
12
+ import { handleCreateNewProjectCommand } from './commands/create-new-project.js';
13
+ import { handlePermissionSelectMenu } from './commands/permissions.js';
14
+ import { handleAbortCommand } from './commands/abort.js';
15
+ import { handleCompactCommand } from './commands/compact.js';
16
+ import { handleShareCommand } from './commands/share.js';
17
+ import { handleRenameCommand } from './commands/rename.js';
18
+ import { handleSessionInfoCommand } from './commands/session-info.js';
19
+ import { handleSyncCommand } from './commands/sync.js';
20
+ import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js';
21
+ import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu, } from './commands/model.js';
22
+ import { handleLoginCommand, handleLoginProviderSelectMenu, handleLoginMethodSelectMenu, handleApiKeyModalSubmit, } from './commands/login.js';
23
+ import { handleAgentCommand, handleAgentSelectMenu, handleQuickAgentCommand, } from './commands/agent.js';
24
+ import { handleAskQuestionSelectMenu } from './commands/ask-question.js';
25
+ import { handleQueueCommand, handleClearQueueCommand, } from './commands/queue.js';
26
+ import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js';
27
+ import { handleUserCommand } from './commands/user-command.js';
28
+ import { handleVerbosityCommand } from './commands/verbosity.js';
29
+ import { handleThemeCommand } from './commands/theme.js';
30
+ import { handleSettingsCommand } from './commands/settings.js';
31
+ import { handleRestartOpencodeServerCommand } from './commands/restart-opencode-server.js';
32
+ import { handleRunCommand, handleRunAutocomplete } from './commands/run.js';
33
+ import { handleRunConfigCommand } from './commands/run-config.js';
34
+ import { handleStatusCommand } from './commands/status.js';
35
+ import { handleHelpCommand } from './commands/help.js';
36
+ import { handlePingCommand } from './commands/ping.js';
37
+ import { handleContextCommand } from './commands/context.js';
38
+ import { handleCostCommand } from './commands/cost.js';
39
+ import { handleDiffCommand } from './commands/diff.js';
40
+ import { handleExportCommand } from './commands/export.js';
41
+ import { handleFilesCommand } from './commands/files.js';
42
+ import { handleScheduleCommand } from './commands/schedule.js';
43
+ import { handleRetryContextMenu, handleForkContextMenu, } from './commands/context-menu.js';
44
+ import { createLogger, LogPrefix } from './logger.js';
45
+ const interactionLogger = createLogger(LogPrefix.INTERACTION);
46
+ export function registerInteractionHandler({ discordClient, appId, }) {
47
+ interactionLogger.log('[REGISTER] Interaction handler registered');
48
+ discordClient.on(Events.InteractionCreate, async (interaction) => {
49
+ try {
50
+ interactionLogger.log(`[INTERACTION] Received: ${interaction.type} - ${interaction.isChatInputCommand()
51
+ ? interaction.commandName
52
+ : interaction.isAutocomplete()
53
+ ? `autocomplete:${interaction.commandName}`
54
+ : 'other'}`);
55
+ if (interaction.isAutocomplete()) {
56
+ switch (interaction.commandName) {
57
+ case 'new-session':
58
+ await handleSessionAutocomplete({ interaction, appId });
59
+ return;
60
+ case 'resume':
61
+ await handleResumeAutocomplete({ interaction, appId });
62
+ return;
63
+ case 'add-project':
64
+ await handleAddProjectAutocomplete({ interaction, appId });
65
+ return;
66
+ case 'remove-project':
67
+ await handleRemoveProjectAutocomplete({ interaction, appId });
68
+ return;
69
+ case 'run':
70
+ await handleRunAutocomplete({ interaction });
71
+ return;
72
+ default:
73
+ await interaction.respond([]);
74
+ return;
75
+ }
76
+ }
77
+ if (interaction.isChatInputCommand()) {
78
+ interactionLogger.log(`[COMMAND] Processing: ${interaction.commandName}`);
79
+ switch (interaction.commandName) {
80
+ case 'new-session':
81
+ await handleSessionCommand({ command: interaction, appId });
82
+ return;
83
+ case 'new-worktree':
84
+ await handleNewWorktreeCommand({ command: interaction, appId });
85
+ return;
86
+ case 'merge-worktree':
87
+ await handleMergeWorktreeCommand({ command: interaction, appId });
88
+ return;
89
+ case 'toggle-worktrees':
90
+ await handleToggleWorktreesCommand({
91
+ command: interaction,
92
+ appId,
93
+ });
94
+ return;
95
+ case 'resume':
96
+ await handleResumeCommand({ command: interaction, appId });
97
+ return;
98
+ case 'add-project':
99
+ await handleAddProjectCommand({ command: interaction, appId });
100
+ return;
101
+ case 'remove-project':
102
+ await handleRemoveProjectCommand({ command: interaction, appId });
103
+ return;
104
+ case 'create-new-project':
105
+ await handleCreateNewProjectCommand({
106
+ command: interaction,
107
+ appId,
108
+ });
109
+ return;
110
+ case 'abort':
111
+ case 'stop':
112
+ await handleAbortCommand({ command: interaction, appId });
113
+ return;
114
+ case 'compact':
115
+ await handleCompactCommand({ command: interaction, appId });
116
+ return;
117
+ case 'share':
118
+ await handleShareCommand({ command: interaction, appId });
119
+ return;
120
+ case 'rename':
121
+ await handleRenameCommand({ command: interaction, appId });
122
+ return;
123
+ case 'session-info':
124
+ await handleSessionInfoCommand({ command: interaction, appId });
125
+ return;
126
+ case 'sync':
127
+ await handleSyncCommand({ command: interaction, appId });
128
+ return;
129
+ case 'fork':
130
+ await handleForkCommand(interaction);
131
+ return;
132
+ case 'model':
133
+ await handleModelCommand({ interaction, appId });
134
+ return;
135
+ case 'login':
136
+ await handleLoginCommand({ interaction, appId });
137
+ return;
138
+ case 'agent':
139
+ await handleAgentCommand({ interaction, appId });
140
+ return;
141
+ case 'queue':
142
+ await handleQueueCommand({ command: interaction, appId });
143
+ return;
144
+ case 'clear-queue':
145
+ await handleClearQueueCommand({ command: interaction, appId });
146
+ return;
147
+ case 'undo':
148
+ await handleUndoCommand({ command: interaction, appId });
149
+ return;
150
+ case 'redo':
151
+ await handleRedoCommand({ command: interaction, appId });
152
+ return;
153
+ case 'verbosity':
154
+ await handleVerbosityCommand({ command: interaction, appId });
155
+ return;
156
+ case 'theme':
157
+ await handleThemeCommand({ command: interaction, appId });
158
+ return;
159
+ case 'restart-opencode-server':
160
+ await handleRestartOpencodeServerCommand({
161
+ command: interaction,
162
+ appId,
163
+ });
164
+ return;
165
+ case 'run':
166
+ await handleRunCommand({ command: interaction, appId });
167
+ return;
168
+ case 'run-config':
169
+ await handleRunConfigCommand({ command: interaction, appId });
170
+ return;
171
+ case 'status':
172
+ await handleStatusCommand({ command: interaction, appId });
173
+ return;
174
+ case 'help':
175
+ await handleHelpCommand({ command: interaction, appId });
176
+ return;
177
+ case 'ping':
178
+ await handlePingCommand({ command: interaction, appId });
179
+ return;
180
+ case 'context':
181
+ await handleContextCommand({ command: interaction, appId });
182
+ return;
183
+ case 'cost':
184
+ await handleCostCommand({ command: interaction, appId });
185
+ return;
186
+ case 'diff':
187
+ await handleDiffCommand({ command: interaction, appId });
188
+ return;
189
+ case 'export':
190
+ await handleExportCommand({ command: interaction, appId });
191
+ return;
192
+ case 'files':
193
+ await handleFilesCommand({ command: interaction, appId });
194
+ return;
195
+ case 'settings':
196
+ await handleSettingsCommand({ command: interaction, appId });
197
+ return;
198
+ case 'schedule':
199
+ await handleScheduleCommand({ command: interaction, appId });
200
+ return;
201
+ }
202
+ // Handle quick agent commands (ending with -agent suffix, but not the base /agent command)
203
+ if (interaction.commandName.endsWith('-agent') &&
204
+ interaction.commandName !== 'agent') {
205
+ await handleQuickAgentCommand({ command: interaction, appId });
206
+ return;
207
+ }
208
+ // Handle user-defined commands (ending with -cmd suffix)
209
+ if (interaction.commandName.endsWith('-cmd')) {
210
+ await handleUserCommand({ command: interaction, appId });
211
+ return;
212
+ }
213
+ return;
214
+ }
215
+ if (interaction.isStringSelectMenu()) {
216
+ const customId = interaction.customId;
217
+ if (customId.startsWith('fork_select:')) {
218
+ await handleForkSelectMenu(interaction);
219
+ return;
220
+ }
221
+ if (customId.startsWith('model_provider:')) {
222
+ await handleProviderSelectMenu(interaction);
223
+ return;
224
+ }
225
+ if (customId.startsWith('model_select:')) {
226
+ await handleModelSelectMenu(interaction);
227
+ return;
228
+ }
229
+ if (customId.startsWith('agent_select:')) {
230
+ await handleAgentSelectMenu(interaction);
231
+ return;
232
+ }
233
+ if (customId.startsWith('ask_question:')) {
234
+ await handleAskQuestionSelectMenu(interaction);
235
+ return;
236
+ }
237
+ if (customId.startsWith('permission:')) {
238
+ await handlePermissionSelectMenu(interaction);
239
+ return;
240
+ }
241
+ if (customId.startsWith('login_provider:')) {
242
+ await handleLoginProviderSelectMenu(interaction);
243
+ return;
244
+ }
245
+ if (customId.startsWith('login_method:')) {
246
+ await handleLoginMethodSelectMenu(interaction);
247
+ return;
248
+ }
249
+ return;
250
+ }
251
+ if (interaction.isModalSubmit()) {
252
+ const customId = interaction.customId;
253
+ if (customId.startsWith('login_apikey:')) {
254
+ await handleApiKeyModalSubmit(interaction);
255
+ return;
256
+ }
257
+ return;
258
+ }
259
+ if (interaction.isMessageContextMenuCommand()) {
260
+ interactionLogger.log(`[CONTEXT-MENU] Processing: ${interaction.commandName}`);
261
+ switch (interaction.commandName) {
262
+ case 'Retry this prompt':
263
+ await handleRetryContextMenu({ interaction, appId });
264
+ return;
265
+ case 'Fork from here':
266
+ await handleForkContextMenu({ interaction, appId });
267
+ return;
268
+ }
269
+ return;
270
+ }
271
+ }
272
+ catch (error) {
273
+ interactionLogger.error('[INTERACTION] Error handling interaction:', error);
274
+ try {
275
+ if (interaction.isRepliable() &&
276
+ !interaction.replied &&
277
+ !interaction.deferred) {
278
+ await interaction.reply({
279
+ content: 'An error occurred processing this command.',
280
+ ephemeral: true,
281
+ });
282
+ }
283
+ }
284
+ catch (replyError) {
285
+ interactionLogger.error('[INTERACTION] Failed to send error reply:', replyError);
286
+ }
287
+ }
288
+ });
289
+ }
@@ -0,0 +1,25 @@
1
+ // Limit heading depth for Discord.
2
+ // Discord only supports headings up to ### (h3), so this converts
3
+ // ####, #####, etc. to ### to maintain consistent rendering.
4
+ import { Lexer } from 'marked';
5
+ export function limitHeadingDepth(markdown, maxDepth = 3) {
6
+ const lexer = new Lexer();
7
+ const tokens = lexer.lex(markdown);
8
+ let result = '';
9
+ for (const token of tokens) {
10
+ if (token.type === 'heading') {
11
+ const heading = token;
12
+ if (heading.depth > maxDepth) {
13
+ const hashes = '#'.repeat(maxDepth);
14
+ result += hashes + ' ' + heading.text + '\n';
15
+ }
16
+ else {
17
+ result += token.raw;
18
+ }
19
+ }
20
+ else {
21
+ result += token.raw;
22
+ }
23
+ }
24
+ return result;
25
+ }
@@ -0,0 +1,105 @@
1
+ import { expect, test } from 'vitest';
2
+ import { limitHeadingDepth } from './limit-heading-depth.js';
3
+ test('converts h4 to h3', () => {
4
+ const input = '#### Fourth level heading';
5
+ const result = limitHeadingDepth(input);
6
+ expect(result).toMatchInlineSnapshot(`
7
+ "### Fourth level heading
8
+ "
9
+ `);
10
+ });
11
+ test('converts h5 to h3', () => {
12
+ const input = '##### Fifth level heading';
13
+ const result = limitHeadingDepth(input);
14
+ expect(result).toMatchInlineSnapshot(`
15
+ "### Fifth level heading
16
+ "
17
+ `);
18
+ });
19
+ test('converts h6 to h3', () => {
20
+ const input = '###### Sixth level heading';
21
+ const result = limitHeadingDepth(input);
22
+ expect(result).toMatchInlineSnapshot(`
23
+ "### Sixth level heading
24
+ "
25
+ `);
26
+ });
27
+ test('preserves h3 unchanged', () => {
28
+ const input = '### Third level heading';
29
+ const result = limitHeadingDepth(input);
30
+ expect(result).toMatchInlineSnapshot(`"### Third level heading"`);
31
+ });
32
+ test('preserves h2 unchanged', () => {
33
+ const input = '## Second level heading';
34
+ const result = limitHeadingDepth(input);
35
+ expect(result).toMatchInlineSnapshot(`"## Second level heading"`);
36
+ });
37
+ test('preserves h1 unchanged', () => {
38
+ const input = '# First level heading';
39
+ const result = limitHeadingDepth(input);
40
+ expect(result).toMatchInlineSnapshot(`"# First level heading"`);
41
+ });
42
+ test('handles multiple headings in document', () => {
43
+ const input = `# Title
44
+
45
+ Some text
46
+
47
+ ## Section
48
+
49
+ ### Subsection
50
+
51
+ #### Too deep
52
+
53
+ ##### Even deeper
54
+
55
+ Regular paragraph
56
+
57
+ ### Back to normal
58
+ `;
59
+ const result = limitHeadingDepth(input);
60
+ expect(result).toMatchInlineSnapshot(`
61
+ "# Title
62
+
63
+ Some text
64
+
65
+ ## Section
66
+
67
+ ### Subsection
68
+
69
+ ### Too deep
70
+ ### Even deeper
71
+ Regular paragraph
72
+
73
+ ### Back to normal
74
+ "
75
+ `);
76
+ });
77
+ test('preserves heading with inline formatting', () => {
78
+ const input = '#### Heading with **bold** and `code`';
79
+ const result = limitHeadingDepth(input);
80
+ expect(result).toMatchInlineSnapshot(`
81
+ "### Heading with **bold** and \`code\`
82
+ "
83
+ `);
84
+ });
85
+ test('handles empty markdown', () => {
86
+ const result = limitHeadingDepth('');
87
+ expect(result).toMatchInlineSnapshot(`""`);
88
+ });
89
+ test('handles markdown with no headings', () => {
90
+ const input = 'Just some text\n\nAnd more text';
91
+ const result = limitHeadingDepth(input);
92
+ expect(result).toMatchInlineSnapshot(`
93
+ "Just some text
94
+
95
+ And more text"
96
+ `);
97
+ });
98
+ test('allows custom maxDepth', () => {
99
+ const input = '### Third level';
100
+ const result = limitHeadingDepth(input, 2);
101
+ expect(result).toMatchInlineSnapshot(`
102
+ "## Third level
103
+ "
104
+ `);
105
+ });
package/dist/logger.js ADDED
@@ -0,0 +1,111 @@
1
+ // Prefixed logging utility.
2
+ // Uses picocolors for compact frequent logs (log, info, debug).
3
+ // Uses @clack/prompts only for important events (warn, error) with visual distinction.
4
+ import { log as clackLog } from '@clack/prompts';
5
+ import fs from 'node:fs';
6
+ import path, { dirname } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import util from 'node:util';
9
+ import pc from 'picocolors';
10
+ // All known log prefixes - add new ones here to keep alignment consistent
11
+ export const LogPrefix = {
12
+ ABORT: 'ABORT',
13
+ ADD_PROJECT: 'ADD_PROJ',
14
+ AGENT: 'AGENT',
15
+ ASK_QUESTION: 'QUESTION',
16
+ CLI: 'CLI',
17
+ COMPACT: 'COMPACT',
18
+ CREATE_PROJECT: 'NEW_PROJ',
19
+ DB: 'DB',
20
+ DISCORD: 'DISCORD',
21
+ FORK: 'FORK',
22
+ FORMATTING: 'FORMAT',
23
+ GENAI: 'GENAI',
24
+ GENAI_WORKER: 'GENAI_W',
25
+ INTERACTION: 'INTERACT',
26
+ LOGIN: 'LOGIN',
27
+ MARKDOWN: 'MARKDOWN',
28
+ MODEL: 'MODEL',
29
+ OPENAI: 'OPENAI',
30
+ OPENCODE: 'OPENCODE',
31
+ PERMISSIONS: 'PERMS',
32
+ QUEUE: 'QUEUE',
33
+ REACTION: 'REACTION',
34
+ REMOVE_PROJECT: 'RM_PROJ',
35
+ SCHEDULE: 'SCHEDULE',
36
+ RESUME: 'RESUME',
37
+ SESSION: 'SESSION',
38
+ SETTINGS: 'SETTINGS',
39
+ SHARE: 'SHARE',
40
+ THEME: 'THEME',
41
+ TOOLS: 'TOOLS',
42
+ UNDO_REDO: 'UNDO',
43
+ USER_CMD: 'USER_CMD',
44
+ VERBOSITY: 'VERBOSE',
45
+ VOICE: 'VOICE',
46
+ WORKER: 'WORKER',
47
+ WORKTREE: 'WORKTREE',
48
+ XML: 'XML',
49
+ };
50
+ // compute max length from all known prefixes for alignment
51
+ const MAX_PREFIX_LENGTH = Math.max(...Object.values(LogPrefix).map((p) => p.length));
52
+ const __filename = fileURLToPath(import.meta.url);
53
+ const __dirname = dirname(__filename);
54
+ const isDev = !__dirname.includes('node_modules');
55
+ const logFilePath = path.join(__dirname, '..', 'tmp', 'disunday.log');
56
+ // reset log file on startup in dev mode
57
+ if (isDev) {
58
+ const logDir = path.dirname(logFilePath);
59
+ if (!fs.existsSync(logDir)) {
60
+ fs.mkdirSync(logDir, { recursive: true });
61
+ }
62
+ fs.writeFileSync(logFilePath, `--- disunday log started at ${new Date().toISOString()} ---\n`);
63
+ }
64
+ function formatArg(arg) {
65
+ if (typeof arg === 'string') {
66
+ return arg;
67
+ }
68
+ return util.inspect(arg, { colors: true, depth: 4 });
69
+ }
70
+ function writeToFile(level, prefix, args) {
71
+ if (!isDev) {
72
+ return;
73
+ }
74
+ const timestamp = new Date().toISOString();
75
+ const message = `[${timestamp}] [${level}] [${prefix}] ${args.map(formatArg).join(' ')}\n`;
76
+ fs.appendFileSync(logFilePath, message);
77
+ }
78
+ function getTimestamp() {
79
+ const now = new Date();
80
+ return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
81
+ }
82
+ function padPrefix(prefix) {
83
+ return prefix.padEnd(MAX_PREFIX_LENGTH);
84
+ }
85
+ export function createLogger(prefix) {
86
+ const paddedPrefix = padPrefix(prefix);
87
+ return {
88
+ log: (...args) => {
89
+ writeToFile('INFO', prefix, args);
90
+ console.log(pc.dim(getTimestamp()), pc.cyan(paddedPrefix), ...args.map(formatArg));
91
+ },
92
+ error: (...args) => {
93
+ writeToFile('ERROR', prefix, args);
94
+ // use clack for errors - visually distinct
95
+ clackLog.error([paddedPrefix, ...args.map(formatArg)].join(' '));
96
+ },
97
+ warn: (...args) => {
98
+ writeToFile('WARN', prefix, args);
99
+ // use clack for warnings - visually distinct
100
+ clackLog.warn([paddedPrefix, ...args.map(formatArg)].join(' '));
101
+ },
102
+ info: (...args) => {
103
+ writeToFile('INFO', prefix, args);
104
+ console.log(pc.dim(getTimestamp()), pc.blue(paddedPrefix), ...args.map(formatArg));
105
+ },
106
+ debug: (...args) => {
107
+ writeToFile('DEBUG', prefix, args);
108
+ console.log(pc.dim(getTimestamp()), pc.dim(paddedPrefix), ...args.map(formatArg));
109
+ },
110
+ };
111
+ }