@swell/cli 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) 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 +163 -139
  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/index.js +5 -1
  30. package/dist/commands/create/model.d.ts +4 -0
  31. package/dist/commands/create/model.js +120 -25
  32. package/dist/commands/create/tests.d.ts +15 -0
  33. package/dist/commands/create/tests.js +79 -0
  34. package/dist/commands/inspect/content.d.ts +25 -0
  35. package/dist/commands/inspect/content.js +159 -0
  36. package/dist/commands/inspect/index.d.ts +7 -0
  37. package/dist/commands/inspect/index.js +16 -0
  38. package/dist/commands/inspect/models.d.ts +27 -0
  39. package/dist/commands/inspect/models.js +197 -0
  40. package/dist/commands/schema.d.ts +27 -0
  41. package/dist/commands/schema.js +231 -0
  42. package/dist/commands/theme/dev.d.ts +1 -1
  43. package/dist/commands/theme/dev.js +2 -2
  44. package/dist/commands/theme/init.d.ts +1 -1
  45. package/dist/commands/theme/init.js +1 -1
  46. package/dist/commands/theme/pull.d.ts +2 -2
  47. package/dist/commands/theme/pull.js +2 -2
  48. package/dist/commands/theme/push.d.ts +1 -1
  49. package/dist/commands/theme/push.js +1 -1
  50. package/dist/create-app-command.d.ts +12 -2
  51. package/dist/create-app-command.js +103 -80
  52. package/dist/create-collection-command.js +4 -1
  53. package/dist/create-config-command.js +4 -0
  54. package/dist/env/local.d.ts +1 -0
  55. package/dist/env/local.js +1 -0
  56. package/dist/env/production.d.ts +1 -0
  57. package/dist/env/production.js +1 -0
  58. package/dist/env/review.d.ts +1 -0
  59. package/dist/env/review.js +1 -0
  60. package/dist/env/staging.d.ts +1 -0
  61. package/dist/env/staging.js +1 -0
  62. package/dist/lib/api.d.ts +16 -5
  63. package/dist/lib/api.js +67 -25
  64. package/dist/lib/app-config.js +1 -0
  65. package/dist/lib/apps/index.d.ts +2 -0
  66. package/dist/lib/apps/index.js +20 -14
  67. package/dist/lib/config.d.ts +1 -1
  68. package/dist/lib/constants.d.ts +1 -0
  69. package/dist/lib/constants.js +1 -0
  70. package/dist/lib/create/content.d.ts +5 -1
  71. package/dist/lib/create/content.js +8 -1
  72. package/dist/lib/create/function.d.ts +2 -1
  73. package/dist/lib/create/function.js +7 -3
  74. package/dist/lib/create/model.d.ts +1 -0
  75. package/dist/lib/create/schemas.d.ts +6 -0
  76. package/dist/lib/create/schemas.js +6 -0
  77. package/dist/lib/create/tests/templates/env-dts.d.ts +1 -0
  78. package/dist/lib/create/tests/templates/env-dts.js +14 -0
  79. package/dist/lib/create/tests/templates/index.d.ts +8 -0
  80. package/dist/lib/create/tests/templates/index.js +8 -0
  81. package/dist/lib/create/tests/templates/integration-test.d.ts +1 -0
  82. package/dist/lib/create/tests/templates/integration-test.js +19 -0
  83. package/dist/lib/create/tests/templates/mock-request.d.ts +2 -0
  84. package/dist/lib/create/tests/templates/mock-request.js +112 -0
  85. package/dist/lib/create/tests/templates/setup-globals.d.ts +1 -0
  86. package/dist/lib/create/tests/templates/setup-globals.js +23 -0
  87. package/dist/lib/create/tests/templates/swell-client.d.ts +1 -0
  88. package/dist/lib/create/tests/templates/swell-client.js +126 -0
  89. package/dist/lib/create/tests/templates/tsconfig.d.ts +1 -0
  90. package/dist/lib/create/tests/templates/tsconfig.js +15 -0
  91. package/dist/lib/create/tests/templates/unit-test.d.ts +1 -0
  92. package/dist/lib/create/tests/templates/unit-test.js +42 -0
  93. package/dist/lib/create/tests/templates/vitest-config.d.ts +2 -0
  94. package/dist/lib/create/tests/templates/vitest-config.js +121 -0
  95. package/dist/lib/create/tests/types.d.ts +21 -0
  96. package/dist/lib/create/tests/types.js +1 -0
  97. package/dist/lib/create/tests.d.ts +4 -0
  98. package/dist/lib/create/tests.js +113 -0
  99. package/dist/lib/sessions.js +4 -3
  100. package/dist/lib/style.js +0 -1
  101. package/dist/lib/theme-sync.d.ts +6 -6
  102. package/dist/lib/theme-sync.js +49 -39
  103. package/dist/push-app-command.d.ts +7 -7
  104. package/dist/push-app-command.js +38 -36
  105. package/dist/remote-app-command.d.ts +1 -1
  106. package/dist/remote-app-command.js +4 -2
  107. package/dist/swell-api-command.d.ts +13 -0
  108. package/dist/swell-api-command.js +75 -0
  109. package/dist/swell-command.d.ts +1 -1
  110. package/dist/swell-command.js +9 -9
  111. package/oclif.manifest.json +639 -46
  112. package/package.json +8 -1
