@heyputer/shell 2.1.0 → 3.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.
@@ -29,11 +29,7 @@ beforeEach(async () => {
29
29
 
30
30
  describe('constants', () => {
31
31
  it('should export PROJECT_NAME', () => {
32
- expect(commons.PROJECT_NAME).toBe('puter-cli');
33
- });
34
-
35
- it('should export NULL_UUID', () => {
36
- expect(commons.NULL_UUID).toBe('00000000-0000-0000-0000-000000000000');
32
+ expect(commons.PROJECT_NAME).toBe('puter-sh');
37
33
  });
38
34
 
39
35
  it('should have default API_BASE', () => {
@@ -86,65 +82,6 @@ describe('getHeaders', () => {
86
82
  });
87
83
  });
88
84
 
89
- describe('generateAppName', () => {
90
- it('should generate a name with default separator', () => {
91
- const name = commons.generateAppName();
92
-
93
- expect(name).toMatch(/^[a-z]+-[a-z]+-\d+$/);
94
- });
95
-
96
- it('should generate a name with custom separator', () => {
97
- const name = commons.generateAppName('_');
98
-
99
- expect(name).toMatch(/^[a-z]+_[a-z]+_\d+$/);
100
- });
101
- });
102
-
103
- describe('displayTable', () => {
104
- it('should display a table with headers and data', () => {
105
- const consoleLogSpy = vi.spyOn(console, 'log');
106
-
107
- const data = [
108
- { name: 'App1', status: 'running' },
109
- { name: 'App2', status: 'stopped' },
110
- ];
111
-
112
- commons.displayTable(data, {
113
- headers: ['Name', 'Status'],
114
- columns: ['name', 'status'],
115
- columnWidth: 15,
116
- });
117
-
118
- expect(consoleLogSpy).toHaveBeenCalled();
119
- });
120
-
121
- it('should handle empty data', () => {
122
- const consoleLogSpy = vi.spyOn(console, 'log');
123
-
124
- commons.displayTable([], {
125
- headers: ['Name'],
126
- columns: ['name'],
127
- });
128
-
129
- expect(consoleLogSpy).toHaveBeenCalledTimes(2); // header + separator
130
- });
131
-
132
- it('should display N/A for missing values', () => {
133
- const consoleLogSpy = vi.spyOn(console, 'log');
134
-
135
- const data = [{ name: 'App1' }];
136
-
137
- commons.displayTable(data, {
138
- headers: ['Name', 'Status'],
139
- columns: ['name', 'status'],
140
- columnWidth: 10,
141
- });
142
-
143
- const calls = consoleLogSpy.mock.calls.flat();
144
- expect(calls.some(call => call.includes('N/A'))).toBe(true);
145
- });
146
- });
147
-
148
85
  describe('showDiskSpaceUsage', () => {
149
86
  it('should display disk usage information', () => {
150
87
  const consoleLogSpy = vi.spyOn(console, 'log');
@@ -209,94 +146,99 @@ describe('resolvePath', () => {
209
146
  it('should normalize duplicate slashes', () => {
210
147
  expect(commons.resolvePath('/home//user', 'documents')).toBe('/home/user/documents');
211
148
  });
212
- });
213
149
 
214
- describe('resolveRemotePath', () => {
215
- it('should return absolute path as-is', () => {
216
- expect(commons.resolveRemotePath('/home/user', '/absolute/path')).toBe('/absolute/path');
150
+ it('should re-root on an absolute path instead of appending it', () => {
151
+ expect(commons.resolvePath('/home/user', '/other/place')).toBe('/other/place');
217
152
  });
218
153
 
219
- it('should resolve relative path', () => {
220
- expect(commons.resolveRemotePath('/home/user', 'relative/path')).toBe('/home/user/relative/path');
154
+ it('should leave "~" unexpanded when no home is resolved yet', () => {
155
+ commons.setHomePath(null);
156
+ expect(commons.resolvePath('/home/user', '~/Desktop')).toBe('~/Desktop');
157
+ expect(commons.resolvePath('/home/user', '~')).toBe('~');
221
158
  });
222
- });
223
159
 
224
- describe('isValidAppName', () => {
225
- it('should return true for valid app name', () => {
226
- expect(commons.isValidAppName('my-app')).toBe(true);
160
+ it('should walk up within a home-anchored path', () => {
161
+ commons.setHomePath(null);
162
+ expect(commons.resolvePath('~/Desktop/notes', '..')).toBe('~/Desktop');
227
163
  });
228
164
 
229
- it('should return true for app name with spaces', () => {
230
- expect(commons.isValidAppName('my app')).toBe(true);
165
+ it('should clamp at the home anchor rather than escaping to root', () => {
166
+ commons.setHomePath(null);
167
+ expect(commons.resolvePath('~/Desktop', '../../..')).toBe('~');
231
168
  });
169
+ });
232
170
 
233
- it('should return false for empty string', () => {
234
- expect(commons.isValidAppName('')).toBe(false);
235
- });
171
+ describe('expandHome', () => {
172
+ afterEach(() => commons.setHomePath(null));
236
173
 
237
- it('should return false for whitespace only', () => {
238
- expect(commons.isValidAppName(' ')).toBe(false);
174
+ it('should expand "~" and "~/..." to the resolved home', () => {
175
+ commons.setHomePath('/alice');
176
+ expect(commons.expandHome('~')).toBe('/alice');
177
+ expect(commons.expandHome('~/Desktop')).toBe('/alice/Desktop');
239
178
  });
240
179
 
241
- it('should return false for reserved name "."', () => {
242
- expect(commons.isValidAppName('.')).toBe(false);
180
+ it('should leave non-home paths untouched', () => {
181
+ commons.setHomePath('/alice');
182
+ expect(commons.expandHome('/bob/public')).toBe('/bob/public');
183
+ expect(commons.expandHome('notes.txt')).toBe('notes.txt');
243
184
  });
244
185
 
245
- it('should return false for reserved name ".."', () => {
246
- expect(commons.isValidAppName('..')).toBe(false);
186
+ it('should be a no-op before a home is resolved', () => {
187
+ commons.setHomePath(null);
188
+ expect(commons.expandHome('~/Desktop')).toBe('~/Desktop');
247
189
  });
248
190
 
249
- it('should return false for name with forward slash', () => {
250
- expect(commons.isValidAppName('my/app')).toBe(false);
191
+ it('should follow a username change without touching stored paths', () => {
192
+ commons.setHomePath('/alice');
193
+ expect(commons.expandHome('~/Desktop')).toBe('/alice/Desktop');
194
+ commons.setHomePath('/alice-renamed');
195
+ expect(commons.expandHome('~/Desktop')).toBe('/alice-renamed/Desktop');
251
196
  });
197
+ });
252
198
 
253
- it('should return false for name with backslash', () => {
254
- expect(commons.isValidAppName('my\\app')).toBe(false);
255
- });
199
+ describe('resolvePath with a resolved home', () => {
200
+ afterEach(() => commons.setHomePath(null));
256
201
 
257
- it('should return false for name with wildcard', () => {
258
- expect(commons.isValidAppName('my*app')).toBe(false);
202
+ it('should resolve "~" to the concrete home directory', () => {
203
+ commons.setHomePath('/alice');
204
+ expect(commons.resolvePath('/anywhere', '~')).toBe('/alice');
205
+ expect(commons.resolvePath('/anywhere', '~/Desktop')).toBe('/alice/Desktop');
259
206
  });
260
207
 
261
- it('should return false for non-string input', () => {
262
- expect(commons.isValidAppName(123)).toBe(false);
263
- expect(commons.isValidAppName(null)).toBe(false);
264
- expect(commons.isValidAppName(undefined)).toBe(false);
208
+ it('should resolve relative paths against a concrete cwd', () => {
209
+ commons.setHomePath('/alice');
210
+ expect(commons.resolvePath('/alice', 'Desktop')).toBe('/alice/Desktop');
211
+ expect(commons.resolvePath('/alice/Desktop', '..')).toBe('/alice');
265
212
  });
266
213
  });
267
214
 
268
- describe('getDefaultHomePage', () => {
269
- it('should generate HTML with app name', () => {
270
- const html = commons.getDefaultHomePage('TestApp');
271
-
272
- expect(html).toContain('<title>TestApp</title>');
273
- expect(html).toContain('Welcome to TestApp!');
215
+ describe('isAbsolutePath', () => {
216
+ it('should accept root- and home-anchored paths', () => {
217
+ expect(commons.isAbsolutePath('/a/b')).toBe(true);
218
+ expect(commons.isAbsolutePath('~')).toBe(true);
219
+ expect(commons.isAbsolutePath('~/a')).toBe(true);
274
220
  });
275
221
 
276
- it('should include CSS files when provided', () => {
277
- const html = commons.getDefaultHomePage('TestApp', [], ['style.css', 'theme.css']);
278
-
279
- expect(html).toContain('<link href="style.css" rel="stylesheet">');
280
- expect(html).toContain('<link href="theme.css" rel="stylesheet">');
222
+ it('should reject relative paths and non-strings', () => {
223
+ expect(commons.isAbsolutePath('a/b')).toBe(false);
224
+ expect(commons.isAbsolutePath('..')).toBe(false);
225
+ expect(commons.isAbsolutePath(undefined)).toBe(false);
281
226
  });
227
+ });
282
228
 
283
- it('should include JS files when provided', () => {
284
- const html = commons.getDefaultHomePage('TestApp', ['app.js', 'utils.js']);
285
-
286
- expect(html).toContain('<script type="text/babel" src="app.js"></script>');
287
- expect(html).toContain('<script src="utils.js"></script>');
229
+ describe('resolveRemotePath', () => {
230
+ it('should return absolute path as-is', () => {
231
+ expect(commons.resolveRemotePath('/home/user', '/absolute/path')).toBe('/absolute/path');
288
232
  });
289
233
 
290
- it('should use id="root" when react is included', () => {
291
- const html = commons.getDefaultHomePage('TestApp', ['react.js']);
292
-
293
- expect(html).toContain('id="root"');
234
+ it('should resolve relative path', () => {
235
+ expect(commons.resolveRemotePath('/home/user', 'relative/path')).toBe('/home/user/relative/path');
294
236
  });
295
237
 
296
- it('should use id="app" when no react', () => {
297
- const html = commons.getDefaultHomePage('TestApp', ['vanilla.js']);
298
-
299
- expect(html).toContain('id="app"');
238
+ it('should expand a home-anchored path against the resolved home', () => {
239
+ commons.setHomePath('/alice');
240
+ expect(commons.resolveRemotePath('/home/user', '~/site')).toBe('/alice/site');
241
+ commons.setHomePath(null);
300
242
  });
301
243
  });
302
244
 
@@ -340,7 +282,7 @@ describe('getLatestVersion', () => {
340
282
  json: () => Promise.resolve({ version: '1.0.0' }),
341
283
  });
342
284
 
343
- const result = await commons.getLatestVersion('puter-cli');
285
+ const result = await commons.getLatestVersion('puter-sh');
344
286
 
345
287
  expect(result).toBe('v1.0.0 (up-to-date)');
346
288
  });
@@ -352,7 +294,7 @@ describe('getLatestVersion', () => {
352
294
  json: () => Promise.resolve({ version: '2.0.0' }),
353
295
  });
354
296
 
355
- const result = await commons.getLatestVersion('puter-cli');
297
+ const result = await commons.getLatestVersion('puter-sh');
356
298
 
357
299
  expect(result).toBe('v1.0.0 (latest: 2.0.0)');
358
300
  });
@@ -361,7 +303,7 @@ describe('getLatestVersion', () => {
361
303
  vi.mocked(readFile).mockResolvedValueOnce(JSON.stringify({ version: '1.0.0' }));
362
304
  vi.mocked(global.fetch).mockRejectedValueOnce(new Error('Network error'));
363
305
 
364
- const result = await commons.getLatestVersion('puter-cli');
306
+ const result = await commons.getLatestVersion('puter-sh');
365
307
 
366
308
  expect(result).toBe('v1.0.0 (offline)');
367
309
  });
@@ -373,7 +315,7 @@ describe('getLatestVersion', () => {
373
315
  json: () => Promise.resolve({ version: '2.0.0' }),
374
316
  });
375
317
 
376
- const result = await commons.getLatestVersion('puter-cli');
318
+ const result = await commons.getLatestVersion('puter-sh');
377
319
 
378
320
  expect(result).toBe('vunknown (latest: 2.0.0)');
379
321
  });
@@ -11,6 +11,14 @@ vi.mock('conf', () => ({
11
11
 
12
12
  vi.mock('node:child_process');
13
13
 
14
+ vi.mock('../src/modules/ProfileModule.js', () => ({
15
+ initProfileModule: vi.fn(),
16
+ getProfileModule: vi.fn(() => ({
17
+ getCwd: () => '/mockuser',
18
+ setCwd: vi.fn(),
19
+ })),
20
+ }));
21
+
14
22
  vi.spyOn(console, 'log').mockImplementation(() => {});
15
23
  vi.spyOn(console, 'error').mockImplementation(() => {});
16
24
 
@@ -459,6 +459,9 @@ describe("renameFileOrDirectory", () => {
459
459
  describe("getInfo", () => {
460
460
  beforeEach(() => {
461
461
  vi.clearAllMocks();
462
+ // Undo the naive resolvePath stub other suites install: getInfo relies on
463
+ // real "~" and absolute-path resolution.
464
+ if (vi.isMockFunction(commons.resolvePath)) commons.resolvePath.mockRestore();
462
465
  vi.spyOn(PuterModule, "getPuter").mockReturnValue(mockPuter);
463
466
  vi.spyOn(auth, "getCurrentDirectory").mockReturnValue("/testuser/files");
464
467
  vi.spyOn(utils, "formatSize").mockImplementation((size) => `${size}B`);
@@ -515,6 +518,23 @@ describe("getInfo", () => {
515
518
  );
516
519
  });
517
520
 
521
+ it("should resolve the argument instead of concatenating it", async () => {
522
+ mockPuter.fs.stat.mockResolvedValue({
523
+ name: "files",
524
+ path: "/testuser/files",
525
+ is_dir: true,
526
+ owner: { username: "testuser" },
527
+ });
528
+ // Raw concatenation used to produce "/testuser/files/~/Desktop"; delegating
529
+ // to resolvePath is what makes "~" and absolute paths work here.
530
+ vi.spyOn(commons, "resolvePath").mockReturnValue("/resolved/path");
531
+
532
+ await getInfo(["~/Desktop"]);
533
+
534
+ expect(commons.resolvePath).toHaveBeenCalledWith("/testuser/files", "~/Desktop");
535
+ expect(mockPuter.fs.stat).toHaveBeenCalledWith("/resolved/path");
536
+ });
537
+
518
538
  it("should use current directory with default argument", async () => {
519
539
  mockPuter.fs.stat.mockResolvedValue({
520
540
  name: "files",
@@ -529,7 +549,7 @@ describe("getInfo", () => {
529
549
 
530
550
  await getInfo([]);
531
551
 
532
- expect(mockPuter.fs.stat).toHaveBeenCalledWith("/testuser/files/.");
552
+ expect(mockPuter.fs.stat).toHaveBeenCalledWith("/testuser/files");
533
553
  });
534
554
  });
535
555
 
@@ -538,8 +558,8 @@ describe("showCwd", () => {
538
558
  vi.clearAllMocks();
539
559
  });
540
560
 
541
- it("should display current working directory from config", async () => {
542
- mockConfigStore.cwd = "/testuser/documents";
561
+ it("should display the selected profile's working directory", async () => {
562
+ vi.spyOn(auth, "getCurrentDirectory").mockReturnValue("/testuser/documents");
543
563
 
544
564
  await showCwd();
545
565
 
@@ -27,6 +27,7 @@ beforeEach(async () => {
27
27
  vi.mocked(getPrompt).mockReturnValue('puter@/> ');
28
28
  vi.mocked(getProfileModule).mockReturnValue({
29
29
  checkLogin: vi.fn(),
30
+ setCwd: vi.fn(),
30
31
  });
31
32
 
32
33
  const mockOn = vi.fn().mockReturnThis();
@@ -97,7 +98,7 @@ describe('startShell', () => {
97
98
 
98
99
  it('should display welcome message', async () => {
99
100
  await startShell();
100
- expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Welcome to Puter-CLI'));
101
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Welcome to Puter Shell'));
101
102
  });
102
103
 
103
104
  it('should set prompt and call prompt()', async () => {
@@ -1,30 +1,5 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import { formatDate, formatDateTime, formatSize, displayNonNullValues, parseArgs, isValidAppUuid, is_valid_uuid4 } from '../src/utils.js';
3
-
4
- describe('formatDate', () => {
5
- it('should format a date string correctly', () => {
6
- const dateString = '2024-10-07T15:03:53.000Z';
7
- const expected = '10/07/2024, 15:03:53';
8
- expect(formatDate(dateString)).toBe(expected);
9
- });
10
-
11
- it('should format a date object correctly', () => {
12
- const dateObject = new Date(Date.UTC(2024, 9, 7, 15, 3, 53)); // Month is 0-indexed
13
- const expected = '10/07/2024, 15:03:53';
14
- expect(formatDate(dateObject)).toBe(expected);
15
- });
16
-
17
- it('should handle different date and time', () => {
18
- const dateString = '2023-01-01T01:30:05.000Z';
19
- const expected = '01/01/2023, 01:30:05';
20
- expect(formatDate(dateString)).toBe(expected);
21
- });
22
-
23
- it('should handle invalid date', () => {
24
- const dateString = 'invalid-date';
25
- expect(formatDate(dateString)).toBe('Invalid Date');
26
- });
27
- });
2
+ import { formatDateTime, formatSize } from '../src/utils.js';
28
3
 
29
4
  describe('formatDateTime', () => {
30
5
  it('should format as time if within 24 hours', () => {
@@ -76,118 +51,3 @@ describe('formatSize', () => {
76
51
  expect(formatSize(undefined)).toBe('0');
77
52
  });
78
53
  });
79
-
80
- describe('displayNonNullValues', () => {
81
- it('should display non-null values in a formatted table', () => {
82
- const data = {
83
- name: 'John Doe',
84
- age: 30,
85
- address: {
86
- street: '123 Main St',
87
- city: 'Anytown',
88
- zip: null
89
- },
90
- email: null
91
- };
92
-
93
- const consoleLogSpy = vi.spyOn(console, 'log');
94
- displayNonNullValues(data);
95
- expect(consoleLogSpy).toHaveBeenCalled();
96
- consoleLogSpy.mockRestore();
97
- });
98
-
99
- it('should handle empty object', () => {
100
- const data = {};
101
- const consoleLogSpy = vi.spyOn(console, 'log');
102
- displayNonNullValues(data);
103
- expect(consoleLogSpy).toHaveBeenCalled();
104
- consoleLogSpy.mockRestore();
105
- });
106
-
107
- it('should handle nested objects with all null values', () => {
108
- const data = { a: null, b: { c: null, d: null } };
109
- const consoleLogSpy = vi.spyOn(console, 'log');
110
- displayNonNullValues(data);
111
- expect(consoleLogSpy).toHaveBeenCalledTimes(5);
112
- consoleLogSpy.mockRestore();
113
- });
114
-
115
- it('should handle non-object input', () => {
116
- const data = "not an object";
117
- const consoleErrorSpy = vi.spyOn(console, 'error');
118
- displayNonNullValues(data);
119
- expect(consoleErrorSpy).toHaveBeenCalledWith("Invalid input: Input must be a non-null object.");
120
- consoleErrorSpy.mockRestore();
121
- });
122
- });
123
-
124
- describe('parseArgs', () => {
125
- it('should parse simple arguments', () => {
126
- const input = 'command --arg1 val1 --arg2 val2';
127
- const expected = { _: ['command'], arg1: 'val1', arg2: 'val2' };
128
- expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
129
- });
130
-
131
- it('should parse command line arguments with different types', () => {
132
- const input = 'command --name="John Doe" --age=30';
133
- const result = parseArgs(input);
134
- expect(result).toEqual({ _: ['command'], name: 'John Doe', age: 30 });
135
- });
136
-
137
- it('should parse quoted arguments', () => {
138
- const input = 'command --arg "quoted value"';
139
- const expected = { _: ['command'], arg: 'quoted value' };
140
- expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
141
- });
142
-
143
- it('should parse arguments with equals sign', () => {
144
- const input = 'command --arg1=val1 --arg2=val2';
145
- const expected = { _: ['command'], arg1: 'val1', arg2: 'val2' };
146
- expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
147
- });
148
-
149
- it('should handle empty input', () => {
150
- const result = parseArgs('');
151
- expect(result).toEqual({ _: []});
152
- });
153
-
154
- it('should parse empty arguments', () => {
155
- const input = '';
156
- const expected = { _: [] };
157
- expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
158
- });
159
- });
160
-
161
- describe('isValidAppUuid', () => {
162
- it('should return true for a valid app UUID', () => {
163
- const uuid = 'app-a1b2c3d4-e5f6-4789-8abc-def012345678';
164
- expect(isValidAppUuid(uuid)).toBe(true);
165
- });
166
-
167
- it('should return false if UUID does not start with "app-"', () => {
168
- const uuid = 'a1b2c3d4-e5f6-4789-8abc-def012345678';
169
- expect(isValidAppUuid(uuid)).toBe(false);
170
- });
171
-
172
- it('should return false for an invalid UUID after "app-"', () => {
173
- const uuid = 'app-invalid-uuid';
174
- expect(isValidAppUuid(uuid)).toBe(false);
175
- });
176
- });
177
-
178
- describe('is_valid_uuid4', () => {
179
- it('should return true for a valid UUID v4', () => {
180
- const uuid = 'a1b2c3d4-e5f6-4789-8abc-def012345678';
181
- expect(is_valid_uuid4(uuid)).toBe(true);
182
- });
183
-
184
- it('should return false for an invalid UUID v4', () => {
185
- const uuid = 'a1b2c3d4-e5f6-5789-8abc-def012345678'; // Invalid version
186
- expect(is_valid_uuid4(uuid)).toBe(false);
187
- });
188
-
189
- it('should return false for a completely invalid UUID', () => {
190
- const uuid = 'invalid-uuid';
191
- expect(is_valid_uuid4(uuid)).toBe(false);
192
- });
193
- });