@outputai/core 0.1.12-next.ecd4dda.0 → 0.1.13-next.04243eb.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.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { copyFileSync, mkdirSync, globSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+
5
+ const srcDir = 'src';
6
+ const destDir = 'dist';
7
+ const matchers = [ '**/*.prompt', '**/*.yml.enc', '**/*.key', '**/*.md' ];
8
+
9
+ for ( const pattern of matchers ) {
10
+ for ( const file of globSync( pattern, { cwd: srcDir } ) ) {
11
+ const src = join( srcDir, file );
12
+ const dest = join( destDir, file );
13
+ mkdirSync( dirname( dest ), { recursive: true } );
14
+ copyFileSync( src, dest );
15
+ }
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.1.12-next.ecd4dda.0",
3
+ "version": "0.1.13-next.04243eb.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -28,10 +28,11 @@
28
28
  "bin": {
29
29
  "output-worker": "./bin/worker.sh",
30
30
  "outputai": "./bin/worker.sh",
31
- "output-healthcheck": "./bin/healthcheck.mjs"
31
+ "output-healthcheck": "./bin/healthcheck.mjs",
32
+ "output-copy-assets": "./bin/copy_assets.js"
32
33
  },
33
34
  "dependencies": {
34
- "@aws-sdk/client-s3": "3.1018.0",
35
+ "@aws-sdk/client-s3": "3.1024.0",
35
36
  "@babel/generator": "7.29.1",
36
37
  "@babel/parser": "7.29.2",
37
38
  "@babel/traverse": "7.29.0",
@@ -43,7 +44,7 @@
43
44
  "@temporalio/workflow": "1.15.0",
44
45
  "redis": "5.11.0",
45
46
  "stacktrace-parser": "0.1.11",
46
- "undici": "7.24.6",
47
+ "undici": "8.0.2",
47
48
  "winston": "3.19.0",
48
49
  "zod": "4.3.6"
49
50
  },
@@ -64,6 +64,7 @@ const baseSchema = z.strictObject( {
64
64
  const stepSchema = baseSchema;
65
65
 
66
66
  const workflowSchema = baseSchema.extend( {
67
+ aliases: z.array( z.string().regex( /^[a-z_][a-z0-9_]*$/i ) ).optional().default( [] ),
67
68
  options: baseSchema.shape.options.unwrap().extend( {
68
69
  disableTrace: z.boolean().optional().default( false )
69
70
  } ).optional()
@@ -189,6 +189,37 @@ describe( 'interface/validator', () => {
189
189
  it( 'rejects non-boolean options.disableTrace', () => {
190
190
  expect( () => validateWorkflow( { ...validArgs, options: { disableTrace: 'yes' } } ) ).toThrow( StaticValidationError );
191
191
  } );
192
+
193
+ it( 'passes with valid aliases array', () => {
194
+ expect( () => validateWorkflow( { ...validArgs, aliases: [ 'old_name', 'legacy_v1' ] } ) ).not.toThrow();
195
+ } );
196
+
197
+ it( 'passes with empty aliases array', () => {
198
+ expect( () => validateWorkflow( { ...validArgs, aliases: [] } ) ).not.toThrow();
199
+ } );
200
+
201
+ it( 'passes without aliases (optional)', () => {
202
+ expect( () => validateWorkflow( { ...validArgs } ) ).not.toThrow();
203
+ } );
204
+
205
+ it( 'rejects aliases with invalid name pattern', () => {
206
+ expect( () => validateWorkflow( { ...validArgs, aliases: [ '-bad' ] } ) ).toThrow( StaticValidationError );
207
+ } );
208
+
209
+ it( 'rejects non-array aliases', () => {
210
+ expect( () => validateWorkflow( { ...validArgs, aliases: 'not_array' } ) ).toThrow( StaticValidationError );
211
+ } );
212
+ } );
213
+
214
+ describe( 'aliases rejected on steps and evaluators', () => {
215
+ it( 'validateStep rejects aliases due to strictObject', () => {
216
+ expect( () => validateStep( { ...validArgs, aliases: [ 'alias' ] } ) ).toThrow( StaticValidationError );
217
+ } );
218
+
219
+ it( 'validateEvaluator rejects aliases due to strictObject', () => {
220
+ const { outputSchema, ...evalArgs } = validArgs;
221
+ expect( () => validateEvaluator( { ...evalArgs, aliases: [ 'alias' ] } ) ).toThrow( StaticValidationError );
222
+ } );
192
223
  } );
193
224
 
194
225
  describe( 'validateEvaluator', () => {
@@ -270,4 +270,10 @@ export declare function workflow<
270
270
  outputSchema?: OutputSchema;
271
271
  fn: WorkflowFunction<InputSchema, OutputSchema>;
272
272
  options?: WorkflowOptions;
273
+
274
+ /**
275
+ * Alternative names that resolve to this workflow. Useful when renaming a workflow
276
+ * while maintaining backward compatibility with existing callers.
277
+ */
278
+ aliases?: string[];
273
279
  } ): WorkflowFunctionWrapper<WorkflowFunction<InputSchema, OutputSchema>>;
@@ -22,8 +22,8 @@ const defaultOptions = {
22
22
  disableTrace: false
23
23
  };
24
24
 
25
- export function workflow( { name, description, inputSchema, outputSchema, fn, options = {} } ) {
26
- validateWorkflow( { name, description, inputSchema, outputSchema, fn, options } );
25
+ export function workflow( { name, description, inputSchema, outputSchema, fn, options = {}, aliases = [] } ) {
26
+ validateWorkflow( { name, description, inputSchema, outputSchema, fn, options, aliases } );
27
27
 
28
28
  const { disableTrace, activityOptions } = deepMerge( defaultOptions, options );
29
29
  const steps = proxyActivities( activityOptions );
@@ -123,6 +123,6 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
123
123
  }
124
124
  };
125
125
 
126
- setMetadata( wrapper, { name, description, inputSchema, outputSchema } );
126
+ setMetadata( wrapper, { name, description, inputSchema, outputSchema, aliases } );
127
127
  return wrapper;
128
128
  };
@@ -140,7 +140,7 @@ describe( 'workflow()', () => {
140
140
  const symbols = Object.getOwnPropertySymbols( wf );
141
141
  expect( symbols ).toHaveLength( 1 );
142
142
  const meta = wf[symbols[0]];
143
- expect( meta ).toEqual( { name: 'meta_wf', description: 'Meta workflow', inputSchema, outputSchema } );
143
+ expect( meta ).toEqual( { name: 'meta_wf', description: 'Meta workflow', inputSchema, outputSchema, aliases: [] } );
144
144
  } );
145
145
  } );
146
146
 
@@ -97,6 +97,11 @@ export class CatalogWorkflow extends CatalogEntry {
97
97
  * @type {Array<CatalogActivity>}
98
98
  */
99
99
  activities;
100
+ /**
101
+ * Alternative names that resolve to this workflow.
102
+ * @type {Array<string>}
103
+ */
104
+ aliases;
100
105
 
101
106
  /**
102
107
  * @param {Object} params - Entry parameters.
@@ -106,9 +111,11 @@ export class CatalogWorkflow extends CatalogEntry {
106
111
  * @param {object} [params.outputSchema] - JSON schema describing the produced output.
107
112
  * @param {string} params.path - Absolute path of the entity in the file system.
108
113
  * @param {Array<CatalogActivity>} params.activities - Each activity of this workflow
114
+ * @param {Array<string>} [params.aliases] - Alternative names for this workflow
109
115
  */
110
- constructor( { activities, ...args } ) {
116
+ constructor( { activities, aliases = [], ...args } ) {
111
117
  super( args );
112
118
  this.activities = activities;
119
+ this.aliases = aliases;
113
120
  };
114
121
  };
@@ -193,4 +193,27 @@ describe( 'createCatalog', () => {
193
193
  expect( workflows[0].path ).toBe( '/flows/flow1/workflow.js' );
194
194
  expect( workflows[1].path ).toBe( '/flows/flow2/workflow.js' );
195
195
  } );
196
+
197
+ it( 'includes aliases in catalog workflow entries', async () => {
198
+ const { createCatalog } = await import( './index.js' );
199
+
200
+ const workflows = [
201
+ {
202
+ name: 'flow1',
203
+ path: '/flows/flow1/workflow.js',
204
+ description: 'desc-flow1',
205
+ aliases: [ 'flow1_old', 'flow1_legacy' ]
206
+ },
207
+ {
208
+ name: 'flow2',
209
+ path: '/flows/flow2/workflow.js',
210
+ description: 'desc-flow2'
211
+ }
212
+ ];
213
+
214
+ const catalog = createCatalog( { workflows, activities: {} } );
215
+
216
+ expect( catalog.workflows[0].aliases ).toEqual( [ 'flow1_old', 'flow1_legacy' ] );
217
+ expect( catalog.workflows[1].aliases ).toEqual( [] );
218
+ } );
196
219
  } );
@@ -25,7 +25,13 @@ import { messageBus } from '#bus';
25
25
  export class ActivityExecutionInterceptor {
26
26
  constructor( { activities, workflows } ) {
27
27
  this.activities = activities;
28
- this.workflowsMap = workflows.reduce( ( map, w ) => map.set( w.name, w ), new Map() );
28
+ this.workflowsMap = workflows.reduce( ( map, w ) => {
29
+ map.set( w.name, w );
30
+ for ( const alias of w.aliases ?? [] ) {
31
+ map.set( alias, w );
32
+ }
33
+ return map;
34
+ }, new Map() );
29
35
  };
30
36
 
31
37
  async execute( input, next ) {
@@ -33,11 +39,17 @@ export class ActivityExecutionInterceptor {
33
39
  const { workflowExecution: { workflowId }, activityId: id, activityType: name, workflowType: workflowName } = Context.current().info;
34
40
  const { executionContext } = headersToObject( input.headers );
35
41
  const { type: kind } = this.activities?.[name]?.[METADATA_ACCESS_SYMBOL];
36
- const workflowFilename = this.workflowsMap.get( workflowName ).path;
37
42
 
38
43
  messageBus.emit( BusEventType.ACTIVITY_START, { id, name, kind, workflowId, workflowName } );
39
44
  Tracing.addEventStart( { id, name, kind, parentId: workflowId, details: input.args[0], executionContext } );
40
45
 
46
+ const workflowEntry = this.workflowsMap.get( workflowName );
47
+ if ( !workflowEntry ) {
48
+ const availableWorkflows = [ ...this.workflowsMap.keys() ].join( ', ' );
49
+ throw new Error( `Activity interceptor: workflow "${workflowName}" not found in workflowsMap. Available: [${availableWorkflows}]` );
50
+ }
51
+ const workflowFilename = workflowEntry.path;
52
+
41
53
  const intervals = { heartbeat: null };
42
54
  try {
43
55
  // Sends heartbeat to communicate that activity is still alive
@@ -191,6 +191,29 @@ describe( 'ActivityExecutionInterceptor', () => {
191
191
  expect( heartbeatMock ).not.toHaveBeenCalled();
192
192
  } );
193
193
 
194
+ it( 'resolves workflow alias in workflowsMap', async () => {
195
+ const { ActivityExecutionInterceptor } = await import( './activity.js' );
196
+ const workflows = [ { name: 'myWorkflow', path: '/workflows/myWorkflow.js', aliases: [ 'myWorkflowOld' ] } ];
197
+ const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows } );
198
+
199
+ // Override context to use alias as workflowType
200
+ contextInfoMock.workflowType = 'myWorkflowOld';
201
+ const next = vi.fn().mockResolvedValue( { result: 'ok' } );
202
+
203
+ const promise = interceptor.execute( makeInput(), next );
204
+ vi.advanceTimersByTime( 0 );
205
+ await promise;
206
+
207
+ // Should resolve to the correct path despite using the alias
208
+ expect( runWithContextMock ).toHaveBeenCalledWith(
209
+ expect.any( Function ),
210
+ expect.objectContaining( { workflowFilename: '/workflows/myWorkflow.js' } )
211
+ );
212
+
213
+ // Restore for other tests
214
+ contextInfoMock.workflowType = 'myWorkflow';
215
+ } );
216
+
194
217
  it( 'does not heartbeat when OUTPUT_ACTIVITY_HEARTBEAT_ENABLED is false', async () => {
195
218
  vi.stubEnv( 'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED', 'false' );
196
219
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
@@ -132,6 +132,40 @@ export async function loadHooks( rootDir ) {
132
132
  }
133
133
  };
134
134
 
135
+ /**
136
+ * Validates that all workflow names and aliases are unique across the project.
137
+ *
138
+ * @param {object[]} workflows
139
+ * @throws {Error} If any alias conflicts with a workflow name or another alias
140
+ */
141
+ function validateWorkflowNames( workflows ) {
142
+ const allNames = new Map();
143
+
144
+ // Register primary names (case-insensitive to prevent confusing collisions)
145
+ for ( const { name } of workflows ) {
146
+ allNames.set( name.toLowerCase(), `workflow "${name}"` );
147
+ }
148
+
149
+ // Check the reserved catalog name
150
+ allNames.set( WORKFLOW_CATALOG.toLowerCase(), 'system workflow "$catalog"' );
151
+
152
+ // Check aliases against all names
153
+ for ( const { name, aliases = [] } of workflows ) {
154
+ const lowerCaseName = name.toLowerCase();
155
+ for ( const alias of aliases ) {
156
+ const lowerAliasName = alias.toLowerCase();
157
+ if ( lowerAliasName === lowerCaseName ) {
158
+ throw new Error( `Workflow "${name}" has an alias identical to its own name` );
159
+ }
160
+ const conflict = allNames.get( lowerAliasName );
161
+ if ( conflict ) {
162
+ throw new Error( `Alias "${alias}" on workflow "${name}" conflicts with ${conflict}` );
163
+ }
164
+ allNames.set( lowerAliasName, `alias "${alias}" on workflow "${name}"` );
165
+ }
166
+ }
167
+ }
168
+
135
169
  /**
136
170
  * Creates a temporary index file importing all workflows for Temporal.
137
171
  *
@@ -139,11 +173,16 @@ export async function loadHooks( rootDir ) {
139
173
  * @returns
140
174
  */
141
175
  export function createWorkflowsEntryPoint( workflows ) {
176
+ validateWorkflowNames( workflows );
177
+
142
178
  const path = join( __dirname, 'temp', WORKFLOWS_INDEX_FILENAME );
143
179
 
144
180
  // default system catalog workflow
145
181
  const catalog = { name: WORKFLOW_CATALOG, path: join( __dirname, './catalog_workflow/workflow.js' ) };
146
- const content = [ ... workflows, catalog ].map( ( { name, path } ) => `export { default as ${name} } from '${path}';` ).join( EOL );
182
+ const aliasExports = workflows.flatMap( ( { aliases = [], path } ) =>
183
+ aliases.map( alias => ( { name: alias, path } ) )
184
+ );
185
+ const content = [ ...workflows, ...aliasExports, catalog ].map( ( { name, path } ) => `export { default as ${name} } from '${path}';` ).join( EOL );
147
186
 
148
187
  mkdirSync( dirname( path ), { recursive: true } );
149
188
  writeFileSync( path, content, 'utf-8' );
@@ -111,6 +111,65 @@ describe( 'worker/loader', () => {
111
111
  expect( fsMocks.mkdirSync ).toHaveBeenCalledTimes( 1 );
112
112
  } );
113
113
 
114
+ it( 'createWorkflowsEntryPoint generates alias exports', async () => {
115
+ const { createWorkflowsEntryPoint } = await import( './loader.js' );
116
+
117
+ const workflows = [ { name: 'W', path: '/abs/wf.js', aliases: [ 'W_old', 'W_legacy' ] } ];
118
+ createWorkflowsEntryPoint( workflows );
119
+
120
+ const [ , contents ] = fsMocks.writeFileSync.mock.calls[0];
121
+ expect( contents ).toContain( 'export { default as W } from \'/abs/wf.js\';' );
122
+ expect( contents ).toContain( 'export { default as W_old } from \'/abs/wf.js\';' );
123
+ expect( contents ).toContain( 'export { default as W_legacy } from \'/abs/wf.js\';' );
124
+ } );
125
+
126
+ it( 'createWorkflowsEntryPoint throws on alias conflicting with primary name', async () => {
127
+ const { createWorkflowsEntryPoint } = await import( './loader.js' );
128
+
129
+ const workflows = [
130
+ { name: 'alpha', path: '/a.js', aliases: [] },
131
+ { name: 'beta', path: '/b.js', aliases: [ 'alpha' ] }
132
+ ];
133
+ expect( () => createWorkflowsEntryPoint( workflows ) ).toThrow( /Alias "alpha" on workflow "beta" conflicts with workflow "alpha"/ );
134
+ } );
135
+
136
+ it( 'createWorkflowsEntryPoint throws on alias conflicting with another alias', async () => {
137
+ const { createWorkflowsEntryPoint } = await import( './loader.js' );
138
+
139
+ const workflows = [
140
+ { name: 'alpha', path: '/a.js', aliases: [ 'shared_alias' ] },
141
+ { name: 'beta', path: '/b.js', aliases: [ 'shared_alias' ] }
142
+ ];
143
+ expect( () => createWorkflowsEntryPoint( workflows ) ).toThrow( /Alias "shared_alias" on workflow "beta" conflicts with/ );
144
+ } );
145
+
146
+ it( 'createWorkflowsEntryPoint throws on alias identical to own name', async () => {
147
+ const { createWorkflowsEntryPoint } = await import( './loader.js' );
148
+
149
+ const workflows = [ { name: 'alpha', path: '/a.js', aliases: [ 'alpha' ] } ];
150
+ expect( () => createWorkflowsEntryPoint( workflows ) ).toThrow( /Workflow "alpha" has an alias identical to its own name/ );
151
+ } );
152
+
153
+ it( 'createWorkflowsEntryPoint catches case-insensitive alias collision with primary name', async () => {
154
+ const { createWorkflowsEntryPoint } = await import( './loader.js' );
155
+
156
+ const workflows = [
157
+ { name: 'Alpha', path: '/a.js', aliases: [] },
158
+ { name: 'beta', path: '/b.js', aliases: [ 'alpha' ] }
159
+ ];
160
+ expect( () => createWorkflowsEntryPoint( workflows ) ).toThrow( /Alias "alpha" on workflow "beta" conflicts with workflow "Alpha"/ );
161
+ } );
162
+
163
+ it( 'createWorkflowsEntryPoint catches case-insensitive alias-to-alias collision', async () => {
164
+ const { createWorkflowsEntryPoint } = await import( './loader.js' );
165
+
166
+ const workflows = [
167
+ { name: 'alpha', path: '/a.js', aliases: [ 'Legacy' ] },
168
+ { name: 'beta', path: '/b.js', aliases: [ 'legacy' ] }
169
+ ];
170
+ expect( () => createWorkflowsEntryPoint( workflows ) ).toThrow( /Alias "legacy" on workflow "beta" conflicts with/ );
171
+ } );
172
+
114
173
  it( 'loadActivities uses folder-based matchers for steps/evaluators and shared', async () => {
115
174
  const { loadActivities } = await import( './loader.js' );
116
175
  // First call (workflow dir): no results