@swell/cli 2.6.0 → 2.7.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.
Files changed (42) hide show
  1. package/dist/commands/app/frontend/dev.js +18 -0
  2. package/dist/commands/inspect/content.d.ts +25 -14
  3. package/dist/commands/inspect/content.js +34 -144
  4. package/dist/commands/inspect/functions.d.ts +48 -0
  5. package/dist/commands/inspect/functions.js +83 -0
  6. package/dist/commands/inspect/index.js +8 -7
  7. package/dist/commands/inspect/models.d.ts +17 -2
  8. package/dist/commands/inspect/models.js +114 -47
  9. package/dist/commands/inspect/notifications.d.ts +31 -0
  10. package/dist/commands/inspect/notifications.js +125 -0
  11. package/dist/commands/inspect/settings.d.ts +28 -0
  12. package/dist/commands/inspect/settings.js +82 -0
  13. package/dist/commands/inspect/webhooks.d.ts +34 -0
  14. package/dist/commands/inspect/webhooks.js +61 -0
  15. package/dist/create-app-command.d.ts +7 -0
  16. package/dist/create-app-command.js +66 -3
  17. package/dist/inspect-resource-command.d.ts +91 -0
  18. package/dist/inspect-resource-command.js +232 -0
  19. package/dist/lib/apps/index.d.ts +2 -1
  20. package/dist/lib/apps/index.js +43 -6
  21. package/dist/lib/apps/inspect-scope.d.ts +58 -0
  22. package/dist/lib/apps/inspect-scope.js +60 -0
  23. package/dist/lib/apps/object-id.d.ts +7 -0
  24. package/dist/lib/apps/object-id.js +9 -0
  25. package/dist/lib/apps/paths.js +8 -1
  26. package/dist/lib/apps/resolve.d.ts +16 -0
  27. package/dist/lib/apps/resolve.js +39 -0
  28. package/dist/lib/apps/slug.d.ts +29 -0
  29. package/dist/lib/apps/slug.js +12 -0
  30. package/dist/lib/inspect/content.d.ts +39 -0
  31. package/dist/lib/inspect/content.js +76 -0
  32. package/dist/lib/inspect/notifications.d.ts +115 -0
  33. package/dist/lib/inspect/notifications.js +173 -0
  34. package/dist/lib/inspect/settings.d.ts +61 -0
  35. package/dist/lib/inspect/settings.js +56 -0
  36. package/dist/lib/inspect/table.d.ts +29 -0
  37. package/dist/lib/inspect/table.js +61 -0
  38. package/dist/push-app-command.js +3 -2
  39. package/dist/swell-api-command.d.ts +0 -4
  40. package/dist/swell-api-command.js +2 -18
  41. package/oclif.manifest.json +328 -35
  42. package/package.json +1 -1
