@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.
- package/dist/commands/app/frontend/dev.js +18 -0
- package/dist/commands/inspect/content.d.ts +25 -14
- package/dist/commands/inspect/content.js +34 -144
- package/dist/commands/inspect/functions.d.ts +48 -0
- package/dist/commands/inspect/functions.js +83 -0
- package/dist/commands/inspect/index.js +8 -7
- package/dist/commands/inspect/models.d.ts +17 -2
- package/dist/commands/inspect/models.js +114 -47
- package/dist/commands/inspect/notifications.d.ts +31 -0
- package/dist/commands/inspect/notifications.js +125 -0
- package/dist/commands/inspect/settings.d.ts +28 -0
- package/dist/commands/inspect/settings.js +82 -0
- package/dist/commands/inspect/webhooks.d.ts +34 -0
- package/dist/commands/inspect/webhooks.js +61 -0
- package/dist/create-app-command.d.ts +7 -0
- package/dist/create-app-command.js +66 -3
- package/dist/inspect-resource-command.d.ts +91 -0
- package/dist/inspect-resource-command.js +232 -0
- package/dist/lib/apps/index.d.ts +2 -1
- package/dist/lib/apps/index.js +43 -6
- package/dist/lib/apps/inspect-scope.d.ts +58 -0
- package/dist/lib/apps/inspect-scope.js +60 -0
- package/dist/lib/apps/object-id.d.ts +7 -0
- package/dist/lib/apps/object-id.js +9 -0
- package/dist/lib/apps/paths.js +8 -1
- package/dist/lib/apps/resolve.d.ts +16 -0
- package/dist/lib/apps/resolve.js +39 -0
- package/dist/lib/apps/slug.d.ts +29 -0
- package/dist/lib/apps/slug.js +12 -0
- package/dist/lib/inspect/content.d.ts +39 -0
- package/dist/lib/inspect/content.js +76 -0
- package/dist/lib/inspect/notifications.d.ts +115 -0
- package/dist/lib/inspect/notifications.js +173 -0
- package/dist/lib/inspect/settings.d.ts +61 -0
- package/dist/lib/inspect/settings.js +56 -0
- package/dist/lib/inspect/table.d.ts +29 -0
- package/dist/lib/inspect/table.js +61 -0
- package/dist/push-app-command.js +3 -2
- package/dist/swell-api-command.d.ts +0 -4
- package/dist/swell-api-command.js +2 -18
- package/oclif.manifest.json +328 -35
- 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:
|
|
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.
|
|
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
|
+
}
|
package/dist/lib/apps/index.d.ts
CHANGED
|
@@ -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.
|
package/dist/lib/apps/index.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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;
|