@meltstudio/meltctl 4.28.0 → 4.29.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.
- package/dist/commands/audit.d.ts +3 -0
- package/dist/commands/audit.js +48 -0
- package/dist/commands/audit.test.d.ts +1 -0
- package/dist/commands/audit.test.js +322 -0
- package/dist/commands/coins.test.d.ts +1 -0
- package/dist/commands/coins.test.js +133 -0
- package/dist/commands/feedback.test.d.ts +1 -0
- package/dist/commands/feedback.test.js +242 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/init.test.js +476 -0
- package/dist/commands/login.test.d.ts +1 -0
- package/dist/commands/login.test.js +194 -0
- package/dist/commands/logout.test.d.ts +1 -0
- package/dist/commands/logout.test.js +59 -0
- package/dist/commands/plan.test.d.ts +1 -0
- package/dist/commands/plan.test.js +283 -0
- package/dist/commands/standup.test.d.ts +1 -0
- package/dist/commands/standup.test.js +252 -0
- package/dist/commands/templates.test.d.ts +1 -0
- package/dist/commands/templates.test.js +89 -0
- package/dist/commands/version.test.d.ts +1 -0
- package/dist/commands/version.test.js +86 -0
- package/dist/index.js +9 -1
- package/dist/utils/api.test.d.ts +1 -0
- package/dist/utils/api.test.js +96 -0
- package/dist/utils/auth.test.d.ts +1 -0
- package/dist/utils/auth.test.js +165 -0
- package/dist/utils/banner.test.d.ts +1 -0
- package/dist/utils/banner.test.js +34 -0
- package/dist/utils/git.test.d.ts +1 -0
- package/dist/utils/git.test.js +184 -0
- package/dist/utils/package-manager.test.d.ts +1 -0
- package/dist/utils/package-manager.test.js +76 -0
- package/dist/utils/templates.test.d.ts +1 -0
- package/dist/utils/templates.test.js +50 -0
- package/dist/utils/version-check.test.d.ts +1 -0
- package/dist/utils/version-check.test.js +135 -0
- package/package.json +2 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('child_process', () => ({
|
|
3
|
+
execSync: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('fs-extra', () => ({
|
|
6
|
+
default: {
|
|
7
|
+
pathExistsSync: vi.fn(),
|
|
8
|
+
readJsonSync: vi.fn(),
|
|
9
|
+
pathExists: vi.fn(),
|
|
10
|
+
readdir: vi.fn(),
|
|
11
|
+
stat: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
import fs from 'fs-extra';
|
|
16
|
+
import { getGitBranch, getGitCommit, getGitRepository, getProjectName, extractTicketId, findMdFiles, } from './git.js';
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
describe('getGitBranch', () => {
|
|
21
|
+
it('returns trimmed branch name', () => {
|
|
22
|
+
;
|
|
23
|
+
execSync.mockReturnValue(' feature/my-branch\n');
|
|
24
|
+
expect(getGitBranch()).toBe('feature/my-branch');
|
|
25
|
+
});
|
|
26
|
+
it('returns "unknown" when git command fails', () => {
|
|
27
|
+
;
|
|
28
|
+
execSync.mockImplementation(() => {
|
|
29
|
+
throw new Error('not a git repo');
|
|
30
|
+
});
|
|
31
|
+
expect(getGitBranch()).toBe('unknown');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('getGitCommit', () => {
|
|
35
|
+
it('returns trimmed commit hash', () => {
|
|
36
|
+
;
|
|
37
|
+
execSync.mockReturnValue('abc1234\n');
|
|
38
|
+
expect(getGitCommit()).toBe('abc1234');
|
|
39
|
+
});
|
|
40
|
+
it('returns "unknown" when git command fails', () => {
|
|
41
|
+
;
|
|
42
|
+
execSync.mockImplementation(() => {
|
|
43
|
+
throw new Error('not a git repo');
|
|
44
|
+
});
|
|
45
|
+
expect(getGitCommit()).toBe('unknown');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('getGitRepository', () => {
|
|
49
|
+
it('parses SSH URL format', () => {
|
|
50
|
+
;
|
|
51
|
+
execSync.mockReturnValue('git@github.com:Owner/Repo.git\n');
|
|
52
|
+
const result = getGitRepository();
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
slug: 'Owner/Repo',
|
|
55
|
+
url: 'git@github.com:Owner/Repo.git',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
it('parses HTTPS URL format', () => {
|
|
59
|
+
;
|
|
60
|
+
execSync.mockReturnValue('https://github.com/Owner/Repo.git\n');
|
|
61
|
+
const result = getGitRepository();
|
|
62
|
+
expect(result).toEqual({
|
|
63
|
+
slug: 'Owner/Repo',
|
|
64
|
+
url: 'https://github.com/Owner/Repo.git',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
it('parses HTTPS URL without .git suffix', () => {
|
|
68
|
+
;
|
|
69
|
+
execSync.mockReturnValue('https://github.com/Owner/Repo\n');
|
|
70
|
+
const result = getGitRepository();
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
slug: 'Owner/Repo',
|
|
73
|
+
url: 'https://github.com/Owner/Repo',
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it('parses GitLab SSH URL', () => {
|
|
77
|
+
;
|
|
78
|
+
execSync.mockReturnValue('git@gitlab.com:My-Org/My-Project.git\n');
|
|
79
|
+
const result = getGitRepository();
|
|
80
|
+
expect(result).toEqual({
|
|
81
|
+
slug: 'My-Org/My-Project',
|
|
82
|
+
url: 'git@gitlab.com:My-Org/My-Project.git',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
it('returns null when git command fails', () => {
|
|
86
|
+
;
|
|
87
|
+
execSync.mockImplementation(() => {
|
|
88
|
+
throw new Error('no remote');
|
|
89
|
+
});
|
|
90
|
+
expect(getGitRepository()).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
it('falls back to raw URL when regex does not match', () => {
|
|
93
|
+
;
|
|
94
|
+
execSync.mockReturnValue('some-weird-url\n');
|
|
95
|
+
const result = getGitRepository();
|
|
96
|
+
expect(result).toEqual({
|
|
97
|
+
slug: 'some-weird-url',
|
|
98
|
+
url: 'some-weird-url',
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('getProjectName', () => {
|
|
103
|
+
it('returns name from package.json when available', () => {
|
|
104
|
+
;
|
|
105
|
+
fs.pathExistsSync.mockReturnValue(true);
|
|
106
|
+
fs.readJsonSync.mockReturnValue({ name: '@meltstudio/meltctl' });
|
|
107
|
+
expect(getProjectName()).toBe('@meltstudio/meltctl');
|
|
108
|
+
});
|
|
109
|
+
it('returns directory basename when package.json has no name', () => {
|
|
110
|
+
;
|
|
111
|
+
fs.pathExistsSync.mockReturnValue(true);
|
|
112
|
+
fs.readJsonSync.mockReturnValue({});
|
|
113
|
+
const result = getProjectName();
|
|
114
|
+
// Should be the basename of cwd
|
|
115
|
+
expect(typeof result).toBe('string');
|
|
116
|
+
expect(result.length).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
it('returns directory basename when package.json does not exist', () => {
|
|
119
|
+
;
|
|
120
|
+
fs.pathExistsSync.mockReturnValue(false);
|
|
121
|
+
const result = getProjectName();
|
|
122
|
+
expect(typeof result).toBe('string');
|
|
123
|
+
expect(result.length).toBeGreaterThan(0);
|
|
124
|
+
});
|
|
125
|
+
it('returns directory basename when readJsonSync throws', () => {
|
|
126
|
+
;
|
|
127
|
+
fs.pathExistsSync.mockReturnValue(true);
|
|
128
|
+
fs.readJsonSync.mockImplementation(() => {
|
|
129
|
+
throw new Error('parse error');
|
|
130
|
+
});
|
|
131
|
+
const result = getProjectName();
|
|
132
|
+
expect(typeof result).toBe('string');
|
|
133
|
+
expect(result.length).toBeGreaterThan(0);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('extractTicketId', () => {
|
|
137
|
+
it('extracts ticket ID from branch name', () => {
|
|
138
|
+
expect(extractTicketId('feature/PROJ-123-add-login')).toBe('PROJ-123');
|
|
139
|
+
});
|
|
140
|
+
it('extracts ticket ID case-insensitively', () => {
|
|
141
|
+
expect(extractTicketId('fix/proj-456')).toBe('proj-456');
|
|
142
|
+
});
|
|
143
|
+
it('returns null when no ticket ID found', () => {
|
|
144
|
+
expect(extractTicketId('main')).toBeNull();
|
|
145
|
+
expect(extractTicketId('feature/add-login')).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('findMdFiles', () => {
|
|
149
|
+
it('returns empty array when directory does not exist', async () => {
|
|
150
|
+
;
|
|
151
|
+
fs.pathExists.mockResolvedValue(false);
|
|
152
|
+
const result = await findMdFiles('/nonexistent');
|
|
153
|
+
expect(result).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
it('finds and sorts .md files by mtime descending', async () => {
|
|
156
|
+
;
|
|
157
|
+
fs.pathExists.mockResolvedValue(true);
|
|
158
|
+
fs.readdir.mockResolvedValue([
|
|
159
|
+
{ name: 'old.md', isDirectory: () => false, isFile: () => true },
|
|
160
|
+
{ name: 'new.md', isDirectory: () => false, isFile: () => true },
|
|
161
|
+
{ name: 'not-md.txt', isDirectory: () => false, isFile: () => true },
|
|
162
|
+
]);
|
|
163
|
+
fs.stat
|
|
164
|
+
.mockResolvedValueOnce({ mtimeMs: 1000 }) // old.md
|
|
165
|
+
.mockResolvedValueOnce({ mtimeMs: 2000 }); // new.md
|
|
166
|
+
const result = await findMdFiles('/test-dir');
|
|
167
|
+
expect(result).toEqual(['/test-dir/new.md', '/test-dir/old.md']);
|
|
168
|
+
});
|
|
169
|
+
it('recurses into subdirectories', async () => {
|
|
170
|
+
;
|
|
171
|
+
fs.pathExists.mockResolvedValue(true);
|
|
172
|
+
fs.readdir
|
|
173
|
+
.mockResolvedValueOnce([
|
|
174
|
+
{ name: 'sub', isDirectory: () => true, isFile: () => false },
|
|
175
|
+
{ name: 'root.md', isDirectory: () => false, isFile: () => true },
|
|
176
|
+
])
|
|
177
|
+
.mockResolvedValueOnce([{ name: 'nested.md', isDirectory: () => false, isFile: () => true }]);
|
|
178
|
+
fs.stat
|
|
179
|
+
.mockResolvedValueOnce({ mtimeMs: 4000 }) // sub/nested.md
|
|
180
|
+
.mockResolvedValueOnce({ mtimeMs: 3000 }); // root.md
|
|
181
|
+
const result = await findMdFiles('/project');
|
|
182
|
+
expect(result).toEqual(['/project/sub/nested.md', '/project/root.md']);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('child_process', () => ({
|
|
3
|
+
execSync: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { detectPackageManager, getUpdateInstructions } from './package-manager.js';
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
describe('detectPackageManager', () => {
|
|
11
|
+
it('returns npm when npm list succeeds', () => {
|
|
12
|
+
vi.mocked(execSync).mockReturnValueOnce('');
|
|
13
|
+
const result = detectPackageManager();
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
type: 'npm',
|
|
16
|
+
updateCommand: 'npm install -g @meltstudio/meltctl@latest',
|
|
17
|
+
});
|
|
18
|
+
expect(execSync).toHaveBeenCalledWith('npm list -g @meltstudio/meltctl', expect.objectContaining({ encoding: 'utf-8', stdio: 'pipe' }));
|
|
19
|
+
});
|
|
20
|
+
it('returns yarn when npm fails but yarn succeeds', () => {
|
|
21
|
+
vi.mocked(execSync)
|
|
22
|
+
.mockImplementationOnce(() => {
|
|
23
|
+
throw new Error('npm not found');
|
|
24
|
+
})
|
|
25
|
+
.mockReturnValueOnce('');
|
|
26
|
+
const result = detectPackageManager();
|
|
27
|
+
expect(result).toEqual({
|
|
28
|
+
type: 'yarn',
|
|
29
|
+
updateCommand: 'yarn global add @meltstudio/meltctl@latest',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it('returns unknown when both npm and yarn fail', () => {
|
|
33
|
+
vi.mocked(execSync)
|
|
34
|
+
.mockImplementationOnce(() => {
|
|
35
|
+
throw new Error('npm not found');
|
|
36
|
+
})
|
|
37
|
+
.mockImplementationOnce(() => {
|
|
38
|
+
throw new Error('yarn not found');
|
|
39
|
+
});
|
|
40
|
+
const result = detectPackageManager();
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
type: 'unknown',
|
|
43
|
+
updateCommand: 'npm install -g @meltstudio/meltctl@latest',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('getUpdateInstructions', () => {
|
|
48
|
+
it('returns npm-first instructions when detected via npm', () => {
|
|
49
|
+
vi.mocked(execSync).mockReturnValueOnce('');
|
|
50
|
+
const instructions = getUpdateInstructions();
|
|
51
|
+
expect(instructions[0]).toBe('npm install -g @meltstudio/meltctl@latest');
|
|
52
|
+
expect(instructions.some(l => l.includes('yarn'))).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('returns yarn-first instructions when detected via yarn', () => {
|
|
55
|
+
vi.mocked(execSync)
|
|
56
|
+
.mockImplementationOnce(() => {
|
|
57
|
+
throw new Error('npm not found');
|
|
58
|
+
})
|
|
59
|
+
.mockReturnValueOnce('');
|
|
60
|
+
const instructions = getUpdateInstructions();
|
|
61
|
+
expect(instructions[0]).toBe('yarn global add @meltstudio/meltctl@latest');
|
|
62
|
+
expect(instructions.some(l => l.includes('npm'))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('returns npm default instructions when detection fails', () => {
|
|
65
|
+
vi.mocked(execSync)
|
|
66
|
+
.mockImplementationOnce(() => {
|
|
67
|
+
throw new Error('npm not found');
|
|
68
|
+
})
|
|
69
|
+
.mockImplementationOnce(() => {
|
|
70
|
+
throw new Error('yarn not found');
|
|
71
|
+
});
|
|
72
|
+
const instructions = getUpdateInstructions();
|
|
73
|
+
expect(instructions[0]).toBe('npm install -g @meltstudio/meltctl@latest');
|
|
74
|
+
expect(instructions.some(l => l.includes('yarn'))).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('./auth.js', () => ({
|
|
3
|
+
authenticatedFetch: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
import { authenticatedFetch } from './auth.js';
|
|
6
|
+
import { fetchTemplates } from './templates.js';
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
describe('fetchTemplates', () => {
|
|
11
|
+
it('returns template files on success', async () => {
|
|
12
|
+
const mockFiles = {
|
|
13
|
+
'AGENTS.md': '# Agents content',
|
|
14
|
+
'.claude/skills/setup.md': '# Setup skill',
|
|
15
|
+
};
|
|
16
|
+
authenticatedFetch.mockResolvedValue({
|
|
17
|
+
ok: true,
|
|
18
|
+
json: vi.fn().mockResolvedValue({ files: mockFiles }),
|
|
19
|
+
});
|
|
20
|
+
const result = await fetchTemplates();
|
|
21
|
+
expect(authenticatedFetch).toHaveBeenCalledWith('/templates');
|
|
22
|
+
expect(result).toEqual(mockFiles);
|
|
23
|
+
});
|
|
24
|
+
it('throws error when API returns failure', async () => {
|
|
25
|
+
;
|
|
26
|
+
authenticatedFetch.mockResolvedValue({
|
|
27
|
+
ok: false,
|
|
28
|
+
statusText: 'Unauthorized',
|
|
29
|
+
});
|
|
30
|
+
await expect(fetchTemplates()).rejects.toThrow('Failed to fetch templates: Unauthorized');
|
|
31
|
+
});
|
|
32
|
+
it('throws error when API returns 500', async () => {
|
|
33
|
+
;
|
|
34
|
+
authenticatedFetch.mockResolvedValue({
|
|
35
|
+
ok: false,
|
|
36
|
+
statusText: 'Internal Server Error',
|
|
37
|
+
});
|
|
38
|
+
await expect(fetchTemplates()).rejects.toThrow('Failed to fetch templates: Internal Server Error');
|
|
39
|
+
});
|
|
40
|
+
it('calls authenticatedFetch with /templates path', async () => {
|
|
41
|
+
;
|
|
42
|
+
authenticatedFetch.mockResolvedValue({
|
|
43
|
+
ok: true,
|
|
44
|
+
json: vi.fn().mockResolvedValue({ files: {} }),
|
|
45
|
+
});
|
|
46
|
+
await fetchTemplates();
|
|
47
|
+
expect(authenticatedFetch).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(authenticatedFetch).toHaveBeenCalledWith('/templates');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('child_process', () => ({
|
|
3
|
+
execSync: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('fs-extra', () => ({
|
|
6
|
+
default: {
|
|
7
|
+
readJson: vi.fn(),
|
|
8
|
+
},
|
|
9
|
+
}));
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
import { getCurrentCliVersion, getLatestCliVersion, compareVersions, isCI, checkAndEnforceUpdate, } from './version-check.js';
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
16
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
17
|
+
// Clean up CI env vars
|
|
18
|
+
delete process.env.CI;
|
|
19
|
+
delete process.env.GITHUB_ACTIONS;
|
|
20
|
+
delete process.env.GITLAB_CI;
|
|
21
|
+
delete process.env.CIRCLECI;
|
|
22
|
+
delete process.env.TRAVIS;
|
|
23
|
+
delete process.env.JENKINS_URL;
|
|
24
|
+
delete process.env.BUILDKITE;
|
|
25
|
+
delete process.env.DRONE;
|
|
26
|
+
delete process.env.MELTCTL_SKIP_UPDATE_CHECK;
|
|
27
|
+
});
|
|
28
|
+
describe('getCurrentCliVersion', () => {
|
|
29
|
+
it('reads version from package.json', async () => {
|
|
30
|
+
;
|
|
31
|
+
fs.readJson.mockResolvedValue({ version: '4.26.0' });
|
|
32
|
+
const version = await getCurrentCliVersion();
|
|
33
|
+
expect(version).toBe('4.26.0');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('getLatestCliVersion', () => {
|
|
37
|
+
it('returns version from npm registry', async () => {
|
|
38
|
+
;
|
|
39
|
+
execSync.mockReturnValue('"4.27.0"\n');
|
|
40
|
+
const version = await getLatestCliVersion();
|
|
41
|
+
expect(version).toBe('4.27.0');
|
|
42
|
+
});
|
|
43
|
+
it('returns null on network error', async () => {
|
|
44
|
+
;
|
|
45
|
+
execSync.mockImplementation(() => {
|
|
46
|
+
throw new Error('network error');
|
|
47
|
+
});
|
|
48
|
+
const version = await getLatestCliVersion();
|
|
49
|
+
expect(version).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('compareVersions', () => {
|
|
53
|
+
it('returns true when latest major is higher', () => {
|
|
54
|
+
expect(compareVersions('1.0.0', '2.0.0')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('returns true when latest minor is higher', () => {
|
|
57
|
+
expect(compareVersions('1.0.0', '1.1.0')).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
it('returns true when latest patch is higher', () => {
|
|
60
|
+
expect(compareVersions('1.0.0', '1.0.1')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
it('returns false when versions are equal', () => {
|
|
63
|
+
expect(compareVersions('1.0.0', '1.0.0')).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it('returns false when current is newer', () => {
|
|
66
|
+
expect(compareVersions('2.0.0', '1.0.0')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
it('returns true when latest is stable and current is pre-release', () => {
|
|
69
|
+
expect(compareVersions('1.0.0-beta.1', '1.0.0')).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it('returns false when current is stable and latest is pre-release', () => {
|
|
72
|
+
expect(compareVersions('1.0.0', '1.0.0-beta.1')).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
it('compares pre-release strings lexicographically', () => {
|
|
75
|
+
expect(compareVersions('1.0.0-alpha', '1.0.0-beta')).toBe(true);
|
|
76
|
+
expect(compareVersions('1.0.0-beta', '1.0.0-alpha')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it('returns false when both pre-releases are equal', () => {
|
|
79
|
+
expect(compareVersions('1.0.0-beta', '1.0.0-beta')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('isCI', () => {
|
|
83
|
+
it('returns false in non-CI environment', () => {
|
|
84
|
+
expect(isCI()).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
it('returns true when CI env var is set', () => {
|
|
87
|
+
process.env.CI = 'true';
|
|
88
|
+
expect(isCI()).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
it('returns true when GITHUB_ACTIONS is set', () => {
|
|
91
|
+
process.env.GITHUB_ACTIONS = 'true';
|
|
92
|
+
expect(isCI()).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it('returns true when MELTCTL_SKIP_UPDATE_CHECK is set', () => {
|
|
95
|
+
process.env.MELTCTL_SKIP_UPDATE_CHECK = '1';
|
|
96
|
+
expect(isCI()).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('checkAndEnforceUpdate', () => {
|
|
100
|
+
it('skips check in CI environment', async () => {
|
|
101
|
+
process.env.CI = 'true';
|
|
102
|
+
await checkAndEnforceUpdate();
|
|
103
|
+
// Should return early without checking versions
|
|
104
|
+
expect(fs.readJson).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
it('allows continuing when latest version cannot be fetched', async () => {
|
|
107
|
+
;
|
|
108
|
+
fs.readJson.mockResolvedValue({ version: '4.26.0' });
|
|
109
|
+
execSync.mockImplementation(() => {
|
|
110
|
+
throw new Error('network error');
|
|
111
|
+
});
|
|
112
|
+
await checkAndEnforceUpdate();
|
|
113
|
+
// Should not throw or exit
|
|
114
|
+
});
|
|
115
|
+
it('allows continuing when versions are equal', async () => {
|
|
116
|
+
;
|
|
117
|
+
fs.readJson.mockResolvedValue({ version: '4.26.0' });
|
|
118
|
+
execSync.mockReturnValue('"4.26.0"\n');
|
|
119
|
+
await checkAndEnforceUpdate();
|
|
120
|
+
// Should not throw or exit
|
|
121
|
+
});
|
|
122
|
+
it('calls process.exit when update is required', async () => {
|
|
123
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
124
|
+
fs.readJson.mockResolvedValue({ version: '4.25.0' });
|
|
125
|
+
execSync.mockReturnValue('"4.26.0"\n');
|
|
126
|
+
await checkAndEnforceUpdate();
|
|
127
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
128
|
+
});
|
|
129
|
+
it('allows continuing when getCurrentCliVersion throws', async () => {
|
|
130
|
+
;
|
|
131
|
+
fs.readJson.mockRejectedValue(new Error('file not found'));
|
|
132
|
+
await checkAndEnforceUpdate();
|
|
133
|
+
// Should not throw - error is caught
|
|
134
|
+
});
|
|
135
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meltstudio/meltctl",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.29.0",
|
|
4
4
|
"description": "AI-first development tools for teams - set up AGENTS.md, Claude Code, Cursor, and OpenCode standards",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"type-check": "tsc --noEmit",
|
|
31
31
|
"format": "prettier --write src/**/*.ts",
|
|
32
32
|
"format:check": "prettier --check src/**/*.ts",
|
|
33
|
+
"test": "vitest run",
|
|
33
34
|
"prepublishOnly": "yarn build"
|
|
34
35
|
},
|
|
35
36
|
"keywords": [
|