@swell/cli 2.3.4 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/commands/api/delete.d.ts +1 -0
  2. package/dist/commands/api/delete.js +8 -1
  3. package/dist/commands/api/get.d.ts +1 -0
  4. package/dist/commands/api/get.js +9 -2
  5. package/dist/commands/api/index.js +4 -2
  6. package/dist/commands/api/post.d.ts +1 -0
  7. package/dist/commands/api/post.js +9 -2
  8. package/dist/commands/api/put.d.ts +1 -0
  9. package/dist/commands/api/put.js +9 -2
  10. package/dist/commands/app/dev.js +6 -2
  11. package/dist/commands/app/frontend/dev.js +6 -3
  12. package/dist/commands/create/app.js +2 -2
  13. package/dist/commands/create/frontend.js +2 -2
  14. package/dist/commands/create/tests.js +5 -2
  15. package/dist/commands/inspect/models.d.ts +0 -1
  16. package/dist/commands/inspect/models.js +2 -15
  17. package/dist/commands/theme/dev.js +10 -7
  18. package/dist/create-app-command.d.ts +8 -2
  19. package/dist/create-app-command.js +137 -79
  20. package/dist/env/local.d.ts +1 -0
  21. package/dist/env/local.js +1 -0
  22. package/dist/env/production.d.ts +1 -0
  23. package/dist/env/production.js +1 -0
  24. package/dist/env/review.d.ts +1 -0
  25. package/dist/env/review.js +1 -0
  26. package/dist/env/staging.d.ts +1 -0
  27. package/dist/env/staging.js +1 -0
  28. package/dist/lib/api.d.ts +5 -2
  29. package/dist/lib/api.js +60 -9
  30. package/dist/lib/apps/index.d.ts +11 -0
  31. package/dist/lib/apps/index.js +45 -46
  32. package/dist/lib/constants.d.ts +1 -0
  33. package/dist/lib/constants.js +5 -0
  34. package/dist/lib/create/tests.js +1 -1
  35. package/dist/lib/package-manager.d.ts +86 -0
  36. package/dist/lib/package-manager.js +186 -0
  37. package/dist/push-app-command.js +13 -6
  38. package/dist/swell-api-command.js +8 -2
  39. package/oclif.manifest.json +76 -15
  40. package/package.json +1 -1
@@ -9,9 +9,39 @@ import { promisify } from 'node:util';
9
9
  import ora from 'ora';
10
10
  import Api from './lib/api.js';
11
11
  import { FrontendProjectTypes, getFrontendProjectValidValues, getAllConfigPaths, getFrontendProjectSlugs, writeFile, writeJsonFile, } from './lib/apps/index.js';
12
+ import { getPackageManagerCommands, transformCreateCommand, } from './lib/package-manager.js';
12
13
  import style from './lib/style.js';
13
14
  import { SwellCommand } from './swell-command.js';
14
15
  const execAsync = promisify(exec);
