@swell/cli 2.2.0 → 2.3.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 (79) hide show
  1. package/dist/app-command.js +1 -1
  2. package/dist/commands/api/delete.js +4 -1
  3. package/dist/commands/api/get.js +1 -0
  4. package/dist/commands/api/index.js +4 -1
  5. package/dist/commands/api/post.js +1 -0
  6. package/dist/commands/api/put.js +1 -0
  7. package/dist/commands/app/dev.js +5 -3
  8. package/dist/commands/app/init.d.ts +8 -7
  9. package/dist/commands/app/init.js +36 -18
  10. package/dist/commands/app/pull.js +2 -2
  11. package/dist/commands/app/push.js +2 -2
  12. package/dist/commands/app/version.d.ts +1 -0
  13. package/dist/commands/app/version.js +14 -4
  14. package/dist/commands/create/app.d.ts +8 -4
  15. package/dist/commands/create/app.js +68 -51
  16. package/dist/commands/create/content.d.ts +5 -6
  17. package/dist/commands/create/content.js +22 -38
  18. package/dist/commands/create/function.d.ts +6 -7
  19. package/dist/commands/create/function.js +94 -31
  20. package/dist/commands/create/index.js +13 -1
  21. package/dist/commands/create/model.d.ts +5 -6
  22. package/dist/commands/create/model.js +21 -33
  23. package/dist/commands/create/notification.d.ts +9 -9
  24. package/dist/commands/create/notification.js +117 -95
  25. package/dist/commands/create/setting.d.ts +21 -0
  26. package/dist/commands/create/setting.js +120 -0
  27. package/dist/commands/create/tests.d.ts +14 -0
  28. package/dist/commands/create/tests.js +76 -0
  29. package/dist/commands/create/webhook.d.ts +22 -0
  30. package/dist/commands/create/webhook.js +176 -0
  31. package/dist/commands/inspect/content.js +1 -1
  32. package/dist/commands/schema.d.ts +1 -0
  33. package/dist/commands/schema.js +51 -5
  34. package/dist/commands/theme/init.d.ts +8 -4
  35. package/dist/commands/theme/init.js +21 -8
  36. package/dist/create-app-command.d.ts +1 -0
  37. package/dist/create-app-command.js +15 -10
  38. package/dist/create-config-command.js +2 -2
  39. package/dist/help/custom-help.d.ts +89 -0
  40. package/dist/help/custom-help.js +337 -0
  41. package/dist/help/types.d.ts +75 -0
  42. package/dist/help/types.js +1 -0
  43. package/dist/lib/apps/app-config.js +2 -2
  44. package/dist/lib/apps/index.d.ts +2 -1
  45. package/dist/lib/apps/index.js +21 -4
  46. package/dist/lib/apps/paths.js +7 -6
  47. package/dist/lib/create/notification.d.ts +1 -0
  48. package/dist/lib/create/schemas.d.ts +1 -0
  49. package/dist/lib/create/schemas.js +1 -0
  50. package/dist/lib/create/setting.d.ts +15 -0
  51. package/dist/lib/create/setting.js +27 -0
  52. package/dist/lib/create/tests/templates/env-dts.d.ts +1 -0
  53. package/dist/lib/create/tests/templates/env-dts.js +15 -0
  54. package/dist/lib/create/tests/templates/index.d.ts +8 -0
  55. package/dist/lib/create/tests/templates/index.js +8 -0
  56. package/dist/lib/create/tests/templates/integration-test.d.ts +1 -0
  57. package/dist/lib/create/tests/templates/integration-test.js +19 -0
  58. package/dist/lib/create/tests/templates/mock-request.d.ts +2 -0
  59. package/dist/lib/create/tests/templates/mock-request.js +112 -0
  60. package/dist/lib/create/tests/templates/setup-globals.d.ts +1 -0
  61. package/dist/lib/create/tests/templates/setup-globals.js +23 -0
  62. package/dist/lib/create/tests/templates/swell-client.d.ts +1 -0
  63. package/dist/lib/create/tests/templates/swell-client.js +128 -0
  64. package/dist/lib/create/tests/templates/tsconfig.d.ts +1 -0
  65. package/dist/lib/create/tests/templates/tsconfig.js +17 -0
  66. package/dist/lib/create/tests/templates/unit-test.d.ts +1 -0
  67. package/dist/lib/create/tests/templates/unit-test.js +42 -0
  68. package/dist/lib/create/tests/templates/vitest-config.d.ts +2 -0
  69. package/dist/lib/create/tests/templates/vitest-config.js +122 -0
  70. package/dist/lib/create/tests/types.d.ts +21 -0
  71. package/dist/lib/create/tests/types.js +1 -0
  72. package/dist/lib/create/tests.d.ts +4 -0
  73. package/dist/lib/create/tests.js +113 -0
  74. package/dist/lib/create/webhook.d.ts +32 -0
  75. package/dist/lib/create/webhook.js +52 -0
  76. package/dist/swell-api-command.d.ts +14 -0
  77. package/dist/swell-api-command.js +113 -7
  78. package/oclif.manifest.json +2879 -0
  79. package/package.json +2 -1
