@meltstudio/meltctl 4.38.0 → 4.39.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 (81) hide show
  1. package/dist/index.js +2178 -210
  2. package/package.json +4 -3
  3. package/dist/commands/audit.d.ts +0 -10
  4. package/dist/commands/audit.js +0 -191
  5. package/dist/commands/audit.test.d.ts +0 -1
  6. package/dist/commands/audit.test.js +0 -324
  7. package/dist/commands/coins.d.ts +0 -5
  8. package/dist/commands/coins.js +0 -51
  9. package/dist/commands/coins.test.d.ts +0 -1
  10. package/dist/commands/coins.test.js +0 -113
  11. package/dist/commands/feedback.d.ts +0 -7
  12. package/dist/commands/feedback.js +0 -90
  13. package/dist/commands/feedback.test.d.ts +0 -1
  14. package/dist/commands/feedback.test.js +0 -177
  15. package/dist/commands/init.d.ts +0 -8
  16. package/dist/commands/init.js +0 -520
  17. package/dist/commands/init.test.d.ts +0 -1
  18. package/dist/commands/init.test.js +0 -478
  19. package/dist/commands/login.d.ts +0 -1
  20. package/dist/commands/login.js +0 -90
  21. package/dist/commands/login.test.d.ts +0 -1
  22. package/dist/commands/login.test.js +0 -194
  23. package/dist/commands/logout.d.ts +0 -1
  24. package/dist/commands/logout.js +0 -12
  25. package/dist/commands/logout.test.d.ts +0 -1
  26. package/dist/commands/logout.test.js +0 -59
  27. package/dist/commands/plan.d.ts +0 -6
  28. package/dist/commands/plan.js +0 -123
  29. package/dist/commands/plan.test.d.ts +0 -1
  30. package/dist/commands/plan.test.js +0 -246
  31. package/dist/commands/standup.d.ts +0 -7
  32. package/dist/commands/standup.js +0 -74
  33. package/dist/commands/standup.test.d.ts +0 -1
  34. package/dist/commands/standup.test.js +0 -218
  35. package/dist/commands/templates.d.ts +0 -1
  36. package/dist/commands/templates.js +0 -37
  37. package/dist/commands/templates.test.d.ts +0 -1
  38. package/dist/commands/templates.test.js +0 -89
  39. package/dist/commands/update.d.ts +0 -2
  40. package/dist/commands/update.js +0 -74
  41. package/dist/commands/update.test.d.ts +0 -1
  42. package/dist/commands/update.test.js +0 -93
  43. package/dist/commands/version.d.ts +0 -1
  44. package/dist/commands/version.js +0 -43
  45. package/dist/commands/version.test.d.ts +0 -1
  46. package/dist/commands/version.test.js +0 -86
  47. package/dist/index.d.ts +0 -2
  48. package/dist/utils/analytics.d.ts +0 -1
  49. package/dist/utils/analytics.js +0 -54
  50. package/dist/utils/analytics.test.d.ts +0 -1
  51. package/dist/utils/analytics.test.js +0 -91
  52. package/dist/utils/api.d.ts +0 -3
  53. package/dist/utils/api.js +0 -23
  54. package/dist/utils/api.test.d.ts +0 -1
  55. package/dist/utils/api.test.js +0 -76
  56. package/dist/utils/auth.d.ts +0 -12
  57. package/dist/utils/auth.js +0 -54
  58. package/dist/utils/auth.test.d.ts +0 -1
  59. package/dist/utils/auth.test.js +0 -165
  60. package/dist/utils/banner.d.ts +0 -1
  61. package/dist/utils/banner.js +0 -22
  62. package/dist/utils/banner.test.d.ts +0 -1
  63. package/dist/utils/banner.test.js +0 -34
  64. package/dist/utils/debug.d.ts +0 -1
  65. package/dist/utils/debug.js +0 -6
  66. package/dist/utils/git.d.ts +0 -9
  67. package/dist/utils/git.js +0 -76
  68. package/dist/utils/git.test.d.ts +0 -1
  69. package/dist/utils/git.test.js +0 -184
  70. package/dist/utils/package-manager.d.ts +0 -7
  71. package/dist/utils/package-manager.js +0 -55
  72. package/dist/utils/package-manager.test.d.ts +0 -1
  73. package/dist/utils/package-manager.test.js +0 -76
  74. package/dist/utils/templates.d.ts +0 -2
  75. package/dist/utils/templates.js +0 -5
  76. package/dist/utils/templates.test.d.ts +0 -1
  77. package/dist/utils/templates.test.js +0 -38
  78. package/dist/utils/version-check.d.ts +0 -7
  79. package/dist/utils/version-check.js +0 -139
  80. package/dist/utils/version-check.test.d.ts +0 -1
  81. package/dist/utils/version-check.test.js +0 -189