@@ -47,7 +47,7 @@ export class CreateAppCommand extends SwellCommand {
47
47
  commandExample = 'swell create app';
48
48
  static baseFlags = {
49
49
  frontend: Flags.string({
50
- description: 'Framework: astro | angular | hono | nuxt | nextjs | none',
50
+ description: `Framework: ${getFrontendProjectSlugs(true, false).join(' | ')}`,
51
51
  options: getFrontendProjectSlugs(true, false),
52
52
  }),
53
53
  'storefront-app': Flags.string({
@@ -115,6 +115,57 @@ export class CreateAppCommand extends SwellCommand {
115
115
  async addAllowedHostsToNuxt(configPath) {
116
116
  return this.addViteAllowedHosts(configPath, 'nuxt.config.ts', 'defineNuxtConfig');
117
117
  }
118
+ /**
119
+ * For frameworks whose dev server is Vite directly. Astro/Nuxt nest the
120
+ * vite block inside their own defineConfig — see addViteAllowedHosts.
121
+ */
122
+ async addAllowedHostsToVite(configPath) {
123
+ const candidates = [
124
+ 'vite.config.ts',
125
+ 'vite.config.js',
126
+ 'vite.config.mts',
127
+ 'vite.config.mjs',
128
+ ];
129
+ let configFilePath;
130
+ let content;
131
+ for (const fileName of candidates) {
132
+ const candidatePath = path.join(configPath, 'frontend', fileName);
133
+ try {
134
+ // eslint-disable-next-line no-await-in-loop
135
+ content = await fs.readFile(candidatePath, 'utf8');
136
+ configFilePath = candidatePath;
137
+ break;
138
+ }
139
+ catch {
140
+ /* try next */
141
+ }
142
+ }
143
+ if (!configFilePath || content === undefined) {
144
+ return;
145
+ }
146
+ if (content.includes('allowedHosts')) {
147
+ return;
148
+ }
149
+ const serverBlock = ` server: {
150
+ allowedHosts: ${JSON.stringify(TUNNEL_ALLOWED_HOSTS)},
151
+ },`;
152
+ const emptyConfig = 'defineConfig({})';
153
+ if (content.includes(emptyConfig)) {
154
+ content = content.replace(emptyConfig, `defineConfig({\n${serverBlock}\n})`);
155
+ await fs.writeFile(configFilePath, content, 'utf8');
156
+ return;
157
+ }
158
+ const closingPos = findConfigClosingBrace(content, 'defineConfig');
159
+ if (closingPos === -1) {
160
+ return;
161
+ }
162
+ const before = content.slice(0, closingPos);
163
+ const after = content.slice(closingPos);
164
+ const needsComma = before.trimEnd().slice(-1) !== ',';
165
+ const separator = needsComma ? ',\n' : '\n';
166
+ content = before.trimEnd() + separator + serverBlock + '\n' + after;
167
+ await fs.writeFile(configFilePath, content, 'utf8');
168
+ }
118
169
  /**
119
170
  * Add vite allowedHosts configuration to a framework config file.
120
171
  * Supports config files that use a defineX({}) pattern (Astro, Nuxt, etc.)
@@ -170,6 +221,7 @@ export class CreateAppCommand extends SwellCommand {
170
221
  const configPath = path.dirname(swellConfig.path);
171
222
  // use 'none' by default for non-interactive mode
172
223
  const inputFrontend = flags.yes ? flags.frontend || 'none' : flags.frontend;
224
+ const appType = swellConfig.get('type');
173
225
  const frameworkType = inputFrontend ||
174
226
  (await select({
175
227
  choices: [
@@ -181,7 +233,10 @@ export class CreateAppCommand extends SwellCommand {
181
233
  value: null,
182
234
  },
183
235
  ]),
184
- ...FrontendProjectTypes.map(({ name, slug }) => ({
236
+ ...FrontendProjectTypes.filter((pt) => !pt.appTypes || pt.appTypes.includes(appType))
237
+ .sort((a, b) => (a.displayOrder ?? Number.POSITIVE_INFINITY) -
238
+ (b.displayOrder ?? Number.POSITIVE_INFINITY))
239
+ .map(({ name, slug }) => ({
185
240
  name,
186
241
  value: slug,
187
242
  })),
@@ -192,6 +247,9 @@ export class CreateAppCommand extends SwellCommand {
192
247
  }));
193
248
  if (frameworkType && frameworkType !== 'none') {
194
249
  const projectType = this.getProjectType(frameworkType);
250
+ if (projectType.appTypes && !projectType.appTypes.includes(appType)) {
251
+ this.error(`${projectType.name} is not available for '${appType}' apps. Allowed app types: ${projectType.appTypes.join(', ')}.`, { exit: 1 });
252
+ }
195
253
  // Determine package manager - 'none' is not valid for frontend scaffolding
196
254
  let pkg = (flags.pkg || 'npm');
197
255
  if (flags.pkg === 'none') {
@@ -449,7 +507,7 @@ export class CreateAppCommand extends SwellCommand {
449
507
  };
450
508
  await writeJsonFile(path.join(configPath, 'package.json'), packageJson);
451
509
  await writeJsonFile(path.join(configPath, 'tsconfig.json'), tsConfig);
452
- await writeFile(path.join(configPath, '.gitignore'), `node_modules`);
510
+ await writeFile(path.join(configPath, '.gitignore'), `node_modules\n.wrangler\n.dev.vars\n.dev.vars.*\n`);
453
511
  // Create pnpm-workspace.yaml for pnpm (required for workspace support)
454
512
  if (pkg === 'pnpm') {
455
513
  await writeFile(path.join(configPath, 'pnpm-workspace.yaml'), 'packages:\n - frontend\n');
@@ -518,6 +576,11 @@ export class CreateAppCommand extends SwellCommand {
518
576
  await this.addAllowedHostsToNuxt(configPath);
519
577
  break;
520
578
  }
579
+ case 'react':
580
+ case 'react-storefront': {
581
+ await this.addAllowedHostsToVite(configPath);
582
+ break;
583
+ }
521
584
  // Other frameworks don't require allowedHosts configuration
522
585
  }
523
586
  }
@@ -0,0 +1,91 @@
1
+ import { InspectScope } from './lib/apps/inspect-scope.js';
2
+ import { GroupInfo } from './lib/inspect/table.js';
3
+ import { SwellCommand } from './swell-command.js';
4
+ export declare const inspectResourceBaseArgs: {
5
+ identifier: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
6
+ };
7
+ export declare const inspectResourceBaseFlags: {
8
+ app: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
+ live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
+ json: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
+ };
13
+ export interface InspectResourceCommandParsed {
14
+ args: {
15
+ identifier?: string;
16
+ };
17
+ flags: {
18
+ app?: string;
19
+ json?: boolean;
20
+ live?: boolean;
21
+ yes?: boolean;
22
+ };
23
+ }
24
+ export declare abstract class InspectResourceCommand extends SwellCommand {
25
+ /** Plural label for preamble + empty state, e.g. 'Webhooks'. */
26
+ protected abstract resourceLabel: string;
27
+ /** Singular label for error messages, e.g. 'webhook'. */
28
+ protected abstract resourceLabelSingular: string;
29
+ /** Admin path prefix, e.g. '/data/:webhooks'. */
30
+ protected abstract adminPath: string;
31
+ /** Subcommand name as used in CLI, e.g. 'webhooks'. */
32
+ protected abstract commandName: string;
33
+ /**
34
+ * Record field used to look up a bare-name identifier. Defaults to 'name';
35
+ * override for resources that identify records by a different key
36
+ * (e.g. webhooks use 'alias').
37
+ */
38
+ protected nameField: string;
39
+ /**
40
+ * Return the strict paste-back identifier for a record. Used as column 1
41
+ * content so every row is individually pasteable back into a detail query.
42
+ *
43
+ * Default implementation:
44
+ * - no `app_id` → `record.id` (store-level records have no app-slug form)
45
+ * - no name/alias → `record.id` (degenerate, no meaningful slug)
46
+ * - otherwise → `app.<slug>.<name>`, using the raw hex when the slug map
47
+ * has no entry (still pasteable via the hex appPart branch)
48
+ */
49
+ protected keyFor(record: any, appSlugById: Record<string, string>): string;
50
+ /**
51
+ * Optional compact meta rendered after the key. Only non-default state
52
+ * should surface — enabled rows with no failures return undefined so the
53
+ * listing stays to one key per line.
54
+ */
55
+ protected metaFor(_record: any, _appSlugById: Record<string, string>): string | undefined;
56
+ /**
57
+ * Post-process raw list results before rendering. Default is identity;
58
+ * override to drop noise that the API returns unconditionally (e.g.
59
+ * deprecated system records that have no server-side filter equivalent).
60
+ * Detail-mode lookups bypass this hook.
61
+ */
62
+ protected filterListResults<T>(results: T[]): T[];
63
+ /**
64
+ * Return the section grouping for a list record in global mode. Override
65
+ * to change labels (e.g. content treats no-app_id rows as `<custom>`).
66
+ */
67
+ protected groupForRecord(record: any, appSlugById: Record<string, string>): GroupInfo;
68
+ protected catch(error: Error): Promise<any>;
69
+ protected runInspect(parsed: InspectResourceCommandParsed): Promise<void>;
70
+ private showList;
71
+ protected showDetail(identifier: string, scope: InspectScope, flags: InspectResourceCommandParsed['flags']): Promise<void>;
72
+ /**
73
+ * Per-subclass detail-mode hints. Return runnable commands pointing to
74
+ * runtime/sibling addresses for the inspected record. Interpolated values
75
+ * must derive from the record's own fields, not constants — that keeps the
76
+ * hint self-consistent with column 1 and absorbs platform-side renames.
77
+ * Default is no hints.
78
+ */
79
+ protected hints(_record: any): string[];
80
+ /**
81
+ * Emit a fetched detail record. Prints JSON unconditionally; in non-`--json`
82
+ * mode follows with a `Next steps:` block of `hints(record)` lines.
83
+ *
84
+ * `--json` is the machine-readable contract — hints belong in the same
85
+ * stream as the human-facing output and are simply omitted there.
86
+ */
87
+ protected emitDetail(record: any, flags: InspectResourceCommandParsed['flags']): void;
88
+ private lookupByAppAndName;
89
+ private printPreamble;
90
+ protected throwNotFound(identifier: string): never;
91
+ }
@@ -0,0 +1,232 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { FetchError } from 'node-fetch';
3
+ import { getCurrentAppSlugId } from './lib/apps/index.js';
4
+ import { classifyIdentifier, resolveInspectScope, ScopeError, } from './lib/apps/inspect-scope.js';
5
+ import { HEX24 } from './lib/apps/object-id.js';
6
+ import { buildAppSlugMap, resolveAppId } from './lib/apps/resolve.js';
7
+ import { default as localConfig } from './lib/config.js';
8
+ import { renderKeyMetaTable, } from './lib/inspect/table.js';
9
+ import { SwellCommand } from './swell-command.js';
10
+ export const inspectResourceBaseArgs = {
11
+ identifier: Args.string({
12
+ description: 'Resource identifier — see "Identifier forms" below.',
13
+ required: false,
14
+ }),
15
+ };
16
+ export const inspectResourceBaseFlags = {
17
+ app: Flags.string({
18
+ description: 'Filter by app slug, or "." for current swell.json.',
19
+ }),
20
+ live: Flags.boolean({
21
+ description: 'Use live environment (default: test).',
22
+ default: false,
23
+ }),
24
+ json: Flags.boolean({
25
+ description: 'Emit pure JSON without the "Next steps" footer. Detail mode only.',
26
+ default: false,
27
+ }),
28
+ yes: Flags.boolean({
29
+ char: 'y',
30
+ description: 'No-op; accepted for agent compatibility.',
31
+ default: false,
32
+ hidden: true,
33
+ }),
34
+ };
35
+ export class InspectResourceCommand extends SwellCommand {
36
+ /**
37
+ * Record field used to look up a bare-name identifier. Defaults to 'name';
38
+ * override for resources that identify records by a different key
39
+ * (e.g. webhooks use 'alias').
40
+ */
41
+ nameField = 'name';
42
+ /**
43
+ * Return the strict paste-back identifier for a record. Used as column 1
44
+ * content so every row is individually pasteable back into a detail query.
45
+ *
46
+ * Default implementation:
47
+ * - no `app_id` → `record.id` (store-level records have no app-slug form)
48
+ * - no name/alias → `record.id` (degenerate, no meaningful slug)
49
+ * - otherwise → `app.<slug>.<name>`, using the raw hex when the slug map
50
+ * has no entry (still pasteable via the hex appPart branch)
51
+ */
52
+ keyFor(record, appSlugById) {
53
+ if (!record.app_id) {
54
+ return record.id ?? '-';
55
+ }
56
+ const name = record[this.nameField];
57
+ if (!name) {
58
+ return record.id ?? '-';
59
+ }
60
+ const slug = appSlugById[record.app_id] ?? record.app_id;
61
+ return `app.${slug}.${name}`;
62
+ }
63
+ /**
64
+ * Optional compact meta rendered after the key. Only non-default state
65
+ * should surface — enabled rows with no failures return undefined so the
66
+ * listing stays to one key per line.
67
+ */
68
+ metaFor(_record, _appSlugById) {
69
+ return undefined;
70
+ }
71
+ /**
72
+ * Post-process raw list results before rendering. Default is identity;
73
+ * override to drop noise that the API returns unconditionally (e.g.
74
+ * deprecated system records that have no server-side filter equivalent).
75
+ * Detail-mode lookups bypass this hook.
76
+ */
77
+ filterListResults(results) {
78
+ return results;
79
+ }
80
+ /**
81
+ * Return the section grouping for a list record in global mode. Override
82
+ * to change labels (e.g. content treats no-app_id rows as `<custom>`).
83
+ */
84
+ groupForRecord(record, appSlugById) {
85
+ if (!record.app_id) {
86
+ return { slug: '<store>', order: 0 };
87
+ }
88
+ const resolved = appSlugById[record.app_id];
89
+ if (!resolved) {
90
+ return { slug: '<not resolved>', order: 2 };
91
+ }
92
+ return { slug: resolved };
93
+ }
94
+ async catch(error) {
95
+ if (error instanceof FetchError) {
96
+ const message = `Could not connect to Swell API. Please try again later: ${error.message}`;
97
+ return this.error(message, { exit: 2, code: error.code });
98
+ }
99
+ if (error instanceof ScopeError) {
100
+ return this.error(error.message, { exit: 1 });
101
+ }
102
+ return this.error(error.message, { exit: 1 });
103
+ }
104
+ async runInspect(parsed) {
105
+ const { args, flags } = parsed;
106
+ if (flags.json && !args.identifier) {
107
+ this.error('--json is only valid in detail mode. Pass an identifier to inspect a single record, or drop --json for the list view.', { exit: 1 });
108
+ }
109
+ if (!flags.live) {
110
+ await this.api.setEnv('test');
111
+ }
112
+ const scope = await resolveInspectScope({
113
+ resolveAppId: (slug) => resolveAppId(this.api, slug),
114
+ readCurrentAppSlug: () => getCurrentAppSlugId(),
115
+ }, { app: flags.app });
116
+ if (args.identifier) {
117
+ await this.showDetail(args.identifier, scope, flags);
118
+ return;
119
+ }
120
+ await this.showList(scope, flags);
121
+ }
122
+ async showList(scope, flags) {
123
+ const { results } = await this.api.getAll({ adminPath: this.adminPath }, { query: scope.query });
124
+ const filtered = this.filterListResults(results ?? []);
125
+ const appSlugById = scope.appId
126
+ ? scope.appSlug
127
+ ? { [scope.appId]: scope.appSlug }
128
+ : {}
129
+ : await buildAppSlugMap(this.api);
130
+ this.printPreamble(flags.live ?? false);
131
+ if (filtered.length === 0) {
132
+ this.log();
133
+ this.log(` (no ${this.resourceLabel.toLowerCase()} found)`);
134
+ this.log();
135
+ return;
136
+ }
137
+ const rows = filtered.map((r) => ({
138
+ key: this.keyFor(r, appSlugById),
139
+ meta: this.metaFor(r, appSlugById),
140
+ group: scope.appId ? undefined : this.groupForRecord(r, appSlugById),
141
+ }));
142
+ this.log();
143
+ for (const line of renderKeyMetaTable(rows)) {
144
+ this.log(line);
145
+ }
146
+ this.log();
147
+ this.log(`Run "swell inspect ${this.commandName} <key>" to view a ${this.resourceLabelSingular}.`);
148
+ this.log();
149
+ }
150
+ async showDetail(identifier, scope, flags) {
151
+ const kind = classifyIdentifier(identifier);
152
+ let record = null;
153
+ switch (kind.kind) {
154
+ case 'id': {
155
+ record = await this.api.get({
156
+ adminPath: `${this.adminPath}/${kind.id}`,
157
+ });
158
+ break;
159
+ }
160
+ case 'slug': {
161
+ const isHex = HEX24.test(kind.appPart);
162
+ const appId = isHex
163
+ ? kind.appPart
164
+ : await resolveAppId(this.api, kind.appPart);
165
+ record = await this.lookupByAppAndName(appId, kind.name);
166
+ break;
167
+ }
168
+ case 'name': {
169
+ if (!scope.appId) {
170
+ this.error(`Bare ${this.resourceLabelSingular} name '${identifier}' requires --app=<slug> or --app=. to scope. ` +
171
+ `Alternatively, pass a full slug (app.<app>.<name>) or 24-char id.`, { exit: 1 });
172
+ }
173
+ record = await this.lookupByAppAndName(scope.appId, identifier);
174
+ break;
175
+ }
176
+ case 'invalid': {
177
+ this.error(`Invalid ${this.resourceLabelSingular} identifier '${identifier}'. ` +
178
+ `Expected bare name, full slug (app.<app>.<name>), or 24-char id.`, { exit: 1 });
179
+ }
180
+ }
181
+ if (!record) {
182
+ this.throwNotFound(identifier);
183
+ }
184
+ this.emitDetail(record, flags);
185
+ }
186
+ /**
187
+ * Per-subclass detail-mode hints. Return runnable commands pointing to
188
+ * runtime/sibling addresses for the inspected record. Interpolated values
189
+ * must derive from the record's own fields, not constants — that keeps the
190
+ * hint self-consistent with column 1 and absorbs platform-side renames.
191
+ * Default is no hints.
192
+ */
193
+ hints(_record) {
194
+ return [];
195
+ }
196
+ /**
197
+ * Emit a fetched detail record. Prints JSON unconditionally; in non-`--json`
198
+ * mode follows with a `Next steps:` block of `hints(record)` lines.
199
+ *
200
+ * `--json` is the machine-readable contract — hints belong in the same
201
+ * stream as the human-facing output and are simply omitted there.
202
+ */
203
+ emitDetail(record, flags) {
204
+ this.log(JSON.stringify(record, null, 2));
205
+ if (flags.json) {
206
+ return;
207
+ }
208
+ const lines = this.hints(record);
209
+ if (lines.length === 0) {
210
+ return;
211
+ }
212
+ this.log();
213
+ this.log('Next steps:');
214
+ for (const line of lines) {
215
+ this.log(` ${line}`);
216
+ }
217
+ this.log();
218
+ }
219
+ async lookupByAppAndName(appId, name) {
220
+ const response = await this.api.get({ adminPath: this.adminPath }, { query: { app_id: appId, [this.nameField]: name, limit: 1 } });
221
+ return response?.results?.[0] ?? null;
222
+ }
223
+ printPreamble(live) {
224
+ const store = localConfig.getDefaultStore();
225
+ const envLabel = live ? '[live]' : '[test]';
226
+ this.log(`${this.resourceLabel} in '${store}' ${envLabel}`);
227
+ }
228
+ throwNotFound(identifier) {
229
+ throw new Error(`No ${this.resourceLabelSingular} found for '${identifier}'.\n` +
230
+ `List available: swell inspect ${this.commandName}`);
231
+ }
232
+ }
@@ -119,8 +119,10 @@ export declare enum ConfigInputFields {
119
119
  VERSION = "version"
120
120
  }
121
121
  export interface FrontendProjectType {
122
+ appTypes?: string[];
122
123
  buildCommand?: string;
123
124
  devCommand: string;
125
+ displayOrder?: number;
124
126
  installCommand?: string;
125
127
  mainPackage: string;
126
128
  name: string;
@@ -130,7 +132,6 @@ export declare const CUSTOM_FRAMEWORK_SLUG = "custom";
130
132
  export declare const FrontendProjectTypes: FrontendProjectType[];
131
133
  export declare function getFrontendProjectSlugs(withNone?: boolean, withLegacy?: boolean): string[];
132
134
  export declare function getFrontendProjectValidValues(withNone?: boolean, withLegacy?: boolean): string;
133
- export declare function getAppSlugId(app: App): string | undefined;
134
135
  export declare function getFrontendProjectType(appPath: string): FrontendProjectType | undefined;
135
136
  /**
136
137
  * Get project commands transformed for the detected package manager.
@@ -2,7 +2,6 @@ import { createHash as createBlake3Hash } from 'blake3-wasm';
2
2
  import { pluralize, titleize } from 'inflection';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
- import { toAppId } from '../create/index.js';
6
5
  import { detectPackageManager, transformCommand } from '../package-manager.js';
7
6
  import { AppConfig } from './app-config.js';
8
7
  export { AppConfig, FunctionProcessingError, IgnoringFileError, } from './app-config.js';
@@ -116,11 +115,27 @@ export var ConfigInputFields;
116
115
  })(ConfigInputFields || (ConfigInputFields = {}));
117
116
  // Slug for unrecognized frontend frameworks (manual dev server mode)
118
117
  export const CUSTOM_FRAMEWORK_SLUG = 'custom';
119
- // Frontend project types - all use direct commands run from frontend/ directory
118
+ // Order matters: getFrontendProjectType is first-match-wins on mainPackage.
119
+ // react-storefront precedes hono (chassis ships hono as a peer); base react
120
+ // is last (any react-using framework above must be caught first).
121
+ // Pinned by test/lib/frontend-detection.test.ts.
120
122
  export const FrontendProjectTypes = [
123
+ {
124
+ // Template ref is `main`; pin to an immutable tag before announcement.
125
+ // `--no-agents` pre-answers C3's agents-helper prompt so install
126
+ // doesn't hang behind our spinner.
127
+ appTypes: ['storefront'],
128
+ devCommand: 'npm run dev -- --port ${PORT}',
129
+ displayOrder: 7,
130
+ installCommand: 'npm create cloudflare@latest -- frontend --template=swellstores/storefront-react-ai-template#main --deploy=false --git=false --no-agents',
131
+ mainPackage: '@swell/storefront-app-sdk-react',
132
+ name: 'Swell React Storefront',
133
+ slug: 'react-storefront',
134
+ },
121
135
  {
122
136
  buildCommand: 'npx astro build',
123
137
  devCommand: 'npx astro dev --port ${PORT}',
138
+ displayOrder: 2,
124
139
  installCommand: 'npm create cloudflare@latest -- frontend --framework=astro --deploy=false --git=false -- --no-git --yes --skip-houston --typescript strict',
125
140
  mainPackage: 'astro',
126
141
  name: 'Astro',
@@ -129,6 +144,7 @@ export const FrontendProjectTypes = [
129
144
  {
130
145
  buildCommand: 'npx ng build',
131
146
  devCommand: 'npx ng serve --port ${PORT}',
147
+ displayOrder: 6,
132
148
  installCommand: 'npm create cloudflare@latest -- frontend --framework=angular --deploy=false --git=false -- --style=sass --zoneless --ai-config=none',
133
149
  mainPackage: '@angular/core',
134
150
  name: 'Angular',
@@ -136,6 +152,7 @@ export const FrontendProjectTypes = [
136
152
  },
137
153
  {
138
154
  devCommand: 'npm run dev -- --port ${PORT}',
155
+ displayOrder: 5,
139
156
  installCommand: 'npm create cloudflare@latest -- frontend --framework=hono --deploy=false --git=false',
140
157
  mainPackage: 'hono',
141
158
  name: 'Hono',
@@ -144,6 +161,7 @@ export const FrontendProjectTypes = [
144
161
  {
145
162
  buildCommand: 'npx nuxt build',
146
163
  devCommand: 'npx nuxt dev --port ${PORT}',
164
+ displayOrder: 3,
147
165
  installCommand: 'npm create cloudflare@latest -- frontend --framework=nuxt --deploy=false --git=false -- --no-modules -f',
148
166
  mainPackage: 'nuxt',
149
167
  name: 'Nuxt',
@@ -152,17 +170,33 @@ export const FrontendProjectTypes = [
152
170
  {
153
171
  buildCommand: 'npx opennextjs-cloudflare build',
154
172
  devCommand: 'npx next dev --turbopack --port ${PORT}',
173
+ displayOrder: 1,
155
174
  installCommand: 'npm create cloudflare@latest -- frontend --framework=next --deploy=false --git=false -- --typescript --use-npm --src-dir --app --eslint --import-alias "@/*" --tailwind --turbopack',
156
175
  mainPackage: 'next',
157
176
  name: 'Next.js',
158
177
  slug: 'nextjs',
159
178
  },
179
+ {
180
+ // `--variant=react-ts` pre-answers C3's variant prompt (TS Vite +
181
+ // plugin-react, not SWC) so install doesn't hang behind our spinner.
182
+ devCommand: 'npm run dev -- --port ${PORT}',
183
+ displayOrder: 4,
184
+ installCommand: 'npm create cloudflare@latest -- frontend --framework=react --deploy=false --git=false --variant=react-ts',
185
+ mainPackage: 'react',
186
+ name: 'React',
187
+ slug: 'react',
188
+ },
160
189
  ];
161
190
  export function getFrontendProjectSlugs(withNone = true, withLegacy = true) {
162
191
  const types = withLegacy
163
192
  ? FrontendProjectTypes
164
193
  : FrontendProjectTypes.filter((projectType) => projectType.installCommand);
165
- const slugs = types.map((projectType) => projectType.slug);
194
+ // Sort so user-facing surfaces match the prompt; detection order is
195
+ // independent.
196
+ const slugs = [...types]
197
+ .sort((a, b) => (a.displayOrder ?? Number.POSITIVE_INFINITY) -
198
+ (b.displayOrder ?? Number.POSITIVE_INFINITY))
199
+ .map((projectType) => projectType.slug);
166
200
  if (withNone) {
167
201
  slugs.push('none');
168
202
  }
@@ -171,9 +205,6 @@ export function getFrontendProjectSlugs(withNone = true, withLegacy = true) {
171
205
  export function getFrontendProjectValidValues(withNone = true, withLegacy = true) {
172
206
  return getFrontendProjectSlugs(withNone, withLegacy).join(', ');
173
207
  }
174
- export function getAppSlugId(app) {
175
- return toAppId(app.private_id) || app.public_id || app.id;
176
- }
177
208
  export function getFrontendProjectType(appPath) {
178
209
  // Try frontend/package.json first (workspace structure), then root package.json (legacy)
179
210
  const pkgPaths = [
@@ -325,6 +356,12 @@ export function appAssetImage(appPath, fileName) {
325
356
  }
326
357
  export function appConfigFromFile(filePath, configType, appPath) {
327
358
  const { name } = path.parse(filePath);
359
+ // Notification basenames must not contain dots: the inspect identifier
360
+ // grammar (app.<slug>.<model>.<name>) splits on '.', so a dotted name
361
+ // would silently misparse on lookup.
362
+ if (configType === ConfigType.NOTIFICATION && name.includes('.')) {
363
+ throw new Error(`Notification file '${filePath}': basename cannot contain '.'. Rename the file.`);
364
+ }
328
365
  const config = AppConfig.create({
329
366
  name,
330
367
  filePath,
@@ -0,0 +1,58 @@
1
+ export interface InspectScope {
2
+ /** Undefined when scope is global (no app filter applied). */
3
+ appId?: string;
4
+ /** Present whenever appId is present; used for section headers and detail lookups. */
5
+ appSlug?: string;
6
+ /** Query fragment merged into list requests. Empty object when global. */
7
+ query: Record<string, any>;
8
+ }
9
+ export interface ResolveScopeContext {
10
+ resolveAppId: (appIdOrSlug: string) => Promise<string>;
11
+ readCurrentAppSlug: () => Promise<string | undefined>;
12
+ }
13
+ export declare class ScopeError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ /**
17
+ * Resolve the scope of an `inspect` subcommand.
18
+ *
19
+ * Grammar:
20
+ * (no --app) → global listing, no filter
21
+ * --app=<slug> → filter by the named app
22
+ * --app=. → filter by the app in the current directory's swell.json
23
+ *
24
+ * The `.` sentinel is the only place swell.json influences behavior; the
25
+ * default is always global so the same invocation produces the same output
26
+ * regardless of cwd.
27
+ */
28
+ export declare function resolveInspectScope(ctx: ResolveScopeContext, flags?: {
29
+ app?: string;
30
+ }): Promise<InspectScope>;
31
+ export type IdentifierKind = {
32
+ id: string;
33
+ kind: 'id';
34
+ } | {
35
+ appPart: string;
36
+ kind: 'slug';
37
+ name: string;
38
+ } | {
39
+ kind: 'name';
40
+ name: string;
41
+ } | {
42
+ input: string;
43
+ kind: 'invalid';
44
+ };
45
+ /**
46
+ * Classify an inspect-subcommand identifier argument.
47
+ *
48
+ * - `id`: 24-char hex (Mongo ObjectId)
49
+ * - `slug`: dotted form `app.<appPart>.<name>` where appPart can be either
50
+ * an ObjectId or a public/private app slug
51
+ * - `name`: bare identifier (letters, digits, hyphen, underscore) —
52
+ * requires app context to resolve
53
+ *
54
+ * Resources whose stored ids are non-hex dotted strings (notifications)
55
+ * handle dispatch in their own `showDetail` override; the base classifier
56
+ * stays narrow.
57
+ */
58
+ export declare function classifyIdentifier(identifier: string): IdentifierKind;