@dk/jolly 0.1.8 → 0.1.9

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 (48) hide show
  1. package/README.md +16 -92
  2. package/package.json +26 -24
  3. package/src/index.ts +2095 -0
  4. package/src/lib/cloud-api.ts +527 -0
  5. package/src/lib/env-file.ts +68 -0
  6. package/src/lib/saleor-url.ts +37 -0
  7. package/.env.example +0 -3
  8. package/.mcp.json +0 -7
  9. package/.sisyphus/boulder.json +0 -13
  10. package/.sisyphus/notepads/saleor-agent-cli/decisions.md +0 -11
  11. package/.sisyphus/notepads/saleor-agent-cli/issues.md +0 -6
  12. package/.sisyphus/notepads/saleor-agent-cli/learnings.md +0 -6
  13. package/.sisyphus/plans/saleor-agent-cli.md +0 -600
  14. package/AGENTS.md +0 -46
  15. package/bun.lock +0 -123
  16. package/bunfig.toml +0 -8
  17. package/dist/agent.js +0 -258
  18. package/dist/bootstrap.js +0 -184
  19. package/dist/index.js +0 -722
  20. package/src/agents/index.ts +0 -1
  21. package/src/agents/setup.ts +0 -210
  22. package/src/api/auth.ts +0 -75
  23. package/src/api/client.ts +0 -152
  24. package/src/api/endpoints.ts +0 -8
  25. package/src/api/index.ts +0 -4
  26. package/src/cli/agent.ts +0 -26
  27. package/src/cli/bootstrap.ts +0 -24
  28. package/src/cli/commands/agent.ts +0 -40
  29. package/src/cli/commands/app.ts +0 -61
  30. package/src/cli/commands/config.ts +0 -38
  31. package/src/cli/commands/store.ts +0 -75
  32. package/src/cli/index.ts +0 -16
  33. package/src/commands/app.ts +0 -126
  34. package/src/commands/index.ts +0 -1
  35. package/src/commands/store.ts +0 -64
  36. package/src/test/command-handlers.test.ts +0 -232
  37. package/src/test/e2e-flows.test.ts +0 -231
  38. package/src/test/entry-points.test.ts +0 -126
  39. package/src/test/error-handling.test.ts +0 -137
  40. package/src/test/helpers.ts +0 -49
  41. package/src/test/index.ts +0 -1
  42. package/src/test/mocks.ts +0 -172
  43. package/src/test/setup.ts +0 -29
  44. package/src/tui/components.ts +0 -77
  45. package/src/tui/index.ts +0 -3
  46. package/src/tui/renderer.ts +0 -34
  47. package/src/tui/theme.ts +0 -38
  48. package/tsconfig.json +0 -20
