@swell/cli 2.9.2 → 2.9.3

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.
@@ -8,6 +8,7 @@ import * as path from 'node:path';
8
8
  import ora from 'ora';
9
9
  import { ConfigType, allConfigFilesInDir, appConfigFromFile, } from '../../lib/apps/index.js';
10
10
  import { bundleFunction } from '../../lib/bundle.js';
11
+ import { analyzeFunctionSource, hasKindDiagnostic, } from '../../lib/function-source-analysis.js';
11
12
  import { detectPackageManager, getExecSpawnArgs, } from '../../lib/package-manager.js';
12
13
  import style from '../../lib/style.js';
13
14
  import { PushAppCommand } from '../../push-app-command.js';
@@ -223,6 +224,18 @@ ENVIRONMENT = "development"
223
224
  if (this.functionFilter && config.name !== this.functionFilter) {
224
225
  continue;
225
226
  }
227
+ const fullPath = path.join(this.appPath, config.filePath);
228
+ // eslint-disable-next-line no-await-in-loop
229
+ const analysis = await analyzeFunctionSource(fullPath);
230
+ if (analysis.kind === 'workflow') {
231
+ if (this.functionFilter) {
232
+ this.log(`Workflow ${style.appConfigValue(config.name)} deploys through ${style.command('swell app push')} and cannot run in local dev.`);
233
+ }
234
+ continue;
235
+ }
236
+ if (hasKindDiagnostic(analysis)) {
237
+ this.functionErrors.set(config.name, analysis.diagnostics.join('\n'));
238
+ }
226
239
  functions.push(config);
227
240
  }
228
241
  }
