@forgehive/forge-cli 0.2.14 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/runner.js +3 -1
  2. package/dist/tasks/auth/add.js +23 -19
  3. package/dist/tasks/auth/list.js +20 -16
  4. package/dist/tasks/auth/load.js +19 -15
  5. package/dist/tasks/auth/loadCurrent.js +13 -9
  6. package/dist/tasks/auth/remove.js +30 -26
  7. package/dist/tasks/auth/switch.js +19 -15
  8. package/dist/tasks/bundle/create.js +16 -12
  9. package/dist/tasks/bundle/fingerprint.d.ts +36 -0
  10. package/dist/tasks/bundle/fingerprint.js +164 -0
  11. package/dist/tasks/bundle/load.js +9 -5
  12. package/dist/tasks/bundle/zip.js +49 -45
  13. package/dist/tasks/conf/info.js +23 -19
  14. package/dist/tasks/conf/load.js +8 -4
  15. package/dist/tasks/fixture/download.js +40 -36
  16. package/dist/tasks/init.js +35 -31
  17. package/dist/tasks/runner/bundle.js +34 -30
  18. package/dist/tasks/runner/create.js +28 -24
  19. package/dist/tasks/runner/remove.js +22 -18
  20. package/dist/tasks/task/createTask.js +35 -28
  21. package/dist/tasks/task/describe.js +85 -81
  22. package/dist/tasks/task/download.js +63 -59
  23. package/dist/tasks/task/fingerprint.d.ts +26 -0
  24. package/dist/tasks/task/fingerprint.js +87 -0
  25. package/dist/tasks/task/list.js +27 -23
  26. package/dist/tasks/task/publish.js +72 -68
  27. package/dist/tasks/task/remove.js +24 -20
  28. package/dist/tasks/task/replay.js +94 -90
  29. package/dist/tasks/task/run.js +78 -79
  30. package/dist/test/tasks/create.test.js +6 -5
  31. package/dist/utils/taskAnalysis.d.ts +21 -0
  32. package/dist/utils/taskAnalysis.js +380 -0
  33. package/forge.json +12 -0
  34. package/logs/task:fingerprint.log +10 -0
  35. package/package.json +7 -7
  36. package/specs/fingerprint.md +380 -0
  37. package/src/runner.ts +3 -1
  38. package/src/tasks/README.md +13 -13
  39. package/src/tasks/auth/add.ts +3 -3
  40. package/src/tasks/auth/list.ts +3 -3
  41. package/src/tasks/auth/load.ts +3 -3
  42. package/src/tasks/auth/loadCurrent.ts +3 -3
  43. package/src/tasks/auth/remove.ts +3 -3
  44. package/src/tasks/auth/switch.ts +3 -3
  45. package/src/tasks/bundle/README.md +7 -7
  46. package/src/tasks/bundle/create.ts +4 -4
  47. package/src/tasks/bundle/fingerprint.ts +218 -0
  48. package/src/tasks/bundle/load.ts +4 -4
  49. package/src/tasks/bundle/zip.ts +3 -3
  50. package/src/tasks/conf/info.ts +3 -3
  51. package/src/tasks/conf/load.ts +3 -3
  52. package/src/tasks/fixture/download.ts +3 -3
  53. package/src/tasks/init.ts +3 -3
  54. package/src/tasks/runner/bundle.ts +3 -3
  55. package/src/tasks/runner/create.ts +3 -3
  56. package/src/tasks/runner/remove.ts +3 -3
  57. package/src/tasks/task/createTask.ts +10 -7
  58. package/src/tasks/task/describe.ts +3 -3
  59. package/src/tasks/task/download.ts +3 -3
  60. package/src/tasks/task/fingerprint.ts +107 -0
  61. package/src/tasks/task/list.ts +3 -3
  62. package/src/tasks/task/publish.ts +3 -3
  63. package/src/tasks/task/remove.ts +3 -3
  64. package/src/tasks/task/replay.ts +3 -3
  65. package/src/tasks/task/run.ts +12 -18
  66. package/src/test/tasks/create.test.ts +9 -9
  67. package/src/utils/taskAnalysis.ts +419 -0
@@ -1,6 +1,7 @@
1
1
  // TASK: run
2
2
  // Run this task with:
3
- // shadow-cli task:run
3
+ // most recursive call on the project
4
+ // forge task:run task:run
4
5
 