@@ -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 (/^[\da-f]{24}$/.test(identifier)) {
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
+ }
@@ -0,0 +1,231 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ // eslint-disable-next-line import/no-named-as-default
3
+ import Ajv2020 from 'ajv/dist/2020.js';
4
+ import ajvErrors from 'ajv-errors';
5
+ import addFormats from 'ajv-formats';
6
+ import * as fs from 'node:fs';
7
+ import parseJson from 'parse-json';
8
+ import { SCHEMAS_BASE_URL } from '../lib/constants.js';
9
+ import { SwellCommand } from '../swell-command.js';
10
+ const SCHEMA_DEFINITIONS = Object.freeze({
11
+ model: {
12
+ description: 'Data model definitions (/models/*.json)',
13
+ },
14
+ notification: {
15
+ description: 'Notification definitions (/notifications/*.json)',
16
+ },
17
+ content: {
18
+ description: 'Content views definitions (/content/*.json)',
19
+ },
20
+ setting: {
21
+ description: 'App settings definitions (/settings/*.json)',
22
+ },
23
+ webhook: {
24
+ description: 'Webhook definitions (/webhooks/*.json)',
25
+ },
26
+ });
27
+ export default class Schema extends SwellCommand {
28
+ static summary = 'View or validate Swell config JSON Schemas.';
29
+ static examples = [
30
+ {
31
+ description: 'List available schema types',
32
+ command: '<%= config.bin %> <%= command.id %>',
33
+ },
34
+ {
35
+ description: 'Output JSON Schema',
36
+ command: '<%= config.bin %> <%= command.id %> content --format=json-schema',
37
+ },
38
+ {
39
+ description: 'Output bundled JSON Schema (all $refs resolved)',
40
+ command: '<%= config.bin %> <%= command.id %> content --format=json-schema-bundle',
41
+ },
42
+ {
43
+ description: 'Output TypeScript declarations',
44
+ command: '<%= config.bin %> <%= command.id %> content --format=dts',
45
+ },
46
+ {
47
+ description: 'Validate a file',
48
+ command: '<%= config.bin %> <%= command.id %> content myfile.json',
49
+ },
50
+ {
51
+ description: 'Validate from stdin',
52
+ command: 'cat myfile.json | <%= config.bin %> <%= command.id %> content -',
53
+ },
54
+ ];
55
+ static args = {
56
+ type: Args.string({
57
+ required: false,
58
+ description: 'Schema type (e.g. model, content, setting, notification, webhook)',
59
+ }),
60
+ file: Args.string({
61
+ required: false,
62
+ description: 'Path to JSON file to validate (or use "-" for stdin)',
63
+ }),
64
+ };
65
+ static flags = {
66
+ format: Flags.string({
67
+ description: 'Output format: json-schema (with $ref pointers), json-schema-bundle (all $refs resolved), or dts (TypeScript declarations)',
68
+ options: ['json-schema', 'json-schema-bundle', 'dts'],
69
+ }),
70
+ yes: Flags.boolean({
71
+ char: 'y',
72
+ description: 'Skip informational output and print schema directly (equivalent to --format=json-schema).',
73
+ }),
74
+ };
75
+ async run() {
76
+ const { args, flags } = await this.parse(Schema);
77
+ const { type, file } = args;
78
+ const { format, yes } = flags;
79
+ // List available schema types
80
+ if (!type) {
81
+ this.listTypes();
82
+ return;
83
+ }
84
+ // Validate type
85
+ if (!(type in SCHEMA_DEFINITIONS)) {
86
+ throw new Error(`Unknown schema type '${type}'. Use 'swell schema' to list available types`);
87
+ }
88
+ // Validate file
89
+ if (file) {
90
+ if (format) {
91
+ throw new Error('--format cannot be used with file validation');
92
+ }
93
+ await this.validate(type, file);
94
+ return;
95
+ }
96
+ // Show schema info or output schema
97
+ if (!format && !yes) {
98
+ await this.showSchemaInfo(type);
99
+ return;
100
+ }
101
+ // Output schema in requested format
102
+ const outputFormat = format || 'json-schema';
103
+ await this.outputSchema(type, outputFormat);
104
+ }
105
+ catch(error) {
106
+ return this.error(error.message, { exit: 1 });
107
+ }
108
+ listTypes() {
109
+ this.log('Available config schemas:\n');
110
+ const entries = Object.entries(SCHEMA_DEFINITIONS);
111
+ const maxKeyLength = Math.max(...entries.map(([key]) => key.length));
112
+ for (const [schemaType, config] of entries) {
113
+ const padded = schemaType.padEnd(maxKeyLength + 4);
114
+ this.log(` ${padded}${config.description}`);
115
+ }
116
+ }
117
+ async showSchemaInfo(_type) {
118
+ this.log(`Use --format to output the schema, or provide a file to validate.`);
119
+ this.log(`Run "swell schema --help" for more information.`);
120
+ }
121
+ async outputSchema(type, format) {
122
+ let output;
123
+ switch (format) {
124
+ case 'json-schema': {
125
+ const schemaUrl = this.getSchemaUrl(type);
126
+ const response = await fetch(schemaUrl);
127
+ if (!response.ok) {
128
+ throw new Error(`Failed to fetch schema: ${response.statusText}`);
129
+ }
130
+ const schema = await response.json();
131
+ output = JSON.stringify(schema, null, 2);
132
+ break;
133
+ }
134
+ case 'json-schema-bundle': {
135
+ const bundledUrl = this.getBundledSchemaUrl(type);
136
+ const response = await fetch(bundledUrl);
137
+ if (!response.ok) {
138
+ throw new Error(`Failed to fetch bundled schema: ${response.statusText}`);
139
+ }
140
+ const schema = await response.json();
141
+ output = JSON.stringify(schema, null, 2);
142
+ break;
143
+ }
144
+ case 'dts': {
145
+ const dtsUrl = this.getTypeScriptDeclarationsUrl(type);
146
+ const response = await fetch(dtsUrl);
147
+ if (!response.ok) {
148
+ throw new Error(`Failed to fetch TypeScript declarations: ${response.statusText}`);
149
+ }
150
+ output = await response.text();
151
+ break;
152
+ }
153
+ default: {
154
+ throw new Error(`Unknown format: ${format}`);
155
+ }
156
+ }
157
+ this.log(output);
158
+ }
159
+ async validate(type, file) {
160
+ const isStdin = file === '-';
161
+ const input = isStdin
162
+ ? await this.readFromStdin()
163
+ : await this.getFileInput(file);
164
+ let json;
165
+ try {
166
+ json = parseJson(input);
167
+ }
168
+ catch (error) {
169
+ let errorTitle = 'Invalid JSON';
170
+ if (!isStdin) {
171
+ errorTitle += ` in '${file}'`;
172
+ }
173
+ throw new Error(`${errorTitle}: ${error.message}`);
174
+ }
175
+ const bundledUrl = this.getBundledSchemaUrl(type);
176
+ const response = await fetch(bundledUrl);
177
+ if (!response.ok) {
178
+ throw new Error(`Failed to fetch bundled schema: ${response.statusText}`);
179
+ }
180
+ const schema = await response.json();
181
+ const ajv = new Ajv2020({
182
+ allErrors: true,
183
+ strict: false,
184
+ });
185
+ addFormats(ajv);
186
+ ajvErrors(ajv);
187
+ const validate = ajv.compile(schema);
188
+ const valid = validate(json);
189
+ if (!valid) {
190
+ const { errors } = validate;
191
+ const errorLength = errors?.length;
192
+ let errorTitle = 'Invalid model definition';
193
+ if (typeof errorLength === 'number' && errorLength > 1) {
194
+ errorTitle += ` (${errorLength} errors)`;
195
+ }
196
+ let errorMessage = '';
197
+ for (const error of errors || []) {
198
+ const path = error.instancePath || '/';
199
+ errorMessage += `\n• ${path}: ${error.message} (schema: ${error.schemaPath})`;
200
+ }
201
+ throw new Error(`${errorTitle}\n${errorMessage}`);
202
+ }
203
+ this.log('Valid model definition');
204
+ }
205
+ getFileInput(file) {
206
+ if (!fs.existsSync(file)) {
207
+ throw new Error(`File not found: ${file}`);
208
+ }
209
+ return fs.promises.readFile(file, 'utf8');
210
+ }
211
+ readFromStdin() {
212
+ return new Promise((resolve, reject) => {
213
+ let data = '';
214
+ process.stdin.setEncoding('utf8');
215
+ process.stdin.on('data', (chunk) => {
216
+ data += chunk;
217
+ });
218
+ process.stdin.on('end', () => resolve(data));
219
+ process.stdin.on('error', reject);
220
+ });
221
+ }
222
+ getSchemaUrl(type) {
223
+ return `${SCHEMAS_BASE_URL}/${type}.json`;
224
+ }
225
+ getBundledSchemaUrl(type) {
226
+ return `${SCHEMAS_BASE_URL}/schema-bundle/${type}.bundled.json`;
227
+ }
228
+ getTypeScriptDeclarationsUrl(type) {
229
+ return `${SCHEMAS_BASE_URL}/types/${type}.d.ts`;
230
+ }
231
+ }
@@ -1,6 +1,5 @@
1
1
  import { PushAppCommand } from '../../push-app-command.js';
2
2
  export default class AppThemeDev extends PushAppCommand {
3
- appType: string;
4
3
  static examples: string[];
5
4
  static flags: {
6
5
  bundle: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
@@ -12,6 +11,7 @@ export default class AppThemeDev extends PushAppCommand {
12
11
  env: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
12
  };
14
13
  static summary: string;
14
+ appType: string;
15
15
  logAppLocalUrl(storeId: string, sessionId: string): void;
16
16
  run(): Promise<void>;
17
17
  private findAndInstallDependencies;