@@ -251,6 +264,15 @@ ENVIRONMENT = "development"
251
264
  try {
252
265
  // Bundle the function to get its config
253
266
  const fullPath = path.join(this.appPath, func.filePath);
267
+ const analysis = await analyzeFunctionSource(fullPath);
268
+ if (hasKindDiagnostic(analysis)) {
269
+ this.log(` ${style.error('Error loading config:', analysis.diagnostics.join('\n'))}`);
270
+ return;
271
+ }
272
+ if (analysis.kind === 'workflow') {
273
+ this.log(` Workflow: deploys through ${style.command('swell app push')}`);
274
+ return;
275
+ }
254
276
  const { config } = await bundleFunction(fullPath);
255
277
  if (config?.model?.events) {
256
278
  const events = Array.isArray(config.model.events)
@@ -311,6 +333,17 @@ ENVIRONMENT = "development"
311
333
  }
312
334
  try {
313
335
  const fullPath = path.join(this.appPath, appConfig.filePath);
336
+ const analysis = await analyzeFunctionSource(fullPath);
337
+ if (hasKindDiagnostic(analysis)) {
338
+ this.functionErrors.set(appConfig.name, analysis.diagnostics.join('\n'));
339
+ this.log(`${style.error('Error loading config')} ${appConfig.name}: ${analysis.diagnostics.join('\n')}`);
340
+ return;
341
+ }
342
+ if (analysis.kind === 'workflow') {
343
+ this.functionPorts.delete(appConfig.name);
344
+ this.log(`\nWorkflow ${style.appConfigValue(appConfig.name)} deploys through ${style.command('swell app push')} and cannot run in local dev.`);
345
+ return;
346
+ }
314
347
  // Re-bundle the function
315
348
  const { code } = await bundleFunction(fullPath);
316
349
  // Update the bundled file (wrangler dev will auto-reload)
@@ -380,6 +413,10 @@ ENVIRONMENT = "development"
380
413
  /* eslint-disable no-await-in-loop */
381
414
  for (const func of functions) {
382
415
  const functionName = func.name;
416
+ if (this.functionErrors.has(functionName)) {
417
+ functionStatus.set(functionName, 'error');
418
+ continue;
419
+ }
383
420
  if (this.functionPorts.has(functionName)) {
384
421
  // Function server already running
385
422
  continue;
@@ -397,6 +434,17 @@ ENVIRONMENT = "development"
397
434
  try {
398
435
  // Bundle the function
399
436
  const fullPath = path.join(this.appPath, func.filePath);
437
+ const analysis = await analyzeFunctionSource(fullPath);
438
+ if (hasKindDiagnostic(analysis)) {
439
+ functionStatus.set(functionName, 'error');
440
+ this.functionErrors.set(functionName, analysis.diagnostics.join('\n'));
441
+ continue;
442
+ }
443
+ if (analysis.kind === 'workflow') {
444
+ functionStatus.set(functionName, 'error');
445
+ this.functionErrors.set(functionName, 'Workflows deploy through swell app push and cannot run in local dev.');
446
+ continue;
447
+ }
400
448
  const { code } = await bundleFunction(fullPath);
401
449
  // Each function gets its own directory to avoid wrangler state conflicts
402
450
  const functionDir = path.join(this.tmpDir, functionName);
@@ -14,11 +14,14 @@ export default class CreateFunction extends CreateConfigCommand {
14
14
  overwrite: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
15
15
  route: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
16
16
  schedule: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
17
+ type: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
17
18
  yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
18
19
  };
19
20
  static summary: string;
20
21
  createType: ConfigType;
21
22
  run(): Promise<void>;
22
23
  private gatherInput;
24
+ private resolveTrigger;
25
+ private validateWorkflowFlags;
23
26
  private writeFunctionFile;
24
27
  }
@@ -5,6 +5,7 @@ import { ConfigType } from '../../lib/apps/index.js';
5
5
  import { getFunctionTemplate, } from '../../lib/create/function.js';
6
6
  import { toFunctionFileName } from '../../lib/create/index.js';
7
7
  const PERMITTED_TRIGGERS = ['cron', 'model', 'route'];
8
+ const PERMITTED_MODES = [...PERMITTED_TRIGGERS, 'workflow'];
8
9
  export default class CreateFunction extends CreateConfigCommand {
9
10
  static args = {
10
11
  name: Args.string({
@@ -13,8 +14,8 @@ export default class CreateFunction extends CreateConfigCommand {
13
14
  }),
14
15
  trigger: Args.string({
15
16
  default: '',
16
- description: 'cron | model | route',
17
- options: PERMITTED_TRIGGERS,
17
+ description: 'cron | model | route | workflow',
18
+ options: PERMITTED_MODES,
18
19
  }),
19
20
  };
20
21
  static examples = [
@@ -22,11 +23,13 @@ export default class CreateFunction extends CreateConfigCommand {
22
23
  '$ swell create function order-handler.ts model -e order.created,order.updated -y',
23
24
  '$ swell create function cleanup.ts cron -s "0 0 * * *" -y',
24
25
  '$ swell create function api-endpoint.ts route -r public -y',
26
+ '$ swell create function run-import.ts --type workflow -y',
27
+ '$ swell create function run-import.ts workflow -y',
25
28
  ];
26
29
  static helpMeta = {
27
- usageDirect: '<name> <trigger> [...] -y',
30
+ usageDirect: '<name> [<trigger>|workflow] [...] -y',
28
31
  variantSection: {
29
- title: 'TRIGGER OPTIONS',
32
+ title: 'FUNCTION MODES',
30
33
  variants: [
31
34
  {
32
35
  name: 'model',
@@ -43,6 +46,11 @@ export default class CreateFunction extends CreateConfigCommand {
43
46
  flag: '-r, --route=<option>',
44
47
  description: 'Access: public (default) | private',
45
48
  },
49
+ {
50
+ name: 'workflow',
51
+ flag: '--type=workflow',
52
+ description: 'Durable explicit-create workflow',
53
+ },
46
54
  ],
47
55
  },
48
56
  variantFlags: ['events', 'schedule', 'route'],
@@ -73,6 +81,10 @@ export default class CreateFunction extends CreateConfigCommand {
73
81
  default: '',
74
82
  description: 'Cron expression (e.g., "0 0 * * *")',
75
83
  }),
84
+ type: Flags.string({
85
+ description: 'Create a workflow function',
86
+ options: ['workflow'],
87
+ }),
76
88
  yes: Flags.boolean({
77
89
  char: 'y',
78
90
  description: 'Skip prompts, require all arguments',
@@ -83,6 +95,7 @@ export default class CreateFunction extends CreateConfigCommand {
83
95
  async run() {
84
96
  const { args, flags } = await this.parse(CreateFunction);
85
97
  const confirmYes = Boolean(flags.yes);
98
+ const trigger = this.resolveTrigger(args.trigger, flags.type);
86
99
  // NON-INTERACTIVE PATH
87
100
  if (confirmYes) {
88
101
  // Validate name argument
@@ -90,8 +103,8 @@ export default class CreateFunction extends CreateConfigCommand {
90
103
  this.error('Missing required argument for non-interactive mode: NAME\n\nExample: swell create function my-func.ts model -e order.created -y', { exit: 1 });
91
104
  }
92
105
  // Validate trigger argument
93
- if (!args.trigger) {
94
- this.error('Missing required argument for non-interactive mode: TRIGGER\n\nValid triggers: cron, model, route\n\nExample: swell create function my-func.ts model -e order.created -y', { exit: 1 });
106
+ if (!trigger) {
107
+ this.error('Missing required argument for non-interactive mode: TRIGGER or --type workflow\n\nValid modes: cron, model, route, workflow\n\nExample: swell create function my-func.ts model -e order.created -y\nExample: swell create function run-import.ts --type workflow -y', { exit: 1 });
95
108
  }
96
109
  // Validate filename has extension
97
110
  const language = args.name.endsWith('.ts')
@@ -102,7 +115,6 @@ export default class CreateFunction extends CreateConfigCommand {
102
115
  if (!language) {
103
116
  this.error('Function name must end with .ts or .js extension\n\nExample: swell create function my-func.ts model -e order.created -y', { exit: 1 });
104
117
  }
105
- const trigger = args.trigger;
106
118
  // Validate trigger-specific requirements
107
119
  if (trigger === 'model' && !flags.events) {
108
120
  this.error('Missing required flag for model trigger: --events\n\nExample: swell create function my-func.ts model -e order.created,order.updated -y', { exit: 1 });
@@ -110,6 +122,7 @@ export default class CreateFunction extends CreateConfigCommand {
110
122
  if (trigger === 'cron' && !flags.schedule) {
111
123
  this.error('Missing required flag for cron trigger: --schedule\n\nExample: swell create function my-func.ts cron -s "0 0 * * *" -y', { exit: 1 });
112
124
  }
125
+ this.validateWorkflowFlags(trigger, flags);
113
126
  // Build params with defaults
114
127
  const writeFunctionFileParams = {
115
128
  description: flags.description || '',
@@ -127,7 +140,7 @@ export default class CreateFunction extends CreateConfigCommand {
127
140
  return;
128
141
  }
129
142
  // INTERACTIVE PATH
130
- const { name: functionName, trigger } = args;
143
+ const { name: functionName } = args;
131
144
  const { description, events, overwrite, route, schedule } = flags;
132
145
  let writeFunctionFileParams = {
133
146
  description,
@@ -147,27 +160,11 @@ export default class CreateFunction extends CreateConfigCommand {
147
160
  ...writeFunctionFileParams,
148
161
  ...(await this.gatherInput(writeFunctionFileParams)),
149
162
  };
163
+ this.validateWorkflowFlags(writeFunctionFileParams.trigger, flags);
150
164
  await this.writeFunctionFile(writeFunctionFileParams, overwrite,
151
165
  /* shouldConfirm */ true);
152
166
  }
153
167
  async gatherInput(params) {
154
- // Check if there are any extensions in the app configuration
155
- const extensions = this.swellConfig.get('extensions') || [];
156
- let { extension } = params;
157
- const extensionsWithId = extensions.filter((ext) => ext.id);
158
- if (extensionsWithId.length > 0 && !extension) {
159
- const extensionChoices = [
160
- { name: 'Not for an extension', value: '' },
161
- ...extensionsWithId.map((ext) => ({
162
- name: ext.id,
163
- value: ext.id,
164
- })),
165
- ];
166
- extension = await select({
167
- choices: extensionChoices,
168
- message: 'Is this function for an app extension?',
169
- });
170
- }
171
168
  let { language } = params;
172
169
  if (!language) {
173
170
  language = await select({
@@ -184,8 +181,23 @@ export default class CreateFunction extends CreateConfigCommand {
184
181
  message: 'What language are you using?',
185
182
  });
186
183
  }
187
- let { trigger } = params;
184
+ let trigger = params.trigger;
188
185
  if (!trigger) {
186
+ trigger = await select({
187
+ choices: [
188
+ {
189
+ name: 'Function',
190
+ value: 'function',
191
+ },
192
+ {
193
+ name: 'Workflow',
194
+ value: 'workflow',
195
+ },
196
+ ],
197
+ message: 'What are you creating?',
198
+ });
199
+ }
200
+ if (trigger === 'function') {
189
201
  trigger = await select({
190
202
  choices: [
191
203
  {
@@ -204,6 +216,24 @@ export default class CreateFunction extends CreateConfigCommand {
204
216
  message: 'How will your function be triggered?',
205
217
  });
206
218
  }
219
+ let { extension } = params;
220
+ if (trigger !== 'workflow') {
221
+ const extensions = this.swellConfig.get('extensions') || [];
222
+ const extensionsWithId = extensions.filter((ext) => ext.id);
223
+ if (extensionsWithId.length > 0 && !extension) {
224
+ const extensionChoices = [
225
+ { name: 'Not for an extension', value: '' },
226
+ ...extensionsWithId.map((ext) => ({
227
+ name: ext.id,
228
+ value: ext.id,
229
+ })),
230
+ ];
231
+ extension = await select({
232
+ choices: extensionChoices,
233
+ message: 'Is this function for an app extension?',
234
+ });
235
+ }
236
+ }
207
237
  let { events } = params;
208
238
  if (trigger === 'model' && !events) {
209
239
  events = await input({
@@ -253,9 +283,32 @@ export default class CreateFunction extends CreateConfigCommand {
253
283
  language,
254
284
  route: route || '',
255
285
  schedule: schedule || '',
256
- trigger,
286
+ trigger: trigger,
257
287
  };
258
288
  }
289
+ resolveTrigger(positionalTrigger, typeFlag) {
290
+ if (typeFlag === 'workflow') {
291
+ if (positionalTrigger &&
292
+ PERMITTED_TRIGGERS.includes(positionalTrigger)) {
293
+ this.error('--type workflow cannot be combined with a positional cron, model, or route trigger.', { exit: 1 });
294
+ }
295
+ return 'workflow';
296
+ }
297
+ return positionalTrigger ? positionalTrigger : undefined;
298
+ }
299
+ validateWorkflowFlags(trigger, flags) {
300
+ if (trigger !== 'workflow') {
301
+ return;
302
+ }
303
+ const invalidFlags = [
304
+ flags.events ? '--events' : '',
305
+ flags.schedule ? '--schedule' : '',
306
+ flags.route ? '--route' : '',
307
+ ].filter(Boolean);
308
+ if (invalidFlags.length > 0) {
309
+ this.error(`Workflow functions do not support ordinary trigger flags: ${invalidFlags.join(', ')}.`, { exit: 1 });
310
+ }
311
+ }
259
312
  async writeFunctionFile({ description, events, extension, functionName, language, route, schedule, trigger, }, overwrite, shouldConfirm = true) {
260
313
  const fileName = toFunctionFileName(functionName);
261
314
  await this.createFile({
@@ -1,6 +1,7 @@
1
- import { InspectResourceCommand } from '../../inspect-resource-command.js';
1
+ import { InspectResourceCommand, InspectResourceCommandParsed } from '../../inspect-resource-command.js';
2
2
  interface FunctionRecord {
3
3
  id?: string;
4
+ kind?: string;
4
5
  name?: string;
5
6
  enabled?: boolean;
6
7
  cron?: {
@@ -37,6 +38,8 @@ export default class Functions extends InspectResourceCommand {
37
38
  protected commandName: string;
38
39
  run(): Promise<void>;
39
40
  protected metaFor(r: FunctionRecord): string | undefined;
41
+ protected filterListResults<T>(results: T[]): T[];
42
+ protected emitDetail(record: FunctionRecord, flags: InspectResourceCommandParsed['flags']): void;
40
43
  /**
41
44
  * Model-triggered functions emit `/events:webhooks` rows on each invocation.
42
45
  * Route and cron functions do not — pointing the agent at an empty query
@@ -64,6 +64,17 @@ Identifier forms:
64
64
  }
65
65
  return parts.length > 0 ? parts.join(' · ') : undefined;
66
66
  }
67
+ filterListResults(results) {
68
+ return results.filter((record) => record.kind !== 'workflow');
69
+ }
70
+ emitDetail(record, flags) {
71
+ if (record.kind === 'workflow') {
72
+ const workflowName = record.name || record.id;
73
+ const workflowRef = record.id || record.name;
74
+ throw new Error(`Function '${workflowName}' is a workflow. Use "swell inspect workflows ${workflowRef}" instead.`);
75
+ }
76
+ super.emitDetail(record, flags);
77
+ }
67
78
  /**
68
79
  * Model-triggered functions emit `/events:webhooks` rows on each invocation.
69
80
  * Route and cron functions do not — pointing the agent at an empty query
@@ -8,6 +8,7 @@ export default class Inspect extends Command {
8
8
  'swell inspect content app.honest_reviews.reviews',
9
9
  'swell inspect webhooks --app=my-app',
10
10
  'swell inspect functions app.my-app.payment-sync',
11
+ 'swell inspect workflow-runs --workflow app.my-app.run-import --status active',
11
12
  'swell inspect notifications com.orders.receipt.v2',
12
13
  'swell inspect settings app.my-app',
13
14
  ];
@@ -0,0 +1,34 @@
1
+ import { InspectResourceCommand } from '../../inspect-resource-command.js';
2
+ export default class WorkflowRuns extends InspectResourceCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static args: {
6
+ identifier: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ limit: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
+ status: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
+ workflow: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
+ app: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
+ live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
14
+ json: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
15
+ yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
16
+ };
17
+ static examples: string[];
18
+ protected resourceLabel: string;
19
+ protected resourceLabelSingular: string;
20
+ protected adminPath: string;
21
+ protected commandName: string;
22
+ private detailContext?;
23
+ run(): Promise<void>;
24
+ protected hints(): string[];
25
+ private showRecentRunList;
26
+ private showRunDetail;
27
+ private getRunDetail;
28
+ private resolveWorkflowRunScope;
29
+ private getRunDetailForApp;
30
+ private getRecentRunsForScopedApp;
31
+ private getRecentRunsForInstalledApps;
32
+ private resolveRunListTargets;
33
+ private getRunListForApp;
34
+ }