@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,194 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- vi.mock('../utils/auth.js', () => ({
3
- API_BASE: 'https://test-api.example.com',
4
- storeAuth: vi.fn(),
5
- }));
6
- vi.mock('../utils/banner.js', () => ({
7
- printBanner: vi.fn(),
8
- }));
9
- vi.mock('child_process', () => ({
10
- exec: vi.fn(),
11
- }));
12
- // We need to mock http.createServer to control the server behavior
13
- // Store the request handler so we can invoke it manually
14
- let capturedRequestHandler = null;
15
- const mockServer = {
16
- listen: vi.fn((_port, cb) => {
17
- if (cb)
18
- cb();
19
- return mockServer;
20
- }),
21
- close: vi.fn((cb) => {
22
- if (cb)
23
- cb();
24
- return mockServer;
25
- }),
26
- address: vi.fn(() => ({ port: 12345 })),
27
- };
28
- vi.mock('http', () => ({
29
- default: {
30
- createServer: vi.fn((handler) => {
31
- // First call is from findFreePort - returns a server that finds a free port
32
- // Second call is the actual OAuth server
33
- if (!capturedRequestHandler && handler) {
34
- capturedRequestHandler = handler;
35
- }
36
- return mockServer;
37
- }),
38
- },
39
- createServer: vi.fn((handler) => {
40
- if (!capturedRequestHandler && handler) {
41
- capturedRequestHandler = handler;
42
- }
43
- return mockServer;
44
- }),
45
- }));
46
- import { storeAuth, API_BASE } from '../utils/auth.js';
47
- import { loginCommand } from './login.js';
48
- function createMockReq(urlPath) {
49
- return { url: urlPath };
50
- }
51
- function createMockRes() {
52
- const res = {
53
- _body: '',
54
- _statusCode: 200,
55
- writeHead: vi.fn(function (code) {
56
- this._statusCode = code;
57
- return res;
58
- }),
59
- end: vi.fn(function (body) {
60
- this._body = body ?? '';
61
- return res;
62
- }),
63
- };
64
- return res;
65
- }
66
- beforeEach(() => {
67
- vi.clearAllMocks();
68
- capturedRequestHandler = null;
69
- vi.spyOn(console, 'log').mockImplementation(() => { });
70
- vi.spyOn(console, 'error').mockImplementation(() => { });
71
- vi.spyOn(process, 'exit').mockImplementation((code) => {
72
- throw new Error(`process.exit(${code})`);
73
- });
74
- });
75
- afterEach(() => {
76
- vi.unstubAllGlobals();
77
- });
78
- describe('loginCommand', () => {
79
- it('handles successful OAuth callback and stores auth', async () => {
80
- const mockTokenResponse = {
81
- token: 'jwt-token-123',
82
- email: 'dev@meltstudio.co',
83
- expiresAt: '2026-04-02T00:00:00Z',
84
- };
85
- const fetchMock = vi.fn().mockResolvedValue({
86
- ok: true,
87
- json: vi.fn().mockResolvedValue(mockTokenResponse),
88
- });
89
- vi.stubGlobal('fetch', fetchMock);
90
- // Start loginCommand - it will call findFreePort then createServer
91
- const loginPromise = loginCommand();
92
- // Allow microtasks to settle so createServer and listen are called
93
- await new Promise(r => setTimeout(r, 10));
94
- // Simulate the OAuth callback with a valid code
95
- expect(capturedRequestHandler).not.toBeNull();
96
- const req = createMockReq('/?code=auth-code-xyz');
97
- const res = createMockRes();
98
- capturedRequestHandler(req, res);
99
- await loginPromise;
100
- expect(fetchMock).toHaveBeenCalledWith(`${API_BASE}/auth/token`, expect.objectContaining({
101
- method: 'POST',
102
- body: expect.stringContaining('auth-code-xyz'),
103
- }));
104
- expect(storeAuth).toHaveBeenCalledWith({
105
- token: 'jwt-token-123',
106
- email: 'dev@meltstudio.co',
107
- expiresAt: '2026-04-02T00:00:00Z',
108
- });
109
- expect(res._statusCode).toBe(200);
110
- expect(res._body).toContain('successful');
111
- });
112
- it('exits with error when token exchange returns 403', async () => {
113
- const fetchMock = vi.fn().mockResolvedValue({
114
- ok: false,
115
- status: 403,
116
- json: vi.fn().mockResolvedValue({ error: 'Forbidden' }),
117
- });
118
- vi.stubGlobal('fetch', fetchMock);
119
- const loginPromise = loginCommand();
120
- await new Promise(r => setTimeout(r, 10));
121
- const req = createMockReq('/?code=test-code');
122
- const res = createMockRes();
123
- capturedRequestHandler(req, res);
124
- await expect(loginPromise).rejects.toThrow('process.exit(1)');
125
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
126
- expect(errorCalls.some((msg) => msg.includes('meltstudio.co'))).toBe(true);
127
- });
128
- it('exits with error when token exchange returns generic failure', async () => {
129
- const fetchMock = vi.fn().mockResolvedValue({
130
- ok: false,
131
- status: 500,
132
- json: vi.fn().mockResolvedValue({ error: 'Server error' }),
133
- });
134
- vi.stubGlobal('fetch', fetchMock);
135
- const loginPromise = loginCommand();
136
- await new Promise(r => setTimeout(r, 10));
137
- const req = createMockReq('/?code=test-code');
138
- const res = createMockRes();
139
- capturedRequestHandler(req, res);
140
- await expect(loginPromise).rejects.toThrow('process.exit(1)');
141
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
142
- expect(errorCalls.some((msg) => msg.includes('Server error'))).toBe(true);
143
- });
144
- it('rejects when OAuth callback returns error param', async () => {
145
- const loginPromise = loginCommand();
146
- await new Promise(r => setTimeout(r, 10));
147
- const req = createMockReq('/?error=access_denied');
148
- const res = createMockRes();
149
- capturedRequestHandler(req, res);
150
- await expect(loginPromise).rejects.toThrow('Authentication denied: access_denied');
151
- expect(res._statusCode).toBe(200);
152
- expect(res._body).toContain('failed');
153
- });
154
- it('handles token response with missing fields gracefully', async () => {
155
- // Token response missing expiresAt
156
- const fetchMock = vi.fn().mockResolvedValue({
157
- ok: true,
158
- json: vi.fn().mockResolvedValue({ token: 'jwt-token', email: 'dev@meltstudio.co' }),
159
- });
160
- vi.stubGlobal('fetch', fetchMock);
161
- const loginPromise = loginCommand();
162
- await new Promise(r => setTimeout(r, 10));
163
- const req = createMockReq('/?code=test-code');
164
- const res = createMockRes();
165
- capturedRequestHandler(req, res);
166
- await loginPromise;
167
- // storeAuth is called with undefined for expiresAt since the field is missing
168
- expect(storeAuth).toHaveBeenCalledWith({
169
- token: 'jwt-token',
170
- email: 'dev@meltstudio.co',
171
- expiresAt: undefined,
172
- });
173
- });
174
- it('returns 400 when callback has no code or error', async () => {
175
- const fetchMock = vi.fn().mockResolvedValue({
176
- ok: true,
177
- json: vi.fn().mockResolvedValue({ token: 't', email: 'e@meltstudio.co', expiresAt: 'x' }),
178
- });
179
- vi.stubGlobal('fetch', fetchMock);
180
- const loginPromise = loginCommand();
181
- await new Promise(r => setTimeout(r, 10));
182
- // Send request with no params
183
- const req1 = createMockReq('/');
184
- const res1 = createMockRes();
185
- capturedRequestHandler(req1, res1);
186
- expect(res1._statusCode).toBe(400);
187
- expect(res1._body).toContain('Missing authorization code');
188
- // Send valid code to complete
189
- const req2 = createMockReq('/?code=valid');
190
- const res2 = createMockRes();
191
- capturedRequestHandler(req2, res2);
192
- await loginPromise;
193
- });
194
- });
@@ -1 +0,0 @@
1
- export declare function logoutCommand(): Promise<void>;
@@ -1,12 +0,0 @@
1
- import chalk from 'chalk';
2
- import { clearAuth, getStoredAuth } from '../utils/auth.js';
3
- export async function logoutCommand() {
4
- const auth = await getStoredAuth();
5
- await clearAuth();
6
- if (auth) {
7
- console.log(chalk.green(`Logged out (was ${auth.email})`));
8
- }
9
- else {
10
- console.log(chalk.dim('No active session found.'));
11
- }
12
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,59 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- vi.mock('../utils/auth.js', () => ({
3
- getStoredAuth: vi.fn(),
4
- clearAuth: vi.fn(),
5
- }));
6
- import { getStoredAuth, clearAuth } from '../utils/auth.js';
7
- import { logoutCommand } from './logout.js';
8
- beforeEach(() => {
9
- vi.clearAllMocks();
10
- vi.spyOn(console, 'log').mockImplementation(() => { });
11
- vi.spyOn(console, 'error').mockImplementation(() => { });
12
- vi.spyOn(process, 'exit').mockImplementation((code) => {
13
- throw new Error(`process.exit(${code})`);
14
- });
15
- });
16
- describe('logoutCommand', () => {
17
- it('clears auth and shows logged-out message with email when session exists', async () => {
18
- ;
19
- getStoredAuth.mockResolvedValue({
20
- token: 'jwt-token',
21
- email: 'dev@meltstudio.co',
22
- expiresAt: '2026-04-01T00:00:00Z',
23
- });
24
- clearAuth.mockResolvedValue(undefined);
25
- await logoutCommand();
26
- expect(clearAuth).toHaveBeenCalled();
27
- const logCalls = console.log.mock.calls.map((c) => c[0]);
28
- expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('Logged out'))).toBe(true);
29
- expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('dev@meltstudio.co'))).toBe(true);
30
- });
31
- it('clears auth and shows "no active session" when no session exists', async () => {
32
- ;
33
- getStoredAuth.mockResolvedValue(undefined);
34
- clearAuth.mockResolvedValue(undefined);
35
- await logoutCommand();
36
- expect(clearAuth).toHaveBeenCalled();
37
- const logCalls = console.log.mock.calls.map((c) => c[0]);
38
- expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('No active session'))).toBe(true);
39
- });
40
- it('always calls clearAuth regardless of session state', async () => {
41
- ;
42
- getStoredAuth.mockResolvedValue(undefined);
43
- clearAuth.mockResolvedValue(undefined);
44
- await logoutCommand();
45
- expect(clearAuth).toHaveBeenCalledTimes(1);
46
- });
47
- it('calls getStoredAuth before clearAuth', async () => {
48
- const callOrder = [];
49
- getStoredAuth.mockImplementation(async () => {
50
- callOrder.push('getStoredAuth');
51
- return { token: 'tok', email: 'a@b.co', expiresAt: '2026-04-01T00:00:00Z' };
52
- });
53
- clearAuth.mockImplementation(async () => {
54
- callOrder.push('clearAuth');
55
- });
56
- await logoutCommand();
57
- expect(callOrder).toEqual(['getStoredAuth', 'clearAuth']);
58
- });
59
- });
@@ -1,6 +0,0 @@
1
- export declare function planSubmitCommand(file?: string): Promise<void>;
2
- export declare function planListCommand(options: {
3
- repository?: string;
4
- author?: string;
5
- limit?: string;
6
- }): Promise<void>;
@@ -1,123 +0,0 @@
1
- import chalk from 'chalk';
2
- import fs from 'fs-extra';
3
- import path from 'path';
4
- import { getClient } from '../utils/api.js';
5
- import { getGitBranch, getGitCommit, getGitRepository, getProjectName, extractTicketId, findMdFiles, } from '../utils/git.js';
6
- function extractFrontmatterStatus(content) {
7
- const match = content.match(/^---\n([\s\S]*?)\n---/);
8
- if (!match)
9
- return null;
10
- const statusMatch = match[1].match(/^status:\s*(.+)$/m);
11
- return statusMatch ? statusMatch[1].trim() : null;
12
- }
13
- async function autoDetectPlanFile() {
14
- const cwd = process.cwd();
15
- const plansDir = path.join(cwd, '.plans');
16
- const mdFiles = await findMdFiles(plansDir);
17
- if (mdFiles.length === 0) {
18
- return null;
19
- }
20
- const branch = getGitBranch();
21
- const ticketId = extractTicketId(branch);
22
- if (ticketId) {
23
- const ticketLower = ticketId.toLowerCase();
24
- const match = mdFiles.find(f => path.basename(f).toLowerCase().includes(ticketLower));
25
- if (match) {
26
- return match;
27
- }
28
- }
29
- return mdFiles[0] ?? null;
30
- }
31
- export async function planSubmitCommand(file) {
32
- const client = await getClient();
33
- let filePath;
34
- if (file) {
35
- filePath = path.resolve(file);
36
- }
37
- else {
38
- const detected = await autoDetectPlanFile();
39
- if (!detected) {
40
- console.error(chalk.red('No plan file found. Provide a file path or create a plan in the .plans/ directory.'));
41
- process.exit(1);
42
- }
43
- filePath = detected;
44
- console.log(chalk.dim(`Auto-detected plan file: ${path.relative(process.cwd(), filePath)}`));
45
- }
46
- if (!(await fs.pathExists(filePath))) {
47
- console.error(chalk.red(`File not found: ${filePath}`));
48
- process.exit(1);
49
- }
50
- const content = await fs.readFile(filePath, 'utf-8');
51
- const filename = path.basename(filePath);
52
- const project = getProjectName();
53
- const branch = getGitBranch();
54
- const commit = getGitCommit();
55
- const repo = getGitRepository();
56
- const ticket = extractTicketId(branch) ?? extractTicketId(filename);
57
- const status = extractFrontmatterStatus(content);
58
- try {
59
- const result = await client.plans.submit({
60
- project,
61
- repository: repo?.slug ?? null,
62
- repositoryUrl: repo?.url ?? null,
63
- branch,
64
- commit,
65
- content,
66
- ticket: ticket ?? null,
67
- status: status ?? null,
68
- metadata: { filename },
69
- });
70
- if (result.created) {
71
- console.log(chalk.green(`\n ✓ Plan submitted! ID: ${result.id}\n`));
72
- }
73
- else {
74
- console.log(chalk.green(`\n ✓ Plan updated! ID: ${result.id}\n`));
75
- }
76
- }
77
- catch (error) {
78
- console.error(chalk.red(`Failed to submit plan: ${error instanceof Error ? error.message : 'Unknown error'}`));
79
- process.exit(1);
80
- }
81
- }
82
- export async function planListCommand(options) {
83
- const client = await getClient();
84
- try {
85
- const body = await client.plans.list({
86
- repository: options.repository,
87
- author: options.author,
88
- limit: options.limit ? parseInt(options.limit, 10) : undefined,
89
- });
90
- if (body.plans.length === 0) {
91
- console.log(chalk.dim('\n No plans found.\n'));
92
- return;
93
- }
94
- console.log(chalk.bold(`\n Plans (${body.count}):\n`));
95
- console.log(chalk.dim(` ${'TICKET'.padEnd(14)} ${'STATUS'.padEnd(12)} ${'REPOSITORY'.padEnd(36)} ${'AUTHOR'.padEnd(30)} UPDATED`));
96
- console.log();
97
- const statusColors = {
98
- submitted: chalk.dim,
99
- approved: chalk.cyan,
100
- validated: chalk.green,
101
- reviewed: chalk.magenta,
102
- };
103
- for (const p of body.plans) {
104
- const date = new Date(p.updatedAt).toLocaleDateString('en-US', {
105
- month: 'short',
106
- day: 'numeric',
107
- year: 'numeric',
108
- hour: '2-digit',
109
- minute: '2-digit',
110
- });
111
- const repo = p.repository ?? p.project;
112
- const ticket = p.ticket ?? '-';
113
- const colorFn = statusColors[p.status] ?? chalk.dim;
114
- console.log(` ${chalk.white(ticket.padEnd(14))} ${colorFn(p.status.padEnd(12))} ${chalk.white(repo.padEnd(36))} ${chalk.dim(p.author.padEnd(30))} ${chalk.dim(date)}`);
115
- }
116
- console.log();
117
- }
118
- catch (error) {
119
- const msg = error instanceof Error ? error.message : 'Unknown error';
120
- console.error(chalk.red(`Failed to list plans: ${msg}`));
121
- process.exit(1);
122
- }
123
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,246 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- vi.mock('fs-extra', () => ({
3
- default: {
4
- pathExists: vi.fn(),
5
- readFile: vi.fn(),
6
- },
7
- }));
8
- const mockClient = vi.hoisted(() => ({
9
- plans: {
10
- submit: vi.fn(),
11
- list: vi.fn(),
12
- },
13
- }));
14
- vi.mock('../utils/api.js', () => ({
15
- getClient: vi.fn().mockResolvedValue(mockClient),
16
- }));
17
- vi.mock('../utils/git.js', () => ({
18
- getGitBranch: vi.fn(),
19
- getGitCommit: vi.fn(),
20
- getGitRepository: vi.fn(),
21
- getProjectName: vi.fn(),
22
- extractTicketId: vi.fn(),
23
- findMdFiles: vi.fn(),
24
- }));
25
- import fs from 'fs-extra';
26
- import { getGitBranch, getGitCommit, getGitRepository, getProjectName, extractTicketId, findMdFiles, } from '../utils/git.js';
27
- import { planSubmitCommand, planListCommand } from './plan.js';
28
- beforeEach(() => {
29
- vi.clearAllMocks();
30
- vi.spyOn(console, 'log').mockImplementation(() => { });
31
- vi.spyOn(console, 'error').mockImplementation(() => { });
32
- vi.spyOn(process, 'exit').mockImplementation((code) => {
33
- throw new Error(`process.exit(${code})`);
34
- });
35
- });
36
- describe('planSubmitCommand', () => {
37
- function setupGitMocks() {
38
- ;
39
- getGitBranch.mockReturnValue('feature/PROJ-123-add-feature');
40
- getGitCommit.mockReturnValue('def5678');
41
- getGitRepository.mockReturnValue({
42
- slug: 'Org/Repo',
43
- url: 'https://github.com/Org/Repo.git',
44
- });
45
- getProjectName.mockReturnValue('test-project');
46
- extractTicketId.mockReturnValue('PROJ-123');
47
- }
48
- it('submits a plan with correct payload fields', async () => {
49
- setupGitMocks();
50
- fs.pathExists.mockResolvedValue(true);
51
- fs.readFile.mockResolvedValue('# Plan\nSome plan content.');
52
- mockClient.plans.submit.mockResolvedValue({
53
- id: 'plan-100',
54
- createdAt: '2026-03-26T00:00:00Z',
55
- updatedAt: '2026-03-26T00:00:00Z',
56
- created: true,
57
- });
58
- await planSubmitCommand('PROJ-123-plan.md');
59
- expect(mockClient.plans.submit).toHaveBeenCalledWith({
60
- project: 'test-project',
61
- repository: 'Org/Repo',
62
- repositoryUrl: 'https://github.com/Org/Repo.git',
63
- branch: 'feature/PROJ-123-add-feature',
64
- commit: 'def5678',
65
- content: '# Plan\nSome plan content.',
66
- metadata: { filename: 'PROJ-123-plan.md' },
67
- ticket: 'PROJ-123',
68
- status: null,
69
- });
70
- });
71
- it('logs "Plan submitted" when API returns created: true', async () => {
72
- setupGitMocks();
73
- fs.pathExists.mockResolvedValue(true);
74
- fs.readFile.mockResolvedValue('plan content');
75
- mockClient.plans.submit.mockResolvedValue({
76
- id: 'plan-new',
77
- createdAt: '2026-03-26T00:00:00Z',
78
- updatedAt: '2026-03-26T00:00:00Z',
79
- created: true,
80
- });
81
- await planSubmitCommand('plan.md');
82
- const logCalls = console.log.mock.calls.map((c) => c[0]);
83
- expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('Plan submitted'))).toBe(true);
84
- });
85
- it('logs "Plan updated" when API returns created: false', async () => {
86
- setupGitMocks();
87
- fs.pathExists.mockResolvedValue(true);
88
- fs.readFile.mockResolvedValue('updated plan content');
89
- mockClient.plans.submit.mockResolvedValue({
90
- id: 'plan-upd',
91
- createdAt: '2026-03-26T00:00:00Z',
92
- updatedAt: '2026-03-26T00:00:00Z',
93
- created: false,
94
- });
95
- await planSubmitCommand('plan.md');
96
- const logCalls = console.log.mock.calls.map((c) => c[0]);
97
- expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('Plan updated'))).toBe(true);
98
- });
99
- it('exits with error when file not found', async () => {
100
- setupGitMocks();
101
- fs.pathExists.mockResolvedValue(false);
102
- await expect(planSubmitCommand('nonexistent.md')).rejects.toThrow('process.exit(1)');
103
- expect(console.error).toHaveBeenCalled();
104
- });
105
- it('exits with error when API returns failure', async () => {
106
- setupGitMocks();
107
- fs.pathExists.mockResolvedValue(true);
108
- fs.readFile.mockResolvedValue('content');
109
- mockClient.plans.submit.mockRejectedValue(new Error('Invalid content'));
110
- await expect(planSubmitCommand('plan.md')).rejects.toThrow('process.exit(1)');
111
- });
112
- it('auto-detects plan file from .plans/ directory when no file provided', async () => {
113
- setupGitMocks();
114
- findMdFiles.mockResolvedValue(['/project/.plans/PROJ-123-plan.md']);
115
- extractTicketId.mockReturnValue('PROJ-123');
116
- fs.pathExists.mockResolvedValue(true);
117
- fs.readFile.mockResolvedValue('auto-detected plan');
118
- mockClient.plans.submit.mockResolvedValue({
119
- id: 'plan-auto',
120
- createdAt: '2026-03-26T00:00:00Z',
121
- updatedAt: '2026-03-26T00:00:00Z',
122
- created: true,
123
- });
124
- await planSubmitCommand();
125
- expect(mockClient.plans.submit).toHaveBeenCalledWith(expect.objectContaining({
126
- content: 'auto-detected plan',
127
- }));
128
- });
129
- it('exits with error when no file provided and none auto-detected', async () => {
130
- ;
131
- findMdFiles.mockResolvedValue([]);
132
- getGitBranch.mockReturnValue('main');
133
- extractTicketId.mockReturnValue(null);
134
- await expect(planSubmitCommand()).rejects.toThrow('process.exit(1)');
135
- expect(console.error).toHaveBeenCalled();
136
- });
137
- it('does not include ticket in payload when extractTicketId returns null', async () => {
138
- ;
139
- getGitBranch.mockReturnValue('main');
140
- getGitCommit.mockReturnValue('abc1234');
141
- getGitRepository.mockReturnValue({
142
- slug: 'Org/Repo',
143
- url: 'https://github.com/Org/Repo.git',
144
- });
145
- getProjectName.mockReturnValue('test-project');
146
- extractTicketId.mockReturnValue(null);
147
- fs.pathExists.mockResolvedValue(true);
148
- fs.readFile.mockResolvedValue('plan without ticket');
149
- mockClient.plans.submit.mockResolvedValue({
150
- id: 'plan-no-ticket',
151
- createdAt: '2026-03-26T00:00:00Z',
152
- updatedAt: '2026-03-26T00:00:00Z',
153
- created: true,
154
- });
155
- await planSubmitCommand('plan.md');
156
- expect(mockClient.plans.submit).toHaveBeenCalledWith(expect.objectContaining({
157
- ticket: null,
158
- }));
159
- });
160
- it('includes status from frontmatter when present', async () => {
161
- setupGitMocks();
162
- fs.pathExists.mockResolvedValue(true);
163
- fs.readFile.mockResolvedValue('---\nstatus: approved\n---\n# Plan');
164
- mockClient.plans.submit.mockResolvedValue({
165
- id: 'plan-status',
166
- createdAt: '2026-03-26T00:00:00Z',
167
- updatedAt: '2026-03-26T00:00:00Z',
168
- created: true,
169
- });
170
- await planSubmitCommand('plan.md');
171
- expect(mockClient.plans.submit).toHaveBeenCalledWith(expect.objectContaining({
172
- status: 'approved',
173
- }));
174
- });
175
- it('does not include status when no frontmatter present', async () => {
176
- setupGitMocks();
177
- fs.pathExists.mockResolvedValue(true);
178
- fs.readFile.mockResolvedValue('# Plan\nNo frontmatter here.');
179
- mockClient.plans.submit.mockResolvedValue({
180
- id: 'plan-no-status',
181
- createdAt: '2026-03-26T00:00:00Z',
182
- updatedAt: '2026-03-26T00:00:00Z',
183
- created: true,
184
- });
185
- await planSubmitCommand('plan.md');
186
- expect(mockClient.plans.submit).toHaveBeenCalledWith(expect.objectContaining({
187
- status: null,
188
- }));
189
- });
190
- });
191
- describe('planListCommand', () => {
192
- it('calls API with correct query params', async () => {
193
- mockClient.plans.list.mockResolvedValue({ plans: [], count: 0 });
194
- await planListCommand({ repository: 'Org/Repo', author: 'dev@meltstudio.co', limit: '5' });
195
- expect(mockClient.plans.list).toHaveBeenCalledWith({
196
- repository: 'Org/Repo',
197
- author: 'dev@meltstudio.co',
198
- limit: 5,
199
- });
200
- });
201
- it('exits with error on 403 response', async () => {
202
- mockClient.plans.list.mockRejectedValue(new Error('Access denied. Only Team Managers can list plans.'));
203
- await expect(planListCommand({})).rejects.toThrow('process.exit(1)');
204
- expect(console.error).toHaveBeenCalled();
205
- });
206
- it('displays plan list when plans exist', async () => {
207
- mockClient.plans.list.mockResolvedValue({
208
- plans: [
209
- {
210
- id: '1',
211
- project: 'my-project',
212
- repository: 'Org/Repo',
213
- author: 'dev@meltstudio.co',
214
- branch: 'feature/PROJ-123',
215
- commit: 'abc1234',
216
- ticket: 'PROJ-123',
217
- status: 'submitted',
218
- createdAt: '2026-03-25T10:00:00Z',
219
- updatedAt: '2026-03-25T12:00:00Z',
220
- },
221
- ],
222
- count: 1,
223
- });
224
- await planListCommand({});
225
- expect(console.log).toHaveBeenCalled();
226
- });
227
- it('shows "No plans found" when list is empty', async () => {
228
- mockClient.plans.list.mockResolvedValue({ plans: [], count: 0 });
229
- await planListCommand({});
230
- const logCalls = console.log.mock.calls.map((c) => c[0]);
231
- expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('No plans found'))).toBe(true);
232
- });
233
- it('calls API without query params when no options provided', async () => {
234
- mockClient.plans.list.mockResolvedValue({ plans: [], count: 0 });
235
- await planListCommand({});
236
- expect(mockClient.plans.list).toHaveBeenCalledWith({
237
- repository: undefined,
238
- author: undefined,
239
- limit: undefined,
240
- });
241
- });
242
- it('exits with error on non-403 failure response', async () => {
243
- mockClient.plans.list.mockRejectedValue(new Error('Server error'));
244
- await expect(planListCommand({})).rejects.toThrow('process.exit(1)');
245
- });
246
- });
@@ -1,7 +0,0 @@
1
- interface StandupOptions {
2
- yesterday?: string;
3
- today?: string;
4
- blockers?: string;
5
- }
6
- export declare function standupCommand(options: StandupOptions): Promise<void>;
7
- export {};