@@ -1,478 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- vi.mock('fs-extra', () => ({
3
- default: {
4
- pathExists: vi.fn(),
5
- pathExistsSync: vi.fn(),
6
- writeFile: vi.fn(),
7
- readFile: vi.fn(),
8
- ensureDir: vi.fn(),
9
- },
10
- }));
11
- vi.mock('../utils/auth.js', () => ({
12
- isAuthenticated: vi.fn(),
13
- }));
14
- vi.mock('../utils/templates.js', () => ({
15
- fetchTemplates: vi.fn(),
16
- }));
17
- vi.mock('@inquirer/prompts', () => ({
18
- checkbox: vi.fn(),
19
- confirm: vi.fn(),
20
- }));
21
- import fs from 'fs-extra';
22
- import { isAuthenticated } from '../utils/auth.js';
23
- import { fetchTemplates } from '../utils/templates.js';
24
- import { checkbox, confirm } from '@inquirer/prompts';
25
- import { initCommand } from './init.js';
26
- const MOCK_TEMPLATES = {
27
- 'agents-md.md': '# AGENTS.md\nProject standards go here.',
28
- 'workflows/setup.md': '# Setup Workflow\nAnalyze project.',
29
- 'workflows/plan.md': '# Plan Workflow\nDesign approach.',
30
- 'workflows/validate.md': '# Validate Workflow\nRun validation.',
31
- 'workflows/review.md': '# Review Workflow\nReview code.',
32
- 'workflows/pr.md': '# PR Workflow\nCreate PR.',
33
- 'workflows/debug.md': '# Debug Workflow\nDebug issue.',
34
- 'workflows/audit.md': '# Audit Workflow\nRun audit.',
35
- 'workflows/ux-audit.md': '# UX Audit Workflow\nReview UI.',
36
- 'workflows/security-audit.md': '# Security Audit Workflow\nSecurity check.',
37
- 'workflows/update.md': '# Update Workflow\nUpdate skills.',
38
- 'workflows/help.md': '# Help Workflow\nAnswer questions.',
39
- 'workflows/link.md': '# Link Workflow\nConnect tools.',
40
- 'claude-settings.json': '{"permissions":{"allow":["Read"],"deny":["Bash"]}}',
41
- 'mcp-configs/base.json': '{"mcpServers":{"context7":{"command":"npx","args":["context7"]}}}',
42
- };
43
- beforeEach(() => {
44
- vi.clearAllMocks();
45
- vi.spyOn(console, 'log').mockImplementation(() => { });
46
- vi.spyOn(console, 'error').mockImplementation(() => { });
47
- vi.spyOn(process, 'exit').mockImplementation((code) => {
48
- throw new Error(`process.exit(${code})`);
49
- });
50
- isAuthenticated.mockResolvedValue(true);
51
- fs.pathExists.mockImplementation(async (p) => {
52
- if (p.endsWith('.git'))
53
- return true;
54
- return false;
55
- });
56
- fs.pathExistsSync.mockReturnValue(false);
57
- fs.writeFile.mockResolvedValue(undefined);
58
- fs.readFile.mockResolvedValue('');
59
- fs.ensureDir.mockResolvedValue(undefined);
60
- fetchTemplates.mockResolvedValue(MOCK_TEMPLATES);
61
- });
62
- describe('initCommand', () => {
63
- describe('authentication', () => {
64
- it('exits when not authenticated', async () => {
65
- ;
66
- isAuthenticated.mockResolvedValue(false);
67
- await expect(initCommand({})).rejects.toThrow('process.exit(1)');
68
- expect(console.error).toHaveBeenCalled();
69
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
70
- expect(errorCalls.some((msg) => msg.includes('Not authenticated'))).toBe(true);
71
- });
72
- });
73
- describe('git repository check', () => {
74
- it('prompts to continue when not in a git repo and aborts if declined', async () => {
75
- ;
76
- fs.pathExists.mockImplementation(async (p) => {
77
- if (p.endsWith('.git'))
78
- return false;
79
- return false;
80
- });
81
- confirm.mockResolvedValue(false);
82
- await initCommand({});
83
- expect(confirm).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining('Continue') }));
84
- // Should not have fetched templates since we aborted
85
- expect(fetchTemplates).not.toHaveBeenCalled();
86
- });
87
- it('proceeds when not in a git repo but user confirms', async () => {
88
- ;
89
- fs.pathExists.mockImplementation(async (p) => {
90
- if (p.endsWith('.git'))
91
- return false;
92
- return false;
93
- });
94
- confirm.mockResolvedValue(true);
95
- checkbox.mockResolvedValue(['claude']);
96
- await initCommand({});
97
- expect(fetchTemplates).toHaveBeenCalled();
98
- });
99
- });
100
- describe('fresh init with flags', () => {
101
- it('creates AGENTS.md from templates', async () => {
102
- await initCommand({ claude: true });
103
- expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining('AGENTS.md'), '# AGENTS.md\nProject standards go here.', 'utf-8');
104
- });
105
- it('creates Claude skills with frontmatter', async () => {
106
- await initCommand({ claude: true });
107
- // Check that skills are created with frontmatter prepended
108
- const writeCalls = fs.writeFile.mock.calls;
109
- const setupSkill = writeCalls.find(c => c[0].includes('.claude/skills/melt-setup') && c[0].endsWith('SKILL.md'));
110
- expect(setupSkill).toBeDefined();
111
- expect(setupSkill[1]).toContain('---');
112
- expect(setupSkill[1]).toContain('user-invocable: true');
113
- expect(setupSkill[1]).toContain('Analyze the project');
114
- expect(setupSkill[1]).toContain('# Setup Workflow');
115
- // Verify ensureDir was called for skill directories
116
- const ensureDirCalls = fs.ensureDir.mock.calls;
117
- const skillDirs = ensureDirCalls.filter(c => c[0].includes('.claude/skills/melt-'));
118
- expect(skillDirs.length).toBeGreaterThanOrEqual(12);
119
- });
120
- it('creates all 11 Claude skills', async () => {
121
- await initCommand({ claude: true });
122
- const writeCalls = fs.writeFile.mock.calls;
123
- const skillFiles = writeCalls.filter(c => c[0].includes('.claude/skills/melt-') && c[0].endsWith('SKILL.md'));
124
- expect(skillFiles.length).toBe(12);
125
- const skillNames = skillFiles.map(c => {
126
- const match = c[0].match(/\.claude\/skills\/melt-([^/]+)/);
127
- return match ? match[1] : '';
128
- });
129
- expect(skillNames).toContain('setup');
130
- expect(skillNames).toContain('plan');
131
- expect(skillNames).toContain('validate');
132
- expect(skillNames).toContain('review');
133
- expect(skillNames).toContain('pr');
134
- expect(skillNames).toContain('debug');
135
- expect(skillNames).toContain('audit');
136
- expect(skillNames).toContain('ux-audit');
137
- expect(skillNames).toContain('security-audit');
138
- expect(skillNames).toContain('update');
139
- expect(skillNames).toContain('help');
140
- expect(skillNames).toContain('link');
141
- });
142
- it('creates Cursor commands without frontmatter', async () => {
143
- await initCommand({ cursor: true });
144
- const writeCalls = fs.writeFile.mock.calls;
145
- const cursorFiles = writeCalls.filter(c => c[0].includes('.cursor/commands/melt-'));
146
- expect(cursorFiles.length).toBe(12);
147
- // Cursor commands should NOT have frontmatter
148
- const setupCmd = cursorFiles.find(c => c[0].includes('melt-setup.md'));
149
- expect(setupCmd).toBeDefined();
150
- expect(setupCmd[1]).toBe('# Setup Workflow\nAnalyze project.');
151
- expect(setupCmd[1]).not.toContain('---');
152
- });
153
- it('creates OpenCode commands with frontmatter', async () => {
154
- await initCommand({ opencode: true });
155
- const writeCalls = fs.writeFile.mock.calls;
156
- const opencodeFiles = writeCalls.filter(c => c[0].includes('.opencode/commands/melt-'));
157
- expect(opencodeFiles.length).toBe(12);
158
- // OpenCode commands should have shorter frontmatter
159
- const setupCmd = opencodeFiles.find(c => c[0].includes('melt-setup.md'));
160
- expect(setupCmd).toBeDefined();
161
- expect(setupCmd[1]).toContain('---');
162
- expect(setupCmd[1]).toContain('description: Analyze the project');
163
- expect(setupCmd[1]).toContain('# Setup Workflow');
164
- });
165
- it('only creates files for --claude flag', async () => {
166
- await initCommand({ claude: true });
167
- const writeCalls = fs.writeFile.mock.calls;
168
- const cursorFiles = writeCalls.filter(c => c[0].includes('.cursor/'));
169
- const opencodeFiles = writeCalls.filter(c => c[0].includes('.opencode/'));
170
- expect(cursorFiles.length).toBe(0);
171
- expect(opencodeFiles.length).toBe(0);
172
- });
173
- it('only creates files for --cursor flag', async () => {
174
- await initCommand({ cursor: true });
175
- const writeCalls = fs.writeFile.mock.calls;
176
- const claudeFiles = writeCalls.filter(c => c[0].includes('.claude/'));
177
- const opencodeFiles = writeCalls.filter(c => c[0].includes('.opencode/'));
178
- expect(claudeFiles.length).toBe(0);
179
- expect(opencodeFiles.length).toBe(0);
180
- });
181
- it('only creates files for --opencode flag', async () => {
182
- await initCommand({ opencode: true });
183
- const writeCalls = fs.writeFile.mock.calls;
184
- const claudeFiles = writeCalls.filter(c => c[0].includes('.claude/'));
185
- const cursorFiles = writeCalls.filter(c => c[0].includes('.cursor/'));
186
- expect(claudeFiles.length).toBe(0);
187
- expect(cursorFiles.length).toBe(0);
188
- });
189
- it('creates files for all tools when all flags are set', async () => {
190
- await initCommand({ claude: true, cursor: true, opencode: true });
191
- const writeCalls = fs.writeFile.mock.calls;
192
- const claudeFiles = writeCalls.filter(c => c[0].includes('.claude/'));
193
- const cursorFiles = writeCalls.filter(c => c[0].includes('.cursor/'));
194
- const opencodeFiles = writeCalls.filter(c => c[0].includes('.opencode/'));
195
- expect(claudeFiles.length).toBeGreaterThan(0);
196
- expect(cursorFiles.length).toBeGreaterThan(0);
197
- expect(opencodeFiles.length).toBeGreaterThan(0);
198
- });
199
- });
200
- describe('interactive tool selection', () => {
201
- it('prompts with checkbox when no flags provided', async () => {
202
- ;
203
- checkbox.mockResolvedValue(['claude', 'cursor']);
204
- await initCommand({});
205
- expect(checkbox).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining('AI coding tools') }));
206
- });
207
- it('exits when no tools selected', async () => {
208
- ;
209
- checkbox.mockResolvedValue([]);
210
- await expect(initCommand({})).rejects.toThrow('process.exit(0)');
211
- const logCalls = console.log.mock.calls.map((c) => String(c[0]));
212
- expect(logCalls.some((msg) => msg.includes('No tools selected'))).toBe(true);
213
- });
214
- });
215
- describe('.mcp.json', () => {
216
- it('creates .mcp.json from template when none exists', async () => {
217
- await initCommand({ claude: true });
218
- const writeCalls = fs.writeFile.mock.calls;
219
- const mcpWrite = writeCalls.find(c => c[0].endsWith('.mcp.json'));
220
- expect(mcpWrite).toBeDefined();
221
- expect(mcpWrite[1]).toBe(MOCK_TEMPLATES['mcp-configs/base.json']);
222
- });
223
- it('merges .mcp.json with existing config', async () => {
224
- const existingMcp = JSON.stringify({
225
- mcpServers: { existing: { command: 'existing-cmd' } },
226
- });
227
- fs.pathExists.mockImplementation(async (p) => {
228
- if (p.endsWith('.git'))
229
- return true;
230
- if (p.endsWith('.mcp.json'))
231
- return true;
232
- return false;
233
- });
234
- fs.readFile.mockImplementation(async (p) => {
235
- if (p.endsWith('.mcp.json'))
236
- return existingMcp;
237
- return '';
238
- });
239
- await initCommand({ claude: true });
240
- const writeCalls = fs.writeFile.mock.calls;
241
- const mcpWrite = writeCalls.find(c => c[0].endsWith('.mcp.json'));
242
- expect(mcpWrite).toBeDefined();
243
- const written = JSON.parse(mcpWrite[1]);
244
- // Should have both existing and template servers
245
- expect(written.mcpServers.existing).toEqual({ command: 'existing-cmd' });
246
- expect(written.mcpServers.context7).toEqual({ command: 'npx', args: ['context7'] });
247
- });
248
- });
249
- describe('.claude/settings.json', () => {
250
- it('creates claude settings from template when none exists', async () => {
251
- await initCommand({ claude: true });
252
- const writeCalls = fs.writeFile.mock.calls;
253
- const settingsWrite = writeCalls.find(c => c[0].includes('.claude/settings.json'));
254
- expect(settingsWrite).toBeDefined();
255
- expect(settingsWrite[1]).toBe(MOCK_TEMPLATES['claude-settings.json']);
256
- });
257
- it('merges claude settings with existing permissions', async () => {
258
- const existingSettings = JSON.stringify({
259
- permissions: { allow: ['Bash'], deny: ['Write'] },
260
- });
261
- fs.pathExists.mockImplementation(async (p) => {
262
- if (p.endsWith('.git'))
263
- return true;
264
- if (p.endsWith('.claude/settings.json'))
265
- return true;
266
- return false;
267
- });
268
- fs.readFile.mockImplementation(async (p) => {
269
- if (p.endsWith('settings.json'))
270
- return existingSettings;
271
- return '';
272
- });
273
- await initCommand({ claude: true });
274
- const writeCalls = fs.writeFile.mock.calls;
275
- const settingsWrite = writeCalls.find(c => c[0].includes('.claude/settings.json'));
276
- expect(settingsWrite).toBeDefined();
277
- const written = JSON.parse(settingsWrite[1]);
278
- // Should have deduplicated merged arrays
279
- expect(written.permissions.allow).toContain('Bash');
280
- expect(written.permissions.allow).toContain('Read');
281
- expect(written.permissions.deny).toContain('Write');
282
- expect(written.permissions.deny).toContain('Bash');
283
- });
284
- });
285
- describe('.gitignore', () => {
286
- it('creates .gitignore with melt entries when none exists', async () => {
287
- await initCommand({ claude: true });
288
- const writeCalls = fs.writeFile.mock.calls;
289
- const gitignoreWrite = writeCalls.find(c => c[0].endsWith('.gitignore'));
290
- expect(gitignoreWrite).toBeDefined();
291
- expect(gitignoreWrite[1]).toContain('.env.local');
292
- expect(gitignoreWrite[1]).toContain('.claude/settings.local.json');
293
- expect(gitignoreWrite[1]).toContain('# Melt - local settings');
294
- });
295
- it('appends missing entries to existing .gitignore', async () => {
296
- ;
297
- fs.pathExists.mockImplementation(async (p) => {
298
- if (p.endsWith('.git'))
299
- return true;
300
- if (p.endsWith('.gitignore'))
301
- return true;
302
- return false;
303
- });
304
- fs.readFile.mockImplementation(async (p) => {
305
- if (p.endsWith('.gitignore'))
306
- return 'node_modules\n.env.local\n';
307
- return '';
308
- });
309
- await initCommand({ claude: true });
310
- const writeCalls = fs.writeFile.mock.calls;
311
- const gitignoreWrite = writeCalls.find(c => c[0].endsWith('.gitignore'));
312
- expect(gitignoreWrite).toBeDefined();
313
- // Should keep existing content and add only missing entry
314
- expect(gitignoreWrite[1]).toContain('node_modules');
315
- expect(gitignoreWrite[1]).toContain('.claude/settings.local.json');
316
- });
317
- it('does not modify .gitignore when all entries already present', async () => {
318
- ;
319
- fs.pathExists.mockImplementation(async (p) => {
320
- if (p.endsWith('.git'))
321
- return true;
322
- if (p.endsWith('.gitignore'))
323
- return true;
324
- return false;
325
- });
326
- fs.readFile.mockImplementation(async (p) => {
327
- if (p.endsWith('.gitignore'))
328
- return '.env.local\n.claude/settings.local.json\n';
329
- return '';
330
- });
331
- await initCommand({ claude: true });
332
- const writeCalls = fs.writeFile.mock.calls;
333
- const gitignoreWrite = writeCalls.find(c => c[0].endsWith('.gitignore'));
334
- // Should not write .gitignore when all entries exist
335
- expect(gitignoreWrite).toBeUndefined();
336
- });
337
- });
338
- describe('re-init flow', () => {
339
- beforeEach(() => {
340
- // AGENTS.md exists (already initialized)
341
- ;
342
- fs.pathExists.mockImplementation(async (p) => {
343
- if (p.endsWith('.git'))
344
- return true;
345
- if (p.endsWith('AGENTS.md'))
346
- return true;
347
- return false;
348
- });
349
- });
350
- it('detects existing tools and prompts to add more', async () => {
351
- ;
352
- fs.pathExistsSync.mockImplementation((p) => {
353
- if (p.includes('.claude/settings.json'))
354
- return true;
355
- return false;
356
- });
357
- confirm.mockResolvedValue(true);
358
- checkbox.mockResolvedValue(['cursor']);
359
- await initCommand({});
360
- // Should prompt to add more
361
- expect(confirm).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining('Add configuration') }));
362
- // Checkbox should NOT include Claude since it already exists
363
- const checkboxCall = checkbox.mock.calls[0][0];
364
- const values = checkboxCall.choices.map((c) => c.value);
365
- expect(values).not.toContain('claude');
366
- expect(values).toContain('cursor');
367
- expect(values).toContain('opencode');
368
- });
369
- it('skips AGENTS.md and .mcp.json on re-init', async () => {
370
- ;
371
- confirm.mockResolvedValue(true);
372
- checkbox.mockResolvedValue(['cursor']);
373
- await initCommand({});
374
- const writeCalls = fs.writeFile.mock.calls;
375
- const agentsWrite = writeCalls.find(c => c[0].endsWith('AGENTS.md'));
376
- const mcpWrite = writeCalls.find(c => c[0].endsWith('.mcp.json'));
377
- expect(agentsWrite).toBeUndefined();
378
- expect(mcpWrite).toBeUndefined();
379
- });
380
- it('exits when user declines to add more tools', async () => {
381
- ;
382
- confirm.mockResolvedValue(false);
383
- await expect(initCommand({})).rejects.toThrow('process.exit(0)');
384
- });
385
- it('exits when all tools are already initialized', async () => {
386
- ;
387
- fs.pathExistsSync.mockImplementation((p) => {
388
- if (p.includes('.claude/settings.json'))
389
- return true;
390
- if (p.includes('.cursor/commands'))
391
- return true;
392
- if (p.includes('.opencode/commands'))
393
- return true;
394
- return false;
395
- });
396
- await expect(initCommand({})).rejects.toThrow('process.exit(1)');
397
- const logCalls = console.log.mock.calls.map((c) => String(c[0]));
398
- expect(logCalls.some((msg) => msg.includes('already initialized with all tools'))).toBe(true);
399
- });
400
- it('uses flags directly in re-init mode without prompting', async () => {
401
- ;
402
- fs.pathExistsSync.mockImplementation((p) => {
403
- if (p.includes('.claude/settings.json'))
404
- return true;
405
- return false;
406
- });
407
- await initCommand({ cursor: true });
408
- // Should NOT have prompted
409
- expect(confirm).not.toHaveBeenCalled();
410
- expect(checkbox).not.toHaveBeenCalled();
411
- // Should have created cursor files
412
- const writeCalls = fs.writeFile.mock.calls;
413
- const cursorFiles = writeCalls.filter(c => c[0].includes('.cursor/'));
414
- expect(cursorFiles.length).toBeGreaterThan(0);
415
- });
416
- });
417
- describe('--force flag', () => {
418
- it('overwrites existing files when --force is set', async () => {
419
- ;
420
- fs.pathExists.mockImplementation(async (p) => {
421
- if (p.endsWith('.git'))
422
- return true;
423
- if (p.endsWith('AGENTS.md'))
424
- return true;
425
- return false;
426
- });
427
- await initCommand({ force: true, claude: true });
428
- // Should create AGENTS.md even though it already exists
429
- const writeCalls = fs.writeFile.mock.calls;
430
- const agentsWrite = writeCalls.find(c => c[0].endsWith('AGENTS.md'));
431
- expect(agentsWrite).toBeDefined();
432
- });
433
- it('does not prompt for re-init when --force is set', async () => {
434
- ;
435
- fs.pathExists.mockImplementation(async (p) => {
436
- if (p.endsWith('.git'))
437
- return true;
438
- if (p.endsWith('AGENTS.md'))
439
- return true;
440
- return false;
441
- });
442
- await initCommand({ force: true, claude: true });
443
- expect(confirm).not.toHaveBeenCalled();
444
- });
445
- });
446
- describe('template fetch failure', () => {
447
- it('handles session expired error', async () => {
448
- ;
449
- fetchTemplates.mockRejectedValue(new Error('Token expired'));
450
- await expect(initCommand({ claude: true })).rejects.toThrow('process.exit(1)');
451
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
452
- expect(errorCalls.some((msg) => msg.includes('expired'))).toBe(true);
453
- });
454
- it('handles network error', async () => {
455
- ;
456
- fetchTemplates.mockRejectedValue(new Error('Failed to fetch'));
457
- await expect(initCommand({ claude: true })).rejects.toThrow('process.exit(1)');
458
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
459
- expect(errorCalls.some((msg) => msg.includes('Could not reach'))).toBe(true);
460
- });
461
- it('handles generic error', async () => {
462
- ;
463
- fetchTemplates.mockRejectedValue(new Error('Something broke'));
464
- await expect(initCommand({ claude: true })).rejects.toThrow('process.exit(1)');
465
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
466
- expect(errorCalls.some((msg) => msg.includes('Something broke'))).toBe(true);
467
- });
468
- });
469
- describe('other tool selection', () => {
470
- it('prints Slack message when "other" is selected', async () => {
471
- ;
472
- checkbox.mockResolvedValue(['other']);
473
- await initCommand({});
474
- const logCalls = console.log.mock.calls.map((c) => String(c[0]));
475
- expect(logCalls.some((msg) => msg.includes('Slack'))).toBe(true);
476
- });
477
- });
478
- });
@@ -1 +0,0 @@
1
- export declare function loginCommand(): Promise<void>;
@@ -1,90 +0,0 @@
1
- import chalk from 'chalk';
2
- import http from 'http';
3
- import { URL } from 'url';
4
- import { exec } from 'child_process';
5
- import { createMeltClient } from '@meltstudio/meltctl-sdk';
6
- import { API_BASE, storeAuth } from '../utils/auth.js';
7
- import { printBanner } from '../utils/banner.js';
8
- const LOGIN_TIMEOUT_MS = 60_000;
9
- function openBrowser(url) {
10
- const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
11
- exec(`${command} "${url}"`);
12
- }
13
- function findFreePort() {
14
- return new Promise((resolve, reject) => {
15
- const server = http.createServer();
16
- server.listen(0, () => {
17
- const address = server.address();
18
- if (address && typeof address === 'object') {
19
- const port = address.port;
20
- server.close(() => resolve(port));
21
- }
22
- else {
23
- reject(new Error('Could not find free port'));
24
- }
25
- });
26
- });
27
- }
28
- export async function loginCommand() {
29
- printBanner();
30
- console.log(chalk.bold(' Logging in to Melt...'));
31
- const port = await findFreePort();
32
- const redirectUri = `http://localhost:${port.toString()}`;
33
- const authCode = await new Promise((resolve, reject) => {
34
- const timeout = setTimeout(() => {
35
- server.close();
36
- reject(new Error('Authentication timed out. Please try again.'));
37
- }, LOGIN_TIMEOUT_MS);
38
- const server = http.createServer((req, res) => {
39
- const url = new URL(req.url ?? '/', `http://localhost:${port.toString()}`);
40
- const code = url.searchParams.get('code');
41
- const error = url.searchParams.get('error');
42
- if (error) {
43
- res.writeHead(200, { 'Content-Type': 'text/html' });
44
- res.end('<html><body><h2>Authentication failed</h2><p>You can close this tab.</p></body></html>');
45
- clearTimeout(timeout);
46
- server.close();
47
- reject(new Error(`Authentication denied: ${error}`));
48
- return;
49
- }
50
- if (!code) {
51
- res.writeHead(400, { 'Content-Type': 'text/html' });
52
- res.end('<html><body><h2>Missing authorization code</h2></body></html>');
53
- return;
54
- }
55
- res.writeHead(200, { 'Content-Type': 'text/html' });
56
- res.end('<html><body><h2>Authentication successful!</h2><p>You can close this tab and return to your terminal.</p></body></html>');
57
- clearTimeout(timeout);
58
- server.close();
59
- resolve(code);
60
- });
61
- server.listen(port, () => {
62
- const authUrl = `${API_BASE}/auth/google?redirect_uri=${encodeURIComponent(redirectUri)}`;
63
- console.log();
64
- console.log(chalk.bold(' A browser window will open shortly.'));
65
- console.log(chalk.dim(' Please log in with your @meltstudio.co Google Workspace account.'));
66
- console.log();
67
- setTimeout(() => {
68
- openBrowser(authUrl);
69
- console.log(chalk.dim(` If the browser didn't open, visit: ${authUrl}`));
70
- }, 2000);
71
- });
72
- });
73
- console.log(chalk.dim('Exchanging authorization code...'));
74
- // Use SDK with empty token for the unauthenticated token exchange
75
- const client = createMeltClient({ baseUrl: API_BASE, token: '' });
76
- try {
77
- const data = await client.auth.exchangeToken(authCode, redirectUri);
78
- await storeAuth({
79
- token: data.token,
80
- email: data.email,
81
- expiresAt: data.expiresAt,
82
- });
83
- console.log();
84
- console.log(chalk.green(`Logged in as ${data.email}`));
85
- }
86
- catch (error) {
87
- console.error(chalk.red(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
88
- process.exit(1);
89
- }
90
- }
@@ -1 +0,0 @@
1
- export {};