puter-cli 1.8.6 → 2.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.
@@ -0,0 +1,274 @@
1
+ import { it } from "vitest";
2
+ import { beforeEach } from "vitest";
3
+ import { describe } from "vitest";
4
+ import { expect } from "vitest";
5
+ import { vi } from "vitest";
6
+
7
+ const mockConfig = {
8
+ get: vi.fn(),
9
+ set: vi.fn(),
10
+ clear: vi.fn(),
11
+ delete: vi.fn(),
12
+ };
13
+
14
+ vi.mock('conf', () => {
15
+ const Conf = vi.fn(() => mockConfig);
16
+ return { default: Conf };
17
+ });
18
+
19
+ vi.mock('../src/commons.js', () => ({
20
+ BASE_URL: 'https://puter.com',
21
+ NULL_UUID: '00000000-0000-0000-0000-000000000000',
22
+ PROJECT_NAME: 'puter-cli',
23
+ getHeaders: vi.fn(() => ({ 'Content-Type': 'application/json' })),
24
+ reconfigureURLs: vi.fn(),
25
+ }));
26
+
27
+ vi.mock('./PuterModule.js', () => ({
28
+ initPuterModule: vi.fn(),
29
+ }));
30
+
31
+ let initProfileModule;
32
+ let getProfileModule;
33
+
34
+ beforeEach(async () => {
35
+ vi.resetModules();
36
+ vi.clearAllMocks();
37
+ mockConfig.get.mockReset();
38
+ mockConfig.set.mockReset();
39
+ mockConfig.delete.mockReset();
40
+ const module = await import('../src/modules/ProfileModule');
41
+ initProfileModule = module.initProfileModule;
42
+ getProfileModule = module.getProfileModule;
43
+ });
44
+
45
+ describe("initProfileModule", () => {
46
+ it("should initialize profile module", () => {
47
+ initProfileModule();
48
+ const profileModule = getProfileModule();
49
+ expect(profileModule).toBeDefined();
50
+ })
51
+ })
52
+
53
+ describe('getProfileModule', () => {
54
+ it('should return profile module if initialized', () => {
55
+ initProfileModule();
56
+ const result = getProfileModule();
57
+ expect(result).toBeDefined();
58
+ });
59
+
60
+ it('should throw error if not initialized', () => {
61
+ expect(() => getProfileModule()).toThrow('Call initprofileModule() first');
62
+ });
63
+ });
64
+
65
+ describe('ProfileModule.getProfiles', () => {
66
+ it('should return profiles from config', () => {
67
+ const mockProfiles = [
68
+ { uuid: '1', username: 'user1', host: 'https://puter.com' },
69
+ { uuid: '2', username: 'user2', host: 'https://puter.com' },
70
+ ];
71
+ mockConfig.get.mockReturnValue(mockProfiles);
72
+
73
+ initProfileModule();
74
+ const profileModule = getProfileModule();
75
+ const profiles = profileModule.getProfiles();
76
+
77
+ expect(profiles).toEqual(mockProfiles);
78
+ expect(mockConfig.get).toHaveBeenCalledWith('profiles');
79
+ });
80
+
81
+ it('should return empty array if no profiles exist', () => {
82
+ mockConfig.get.mockReturnValue(undefined);
83
+
84
+ initProfileModule();
85
+ const profileModule = getProfileModule();
86
+ const profiles = profileModule.getProfiles();
87
+
88
+ expect(profiles).toEqual([]);
89
+ });
90
+ });
91
+
92
+ describe('ProfileModule.addProfile', () => {
93
+ it('should add a new profile to existing profiles', () => {
94
+ const existingProfiles = [
95
+ { uuid: '1', username: 'user1', host: 'https://puter.com' },
96
+ ];
97
+ const newProfile = { uuid: '2', username: 'user2', host: 'https://puter.com' };
98
+ mockConfig.get.mockReturnValue(existingProfiles);
99
+
100
+ initProfileModule();
101
+ const profileModule = getProfileModule();
102
+ profileModule.addProfile(newProfile);
103
+
104
+ expect(mockConfig.set).toHaveBeenCalledWith('profiles', [...existingProfiles, newProfile]);
105
+ });
106
+
107
+ it('should filter out transient profiles when adding', () => {
108
+ const existingProfiles = [
109
+ { uuid: '1', username: 'user1', host: 'https://puter.com', transient: true },
110
+ { uuid: '2', username: 'user2', host: 'https://puter.com' },
111
+ ];
112
+ const newProfile = { uuid: '3', username: 'user3', host: 'https://puter.com' };
113
+ mockConfig.get.mockReturnValue(existingProfiles);
114
+
115
+ initProfileModule();
116
+ const profileModule = getProfileModule();
117
+ profileModule.addProfile(newProfile);
118
+
119
+ expect(mockConfig.set).toHaveBeenCalledWith('profiles', [
120
+ { uuid: '2', username: 'user2', host: 'https://puter.com' },
121
+ newProfile,
122
+ ]);
123
+ });
124
+ });
125
+
126
+ describe('ProfileModule.selectProfile', () => {
127
+ it('should set selected profile in config', () => {
128
+ const profile = { uuid: 'test-uuid', username: 'testuser', host: 'https://puter.com' };
129
+ mockConfig.get.mockImplementation((key) => {
130
+ if (key === 'profiles') return [profile];
131
+ if (key === 'selected_profile') return 'test-uuid';
132
+ return undefined;
133
+ });
134
+
135
+ initProfileModule();
136
+ const profileModule = getProfileModule();
137
+ profileModule.selectProfile(profile);
138
+
139
+ expect(mockConfig.set).toHaveBeenCalledWith('selected_profile', 'test-uuid');
140
+ expect(mockConfig.set).toHaveBeenCalledWith('username', 'testuser');
141
+ expect(mockConfig.set).toHaveBeenCalledWith('cwd', '/testuser');
142
+ });
143
+ });
144
+
145
+ describe('ProfileModule.getCurrentProfile', () => {
146
+ it('should return the currently selected profile', () => {
147
+ const profiles = [
148
+ { uuid: '1', username: 'user1', host: 'https://puter.com' },
149
+ { uuid: '2', username: 'user2', host: 'https://puter.com' },
150
+ ];
151
+ mockConfig.get.mockImplementation((key) => {
152
+ if (key === 'profiles') return profiles;
153
+ if (key === 'selected_profile') return '2';
154
+ return undefined;
155
+ });
156
+
157
+ initProfileModule();
158
+ const profileModule = getProfileModule();
159
+ const currentProfile = profileModule.getCurrentProfile();
160
+
161
+ expect(currentProfile).toEqual(profiles[1]);
162
+ });
163
+
164
+ it('should return undefined if no profile matches', () => {
165
+ const profiles = [
166
+ { uuid: '1', username: 'user1', host: 'https://puter.com' },
167
+ ];
168
+ mockConfig.get.mockImplementation((key) => {
169
+ if (key === 'profiles') return profiles;
170
+ if (key === 'selected_profile') return 'non-existent';
171
+ return undefined;
172
+ });
173
+
174
+ initProfileModule();
175
+ const profileModule = getProfileModule();
176
+ const currentProfile = profileModule.getCurrentProfile();
177
+
178
+ expect(currentProfile).toBeUndefined();
179
+ });
180
+ });
181
+
182
+ describe('ProfileModule.getAuthToken', () => {
183
+ it('should return auth token for selected profile', () => {
184
+ const profiles = [
185
+ { uuid: '1', username: 'user1', host: 'https://puter.com', token: 'token1' },
186
+ { uuid: '2', username: 'user2', host: 'https://puter.com', token: 'token2' },
187
+ ];
188
+ mockConfig.get.mockImplementation((key) => {
189
+ if (key === 'profiles') return profiles;
190
+ if (key === 'selected_profile') return '2';
191
+ return undefined;
192
+ });
193
+
194
+ initProfileModule();
195
+ const profileModule = getProfileModule();
196
+ const token = profileModule.getAuthToken();
197
+
198
+ expect(token).toBe('token2');
199
+ });
200
+
201
+ it('should return undefined if no profile is selected', () => {
202
+ const profiles = [
203
+ { uuid: '1', username: 'user1', host: 'https://puter.com', token: 'token1' },
204
+ ];
205
+ mockConfig.get.mockImplementation((key) => {
206
+ if (key === 'profiles') return profiles;
207
+ if (key === 'selected_profile') return undefined;
208
+ return undefined;
209
+ });
210
+
211
+ initProfileModule();
212
+ const profileModule = getProfileModule();
213
+ const token = profileModule.getAuthToken();
214
+
215
+ expect(token).toBeUndefined();
216
+ });
217
+ });
218
+
219
+ describe('ProfileModule.getDefaultProfile', () => {
220
+ it('should return default profile if auth_token exists', () => {
221
+ mockConfig.get.mockImplementation((key) => {
222
+ if (key === 'auth_token') return 'legacy-token';
223
+ if (key === 'username') return 'legacyuser';
224
+ return undefined;
225
+ });
226
+
227
+ initProfileModule();
228
+ const profileModule = getProfileModule();
229
+ const defaultProfile = profileModule.getDefaultProfile();
230
+
231
+ expect(defaultProfile).toEqual({
232
+ host: 'puter.com',
233
+ username: 'legacyuser',
234
+ token: 'legacy-token',
235
+ });
236
+ });
237
+
238
+ it('should return undefined if no auth_token exists', () => {
239
+ mockConfig.get.mockReturnValue(undefined);
240
+
241
+ initProfileModule();
242
+ const profileModule = getProfileModule();
243
+ const defaultProfile = profileModule.getDefaultProfile();
244
+
245
+ expect(defaultProfile).toBeUndefined();
246
+ });
247
+ });
248
+
249
+ describe('ProfileModule.migrateLegacyConfig', () => {
250
+ it('should migrate legacy config to profile format', () => {
251
+ mockConfig.get.mockImplementation((key) => {
252
+ if (key === 'auth_token') return 'legacy-token';
253
+ if (key === 'username') return 'legacyuser';
254
+ if (key === 'profiles') return [];
255
+ return undefined;
256
+ });
257
+
258
+ initProfileModule();
259
+ const profileModule = getProfileModule();
260
+ profileModule.migrateLegacyConfig();
261
+
262
+ expect(mockConfig.set).toHaveBeenCalledWith('profiles', [
263
+ {
264
+ host: 'https://puter.com',
265
+ username: 'legacyuser',
266
+ cwd: '/legacyuser',
267
+ token: 'legacy-token',
268
+ uuid: '00000000-0000-0000-0000-000000000000',
269
+ },
270
+ ]);
271
+ expect(mockConfig.delete).toHaveBeenCalledWith('auth_token');
272
+ expect(mockConfig.delete).toHaveBeenCalledWith('username');
273
+ });
274
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ vi.spyOn(console, 'log').mockImplementation(() => { });
4
+
5
+ const mockSetAuthToken = vi.hoisted(() => vi.fn());
6
+
7
+ vi.mock('conf', () => {
8
+ const Conf = vi.fn(() => ({
9
+ get: vi.fn((key) => {
10
+ if (key === 'selected_profile') return 'test-uuid';
11
+ if (key === 'profiles') return [{
12
+ uuid: 'test-uuid',
13
+ token: 'test-token'
14
+ }];
15
+ return null;
16
+ }),
17
+ set: vi.fn(),
18
+ clear: vi.fn(),
19
+ }));
20
+ return { default: Conf };
21
+ });
22
+
23
+ vi.mock('@heyputer/puter.js', () => ({
24
+ puter: {
25
+ setAuthToken: mockSetAuthToken,
26
+ }
27
+ }));
28
+
29
+ let initPuterModule;
30
+ let getPuter;
31
+
32
+ beforeEach(async () => {
33
+ vi.resetModules();
34
+ const module = await import('../src/modules/PuterModule.js');
35
+ initPuterModule = module.initPuterModule;
36
+ getPuter = module.getPuter;
37
+ });
38
+
39
+ describe('initPuterModule', () => {
40
+ it('should call setAuthToken when initialized', () => {
41
+ initPuterModule();
42
+ expect(mockSetAuthToken).toHaveBeenCalledWith(expect.stringContaining('test-token'));
43
+ });
44
+ });
45
+
46
+ describe('getPuter', () => {
47
+ it('should return puter module if initialized', () => {
48
+ initPuterModule();
49
+ const result = getPuter();
50
+ expect(result).toBeDefined();
51
+ });
52
+
53
+ it('should throw error if not initialized', () => {
54
+ expect(() => getPuter()).toThrow('Call initPuterModule() first');
55
+ });
56
+ });
@@ -0,0 +1,194 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { listApps, appInfo, createApp, updateApp, deleteApp } from '../src/commands/apps.js';
3
+ import chalk from 'chalk';
4
+ import Table from 'cli-table3';
5
+ import * as PuterModule from '../src/modules/PuterModule.js';
6
+ import * as subdomains from '../src/commands/subdomains.js';
7
+ import * as sites from '../src/commands/sites.js';
8
+ import * as files from '../src/commands/files.js';
9
+ import * as auth from '../src/commands/auth.js';
10
+ import * as commons from '../src/commons.js';
11
+ import * as utils from '../src/utils.js';
12
+ import crypto from '../src/crypto.js';
13
+
14
+ // Mock console to prevent actual logging
15
+ vi.spyOn(console, 'log').mockImplementation(() => {});
16
+ vi.spyOn(console, 'error').mockImplementation(() => {});
17
+
18
+ vi.mock("conf", () => {
19
+ const Conf = vi.fn(() => ({
20
+ get: vi.fn(),
21
+ set: vi.fn(),
22
+ clear: vi.fn(),
23
+ }));
24
+ return { default: Conf };
25
+ });
26
+
27
+ // Mock dependencies
28
+ vi.mock('chalk', () => ({
29
+ default: {
30
+ green: vi.fn(text => text),
31
+ red: vi.fn(text => text),
32
+ dim: vi.fn(text => text),
33
+ yellow: vi.fn(text => text),
34
+ cyan: vi.fn(text => text),
35
+ cyanBright: vi.fn(text => text),
36
+ bold: vi.fn(text => text),
37
+ }
38
+ }));
39
+ vi.mock('cli-table3');
40
+ vi.mock('node-fetch');
41
+ vi.mock('../src/modules/PuterModule.js');
42
+ vi.mock('../src/commands/subdomains.js');
43
+ vi.mock('../src/commands/sites.js');
44
+ vi.mock('../src/commands/files.js');
45
+ vi.mock('../src/commands/auth.js');
46
+ vi.mock('../src/commons.js');
47
+ vi.mock('../src/utils.js');
48
+ vi.mock('../src/crypto.js');
49
+
50
+ const mockPuter = {
51
+ apps: {
52
+ list: vi.fn(),
53
+ get: vi.fn(),
54
+ create: vi.fn(),
55
+ update: vi.fn(),
56
+ delete: vi.fn(),
57
+ },
58
+ fs: {
59
+ mkdir: vi.fn(),
60
+ },
61
+ };
62
+
63
+ describe('apps.js', () => {
64
+ let mockTable;
65
+
66
+ beforeEach(() => {
67
+ vi.clearAllMocks();
68
+ vi.spyOn(PuterModule, 'getPuter').mockReturnValue(mockPuter);
69
+ vi.spyOn(auth, 'getCurrentDirectory').mockReturnValue('/testuser');
70
+ vi.spyOn(commons, 'isValidAppName').mockReturnValue(true);
71
+ vi.spyOn(commons, 'resolvePath').mockImplementation((_, newPath) => newPath);
72
+ vi.spyOn(crypto, 'randomUUID').mockReturnValue('mock-uuid');
73
+
74
+ mockTable = {
75
+ push: vi.fn(),
76
+ toString: vi.fn(() => 'table string'),
77
+ };
78
+ Table.mockImplementation(() => mockTable);
79
+ });
80
+
81
+ describe('listApps', () => {
82
+ it('should list apps successfully', async () => {
83
+ const mockApps = [
84
+ { title: 'App 1', name: 'app-1', created_at: new Date().toISOString(), index_url: 'https://app-1.puter.site', stats: { open_count: 10, user_count: 5 } },
85
+ { title: 'App 2', name: 'app-2', created_at: new Date().toISOString(), index_url: 'https://app-2.puter.site', stats: { open_count: 20, user_count: 15 } },
86
+ ];
87
+ mockPuter.apps.list.mockResolvedValue(mockApps);
88
+ vi.spyOn(utils, 'formatDate').mockReturnValue('formatted-date');
89
+
90
+ await listApps();
91
+
92
+ expect(mockPuter.apps.list).toHaveBeenCalled();
93
+ expect(mockTable.push).toHaveBeenCalledTimes(2);
94
+ expect(console.log).toHaveBeenCalledWith('table string');
95
+ });
96
+
97
+ it('should handle API error when listing apps', async () => {
98
+ mockPuter.apps.list.mockRejectedValue(new Error('API Error'));
99
+ await listApps();
100
+ expect(console.error).toHaveBeenCalledWith('Failed to list apps. Error: API Error');
101
+ });
102
+ });
103
+
104
+ describe('appInfo', () => {
105
+ it('should show app info successfully', async () => {
106
+ const mockApp = { name: 'test-app', title: 'Test App' };
107
+ mockPuter.apps.get.mockResolvedValue(mockApp);
108
+ vi.spyOn(utils, 'displayNonNullValues').mockImplementation(() => {});
109
+
110
+ await appInfo(['test-app']);
111
+
112
+ expect(mockPuter.apps.get).toHaveBeenCalledWith('test-app');
113
+ expect(utils.displayNonNullValues).toHaveBeenCalledWith(mockApp);
114
+ });
115
+
116
+ it('should show usage if no app name is provided', async () => {
117
+ await appInfo([]);
118
+ expect(console.log).toHaveBeenCalledWith(chalk.red('Usage: app <name>'));
119
+ });
120
+
121
+ it('should handle app not found', async () => {
122
+ mockPuter.apps.get.mockResolvedValue(null);
123
+ await appInfo(['non-existent-app']);
124
+ expect(console.error).toHaveBeenCalledWith(chalk.red('Could not find this app.'));
125
+ });
126
+ });
127
+
128
+ describe('createApp', () => {
129
+ beforeEach(() => {
130
+ mockPuter.apps.create.mockResolvedValue({ uid: 'app-uid', name: 'new-app', owner: { username: 'testuser' } });
131
+ mockPuter.fs.mkdir.mockResolvedValue({ uid: 'dir-uid', name: 'app-mock-uuid' });
132
+ vi.spyOn(subdomains, 'createSubdomain').mockResolvedValue(true);
133
+ vi.spyOn(files, 'createFile').mockResolvedValue(true);
134
+ mockPuter.apps.update.mockResolvedValue(true);
135
+ })
136
+ it('should create an app successfully', async () => {
137
+ await createApp({ name: 'new-app' });
138
+
139
+ expect(mockPuter.apps.create).toHaveBeenCalled();
140
+ expect(mockPuter.fs.mkdir).toHaveBeenCalled();
141
+ expect(subdomains.createSubdomain).toHaveBeenCalled();
142
+ expect(files.createFile).toHaveBeenCalled();
143
+ expect(mockPuter.apps.update).toHaveBeenCalled();
144
+ expect(console.log).toHaveBeenCalledWith(chalk.green('App deployed successfully at:'));
145
+ });
146
+
147
+ it('should show usage if app name is invalid', async () => {
148
+ vi.spyOn(commons, 'isValidAppName').mockReturnValue(false);
149
+ await createApp({ name: 'invalid-' });
150
+ expect(console.log).toHaveBeenCalledWith(chalk.red('Usage: app:create <name> <directory>'));
151
+ });
152
+ });
153
+
154
+ describe('updateApp', () => {
155
+ it('should update an app successfully', async () => {
156
+ mockPuter.apps.get.mockResolvedValue({ uid: 'app-uid', name: 'test-app', owner: { username: 'testuser' }, index_url: 'https://test.puter.site' });
157
+ vi.spyOn(files, 'pathExists').mockResolvedValue(true);
158
+ vi.spyOn(subdomains, 'getSubdomains').mockResolvedValue([{ root_dir: { dirname: 'app-uid', path: '/path/to/app' }, uid: 'sub-uid' }]);
159
+ vi.spyOn(files, 'listRemoteFiles').mockResolvedValue([{ name: 'index.html' }]);
160
+ vi.spyOn(files, 'copyFile').mockResolvedValue(true);
161
+ vi.spyOn(files, 'removeFileOrDirectory').mockResolvedValue(true);
162
+
163
+ await updateApp(['test-app', '.']);
164
+
165
+ expect(mockPuter.apps.get).toHaveBeenCalledWith('test-app');
166
+ expect(files.listRemoteFiles).toHaveBeenCalled();
167
+ expect(files.copyFile).toHaveBeenCalled();
168
+ expect(console.log).toHaveBeenCalledWith(chalk.green('App updated successfully at:'));
169
+ });
170
+ });
171
+
172
+ describe('deleteApp', () => {
173
+ it('should delete an app successfully', async () => {
174
+ mockPuter.apps.get.mockResolvedValue({ uid: 'app-uid', name: 'test-app', title: 'Test App', created_at: new Date().toISOString() });
175
+ mockPuter.apps.delete.mockResolvedValue(true);
176
+ vi.spyOn(subdomains, 'getSubdomains').mockResolvedValue([{ root_dir: { dirname: 'app-uid' }, uid: 'sub-uid' }]);
177
+ vi.spyOn(sites, 'deleteSite').mockResolvedValue(true);
178
+
179
+ const result = await deleteApp('test-app');
180
+
181
+ expect(result).toBe(true);
182
+ expect(mockPuter.apps.delete).toHaveBeenCalledWith('test-app');
183
+ expect(sites.deleteSite).toHaveBeenCalled();
184
+ expect(console.log).toHaveBeenCalledWith(chalk.green('App "test-app" deleted successfully!'));
185
+ });
186
+
187
+ it('should return false if app not found', async () => {
188
+ mockPuter.apps.get.mockResolvedValue(null);
189
+ const result = await deleteApp('non-existent-app');
190
+ expect(result).toBe(false);
191
+ expect(console.log).toHaveBeenCalledWith(chalk.red('App "non-existent-app" not found.'));
192
+ });
193
+ });
194
+ });