16
+ /** Allowed hosts for tunnel providers used in local development */
17
+ const TUNNEL_ALLOWED_HOSTS = ['.ngrok.app', '.loca.lt', '.trycloudflare.com'];
18
+ /**
19
+ * Find the closing brace position for a config function call (e.g., defineConfig, defineNuxtConfig).
20
+ * Uses brace counting to find the matching `}` for the opening `{`.
21
+ * @returns Position of the closing `}`, or -1 if not found
22
+ */
23
+ function findConfigClosingBrace(content, functionName) {
24
+ const pattern = new RegExp(`${functionName}\\s*\\(\\s*\\{`);
25
+ const match = content.match(pattern);
26
+ if (!match || match.index === undefined) {
27
+ return -1;
28
+ }
29
+ const startPos = match.index + match[0].length;
30
+ let depth = 1;
31
+ for (let i = startPos; i < content.length && depth > 0; i++) {
32
+ const char = content[i];
33
+ if (char === '{') {
34
+ depth++;
35
+ }
36
+ else if (char === '}') {
37
+ depth--;
38
+ if (depth === 0) {
39
+ return i;
40
+ }
41
+ }
42
+ }
43
+ return -1;
44
+ }
15
45
  export class CreateAppCommand extends SwellCommand {
16
46
  // Command name used in error message examples; override in subclasses
17
47
  commandExample = 'swell create app';
@@ -76,51 +106,24 @@ export class CreateAppCommand extends SwellCommand {
76
106
  if (project.architect.serve.options.allowedHosts) {
77
107
  return;
78
108
  }
79
- project.architect.serve.options.allowedHosts = ['.ngrok.app', '.loca.lt'];
109
+ project.architect.serve.options.allowedHosts = TUNNEL_ALLOWED_HOSTS;
80
110
  await fs.writeFile(angularJsonPath, JSON.stringify(config, null, 2), 'utf8');
81
111
  }
82
112
  async addAllowedHostsToAstro(configPath) {
83
- const astroConfigPath = path.join(configPath, 'frontend', 'astro.config.mjs');
84
- let content;
85
- try {
86
- content = await fs.readFile(astroConfigPath, 'utf8');
87
- }
88
- catch {
89
- return;
90
- }
91
- if (content.includes('allowedHosts')) {
92
- return;
93
- }
94
- const lines = content.split('\n');
95
- let insertIndex = -1;
96
- for (const [i, line] of lines.entries()) {
97
- if (line.includes('defineConfig({')) {
98
- insertIndex = i + 1;
99
- break;
100
- }
101
- }
102
- if (insertIndex === -1) {
103
- return;
104
- }
105
- // Detect indentation from next line or use default
106
- const nextLine = lines[insertIndex];
107
- const indentMatch = nextLine?.match(/^(\s+)/);
108
- const baseIndent = indentMatch ? indentMatch[1] : ' ';
109
- const viteConfig = [
110
- `${baseIndent}vite: {`,
111
- `${baseIndent} server: {`,
112
- `${baseIndent} allowedHosts: ['.ngrok.app', '.loca.lt'],`,
113
- `${baseIndent} },`,
114
- `${baseIndent}},`,
115
- ];
116
- lines.splice(insertIndex, 0, ...viteConfig);
117
- await fs.writeFile(astroConfigPath, lines.join('\n'), 'utf8');
113
+ return this.addViteAllowedHosts(configPath, 'astro.config.mjs', 'defineConfig');
118
114
  }
119
115
  async addAllowedHostsToNuxt(configPath) {
120
- const nuxtConfigPath = path.join(configPath, 'frontend', 'nuxt.config.ts');
116
+ return this.addViteAllowedHosts(configPath, 'nuxt.config.ts', 'defineNuxtConfig');
117
+ }
118
+ /**
119
+ * Add vite allowedHosts configuration to a framework config file.
120
+ * Supports config files that use a defineX({}) pattern (Astro, Nuxt, etc.)
121
+ */
122
+ async addViteAllowedHosts(configPath, configFile, functionName) {
123
+ const configFilePath = path.join(configPath, 'frontend', configFile);
121
124
  let content;
122
125
  try {
123
- content = await fs.readFile(nuxtConfigPath, 'utf8');
126
+ content = await fs.readFile(configFilePath, 'utf8');
124
127
  }
125
128
  catch {
126
129
  return;
@@ -128,29 +131,30 @@ export class CreateAppCommand extends SwellCommand {
128
131
  if (content.includes('allowedHosts')) {
129
132
  return;
130
133
  }
131
- const lines = content.split('\n');
132
- let insertIndex = -1;
133
- for (const [i, line] of lines.entries()) {
134
- if (line.includes('defineNuxtConfig({')) {
135
- insertIndex = i + 1;
136
- break;
137
- }
134
+ const viteConfig = ` vite: {
135
+ server: {
136
+ allowedHosts: ${JSON.stringify(TUNNEL_ALLOWED_HOSTS)},
137
+ },
138
+ },`;
139
+ // Handle empty config: functionName({})
140
+ const emptyConfig = `${functionName}({})`;
141
+ if (content.includes(emptyConfig)) {
142
+ content = content.replace(emptyConfig, `${functionName}({\n${viteConfig}\n})`);
143
+ await fs.writeFile(configFilePath, content, 'utf8');
144
+ return;
138
145
  }
139
- if (insertIndex === -1) {
146
+ // Find the closing brace of the config function using brace counting
147
+ const closingPos = findConfigClosingBrace(content, functionName);
148
+ if (closingPos === -1) {
140
149
  return;
141
150
  }
142
- const nextLine = lines[insertIndex];
143
- const indentMatch = nextLine?.match(/^(\s+)/);
144
- const baseIndent = indentMatch ? indentMatch[1] : ' ';
145
- const viteConfig = [
146
- `${baseIndent}vite: {`,
147
- `${baseIndent} server: {`,
148
- `${baseIndent} allowedHosts: ['.ngrok.app', '.loca.lt'],`,
149
- `${baseIndent} },`,
150
- `${baseIndent}},`,
151
- ];
152
- lines.splice(insertIndex, 0, ...viteConfig);
153
- await fs.writeFile(nuxtConfigPath, lines.join('\n'), 'utf8');
151
+ const before = content.slice(0, closingPos);
152
+ const after = content.slice(closingPos);
153
+ // Ensure proper comma before vite config
154
+ const needsComma = before.trimEnd().slice(-1) !== ',';
155
+ const separator = needsComma ? ',\n' : '\n';
156
+ content = before.trimEnd() + separator + viteConfig + '\n' + after;
157
+ await fs.writeFile(configFilePath, content, 'utf8');
154
158
  }
155
159
  async createAppConfigFolders(swellConfig) {
156
160
  for (const type of getAllConfigPaths(swellConfig.get('type'))) {
@@ -188,19 +192,63 @@ export class CreateAppCommand extends SwellCommand {
188
192
  }));
189
193
  if (frameworkType && frameworkType !== 'none') {
190
194
  const projectType = this.getProjectType(frameworkType);
195
+ // Determine package manager - 'none' is not valid for frontend scaffolding
196
+ let pkg = (flags.pkg || 'npm');
197
+ if (flags.pkg === 'none') {
198
+ this.log(`\n${style.dim('Note: --pkg none is not available for frontend scaffolding, using npm.')}`);
199
+ pkg = 'npm';
200
+ // Ensure root package.json exists (skipped when --pkg none)
201
+ const rootPkgPath = path.join(configPath, 'package.json');
202
+ try {
203
+ await fs.access(rootPkgPath);
204
+ }
205
+ catch {
206
+ // Root package.json doesn't exist, create it
207
+ await this.setupPackage(swellConfig.get('id'), swellConfig, pkg);
208
+ }
209
+ }
191
210
  this.log();
211
+ // Check for placeholder frontend/package.json and remove it before C3 scaffolding
212
+ const frontendPath = path.join(configPath, 'frontend');
213
+ const frontendPkgPath = path.join(frontendPath, 'package.json');
214
+ try {
215
+ const content = await fs.readFile(frontendPkgPath, 'utf8');
216
+ const frontendPkg = JSON.parse(content);
217
+ // Only remove if it matches our placeholder signature
218
+ if (frontendPkg.version === '0.0.0' &&
219
+ frontendPkg.name === 'frontend' &&
220
+ frontendPkg.private === true) {
221
+ await fs.unlink(frontendPkgPath);
222
+ }
223
+ }
224
+ catch {
225
+ // No package.json or can't read, that's fine
226
+ }
227
+ // Check if frontend folder exists and has files (user-created content)
228
+ try {
229
+ const files = await fs.readdir(frontendPath);
230
+ if (files.length > 0) {
231
+ spinner.fail('frontend/ folder is not empty. Please remove existing files before scaffolding.');
232
+ return false;
233
+ }
234
+ }
235
+ catch {
236
+ // Folder doesn't exist, that's fine - C3 will create it
237
+ }
192
238
  spinner.start(`Creating ${projectType?.name} frontend app (this may take a while)...`);
193
239
  try {
194
240
  await execAsync(`mkdir -p frontend`, {
195
241
  cwd: configPath,
196
242
  });
197
- await execAsync(projectType.installCommand, {
243
+ // Transform install command for the selected package manager
244
+ const installCommand = transformCreateCommand(projectType.installCommand, pkg);
245
+ await execAsync(installCommand, {
198
246
  cwd: configPath,
199
247
  });
200
248
  // Use this command to debug output, i.e.e when command becomes non-responsive
201
249
  /* await this.execWithStdio(
202
250
  configPath,
203
- projectType.installCommand,
251
+ installCommand,
204
252
  ); */
205
253
  }
206
254
  catch (error) {
@@ -210,25 +258,23 @@ export class CreateAppCommand extends SwellCommand {
210
258
  return false;
211
259
  }
212
260
  // Ensure frontend package.json has correct name for workspace
213
- const frontendPkgPath = path.join(configPath, 'frontend', 'package.json');
214
261
  try {
215
262
  const pkgContent = await fs.readFile(frontendPkgPath, 'utf8');
216
- const pkg = JSON.parse(pkgContent);
217
- if (pkg.name !== 'frontend') {
218
- pkg.name = 'frontend';
219
- await fs.writeFile(frontendPkgPath, JSON.stringify(pkg, null, 2), 'utf8');
263
+ const frontendPkgJson = JSON.parse(pkgContent);
264
+ if (frontendPkgJson.name !== 'frontend') {
265
+ frontendPkgJson.name = 'frontend';
266
+ await fs.writeFile(frontendPkgPath, JSON.stringify(frontendPkgJson, null, 2), 'utf8');
220
267
  }
221
268
  }
222
269
  catch {
223
270
  // Ignore if package.json doesn't exist or can't be read
224
271
  }
225
272
  await this.addFrontendAllowedHosts(projectType, configPath);
226
- // Re-run npm install at root to properly initialize workspace structure
227
- // (removes frontend/package-lock.json, hoists dependencies, creates root lock file)
273
+ // Run install at root to hoist dependencies to workspace
228
274
  spinner.start('Initializing workspace...');
229
275
  try {
230
- const execAsync = promisify(exec);
231
- await execAsync('npm install', { cwd: configPath });
276
+ const { install } = getPackageManagerCommands(pkg);
277
+ await execAsync(install, { cwd: configPath });
232
278
  spinner.succeed('Workspace initialized');
233
279
  }
234
280
  catch {
@@ -375,19 +421,22 @@ export class CreateAppCommand extends SwellCommand {
375
421
  const configPath = path.dirname(config.path);
376
422
  const packageJson = {
377
423
  description: config.get('description'),
378
- // Include workspace even if frontend doesn't exist yet
379
- // npm tolerates missing workspace directories without errors
380
- workspaces: ['frontend'],
381
424
  devDependencies: {
382
425
  '@swell/app-types': '^1.0.5',
383
426
  typescript: '^5.9.3',
384
427
  },
385
428
  name,
429
+ // Required for yarn workspaces, good practice for all package managers
430
+ private: true,
386
431
  scripts: {
387
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)',
388
433
  },
389
434
  version: config.get('version'),
390
435
  };
436
+ // pnpm uses pnpm-workspace.yaml instead of workspaces field in package.json
437
+ if (pkg !== 'pnpm') {
438
+ packageJson.workspaces = ['frontend'];
439
+ }
391
440
  const tsConfig = {
392
441
  compilerOptions: {
393
442
  lib: ['esnext', 'webworker'],
@@ -401,15 +450,24 @@ export class CreateAppCommand extends SwellCommand {
401
450
  await writeJsonFile(path.join(configPath, 'package.json'), packageJson);
402
451
  await writeJsonFile(path.join(configPath, 'tsconfig.json'), tsConfig);
403
452
  await writeFile(path.join(configPath, '.gitignore'), `node_modules`);
404
- try {
405
- const packageManager = await this.findPackageManager(pkg);
406
- await execAsync(`${packageManager} install`, {
407
- cwd: configPath,
453
+ // Create pnpm-workspace.yaml for pnpm (required for workspace support)
454
+ if (pkg === 'pnpm') {
455
+ await writeFile(path.join(configPath, 'pnpm-workspace.yaml'), 'packages:\n - frontend\n');
456
+ }
457
+ // Create placeholder frontend/package.json for bun and yarn
458
+ // These package managers require workspace directories to exist with a package.json
459
+ if (pkg === 'bun' || pkg === 'yarn') {
460
+ const frontendPath = path.join(configPath, 'frontend');
461
+ await fs.mkdir(frontendPath, { recursive: true });
462
+ await writeJsonFile(path.join(frontendPath, 'package.json'), {
463
+ name: 'frontend',
464
+ private: true,
465
+ version: '0.0.0',
408
466
  });
409
467
  }
410
- catch (error) {
411
- this.error(error.message);
412
- }
468
+ // Install root dependencies using selected package manager
469
+ const { install } = getPackageManagerCommands(pkg);
470
+ await execAsync(install, { cwd: configPath });
413
471
  }
414
472
  async tryPackageSetup(name, config, pkg) {
415
473
  try {
@@ -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;
@@ -131,6 +131,15 @@ export declare function getFrontendProjectSlugs(withNone?: boolean, withLegacy?:
131
131
  export declare function getFrontendProjectValidValues(withNone?: boolean, withLegacy?: boolean): string;
132
132
  export declare function getAppSlugId(app: App): string | undefined;
133
133
  export declare function getFrontendProjectType(appPath: string): FrontendProjectType | undefined;
134
+ /**
135
+ * Get project commands transformed for the detected package manager.
136
+ * Detects the package manager from lock files in appPath and transforms
137
+ * npm-style commands to the equivalent for that package manager.
138
+ */
139
+ export declare function getProjectCommands(appPath: string, projectType: FrontendProjectType): {
140
+ buildCommand?: string;
141
+ devCommand: string;
142
+ };
134
143
  export declare function getConfigTypeFromPath(path: string): ConfigType | undefined;
135
144
  export declare function getConfigTypeKeyFromValue(value: string): string | undefined;
136
145
  export declare function filePathExists(filePath: string): boolean;
@@ -156,3 +165,5 @@ interface BatchAppFiles {
156
165
  * @returns {BatchAppFiles[]} Batches of files to push
157
166
  */
158
167
  export declare function batchAppFilesByType(configs: AppConfig[]): BatchAppFiles[];
168
+ export declare function hasAppContext(): boolean;
169
+ export declare function getCurrentAppSlugId(): Promise<string | undefined>;