@swell/cli 2.1.0 → 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.
Files changed (87) hide show
  1. package/dist/commands/api/delete.d.ts +15 -0
  2. package/dist/commands/api/delete.js +26 -0
  3. package/dist/commands/api/get.d.ts +15 -0
  4. package/dist/commands/api/get.js +31 -0
  5. package/dist/commands/api/index.d.ts +7 -0
  6. package/dist/commands/api/index.js +16 -0
  7. package/dist/commands/api/post.d.ts +16 -0
  8. package/dist/commands/api/post.js +33 -0
  9. package/dist/commands/api/put.d.ts +16 -0
  10. package/dist/commands/api/put.js +33 -0
  11. package/dist/commands/app/dev.d.ts +9 -9
  12. package/dist/commands/app/dev.js +158 -136
  13. package/dist/commands/app/frontend/dev.d.ts +1 -1
  14. package/dist/commands/app/frontend/dev.js +84 -78
  15. package/dist/commands/app/install.js +3 -3
  16. package/dist/commands/app/pull.d.ts +1 -1
  17. package/dist/commands/app/pull.js +6 -6
  18. package/dist/commands/app/push.d.ts +3 -3
  19. package/dist/commands/app/push.js +8 -6
  20. package/dist/commands/app/release.d.ts +1 -1
  21. package/dist/commands/app/release.js +1 -1
  22. package/dist/commands/app/version.d.ts +1 -1
  23. package/dist/commands/app/version.js +5 -3
  24. package/dist/commands/create/app.d.ts +10 -2
  25. package/dist/commands/create/app.js +226 -57
  26. package/dist/commands/create/content.d.ts +4 -0
  27. package/dist/commands/create/content.js +83 -15
  28. package/dist/commands/create/function.js +27 -5
  29. package/dist/commands/create/model.d.ts +4 -0
  30. package/dist/commands/create/model.js +120 -25
  31. package/dist/commands/inspect/content.d.ts +25 -0
  32. package/dist/commands/inspect/content.js +159 -0
  33. package/dist/commands/inspect/index.d.ts +7 -0
  34. package/dist/commands/inspect/index.js +16 -0
  35. package/dist/commands/inspect/models.d.ts +27 -0
  36. package/dist/commands/inspect/models.js +197 -0
  37. package/dist/commands/schema.d.ts +27 -0
  38. package/dist/commands/schema.js +231 -0
  39. package/dist/commands/theme/dev.d.ts +1 -1
  40. package/dist/commands/theme/dev.js +2 -2
  41. package/dist/commands/theme/init.d.ts +1 -1
  42. package/dist/commands/theme/init.js +1 -1
  43. package/dist/commands/theme/pull.d.ts +2 -2
  44. package/dist/commands/theme/pull.js +2 -2
  45. package/dist/commands/theme/push.d.ts +1 -1
  46. package/dist/commands/theme/push.js +1 -1
  47. package/dist/create-app-command.d.ts +12 -2
  48. package/dist/create-app-command.js +103 -80
  49. package/dist/create-collection-command.js +4 -1
  50. package/dist/create-config-command.js +4 -0
  51. package/dist/env/local.d.ts +1 -0
  52. package/dist/env/local.js +1 -0
  53. package/dist/env/production.d.ts +1 -0
  54. package/dist/env/production.js +1 -0
  55. package/dist/env/review.d.ts +1 -0
  56. package/dist/env/review.js +1 -0
  57. package/dist/env/staging.d.ts +1 -0
  58. package/dist/env/staging.js +1 -0
  59. package/dist/lib/api.d.ts +16 -5
  60. package/dist/lib/api.js +67 -25
  61. package/dist/lib/app-config.js +1 -0
  62. package/dist/lib/apps/index.d.ts +2 -0
  63. package/dist/lib/apps/index.js +20 -15
  64. package/dist/lib/config.d.ts +1 -1
  65. package/dist/lib/constants.d.ts +1 -0
  66. package/dist/lib/constants.js +1 -0
  67. package/dist/lib/create/content.d.ts +5 -1
  68. package/dist/lib/create/content.js +8 -1
  69. package/dist/lib/create/function.d.ts +2 -1
  70. package/dist/lib/create/function.js +7 -3
  71. package/dist/lib/create/model.d.ts +1 -0
  72. package/dist/lib/create/schemas.d.ts +6 -0
  73. package/dist/lib/create/schemas.js +6 -0
  74. package/dist/lib/sessions.js +4 -3
  75. package/dist/lib/style.js +0 -1
  76. package/dist/lib/theme-sync.d.ts +6 -6
  77. package/dist/lib/theme-sync.js +49 -39
  78. package/dist/push-app-command.d.ts +7 -7
  79. package/dist/push-app-command.js +38 -36
  80. package/dist/remote-app-command.d.ts +1 -1
  81. package/dist/remote-app-command.js +4 -2
  82. package/dist/swell-api-command.d.ts +13 -0
  83. package/dist/swell-api-command.js +75 -0
  84. package/dist/swell-command.d.ts +1 -1
  85. package/dist/swell-command.js +9 -9
  86. package/package.json +8 -1
  87. package/oclif.manifest.json +0 -1936
@@ -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 description = `This command assists in generating data model configuration files. CLI will prompt for values when flags are not present.`;
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 visitors -e created,updated,deleted.',
16
- description: 'Specify events of the data model.',
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 first_name:string,last_name:string,photo:file,visit_count:int,visits:link',
20
- description: 'Specify fields of the data model.',
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 create model visitors -f first_name:string,last_name:string,photo:file,visit_count:int,visits:link -d "Visitors Tracking" -o -e created,updated,deleted',
24
- description: 'Scaffold a data model configuration with args and flags.',
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: 'list of data model events delimited by commas',
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: 'list of data model fields delimited by commas',
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: 'data model label, i.e. Products',
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: 'Overwrite existing data model configuration file.',
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 = 'Initialize a data model config file with properties in the models folder.';
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
- fileBody.fields = fields ? parseFields(fields.split(',')) : {};
68
- if (fileBody.events) {
69
- fileBody.events = events ? parseEvents(events.split(',')) : {};
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,7 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Inspect extends Command {
3
+ static summary: string;
4
+ static description: string;
5
+ static examples: string[];
6
+ run(): Promise<void>;
7
+ }
@@ -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
+ }