@@ -0,0 +1,21 @@
1
+ import { CreateConfigCommand } from '../../create-config-command.js';
2
+ import { HelpMeta } from '../../help/types.js';
3
+ import { ConfigType } from '../../lib/apps/index.js';
4
+ export default class CreateSetting extends CreateConfigCommand {
5
+ static args: {
6
+ name: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static description: string;
9
+ static examples: string[];
10
+ static helpMeta: HelpMeta;
11
+ static flags: {
12
+ label: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
+ description: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
14
+ fields: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
15
+ overwrite: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
16
+ yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
17
+ };
18
+ static summary: string;
19
+ createType: ConfigType;
20
+ run(): Promise<void>;
21
+ }
@@ -0,0 +1,120 @@
1
+ import { input } from '@inquirer/prompts';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { CreateConfigCommand } from '../../create-config-command.js';
4
+ import { ConfigType } from '../../lib/apps/index.js';
5
+ import { toFileName } from '../../lib/create/index.js';
6
+ import { SCHEMAS } from '../../lib/create/schemas.js';
7
+ import { parseFields, toSettingLabel, } from '../../lib/create/setting.js';
8
+ export default class CreateSetting extends CreateConfigCommand {
9
+ static args = {
10
+ name: Args.string({
11
+ default: '',
12
+ description: 'Setting name (e.g., api-config)',
13
+ }),
14
+ };
15
+ static description = 'Create a setting configuration in the settings folder.';
16
+ static examples = [
17
+ '$ swell create setting',
18
+ '$ swell create setting api-config -y',
19
+ '$ swell create setting api-config -f api_key:short_text,enabled:boolean -l "API Config" -y',
20
+ ];
21
+ static helpMeta = {
22
+ usageDirect: '<name> [...] -y',
23
+ };
24
+ static flags = {
25
+ label: Flags.string({
26
+ char: 'l',
27
+ default: '',
28
+ description: 'Display label (default: titleized name)',
29
+ }),
30
+ description: Flags.string({
31
+ char: 'd',
32
+ default: '',
33
+ description: 'Description',
34
+ }),
35
+ fields: Flags.string({
36
+ char: 'f',
37
+ default: '',
38
+ description: 'Fields as id:type pairs (e.g., api_key:short_text,enabled:boolean)',
39
+ }),
40
+ overwrite: Flags.boolean({
41
+ default: false,
42
+ description: 'Overwrite existing file',
43
+ }),
44
+ yes: Flags.boolean({
45
+ char: 'y',
46
+ description: 'Skip prompts, require all arguments',
47
+ }),
48
+ };
49
+ static summary = 'Create a setting configuration in the settings folder.';
50
+ createType = ConfigType.SETTING;
51
+ async run() {
52
+ const { args, flags } = await this.parse(CreateSetting);
53
+ const confirmYes = Boolean(flags.yes);
54
+ // NON-INTERACTIVE PATH
55
+ if (confirmYes) {
56
+ const argName = args.name;
57
+ if (!argName) {
58
+ this.error('Missing required argument for non-interactive mode: NAME\n\nExample: swell create setting api-config -y', { exit: 1 });
59
+ }
60
+ const name = toFileName(argName);
61
+ const label = flags.label || toSettingLabel(name);
62
+ const description = flags.description || '';
63
+ const fieldsRaw = (flags.fields || '');
64
+ const fieldPairs = fieldsRaw
65
+ ? fieldsRaw
66
+ .split(',')
67
+ .map((v) => v.trim())
68
+ .filter(Boolean)
69
+ : [];
70
+ const fileName = toFileName(name);
71
+ const fileBody = {
72
+ $schema: SCHEMAS.SETTING,
73
+ description,
74
+ fields: parseFields(fieldPairs),
75
+ label,
76
+ };
77
+ await this.createFile({ fileBody, fileName }, flags.overwrite,
78
+ /* shouldConfirm */ false);
79
+ return;
80
+ }
81
+ // INTERACTIVE PATH
82
+ let name = args.name;
83
+ if (!name) {
84
+ name = await input({
85
+ default: 'my-setting',
86
+ message: 'Setting name',
87
+ });
88
+ }
89
+ name = toFileName(name);
90
+ let { label, description } = flags;
91
+ const { fields: fieldsFlag, overwrite } = flags;
92
+ if (!label) {
93
+ label = await input({
94
+ default: toSettingLabel(name),
95
+ message: 'Setting label',
96
+ });
97
+ }
98
+ if (!description) {
99
+ description = await input({
100
+ default: '',
101
+ message: 'Describe what this setting is for',
102
+ });
103
+ }
104
+ const fieldsRaw = (fieldsFlag || '');
105
+ const fieldPairs = fieldsRaw
106
+ ? fieldsRaw
107
+ .split(',')
108
+ .map((v) => v.trim())
109
+ .filter(Boolean)
110
+ : [];
111
+ const fileName = toFileName(name);
112
+ const fileBody = {
113
+ $schema: SCHEMAS.SETTING,
114
+ description,
115
+ fields: parseFields(fieldPairs),
116
+ label,
117
+ };
118
+ await this.createFile({ fileBody, fileName }, overwrite);
119
+ }
120
+ }
@@ -0,0 +1,14 @@
1
+ import { AppCommand } from '../../app-command.js';
2
+ import { HelpMeta } from '../../help/types.js';
3
+ export default class CreateTests extends AppCommand {
4
+ static description: string;
5
+ static examples: string[];
6
+ static helpMeta: HelpMeta;
7
+ static flags: {
8
+ overwrite: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
+ yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
+ };
11
+ static summary: string;
12
+ run(): Promise<void>;
13
+ private logScaffoldResult;
14
+ }
@@ -0,0 +1,76 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { AppCommand } from '../../app-command.js';
3
+ import { toAppId } from '../../lib/create/index.js';
4
+ import { createTestsScaffold, } from '../../lib/create/tests.js';
5
+ export default class CreateTests extends AppCommand {
6
+ static description = 'Initialize a vitest + Cloudflare Workers test setup that reuses swell-cli authentication.';
7
+ static examples = [
8
+ '$ swell create tests',
9
+ '$ swell create tests --overwrite',
10
+ ];
11
+ static helpMeta = {
12
+ usageDirect: '[-y]',
13
+ };
14
+ static flags = {
15
+ overwrite: Flags.boolean({
16
+ default: false,
17
+ description: 'Overwrite existing test files',
18
+ }),
19
+ yes: Flags.boolean({
20
+ char: 'y',
21
+ default: false,
22
+ description: 'Skip prompts, require all arguments',
23
+ }),
24
+ };
25
+ static summary = 'Create tests scaffolding for your Swell app.';
26
+ async run() {
27
+ const { flags } = await this.parse(CreateTests);
28
+ const appIdFromConfig = (this.swellConfig.get('id') ||
29
+ this.swellConfig.get('private_id') ||
30
+ this.swellConfig.get('public_id'));
31
+ const appId = appIdFromConfig ||
32
+ toAppId(this.swellConfig.get('name')) ||
33
+ 'app';
34
+ const result = await createTestsScaffold({
35
+ appPath: this.appPath,
36
+ appId,
37
+ overwrite: Boolean(flags.overwrite),
38
+ });
39
+ this.logScaffoldResult(result);
40
+ }
41
+ logScaffoldResult(result) {
42
+ const hasChanges = result.createdFiles.length > 0 || result.packageJsonUpdated;
43
+ if (hasChanges) {
44
+ this.log('');
45
+ this.log('✓ Created test scaffolding');
46
+ }
47
+ if (result.createdFiles.length > 0) {
48
+ this.log('');
49
+ this.log('Created:');
50
+ for (const file of result.createdFiles) {
51
+ this.log(` ${file}`);
52
+ }
53
+ }
54
+ if (result.skippedFiles.length > 0) {
55
+ this.log('');
56
+ this.log('Skipped (already exist):');
57
+ for (const file of result.skippedFiles) {
58
+ this.log(` ${file}`);
59
+ }
60
+ }
61
+ if (result.packageJsonUpdated) {
62
+ this.log('');
63
+ this.log('Updated package.json with test scripts and devDependencies.');
64
+ }
65
+ for (const warning of result.warnings) {
66
+ this.warn(warning);
67
+ }
68
+ if (hasChanges) {
69
+ this.log('');
70
+ this.log('Next steps:');
71
+ this.log(' 1. npm install');
72
+ this.log(' 2. npm test');
73
+ }
74
+ this.log('');
75
+ }
76
+ }
@@ -0,0 +1,22 @@
1
+ import { CreateConfigCommand } from '../../create-config-command.js';
2
+ import { HelpMeta } from '../../help/types.js';
3
+ import { ConfigType } from '../../lib/apps/index.js';
4
+ export default class CreateWebhook extends CreateConfigCommand {
5
+ static args: {
6
+ name: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static description: string;
9
+ static examples: string[];
10
+ static helpMeta: HelpMeta;
11
+ static flags: {
12
+ url: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
+ events: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
14
+ description: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
15
+ enabled: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
16
+ overwrite: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
17
+ yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
18
+ };
19
+ static summary: string;
20
+ createType: ConfigType;
21
+ run(): Promise<void>;
22
+ }
@@ -0,0 +1,176 @@
1
+ import { confirm, input } from '@inquirer/prompts';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { CreateConfigCommand } from '../../create-config-command.js';
4
+ import { ConfigType } from '../../lib/apps/index.js';
5
+ import { toFileName } from '../../lib/create/index.js';
6
+ import { SCHEMAS } from '../../lib/create/schemas.js';
7
+ import { parseEvents, validateEventFormat, validateUrl, } from '../../lib/create/webhook.js';
8
+ export default class CreateWebhook extends CreateConfigCommand {
9
+ static args = {
10
+ name: Args.string({
11
+ default: '',
12
+ description: 'Webhook name (e.g., order-sync)',
13
+ }),
14
+ };
15
+ static description = 'Create a webhook configuration in the webhooks folder.';
16
+ static examples = [
17
+ '$ swell create webhook',
18
+ '$ swell create webhook order-sync -u https://example.com/hook -e order.created -y',
19
+ '$ swell create webhook payment-handler -u https://api.example.com/payments -e payment.succeeded,payment.failed --enabled -y',
20
+ ];
21
+ static helpMeta = {
22
+ usageDirect: '<name> -u <url> -e <events> [...] -y',
23
+ };
24
+ static flags = {
25
+ url: Flags.string({
26
+ char: 'u',
27
+ default: '',
28
+ description: 'Endpoint URL (required with -y)',
29
+ }),
30
+ events: Flags.string({
31
+ char: 'e',
32
+ default: '',
33
+ description: 'Events to listen for (e.g., order.created,payment.succeeded)',
34
+ }),
35
+ description: Flags.string({
36
+ char: 'd',
37
+ default: '',
38
+ description: 'Description',
39
+ }),
40
+ enabled: Flags.boolean({
41
+ default: false,
42
+ description: 'Enable webhook (default: false)',
43
+ }),
44
+ overwrite: Flags.boolean({
45
+ default: false,
46
+ description: 'Overwrite existing file',
47
+ }),
48
+ yes: Flags.boolean({
49
+ char: 'y',
50
+ description: 'Skip prompts, require all arguments',
51
+ }),
52
+ };
53
+ static summary = 'Create a webhook configuration in the webhooks folder.';
54
+ createType = ConfigType.WEBHOOK;
55
+ async run() {
56
+ const { args, flags } = await this.parse(CreateWebhook);
57
+ const confirmYes = Boolean(flags.yes);
58
+ // NON-INTERACTIVE PATH
59
+ if (confirmYes) {
60
+ const argName = args.name;
61
+ if (!argName) {
62
+ this.error('Missing required argument for non-interactive mode: NAME\n\nExample: swell create webhook payment-handler -u https://example.com/hook -e order.created -y', { exit: 1 });
63
+ }
64
+ if (!flags.url) {
65
+ this.error('Missing required flag for non-interactive mode: --url\n\nExample: swell create webhook payment-handler -u https://example.com/hook -e order.created -y', { exit: 1 });
66
+ }
67
+ if (!flags.events) {
68
+ this.error('Missing required flag for non-interactive mode: --events\n\nExample: swell create webhook payment-handler -u https://example.com/hook -e order.created -y', { exit: 1 });
69
+ }
70
+ // Validate URL format
71
+ validateUrl(flags.url, this.error.bind(this));
72
+ // Parse and validate events
73
+ const events = parseEvents(flags.events);
74
+ if (events.length === 0) {
75
+ this.error('At least one event is required.\n\nExample: swell create webhook payment-handler -u https://example.com/hook -e order.created -y', { exit: 1 });
76
+ }
77
+ validateEventFormat(events, this.error.bind(this));
78
+ const name = toFileName(argName);
79
+ const fileName = toFileName(name);
80
+ const fileBody = {
81
+ $schema: SCHEMAS.WEBHOOK,
82
+ description: flags.description || '',
83
+ enabled: flags.enabled,
84
+ events,
85
+ url: flags.url,
86
+ };
87
+ await this.createFile({ fileBody, fileName }, flags.overwrite,
88
+ /* shouldConfirm */ false);
89
+ return;
90
+ }
91
+ // INTERACTIVE PATH
92
+ let name = args.name;
93
+ if (!name) {
94
+ name = await input({
95
+ default: 'my-webhook',
96
+ message: 'Webhook name',
97
+ });
98
+ }
99
+ name = toFileName(name);
100
+ // URL prompt (required)
101
+ let { url } = flags;
102
+ if (url) {
103
+ validateUrl(url, this.error.bind(this));
104
+ }
105
+ else {
106
+ url = await input({
107
+ message: 'Webhook endpoint URL',
108
+ validate(value) {
109
+ if (!value) {
110
+ return 'URL is required';
111
+ }
112
+ try {
113
+ const parsed = new URL(value);
114
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
115
+ return 'URL must use HTTP or HTTPS protocol';
116
+ }
117
+ }
118
+ catch {
119
+ return 'Invalid URL format';
120
+ }
121
+ return true;
122
+ },
123
+ });
124
+ }
125
+ // Events prompt (required)
126
+ let events;
127
+ if (flags.events) {
128
+ events = parseEvents(flags.events);
129
+ validateEventFormat(events, this.error.bind(this));
130
+ }
131
+ else {
132
+ const eventsInput = await input({
133
+ message: 'Events to subscribe (e.g., order.created,payment.succeeded)',
134
+ validate(value) {
135
+ if (!value) {
136
+ return 'At least one event is required';
137
+ }
138
+ const parsed = parseEvents(value);
139
+ for (const event of parsed) {
140
+ const parts = event.split('.');
141
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
142
+ return `Invalid event format: ${event}. Use model.action pattern.`;
143
+ }
144
+ }
145
+ return true;
146
+ },
147
+ });
148
+ events = parseEvents(eventsInput);
149
+ }
150
+ // Description prompt (optional)
151
+ let { description, enabled } = flags;
152
+ const { overwrite } = flags;
153
+ if (!description) {
154
+ description = await input({
155
+ default: '',
156
+ message: 'Describe what this webhook does',
157
+ });
158
+ }
159
+ // Enabled prompt (optional)
160
+ if (!enabled) {
161
+ enabled = await confirm({
162
+ default: false,
163
+ message: 'Enable webhook now?',
164
+ });
165
+ }
166
+ const fileName = toFileName(name);
167
+ const fileBody = {
168
+ $schema: SCHEMAS.WEBHOOK,
169
+ description,
170
+ enabled,
171
+ events,
172
+ url,
173
+ };
174
+ await this.createFile({ fileBody, fileName }, overwrite);
175
+ }
176
+ }
@@ -132,7 +132,7 @@ With a content ID, it retrieves the configuration for that specific content view
132
132
  return resolvedId; // custom IDs don't need reverse resolution
