motia 0.0.21 → 0.0.23

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 (33) hide show
  1. package/dist/src/cli.js +31 -0
  2. package/dist/src/create/index.js +2 -2
  3. package/dist/src/create-step/getAnswers.js +171 -0
  4. package/dist/src/create-step/index.js +47 -0
  5. package/dist/src/create-step/teamplateUtils.js +50 -0
  6. package/dist/src/create-step/templates/api/template.javascript.txt +25 -0
  7. package/dist/src/create-step/templates/api/template.python.txt +19 -0
  8. package/dist/src/create-step/templates/api/template.ruby.txt +22 -0
  9. package/dist/src/create-step/templates/api/template.typescript.txt +26 -0
  10. package/dist/src/create-step/templates/cron/template.javascript.txt +20 -0
  11. package/dist/src/create-step/templates/cron/template.python.txt +19 -0
  12. package/dist/src/create-step/templates/cron/template.ruby.txt +22 -0
  13. package/dist/src/create-step/templates/cron/template.type.txt +22 -0
  14. package/dist/src/create-step/templates/cron/template.typescript.txt +22 -0
  15. package/dist/src/create-step/templates/event/template.javascript.txt +27 -0
  16. package/dist/src/create-step/templates/event/template.python.txt +20 -0
  17. package/dist/src/create-step/templates/event/template.ruby.txt +22 -0
  18. package/dist/src/create-step/templates/event/template.typescript.txt +30 -0
  19. package/dist/src/create-step/templates/noop/template.javascript.txt +8 -0
  20. package/dist/src/create-step/templates/noop/template.python.txt +8 -0
  21. package/dist/src/create-step/templates/noop/template.ruby.txt +10 -0
  22. package/dist/src/create-step/templates/noop/template.typescript.txt +10 -0
  23. package/dist/src/create-step/templates/ui/api.step.txt +20 -0
  24. package/dist/src/create-step/templates/ui/cron.step.txt +15 -0
  25. package/dist/src/create-step/templates/ui/event.step.txt +15 -0
  26. package/dist/src/create-step/templates/ui/noop.step.txt +15 -0
  27. package/dist/src/create-step/templates/ui/overrides.js +26 -0
  28. package/dist/src/create-step/types.js +6 -0
  29. package/dist/src/create-step/utils.js +13 -0
  30. package/dist/src/generate-locked-data.js +5 -23
  31. package/dist/src/watcher.js +12 -15
  32. package/package.json +5 -3
  33. package/scripts/move-templates.sh +31 -17
package/dist/src/cli.js CHANGED
@@ -6,6 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const commander_1 = require("commander");
8
8
  const path_1 = __importDefault(require("path"));
9
+ const fs_1 = __importDefault(require("fs"));
9
10
  const defaultPort = 3000;
10
11
  // eslint-disable-next-line @typescript-eslint/no-require-imports
11
12
  require('dotenv/config');
@@ -94,4 +95,34 @@ commander_1.program
94
95
  process.exit(1);
95
96
  }
96
97
  });
98
+ const generate = commander_1.program.command('generate').description('Generate motia resources');
99
+ generate
100
+ .command('step')
101
+ .description('Create a new step with interactive prompts')
102
+ .option('-d, --dir <step file path>', 'The path relative to the steps directory, used to create the step file')
103
+ .action(async (arg) => {
104
+ const { createStep } = require('./create-step'); // eslint-disable-line @typescript-eslint/no-require-imports
105
+ await createStep({
106
+ stepFilePath: arg.dir,
107
+ });
108
+ });
109
+ const state = commander_1.program.command('state').description('Manage application state');
110
+ state
111
+ .command('list')
112
+ .description('List the current file state')
113
+ .action(async () => {
114
+ try {
115
+ const statePath = path_1.default.join(process.cwd(), '.motia', 'motia.state.json');
116
+ if (!fs_1.default.existsSync(statePath)) {
117
+ console.error('Error: State file not found at', statePath);
118
+ process.exit(1);
119
+ }
120
+ const state = require(statePath); // eslint-disable-line @typescript-eslint/no-require-imports
121
+ console.log(JSON.stringify(state, null, 2));
122
+ }
123
+ catch (error) {
124
+ console.error('Error:', error instanceof Error ? error.message : 'Unknown error');
125
+ process.exit(1);
126
+ }
127
+ });
97
128
  commander_1.program.parse(process.argv);
@@ -62,8 +62,8 @@ const installRequiredDependencies = async (packageManager, rootDir) => {
62
62
  yarn: 'yarn add',
63
63
  pnpm: 'pnpm add',
64
64
  }[packageManager];