@@ -1,231 +0,0 @@
1
- import { describe, expect, mock, beforeEach, afterEach } from 'bun:test';
2
- import { Given, When, Then, And } from './helpers';
3
- import {
4
- fixtures, captureConsole, mockFetch, mockFetchError,
5
- mockProcessExit, withToken, withoutToken,
6
- } from './mocks';
7
-
8
- const tuiCalls: { fn: string; msg: string }[] = [];
9
- mock.module('../tui/components', () => ({
10
- info: (msg: string) => { tuiCalls.push({ fn: 'info', msg }); return msg; },
11
- success: (msg: string) => { tuiCalls.push({ fn: 'success', msg }); return msg; },
12
- error: (msg: string) => { tuiCalls.push({ fn: 'error', msg }); return msg; },
13
- warning: (msg: string) => { tuiCalls.push({ fn: 'warning', msg }); return msg; },
14
- spinner: () => ({ stop: () => {} }),
15
- }));
16
-
17
- describe('End-to-End Flows', () => {
18
- let console_: ReturnType<typeof captureConsole>;
19
- let exitSpy: ReturnType<typeof mockProcessExit>;
20
- const originalFetch = globalThis.fetch;
21
-
22
- beforeEach(() => {
23
- console_ = captureConsole();
24
- exitSpy = mockProcessExit();
25
- tuiCalls.length = 0;
26
- withToken();
27
- });
28
-
29
- afterEach(() => {
30
- console_.restore();
31
- exitSpy.mockRestore();
32
- globalThis.fetch = originalFetch;
33
- withoutToken();
34
- });
35
-
36
- describe('Store Creation Flow', () => {
37
- Given('a valid token and working API', () => {
38
- When('creating a store end-to-end', () => {
39
- Then('it should call API and display project slug and dashboard URL', async () => {
40
- const fetchMock = mockFetch({
41
- '/organizations': { organizations: [fixtures.organization] },
42
- '/organizations/my-org/projects': { project: fixtures.project },
43
- });
44
-
45
- const { createStore } = await import('../commands/store');
46
- await createStore('my-store', 'us-east-1');
47
-
48
- expect(fetchMock).toHaveBeenCalled();
49
-
50
- const allTui = tuiCalls.map(c => c.msg).join('\n');
51
- expect(allTui).toContain('my-store');
52
- expect(allTui).toContain('cloud.saleor.io');
53
- });
54
- });
55
- });
56
- });
57
-
58
- describe('Store List Flow', () => {
59
- Given('a valid token and multiple organizations', () => {
60
- When('listing stores end-to-end', () => {
61
- Then('it should display all organizations with emails and dates', async () => {
62
- mockFetch({
63
- '/organizations': { organizations: [fixtures.organization, fixtures.organization2] },
64
- });
65
-
66
- const { listStores } = await import('../commands/store');
67
- await listStores();
68
-
69
- const allTui = tuiCalls.map(c => c.msg).join('\n');
70
- const allConsole = console_.logs.join('\n');
71
- expect(allTui).toContain('2 organization(s)');
72
- expect(allConsole).toContain('My Organization');
73
- expect(allConsole).toContain('owner@example.com');
74
- expect(allConsole).toContain('Other Organization');
75
- expect(allConsole).toContain('other@example.com');
76
- });
77
- });
78
- });
79
- });
80
-
81
- describe('Environment Creation Flow', () => {
82
- Given('a valid token and existing organization', () => {
83
- When('creating an environment end-to-end', () => {
84
- Then('it should call API and display environment key and domain', async () => {
85
- const fetchMock = mockFetch({
86
- '/organizations/my-org/projects/default/environments': { environment: fixtures.environment },
87
- });
88
-
89
- const { createEnvironment } = await import('../commands/store');
90
- await createEnvironment('my-org', 'staging');
91
-
92
- expect(fetchMock).toHaveBeenCalled();
93
-
94
- const allTui = tuiCalls.map(c => c.msg).join('\n');
95
- expect(allTui).toContain('staging');
96
- expect(allTui).toContain('saleor.cloud');
97
- });
98
- });
99
- });
100
- });
101
-
102
- describe('Payment App Registration Flow', () => {
103
- Given('a valid token and environment', () => {
104
- When('creating a payment app with environment', () => {
105
- Then('it should register via API and display app ID', async () => {
106
- const fetchMock = mockFetch({
107
- '/environments/staging/apps': { app: fixtures.app },
108
- });
109
-
110
- const { createApp } = await import('../commands/app');
111
- await createApp('my-payment', 'payment', 'staging', 'stripe');
112
-
113
- expect(fetchMock).toHaveBeenCalledTimes(1);
114
-
115
- const allTui = tuiCalls.map(c => c.msg).join('\n');
116
- expect(allTui).toContain('app-1');
117
- expect(allTui).toContain('stripe');
118
- });
119
- });
120
- });
121
-
122
- Given('a valid token but no environment', () => {
123
- When('creating a payment app without environment', () => {
124
- Then('it should display manual setup instructions', async () => {
125
- const { createApp } = await import('../commands/app');
126
- await createApp('my-payment', 'payment', undefined, 'stripe');
127
-
128
- const allTui = tuiCalls.map(c => c.msg).join('\n');
129
- expect(allTui).toContain('stripe');
130
- expect(allTui).toContain('cloud.saleor.io');
131
- expect(allTui).toContain('Third party apps');
132
- });
133
- });
134
- });
135
- });
136
-
137
- describe('Full Error Recovery Flow', () => {
138
- Given('no token set', () => {
139
- When('attempting any store operation', () => {
140
- Then('it should fail fast with auth error before API call', async () => {
141
- withoutToken();
142
- const fetchMock = mockFetch({ '/organizations': { organizations: [] } });
143
-
144
- const { listStores } = await import('../commands/store');
145
-
146
- try {
147
- await listStores();
148
- } catch {}
149
-
150
- expect(exitSpy).toHaveBeenCalledWith(1);
151
- expect(fetchMock).not.toHaveBeenCalled();
152
- });
153
- });
154
- });
155
-
156
- Given('a valid token but network failure', () => {
157
- And('the API returns 500', () => {
158
- When('creating a store', () => {
159
- Then('it should display the error and exit 1', async () => {
160
- mockFetchError(500, 'Internal Server Error');
161
-
162
- const { createStore } = await import('../commands/store');
163
-
164
- try {
165
- await createStore('fail-store', 'us-east-1');
166
- } catch {}
167
-
168
- expect(exitSpy).toHaveBeenCalledWith(1);
169
- });
170
- });
171
- });
172
- });
173
- });
174
-
175
- describe('API Client Request Construction', () => {
176
- Given('a SaleorCloudClient', () => {
177
- When('listing organizations', () => {
178
- Then('it should send correct URL, method, and headers', async () => {
179
- const fetchMock = mockFetch({
180
- '/organizations': { organizations: [fixtures.organization] },
181
- });
182
-
183
- const { SaleorCloudClient } = await import('../api/client');
184
- const client = new SaleorCloudClient(fixtures.token);
185
- await client.getOrganizations();
186
-
187
- expect(fetchMock).toHaveBeenCalledTimes(1);
188
- const [url, opts] = fetchMock.mock.calls[0];
189
- expect(url).toBe('https://cloud.saleor.io/api/v1/organizations');
190
- expect(opts.method).toBe('GET');
191
- expect(opts.headers.Authorization).toBe(`Token ${fixtures.token}`);
192
- });
193
- });
194
-
195
- When('creating a project', () => {
196
- Then('it should send correct URL, method, headers, and body', async () => {
197
- const fetchMock = mockFetch({
198
- '/organizations/default/projects': { project: fixtures.project },
199
- });
200
-
201
- const { SaleorCloudClient } = await import('../api/client');
202
- const client = new SaleorCloudClient(fixtures.token);
203
- await client.createProject('default', 'test-store', 'eu-west-1');
204
-
205
- expect(fetchMock).toHaveBeenCalledTimes(1);
206
- const [url, opts] = fetchMock.mock.calls[0];
207
- expect(url).toBe('https://cloud.saleor.io/api/v1/organizations/default/projects');
208
- expect(opts.method).toBe('POST');
209
- expect(opts.headers.Authorization).toBe(`Token ${fixtures.token}`);
210
- expect(JSON.parse(opts.body)).toEqual({ name: 'test-store', region: 'eu-west-1' });
211
- });
212
- });
213
-
214
- When('registering an app', () => {
215
- Then('it should send correct URL with environment key', async () => {
216
- const fetchMock = mockFetch({
217
- '/environments/staging/apps': { app: fixtures.app },
218
- });
219
-
220
- const { SaleorCloudClient } = await import('../api/client');
221
- const client = new SaleorCloudClient(fixtures.token);
222
- await client.registerApp('staging', 'payment', 'my-app');
223
-
224
- const [url, opts] = fetchMock.mock.calls[0];
225
- expect(url).toBe('https://cloud.saleor.io/api/v1/environments/staging/apps');
226
- expect(JSON.parse(opts.body)).toEqual({ type: 'payment', name: 'my-app' });
227
- });
228
- });
229
- });
230
- });
231
- });
@@ -1,126 +0,0 @@
1
- import { describe, expect, mock, beforeEach, afterEach } from 'bun:test';
2
- import { Given, When, Then } from './helpers';
3
- import {
4
- fixtures, captureConsole, mockFetch, mockProcessExit, withToken, withoutToken,
5
- } from './mocks';
6
-
7
- const tuiCalls: { fn: string; msg: string }[] = [];
8
- mock.module('../tui/components', () => ({
9
- info: (msg: string) => { tuiCalls.push({ fn: 'info', msg }); return msg; },
10
- success: (msg: string) => { tuiCalls.push({ fn: 'success', msg }); return msg; },
11
- error: (msg: string) => { tuiCalls.push({ fn: 'error', msg }); return msg; },
12
- warning: (msg: string) => { tuiCalls.push({ fn: 'warning', msg }); return msg; },
13
- spinner: () => ({ stop: () => {} }),
14
- }));
15
-
16
- describe('Entry Points', () => {
17
- let console_: ReturnType<typeof captureConsole>;
18
- let exitSpy: ReturnType<typeof mockProcessExit>;
19
- const originalFetch = globalThis.fetch;
20
-
21
- beforeEach(() => {
22
- console_ = captureConsole();
23
- exitSpy = mockProcessExit();
24
- tuiCalls.length = 0;
25
- });
26
-
27
- afterEach(() => {
28
- console_.restore();
29
- exitSpy.mockRestore();
30
- globalThis.fetch = originalFetch;
31
- withoutToken();
32
- });
33
-
34
- describe('Bootstrap Logic', () => {
35
- Given('no project name argument', () => {
36
- When('bootstrap runs without args', () => {
37
- Then('it should print usage instructions to console', () => {
38
- console.log('Saleor Store Bootstrapper');
39
- console.log('------------------------\n');
40
- console.log('Usage: npm create @saleor/jolly <project-name>');
41
-
42
- const allOutput = console_.logs.join('\n');
43
- expect(allOutput).toContain('Usage:');
44
- expect(allOutput).toContain('npm create @saleor/jolly');
45
- });
46
- });
47
- });
48
-
49
- Given('a project name is provided', () => {
50
- When('bootstrap calls createStore', () => {
51
- Then('it should pass project name and default region', async () => {
52
- withToken();
53
- mockFetch({
54
- '/organizations': { organizations: [fixtures.organization] },
55
- '/organizations/my-org/projects': { project: fixtures.project },
56
- });
57
-
58
- const { createStore } = await import('../commands/store');
59
- await createStore('my-project', 'us-east-1');
60
-
61
- const allTui = tuiCalls.map(c => c.msg).join('\n');
62
- expect(allTui).toContain('my-project');
63
- expect(allTui).toContain('us-east-1');
64
- });
65
- });
66
- });
67
-
68
- Given('bootstrap encounters an auth error', () => {
69
- When('createStore is called without a token', () => {
70
- Then('it should exit with code 1', async () => {
71
- withoutToken();
72
-
73
- const { createStore } = await import('../commands/store');
74
-
75
- try {
76
- await createStore('fail-project', 'us-east-1');
77
- } catch {}
78
-
79
- expect(exitSpy).toHaveBeenCalledWith(1);
80
- });
81
- });
82
- });
83
- });
84
-
85
- describe('Agent Entry Logic', () => {
86
- Given('the default setup action', () => {
87
- When('setupAgent is called with current directory', () => {
88
- Then('it should detect agents and begin installation', async () => {
89
- const { setupAgent } = await import('../agents/setup');
90
-
91
- try {
92
- await setupAgent('.');
93
- } catch {}
94
-
95
- const allTui = tuiCalls.map(c => c.msg).join('\n');
96
- expect(allTui).toContain('Detecting AI agents');
97
- });
98
- });
99
- });
100
-
101
- Given('the skills action', () => {
102
- When('installSkillsCommand is called', () => {
103
- Then('it should install skills for detected agents', async () => {
104
- const { installSkillsCommand } = await import('../agents/setup');
105
-
106
- try {
107
- await installSkillsCommand('.');
108
- } catch {}
109
-
110
- const allTui = tuiCalls.map(c => c.msg).join('\n');
111
- expect(allTui).toContain('skill');
112
- });
113
- });
114
- });
115
-
116
- Given('an unknown action string', () => {
117
- When('checking action routing logic', () => {
118
- Then('it should not match any known action', () => {
119
- const action = 'unknown-action';
120
- const isKnown = action === 'setup' || action === 'install' || action === 'skills';
121
- expect(isKnown).toBe(false);
122
- });
123
- });
124
- });
125
- });
126
- });
@@ -1,137 +0,0 @@
1
- import { describe, expect, beforeEach, afterEach } from 'bun:test';
2
- import { Given, When, Then } from './helpers';
3
- import {
4
- fixtures, captureConsole, mockFetch, mockFetchError,
5
- mockProcessExit, withToken, withoutToken,
6
- } from './mocks';
7
-
8
- describe('Error Handling', () => {
9
- let console_: ReturnType<typeof captureConsole>;
10
- let exitSpy: ReturnType<typeof mockProcessExit>;
11
- const originalFetch = globalThis.fetch;
12
-
13
- beforeEach(() => {
14
- console_ = captureConsole();
15
- exitSpy = mockProcessExit();
16
- });
17
-
18
- afterEach(() => {
19
- console_.restore();
20
- exitSpy.mockRestore();
21
- globalThis.fetch = originalFetch;
22
- withoutToken();
23
- });
24
-
25
- describe('Auth Token Validation', () => {
26
- Given('no SALEOR_CLOUD_TOKEN in environment', () => {
27
- When('calling requireToken', () => {
28
- Then('it should print error message with token URL', async () => {
29
- withoutToken();
30
-
31
- const { requireToken } = await import('../api/auth');
32
-
33
- try {
34
- requireToken();
35
- } catch {}
36
-
37
- const allErrors = console_.errors.join('\n');
38
- expect(allErrors).toContain('SALEOR_CLOUD_TOKEN');
39
- expect(allErrors).toContain('cloud.saleor.io');
40
- expect(exitSpy).toHaveBeenCalledWith(1);
41
- });
42
- });
43
-
44
- When('calling getToken', () => {
45
- Then('it should throw with descriptive message', async () => {
46
- withoutToken();
47
-
48
- const { getToken } = await import('../api/auth');
49
- expect(() => getToken()).toThrow('SALEOR_CLOUD_TOKEN');
50
- });
51
- });
52
- });
53
-
54
- Given('an empty string SALEOR_CLOUD_TOKEN', () => {
55
- When('creating a SaleorCloudClient', () => {
56
- Then('it should throw', async () => {
57
- process.env.SALEOR_CLOUD_TOKEN = '';
58
-
59
- const { SaleorCloudClient } = await import('../api/client');
60
- expect(() => new SaleorCloudClient()).toThrow('SALEOR_CLOUD_TOKEN');
61
- });
62
- });
63
- });
64
- });
65
-
66
- describe('API Error Responses', () => {
67
- Given('a valid token', () => {
68
- beforeEach(() => {
69
- withToken();
70
- });
71
-
72
- When('API returns 401 Unauthorized', () => {
73
- Then('the client should throw with status info', async () => {
74
- mockFetchError(401, 'Unauthorized');
75
-
76
- const { SaleorCloudClient } = await import('../api/client');
77
- const client = new SaleorCloudClient(fixtures.token);
78
-
79
- await expect(client.getOrganizations()).rejects.toThrow('401');
80
- });
81
- });
82
-
83
- When('API returns 403 Forbidden', () => {
84
- Then('the client should throw with status info', async () => {
85
- mockFetchError(403, 'Forbidden');
86
-
87
- const { SaleorCloudClient } = await import('../api/client');
88
- const client = new SaleorCloudClient(fixtures.token);
89
-
90
- await expect(client.createProject('test-org', 'test', 'us-east-1')).rejects.toThrow('403');
91
- });
92
- });
93
-
94
- When('API returns 500 Internal Server Error', () => {
95
- Then('the client should throw with status info', async () => {
96
- mockFetchError(500, 'Internal Server Error');
97
-
98
- const { SaleorCloudClient } = await import('../api/client');
99
- const client = new SaleorCloudClient(fixtures.token);
100
-
101
- await expect(client.getEnvironments('test-org', 'test-project')).rejects.toThrow('500');
102
- });
103
- });
104
-
105
- When('API returns 404 for non-existent resource', () => {
106
- Then('the client should throw with status info', async () => {
107
- mockFetchError(404, 'Not Found');
108
-
109
- const { SaleorCloudClient } = await import('../api/client');
110
- const client = new SaleorCloudClient(fixtures.token);
111
-
112
- await expect(client.createEnvironment('bad-org', 'bad-project', 'staging', 'us-east-1')).rejects.toThrow('404');
113
- });
114
- });
115
- });
116
- });
117
-
118
- describe('API Request Headers', () => {
119
- Given('a SaleorCloudClient with a token', () => {
120
- When('making any request', () => {
121
- Then('it should include Authorization Token header', async () => {
122
- const fetchMock = mockFetch({
123
- '/organizations': { organizations: [] },
124
- });
125
-
126
- const { SaleorCloudClient } = await import('../api/client');
127
- const client = new SaleorCloudClient(fixtures.token);
128
- await client.getOrganizations();
129
-
130
- const [, opts] = fetchMock.mock.calls[0];
131
- expect(opts.headers.Authorization).toBe(`Token ${fixtures.token}`);
132
- expect(opts.headers['Content-Type']).toBe('application/json');
133
- });
134
- });
135
- });
136
- });
137
- });
@@ -1,49 +0,0 @@
1
- export {};
2
-
3
- declare global {
4
- namespace jest {
5
- interface Each {
6
- given: (name: string, fn: () => void) => void;
7
- when: (name: string, fn: () => void) => void;
8
- then: (name: string, fn: () => void) => void;
9
- }
10
- }
11
- }
12
-
13
- export function Given(name: string, fn: () => void): void {
14
- describe(`Given ${name}`, fn);
15
- }
16
-
17
- export function When(name: string, fn: () => void): void {
18
- describe(`When ${name}`, fn);
19
- }
20
-
21
- export function Then(name: string, fn: () => void): void {
22
- it(`Then ${name}`, fn);
23
- }
24
-
25
- export function And(name: string, fn: () => void): void {
26
- describe(`And ${name}`, fn);
27
- }
28
-
29
- export function But(name: string, fn: () => void): void {
30
- describe(`But ${name}`, fn);
31
- }
32
-
33
- export async function GivenAsync(name: string, fn: () => Promise<void>): Promise<void> {
34
- describe(`Given ${name}`, async () => {
35
- await fn();
36
- });
37
- }
38
-
39
- export async function WhenAsync(name: string, fn: () => Promise<void>): Promise<void> {
40
- describe(`When ${name}`, async () => {
41
- await fn();
42
- });
43
- }
44
-
45
- export async function ThenAsync(name: string, fn: () => Promise<void>): Promise<void> {
46
- it(`Then ${name}`, async () => {
47
- await fn();
48
- });
49
- }
package/src/test/index.ts DELETED
@@ -1 +0,0 @@
1
- export {};