@dk/jolly 0.1.6 → 0.1.8

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/api/client.ts
4
4
  class SaleorCloudClient {
5
- baseUrl = "https://cloud.saleor.io/api";
5
+ baseUrl = "https://cloud.saleor.io/api/v1";
6
6
  token;
7
7
  constructor(token) {
8
8
  this.token = token || process.env.SALEOR_CLOUD_TOKEN || "";
@@ -11,43 +11,67 @@ 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
+ const truncatedBody = body.length > 200 ? body.substring(0, 200) + "..." : body;
27
+ throw new Error(`API error: ${response.status} ${response.statusText} - ${truncatedBody}`);
24
28
  }
25
29
  return response.json();
26
30
  }
27
- async getStores() {
28
- return this.request("/stores");
31
+ async getOrganizations() {
32
+ return this.request("/organizations");
29
33
  }
30
- async createStore(name, region = "us-east-1") {
31
- return this.request("/stores", {
34
+ async getProjects(organizationSlug) {
35
+ return this.request(`/organizations/${organizationSlug}/projects`);
36
+ }
37
+ async createProject(organizationSlug, name, region) {
38
+ return this.request(`/organizations/${organizationSlug}/projects`, {
32
39
  method: "POST",
33
40
  body: JSON.stringify({ name, region })
34
41
  });
35
42
  }
36
- async getEnvironments(storeId) {
37
- return this.request(`/stores/${storeId}/environments`);
43
+ async getEnvironments(organizationSlug, projectSlug) {
44
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments`);
38
45
  }
39
- async createEnvironment(storeId, name) {
40
- return this.request(`/stores/${storeId}/environments`, {
46
+ async createEnvironment(organizationSlug, projectSlug, name, region) {
47
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments`, {
41
48
  method: "POST",
42
- body: JSON.stringify({ name })
49
+ body: JSON.stringify({ name, region })
43
50
  });
44
51
  }
52
+ async getEnvironment(organizationSlug, projectSlug, environmentSlug) {
53
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments/${environmentSlug}`);
54
+ }
45
55
  async registerApp(environmentId, appType, name) {
46
56
  return this.request(`/environments/${environmentId}/apps`, {
47
57
  method: "POST",
48
58
  body: JSON.stringify({ type: appType, name })
49
59
  });
50
60
  }
61
+ async getStores() {
62
+ return this.getOrganizations();
63
+ }
64
+ async createStore(name, region = "us-east-1") {
65
+ const { organizations } = await this.getOrganizations();
66
+ if (organizations.length === 0) {
67
+ throw new Error("No organizations found. Create one at https://cloud.saleor.io");
68
+ }
69
+ return this.createProject(organizations[0].slug, name, region);
70
+ }
71
+ async createEnvironmentFromStore(storeId, name) {
72
+ const { environments } = await this.getEnvironments(storeId, "default");
73
+ return this.createEnvironment(storeId, environments.length > 0 ? environments[0].project?.slug || "default" : "default", name, "default");
74
+ }
51
75
  }
52
76
 
53
77
  // src/api/auth.ts
@@ -129,8 +153,8 @@ async function createStore(name, region) {
129
153
  try {
130
154
  const result = await client.createStore(name, region);
131
155
  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}`));
156
+ console.log(info(`Project slug: ${result.project.slug}`));
157
+ console.log(info(`Dashboard: https://cloud.saleor.io/organizations/default/projects/${result.project.slug}`));
134
158
  } catch (err) {
135
159
  console.log(error(`Failed to create store: ${err}`));
136
160
  process.exit(1);
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import { hideBin } from "yargs/helpers";
24
24
 
25
25
  // src/api/client.ts
26
26
  class SaleorCloudClient {
27
- baseUrl = "https://cloud.saleor.io/api";
27
+ baseUrl = "https://cloud.saleor.io/api/v1";
28
28
  token;
29
29
  constructor(token) {
30
30
  this.token = token || process.env.SALEOR_CLOUD_TOKEN || "";
@@ -33,43 +33,67 @@ 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
+ const truncatedBody = body.length > 200 ? body.substring(0, 200) + "..." : body;
49
+ throw new Error(`API error: ${response.status} ${response.statusText} - ${truncatedBody}`);
46
50
  }
47
51
  return response.json();
48
52
  }
49
- async getStores() {
50
- return this.request("/stores");
53
+ async getOrganizations() {
54
+ return this.request("/organizations");
51
55
  }
52
- async createStore(name, region = "us-east-1") {
53
- return this.request("/stores", {
56
+ async getProjects(organizationSlug) {
57
+ return this.request(`/organizations/${organizationSlug}/projects`);
58
+ }
59
+ async createProject(organizationSlug, name, region) {
60
+ return this.request(`/organizations/${organizationSlug}/projects`, {
54
61
  method: "POST",
55
62
  body: JSON.stringify({ name, region })
56
63
  });
57
64
  }
58
- async getEnvironments(storeId) {
59
- return this.request(`/stores/${storeId}/environments`);
65
+ async getEnvironments(organizationSlug, projectSlug) {
66
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments`);
60
67
  }
61
- async createEnvironment(storeId, name) {
62
- return this.request(`/stores/${storeId}/environments`, {
68
+ async createEnvironment(organizationSlug, projectSlug, name, region) {
69
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments`, {
63
70
  method: "POST",
64
- body: JSON.stringify({ name })
71
+ body: JSON.stringify({ name, region })
65
72
  });
66
73
  }
74
+ async getEnvironment(organizationSlug, projectSlug, environmentSlug) {
75
+ return this.request(`/organizations/${organizationSlug}/projects/${projectSlug}/environments/${environmentSlug}`);
76
+ }
67
77
  async registerApp(environmentId, appType, name) {
68
78
  return this.request(`/environments/${environmentId}/apps`, {
69
79
  method: "POST",
70
80
  body: JSON.stringify({ type: appType, name })
71
81
  });
72
82
  }
83
+ async getStores() {
84
+ return this.getOrganizations();
85
+ }
86
+ async createStore(name, region = "us-east-1") {
87
+ const { organizations } = await this.getOrganizations();
88
+ if (organizations.length === 0) {
89
+ throw new Error("No organizations found. Create one at https://cloud.saleor.io");
90
+ }
91
+ return this.createProject(organizations[0].slug, name, region);
92
+ }
93
+ async createEnvironmentFromStore(storeId, name) {
94
+ const { environments } = await this.getEnvironments(storeId, "default");
95
+ return this.createEnvironment(storeId, environments.length > 0 ? environments[0].project?.slug || "default" : "default", name, "default");
96
+ }
73
97
  }
74
98
 
75
99
  // src/api/auth.ts
@@ -198,8 +222,8 @@ async function createStore(name, region) {
198
222
  try {
199
223
  const result = await client.createStore(name, region);
200
224
  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}`));
225
+ console.log(info(`Project slug: ${result.project.slug}`));
226
+ console.log(info(`Dashboard: https://cloud.saleor.io/organizations/default/projects/${result.project.slug}`));
203
227
  } catch (err) {
204
228
  console.log(error(`Failed to create store: ${err}`));
205
229
  process.exit(1);
@@ -208,19 +232,19 @@ async function createStore(name, region) {
208
232
  async function listStores() {
209
233
  const token = requireToken();
210
234
  const client = new SaleorCloudClient(token);
211
- console.log(info("Fetching stores..."));
235
+ console.log(info("Fetching organizations..."));
212
236
  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>"));
237
+ const { organizations } = await client.getOrganizations();
238
+ if (organizations.length === 0) {
239
+ console.log(info("No organizations found. Create one at https://cloud.saleor.io"));
216
240
  return;
217
241
  }
218
- console.log(success(`Found ${result.stores.length} store(s):
242
+ console.log(success(`Found ${organizations.length} organization(s):
219
243
  `));
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()}`);
244
+ for (const org of organizations) {
245
+ console.log(` ${org.name} (${org.slug})`);
246
+ console.log(` Email: ${org.owner_email}`);
247
+ console.log(` Created: ${new Date(org.created).toLocaleDateString()}`);
224
248
  console.log();
225
249
  }
226
250
  } catch (err) {
@@ -228,15 +252,15 @@ async function listStores() {
228
252
  process.exit(1);
229
253
  }
230
254
  }
231
- async function createEnvironment(storeId, name) {
255
+ async function createEnvironment(organizationSlug, name) {
232
256
  const token = requireToken();
233
257
  const client = new SaleorCloudClient(token);
234
- console.log(info(`Creating environment: ${name} for store ${storeId}...`));
258
+ console.log(info(`Creating environment: ${name} for organization ${organizationSlug}...`));
235
259
  try {
236
- const result = await client.createEnvironment(storeId, name);
260
+ const { environment } = await client.createEnvironment(organizationSlug, "default", name, "us-east-1");
237
261
  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`));
262
+ console.log(info(`Environment key: ${environment.key}`));
263
+ console.log(info(`Domain: ${environment.domain}`));
240
264
  } catch (err) {
241
265
  console.log(error(`Failed to create environment: ${err}`));
242
266
  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.8",
4
4
  "description": "Saleor project bootstrapper and agent configurator",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api/client.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export class SaleorCloudClient {
2
- private baseUrl = 'https://cloud.saleor.io/api';
2
+ private baseUrl = 'https://cloud.saleor.io/api/v1';
3
3
  private token: string;
4
4
 
5
5
  constructor(token?: string) {
@@ -10,63 +10,137 @@ 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
+ const truncatedBody = body.length > 200 ? body.substring(0, 200) + '...' : body;
28
+ throw new Error(`API error: ${response.status} ${response.statusText} - ${truncatedBody}`);
24
29
  }
25
30
 
26
- return response.json();
31
+ return response.json() as T;
27
32
  }
28
33
 
29
- async getStores() {
30
- return this.request<{ stores: Store[] }>('/stores');
34
+ // Organizations
35
+ async getOrganizations() {
36
+ return this.request<{ organizations: Organization[] }>('/organizations');
31
37
  }
32
38
 
33
- async createStore(name: string, region: string = 'us-east-1') {
34
- return this.request<{ store: Store }>('/stores', {
39
+ // Projects
40
+ async getProjects(organizationSlug: string) {
41
+ return this.request<{ projects: Project[] }>(`/organizations/${organizationSlug}/projects`);
42
+ }
43
+
44
+ async createProject(organizationSlug: string, name: string, region: string) {
45
+ return this.request<{ project: Project }>(`/organizations/${organizationSlug}/projects`, {
35
46
  method: 'POST',
36
47
  body: JSON.stringify({ name, region }),
37
48
  });
38
49
  }
39
50
 
40
- async getEnvironments(storeId: string) {
41
- return this.request<{ environments: Environment[] }>(`/stores/${storeId}/environments`);
51
+ // Environments
52
+ async getEnvironments(organizationSlug: string, projectSlug: string) {
53
+ return this.request<{ environments: Environment[] }>(
54
+ `/organizations/${organizationSlug}/projects/${projectSlug}/environments`
55
+ );
42
56
  }
43
57
 
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
- });
58
+ async createEnvironment(organizationSlug: string, projectSlug: string, name: string, region: string) {
59
+ return this.request<{ environment: Environment; task: Task }>(
60
+ `/organizations/${organizationSlug}/projects/${projectSlug}/environments`,
61
+ {
62
+ method: 'POST',
63
+ body: JSON.stringify({ name, region }),
64
+ }
65
+ );
49
66
  }
50
67
 
68
+ async getEnvironment(organizationSlug: string, projectSlug: string, environmentSlug: string) {
69
+ return this.request<Environment>(
70
+ `/organizations/${organizationSlug}/projects/${projectSlug}/environments/${environmentSlug}`
71
+ );
72
+ }
73
+
74
+ // Apps
51
75
  async registerApp(environmentId: string, appType: string, name: string) {
52
76
  return this.request<{ app: App }>(`/environments/${environmentId}/apps`, {
53
77
  method: 'POST',
54
78
  body: JSON.stringify({ type: appType, name }),
55
79
  });
56
80
  }
81
+
82
+ // Backward compatibility methods
83
+ async getStores() {
84
+ return this.getOrganizations();
85
+ }
86
+
87
+ async createStore(name: string, region: string = 'us-east-1') {
88
+ // Create organization as "store" - requires org slug
89
+ // For now, create in first available org
90
+ const { organizations } = await this.getOrganizations();
91
+ if (organizations.length === 0) {
92
+ throw new Error('No organizations found. Create one at https://cloud.saleor.io');
93
+ }
94
+ return this.createProject(organizations[0].slug, name, region);
95
+ }
96
+
97
+ async createEnvironmentFromStore(storeId: string, name: string) {
98
+ // storeId is used as organization slug for backward compatibility
99
+ const { environments } = await this.getEnvironments(storeId, 'default');
100
+ // Create sandbox environment
101
+ return this.createEnvironment(storeId, environments.length > 0 ? environments[0].project?.slug || 'default' : 'default', name, 'default');
102
+ }
57
103
  }
58
104
 
59
- export interface Store {
60
- id: string;
105
+ export interface Organization {
106
+ slug: string;
107
+ name: string;
108
+ created: string;
109
+ company_name?: string;
110
+ owner_email: string;
111
+ }
112
+
113
+ export interface Project {
114
+ slug: string;
61
115
  name: string;
62
116
  region: string;
63
- created_at: string;
117
+ created: string;
118
+ billing_period?: { start: string };
119
+ sandboxes: { count: number };
64
120
  }
65
121
 
66
122
  export interface Environment {
123
+ key: string;
124
+ name: string;
125
+ domain: string;
126
+ service: {
127
+ version: string;
128
+ type: string;
129
+ region: string;
130
+ };
131
+ created: string;
132
+ project: { name: string; slug: string };
133
+ }
134
+
135
+ export interface Task {
136
+ id: string;
137
+ status: string;
138
+ }
139
+
140
+ export interface Store {
67
141
  id: string;
68
142
  name: string;
69
- store_id: string;
143
+ region: string;
70
144
  created_at: string;
71
145
  }
72
146
 
@@ -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/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', () => {
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/v1/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/v1/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' },