@dk/jolly 0.1.6 → 0.1.7

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/bootstrap.js CHANGED
@@ -11,43 +11,66 @@ class SaleorCloudClient {
11
11
  }
12
12
  }
13
13
  async request(endpoint, options) {
14
- const response = await fetch(`${this.baseUrl}${endpoint}`, {
14
+ const mergedOptions = {
15
+ method: options?.method || "GET",
15
16
  ...options,
16
17
  headers: {
17
- Authorization: `Bearer ${this.token}`,
18
+ Authorization: `Token ${this.token}`,
18
19
  "Content-Type": "application/json",
19
20
  ...options?.headers
20
21
  }
21
- });
22
+ };
23
+ const response = await fetch(`${this.baseUrl}${endpoint}`, mergedOptions);
22
24
  if (!response.ok) {
23
- throw new Error(`API error: ${response.status} ${response.statusText}`);
25
+ const body = await response.text();
26
+ throw new Error(`API error: ${response.status} ${response.statusText} - ${body}`);
24
27
  }
25
28
  return response.json();
26
29
  }
27
- async getStores() {
28
- return this.request("/stores");
30
+ async getOrganizations() {
31
+ return this.request("/organizations");
29
32
  }
30
- async createStore(name, region = "us-east-1") {
31
- return this.request("/stores", {
33
+ async getProjects(organizationSlug) {
34
+ return this.request(`/organizations/${organizationSlug}/projects`);
35
+ }
36
+ async createProject(organizationSlug, name, region) {
37
+ return this.request(`/organizations/${organizationSlug}/projects`, {
32
38
  method: "POST",
33
39
  body: JSON.stringify({ name, region })
34
40
  });
35
41
  }
36
- async getEnvironments(storeId) {
37
- return this.request(`/stores/${storeId}/environments`);
42
+ async getEnvironments(organizationSlug, projectSlug) {
43
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments`);
38
44
  }
39
- async createEnvironment(storeId, name) {
40
- return this.request(`/stores/${storeId}/environments`, {
45
+ async createEnvironment(organizationSlug, projectSlug, name, region) {
46
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments`, {
41
47
  method: "POST",
42
- body: JSON.stringify({ name })
48
+ body: JSON.stringify({ name, region })
43
49
  });
44
50
  }
51
+ async getEnvironment(organizationSlug, projectSlug, environmentSlug) {
52
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments/${environmentSlug}`);
53
+ }
45
54
  async registerApp(environmentId, appType, name) {
46
55
  return this.request(`/environments/${environmentId}/apps`, {
47
56
  method: "POST",
48
57
  body: JSON.stringify({ type: appType, name })
49
58
  });
50
59
  }
60
+ async getStores() {
61
+ return this.getOrganizations();
62
+ }
63
+ async createStore(name, region = "us-east-1") {
64
+ const { organizations } = await this.getOrganizations();
65
+ if (organizations.length === 0) {
66
+ throw new Error("No organizations found. Create one at https://cloud.saleor.io");
67
+ }
68
+ return this.createProject(organizations[0].slug, name, region);
69
+ }
70
+ async createEnvironmentFromStore(storeId, name) {
71
+ const { environments } = await this.getEnvironments(storeId, "default");
72
+ return this.createEnvironment(storeId, environments.length > 0 ? environments[0].project?.slug || "default" : "default", name, "default");
73
+ }
51
74
  }
52
75
 
53
76
  // src/api/auth.ts
@@ -129,8 +152,8 @@ async function createStore(name, region) {
129
152
  try {
130
153
  const result = await client.createStore(name, region);
131
154
  console.log(success(`Store created successfully!`));
132
- console.log(info(`Store ID: ${result.store.id}`));
133
- console.log(info(`Dashboard: https://cloud.saleor.io/stores/${result.store.id}`));
155
+ console.log(info(`Project slug: ${result.project.slug}`));
156
+ console.log(info(`Dashboard: https://cloud.saleor.io/organizations/default/projects/${result.project.slug}`));
134
157
  } catch (err) {
135
158
  console.log(error(`Failed to create store: ${err}`));
136
159
  process.exit(1);
package/dist/index.js CHANGED
@@ -33,43 +33,66 @@ class SaleorCloudClient {
33
33
  }
34
34
  }
35
35
  async request(endpoint, options) {
36
- const response = await fetch(`${this.baseUrl}${endpoint}`, {
36
+ const mergedOptions = {
37
+ method: options?.method || "GET",
37
38
  ...options,
38
39
  headers: {
39
- Authorization: `Bearer ${this.token}`,
40
+ Authorization: `Token ${this.token}`,
40
41
  "Content-Type": "application/json",
41
42
  ...options?.headers
42
43
  }
43
- });
44
+ };
45
+ const response = await fetch(`${this.baseUrl}${endpoint}`, mergedOptions);
44
46
  if (!response.ok) {
45
- throw new Error(`API error: ${response.status} ${response.statusText}`);
47
+ const body = await response.text();
48
+ throw new Error(`API error: ${response.status} ${response.statusText} - ${body}`);
46
49
  }
47
50
  return response.json();
48
51
  }
49
- async getStores() {
50
- return this.request("/stores");
52
+ async getOrganizations() {
53
+ return this.request("/organizations");
51
54
  }
52
- async createStore(name, region = "us-east-1") {
53
- return this.request("/stores", {
55
+ async getProjects(organizationSlug) {
56
+ return this.request(`/organizations/${organizationSlug}/projects`);
57
+ }
58
+ async createProject(organizationSlug, name, region) {
59
+ return this.request(`/organizations/${organizationSlug}/projects`, {
54
60
  method: "POST",
55
61
  body: JSON.stringify({ name, region })
56
62
  });
57
63
  }
58
- async getEnvironments(storeId) {
59
- return this.request(`/stores/${storeId}/environments`);
64
+ async getEnvironments(organizationSlug, projectSlug) {
65
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments`);
60
66
  }
61
- async createEnvironment(storeId, name) {
62
- return this.request(`/stores/${storeId}/environments`, {
67
+ async createEnvironment(organizationSlug, projectSlug, name, region) {
68
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments`, {
63
69
  method: "POST",
64
- body: JSON.stringify({ name })
70
+ body: JSON.stringify({ name, region })
65
71
  });
66
72
  }
73
+ async getEnvironment(organizationSlug, projectSlug, environmentSlug) {
74
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments/${environmentSlug}`);
75
+ }
67
76
  async registerApp(environmentId, appType, name) {
68
77
  return this.request(`/environments/${environmentId}/apps`, {
69
78
  method: "POST",
70
79
  body: JSON.stringify({ type: appType, name })
71
80
  });
72
81
  }
82
+ async getStores() {
83
+ return this.getOrganizations();
84
+ }
85
+ async createStore(name, region = "us-east-1") {
86
+ const { organizations } = await this.getOrganizations();
87
+ if (organizations.length === 0) {
88
+ throw new Error("No organizations found. Create one at https://cloud.saleor.io");
89
+ }
90
+ return this.createProject(organizations[0].slug, name, region);
91
+ }
92
+ async createEnvironmentFromStore(storeId, name) {
93
+ const { environments } = await this.getEnvironments(storeId, "default");
94
+ return this.createEnvironment(storeId, environments.length > 0 ? environments[0].project?.slug || "default" : "default", name, "default");
95
+ }
73
96
  }
74
97
 
75
98
  // src/api/auth.ts
@@ -198,8 +221,8 @@ async function createStore(name, region) {
198
221
  try {
199
222
  const result = await client.createStore(name, region);
200
223
  console.log(success(`Store created successfully!`));
201
- console.log(info(`Store ID: ${result.store.id}`));
202
- console.log(info(`Dashboard: https://cloud.saleor.io/stores/${result.store.id}`));
224
+ console.log(info(`Project slug: ${result.project.slug}`));
225
+ console.log(info(`Dashboard: https://cloud.saleor.io/organizations/default/projects/${result.project.slug}`));
203
226
  } catch (err) {
204
227
  console.log(error(`Failed to create store: ${err}`));
205
228
  process.exit(1);
@@ -208,19 +231,19 @@ async function createStore(name, region) {
208
231
  async function listStores() {
209
232
  const token = requireToken();
210
233
  const client = new SaleorCloudClient(token);
211
- console.log(info("Fetching stores..."));
234
+ console.log(info("Fetching organizations..."));
212
235
  try {
213
- const result = await client.getStores();
214
- if (result.stores.length === 0) {
215
- console.log(info("No stores found. Create one with: jolly store create --name <name>"));
236
+ const { organizations } = await client.getOrganizations();
237
+ if (organizations.length === 0) {
238
+ console.log(info("No organizations found. Create one at https://cloud.saleor.io"));
216
239
  return;
217
240
  }
218
- console.log(success(`Found ${result.stores.length} store(s):
241
+ console.log(success(`Found ${organizations.length} organization(s):
219
242
  `));
220
- for (const store of result.stores) {
221
- console.log(` ${store.name} (${store.id})`);
222
- console.log(` Region: ${store.region}`);
223
- console.log(` Created: ${new Date(store.created_at).toLocaleDateString()}`);
243
+ for (const org of organizations) {
244
+ console.log(` ${org.name} (${org.slug})`);
245
+ console.log(` Email: ${org.owner_email}`);
246
+ console.log(` Created: ${new Date(org.created).toLocaleDateString()}`);
224
247
  console.log();
225
248
  }
226
249
  } catch (err) {
@@ -228,15 +251,15 @@ async function listStores() {
228
251
  process.exit(1);
229
252
  }
230
253
  }
231
- async function createEnvironment(storeId, name) {
254
+ async function createEnvironment(organizationSlug, name) {
232
255
  const token = requireToken();
233
256
  const client = new SaleorCloudClient(token);
234
- console.log(info(`Creating environment: ${name} for store ${storeId}...`));
257
+ console.log(info(`Creating environment: ${name} for organization ${organizationSlug}...`));
235
258
  try {
236
- const result = await client.createEnvironment(storeId, name);
259
+ const { environment } = await client.createEnvironment(organizationSlug, "default", name, "us-east-1");
237
260
  console.log(success(`Environment created successfully!`));
238
- console.log(info(`Environment ID: ${result.environment.id}`));
239
- console.log(info(`API URL will be available at: https://${result.environment.id}.saleor.cloud`));
261
+ console.log(info(`Environment key: ${environment.key}`));
262
+ console.log(info(`Domain: ${environment.domain}`));
240
263
  } catch (err) {
241
264
  console.log(error(`Failed to create environment: ${err}`));
242
265
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dk/jolly",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Saleor project bootstrapper and agent configurator",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api/client.ts CHANGED
@@ -10,63 +10,136 @@ export class SaleorCloudClient {
10
10
  }
11
11
 
12
12
  async request<T>(endpoint: string, options?: RequestInit): Promise<T> {
13
- const response = await fetch(`${this.baseUrl}${endpoint}`, {
13
+ const mergedOptions: RequestInit = {
14
+ method: options?.method || 'GET',
14
15
  ...options,
15
16
  headers: {
16
- 'Authorization': `Bearer ${this.token}`,
17
+ 'Authorization': `Token ${this.token}`,
17
18
  'Content-Type': 'application/json',
18
19
  ...options?.headers,
19
20
  },
20
- });
21
+ };
22
+
23
+ const response = await fetch(`${this.baseUrl}${endpoint}`, mergedOptions);
21
24
 
22
25
  if (!response.ok) {
23
- throw new Error(`API error: ${response.status} ${response.statusText}`);
26
+ const body = await response.text();
27
+ throw new Error(`API error: ${response.status} ${response.statusText} - ${body}`);
24
28
  }
25
29
 
26
- return response.json();
30
+ return response.json() as T;
27
31
  }
28
32
 
29
- async getStores() {
30
- return this.request<{ stores: Store[] }>('/stores');
33
+ // Organizations
34
+ async getOrganizations() {
35
+ return this.request<{ organizations: Organization[] }>('/organizations');
31
36
  }
32
37
 
33
- async createStore(name: string, region: string = 'us-east-1') {
34
- return this.request<{ store: Store }>('/stores', {
38
+ // Projects
39
+ async getProjects(organizationSlug: string) {
40
+ return this.request<{ projects: Project[] }>(`/organizations/${organizationSlug}/projects`);
41
+ }
42
+
43
+ async createProject(organizationSlug: string, name: string, region: string) {
44
+ return this.request<{ project: Project }>(`/organizations/${organizationSlug}/projects`, {
35
45
  method: 'POST',
36
46
  body: JSON.stringify({ name, region }),
37
47
  });
38
48
  }
39
49
 
40
- async getEnvironments(storeId: string) {
41
- return this.request<{ environments: Environment[] }>(`/stores/${storeId}/environments`);
50
+ // Environments
51
+ async getEnvironments(organizationSlug: string, projectSlug: string) {
52
+ return this.request<{ environments: Environment[] }>(
53
+ `/organizations/${organizationSlug}/projects/${projectSlug}/environments`
54
+ );
42
55
  }
43
56
 
44
- async createEnvironment(storeId: string, name: string) {
45
- return this.request<{ environment: Environment }>(`/stores/${storeId}/environments`, {
46
- method: 'POST',
47
- body: JSON.stringify({ name }),
48
- });
57
+ async createEnvironment(organizationSlug: string, projectSlug: string, name: string, region: string) {
58
+ return this.request<{ environment: Environment; task: Task }>(
59
+ `/organizations/${organizationSlug}/projects/${projectSlug}/environments`,
60
+ {
61
+ method: 'POST',
62
+ body: JSON.stringify({ name, region }),
63
+ }
64
+ );
49
65
  }
50
66
 
67
+ async getEnvironment(organizationSlug: string, projectSlug: string, environmentSlug: string) {
68
+ return this.request<Environment>(
69
+ `/organizations/${organizationSlug}/projects/${projectSlug}/environments/${environmentSlug}`
70
+ );
71
+ }
72
+
73
+ // Apps
51
74
  async registerApp(environmentId: string, appType: string, name: string) {
52
75
  return this.request<{ app: App }>(`/environments/${environmentId}/apps`, {
53
76
  method: 'POST',
54
77
  body: JSON.stringify({ type: appType, name }),
55
78
  });
56
79
  }
80
+
81
+ // Backward compatibility methods
82
+ async getStores() {
83
+ return this.getOrganizations();
84
+ }
85
+
86
+ async createStore(name: string, region: string = 'us-east-1') {
87
+ // Create organization as "store" - requires org slug
88
+ // For now, create in first available org
89
+ const { organizations } = await this.getOrganizations();
90
+ if (organizations.length === 0) {
91
+ throw new Error('No organizations found. Create one at https://cloud.saleor.io');
92
+ }
93
+ return this.createProject(organizations[0].slug, name, region);
94
+ }
95
+
96
+ async createEnvironmentFromStore(storeId: string, name: string) {
97
+ // storeId is used as organization slug for backward compatibility
98
+ const { environments } = await this.getEnvironments(storeId, 'default');
99
+ // Create sandbox environment
100
+ return this.createEnvironment(storeId, environments.length > 0 ? environments[0].project?.slug || 'default' : 'default', name, 'default');
101
+ }
57
102
  }
58
103
 
59
- export interface Store {
60
- id: string;
104
+ export interface Organization {
105
+ slug: string;
106
+ name: string;
107
+ created: string;
108
+ company_name?: string;
109
+ owner_email: string;
110
+ }
111
+
112
+ export interface Project {
113
+ slug: string;
61
114
  name: string;
62
115
  region: string;
63
- created_at: string;
116
+ created: string;
117
+ billing_period?: { start: string };
118
+ sandboxes: { count: number };
64
119
  }
65
120
 
66
121
  export interface Environment {
122
+ key: string;
123
+ name: string;
124
+ domain: string;
125
+ service: {
126
+ version: string;
127
+ type: string;
128
+ region: string;
129
+ };
130
+ created: string;
131
+ project: { name: string; slug: string };
132
+ }
133
+
134
+ export interface Task {
135
+ id: string;
136
+ status: string;
137
+ }
138
+
139
+ export interface Store {
67
140
  id: string;
68
141
  name: string;
69
- store_id: string;
142
+ region: string;
70
143
  created_at: string;
71
144
  }
72
145
 
@@ -11,8 +11,8 @@ export async function createStore(name: string, region: string): Promise<void> {
11
11
  try {
12
12
  const result = await client.createStore(name, region);
13
13
  console.log(success(`Store created successfully!`));
14
- console.log(info(`Store ID: ${result.store.id}`));
15
- console.log(info(`Dashboard: https://cloud.saleor.io/stores/${result.store.id}`));
14
+ console.log(info(`Project slug: ${result.project.slug}`));
15
+ console.log(info(`Dashboard: https://cloud.saleor.io/organizations/default/projects/${result.project.slug}`));
16
16
  } catch (err) {
17
17
  console.log(error(`Failed to create store: ${err}`));
18
18
  process.exit(1);
@@ -23,21 +23,21 @@ export async function listStores(): Promise<void> {
23
23
  const token = requireToken();
24
24
  const client = new SaleorCloudClient(token);
25
25
 
26
- console.log(info('Fetching stores...'));
26
+ console.log(info('Fetching organizations...'));
27
27
 
28
28
  try {
29
- const result = await client.getStores();
29
+ const { organizations } = await client.getOrganizations();
30
30
 
31
- if (result.stores.length === 0) {
32
- console.log(info('No stores found. Create one with: jolly store create --name <name>'));
31
+ if (organizations.length === 0) {
32
+ console.log(info('No organizations found. Create one at https://cloud.saleor.io'));
33
33
  return;
34
34
  }
35
35
 
36
- console.log(success(`Found ${result.stores.length} store(s):\n`));
37
- for (const store of result.stores) {
38
- console.log(` ${store.name} (${store.id})`);
39
- console.log(` Region: ${store.region}`);
40
- console.log(` Created: ${new Date(store.created_at).toLocaleDateString()}`);
36
+ console.log(success(`Found ${organizations.length} organization(s):\n`));
37
+ for (const org of organizations) {
38
+ console.log(` ${org.name} (${org.slug})`);
39
+ console.log(` Email: ${org.owner_email}`);
40
+ console.log(` Created: ${new Date(org.created).toLocaleDateString()}`);
41
41
  console.log();
42
42
  }
43
43
  } catch (err) {
@@ -46,17 +46,17 @@ export async function listStores(): Promise<void> {
46
46
  }
47
47
  }
48
48
 
49
- export async function createEnvironment(storeId: string, name: string): Promise<void> {
49
+ export async function createEnvironment(organizationSlug: string, name: string): Promise<void> {
50
50
  const token = requireToken();
51
51
  const client = new SaleorCloudClient(token);
52
52
 
53
- console.log(info(`Creating environment: ${name} for store ${storeId}...`));
53
+ console.log(info(`Creating environment: ${name} for organization ${organizationSlug}...`));
54
54
 
55
55
  try {
56
- const result = await client.createEnvironment(storeId, name);
56
+ const { environment } = await client.createEnvironment(organizationSlug, 'default', name, 'us-east-1');
57
57
  console.log(success(`Environment created successfully!`));
58
- console.log(info(`Environment ID: ${result.environment.id}`));
59
- console.log(info(`API URL will be available at: https://${result.environment.id}.saleor.cloud`));
58
+ console.log(info(`Environment key: ${environment.key}`));
59
+ console.log(info(`Domain: ${environment.domain}`));
60
60
  } catch (err) {
61
61
  console.log(error(`Failed to create environment: ${err}`));
62
62
  process.exit(1);
@@ -36,67 +36,72 @@ describe('Command Handlers', () => {
36
36
  describe('Store Handlers', () => {
37
37
  Given('a valid token and successful API', () => {
38
38
  When('calling createStore', () => {
39
- Then('it should call POST /stores with name and region', async () => {
39
+ Then('it should call POST /organizations/:slug/projects with name and region', async () => {
40
40
  const fetchMock = mockFetch({
41
- '/stores': { store: fixtures.store },
41
+ '/organizations': { organizations: [fixtures.organization] },
42
+ '/organizations/my-org/projects': { project: fixtures.project },
42
43
  });
43
44
 
44
45
  const { createStore } = await import('../commands/store');
45
46
  await createStore('my-store', 'us-east-1');
46
47
 
47
- expect(fetchMock).toHaveBeenCalledTimes(1);
48
- const [url, opts] = fetchMock.mock.calls[0];
49
- expect(url).toContain('/stores');
50
- expect(opts.method).toBe('POST');
51
- expect(JSON.parse(opts.body)).toEqual({ name: 'my-store', region: 'us-east-1' });
48
+ expect(fetchMock).toHaveBeenCalled();
49
+ const createCall = fetchMock.mock.calls.find(([url]: [string]) =>
50
+ (url as string).includes('/organizations/my-org/projects')
51
+ );
52
+ expect(createCall).toBeDefined();
53
+ expect(createCall?.[1].method).toBe('POST');
54
+ expect(JSON.parse(createCall?.[1].body)).toEqual({ name: 'my-store', region: 'us-east-1' });
52
55
  });
53
56
  });
54
57
 
55
58
  When('calling listStores with results', () => {
56
- Then('it should display store names and IDs', async () => {
59
+ Then('it should display organization names and slugs', async () => {
57
60
  mockFetch({
58
- '/stores': { stores: [fixtures.store, fixtures.store2] },
61
+ '/organizations': { organizations: [fixtures.organization, fixtures.organization2] },
59
62
  });
60
63
 
61
64
  const { listStores } = await import('../commands/store');
62
65
  await listStores();
63
66
 
64
67
  const allOutput = console_.logs.join('\n');
65
- expect(allOutput).toContain('my-store');
66
- expect(allOutput).toContain('store-1');
67
- expect(allOutput).toContain('other-store');
68
- expect(allOutput).toContain('store-2');
68
+ expect(allOutput).toContain('My Organization');
69
+ expect(allOutput).toContain('my-org');
70
+ expect(allOutput).toContain('Other Organization');
71
+ expect(allOutput).toContain('other-org');
69
72
  });
70
73
  });
71
74
 
72
75
  When('calling listStores with empty results', () => {
73
76
  Then('it should display a helpful message', async () => {
74
77
  mockFetch({
75
- '/stores': { stores: [] },
78
+ '/organizations': { organizations: [] },
76
79
  });
77
80
 
78
81
  const { listStores } = await import('../commands/store');
79
82
  await listStores();
80
83
 
81
84
  const infoMessages = tuiCalls.filter(c => c.fn === 'info').map(c => c.msg).join('\n');
82
- expect(infoMessages).toContain('No stores found');
85
+ expect(infoMessages).toContain('No organizations found');
83
86
  });
84
87
  });
85
88
 
86
89
  When('calling createEnvironment', () => {
87
- Then('it should call POST /stores/:id/environments', async () => {
90
+ Then('it should call POST /organizations/:slug/projects/:slug/environments', async () => {
88
91
  const fetchMock = mockFetch({
89
- '/stores/store-1/environments': { environment: fixtures.environment },
92
+ '/organizations/my-org/projects/default/environments': { environment: fixtures.environment },
90
93
  });
91
94
 
92
95
  const { createEnvironment } = await import('../commands/store');
93
- await createEnvironment('store-1', 'staging');
94
-
95
- expect(fetchMock).toHaveBeenCalledTimes(1);
96
- const [url, opts] = fetchMock.mock.calls[0];
97
- expect(url).toContain('/stores/store-1/environments');
98
- expect(opts.method).toBe('POST');
99
- expect(JSON.parse(opts.body)).toEqual({ name: 'staging' });
96
+ await createEnvironment('my-org', 'staging');
97
+
98
+ expect(fetchMock).toHaveBeenCalled();
99
+ const createCall = fetchMock.mock.calls.find(([url]: [string]) =>
100
+ (url as string).includes('/organizations/my-org/projects/default/environments')
101
+ );
102
+ expect(createCall).toBeDefined();
103
+ expect(createCall?.[1].method).toBe('POST');
104
+ expect(JSON.parse(createCall?.[1].body)).toEqual({ name: 'staging', region: 'us-east-1' });
100
105
  });
101
106
  });
102
107
  });
@@ -137,7 +142,7 @@ describe('Command Handlers', () => {
137
142
  const { createEnvironment } = await import('../commands/store');
138
143
 
139
144
  try {
140
- await createEnvironment('bad-store', 'staging');
145
+ await createEnvironment('bad-org', 'staging');
141
146
  } catch {}
142
147
 
143
148
  expect(exitSpy).toHaveBeenCalledWith(1);
@@ -194,15 +199,15 @@ describe('Command Handlers', () => {
194
199
  When('calling createApp', () => {
195
200
  Then('it should register the hosted app via API', async () => {
196
201
  const fetchMock = mockFetch({
197
- '/environments/env-1/apps': { app: fixtures.app },
202
+ '/environments/staging/apps': { app: fixtures.app },
198
203
  });
199
204
 
200
205
  const { createApp } = await import('../commands/app');
201
- await createApp('pay-app', 'payment', 'env-1', 'dummy');
206
+ await createApp('pay-app', 'payment', 'staging', 'dummy');
202
207
 
203
208
  expect(fetchMock).toHaveBeenCalledTimes(1);
204
209
  const [url, opts] = fetchMock.mock.calls[0];
205
- expect(url).toContain('/environments/env-1/apps');
210
+ expect(url).toContain('/environments/staging/apps');
206
211
  expect(opts.method).toBe('POST');
207
212
  });
208
213
  });
@@ -216,7 +221,7 @@ describe('Command Handlers', () => {
216
221
  const { createApp } = await import('../commands/app');
217
222
 
218
223
  try {
219
- await createApp('pay-app', 'payment', 'env-1', 'dummy');
224
+ await createApp('pay-app', 'payment', 'staging', 'dummy');
220
225
  } catch {}
221
226
 
222
227
  expect(exitSpy).toHaveBeenCalledWith(1);
@@ -36,30 +36,31 @@ describe('End-to-End Flows', () => {
36
36
  describe('Store Creation Flow', () => {
37
37
  Given('a valid token and working API', () => {
38
38
  When('creating a store end-to-end', () => {
39
- Then('it should call API and display store ID and dashboard URL', async () => {
39
+ Then('it should call API and display project slug and dashboard URL', async () => {
40
40
  const fetchMock = mockFetch({
41
- '/stores': { store: fixtures.store },
41
+ '/organizations': { organizations: [fixtures.organization] },
42
+ '/organizations/my-org/projects': { project: fixtures.project },
42
43
  });
43
44
 
44
45
  const { createStore } = await import('../commands/store');
45
46
  await createStore('my-store', 'us-east-1');
46
47
 
47
- expect(fetchMock).toHaveBeenCalledTimes(1);
48
+ expect(fetchMock).toHaveBeenCalled();
48
49
 
49
50
  const allTui = tuiCalls.map(c => c.msg).join('\n');
50
- expect(allTui).toContain('store-1');
51
- expect(allTui).toContain('cloud.saleor.io/stores/store-1');
51
+ expect(allTui).toContain('my-store');
52
+ expect(allTui).toContain('cloud.saleor.io');
52
53
  });
53
54
  });
54
55
  });
55
56
  });
56
57
 
57
58
  describe('Store List Flow', () => {
58
- Given('a valid token and multiple stores', () => {
59
+ Given('a valid token and multiple organizations', () => {
59
60
  When('listing stores end-to-end', () => {
60
- Then('it should display all stores with regions and dates', async () => {
61
+ Then('it should display all organizations with emails and dates', async () => {
61
62
  mockFetch({
62
- '/stores': { stores: [fixtures.store, fixtures.store2] },
63
+ '/organizations': { organizations: [fixtures.organization, fixtures.organization2] },
63
64
  });
64
65
 
65
66
  const { listStores } = await import('../commands/store');
@@ -67,31 +68,31 @@ describe('End-to-End Flows', () => {
67
68
 
68
69
  const allTui = tuiCalls.map(c => c.msg).join('\n');
69
70
  const allConsole = console_.logs.join('\n');
70
- expect(allTui).toContain('2 store(s)');
71
- expect(allConsole).toContain('my-store');
72
- expect(allConsole).toContain('us-east-1');
73
- expect(allConsole).toContain('other-store');
74
- expect(allConsole).toContain('eu-west-1');
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');
75
76
  });
76
77
  });
77
78
  });
78
79
  });
79
80
 
80
81
  describe('Environment Creation Flow', () => {
81
- Given('a valid token and existing store', () => {
82
+ Given('a valid token and existing organization', () => {
82
83
  When('creating an environment end-to-end', () => {
83
- Then('it should call API and display environment ID and URL', async () => {
84
+ Then('it should call API and display environment key and domain', async () => {
84
85
  const fetchMock = mockFetch({
85
- '/stores/store-1/environments': { environment: fixtures.environment },
86
+ '/organizations/my-org/projects/default/environments': { environment: fixtures.environment },
86
87
  });
87
88
 
88
89
  const { createEnvironment } = await import('../commands/store');
89
- await createEnvironment('store-1', 'staging');
90
+ await createEnvironment('my-org', 'staging');
90
91
 
91
- expect(fetchMock).toHaveBeenCalledTimes(1);
92
+ expect(fetchMock).toHaveBeenCalled();
92
93
 
93
94
  const allTui = tuiCalls.map(c => c.msg).join('\n');
94
- expect(allTui).toContain('env-1');
95
+ expect(allTui).toContain('staging');
95
96
  expect(allTui).toContain('saleor.cloud');
96
97
  });
97
98
  });
@@ -103,11 +104,11 @@ describe('End-to-End Flows', () => {
103
104
  When('creating a payment app with environment', () => {
104
105
  Then('it should register via API and display app ID', async () => {
105
106
  const fetchMock = mockFetch({
106
- '/environments/env-1/apps': { app: fixtures.app },
107
+ '/environments/staging/apps': { app: fixtures.app },
107
108
  });
108
109
 
109
110
  const { createApp } = await import('../commands/app');
110
- await createApp('my-payment', 'payment', 'env-1', 'stripe');
111
+ await createApp('my-payment', 'payment', 'staging', 'stripe');
111
112
 
112
113
  expect(fetchMock).toHaveBeenCalledTimes(1);
113
114
 
@@ -138,7 +139,7 @@ describe('End-to-End Flows', () => {
138
139
  When('attempting any store operation', () => {
139
140
  Then('it should fail fast with auth error before API call', async () => {
140
141
  withoutToken();
141
- const fetchMock = mockFetch({ '/stores': { stores: [] } });
142
+ const fetchMock = mockFetch({ '/organizations': { organizations: [] } });
142
143
 
143
144
  const { listStores } = await import('../commands/store');
144
145
 
@@ -173,37 +174,55 @@ describe('End-to-End Flows', () => {
173
174
 
174
175
  describe('API Client Request Construction', () => {
175
176
  Given('a SaleorCloudClient', () => {
176
- When('creating a store', () => {
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/organizations');
190
+ expect(opts.method).toBe('GET');
191
+ expect(opts.headers.Authorization).toBe(`Token ${fixtures.token}`);
192
+ });
193
+ });
194
+
195
+ When('creating a project', () => {
177
196
  Then('it should send correct URL, method, headers, and body', async () => {
178
197
  const fetchMock = mockFetch({
179
- '/stores': { store: fixtures.store },
198
+ '/organizations/default/projects': { project: fixtures.project },
180
199
  });
181
200
 
182
201
  const { SaleorCloudClient } = await import('../api/client');
183
202
  const client = new SaleorCloudClient(fixtures.token);
184
- await client.createStore('test-store', 'eu-west-1');
203
+ await client.createProject('default', 'test-store', 'eu-west-1');
185
204
 
186
205
  expect(fetchMock).toHaveBeenCalledTimes(1);
187
206
  const [url, opts] = fetchMock.mock.calls[0];
188
- expect(url).toBe('https://cloud.saleor.io/api/stores');
207
+ expect(url).toBe('https://cloud.saleor.io/api/organizations/default/projects');
189
208
  expect(opts.method).toBe('POST');
190
- expect(opts.headers.Authorization).toBe(`Bearer ${fixtures.token}`);
209
+ expect(opts.headers.Authorization).toBe(`Token ${fixtures.token}`);
191
210
  expect(JSON.parse(opts.body)).toEqual({ name: 'test-store', region: 'eu-west-1' });
192
211
  });
193
212
  });
194
213
 
195
214
  When('registering an app', () => {
196
- Then('it should send correct URL with environment ID', async () => {
215
+ Then('it should send correct URL with environment key', async () => {
197
216
  const fetchMock = mockFetch({
198
- '/environments/env-1/apps': { app: fixtures.app },
217
+ '/environments/staging/apps': { app: fixtures.app },
199
218
  });
200
219
 
201
220
  const { SaleorCloudClient } = await import('../api/client');
202
221
  const client = new SaleorCloudClient(fixtures.token);
203
- await client.registerApp('env-1', 'payment', 'my-app');
222
+ await client.registerApp('staging', 'payment', 'my-app');
204
223
 
205
224
  const [url, opts] = fetchMock.mock.calls[0];
206
- expect(url).toBe('https://cloud.saleor.io/api/environments/env-1/apps');
225
+ expect(url).toBe('https://cloud.saleor.io/api/environments/staging/apps');
207
226
  expect(JSON.parse(opts.body)).toEqual({ type: 'payment', name: 'my-app' });
208
227
  });
209
228
  });
@@ -50,7 +50,10 @@ describe('Entry Points', () => {
50
50
  When('bootstrap calls createStore', () => {
51
51
  Then('it should pass project name and default region', async () => {
52
52
  withToken();
53
- mockFetch({ '/stores': { store: fixtures.store } });
53
+ mockFetch({
54
+ '/organizations': { organizations: [fixtures.organization] },
55
+ '/organizations/my-org/projects': { project: fixtures.project },
56
+ });
54
57
 
55
58
  const { createStore } = await import('../commands/store');
56
59
  await createStore('my-project', 'us-east-1');
@@ -76,7 +76,7 @@ describe('Error Handling', () => {
76
76
  const { SaleorCloudClient } = await import('../api/client');
77
77
  const client = new SaleorCloudClient(fixtures.token);
78
78
 
79
- await expect(client.getStores()).rejects.toThrow('401');
79
+ await expect(client.getOrganizations()).rejects.toThrow('401');
80
80
  });
81
81
  });
82
82
 
@@ -87,7 +87,7 @@ describe('Error Handling', () => {
87
87
  const { SaleorCloudClient } = await import('../api/client');
88
88
  const client = new SaleorCloudClient(fixtures.token);
89
89
 
90
- await expect(client.createStore('test', 'us-east-1')).rejects.toThrow('403');
90
+ await expect(client.createProject('test-org', 'test', 'us-east-1')).rejects.toThrow('403');
91
91
  });
92
92
  });
93
93
 
@@ -98,7 +98,7 @@ describe('Error Handling', () => {
98
98
  const { SaleorCloudClient } = await import('../api/client');
99
99
  const client = new SaleorCloudClient(fixtures.token);
100
100
 
101
- await expect(client.getEnvironments('store-1')).rejects.toThrow('500');
101
+ await expect(client.getEnvironments('test-org', 'test-project')).rejects.toThrow('500');
102
102
  });
103
103
  });
104
104
 
@@ -109,7 +109,7 @@ describe('Error Handling', () => {
109
109
  const { SaleorCloudClient } = await import('../api/client');
110
110
  const client = new SaleorCloudClient(fixtures.token);
111
111
 
112
- await expect(client.createEnvironment('bad-id', 'staging')).rejects.toThrow('404');
112
+ await expect(client.createEnvironment('bad-org', 'bad-project', 'staging', 'us-east-1')).rejects.toThrow('404');
113
113
  });
114
114
  });
115
115
  });
@@ -118,17 +118,17 @@ describe('Error Handling', () => {
118
118
  describe('API Request Headers', () => {
119
119
  Given('a SaleorCloudClient with a token', () => {
120
120
  When('making any request', () => {
121
- Then('it should include Authorization Bearer header', async () => {
121
+ Then('it should include Authorization Token header', async () => {
122
122
  const fetchMock = mockFetch({
123
- '/stores': { stores: [] },
123
+ '/organizations': { organizations: [] },
124
124
  });
125
125
 
126
126
  const { SaleorCloudClient } = await import('../api/client');
127
127
  const client = new SaleorCloudClient(fixtures.token);
128
- await client.getStores();
128
+ await client.getOrganizations();
129
129
 
130
130
  const [, opts] = fetchMock.mock.calls[0];
131
- expect(opts.headers.Authorization).toBe(`Bearer ${fixtures.token}`);
131
+ expect(opts.headers.Authorization).toBe(`Token ${fixtures.token}`);
132
132
  expect(opts.headers['Content-Type']).toBe('application/json');
133
133
  });
134
134
  });
package/src/test/mocks.ts CHANGED
@@ -1,44 +1,84 @@
1
1
  import { mock, spyOn } from 'bun:test';
2
- import type { Store, Environment, App } from '../api/client';
2
+ import type { Organization, Project, Environment, App } from '../api/client';
3
3
 
4
4
  export const fixtures = {
5
5
  token: 'test-token-abc123',
6
6
 
7
- store: {
8
- id: 'store-1',
7
+ // New API structure: Organizations → Projects → Environments
8
+ organization: {
9
+ slug: 'my-org',
10
+ name: 'My Organization',
11
+ created: '2025-01-01T00:00:00Z',
12
+ owner_email: 'owner@example.com',
13
+ company_name: 'My Company',
14
+ } satisfies Organization,
15
+
16
+ organization2: {
17
+ slug: 'other-org',
18
+ name: 'Other Organization',
19
+ created: '2025-06-15T00:00:00Z',
20
+ owner_email: 'other@example.com',
21
+ } satisfies Organization,
22
+
23
+ project: {
24
+ slug: 'my-store',
9
25
  name: 'my-store',
10
26
  region: 'us-east-1',
11
- created_at: '2025-01-01T00:00:00Z',
12
- } satisfies Store,
27
+ created: '2025-01-01T00:00:00Z',
28
+ sandboxes: { count: 2 },
29
+ } satisfies Project,
13
30
 
14
- store2: {
15
- id: 'store-2',
31
+ project2: {
32
+ slug: 'other-store',
16
33
  name: 'other-store',
17
34
  region: 'eu-west-1',
18
- created_at: '2025-06-15T00:00:00Z',
19
- } satisfies Store,
35
+ created: '2025-06-15T00:00:00Z',
36
+ sandboxes: { count: 1 },
37
+ } satisfies Project,
20
38
 
21
39
  environment: {
22
- id: 'env-1',
40
+ key: 'staging',
23
41
  name: 'staging',
24
- store_id: 'store-1',
25
- created_at: '2025-01-02T00:00:00Z',
42
+ domain: 'staging-my-store.saleor.cloud',
43
+ service: {
44
+ version: '3.15.0',
45
+ type: 'main',
46
+ region: 'us-east-1',
47
+ },
48
+ created: '2025-01-02T00:00:00Z',
49
+ project: { name: 'my-store', slug: 'my-store' },
26
50
  } satisfies Environment,
27
51
 
28
52
  app: {
29
53
  id: 'app-1',
30
54
  name: 'my-app',
31
55
  type: 'payment',
32
- environment_id: 'env-1',
56
+ environment_id: 'staging',
33
57
  } satisfies App,
58
+
59
+ // Backward compat aliases (for tests that haven't been migrated yet)
60
+ store: {
61
+ id: 'my-org',
62
+ name: 'My Organization',
63
+ region: 'us-east-1',
64
+ created_at: '2025-01-01T00:00:00Z',
65
+ },
66
+ store2: {
67
+ id: 'other-org',
68
+ name: 'Other Organization',
69
+ region: 'eu-west-1',
70
+ created_at: '2025-06-15T00:00:00Z',
71
+ },
34
72
  };
35
73
 
36
74
  export function mockFetch(routes: Record<string, unknown>): ReturnType<typeof mock> {
37
75
  const handler = mock((url: string | URL | Request, init?: RequestInit) => {
38
76
  const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url;
39
77
 
40
- for (const [pattern, body] of Object.entries(routes)) {
41
- if (urlStr.includes(pattern)) {
78
+ const sortedEntries = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
79
+
80
+ for (const [pattern, body] of sortedEntries) {
81
+ if (urlStr.endsWith(pattern)) {
42
82
  return Promise.resolve(new Response(JSON.stringify(body), {
43
83
  status: 200,
44
84
  headers: { 'Content-Type': 'application/json' },