65
- const dependencies = ['@motiadev/core', 'motia', '@motiadev/workbench', 'zod'].join(' ');
66
- const devDependencies = ['ts-node@^10.9.2', 'typescript@^5.7.3'].join(' ');
65
+ const dependencies = ['@motiadev/core', 'motia', '@motiadev/workbench', 'zod', 'react@^19.0.0'].join(' ');
66
+ const devDependencies = ['ts-node@^10.9.2', 'typescript@^5.7.3', '@types/react@^18.3.18'].join(' ');
67
67
  try {
68
68
  await executeCommand(`${installCommand} ${dependencies}`, rootDir);
69
69
  await executeCommand(`${installCommand} -D ${devDependencies}`, rootDir);
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getStepAnswers = void 0;
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const colors_1 = __importDefault(require("colors"));
9
+ const types_1 = require("./types");
10
+ const getStepAnswers = async () => {
11
+ console.log('\nšŸ“ ', colors_1.default.bold('Create a new Motia step\n'));
12
+ // Basic information prompts
13
+ const basicInfo = await inquirer_1.default.prompt([
14
+ {
15
+ type: 'input',
16
+ name: 'name',
17
+ message: 'Step name:',
18
+ validate: (input) => {
19
+ if (input.length === 0)
20
+ return 'Name is required';
21
+ if (!/^[a-zA-Z][a-zA-Z0-9-_]*$/.test(input)) {
22
+ return 'Name must start with a letter and contain only letters, numbers, hyphens, and underscores';
23
+ }
24
+ return true;
25
+ },
26
+ },
27
+ {
28
+ type: 'list',
29
+ name: 'type',
30
+ message: 'Select step type:',
31
+ choices: types_1.STEP_TYPES.map((type) => ({
32
+ name: type.toUpperCase(),
33
+ value: type,
34
+ })),
35
+ },
36
+ ]);
37
+ let answers = { ...basicInfo };
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const languageAnswer = {
40
+ type: 'list',
41
+ name: 'language',
42
+ message: 'Select language:',
43
+ choices: types_1.LANGUAGES.map((lang) => ({
44
+ name: lang.charAt(0).toUpperCase() + lang.slice(1),
45
+ value: lang,
46
+ })),
47
+ };
48
+ // Type-specific configuration prompts
49
+ if (answers.type === 'api') {
50
+ const apiConfig = await inquirer_1.default.prompt([
51
+ {
52
+ type: 'list',
53
+ name: 'method',
54
+ message: 'HTTP method:',
55
+ choices: types_1.HTTP_METHODS,
56
+ },
57
+ {
58
+ type: 'input',
59
+ name: 'path',
60
+ message: 'API path (e.g. /users):',
61
+ validate: (input) => {
62
+ if (!input.startsWith('/'))
63
+ return 'Path must start with /';
64
+ return true;
65
+ },
66
+ },
67
+ ]);
68
+ answers = { ...answers, ...apiConfig };
69
+ }
70
+ else if (answers.type === 'event') {
71
+ const eventConfig = await inquirer_1.default.prompt([
72
+ languageAnswer,
73
+ {
74
+ type: 'input',
75
+ name: 'subscriptions',
76
+ message: 'Event subscriptions (comma-separated):',
77
+ filter: (input) => input
78
+ .split(',')
79
+ .map((s) => s.trim())
80
+ .filter(Boolean),
81
+ },
82
+ ]);
83
+ answers = { ...answers, ...eventConfig };
84
+ }
85
+ else if (answers.type === 'cron') {
86
+ const cronConfig = await inquirer_1.default.prompt([
87
+ languageAnswer,
88
+ {
89
+ type: 'input',
90
+ name: 'cronExpression',
91
+ message: 'Cron expression:',
92
+ validate: (input) => {
93
+ if (!input)
94
+ return 'Cron expression is required';
95
+ const parts = input.split(' ');
96
+ if (parts.length !== 5)
97
+ return 'Invalid cron expression format';
98
+ return true;
99
+ },
100
+ },
101
+ ]);
102
+ answers = { ...answers, ...cronConfig };
103
+ }
104
+ else if (answers.type === 'noop') {
105
+ const noopConfig = await inquirer_1.default.prompt([
106
+ languageAnswer,
107
+ {
108
+ type: 'input',
109
+ name: 'virtualEmits',
110
+ message: 'Virtual emits (comma-separated):',
111
+ filter: (input) => input
112
+ .split(',')
113
+ .map((s) => s.trim())
114
+ .filter(Boolean),
115
+ },
116
+ {
117
+ type: 'input',
118
+ name: 'virtualSubscribes',
119
+ message: 'Virtual subscribes (comma-separated):',
120
+ filter: (input) => input
121
+ .split(',')
122
+ .map((s) => s.trim())
123
+ .filter(Boolean),
124
+ },
125
+ ]);
126
+ answers = { ...answers, ...noopConfig };
127
+ }
128
+ // Common configuration prompts
129
+ const commonConfig = await inquirer_1.default.prompt([
130
+ {
131
+ type: 'input',
132
+ name: 'flows',
133
+ message: 'Flow names (comma-separated):',
134
+ filter: (input) => input
135
+ .split(',')
136
+ .map((s) => s.trim())
137
+ .filter(Boolean),
138
+ validate: (input) => {
139
+ if (input.length === 0)
140
+ return 'At least one flow is required';
141
+ return true;
142
+ },
143
+ },
144
+ {
145
+ type: 'input',
146
+ name: 'emits',
147
+ message: 'Events to emit (comma-separated):',
148
+ filter: (input) => input
149
+ .split(',')
150
+ .map((s) => s.trim())
151
+ .filter(Boolean),
152
+ },
153
+ {
154
+ type: 'input',
155
+ name: 'description',
156
+ message: 'Step description:',
157
+ },
158
+ {
159
+ type: 'confirm',
160
+ name: 'createOverride',
161
+ message: 'Create UI component override?',
162
+ default: false,
163
+ },
164
+ ]);
165
+ const nextAnswers = { ...answers, ...commonConfig };
166
+ return {
167
+ ...nextAnswers,
168
+ language: nextAnswers.type === 'api' ? 'typescript' : nextAnswers.language,
169
+ };
170
+ };
171
+ exports.getStepAnswers = getStepAnswers;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createStep = createStep;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const colors_1 = __importDefault(require("colors"));
10
+ const teamplateUtils_1 = require("./teamplateUtils");
11
+ const overrides_1 = require("./templates/ui/overrides");
12
+ const getAnswers_1 = require("./getAnswers");
13
+ const utils_1 = require("./utils");
14
+ async function createStep(options) {
15
+ try {
16
+ const answers = await (0, getAnswers_1.getStepAnswers)();
17
+ // Create steps directory if it doesn't exist
18
+ const stepDir = path_1.default.join(process.cwd(), 'steps', options?.stepFilePath || '');
19
+ if (!fs_1.default.existsSync(stepDir)) {
20
+ fs_1.default.mkdirSync(stepDir, { recursive: true });
21
+ }
22
+ // Create step file
23
+ const extension = (0, utils_1.getFileExtension)(answers.language);
24
+ const stepPath = path_1.default.join(stepDir, `${answers.name}${answers.type === 'api' ? '.api' : ''}.step${extension}`);
25
+ // Check if file already exists
26
+ if (fs_1.default.existsSync(stepPath)) {
27
+ console.error(colors_1.default.red(`\nāŒ Error: Step file already exists at ${stepPath}`));
28
+ process.exit(1);
29
+ }
30
+ // Generate and write step file
31
+ const stepContent = await (0, teamplateUtils_1.generateTemplate)(answers);
32
+ fs_1.default.writeFileSync(stepPath, stepContent);
33
+ console.log(colors_1.default.green(`\n✨ Created step file at ${stepPath}`));
34
+ // Create UI override if requested
35
+ if (answers.createOverride) {
36
+ const overridePath = path_1.default.join(stepDir, `${answers.name}.step.tsx`);
37
+ const overrideContent = await (0, overrides_1.generateOverride)(answers);
38
+ fs_1.default.writeFileSync(overridePath, overrideContent);
39
+ console.log(colors_1.default.green(`✨ Created UI override at ${overridePath}`));
40
+ }
41
+ console.log(colors_1.default.bold('\nšŸŽ‰ Step creation complete!'));
42
+ }
43
+ catch (error) {
44
+ console.error(colors_1.default.red('\nāŒ Error creating step:'), error);
45
+ process.exit(1);
46
+ }
47
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateTemplate = generateTemplate;
7
+ const fs_1 = require("fs");
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * Reads a template file and replaces variables
11
+ */
12
+ async function readTemplate(templatePath, answers) {
13
+ const content = await fs_1.promises.readFile(templatePath, 'utf8');
14
+ return replaceTemplateVariables(content, answers);
15
+ }
16
+ /**
17
+ * Replaces template variables with actual values
18
+ */
19
+ function replaceTemplateVariables(content, answers) {
20
+ const replacements = {
21
+ STEP_NAME: answers.name,
22
+ DESCRIPTION: answers.description || '',
23
+ FLOWS: JSON.stringify(answers.flows),
24
+ EMITS: JSON.stringify(answers.emits),
25
+ METHOD: answers.method || '',
26
+ PATH: answers.path || '',
27
+ SUBSCRIPTIONS: JSON.stringify(answers.subscriptions || []),
28
+ CRON_EXPRESSION: answers.cronExpression || '',
29
+ VIRTUAL_EMITS: JSON.stringify(answers.virtualEmits || []),
30
+ VIRTUAL_SUBSCRIBES: JSON.stringify(answers.virtualSubscribes || []),
31
+ };
32
+ return Object.entries(replacements).reduce((content, [key, value]) => {
33
+ const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
34
+ return content.replace(regex, String(value));
35
+ }, content);
36
+ }
37
+ /**
38
+ * Generates the appropriate template based on language and type
39
+ */
40
+ async function generateTemplate(answers) {
41
+ const templateDir = path_1.default.join(__dirname, 'templates', answers.type);
42
+ const templateFile = `template.${answers.language}.txt`;
43
+ const templatePath = path_1.default.join(templateDir, templateFile);
44
+ try {
45
+ return await readTemplate(templatePath, answers);
46
+ }
47
+ catch (error) {
48
+ throw new Error(`Failed to generate template: ${error}`);
49
+ }
50
+ }
@@ -0,0 +1,25 @@
1
+ const { z } = require('zod')
2
+
3
+ export const config = {
4
+ type: 'api',
5
+ name: '{{STEP_NAME}}',
6
+ description: '{{DESCRIPTION}}',
7
+ path: '{{PATH}}',
8
+ method: '{{METHOD}}',
9
+ emits: {{EMITS}},
10
+ flows: {{FLOWS}},
11
+ bodySchema: z.object({
12
+ // Add your schema here
13
+ })
14
+ }
15
+
16
+ export const handler = async (req, { logger, emit }) => {
17
+ logger.info('Processing {{STEP_NAME}}', req)
18
+
19
+ // Add your handler logic here
20
+
21
+ return {
22
+ status: 200,
23
+ body: { message: 'Success' }
24
+ }
25
+ }
@@ -0,0 +1,19 @@
1
+ config = {
2
+ "type": "api",
3
+ "name": "{{STEP_NAME}}",
4
+ "description": "{{DESCRIPTION}}",
5
+ "path": "{{PATH}}",
6
+ "method": "{{METHOD}}",
7
+ "emits": {{EMITS}},
8
+ "flows": {{FLOWS}}
9
+ }
10
+
11
+ async def handler(req, context):
12
+ context.logger.info("Processing {{STEP_NAME}}", req)
13
+
14
+ # Add your handler logic here
15
+
16
+ return {
17
+ "status": 200,
18
+ "body": {"message": "Success"}
19
+ }
@@ -0,0 +1,22 @@
1
+ def config
2
+ {
3
+ type: 'api',
4
+ name: '{{STEP_NAME}}',
5
+ description: '{{DESCRIPTION}}',
6
+ path: '{{PATH}}',
7
+ method: '{{METHOD}}',
8
+ emits: {{EMITS}},
9
+ flows: {{FLOWS}}
10
+ }
11
+ end
12
+
13
+ def handler(req, context)
14
+ context.logger.info('Processing {{STEP_NAME}}', req)
15
+
16
+ # Add your handler logic here
17
+
18
+ {
19
+ status: 200,
20
+ body: { message: 'Success' }
21
+ }
22
+ end
@@ -0,0 +1,26 @@
1
+ import { ApiRouteConfig, StepHandler } from '@motiadev/core'
2
+ import { z } from 'zod'
3
+
4
+ export const config: ApiRouteConfig = {
5
+ type: 'api',
6
+ name: '{{STEP_NAME}}',
7
+ description: '{{DESCRIPTION}}',
8
+ path: '{{PATH}}',
9
+ method: '{{METHOD}}',
10
+ emits: {{EMITS}},
11
+ flows: {{FLOWS}},
12
+ bodySchema: z.object({
13
+ // Add your schema here
14
+ })
15
+ }
16
+
17
+ export const handler: StepHandler<typeof config> = async (req, { logger, emit }) => {
18
+ logger.info('Processing {{STEP_NAME}}', req)
19
+
20
+ // Add your handler logic here
21
+
22
+ return {
23
+ status: 200,
24
+ body: { message: 'Success' }
25
+ }
26
+ }
@@ -0,0 +1,20 @@
1
+ export const config = {
2
+ type: 'cron',
3
+ name: '{{STEP_NAME}}',
4
+ description: '{{DESCRIPTION}}',
5
+ cron: '{{CRON_EXPRESSION}}',
6
+ emits: {{EMITS}},
7
+ flows: {{FLOWS}}
8
+ }
9
+
10
+ export const handler = async ({ logger, emit }) => {
11
+ logger.info('Running {{STEP_NAME}} cron job')
12
+
13
+ // Add your cron logic here
14
+
15
+ // Example emit
16
+ // await emit({
17
+ // type: 'event-type',
18
+ // data: {}
19
+ // })
20
+ }
@@ -0,0 +1,19 @@
1
+ config = {
2
+ "type": "cron",
3
+ "name": "{{STEP_NAME}}",
4
+ "description": "{{DESCRIPTION}}",
5
+ "cron": "{{CRON_EXPRESSION}}",
6
+ "emits": {{EMITS}},
7
+ "flows": {{FLOWS}}
8
+ }
9
+
10
+ async def handler(context):
11
+ context.logger.info("Running {{STEP_NAME}} cron job")
12
+
13
+ # Add your cron logic here
14
+
15
+ # Example emit
16
+ # await context.emit({
17
+ # "type": "event-type",
18
+ # "data": {}
19
+ # })
@@ -0,0 +1,22 @@
1
+ def config
2
+ {
3
+ type: 'cron',
4
+ name: '{{STEP_NAME}}',
5
+ description: '{{DESCRIPTION}}',
6
+ cron: '{{CRON_EXPRESSION}}',
7
+ emits: {{EMITS}},
8
+ flows: {{FLOWS}}
9
+ }
10
+ end
11
+
12
+ def handler(context)
13
+ context.logger.info('Running {{STEP_NAME}} cron job')
14
+
15
+ # Add your cron logic here
16
+
17
+ # Example emit
18
+ # context.emit({
19
+ # type: 'event-type',
20
+ # data: {}
21
+ # })
22
+ end
@@ -0,0 +1,22 @@
1
+ import { CronConfig, StepHandler } from '@motiadev/core'
2
+
3
+ export const config: CronConfig = {
4
+ type: 'cron',
5
+ name: '{{STEP_NAME}}',
6
+ description: '{{DESCRIPTION}}',
7
+ cron: '{{CRON_EXPRESSION}}',
8
+ emits: {{EMITS}},
9
+ flows: {{FLOWS}}
10
+ }
11
+
12
+ export const handler: StepHandler<typeof config> = async ({ logger, emit }) => {
13
+ logger.info('Running {{STEP_NAME}} cron job')
14
+
15
+ // Add your cron logic here
16
+
17
+ // Example emit
18
+ // await emit({
19
+ // type: 'event-type',
20
+ // data: {}
21
+ // })
22
+ }
@@ -0,0 +1,22 @@
1
+ import { CronConfig, FlowContext } from '@motiadev/core'
2
+
3
+ export const config = {
4
+ type: 'cron',
5
+ name: '{{STEP_NAME}}',
6
+ description: '{{DESCRIPTION}}',
7
+ cron: '{{CRON_EXPRESSION}}',
8
+ emits: {{EMITS}},
9
+ flows: {{FLOWS}}
10
+ }
11
+
12
+ export const handler = async ({ logger, emit }: FlowContext) => {
13
+ logger.info('Running {{STEP_NAME}} cron job')
14
+
15
+ // Add your cron logic here
16
+
17
+ // Example emit
18
+ // await emit({
19
+ // type: 'event-type',
20
+ // data: {}
21
+ // })
22
+ }
@@ -0,0 +1,27 @@
1
+ const { z } = require('zod')
2
+
3
+ const inputSchema = z.object({
4
+ // Add your schema here
5
+ })
6
+
7
+ export const config = {
8
+ type: 'event',
9
+ name: '{{STEP_NAME}}',
10
+ description: '{{DESCRIPTION}}',
11
+ subscribes: {{SUBSCRIPTIONS}},
12
+ emits: {{EMITS}},
13
+ input: inputSchema,
14
+ flows: {{FLOWS}}
15
+ }
16
+
17
+ export const handler = async (input, { logger, emit }) => {
18
+ logger.info('Processing {{STEP_NAME}}', input)
19
+
20
+ // Add your handler logic here
21
+
22
+ // Example emit
23
+ // await emit({
24
+ // type: 'event-type',
25
+ // data: {}
26
+ // })
27
+ }
@@ -0,0 +1,20 @@
1
+ config = {
2
+ "type": "event",
3
+ "name": "{{STEP_NAME}}",
4
+ "description": "{{DESCRIPTION}}",
5
+ "subscribes": {{SUBSCRIPTIONS}},
6
+ "emits": {{EMITS}},
7
+ "flows": {{FLOWS}},
8
+ "input": None, # No schema validation in Python
9
+ }
10
+
11
+ async def handler(args, ctx):
12
+ ctx.logger.info('Processing {{STEP_NAME}}', args)
13
+
14
+ # Add your handler logic here
15
+
16
+ # Add your handler logic here
17
+ # await ctx.emit({
18
+ # type: 'event-type',
19
+ # data: {}
20
+ # })
@@ -0,0 +1,22 @@
1
+ def config
2
+ {
3
+ type: 'event',
4
+ name: '{{STEP_NAME}}',
5
+ description: '{{DESCRIPTION}}',
6
+ subscribes: {{SUBSCRIPTIONS}},
7
+ emits: {{EMITS}},
8
+ flows: {{FLOWS}}
9
+ }
10
+ end
11
+
12
+ def handler(input, context)
13
+ context.logger.info('Processing {{STEP_NAME}}', input)
14
+
15
+ # Add your handler logic here
16
+
17
+ # Example emit
18
+ # context.emit({
19
+ # type: 'event-type',
20
+ # data: {}
21
+ # })
22
+ end
@@ -0,0 +1,30 @@
1
+ import { EventConfig, StepHandler } from '@motiadev/core'
2
+ import { z } from 'zod'
3
+
4
+ type Input = typeof inputSchema
5
+
6
+ const inputSchema = z.object({
7
+ // Add your schema here
8
+ })
9
+
10
+ export const config: EventConfig = {
11
+ type: 'event',
12
+ name: '{{STEP_NAME}}',
13
+ description: '{{DESCRIPTION}}',
14
+ subscribes: {{SUBSCRIPTIONS}},
15
+ emits: {{EMITS}},
16
+ input: inputSchema,
17
+ flows: {{FLOWS}}
18
+ }
19
+
20
+ export const handler: StepHandler<typeof config> = async (input, { logger, emit }) => {
21
+ logger.info('Processing {{STEP_NAME}}', input)
22
+
23
+ // Add your handler logic here
24
+
25
+ // Example emit
26
+ // await emit({
27
+ // type: 'event-type',
28
+ // data: {}
29
+ // })
30
+ }
@@ -0,0 +1,8 @@
1
+ export const config = {
2
+ type: 'noop',
3
+ name: '{{STEP_NAME}}',
4
+ description: '{{DESCRIPTION}}',
5
+ virtualEmits: {{VIRTUAL_EMITS}},
6
+ virtualSubscribes: {{VIRTUAL_SUBSCRIBES}},
7
+ flows: {{FLOWS}}
8
+ }
@@ -0,0 +1,8 @@
1
+ config = {
2
+ "type": "noop",
3
+ "name": "{{STEP_NAME}}",
4
+ "description": "{{DESCRIPTION}}",
5
+ "virtualEmits": {{VIRTUAL_EMITS}},
6
+ "virtualSubscribes": {{VIRTUAL_SUBSCRIBES}},
7
+ "flows": {{FLOWS}}
8
+ }
@@ -0,0 +1,10 @@
1
+ def config
2
+ {
3
+ type: 'noop',
4
+ name: '{{STEP_NAME}}',
5
+ description: '{{DESCRIPTION}}',
6
+ virtualEmits: {{VIRTUAL_EMITS}},
7
+ virtualSubscribes: {{VIRTUAL_SUBSCRIBES}},
8
+ flows: {{FLOWS}}
9
+ }
10
+ end
@@ -0,0 +1,10 @@
1
+ import { NoopConfig } from '@motiadev/core'
2
+
3
+ export const config: NoopConfig = {
4
+ type: 'noop',
5
+ name: '{{STEP_NAME}}',
6
+ description: '{{DESCRIPTION}}',
7
+ virtualEmits: {{VIRTUAL_EMITS}},
8
+ virtualSubscribes: {{VIRTUAL_SUBSCRIBES}},
9
+ flows: {{FLOWS}}
10
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react'
2
+ import { ApiNode, Button, ApiNodeProps } from '@motiadev/workbench'
3
+
4
+ export const Node: React.FC<ApiNodeProps> = ({ data }) => {
5
+ const doSomething = () => {
6
+ window.alert('Replace this with your custom logic')
7
+ }
8
+
9
+
10
+ return (
11
+ <ApiNode data={{ ...data, description: undefined }}>
12
+ <div className="flex flex-row gap-4">
13
+ <div className="flex flex-col items-center text-sm">{data.description}</div>
14
+ <div className="flex flex-col items-end text-sm">
15
+ <Button onClick={doSomething}>Play</Button>
16
+ </div>
17
+ </div>
18
+ </ApiNode>
19
+ )
20
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react'
2
+ import { BaseNode, Button, CronNodeProps } from '@motiadev/workbench'
3
+
4
+ export const Node: React.FC<CronNodeProps> = ({ data }) => {
5
+ const doSomething = () => {
6
+ window.alert('Replace this with your custom logic')
7
+ }
8
+
9
+
10
+ return (
11
+ <BaseNode title="{{STEP_NAME}}" variant="noop" {...data} disableTargetHandle>
12
+ <Button onClick={doSomething}>Click me</Button>
13
+ </BaseNode>
14
+ )
15
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react'
2
+ import { BaseNode, Button, EventNodeProps } from '@motiadev/workbench'
3
+
4
+ export const Node: React.FC<EventNodeProps> = ({ data }) => {
5
+ const doSomething = () => {
6
+ window.alert('Replace this with your custom logic')
7
+ }
8
+
9
+
10
+ return (
11
+ <BaseNode title="{{STEP_NAME}}" variant="noop" {...data} disableTargetHandle>
12
+ <Button onClick={doSomething}>Click me</Button>
13
+ </BaseNode>
14
+ )
15
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react'
2
+ import { BaseNode, Button, NoopNodeProps } from '@motiadev/workbench'
3
+
4
+ export const Node: React.FC<NoopNodeProps> = ({ data }) => {
5
+ const doSomething = () => {
6
+ window.alert('Replace this with your custom logic')
7
+ }
8
+
9
+
10
+ return (
11
+ <BaseNode title="{{STEP_NAME}}" variant="noop" {...data} disableTargetHandle>
12
+ <Button onClick={doSomething}>Click me</Button>
13
+ </BaseNode>
14
+ )
15
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateOverride = generateOverride;
7
+ const fs_1 = require("fs");
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * Generates a React component override for the step
11
+ */
12
+ async function generateOverride(answers) {
13
+ const templatePath = path_1.default.join(__dirname, `${answers.type}.step.txt`);
14
+ const content = await fs_1.promises.readFile(templatePath, 'utf8');
15
+ const replacements = {
16
+ STEP_NAME: answers.name,
17
+ DESCRIPTION: answers.description || '',
18
+ TYPE: answers.type,
19
+ FLOWS: JSON.stringify(answers.flows),
20
+ EMITS: JSON.stringify(answers.emits),
21
+ };
22
+ return Object.entries(replacements).reduce((content, [key, value]) => {
23
+ const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
24
+ return content.replace(regex, String(value));
25
+ }, content);
26
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HTTP_METHODS = exports.LANGUAGES = exports.STEP_TYPES = void 0;
4
+ exports.STEP_TYPES = ['api', 'event', 'cron', 'noop'];
5
+ exports.LANGUAGES = ['typescript', 'javascript', 'python', 'ruby'];
6
+ exports.HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE'];
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getFileExtension = void 0;
4
+ const getFileExtension = (language) => {
5
+ const extensions = {
6
+ typescript: '.ts',
7
+ javascript: '.js',
8
+ python: '.py',
9
+ ruby: '.rb',
10
+ };
11
+ return extensions[language];
12
+ };
13
+ exports.getFileExtension = getFileExtension;
@@ -3,45 +3,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.generateLockedData = exports.readConfig = void 0;
6
+ exports.generateLockedData = void 0;
7
7
  const core_1 = require("@motiadev/core");
8
8
  const crypto_1 = require("crypto");
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
- const yaml_1 = __importDefault(require("yaml"));
12
11
  const version = `${(0, crypto_1.randomUUID)()}:${Math.floor(Date.now() / 1000)}`;
13
- const baseFlowRegex = new RegExp(/flows"?\s?.*\s*\[([^\]]+)\]/);
14
- // Helper function to read config.yml
15
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
- const readConfig = (configPath) => {
17
- if (!fs_1.default.existsSync(configPath)) {
18
- console.warn(`Config file not found at ${configPath}`);
19
- return {};
20
- }
21
- const configContent = fs_1.default.readFileSync(configPath, 'utf-8');
22
- return yaml_1.default.parse(configContent);
23
- };
24
- exports.readConfig = readConfig;
25
12
  // Helper function to recursively collect flow data
26
- const collectFlows = async (baseDir) => {
13
+ const collectFlows = async (baseDir, lockedData) => {
27
14
  const folderItems = fs_1.default.readdirSync(baseDir, { withFileTypes: true });
28
15
  let steps = [];
29
16
  for (const item of folderItems) {
30
17
  const filePath = path_1.default.join(baseDir, item.name);
31
18
  if (item.isDirectory()) {
32
- steps = steps.concat(await collectFlows(filePath));
19
+ steps = steps.concat(await collectFlows(filePath, lockedData));
33
20
  }
34
21
  else if (item.name.match(/\.step\.(ts|js|py|rb)$/)) {
35
- const fileContent = fs_1.default.readFileSync(filePath, 'utf-8');
36
- const flowMatch = fileContent.match(baseFlowRegex);
37
22
  const config = await (0, core_1.getStepConfig)(filePath);
38
23
  if (!config) {
39
24
  console.warn(`No config found in step ${filePath}, step skipped`);
40
25
  continue;
41
26
  }
42
- if (flowMatch) {
43
- steps.push({ filePath, version, config });
44
- }
27
+ lockedData.createStep({ filePath, version, config });
45
28
  }
46
29
  }
47
30
  return steps;
@@ -52,9 +35,8 @@ const generateLockedData = async (projectDir) => {
52
35
  * NOTE: right now for performance and simplicity let's enforce a folder,
53
36
  * but we might want to remove this and scan the entire current directory
54
37
  */
55
- const sourceSteps = await collectFlows(path_1.default.join(projectDir, 'steps'));
56
38
  const lockedData = new core_1.LockedData(projectDir);
57
- sourceSteps.forEach((step) => lockedData.createStep(step));
39
+ await collectFlows(path_1.default.join(projectDir, 'steps'), lockedData);
58
40
  return lockedData;
59
41
  }
60
42
  catch (error) {
@@ -30,9 +30,10 @@ class Watcher {
30
30
  console.warn(`No step create handler, step skipped`);
31
31
  return;
32
32
  }
33
- const config = await (0, core_1.getStepConfig)(path);
33
+ const config = await (0, core_1.getStepConfig)(path).catch((err) => {
34
+ console.error(err);
35
+ });
34
36
  if (!config) {
35
- console.warn(`No config found in step ${path}, step skipped`);
36
37
  return;
37
38
  }
38
39
  const version = `${(0, crypto_1.randomUUID)()}:${Math.floor(Date.now() / 1000)}`;
@@ -40,14 +41,11 @@ class Watcher {
40
41
  this.stepCreateHandler?.(step);
41
42
  }
42
43
  async onFileChange(path) {
43
- if (!this.stepChangeHandler) {
44
- console.warn(`No step change handler, step skipped`);
45
- return;
46
- }
47
- const config = await (0, core_1.getStepConfig)(path);
44
+ const config = await (0, core_1.getStepConfig)(path).catch((err) => {
45
+ console.error(err);
46
+ });
48
47
  const step = this.findStep(path);
49
48
  if (!step && !config) {
50
- console.warn(`Step ${path} not found, step skipped`);
51
49
  return;
52
50
  }
53
51
  // didn't have a step, but now we have a config
@@ -67,10 +65,6 @@ class Watcher {
67
65
  }
68
66
  }
69
67
  async onFileDelete(path) {
70
- if (!this.stepDeleteHandler) {
71
- console.warn(`No step delete handler, step skipped`);
72
- return;
73
- }
74
68
  const step = this.findStep(path);
75
69
  if (!step) {
76
70
  console.warn(`Step ${path} not found, step skipped`);
@@ -81,9 +75,12 @@ class Watcher {
81
75
  init() {
82
76
  this.watcher = chokidar_1.default
83
77
  .watch(this.dir, { persistent: true, ignoreInitial: true })
84
- .on('add', (path) => this.onFileAdd(path))
85
- .on('change', (path) => this.onFileChange(path))
86
- .on('unlink', (path) => this.onFileDelete(path));
78
+ .on('add', (path) => this.isStepFile(path) && this.onFileAdd(path))
79
+ .on('change', (path) => this.isStepFile(path) && this.onFileChange(path))
80
+ .on('unlink', (path) => this.isStepFile(path) && this.onFileDelete(path));
81
+ }
82
+ isStepFile(path) {
83
+ return /\.step\.[^.]+$/.test(path) && !/\.tsx$/.test(path);
87
84
  }
88
85
  async stop() {
89
86
  if (this.watcher) {
package/package.json CHANGED
@@ -1,19 +1,21 @@
1
1
  {
2
2
  "name": "motia",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "motia": "dist/src/cli.js"
7
7
  },
8
8
  "dependencies": {
9
9
  "chokidar": "^4.0.3",
10
+ "colors": "^1.4.0",
10
11
  "commander": "^13.0.0",
11
12
  "dotenv": "^16.4.7",
12
13
  "figlet": "^1.8.0",
14
+ "inquirer": "^12.4.1",
13
15
  "ts-node": "^10.9.2",
14
16
  "yaml": "^2.7.0",
15
- "@motiadev/core": "0.0.21",
16
- "@motiadev/workbench": "0.0.21"
17
+ "@motiadev/core": "0.0.23",
18
+ "@motiadev/workbench": "0.0.23"
17
19
  },
18
20
  "devDependencies": {
19
21
  "@types/figlet": "^1.7.0",
@@ -1,20 +1,34 @@
1
1
  #!/bin/bash
2
2
 
3
+ copy_templates() {
4
+ local src_dir="$1"
5
+ local dest_dir="$2"
6
+
7
+ # Create the destination directory if it doesn't exist
8
+ mkdir -p "$dest_dir"
9
+
10
+ # Copy all .txt files while preserving the directory structure
11
+ find "$src_dir" -type f -name "*.txt" | while read -r file; do
12
+ # Get the relative path of the file
13
+ rel_path="${file#$src_dir/}"
14
+ # Create the destination directory for the file
15
+ mkdir -p "$dest_dir/$(dirname "$rel_path")"
16
+ # Copy the file to the destination directory
17
+ cp "$file" "$dest_dir/$rel_path"
18
+ done
19
+
20
+ echo "All .txt files from $src_dir have been copied successfully to $dest_dir."
21
+ }
22
+
3
23
  # Define source and destination directories
4
- SRC_DIR="$(dirname "$0")/../src/create/templates"
5
- DEST_DIR="$(dirname "$0")/../dist/src/create/templates"
6
-
7
- # Create the destination directory if it doesn't exist
8
- mkdir -p "$DEST_DIR"
9
-
10
- # Copy all .txt files while preserving the directory structure
11
- find "$SRC_DIR" -type f -name "*.txt" | while read -r file; do
12
- # Get the relative path of the file
13
- rel_path="${file#$SRC_DIR/}"
14
- # Create the destination directory for the file
15
- mkdir -p "$DEST_DIR/$(dirname "$rel_path")"
16
- # Copy the file to the destination directory
17
- cp "$file" "$DEST_DIR/$rel_path"
18
- done
19
-
20
- echo "All .txt files have been copied successfully."
24
+ CREATE_TEMPLATES_SRC_DIR="$(dirname "$0")/../src/create/templates"
25
+ CREATE_TEMPLATES_DEST_DIR="$(dirname "$0")/../dist/src/create/templates"
26
+
27
+ # Call the function with the directories
28
+ copy_templates "$CREATE_TEMPLATES_SRC_DIR" "$CREATE_TEMPLATES_DEST_DIR"
29
+
30
+ CREATE_STEP_TEMPLATES_SRC_DIR="$(dirname "$0")/../src/create-step/templates"
31
+ CREATE_STEP_TEMPLATES_DEST_DIR="$(dirname "$0")/../dist/src/create-step/templates"
32
+
33
+ # Call the function with the directories
34
+ copy_templates "$CREATE_STEP_TEMPLATES_SRC_DIR" "$CREATE_STEP_TEMPLATES_DEST_DIR"