133
133
  }
134
134
  // Check if identifier is already an app ID (ObjectID format)
135
- if (identifier.match(/^[0-9a-f]{24}$/)) {
135
+ if (/^[\da-f]{24}$/.test(identifier)) {
136
136
  return resolvedId; // already unresolved format
137
137
  }
138
138
  // type === 'app': resolve slug to app ID
@@ -19,6 +19,7 @@ export default class Schema extends SwellCommand {
19
19
  private showSchemaInfo;
20
20
  private outputSchema;
21
21
  private validate;
22
+ private validateFunction;
22
23
  private getFileInput;
23
24
  private readFromStdin;
24
25
  private getSchemaUrl;
@@ -5,6 +5,7 @@ import ajvErrors from 'ajv-errors';
5
5
  import addFormats from 'ajv-formats';
6
6
  import * as fs from 'node:fs';
7
7
  import parseJson from 'parse-json';
8
+ import { bundleFunction } from '../lib/bundle.js';
8
9
  import { SCHEMAS_BASE_URL } from '../lib/constants.js';
9
10
  import { SwellCommand } from '../swell-command.js';
10
11
  const SCHEMA_DEFINITIONS = Object.freeze({
@@ -23,9 +24,13 @@ const SCHEMA_DEFINITIONS = Object.freeze({
23
24
  webhook: {
24
25
  description: 'Webhook definitions (/webhooks/*.json)',
25
26
  },
27
+ function: {
28
+ description: 'Function definitions (/functions/*.ts or *.js)',
29
+ hasJsonSchema: false,
30
+ },
26
31
  });
27
32
  export default class Schema extends SwellCommand {
28
- static summary = 'View or validate Swell config JSON Schemas.';
33
+ static summary = 'View or validate Swell config schemas.';
29
34
  static examples = [
30
35
  {
31
36
  description: 'List available schema types',
@@ -44,22 +49,26 @@ export default class Schema extends SwellCommand {
44
49
  command: '<%= config.bin %> <%= command.id %> content --format=dts',
45
50
  },
46
51
  {
47
- description: 'Validate a file',
52
+ description: 'Validate a JSON config file',
48
53
  command: '<%= config.bin %> <%= command.id %> content myfile.json',
49
54
  },
50
55
  {
51
56
  description: 'Validate from stdin',
52
57
  command: 'cat myfile.json | <%= config.bin %> <%= command.id %> content -',
53
58
  },
59
+ {
60
+ description: 'Validate a function file',
61
+ command: '<%= config.bin %> <%= command.id %> function myfunction.ts',
62
+ },
54
63
  ];
55
64
  static args = {
56
65
  type: Args.string({
57
66
  required: false,
58
- description: 'Schema type (e.g. model, content, setting, notification, webhook)',
67
+ description: 'Schema type (e.g. model, content, setting, notification, webhook, function)',
59
68
  }),
60
69
  file: Args.string({
61
70
  required: false,
62
- description: 'Path to JSON file to validate (or use "-" for stdin)',
71
+ description: 'Path to file to validate (or use "-" for stdin)',
63
72
  }),
64
73
  };
65
74
  static flags = {
@@ -90,7 +99,9 @@ export default class Schema extends SwellCommand {
90
99
  if (format) {
91
100
  throw new Error('--format cannot be used with file validation');
92
101
  }
93
- await this.validate(type, file);
102
+ await (type === 'function'
103
+ ? this.validateFunction(file)
104
+ : this.validate(type, file));
94
105
  return;
95
106
  }
96
107
  // Show schema info or output schema
@@ -119,6 +130,13 @@ export default class Schema extends SwellCommand {
119
130
  this.log(`Run "swell schema --help" for more information.`);
120
131
  }
121
132
  async outputSchema(type, format) {
133
+ const definition = SCHEMA_DEFINITIONS[type];
134
+ // Guard against json-schema formats for types without JSON Schema
135
+ if ('hasJsonSchema' in definition &&
136
+ definition.hasJsonSchema === false &&
137
+ (format === 'json-schema' || format === 'json-schema-bundle')) {
138
+ throw new Error(`'${type}' does not have a JSON Schema. Use --format=dts for TypeScript declarations.`);
139
+ }
122
140
  let output;
123
141
  switch (format) {
124
142
  case 'json-schema': {
@@ -202,6 +220,34 @@ export default class Schema extends SwellCommand {
202
220
  }
203
221
  this.log('Valid model definition');
204
222
  }
223
+ async validateFunction(file) {
224
+ if (!fs.existsSync(file)) {
225
+ throw new Error(`File not found: ${file}`);
226
+ }
227
+ const filePath = fs.realpathSync(file);
228
+ // Attempt to bundle the function (validates syntax, imports, etc.)
229
+ let bundleResult;
230
+ try {
231
+ bundleResult = await bundleFunction(filePath);
232
+ }
233
+ catch (error) {
234
+ throw new Error(`Function compilation failed:\n• ${error.message}`);
235
+ }
236
+ const { config } = bundleResult;
237
+ // Validate config exists
238
+ if (!config) {
239
+ throw new Error('Invalid function:\n• Function must export a `config` object');
240
+ }
241
+ // Validate trigger exclusivity
242
+ const triggers = ['route', 'model', 'cron'].filter((t) => config[t]);
243
+ if (triggers.length === 0) {
244
+ throw new Error('Invalid function config:\n• Config must specify one of: route, model, cron');
245
+ }
246
+ if (triggers.length > 1) {
247
+ throw new Error(`Invalid function config:\n• Multiple triggers specified: ${triggers.join(', ')}. Use exactly one.`);
248
+ }
249
+ this.log('Valid function definition');
250
+ }
205
251
  getFileInput(file) {
206
252
  if (!fs.existsSync(file)) {
207
253
  throw new Error(`File not found: ${file}`);
@@ -1,9 +1,13 @@
1
+ import { HelpMeta } from '../../help/types.js';
1
2
  import InitApp from '../app/init.js';
2
3
  export default class InitTheme extends InitApp {
4
+ protected commandExample: string;
5
+ static args: {
6
+ id: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
7
+ };
3
8
  static description: string;
4
- static examples: {
5
- command: string;
6
- description: string;
7
- }[];
9
+ static examples: string[];
10
+ static helpMeta: HelpMeta;
11
+ static flags: any;
8
12
  appType: string;
9
13
  }
@@ -1,15 +1,28 @@
1
+ import { Args } from '@oclif/core';
1
2
  import InitApp from '../app/init.js';
2
3
  export default class InitTheme extends InitApp {
4
+ // Override command example for error messages
5
+ commandExample = 'swell theme init';
6
+ static args = {
7
+ id: Args.string({
8
+ default: '',
9
+ description: 'Theme identifier (defaults to current directory name)',
10
+ }),
11
+ };
3
12
  static description = 'Initialize theme swell.json in the current directory.';
4
13
  static examples = [
5
- {
6
- command: 'swell theme init',
7
- description: 'Initialize theme following command prompts.',
8
- },
9
- {
10
- command: 'swell theme init -y',
11
- description: 'Initialize theme and accept all default values.',
12
- },
14
+ '$ swell theme init',
15
+ '$ swell theme init my-theme',
16
+ '$ swell theme init -y',
17
+ '$ swell theme init my-theme -y',
18
+ '$ swell theme init my-theme --storefront-app proxima -y',
13
19
  ];
20
+ static helpMeta = {
21
+ usageDirect: '[id] --storefront-app <app> [...] -y',
22
+ };
23
+ // Inherit all flags from InitApp (which inherits from CreateApp)
24
+ static flags = {
25
+ ...InitApp.flags,
26
+ };
14
27
  appType = 'theme';
15
28
  }
@@ -1,5 +1,6 @@
1
1
  import { SwellCommand } from './swell-command.js';
2
2
  export declare abstract class CreateAppCommand extends SwellCommand {
3
+ protected commandExample: string;
3
4
  static baseFlags: {
4
5
  frontend: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
5
6
  'storefront-app': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;