@swell/cli 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ export default class ApiDelete extends SwellApiCommand {
8
8
  };
9
9
  static flags: {
10
10
  live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ api: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
12
  };
12
13
  static examples: string[];
13
14
  get method(): HttpMethod;
@@ -3,7 +3,8 @@ import { HttpMethod } from '../../lib/api.js';
3
3
  import { SwellApiCommand } from '../../swell-api-command.js';
4
4
  export default class ApiDelete extends SwellApiCommand {
5
5
  static summary = 'Send a DELETE request to the Swell API.';
6
- static description = 'Remove existing resources from the Swell Backend API.';
6
+ static description = `Remove existing resources from the Swell API.
7
+ Use --api frontend for public key authentication scoped to your app or user.`;
7
8
  static args = {
8
9
  path: Args.string({
9
10
  description: 'API endpoint path (must start with /)',
@@ -15,10 +16,16 @@ export default class ApiDelete extends SwellApiCommand {
15
16
  description: 'Use live environment instead of test',
16
17
  default: false,
17
18
  }),
19
+ api: Flags.string({
20
+ description: 'Use frontend or backend API (frontend uses public key auth).',
21
+ options: ['backend', 'frontend'],
22
+ default: 'backend',
23
+ }),
18
24
  };
19
25
  static examples = [
20
26
  'swell api delete /products/abc123',
21
27
  'swell api delete /functions/my-app/clear-cache',
28
+ 'swell api delete /products/abc123 --api frontend',
22
29
  ];
23
30
  get method() {
24
31
  return HttpMethod.DELETE;
@@ -8,6 +8,7 @@ export default class ApiGet extends SwellApiCommand {
8
8
  };
9
9
  static flags: {
10
10
  live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ api: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
12
  };
12
13
  static examples: string[];
13
14
  get method(): HttpMethod;
@@ -3,8 +3,9 @@ import { HttpMethod } from '../../lib/api.js';
3
3
  import { SwellApiCommand } from '../../swell-api-command.js';
4
4
  export default class ApiGet extends SwellApiCommand {
5
5
  static summary = 'Send a GET request to the Swell API.';
6
- static description = `Retrieve data from the Swell Backend API.
7
- The command performs a GET request to the specified endpoint path.`;
6
+ static description = `Retrieve data from the Swell API.
7
+ The command performs a GET request to the specified endpoint path.
8
+ Use --api frontend for public key authentication scoped to your app or user.`;
8
9
  static args = {
9
10
  path: Args.string({
10
11
  description: 'API endpoint path (must start with /)',
@@ -16,12 +17,18 @@ export default class ApiGet extends SwellApiCommand {
16
17
  description: 'Use live environment instead of test',
17
18
  default: false,
18
19
  }),
20
+ api: Flags.string({
21
+ description: 'Use frontend or backend API (frontend uses public key auth).',
22
+ options: ['backend', 'frontend'],
23
+ default: 'backend',
24
+ }),
19
25
  };
20
26
  static examples = [
21
27
  'swell api get /products',
22
28
  'swell api get "/products?limit=10&category=shoes"',
23
29
  'swell api get /products/abc123',
24
30
  'swell api get /functions/my-app/get-inventory',
31
+ 'swell api get /products --api frontend',
25
32
  ];
26
33
  get method() {
27
34
  return HttpMethod.GET;
@@ -2,8 +2,9 @@ import { Command } from '@oclif/core';
2
2
  export default class Api extends Command {
3
3
  static summary = 'Send requests directly to the Swell Backend API.';
4
4
  static description = `Run Swell API requests directly from the command line.
5
- Supports GET, POST, PUT, and DELETE methods with optional test/live environments.
6
- Call app functions via /functions/<app_id>/<function_name> paths.`;
5
+ Supports GET, POST, PUT, and DELETE methods with optional test/live environments.
6
+ Use --api frontend for public key authentication via the frontend API.
7
+ Call app functions via /functions/<app_id>/<function_name> paths.`;
7
8
  static examples = [
8
9
  'swell api get /products',
9
10
  'swell api post /products --body \'{"name":"New product"}\'',
@@ -11,6 +12,7 @@ export default class Api extends Command {
11
12
  'swell api delete /products/abc123',
12
13
  'swell api get /functions/my-app/get-inventory',
13
14
  'swell api post /functions/my-app/create-order --body \'{"items":["sku-1"]}\'',
15
+ 'swell api get /products --api frontend',
14
16
  ];
15
17
  async run() {
16
18
  this.log('Use "swell api get|post|put|delete" to interact with the Swell API.');
@@ -9,6 +9,7 @@ export default class ApiPost extends SwellApiCommand {
9
9
  static flags: {
10
10
  body: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
11
  live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
+ api: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
13
  };
13
14
  static examples: string[];
14
15
  get method(): HttpMethod;
@@ -3,8 +3,9 @@ import { HttpMethod } from '../../lib/api.js';
3
3
  import { SwellApiCommand } from '../../swell-api-command.js';
4
4
  export default class ApiPost extends SwellApiCommand {
5
5
  static summary = 'Send a POST request to the Swell API.';
6
- static description = `Create new resources in the Swell Backend API.
7
- You can provide a JSON body directly or specify a file path containing JSON data.`;
6
+ static description = `Create new resources in the Swell API.
7
+ You can provide a JSON body directly or specify a file path containing JSON data.
8
+ Use --api frontend for public key authentication scoped to your app or user.`;
8
9
  static args = {
9
10
  path: Args.string({
10
11
  description: 'API endpoint path (must start with /)',
@@ -19,11 +20,17 @@ export default class ApiPost extends SwellApiCommand {
19
20
  description: 'Use live environment instead of test',
20
21
  default: false,
21
22
  }),
23
+ api: Flags.string({
24
+ description: 'Use frontend or backend API (frontend uses public key auth).',
25
+ options: ['backend', 'frontend'],
26
+ default: 'backend',
27
+ }),
22
28
  };
23
29
  static examples = [
24
30
  'swell api post /products --body \'{"name": "Product", "price": 100}\'',
25
31
  'swell api post /products --body ./product.json',
26
32
  'swell api post /functions/my-app/create-order --body \'{"items":["sku-1"]}\'',
33
+ `swell api post /products --body '{"name": "Product"}' --api frontend`,
27
34
  ];
28
35
  get method() {
29
36
  return HttpMethod.POST;
@@ -9,6 +9,7 @@ export default class ApiPut extends SwellApiCommand {
9
9
  static flags: {
10
10
  body: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
11
  live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
+ api: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
13
  };
13
14
  static examples: string[];
14
15
  get method(): HttpMethod;
@@ -3,8 +3,9 @@ import { HttpMethod } from '../../lib/api.js';
3
3
  import { SwellApiCommand } from '../../swell-api-command.js';
4
4
  export default class ApiPut extends SwellApiCommand {
5
5
  static summary = 'Send a PUT request to the Swell API.';
6
- static description = `Update existing resources in the Swell Backend API.
7
- You can provide a JSON body directly or specify a file path containing JSON data.`;
6
+ static description = `Update existing resources in the Swell API.
7
+ You can provide a JSON body directly or specify a file path containing JSON data.
8
+ Use --api frontend for public key authentication scoped to your app or user.`;
8
9
  static args = {
9
10
  path: Args.string({
10
11
  description: 'API endpoint path (must start with /)',
@@ -19,11 +20,17 @@ export default class ApiPut extends SwellApiCommand {
19
20
  description: 'Use live environment instead of test',
20
21
  default: false,
21
22
  }),
23
+ api: Flags.string({
24
+ description: 'Use frontend or backend API (frontend uses public key auth).',
25
+ options: ['backend', 'frontend'],
26
+ default: 'backend',
27
+ }),
22
28
  };
23
29
  static examples = [
24
30
  'swell api put /products/{id} --body \'{"id": "abc123", "name": "Updated"}\'',
25
31
  'swell api put /products/abc123 --body ./product.json',
26
32
  'swell api put /functions/my-app/update-stock --body \'{"sku":"abc","qty":10}\'',
33
+ `swell api put /products/abc123 --body '{"name": "Updated"}' --api frontend`,
27
34
  ];
28
35
  get method() {
29
36
  return HttpMethod.PUT;
@@ -1,5 +1,5 @@
1
1
  import { Flags } from '@oclif/core';
2
- import { getProjectCommands, } from '../../../lib/apps/index.js';
2
+ import { CUSTOM_FRAMEWORK_SLUG, getManualDevHint, getProjectCommands, } from '../../../lib/apps/index.js';
3
3
  import { default as localConfig } from '../../../lib/config.js';
4
4
  import style from '../../../lib/style.js';
5
5
  import { PushAppCommand } from '../../../push-app-command.js';
@@ -70,6 +70,23 @@ export default class AppFrontendDev extends PushAppCommand {
70
70
  if (!projectType) {
71
71
  return;
72
72
  }
73
+ // Manual mode for unrecognized frameworks
74
+ if (projectType.slug === CUSTOM_FRAMEWORK_SLUG) {
75
+ const hintCommand = getManualDevHint(this.appPath, serverPort);
76
+ const currentStore = localConfig.getDefaultStore();
77
+ const sessionId = localConfig.getSessionId(currentStore);
78
+ this.log(`Detected a frontend project but could not identify the framework.`);
79
+ this.log(`Start your dev server manually on port ${serverPort}:\n`);
80
+ this.log(` cd frontend && ${hintCommand}\n`);
81
+ this.log(`The Swell tunnel is ready and will route traffic once your server is running.\n`);
82
+ this.logAppLocalUrl(currentStore, sessionId);
83
+ if (!isAppDev) {
84
+ this.watchForChanges();
85
+ }
86
+ // Keep the process alive so the tunnel remains open
87
+ await new Promise(() => { });
88
+ return;
89
+ }
73
90
  // Get commands transformed for the detected package manager
74
91
  const { devCommand } = getProjectCommands(this.appPath, projectType);
75
92
  const currentStore = localConfig.getDefaultStore();
@@ -17,7 +17,6 @@ export default class Models extends SwellCommand {
17
17
  private showAppModels;
18
18
  private showModels;
19
19
  private getModelPath;
20
- private getCurrentAppSlugId;
21
20
  private getAppSlugId;
22
21
  private showModelDetail;
23
22
  private resolveModelPath;
@@ -1,6 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
- import * as fs from 'node:fs';
3
2
  import { FetchError } from 'node-fetch';
3
+ import { getCurrentAppSlugId } from '../../lib/apps/index.js';
4
4
  import { default as localConfig } from '../../lib/config.js';
5
5
  import { SwellCommand } from '../../swell-command.js';
6
6
  export default class Models extends SwellCommand {
@@ -109,7 +109,7 @@ With a collection path, it retrieves the model for that specific collection in J
109
109
  this.showModels(models, 'Standard');
110
110
  }
111
111
  async showAppModels(appsById, modelsByAppId) {
112
- const currentAppSlugId = await this.getCurrentAppSlugId();
112
+ const currentAppSlugId = await getCurrentAppSlugId();
113
113
  for (const [appId, app] of Object.entries(appsById)) {
114
114
  const models = modelsByAppId[appId];
115
115
  if (models) {
@@ -153,19 +153,6 @@ With a collection path, it retrieves the model for that specific collection in J
153
153
  }
154
154
  return `/${model.name}`;
155
155
  }
156
- async getCurrentAppSlugId() {
157
- const hasAppContext = fs.existsSync('swell.json');
158
- if (!hasAppContext) {
159
- return;
160
- }
161
- try {
162
- const appConfig = JSON.parse(await fs.promises.readFile('swell.json', 'utf8'));
163
- return appConfig.id;
164
- }
165
- catch {
166
- // noop
167
- }
168
- }
169
156
  getAppSlugId(app) {
170
157
  return app.public_id || app.private_id.replace(/^_/, '');
171
158
  }
@@ -429,7 +429,7 @@ export class CreateAppCommand extends SwellCommand {
429
429
  // Required for yarn workspaces, good practice for all package managers
430
430
  private: true,
431
431
  scripts: {
432
- typecheck: '([ -z "$(find functions -name \'*.ts\' 2>/dev/null | head -1)" ] || tsc --noEmit) && ([ ! -f test/tsconfig.json ] || tsc --noEmit -p test) && ([ ! -f frontend/tsconfig.json ] || tsc --noEmit -p frontend)',
432
+ typecheck: '([ -z "$(find functions -name \'*.ts\' 2>/dev/null | head -1)" ] || tsc --noEmit) && ([ ! -f test/tsconfig.json ] || tsc --build --noEmit test) && ([ ! -f frontend/tsconfig.json ] || tsc --build --noEmit frontend)',
433
433
  },
434
434
  version: config.get('version'),
435
435
  };
@@ -1,6 +1,7 @@
1
1
  declare namespace _default {
2
2
  let API_BASE_URL: string;
3
3
  let ADMIN_API_BASE_URL: string;
4
+ let FRONTEND_API_BASE_URL: string;
4
5
  let LOGIN_HOST: string;
5
6
  let APP_PAGES_HOST: string;
6
7
  let STOREFRONT_FRONTEND_HOST: string;
package/dist/env/local.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export default {
2
2
  API_BASE_URL: 'https://localapi.schema.io:4443',
3
3
  ADMIN_API_BASE_URL: 'http://${STORE_ID}.swell.test:4001/admin/api',
4
+ FRONTEND_API_BASE_URL: 'http://${STORE_ID}.swell.test:4001/api',
4
5
  LOGIN_HOST: 'http://${STORE_ID}.swell.test:3000',
5
6
  APP_PAGES_HOST: 'http://${STORE_ID}--${APP_ID}--${ENV}.swell.test:4001',
6
7
  STOREFRONT_FRONTEND_HOST: 'http://${BRANCH_PREFIX}${STOREFRONT_SLUG}.swell.test:4001',
@@ -1,6 +1,7 @@
1
1
  declare namespace _default {
2
2
  let API_BASE_URL: string;
3
3
  let ADMIN_API_BASE_URL: string;
4
+ let FRONTEND_API_BASE_URL: string;
4
5
  let LOGIN_HOST: string;
5
6
  let APP_PAGES_HOST: string;
6
7
  let STOREFRONT_FRONTEND_HOST: string;
@@ -1,6 +1,7 @@
1
1
  export default {
2
2
  API_BASE_URL: 'https://api.swell.store',
3
3
  ADMIN_API_BASE_URL: 'https://${STORE_ID}.swell.store/admin/api',
4
+ FRONTEND_API_BASE_URL: 'https://${STORE_ID}.swell.store/api',
4
5
  LOGIN_HOST: 'https://${STORE_ID}.swell.store',
5
6
  APP_PAGES_HOST: 'https://${STORE_ID}--${APP_ID}--${ENV}.swell.store',
6
7
  STOREFRONT_FRONTEND_HOST: 'https://${BRANCH_PREFIX}${STOREFRONT_SLUG}.swell.store',
@@ -1,6 +1,7 @@
1
1
  declare namespace _default {
2
2
  let API_BASE_URL: string;
3
3
  let ADMIN_API_BASE_URL: string;
4
+ let FRONTEND_API_BASE_URL: string;
4
5
  let LOGIN_HOST: string;
5
6
  let APP_PAGES_HOST: string;
6
7
  let STOREFRONT_FRONTEND_HOST: string;
@@ -1,6 +1,7 @@
1
1
  export default {
2
2
  API_BASE_URL: 'https://${REVIEW_DOMAIN}.rav2.swell.store',
3
3
  ADMIN_API_BASE_URL: 'https://${STORE_ID}.${REVIEW_DOMAIN}.rav2.swell.store/admin/api',
4
+ FRONTEND_API_BASE_URL: 'https://${STORE_ID}.${REVIEW_DOMAIN}.rav2.swell.store/api',
4
5
  LOGIN_HOST: 'https://${REVIEW_DOMAIN}.rav2.swell.store',
5
6
  APP_PAGES_HOST: 'https://${STORE_ID}--${APP_ID}--${ENV}.${REVIEW_DOMAIN}.rav2.swell.store',
6
7
  STOREFRONT_FRONTEND_HOST: 'https://${BRANCH_PREFIX}${STOREFRONT_SLUG}.${REVIEW_DOMAIN}.rav2.swell.store',
@@ -1,6 +1,7 @@
1
1
  declare namespace _default {
2
2
  let API_BASE_URL: string;
3
3
  let ADMIN_API_BASE_URL: string;
4
+ let FRONTEND_API_BASE_URL: string;
4
5
  let LOGIN_HOST: string;
5
6
  let APP_PAGES_HOST: string;
6
7
  let STOREFRONT_FRONTEND_HOST: string;
@@ -1,6 +1,7 @@
1
1
  export default {
2
2
  API_BASE_URL: 'https://staging.swell.store',
3
3
  ADMIN_API_BASE_URL: 'https://${STORE_ID}.staging.swell.store/admin/api',
4
+ FRONTEND_API_BASE_URL: 'https://${STORE_ID}.staging.swell.store/api',
4
5
  LOGIN_HOST: 'https://staging.swell.store',
5
6
  APP_PAGES_HOST: 'https://${STORE_ID}--${APP_ID}--${ENV}.staging.swell.store',
6
7
  STOREFRONT_FRONTEND_HOST: 'https://${BRANCH_PREFIX}${STOREFRONT_SLUG}.staging.swell.store',
package/dist/lib/api.d.ts CHANGED
@@ -5,10 +5,11 @@ export declare enum HttpMethod {
5
5
  DELETE = "delete"
6
6
  }
7
7
  export default class Api {
8
+ storeId: string | undefined;
8
9
  envId: string | undefined;
9
10
  secretKey: string | undefined;
10
- storeId: string | undefined;
11
- constructor(storeId?: string, envId?: string, secretKey?: string);
11
+ publicKey: string | undefined;
12
+ constructor(storeId?: string, envId?: string, publicKey?: string, secretKey?: string);
12
13
  api(pathOpts: Api.Paths, options: Api.RequestOptions): Promise<any>;
13
14
  get(pathOpts: Api.Paths, options?: Api.RequestOptions): Promise<any>;
14
15
  getAll(pathOpts: Api.Paths, options?: Api.RequestOptions): Promise<{
@@ -21,6 +22,8 @@ export default class Api {
21
22
  isTestEnvEnabled(): Promise<boolean>;
22
23
  setEnv(envId: string): Promise<void>;
23
24
  setStoreEnv(storeId: string, envId?: string): Promise<void>;
25
+ setPublicKey(publicKey?: string): Promise<void>;
26
+ private isFrontend;
24
27
  private isAdmin;
25
28
  private truncatePath;
26
29
  private waitForAsyncResponse;
package/dist/lib/api.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import fetch from 'node-fetch';
2
2
  import { stringify } from 'qs';
3
+ import { getCurrentAppSlugId, hasAppContext } from './apps/index.js';
3
4
  import config from './config.js';
4
- import { API_BASE_URL, getAdminApiBaseUrl } from './constants.js';
5
+ import { API_BASE_URL, getAdminApiBaseUrl, getFrontendApiBaseUrl, } from './constants.js';
6
+ import style from './style.js';
5
7
  export var HttpMethod;
6
8
  (function (HttpMethod) {
7
9
  HttpMethod["GET"] = "get";
@@ -15,7 +17,7 @@ const defaultHeaders = (opts) => ({
15
17
  ...opts,
16
18
  });
17
19
  const authenticatedHeaders = (storeId, opts = {}) => {
18
- let { secretKey, sessionId, ...moreOpts } = opts;
20
+ let { publicKey, secretKey, sessionId, ...moreOpts } = opts;
19
21
  sessionId ??= config.getSessionId(storeId);
20
22
  if (secretKey) {
21
23
  return defaultHeaders({
@@ -23,6 +25,12 @@ const authenticatedHeaders = (storeId, opts = {}) => {
23
25
  ...moreOpts,
24
26
  });
25
27
  }
28
+ if (publicKey) {
29
+ return defaultHeaders({
30
+ Authorization: `Basic ${Buffer.from(publicKey).toString('base64')}`,
31
+ ...moreOpts,
32
+ });
33
+ }
26
34
  if (sessionId) {
27
35
  return defaultHeaders({
28
36
  'X-Session': sessionId,
@@ -32,12 +40,14 @@ const authenticatedHeaders = (storeId, opts = {}) => {
32
40
  return defaultHeaders(moreOpts);
33
41
  };
34
42
  export default class Api {
43
+ storeId;
35
44
  envId;
36
45
  secretKey;
37
- storeId;
38
- constructor(storeId, envId, secretKey) {
46
+ publicKey;
47
+ constructor(storeId, envId, publicKey, secretKey) {
39
48
  this.storeId = storeId;
40
49
  this.envId = envId;
50
+ this.publicKey = publicKey;
41
51
  this.secretKey = secretKey;
42
52
  }
43
53
  async api(pathOpts, options) {
@@ -50,14 +60,17 @@ export default class Api {
50
60
  ...options?.headers,
51
61
  ...(this.envId ? { 'Swell-Env': this.envId } : undefined),
52
62
  ...authenticatedHeaders(storeId, {
63
+ publicKey: this.publicKey,
53
64
  secretKey: this.secretKey,
54
65
  sessionId: options.sessionId,
55
66
  }),
56
67
  },
57
68
  };
58
- path = this.isAdmin()
59
- ? `${getAdminApiBaseUrl(storeId)}/${this.truncatePath(pathOpts.adminPath)}`
60
- : `${API_BASE_URL}/${this.truncatePath(pathOpts.backendPath)}`;
69
+ path = this.isFrontend(pathOpts)
70
+ ? `${getFrontendApiBaseUrl(storeId)}/${this.truncatePath(pathOpts.frontendPath)}`
71
+ : this.isAdmin(pathOpts)
72
+ ? `${getAdminApiBaseUrl(storeId)}/${this.truncatePath(pathOpts.adminPath)}`
73
+ : `${API_BASE_URL}/${this.truncatePath(pathOpts.backendPath)}`;
61
74
  if (options.query) {
62
75
  path += `?${stringify(options.query)}`;
63
76
  }
@@ -179,8 +192,46 @@ export default class Api {
179
192
  this.storeId = storeId;
180
193
  this.envId = envId;
181
194
  }
182
- isAdmin() {
183
- return !this.secretKey;
195
+ async setPublicKey(publicKey) {
196
+ if (publicKey) {
197
+ this.publicKey = publicKey;
198
+ return;
199
+ }
200
+ const appSlugId = await getCurrentAppSlugId();
201
+ const response = await this.post({ adminPath: '/data/:batch' }, {
202
+ query: [
203
+ {
204
+ method: 'get',
205
+ url: '/:clients/:self/keys/:last',
206
+ data: {
207
+ scope: 'user',
208
+ public: { $exists: true },
209
+ revoked: { $ne: true },
210
+ },
211
+ },
212
+ appSlugId && {
213
+ method: 'get',
214
+ url: '/:clients/:self/apps/:last',
215
+ data: {
216
+ app_private_id: `_${appSlugId}`,
217
+ },
218
+ },
219
+ ],
220
+ });
221
+ const [userKey, installedApp] = Object.values(response);
222
+ this.publicKey = installedApp?.public_key || userKey?.public;
223
+ if (!this.publicKey) {
224
+ throw new Error('No API public key available. Run inside a deployed app or create a user-scoped public key.');
225
+ }
226
+ else if (hasAppContext() && !installedApp?.public_key) {
227
+ console.warn(style.funcWarn('⚠ No app context found. Running in user scope.'));
228
+ }
229
+ }
230
+ isFrontend(pathOpts) {
231
+ return Boolean(pathOpts.frontendPath) && Boolean(this.publicKey);
232
+ }
233
+ isAdmin(pathOpts) {
234
+ return Boolean(pathOpts.adminPath) && !this.secretKey;
184
235
  }
185
236
  truncatePath(path) {
186
237
  return path?.startsWith('/') ? path.slice(1) : path;
@@ -126,6 +126,7 @@ export interface FrontendProjectType {
126
126
  name: string;
127
127
  slug: string;
128
128
  }
129
+ export declare const CUSTOM_FRAMEWORK_SLUG = "custom";
129
130
  export declare const FrontendProjectTypes: FrontendProjectType[];
130
131
  export declare function getFrontendProjectSlugs(withNone?: boolean, withLegacy?: boolean): string[];
131
132
  export declare function getFrontendProjectValidValues(withNone?: boolean, withLegacy?: boolean): string;
@@ -140,6 +141,11 @@ export declare function getProjectCommands(appPath: string, projectType: Fronten
140
141
  buildCommand?: string;
141
142
  devCommand: string;
142
143
  };
144
+ /**
145
+ * Get a suggested dev command for an unrecognized frontend project,
146
+ * translated for the detected package manager.
147
+ */
148
+ export declare function getManualDevHint(appPath: string, port: number): string;
143
149
  export declare function getConfigTypeFromPath(path: string): ConfigType | undefined;
144
150
  export declare function getConfigTypeKeyFromValue(value: string): string | undefined;
145
151
  export declare function filePathExists(filePath: string): boolean;
@@ -165,3 +171,5 @@ interface BatchAppFiles {
165
171
  * @returns {BatchAppFiles[]} Batches of files to push
166
172
  */
167
173
  export declare function batchAppFilesByType(configs: AppConfig[]): BatchAppFiles[];
174
+ export declare function hasAppContext(): boolean;
175
+ export declare function getCurrentAppSlugId(): Promise<string | undefined>;
@@ -114,6 +114,8 @@ export var ConfigInputFields;
114
114
  ConfigInputFields["VALUES"] = "values";
115
115
  ConfigInputFields["VERSION"] = "version";
116
116
  })(ConfigInputFields || (ConfigInputFields = {}));
117
+ // Slug for unrecognized frontend frameworks (manual dev server mode)
118
+ export const CUSTOM_FRAMEWORK_SLUG = 'custom';
117
119
  // Frontend project types - all use direct commands run from frontend/ directory
118
120
  export const FrontendProjectTypes = [
119
121
  {
@@ -198,6 +200,16 @@ export function getFrontendProjectType(appPath) {
198
200
  return detectedType;
199
201
  }
200
202
  }
203
+ // Fallback: if package.json has a dev script, treat as unrecognized framework
204
+ if (pkg.scripts?.dev) {
205
+ return {
206
+ buildCommand: pkg.scripts?.build ? 'npm run build' : '',
207
+ devCommand: '',
208
+ mainPackage: '',
209
+ name: 'Custom',
210
+ slug: CUSTOM_FRAMEWORK_SLUG,
211
+ };
212
+ }
201
213
  }
202
214
  catch {
203
215
  // Ignore JSON parse errors, try next path
@@ -220,6 +232,14 @@ export function getProjectCommands(appPath, projectType) {
220
232
  devCommand: transformCommand(projectType.devCommand, pm),
221
233
  };
222
234
  }
235
+ /**
236
+ * Get a suggested dev command for an unrecognized frontend project,
237
+ * translated for the detected package manager.
238
+ */
239
+ export function getManualDevHint(appPath, port) {
240
+ const pm = detectPackageManager(appPath);
241
+ return transformCommand(`npm run dev -- --port ${port}`, pm);
242
+ }
223
243
  export function getConfigTypeFromPath(path) {
224
244
  for (const type in AllConfigPaths) {
225
245
  if (AllConfigPaths[type] === path) {
@@ -361,3 +381,18 @@ export function batchAppFilesByType(configs) {
361
381
  // Add base files if config types are not specified
362
382
  return batches;
363
383
  }
384
+ export function hasAppContext() {
385
+ return fs.existsSync('swell.json');
386
+ }
387
+ export async function getCurrentAppSlugId() {
388
+ if (!hasAppContext()) {
389
+ return;
390
+ }
391
+ try {
392
+ const appConfig = JSON.parse(await fs.promises.readFile('swell.json', 'utf8'));
393
+ return appConfig.id;
394
+ }
395
+ catch {
396
+ // noop
397
+ }
398
+ }
@@ -2,6 +2,7 @@ export declare const API_BASE_URL: any;
2
2
  export declare const LOCAL_PROXY_PROVIDER: any;
3
3
  export declare const SCHEMAS_BASE_URL: any;
4
4
  export declare function getAdminApiBaseUrl(storeId: string): any;
5
+ export declare function getFrontendApiBaseUrl(storeId: string): any;
5
6
  export declare function getLoginHost(storeId?: string): any;
6
7
  export declare function getAppFrontendHost(storeId: string, installedAppId: string): any;
7
8
  export declare function getStorefrontFrontendHost(storefrontSlug: string, branchId?: string): any;
@@ -5,6 +5,8 @@ import { env } from './config.js';
5
5
  export const API_BASE_URL = env.get('API_BASE_URL') || defaultEnv.API_BASE_URL;
6
6
  // url for admin api calls
7
7
  const ADMIN_API_BASE_URL = env.get('ADMIN_API_BASE_URL') || defaultEnv.ADMIN_API_BASE_URL;
8
+ // url for store api calls
9
+ const FRONTEND_API_BASE_URL = env.get('FRONTEND_API_BASE_URL') || defaultEnv.FRONTEND_API_BASE_URL;
8
10
  // url host for admin login
9
11
  const LOGIN_HOST = env.get('LOGIN_HOST') || defaultEnv.LOGIN_HOST;
10
12
  // url host for app frontend
@@ -19,6 +21,9 @@ export const SCHEMAS_BASE_URL = env.get('SCHEMAS_BASE_URL') || defaultEnv.SCHEMA
19
21
  export function getAdminApiBaseUrl(storeId) {
20
22
  return ADMIN_API_BASE_URL.replace('${STORE_ID}', storeId);
21
23
  }
24
+ export function getFrontendApiBaseUrl(storeId) {
25
+ return FRONTEND_API_BASE_URL.replace('${STORE_ID}', storeId);
26
+ }
22
27
  export function getLoginHost(storeId) {
23
28
  return LOGIN_HOST.replace('${STORE_ID}', storeId || 'login');
24
29
  }
@@ -6,7 +6,7 @@ import * as path from 'node:path';
6
6
  import Stream from 'node:stream';
7
7
  import ora from 'ora';
8
8
  import { default as swellConfig } from './lib/app-config.js';
9
- import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
9
+ import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
10
10
  import { getGlobIgnorePathsChecker } from './lib/apps/paths.js';
11
11
  import { default as localConfig } from './lib/config.js';
12
12
  import { toAppId } from './lib/create/index.js';
@@ -110,6 +110,14 @@ export class PushAppCommand extends RemoteAppCommand {
110
110
  this.showFrontendMigrationError();
111
111
  return; // unreachable but helps TypeScript narrow the type
112
112
  }
113
+ // Skip automatic build for unrecognized frameworks
114
+ if (projectType.slug === CUSTOM_FRAMEWORK_SLUG) {
115
+ if (projectType.buildCommand) {
116
+ this.log(`Skipping automatic frontend build for unrecognized framework.\n` +
117
+ `Run your build command manually before deploying.\n`);
118
+ }
119
+ return;
120
+ }
113
121
  // Get commands transformed for the detected package manager
114
122
  const { buildCommand } = getProjectCommands(this.appPath, projectType);
115
123
  if (!buildCommand) {
@@ -758,7 +766,9 @@ export class PushAppCommand extends RemoteAppCommand {
758
766
  $environment_id: 'test',
759
767
  frontend: {
760
768
  deployment_hash: deploymentHash,
761
- framework: projectType.slug,
769
+ ...(projectType.slug !== CUSTOM_FRAMEWORK_SLUG && {
770
+ framework: projectType.slug,
771
+ }),
762
772
  service: 'cloudflare',
763
773
  url: deploymentUrl,
764
774
  },
@@ -29,10 +29,14 @@ export class SwellApiCommand extends SwellCommand {
29
29
  const parsedInput = await super.parse(options, argv);
30
30
  const { args, flags } = parsedInput;
31
31
  const { path: requestPath } = args;
32
- const { live, body } = flags;
32
+ const { live, api, body } = flags;
33
+ const isFrontendAPI = api === 'frontend';
33
34
  if (!live) {
34
35
  await this.api.setEnv('test');
35
36
  }
37
+ if (isFrontendAPI) {
38
+ await this.api.setPublicKey();
39
+ }
36
40
  if (!requestPath.startsWith('/')) {
37
41
  throw new Error('Path must start with a forward slash (/)');
38
42
  }
@@ -53,7 +57,9 @@ export class SwellApiCommand extends SwellCommand {
53
57
  const queryParams = this.parseQueryString(queryString);
54
58
  return this.buildFunctionCallRequest(parsedInput, functionId, processedBody, queryParams);
55
59
  }
56
- const paths = { adminPath: `/data${requestPath}` };
60
+ const paths = isFrontendAPI
61
+ ? { frontendPath: requestPath }
62
+ : { adminPath: `/data${requestPath}` };
57
63
  return {
58
64
  ...parsedInput,
59
65
  paths,
@@ -381,10 +381,11 @@
381
381
  "required": true
382
382
  }
383
383
  },
384
- "description": "Remove existing resources from the Swell Backend API.",
384
+ "description": "Remove existing resources from the Swell API.\n Use --api frontend for public key authentication scoped to your app or user.",
385
385
  "examples": [
386
386
  "swell api delete /products/abc123",
387
- "swell api delete /functions/my-app/clear-cache"
387
+ "swell api delete /functions/my-app/clear-cache",
388
+ "swell api delete /products/abc123 --api frontend"
388
389
  ],
389
390
  "flags": {
390
391
  "live": {
@@ -392,6 +393,18 @@
392
393
  "name": "live",
393
394
  "allowNo": false,
394
395
  "type": "boolean"
396
+ },
397
+ "api": {
398
+ "description": "Use frontend or backend API (frontend uses public key auth).",
399
+ "name": "api",
400
+ "default": "backend",
401
+ "hasDynamicHelp": false,
402
+ "multiple": false,
403
+ "options": [
404
+ "backend",
405
+ "frontend"
406
+ ],
407
+ "type": "option"
395
408
  }
396
409
  },
397
410
  "hasDynamicHelp": false,
@@ -419,12 +432,13 @@
419
432
  "required": true
420
433
  }
421
434
  },
422
- "description": "Retrieve data from the Swell Backend API.\n The command performs a GET request to the specified endpoint path.",
435
+ "description": "Retrieve data from the Swell API.\n The command performs a GET request to the specified endpoint path.\n Use --api frontend for public key authentication scoped to your app or user.",
423
436
  "examples": [
424
437
  "swell api get /products",
425
438
  "swell api get \"/products?limit=10&category=shoes\"",
426
439
  "swell api get /products/abc123",
427
- "swell api get /functions/my-app/get-inventory"
440
+ "swell api get /functions/my-app/get-inventory",
441
+ "swell api get /products --api frontend"
428
442
  ],
429
443
  "flags": {
430
444
  "live": {
@@ -432,6 +446,18 @@
432
446
  "name": "live",
433
447
  "allowNo": false,
434
448
  "type": "boolean"
449
+ },
450
+ "api": {
451
+ "description": "Use frontend or backend API (frontend uses public key auth).",
452
+ "name": "api",
453
+ "default": "backend",
454
+ "hasDynamicHelp": false,
455
+ "multiple": false,
456
+ "options": [
457
+ "backend",
458
+ "frontend"
459
+ ],
460
+ "type": "option"
435
461
  }
436
462
  },
437
463
  "hasDynamicHelp": false,
@@ -453,14 +479,15 @@
453
479
  "api": {
454
480
  "aliases": [],
455
481
  "args": {},
456
- "description": "Run Swell API requests directly from the command line.\n Supports GET, POST, PUT, and DELETE methods with optional test/live environments.\n Call app functions via /functions/<app_id>/<function_name> paths.",
482
+ "description": "Run Swell API requests directly from the command line.\n Supports GET, POST, PUT, and DELETE methods with optional test/live environments.\n Use --api frontend for public key authentication via the frontend API.\n Call app functions via /functions/<app_id>/<function_name> paths.",
457
483
  "examples": [
458
484
  "swell api get /products",
459
485
  "swell api post /products --body '{\"name\":\"New product\"}'",
460
486
  "swell api put /products/abc123 --body ./update.json",
461
487
  "swell api delete /products/abc123",
462
488
  "swell api get /functions/my-app/get-inventory",
463
- "swell api post /functions/my-app/create-order --body '{\"items\":[\"sku-1\"]}'"
489
+ "swell api post /functions/my-app/create-order --body '{\"items\":[\"sku-1\"]}'",
490
+ "swell api get /products --api frontend"
464
491
  ],
465
492
  "flags": {},
466
493
  "hasDynamicHelp": false,
@@ -489,11 +516,12 @@
489
516
  "required": true
490
517
  }
491
518
  },
492
- "description": "Create new resources in the Swell Backend API.\n You can provide a JSON body directly or specify a file path containing JSON data.",
519
+ "description": "Create new resources in the Swell API.\n You can provide a JSON body directly or specify a file path containing JSON data.\n Use --api frontend for public key authentication scoped to your app or user.",
493
520
  "examples": [
494
521
  "swell api post /products --body '{\"name\": \"Product\", \"price\": 100}'",
495
522
  "swell api post /products --body ./product.json",
496
- "swell api post /functions/my-app/create-order --body '{\"items\":[\"sku-1\"]}'"
523
+ "swell api post /functions/my-app/create-order --body '{\"items\":[\"sku-1\"]}'",
524
+ "swell api post /products --body '{\"name\": \"Product\"}' --api frontend"
497
525
  ],
498
526
  "flags": {
499
527
  "body": {
@@ -508,6 +536,18 @@
508
536
  "name": "live",
509
537
  "allowNo": false,
510
538
  "type": "boolean"
539
+ },
540
+ "api": {
541
+ "description": "Use frontend or backend API (frontend uses public key auth).",
542
+ "name": "api",
543
+ "default": "backend",
544
+ "hasDynamicHelp": false,
545
+ "multiple": false,
546
+ "options": [
547
+ "backend",
548
+ "frontend"
549
+ ],
550
+ "type": "option"
511
551
  }
512
552
  },
513
553
  "hasDynamicHelp": false,
@@ -535,11 +575,12 @@
535
575
  "required": true
536
576
  }
537
577
  },
538
- "description": "Update existing resources in the Swell Backend API.\n You can provide a JSON body directly or specify a file path containing JSON data.",
578
+ "description": "Update existing resources in the Swell API.\n You can provide a JSON body directly or specify a file path containing JSON data.\n Use --api frontend for public key authentication scoped to your app or user.",
539
579
  "examples": [
540
580
  "swell api put /products/{id} --body '{\"id\": \"abc123\", \"name\": \"Updated\"}'",
541
581
  "swell api put /products/abc123 --body ./product.json",
542
- "swell api put /functions/my-app/update-stock --body '{\"sku\":\"abc\",\"qty\":10}'"
582
+ "swell api put /functions/my-app/update-stock --body '{\"sku\":\"abc\",\"qty\":10}'",
583
+ "swell api put /products/abc123 --body '{\"name\": \"Updated\"}' --api frontend"
543
584
  ],
544
585
  "flags": {
545
586
  "body": {
@@ -554,6 +595,18 @@
554
595
  "name": "live",
555
596
  "allowNo": false,
556
597
  "type": "boolean"
598
+ },
599
+ "api": {
600
+ "description": "Use frontend or backend API (frontend uses public key auth).",
601
+ "name": "api",
602
+ "default": "backend",
603
+ "hasDynamicHelp": false,
604
+ "multiple": false,
605
+ "options": [
606
+ "backend",
607
+ "frontend"
608
+ ],
609
+ "type": "option"
557
610
  }
558
611
  },
559
612
  "hasDynamicHelp": false,
@@ -2884,5 +2937,5 @@
2884
2937
  ]
2885
2938
  }
2886
2939
  },
2887
- "version": "2.4.0"
2940
+ "version": "2.5.0"
2888
2941
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "type": "module",
5
5
  "description": "Swell's command line interface/utility",
6
6
  "keywords": [