@swell/cli 2.0.20 → 2.2.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/api/delete.d.ts +15 -0
- package/dist/commands/api/delete.js +26 -0
- package/dist/commands/api/get.d.ts +15 -0
- package/dist/commands/api/get.js +31 -0
- package/dist/commands/api/index.d.ts +7 -0
- package/dist/commands/api/index.js +16 -0
- package/dist/commands/api/post.d.ts +16 -0
- package/dist/commands/api/post.js +33 -0
- package/dist/commands/api/put.d.ts +16 -0
- package/dist/commands/api/put.js +33 -0
- package/dist/commands/app/dev.d.ts +18 -4
- package/dist/commands/app/dev.js +383 -5
- package/dist/commands/app/frontend/dev.d.ts +6 -5
- package/dist/commands/app/frontend/dev.js +127 -58
- package/dist/commands/app/install.js +3 -3
- package/dist/commands/app/pull.d.ts +1 -1
- package/dist/commands/app/pull.js +6 -6
- package/dist/commands/app/push.d.ts +3 -3
- package/dist/commands/app/push.js +8 -6
- package/dist/commands/app/release.d.ts +1 -1
- package/dist/commands/app/release.js +1 -1
- package/dist/commands/app/version.d.ts +1 -1
- package/dist/commands/app/version.js +5 -3
- package/dist/commands/create/app.d.ts +10 -2
- package/dist/commands/create/app.js +226 -57
- package/dist/commands/create/content.d.ts +4 -0
- package/dist/commands/create/content.js +83 -15
- package/dist/commands/create/function.js +27 -5
- package/dist/commands/create/model.d.ts +4 -0
- package/dist/commands/create/model.js +120 -25
- package/dist/commands/inspect/content.d.ts +25 -0
- package/dist/commands/inspect/content.js +159 -0
- package/dist/commands/inspect/index.d.ts +7 -0
- package/dist/commands/inspect/index.js +16 -0
- package/dist/commands/inspect/models.d.ts +27 -0
- package/dist/commands/inspect/models.js +197 -0
- package/dist/commands/schema.d.ts +27 -0
- package/dist/commands/schema.js +231 -0
- package/dist/commands/theme/dev.d.ts +1 -1
- package/dist/commands/theme/dev.js +2 -2
- package/dist/commands/theme/init.d.ts +1 -1
- package/dist/commands/theme/init.js +1 -1
- package/dist/commands/theme/pull.d.ts +2 -2
- package/dist/commands/theme/pull.js +2 -2
- package/dist/commands/theme/push.d.ts +1 -1
- package/dist/commands/theme/push.js +1 -1
- package/dist/create-app-command.d.ts +14 -0
- package/dist/create-app-command.js +198 -15
- package/dist/create-collection-command.js +4 -1
- package/dist/create-config-command.js +4 -0
- package/dist/env/local.d.ts +2 -0
- package/dist/env/local.js +2 -0
- package/dist/env/production.d.ts +2 -0
- package/dist/env/production.js +2 -0
- package/dist/env/review.d.ts +2 -0
- package/dist/env/review.js +2 -0
- package/dist/env/staging.d.ts +2 -0
- package/dist/env/staging.js +2 -0
- package/dist/lib/api.d.ts +16 -5
- package/dist/lib/api.js +67 -25
- package/dist/lib/app-config.js +1 -0
- package/dist/lib/apps/app-config.d.ts +49 -0
- package/dist/lib/apps/app-config.js +21 -14
- package/dist/lib/apps/index.d.ts +5 -4
- package/dist/lib/apps/index.js +109 -21
- package/dist/lib/bundle.js +2 -1
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/constants.d.ts +2 -4
- package/dist/lib/constants.js +6 -4
- package/dist/lib/create/content.d.ts +5 -1
- package/dist/lib/create/content.js +8 -1
- package/dist/lib/create/function.d.ts +2 -1
- package/dist/lib/create/function.js +7 -3
- package/dist/lib/create/model.d.ts +1 -0
- package/dist/lib/create/schemas.d.ts +6 -0
- package/dist/lib/create/schemas.js +6 -0
- package/dist/lib/proxy.d.ts +1 -0
- package/dist/lib/proxy.js +29 -1
- package/dist/lib/sessions.js +4 -3
- package/dist/lib/style.js +0 -1
- package/dist/lib/swell-function-wrapper.d.ts +23 -4
- package/dist/lib/swell-function-wrapper.js +11 -4
- package/dist/lib/theme-sync.d.ts +6 -6
- package/dist/lib/theme-sync.js +49 -39
- package/dist/push-app-command.d.ts +12 -8
- package/dist/push-app-command.js +108 -29
- package/dist/remote-app-command.d.ts +1 -1
- package/dist/remote-app-command.js +4 -2
- package/dist/swell-api-command.d.ts +13 -0
- package/dist/swell-api-command.js +75 -0
- package/dist/swell-command.d.ts +1 -1
- package/dist/swell-command.js +9 -9
- package/package.json +10 -1
- package/oclif.manifest.json +0 -1895
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
import { checkbox } from '@inquirer/prompts';
|
|
2
|
-
import { Flags } from '@oclif/core';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
3
|
import { CreateCollectionCommand } from '../../create-collection-command.js';
|
|
4
4
|
import { ConfigType } from '../../lib/apps/index.js';
|
|
5
|
-
import { toFileName } from '../../lib/create/index.js';
|
|
5
|
+
import { toCollectionLabel, toCollectionName, toFileName, } from '../../lib/create/index.js';
|
|
6
6
|
import { parseEvents, parseFields, } from '../../lib/create/model.js';
|
|
7
|
+
import { SCHEMAS } from '../../lib/create/schemas.js';
|
|
7
8
|
export default class CreateModel extends CreateCollectionCommand {
|
|
8
|
-
static
|
|
9
|
+
static args = {
|
|
10
|
+
collection: Args.string({
|
|
11
|
+
default: '',
|
|
12
|
+
description: 'collection ID to create or apply to, e.g., products (required with -y)',
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
static description = `Interactive mode: choose to create a new collection or extend an existing standard collection. The CLI will propose a list of standard collections you can extend.
|
|
16
|
+
|
|
17
|
+
Non-interactive (-y): provide COLLECTION explicitly. To discover standard collections first, run: swell inspect models.`;
|
|
9
18
|
static examples = [
|
|
10
19
|
{
|
|
11
20
|
command: 'swell create model',
|
|
12
|
-
description: 'Create data model configuration interactively
|
|
21
|
+
description: 'Create data model configuration interactively',
|
|
13
22
|
},
|
|
14
23
|
{
|
|
15
|
-
command: 'swell create model
|
|
16
|
-
description: '
|
|
24
|
+
command: 'swell create model products -y',
|
|
25
|
+
description: 'Apply configuration to a standard collection (non-interactive)',
|
|
17
26
|
},
|
|
18
27
|
{
|
|
19
|
-
command: 'swell create model visitors -f
|
|
20
|
-
description: '
|
|
28
|
+
command: 'swell create model visitors -e created,updated -f title:string,photo:image -l "Visitors" -d "" -y',
|
|
29
|
+
description: 'Create custom collection with explicit fields (non-interactive)',
|
|
21
30
|
},
|
|
22
31
|
{
|
|
23
|
-
command: 'swell
|
|
24
|
-
description: '
|
|
32
|
+
command: 'swell inspect models',
|
|
33
|
+
description: 'List standard collections available for extending',
|
|
25
34
|
},
|
|
26
35
|
];
|
|
27
36
|
static flags = {
|
|
@@ -33,40 +42,129 @@ export default class CreateModel extends CreateCollectionCommand {
|
|
|
33
42
|
events: Flags.string({
|
|
34
43
|
char: 'e',
|
|
35
44
|
default: 'created,updated,deleted',
|
|
36
|
-
description: '
|
|
45
|
+
description: 'comma-separated events - valid: created, updated, deleted',
|
|
37
46
|
}),
|
|
38
47
|
fields: Flags.string({
|
|
39
48
|
char: 'f',
|
|
40
49
|
default: '',
|
|
41
|
-
description: '
|
|
50
|
+
description: 'comma-separated id:type pairs (e.g., title:string, price:currency). Types: collection, currency, image, int, link, string',
|
|
42
51
|
}),
|
|
43
52
|
label: Flags.string({
|
|
44
53
|
char: 'l',
|
|
45
54
|
default: '',
|
|
46
|
-
description: '
|
|
55
|
+
description: 'display label for the model (defaults to capitalized collection name)',
|
|
47
56
|
}),
|
|
48
57
|
overwrite: Flags.boolean({
|
|
49
58
|
default: false,
|
|
50
|
-
description: '
|
|
59
|
+
description: 'overwrite existing data model configuration file (use with -y to force overwrite)',
|
|
60
|
+
}),
|
|
61
|
+
yes: Flags.boolean({
|
|
62
|
+
char: 'y',
|
|
63
|
+
description: 'accept all default values; non-interactive, never prompts; errors if required values are missing',
|
|
51
64
|
}),
|
|
52
65
|
};
|
|
53
|
-
static summary = '
|
|
66
|
+
static summary = 'Create or extend a data model configuration in the models folder.';
|
|
54
67
|
createType = ConfigType.MODEL;
|
|
55
68
|
async run() {
|
|
56
|
-
const { flags } = await this.parse(CreateModel);
|
|
69
|
+
const { args, flags } = await this.parse(CreateModel);
|
|
70
|
+
const confirmYes = Boolean(flags.yes);
|
|
71
|
+
// Helper validators
|
|
72
|
+
const validEventValues = ['created', 'updated', 'deleted'];
|
|
73
|
+
const validFieldTypes = [
|
|
74
|
+
'collection',
|
|
75
|
+
'currency',
|
|
76
|
+
'image',
|
|
77
|
+
'int',
|
|
78
|
+
'link',
|
|
79
|
+
'string',
|
|
80
|
+
];
|
|
81
|
+
const validateEvents = (values) => {
|
|
82
|
+
const invalid = values.filter((v) => !validEventValues.includes(v));
|
|
83
|
+
if (invalid.length > 0) {
|
|
84
|
+
this.error(`Invalid event(s): ${invalid.join(', ')}\n\nValid events: ${validEventValues.join(', ')}\n\nExample: swell create model visitors -e created,updated -y`, { exit: 1 });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const validateFields = (pairs) => {
|
|
88
|
+
const invalid = [];
|
|
89
|
+
for (const pair of pairs) {
|
|
90
|
+
if (!pair.includes(':')) {
|
|
91
|
+
invalid.push(pair);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const type = pair.split(':')[1]?.trim();
|
|
95
|
+
if (!validFieldTypes.includes(type)) {
|
|
96
|
+
invalid.push(pair);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (invalid.length > 0) {
|
|
100
|
+
this.error(`Invalid field(s): ${invalid.join(', ')}\n\nValid types: ${validFieldTypes.join(', ')}\n\nExample: swell create model visitors -f title:string,price:currency -y`, { exit: 1 });
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
if (confirmYes) {
|
|
104
|
+
const argCollection = args.collection;
|
|
105
|
+
if (!argCollection) {
|
|
106
|
+
this.error('Missing required argument for non-interactive mode: COLLECTION\n\nExample: swell create model products -y\n\nTip: run `swell inspect models` to list standard collections you can extend.', { exit: 1 });
|
|
107
|
+
}
|
|
108
|
+
const collection = toCollectionName(argCollection);
|
|
109
|
+
const label = flags.label || toCollectionLabel(collection);
|
|
110
|
+
const description = flags.description || '';
|
|
111
|
+
const eventsRaw = (flags.events || 'created,updated,deleted');
|
|
112
|
+
const eventsList = eventsRaw
|
|
113
|
+
.split(',')
|
|
114
|
+
.map((v) => v.trim())
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
validateEvents(eventsList);
|
|
117
|
+
const fieldsRaw = (flags.fields || '');
|
|
118
|
+
const fieldPairs = fieldsRaw
|
|
119
|
+
? fieldsRaw
|
|
120
|
+
.split(',')
|
|
121
|
+
.map((v) => v.trim())
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
: [];
|
|
124
|
+
validateFields(fieldPairs);
|
|
125
|
+
const fileName = toFileName(collection);
|
|
126
|
+
const fileBody = {
|
|
127
|
+
$schema: SCHEMAS.MODEL,
|
|
128
|
+
collection,
|
|
129
|
+
description,
|
|
130
|
+
fields: parseFields(fieldPairs),
|
|
131
|
+
label,
|
|
132
|
+
events: parseEvents(eventsList),
|
|
133
|
+
};
|
|
134
|
+
await this.createFile({ fileBody, fileName }, flags.overwrite,
|
|
135
|
+
/* shouldConfirm */ false);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Interactive flow (prompts)
|
|
57
139
|
const { collection, description, label } = await this.promptCollection();
|
|
58
140
|
const fileName = toFileName(collection);
|
|
59
141
|
const fileBody = {
|
|
142
|
+
$schema: SCHEMAS.MODEL,
|
|
60
143
|
collection,
|
|
61
144
|
};
|
|
62
145
|
const { events, fields, overwrite } = flags;
|
|
63
|
-
if (label)
|
|
146
|
+
if (label)
|
|
64
147
|
fileBody.label = label;
|
|
65
|
-
}
|
|
66
148
|
fileBody.description = description;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
149
|
+
if (fields) {
|
|
150
|
+
const pairs = fields
|
|
151
|
+
.split(',')
|
|
152
|
+
.map((v) => v.trim())
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
validateFields(pairs);
|
|
155
|
+
fileBody.fields = parseFields(pairs);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
fileBody.fields = {};
|
|
159
|
+
}
|
|
160
|
+
// If events were provided via flag, use them; otherwise prompt
|
|
161
|
+
if (events && events !== 'created,updated,deleted') {
|
|
162
|
+
const eventsList = events
|
|
163
|
+
.split(',')
|
|
164
|
+
.map((v) => v.trim())
|
|
165
|
+
.filter(Boolean);
|
|
166
|
+
validateEvents(eventsList);
|
|
167
|
+
fileBody.events = parseEvents(eventsList);
|
|
70
168
|
}
|
|
71
169
|
else {
|
|
72
170
|
const eventsInput = await checkbox({
|
|
@@ -79,9 +177,6 @@ export default class CreateModel extends CreateCollectionCommand {
|
|
|
79
177
|
});
|
|
80
178
|
fileBody.events = parseEvents(eventsInput);
|
|
81
179
|
}
|
|
82
|
-
await this.createFile({
|
|
83
|
-
fileBody,
|
|
84
|
-
fileName,
|
|
85
|
-
}, overwrite);
|
|
180
|
+
await this.createFile({ fileBody, fileName }, overwrite);
|
|
86
181
|
}
|
|
87
182
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SwellCommand } from '../../swell-command.js';
|
|
2
|
+
export default class Content extends SwellCommand {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static args: {
|
|
6
|
+
'content-id': import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
|
+
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
static examples: string[];
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
protected catch(error: Error): Promise<any>;
|
|
15
|
+
private listContent;
|
|
16
|
+
private showContentViews;
|
|
17
|
+
private showContentView;
|
|
18
|
+
private resolveContentId;
|
|
19
|
+
private showContentDetail;
|
|
20
|
+
private reverseResolveContentId;
|
|
21
|
+
private getAppBySlug;
|
|
22
|
+
private getAppSlugId;
|
|
23
|
+
private throwContentNotFound;
|
|
24
|
+
private showContent;
|
|
25
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { FetchError } from 'node-fetch';
|
|
3
|
+
import { default as localConfig } from '../../lib/config.js';
|
|
4
|
+
import { SwellCommand } from '../../swell-command.js';
|
|
5
|
+
export default class Content extends SwellCommand {
|
|
6
|
+
static summary = 'Inspect content view extensions deployed in your store.';
|
|
7
|
+
static description = `
|
|
8
|
+
View content view extensions available in your Swell store.
|
|
9
|
+
|
|
10
|
+
Without a content ID, this command lists all available content views.
|
|
11
|
+
With a content ID, it retrieves the configuration for that specific content view in JSON format.
|
|
12
|
+
`;
|
|
13
|
+
static args = {
|
|
14
|
+
'content-id': Args.string({
|
|
15
|
+
description: 'Optional content view ID. Example: custom.admin.products, app.myapp.products',
|
|
16
|
+
required: false,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
static flags = {
|
|
20
|
+
live: Flags.boolean({
|
|
21
|
+
description: 'Use the live environment instead of test (default: test).',
|
|
22
|
+
default: false,
|
|
23
|
+
}),
|
|
24
|
+
yes: Flags.boolean({
|
|
25
|
+
char: 'y',
|
|
26
|
+
description: 'Accept all prompts automatically (for agent compatibility; no functional effect).',
|
|
27
|
+
default: false,
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
static examples = [
|
|
31
|
+
'swell inspect content',
|
|
32
|
+
'swell inspect content custom.admin.products',
|
|
33
|
+
'swell inspect content app.honest_reviews.reviews',
|
|
34
|
+
'swell inspect content app.myapp.products --live',
|
|
35
|
+
];
|
|
36
|
+
async run() {
|
|
37
|
+
const { args, flags } = await this.parse(Content);
|
|
38
|
+
const { 'content-id': contentId } = args;
|
|
39
|
+
const { live } = flags;
|
|
40
|
+
if (!live) {
|
|
41
|
+
await this.api.setEnv('test');
|
|
42
|
+
}
|
|
43
|
+
// Detail mode: show configuration for a specific content view
|
|
44
|
+
if (contentId) {
|
|
45
|
+
await this.showContentDetail(contentId);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const store = localConfig.getDefaultStore();
|
|
49
|
+
// List mode: show all content views
|
|
50
|
+
await this.listContent(store, live);
|
|
51
|
+
}
|
|
52
|
+
async catch(error) {
|
|
53
|
+
if (error instanceof FetchError) {
|
|
54
|
+
const message = `Could not connect to Swell API. Please try again later: ${error.message}`;
|
|
55
|
+
return this.error(message, { exit: 2, code: error.code });
|
|
56
|
+
}
|
|
57
|
+
return this.error(error.message, { exit: 1 });
|
|
58
|
+
}
|
|
59
|
+
async listContent(store, live) {
|
|
60
|
+
const { results: apps } = await this.api.get({
|
|
61
|
+
adminPath: '/apps',
|
|
62
|
+
});
|
|
63
|
+
const appsById = Object.fromEntries(apps.map((app) => [app.id, app]));
|
|
64
|
+
const { results: contentViews } = await this.api.getAll({
|
|
65
|
+
adminPath: '/data/:content',
|
|
66
|
+
});
|
|
67
|
+
// Sort by resolved ID in reverse order (custom.* first)
|
|
68
|
+
const sortedContentViews = [...contentViews].sort((a, b) => {
|
|
69
|
+
const resolvedA = this.resolveContentId(a, appsById);
|
|
70
|
+
const resolvedB = this.resolveContentId(b, appsById);
|
|
71
|
+
return resolvedB.localeCompare(resolvedA);
|
|
72
|
+
});
|
|
73
|
+
this.log(`Content views extensions available in '${store}' ${live ? '[live]' : '[test]'}:`);
|
|
74
|
+
this.log();
|
|
75
|
+
this.showContentViews(sortedContentViews, appsById);
|
|
76
|
+
// Show next step hints
|
|
77
|
+
this.log();
|
|
78
|
+
this.log('Examples:');
|
|
79
|
+
this.log(' swell inspect content custom.admin.products');
|
|
80
|
+
this.log(' swell inspect content app.honest_reviews.accounts');
|
|
81
|
+
}
|
|
82
|
+
showContentViews(contentViews, appsById) {
|
|
83
|
+
const maxNameWidth = Math.max(...contentViews.map((cv) => cv.name.length), 15);
|
|
84
|
+
for (const cv of contentViews) {
|
|
85
|
+
this.showContentView(cv, maxNameWidth, appsById);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
showContentView(contentView, nameWidth, appsById) {
|
|
89
|
+
const resolvedId = this.resolveContentId(contentView, appsById);
|
|
90
|
+
const paddedName = contentView.name.padEnd(nameWidth + 2);
|
|
91
|
+
this.log(`${paddedName}${resolvedId}`);
|
|
92
|
+
}
|
|
93
|
+
resolveContentId(contentView, appsById) {
|
|
94
|
+
const { id, name } = contentView;
|
|
95
|
+
// Parse ID: app.{appId}.{name} or custom.{source}.{name}
|
|
96
|
+
const match = id.match(/^(app|custom)\.([^.]+)\.(.+)$/);
|
|
97
|
+
if (!match)
|
|
98
|
+
return id; // fallback to original
|
|
99
|
+
const [, type, identifier] = match;
|
|
100
|
+
if (type === 'custom') {
|
|
101
|
+
return id; // custom IDs are already resolved
|
|
102
|
+
}
|
|
103
|
+
// type === 'app': resolve app ID to slug
|
|
104
|
+
const appId = identifier;
|
|
105
|
+
const app = appsById[appId];
|
|
106
|
+
if (!app) {
|
|
107
|
+
return id; // fallback if app not found
|
|
108
|
+
}
|
|
109
|
+
const appSlug = this.getAppSlugId(app);
|
|
110
|
+
return `app.${appSlug}.${name}`;
|
|
111
|
+
}
|
|
112
|
+
async showContentDetail(resolvedId) {
|
|
113
|
+
// Reverse resolve to get unresolved ID for API
|
|
114
|
+
const unresolvedId = await this.reverseResolveContentId(resolvedId);
|
|
115
|
+
// Fetch content view
|
|
116
|
+
const content = await this.api.get({
|
|
117
|
+
adminPath: `/data/:content/${unresolvedId}`,
|
|
118
|
+
});
|
|
119
|
+
if (!content) {
|
|
120
|
+
this.throwContentNotFound(resolvedId);
|
|
121
|
+
}
|
|
122
|
+
this.showContent(content);
|
|
123
|
+
}
|
|
124
|
+
async reverseResolveContentId(resolvedId) {
|
|
125
|
+
// Parse ID: app.{slug}.{name} or custom.{source}.{name}
|
|
126
|
+
const match = resolvedId.match(/^(app|custom)\.([^.]+)\.(.+)$/);
|
|
127
|
+
if (!match) {
|
|
128
|
+
throw new Error(`Invalid content ID format: ${resolvedId}`);
|
|
129
|
+
}
|
|
130
|
+
const [, type, identifier, name] = match;
|
|
131
|
+
if (type === 'custom') {
|
|
132
|
+
return resolvedId; // custom IDs don't need reverse resolution
|
|
133
|
+
}
|
|
134
|
+
// Check if identifier is already an app ID (ObjectID format)
|
|
135
|
+
if (identifier.match(/^[0-9a-f]{24}$/)) {
|
|
136
|
+
return resolvedId; // already unresolved format
|
|
137
|
+
}
|
|
138
|
+
// type === 'app': resolve slug to app ID
|
|
139
|
+
const appSlug = identifier;
|
|
140
|
+
const app = await this.getAppBySlug(appSlug);
|
|
141
|
+
return `app.${app.id}.${name}`;
|
|
142
|
+
}
|
|
143
|
+
async getAppBySlug(slug) {
|
|
144
|
+
const app = await this.api.get({ adminPath: `/apps/${slug}` });
|
|
145
|
+
if (!app) {
|
|
146
|
+
throw new Error(`App '${slug}' not found.\nList available content: swell inspect content`);
|
|
147
|
+
}
|
|
148
|
+
return app;
|
|
149
|
+
}
|
|
150
|
+
getAppSlugId(app) {
|
|
151
|
+
return app.public_id || app.private_id.replace(/^_/, '');
|
|
152
|
+
}
|
|
153
|
+
throwContentNotFound(id) {
|
|
154
|
+
throw new Error(`No content view found for '${id}'.\nList available content: swell inspect content`);
|
|
155
|
+
}
|
|
156
|
+
showContent(content) {
|
|
157
|
+
this.log(JSON.stringify(content, null, 2));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Inspect extends Command {
|
|
3
|
+
static summary = 'Inspect deployed artifacts in your store.';
|
|
4
|
+
static description = `View configuration and data for deployed resources.
|
|
5
|
+
|
|
6
|
+
Inspect models, content views, and other deployed artifacts in your Swell store.`;
|
|
7
|
+
static examples = [
|
|
8
|
+
'swell inspect models',
|
|
9
|
+
'swell inspect models /products',
|
|
10
|
+
'swell inspect models /apps/myapp/orders',
|
|
11
|
+
];
|
|
12
|
+
async run() {
|
|
13
|
+
this.log('Use "swell inspect models" to inspect deployed artifacts.');
|
|
14
|
+
this.log('Run "swell inspect --help" for more information.');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SwellCommand } from '../../swell-command.js';
|
|
2
|
+
export default class Models extends SwellCommand {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static args: {
|
|
6
|
+
'collection-path': import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
|
+
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
static examples: string[];
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
protected catch(error: Error): Promise<any>;
|
|
15
|
+
private listModels;
|
|
16
|
+
private showStandardModels;
|
|
17
|
+
private showAppModels;
|
|
18
|
+
private showModels;
|
|
19
|
+
private showCollectionPath;
|
|
20
|
+
private getCurrentAppSlugId;
|
|
21
|
+
private getAppSlugId;
|
|
22
|
+
private showModelDetail;
|
|
23
|
+
private resolveModelPath;
|
|
24
|
+
private resolveAppModelPath;
|
|
25
|
+
private throwModelNotFound;
|
|
26
|
+
private showModel;
|
|
27
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { FetchError } from 'node-fetch';
|
|
4
|
+
import { default as localConfig } from '../../lib/config.js';
|
|
5
|
+
import { SwellCommand } from '../../swell-command.js';
|
|
6
|
+
export default class Models extends SwellCommand {
|
|
7
|
+
static summary = 'Inspect models for collections deployed in your store.';
|
|
8
|
+
static description = `
|
|
9
|
+
View models for collections available in your Swell store.
|
|
10
|
+
|
|
11
|
+
Without a collection path, this command lists all available collections grouped by type.
|
|
12
|
+
With a collection path, it retrieves the model for that specific collection in JSON format.
|
|
13
|
+
`;
|
|
14
|
+
static args = {
|
|
15
|
+
'collection-path': Args.string({
|
|
16
|
+
description: "Optional collection path (must start with '/'). Example: /products, /apps/myapp/orders",
|
|
17
|
+
required: false,
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
static flags = {
|
|
21
|
+
live: Flags.boolean({
|
|
22
|
+
description: 'Use the live environment instead of test (default: test).',
|
|
23
|
+
default: false,
|
|
24
|
+
}),
|
|
25
|
+
yes: Flags.boolean({
|
|
26
|
+
char: 'y',
|
|
27
|
+
description: 'Accept all prompts automatically (for agent compatibility; no functional effect).',
|
|
28
|
+
default: false,
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
static examples = [
|
|
32
|
+
'swell inspect models',
|
|
33
|
+
'swell inspect models /products',
|
|
34
|
+
'swell inspect models /apps/myapp/orders',
|
|
35
|
+
'swell inspect models /orders --live',
|
|
36
|
+
];
|
|
37
|
+
async run() {
|
|
38
|
+
const { args, flags } = await this.parse(Models);
|
|
39
|
+
const { 'collection-path': path } = args;
|
|
40
|
+
const { live } = flags;
|
|
41
|
+
if (!live) {
|
|
42
|
+
await this.api.setEnv('test');
|
|
43
|
+
}
|
|
44
|
+
// Detail mode: show schema for a specific model
|
|
45
|
+
if (path) {
|
|
46
|
+
if (!path.startsWith('/')) {
|
|
47
|
+
throw new Error(`Collection path must start with '/'. Did you mean '/${path}'?`);
|
|
48
|
+
}
|
|
49
|
+
await this.showModelDetail(path);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const store = localConfig.getDefaultStore();
|
|
53
|
+
// List mode: show all models
|
|
54
|
+
await this.listModels(store, live);
|
|
55
|
+
}
|
|
56
|
+
async catch(error) {
|
|
57
|
+
if (error instanceof FetchError) {
|
|
58
|
+
const message = `Could not connect to Swell API. Please try again later: ${error.message}`;
|
|
59
|
+
return this.error(message, { exit: 2, code: error.code });
|
|
60
|
+
}
|
|
61
|
+
return this.error(error.message, { exit: 1 });
|
|
62
|
+
}
|
|
63
|
+
async listModels(store, live) {
|
|
64
|
+
const { results: apps } = await this.api.get({
|
|
65
|
+
adminPath: '/apps',
|
|
66
|
+
});
|
|
67
|
+
const appsById = Object.fromEntries(apps.map((app) => [app.id, app]));
|
|
68
|
+
const { results: models } = await this.api.getAll({
|
|
69
|
+
adminPath: '/data/:models',
|
|
70
|
+
}, {
|
|
71
|
+
query: {
|
|
72
|
+
content_id: { $exists: false },
|
|
73
|
+
deprecated: { $ne: true },
|
|
74
|
+
development: { $ne: true },
|
|
75
|
+
abstract: { $ne: true },
|
|
76
|
+
reserved: { $ne: true },
|
|
77
|
+
$or: [
|
|
78
|
+
{ app_id: { $exists: false } },
|
|
79
|
+
{ app_id: { $in: Object.keys(appsById) } },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const standardModels = [];
|
|
84
|
+
const modelsByAppId = {};
|
|
85
|
+
for (const model of models) {
|
|
86
|
+
if (model.app_id) {
|
|
87
|
+
modelsByAppId[model.app_id] ||= [];
|
|
88
|
+
modelsByAppId[model.app_id].push(model);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
standardModels.push(model);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
this.log(`Collections available in '${store}' ${live ? '[live]' : '[test]'}:`);
|
|
95
|
+
this.log();
|
|
96
|
+
this.showStandardModels(standardModels);
|
|
97
|
+
await this.showAppModels(appsById, modelsByAppId);
|
|
98
|
+
// Show next step hint
|
|
99
|
+
this.log();
|
|
100
|
+
this.log('View collection model');
|
|
101
|
+
this.log(' swell inspect models <collection-path>');
|
|
102
|
+
this.log();
|
|
103
|
+
this.log('Examples:');
|
|
104
|
+
this.log(' swell inspect models /products');
|
|
105
|
+
this.log(' swell inspect models /apps/myapp/orders');
|
|
106
|
+
}
|
|
107
|
+
showStandardModels(models) {
|
|
108
|
+
this.showModels(models, 'Standard');
|
|
109
|
+
}
|
|
110
|
+
async showAppModels(appsById, modelsByAppId) {
|
|
111
|
+
const currentAppSlugId = await this.getCurrentAppSlugId();
|
|
112
|
+
for (const [appId, app] of Object.entries(appsById)) {
|
|
113
|
+
const models = modelsByAppId[appId];
|
|
114
|
+
if (models) {
|
|
115
|
+
const appSlugId = this.getAppSlugId(app);
|
|
116
|
+
const pathPrefix = `/apps/${appSlugId}`;
|
|
117
|
+
const label = `${app.name} App ${currentAppSlugId === appSlugId ? '(current app)' : ''}`;
|
|
118
|
+
this.log();
|
|
119
|
+
this.showModels(models, label, { pathPrefix });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
showModels(models, label, options) {
|
|
124
|
+
this.log(label);
|
|
125
|
+
this.log();
|
|
126
|
+
const sortedModels = [...models].sort((a, b) => a.name.localeCompare(b.name));
|
|
127
|
+
for (const model of sortedModels) {
|
|
128
|
+
this.showCollectionPath(model.name, 2, options);
|
|
129
|
+
const sortedSubModels = Object.values(model.fields)
|
|
130
|
+
.filter((field) => field.type === 'collection')
|
|
131
|
+
.map((field) => field.name)
|
|
132
|
+
.sort((a, b) => a.localeCompare(b));
|
|
133
|
+
for (const sub of sortedSubModels) {
|
|
134
|
+
this.showCollectionPath(sub, 4, options);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
showCollectionPath(collection, indent = 0, options) {
|
|
139
|
+
this.log(`${' '.repeat(indent)}${options?.pathPrefix || ''}/${collection}`);
|
|
140
|
+
}
|
|
141
|
+
async getCurrentAppSlugId() {
|
|
142
|
+
const hasAppContext = fs.existsSync('swell.json');
|
|
143
|
+
if (!hasAppContext) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const appConfig = JSON.parse(await fs.promises.readFile('swell.json', 'utf8'));
|
|
148
|
+
return appConfig.id;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// noop
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
getAppSlugId(app) {
|
|
155
|
+
return app.public_id || app.private_id.replace(/^_/, '');
|
|
156
|
+
}
|
|
157
|
+
async showModelDetail(path) {
|
|
158
|
+
const [modelPath, subModelKey] = await this.resolveModelPath(path);
|
|
159
|
+
const model = await this.api.get({
|
|
160
|
+
adminPath: `/data/:models${modelPath}`,
|
|
161
|
+
}, { query: { $app: true } });
|
|
162
|
+
if (!model) {
|
|
163
|
+
this.throwModelNotFound(path);
|
|
164
|
+
}
|
|
165
|
+
if (!subModelKey) {
|
|
166
|
+
return this.showModel(model);
|
|
167
|
+
}
|
|
168
|
+
const subModel = model.fields[subModelKey];
|
|
169
|
+
if (!subModel) {
|
|
170
|
+
this.throwModelNotFound(path);
|
|
171
|
+
}
|
|
172
|
+
this.showModel(subModel);
|
|
173
|
+
}
|
|
174
|
+
async resolveModelPath(path) {
|
|
175
|
+
if (path.startsWith('/apps/')) {
|
|
176
|
+
return this.resolveAppModelPath(path);
|
|
177
|
+
}
|
|
178
|
+
const [modelPath, subModelKey] = path.split(':');
|
|
179
|
+
return [modelPath, subModelKey];
|
|
180
|
+
}
|
|
181
|
+
async resolveAppModelPath(path) {
|
|
182
|
+
const parts = path.split('/');
|
|
183
|
+
const appSlugId = parts[2];
|
|
184
|
+
const model = parts[3];
|
|
185
|
+
const app = await this.api.get({ adminPath: `/apps/${appSlugId}` });
|
|
186
|
+
if (!app) {
|
|
187
|
+
this.throwModelNotFound(path);
|
|
188
|
+
}
|
|
189
|
+
return this.resolveModelPath(`/app_${app.id}.${model}`);
|
|
190
|
+
}
|
|
191
|
+
throwModelNotFound(path) {
|
|
192
|
+
throw new Error(`No model found for collection '${path}'.\nList available collections: swell inspect models`);
|
|
193
|
+
}
|
|
194
|
+
showModel(model) {
|
|
195
|
+
this.log(JSON.stringify(model, null, 2));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SwellCommand } from '../swell-command.js';
|
|
2
|
+
export default class Schema extends SwellCommand {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
description: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}[];
|
|
8
|
+
static args: {
|
|
9
|
+
type: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
10
|
+
file: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
11
|
+
};
|
|
12
|
+
static flags: {
|
|
13
|
+
format: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
14
|
+
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
protected catch(error: Error): Promise<any>;
|
|
18
|
+
private listTypes;
|
|
19
|
+
private showSchemaInfo;
|
|
20
|
+
private outputSchema;
|
|
21
|
+
private validate;
|
|
22
|
+
private getFileInput;
|
|
23
|
+
private readFromStdin;
|
|
24
|
+
private getSchemaUrl;
|
|
25
|
+
private getBundledSchemaUrl;
|
|
26
|
+
private getTypeScriptDeclarationsUrl;
|
|
27
|
+
}
|