5
6
  import path from 'path'
6
7
  import fs from 'fs/promises'
@@ -78,10 +79,10 @@ const boundaries = {
78
79
  }
79
80
  }
80
81
 
81
- export const run = createTask(
82
+ export const run = createTask({
82
83
  schema,
83
84
  boundaries,
84
- async function ({ descriptorName, args }, {
85
+ fn: async function ({ descriptorName, args }, {
85
86
  loadConf,
86
87
  bundleCreate,
87
88
  bundleLoad,
@@ -140,7 +141,7 @@ export const run = createTask(
140
141
  }
141
142
 
142
143
  // Setup record tape
143
- let tape = new RecordTape({
144
+ const tape = new RecordTape({
144
145
  path: logsPath
145
146
  })
146
147
 
@@ -148,19 +149,12 @@ export const run = createTask(
148
149
  try {
149
150
  await tape.load()
150
151
 
151
- // Need to figure out how to handle the log length
152
- // and other options for the RecordTape
153
- // For now, we'll just keep the implementation simple
154
- const maxLogLength = 9
155
- const log = tape.getLog()
152
+ // Maintain a maximum log length by removing old records
153
+ const maxLogLength = 10
156
154
 
157
- if (log.length > maxLogLength) {
158
- const newTape = new RecordTape({
159
- path: logsPath,
160
- log: log.slice(-maxLogLength)
161
- })
162
-
163
- tape = newTape
155
+ // Remove records from the beginning until we're within the limit
156
+ while (tape.getLength() >= maxLogLength) {
157
+ tape.shift()
164
158
  }
165
159
  } catch (_error) {
166
160
  // if the tape is not found, create a new one on saving
@@ -168,7 +162,7 @@ export const run = createTask(
168
162
 
169
163
  // Run the task with provided arguments
170
164
  const [result, error, record] = await task.safeRun(args)
171
- const logItem = tape.push(descriptorName, record, {
165
+ const logItem = tape.push(record, {
172
166
  environment: 'cli'
173
167
  })
174
168
  await tape.save()
@@ -187,4 +181,4 @@ export const run = createTask(
187
181
 
188
182
  return result
189
183
  }
190
- )
184
+ })
@@ -1,5 +1,5 @@
1
1
  import { createTaskCommand } from '../../tasks/task/createTask'
2
- import { createFsFromVolume, Volume } from 'memfs'
2
+ import { createFsFromVolume, Volume, type IFs } from 'memfs'
3
3
  import path from 'path'
4
4
  import { createMockBoundary } from '../testUtils'
5
5
  import { ForgeConf } from '../../tasks/types'
@@ -12,6 +12,7 @@ const expectedContent = `// TASK: newTask
12
12
  import { createTask } from '@forgehive/task'
13
13
  import { Schema } from '@forgehive/schema'
14
14
 
15
+ const name = 'sample:newTask'
15
16
  const description = 'Add task description here'
16
17
 
17
18
  const schema = new Schema({
@@ -24,10 +25,12 @@ const boundaries = {
24
25
  // example: readFile: async (path: string) => fs.readFile(path, 'utf-8')
25
26
  }
26
27
 
27
- export const newTask = createTask(
28
+ export const newTask = createTask({
29
+ name,
30
+ description,
28
31
  schema,
29
32
  boundaries,
30
- async function (argv, boundaries) {
33
+ fn: async function (argv, boundaries) {
31
34
  console.log('input:', argv)
32
35
  console.log('boundaries:', boundaries)
33
36
  // Your task implementation goes here
@@ -35,15 +38,13 @@ export const newTask = createTask(
35
38
 
36
39
  return status
37
40
  }
38
- )
41
+ })
39
42
 
40
- newTask.setDescription(description)
41
43
  `
42
44
 
43
45
  describe('Create task', () => {
44
46
  let volume: InstanceType<typeof Volume>
45
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
- let fs: any
47
+ let fs: IFs
47
48
  let rootDir: string
48
49
 
49
50
  beforeEach(() => {
@@ -96,11 +97,10 @@ describe('Create task', () => {
96
97
 
97
98
  // Read the created task file
98
99
  const fileContent = await fs.promises.readFile(path.join(rootDir, 'src/tasks/sample', 'newTask.ts'), 'utf-8')
99
-
100
100
  expect(fileContent).toBe(expectedContent)
101
101
 
102
102
  // Read the updated forge.json
103
- const forgeContent = await fs.promises.readFile(path.join(rootDir, 'forge.json'), 'utf-8')
103
+ const forgeContent = await fs.promises.readFile(path.join(rootDir, 'forge.json'), 'utf-8') as string
104
104
  const forgeConf = JSON.parse(forgeContent)
105
105
  expect(forgeConf.tasks['sample:newTask']).toBeDefined()
106
106
  })
@@ -0,0 +1,419 @@
1
+ import * as ts from 'typescript'
2
+
3
+ interface TaskLocation {
4
+ file: string
5
+ line: number
6
+ column: number
7
+ }
8
+
9
+ interface SchemaProperty {
10
+ type: string
11
+ optional?: boolean
12
+ default?: string
13
+ }
14
+
15
+ interface InputSchema {
16
+ type: string
17
+ properties: Record<string, SchemaProperty>
18
+ }
19
+
20
+ interface OutputType {
21
+ type: string
22
+ properties?: Record<string, SchemaProperty>
23
+ }
24
+
25
+ interface TaskFingerprint {
26
+ name: string
27
+ description?: string
28
+ location: TaskLocation
29
+ inputSchema: InputSchema
30
+ outputType: OutputType
31
+ boundaries: string[]
32
+ hash: string
33
+ }
34
+
35
+ // Simplified interface for filesystem output (excludes name, location, hash)
36
+ export interface TaskFingerprintOutput {
37
+ description?: string
38
+ inputSchema: InputSchema
39
+ outputType: OutputType
40
+ boundaries: string[]
41
+ }
42
+
43
+ // Hash generation function
44
+ function generateHash(input: string): string {
45
+ let hash = 0
46
+ for (let i = 0; i < input.length; i++) {
47
+ const char = input.charCodeAt(i)
48
+ hash = ((hash << 5) - hash) + char
49
+ hash = hash & hash // Convert to 32-bit integer
50
+ }
51
+ return Math.abs(hash).toString(36)
52
+ }
53
+
54
+ // TypeScript AST analysis function
55
+ function extractTaskFingerprints(sourceCode: string, filePath: string): TaskFingerprint[] {
56
+ const sourceFile = ts.createSourceFile(
57
+ filePath,
58
+ sourceCode,
59
+ ts.ScriptTarget.Latest,
60
+ true
61
+ )
62
+
63
+ const fingerprints: TaskFingerprint[] = []
64
+ let schemaNode: ts.Expression | null = null
65
+ let boundariesNode: ts.Expression | null = null
66
+
67
+ // First pass: find schema and boundaries variable declarations
68
+ function findVariables(node: ts.Node): void {
69
+ if (ts.isVariableStatement(node)) {
70
+ node.declarationList.declarations.forEach(decl => {
71
+ if (ts.isIdentifier(decl.name)) {
72
+ if (decl.name.text === 'schema' && decl.initializer) {
73
+ schemaNode = decl.initializer
74
+ } else if (decl.name.text === 'boundaries' && decl.initializer) {
75
+ boundariesNode = decl.initializer
76
+ }
77
+ }
78
+ })
79
+ }
80
+ ts.forEachChild(node, findVariables)
81
+ }
82
+
83
+ // Second pass: find createTask calls
84
+ function findCreateTask(node: ts.Node): void {
85
+ // Look for createTask calls
86
+ if (ts.isCallExpression(node) &&
87
+ ts.isIdentifier(node.expression) &&
88
+ node.expression.text === 'createTask') {
89
+
90
+ const taskName = extractTaskName(node, sourceFile)
91
+ if (taskName) {
92
+ const fingerprint = analyzeCreateTaskCall(node, sourceFile, filePath, taskName, schemaNode, boundariesNode)
93
+ if (fingerprint) {
94
+ fingerprints.push(fingerprint)
95
+ }
96
+ }
97
+ }
98
+
99
+ // Look for exported createTask assignments
100
+ if (ts.isVariableStatement(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
101
+ node.declarationList.declarations.forEach(decl => {
102
+ if (ts.isVariableDeclaration(decl) &&
103
+ decl.initializer &&
104
+ ts.isCallExpression(decl.initializer) &&
105
+ ts.isIdentifier(decl.initializer.expression) &&
106
+ decl.initializer.expression.text === 'createTask') {
107
+
108
+ const taskName = ts.isIdentifier(decl.name) ? decl.name.text : 'unknown'
109
+ const fingerprint = analyzeCreateTaskCall(decl.initializer, sourceFile, filePath, taskName, schemaNode, boundariesNode)
110
+ if (fingerprint) {
111
+ fingerprints.push(fingerprint)
112
+ }
113
+ }
114
+ })
115
+ }
116
+
117
+ ts.forEachChild(node, findCreateTask)
118
+ }
119
+
120
+ // Execute both passes
121
+ findVariables(sourceFile)
122
+ findCreateTask(sourceFile)
123
+
124
+ return fingerprints
125
+ }
126
+
127
+ function extractTaskName(node: ts.CallExpression, _sourceFile: ts.SourceFile): string | null {
128
+ // Try to find the task name from variable assignment or export
129
+ let parent = node.parent
130
+ while (parent) {
131
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
132
+ return parent.name.text
133
+ }
134
+ parent = parent.parent
135
+ }
136
+ return null
137
+ }
138
+
139
+ function analyzeCreateTaskCall(
140
+ node: ts.CallExpression,
141
+ sourceFile: ts.SourceFile,
142
+ filePath: string,
143
+ taskName: string,
144
+ schemaNode: ts.Expression | null = null,
145
+ boundariesNode: ts.Expression | null = null
146
+ ): TaskFingerprint | null {
147
+ try {
148
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart())
149
+ const args = node.arguments
150
+
151
+ // Use pre-found schema or fall back to argument analysis
152
+ let inputSchema: InputSchema = { type: 'object', properties: {} }
153
+ if (schemaNode) {
154
+ inputSchema = analyzeSchemaArg(schemaNode, sourceFile)
155
+ } else if (args[0]) {
156
+ inputSchema = analyzeSchemaArg(args[0], sourceFile)
157
+ }
158
+
159
+ // Use pre-found boundaries or fall back to argument analysis - simplified to just names
160
+ let boundaries: string[] = []
161
+ if (boundariesNode) {
162
+ boundaries = analyzeBoundariesArg(boundariesNode, sourceFile)
163
+ } else if (args[1]) {
164
+ boundaries = analyzeBoundariesArg(args[1], sourceFile)
165
+ }
166
+
167
+ // Extract function output type with better detection
168
+ let outputType: OutputType = { type: 'unknown' }
169
+ const functionArg = args[2]
170
+
171
+ if (functionArg) {
172
+ if (ts.isFunctionExpression(functionArg) || ts.isArrowFunction(functionArg)) {
173
+ // Better return type extraction
174
+ if (functionArg.type) {
175
+ const typeString = cleanTypeString(functionArg.type.getText(sourceFile))
176
+ outputType = { type: typeString }
177
+ } else {
178
+ // Try to infer from return statements with better object analysis
179
+ outputType = inferDetailedReturnType(functionArg, sourceFile)
180
+ }
181
+ }
182
+ }
183
+
184
+ // Generate hash from task signature
185
+ const hashInput = `${taskName}:${JSON.stringify(inputSchema)}:${JSON.stringify(boundaries)}`
186
+ const hash = generateHash(hashInput)
187
+
188
+ return {
189
+ name: taskName,
190
+ location: {
191
+ file: filePath,
192
+ line: position.line + 1,
193
+ column: position.character + 1
194
+ },
195
+ inputSchema,
196
+ outputType,
197
+ boundaries,
198
+ hash
199
+ }
200
+ } catch (error) {
201
+ console.warn(`Failed to analyze createTask call for ${taskName}:`, error)
202
+ return null
203
+ }
204
+ }
205
+
206
+ // Enhanced return type inference with detailed object analysis
207
+ function inferDetailedReturnType(func: ts.FunctionExpression | ts.ArrowFunction, _sourceFile: ts.SourceFile): OutputType {
208
+ let returnType: OutputType = { type: 'unknown' }
209
+
210
+ function visitReturnStatements(node: ts.Node): void {
211
+ if (ts.isReturnStatement(node) && node.expression) {
212
+ if (ts.isObjectLiteralExpression(node.expression)) {
213
+ // Analyze object literal properties
214
+ const properties: Record<string, SchemaProperty> = {}
215
+ node.expression.properties.forEach(prop => {
216
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
217
+ // Handle explicit property assignments: { propName: value }
218
+ const propName = prop.name.text
219
+ let propType = 'unknown'
220
+
221
+ // Try to infer property type from the initializer
222
+ if (ts.isStringLiteral(prop.initializer)) {
223
+ propType = 'string'
224
+ } else if (ts.isNumericLiteral(prop.initializer)) {
225
+ propType = 'number'
226
+ } else if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
227
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
228
+ propType = 'boolean'
229
+ } else if (ts.isIdentifier(prop.initializer)) {
230
+ // Variable reference - try to infer from name
231
+ const varName = prop.initializer.text
232
+ propType = inferTypeFromVariableName(varName)
233
+ } else if (ts.isPropertyAccessExpression(prop.initializer)) {
234
+ // Property access like response.handler
235
+ propType = 'unknown'
236
+ }
237
+
238
+ properties[propName] = { type: propType }
239
+ } else if (ts.isShorthandPropertyAssignment(prop)) {
240
+ // Handle shorthand properties: { propName } (equivalent to { propName: propName })
241
+ const propName = prop.name.text
242
+ const propType = inferTypeFromVariableName(propName)
243
+ properties[propName] = { type: propType }
244
+ }
245
+ })
246
+
247
+ if (Object.keys(properties).length > 0) {
248
+ returnType = {
249
+ type: 'object',
250
+ properties
251
+ }
252
+ } else {
253
+ returnType = { type: 'object' }
254
+ }
255
+ } else if (ts.isStringLiteral(node.expression)) {
256
+ returnType = { type: 'string' }
257
+ } else if (ts.isNumericLiteral(node.expression)) {
258
+ returnType = { type: 'number' }
259
+ } else if (node.expression.kind === ts.SyntaxKind.TrueKeyword ||
260
+ node.expression.kind === ts.SyntaxKind.FalseKeyword) {
261
+ returnType = { type: 'boolean' }
262
+ } else if (ts.isIdentifier(node.expression)) {
263
+ // Single variable return
264
+ const varType = inferTypeFromVariableName(node.expression.text)
265
+ returnType = { type: varType }
266
+ }
267
+ }
268
+ ts.forEachChild(node, visitReturnStatements)
269
+ }
270
+
271
+ if (func.body) {
272
+ visitReturnStatements(func.body)
273
+ }
274
+
275
+ return returnType
276
+ }
277
+
278
+ // Helper function to infer types from variable names
279
+ function inferTypeFromVariableName(varName: string): string {
280
+ // Common patterns for type inference based on variable names
281
+ if (varName.includes('path') || varName.includes('Path') ||
282
+ varName.includes('name') || varName.includes('Name') ||
283
+ varName.includes('descriptor') || varName.includes('Descriptor') ||
284
+ varName.includes('fileName') || varName.includes('handler') ||
285
+ varName.includes('url') || varName.includes('id') ||
286
+ varName.includes('uuid') || varName.includes('token')) {
287
+ return 'string'
288
+ } else if (varName.includes('count') || varName.includes('Count') ||
289
+ varName.includes('size') || varName.includes('Size') ||
290
+ varName.includes('length') || varName.includes('Length') ||
291
+ varName.includes('index') || varName.includes('Index')) {
292
+ return 'number'
293
+ } else if (varName.includes('is') || varName.includes('has') ||
294
+ varName.includes('can') || varName.includes('should') ||
295
+ varName.includes('enabled') || varName.includes('success')) {
296
+ return 'boolean'
297
+ } else if (varName.includes('config') || varName.includes('Config') ||
298
+ varName.includes('options') || varName.includes('Options') ||
299
+ varName.includes('data') || varName.includes('result') ||
300
+ varName.includes('response') || varName.includes('error')) {
301
+ return 'unknown'
302
+ }
303
+
304
+ return 'unknown'
305
+ }
306
+
307
+ // Clean up type strings (remove Promise wrappers for boundaries)
308
+ function cleanTypeString(typeString: string): string {
309
+ // Remove Promise wrapper for boundary functions
310
+ const promiseMatch = typeString.match(/Promise<(.+)>/)
311
+ if (promiseMatch) {
312
+ return promiseMatch[1]
313
+ }
314
+ return typeString
315
+ }
316
+
317
+ function analyzeSchemaArg(node: ts.Expression, sourceFile: ts.SourceFile): InputSchema {
318
+ // Handle variable references (e.g., when schema is defined as const schema = ...)
319
+ if (ts.isIdentifier(node) && node.text === 'schema') {
320
+ // This case is now handled by pre-finding the schema node
321
+ return { type: 'object', properties: {} }
322
+ }
323
+
324
+ // Handle direct Schema constructor calls
325
+ if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'Schema') {
326
+ const arg = node.arguments?.[0]
327
+ if (arg && ts.isObjectLiteralExpression(arg)) {
328
+ const properties: Record<string, SchemaProperty> = {}
329
+ arg.properties.forEach(prop => {
330
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
331
+ const propName = prop.name.text
332
+ const propValue = analyzeSchemaProp(prop.initializer, sourceFile)
333
+ properties[propName] = propValue
334
+ }
335
+ })
336
+ return { type: 'object', properties }
337
+ }
338
+ }
339
+ return { type: 'object', properties: {} }
340
+ }
341
+
342
+ // Enhanced schema property analysis
343
+ function analyzeSchemaProp(node: ts.Expression, _sourceFile: ts.SourceFile): SchemaProperty {
344
+ // Analyze Schema.string(), Schema.number(), etc.
345
+ if (ts.isCallExpression(node)) {
346
+ if (ts.isPropertyAccessExpression(node.expression) &&
347
+ ts.isIdentifier(node.expression.expression) &&
348
+ node.expression.expression.text === 'Schema') {
349
+
350
+ const methodName = node.expression.name.text
351
+ let baseType: SchemaProperty = { type: getSchemaTypeFromMethod(methodName) }
352
+
353
+ // Check for chained methods like .optional() or .default()
354
+ let current = node
355
+ while (current.parent && ts.isCallExpression(current.parent)) {
356
+ current = current.parent
357
+ if (ts.isPropertyAccessExpression(current.expression)) {
358
+ const chainedMethod = current.expression.name.text
359
+ if (chainedMethod === 'optional') {
360
+ baseType = { ...baseType, optional: true }
361
+ } else if (chainedMethod === 'default' && current.arguments[0]) {
362
+ baseType = { ...baseType, default: current.arguments[0].getText() }
363
+ }
364
+ }
365
+ }
366
+
367
+ return baseType
368
+ }
369
+ }
370
+ return { type: 'unknown' }
371
+ }
372
+
373
+ function getSchemaTypeFromMethod(methodName: string): string {
374
+ const typeMap: Record<string, string> = {
375
+ string: 'string',
376
+ number: 'number',
377
+ boolean: 'boolean',
378
+ array: 'array',
379
+ object: 'object'
380
+ }
381
+ return typeMap[methodName] || 'unknown'
382
+ }
383
+
384
+ function analyzeBoundariesArg(node: ts.Expression, _sourceFile: ts.SourceFile): string[] {
385
+ const boundaries: string[] = []
386
+
387
+ // Handle variable references (e.g., when boundaries is defined as const boundaries = ...)
388
+ if (ts.isIdentifier(node) && node.text === 'boundaries') {
389
+ // This case is now handled by pre-finding the boundaries node
390
+ return []
391
+ }
392
+
393
+ if (ts.isObjectLiteralExpression(node)) {
394
+ node.properties.forEach(prop => {
395
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
396
+ const boundaryName = prop.name.text
397
+ boundaries.push(boundaryName)
398
+ }
399
+ })
400
+ }
401
+
402
+ return boundaries
403
+ }
404
+
405
+ // Export the core analysis function for reuse
406
+ export function analyzeTaskFile(sourceCode: string, filePath: string, _expectedTaskName?: string): TaskFingerprintOutput | null {
407
+ const taskFingerprint = extractTaskFingerprints(sourceCode, filePath)[0]
408
+ if (!taskFingerprint) {
409
+ return null
410
+ }
411
+
412
+ // Return simplified output without name, location, hash
413
+ return {
414
+ description: taskFingerprint.description,
415
+ inputSchema: taskFingerprint.inputSchema,
416
+ outputType: taskFingerprint.outputType,
417
+ boundaries: taskFingerprint.boundaries
418
+ }
419
+ }