@forgehive/forge-cli 0.2.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/runner.js +15 -7
  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.d.ts +35 -0
  22. package/dist/tasks/task/describe.js +130 -0
  23. package/dist/tasks/task/download.js +63 -59
  24. package/dist/tasks/task/fingerprint.d.ts +26 -0
  25. package/dist/tasks/task/fingerprint.js +87 -0
  26. package/dist/tasks/task/list.d.ts +12 -0
  27. package/dist/tasks/task/list.js +46 -0
  28. package/dist/tasks/task/publish.js +72 -68
  29. package/dist/tasks/task/remove.js +24 -20
  30. package/dist/tasks/task/replay.js +94 -90
  31. package/dist/tasks/task/run.js +84 -79
  32. package/dist/test/setup.d.ts +0 -0
  33. package/dist/test/setup.js +14 -0
  34. package/dist/test/tasks/create.test.js +6 -5
  35. package/dist/utils/taskAnalysis.d.ts +21 -0
  36. package/dist/utils/taskAnalysis.js +380 -0
  37. package/forge.json +20 -0
  38. package/jest.config.js +2 -1
  39. package/logs/task:fingerprint.log +10 -0
  40. package/package.json +8 -8
  41. package/specs/fingerprint.md +380 -0
  42. package/src/runner.ts +14 -5
  43. package/src/tasks/README.md +13 -13
  44. package/src/tasks/auth/add.ts +3 -3
  45. package/src/tasks/auth/list.ts +3 -3
  46. package/src/tasks/auth/load.ts +3 -3
  47. package/src/tasks/auth/loadCurrent.ts +3 -3
  48. package/src/tasks/auth/remove.ts +3 -3
  49. package/src/tasks/auth/switch.ts +3 -3
  50. package/src/tasks/bundle/README.md +7 -7
  51. package/src/tasks/bundle/create.ts +4 -4
  52. package/src/tasks/bundle/fingerprint.ts +218 -0
  53. package/src/tasks/bundle/load.ts +4 -4
  54. package/src/tasks/bundle/zip.ts +3 -3
  55. package/src/tasks/conf/info.ts +3 -3
  56. package/src/tasks/conf/load.ts +3 -3
  57. package/src/tasks/fixture/download.ts +3 -3
  58. package/src/tasks/init.ts +3 -3
  59. package/src/tasks/runner/bundle.ts +3 -3
  60. package/src/tasks/runner/create.ts +3 -3
  61. package/src/tasks/runner/remove.ts +3 -3
  62. package/src/tasks/task/createTask.ts +10 -7
  63. package/src/tasks/task/describe.ts +148 -0
  64. package/src/tasks/task/download.ts +3 -3
  65. package/src/tasks/task/fingerprint.ts +107 -0
  66. package/src/tasks/task/list.ts +58 -0
  67. package/src/tasks/task/publish.ts +3 -3
  68. package/src/tasks/task/remove.ts +3 -3
  69. package/src/tasks/task/replay.ts +3 -3
  70. package/src/tasks/task/run.ts +5 -4
  71. package/src/test/setup.ts +14 -0
  72. package/src/test/tasks/create.test.ts +9 -9
  73. package/src/utils/taskAnalysis.ts +419 -0
  74. package/dist/taskAdapter.d.ts +0 -34
  75. package/dist/taskAdapter.js +0 -85
  76. package/dist/templates/README.md +0 -23
  77. package/dist/templates/task.hbs +0 -27
  78. package/dist/test/utils.d.ts +0 -2
  79. package/dist/test/utils.js +0 -17
  80. package/logs/auth:list.log +0 -4
  81. package/logs/auth:load.log +0 -2
  82. package/logs/auth:loadCurrent.log +0 -1
  83. package/logs/conf:info.log +0 -2
  84. package/logs/runner:create.log +0 -4
@@ -0,0 +1,10 @@
1
+ {"name":"task:fingerprint","type":"success","input":{"descriptorName":"task:download"},"output":{"taskName":"download","fingerprint":{"name":"download","location":{"file":"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/download.ts","line":93,"column":25},"inputSchema":{"type":"object","properties":{"descriptorName":{"type":"string"},"uuid":{"type":"string"}}},"outputType":{"type":"object","properties":{"taskPath":{"type":"string"},"fileName":{"type":"string"},"descriptor":{"type":"string"},"handler":{"type":"any"}}},"boundaries":{"loadCurrentProfile":{"inputTypes":["any"],"outputType":"any"},"downloadTask":{"inputTypes":["uuid: string","profile: Profile"],"outputType":"any"},"loadConf":{"inputTypes":["any"],"outputType":"any"},"getCwd":{"inputTypes":[],"outputType":"string"},"parseTaskName":{"inputTypes":["taskDescriptor: string"],"outputType":"Promise<{\n descriptor: string\n taskName: string\n fileName: string\n dir?: string\n }>"},"persistTask":{"inputTypes":["dir: string","fileName: string","content: string","cwd: string"],"outputType":"{ path: string }"},"persistConf":{"inputTypes":["forge: ForgeConf","cwd: string"],"outputType":"void"},"checkTaskExists":{"inputTypes":["dir: string","fileName: string"],"outputType":"boolean"}},"hash":"dbhx5s"},"fingerprintFile":"/Users/danielzavaladlvega/.forge/task:download.task-fingerprint.json","hash":"dbhx5s","analysis":{"inputSchemaProps":["descriptorName","uuid"],"boundaryCount":8,"hasDescription":false,"outputType":"object"}},"boundaries":{"getCwd":[{"input":[],"output":"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli","error":null}],"loadConf":[{"input":[{}],"output":{"project":{"name":"forge-cli"},"paths":{"logs":"logs/","fixtures":"fixtures","tasks":"src/tasks/","runners":"src/runners/","tests":"src/tests/"},"infra":{"region":"us-west-2","bucket":""},"tasks":{"bundle:create":{"path":"src/tasks/bundle/create.ts","handler":"create"},"bundle:load":{"path":"src/tasks/bundle/load.ts","handler":"load"},"task:run":{"path":"src/tasks/task/run.ts","handler":"run"},"task:remove":{"path":"src/tasks/task/remove.ts","handler":"remove"},"conf:info":{"path":"src/tasks/conf/info.ts","handler":"info"},"runner:create":{"path":"src/tasks/runner/create.ts","handler":"create"},"runner:remove":{"path":"src/tasks/runner/remove.ts","handler":"remove"},"runner:bundle":{"path":"src/tasks/runner/bundle.ts","handler":"bundle"},"task:publish":{"path":"src/tasks/task/publish.ts","handler":"publish"},"task:download":{"path":"src/tasks/task/download.ts","handler":"download"},"auth:add":{"path":"src/tasks/auth/add.ts","handler":"add"},"auth:load":{"path":"src/tasks/auth/load.ts","handler":"load"},"auth:loadCurrent":{"path":"src/tasks/auth/loadCurrent.ts","handler":"loadCurrent"},"auth:switch":{"path":"src/tasks/auth/switch.ts","handler":"switchProfile"},"auth:list":{"path":"src/tasks/auth/list.ts","handler":"list"},"auth:remove":{"path":"src/tasks/auth/remove.ts","handler":"remove"},"task:replay":{"path":"src/tasks/task/replay.ts","handler":"replay"},"fixture:download":{"path":"src/tasks/fixture/download.ts","handler":"download"},"bundle:zip":{"path":"src/tasks/bundle/zip.ts","handler":"zip"},"task:list":{"path":"src/tasks/task/list.ts","handler":"list"},"task:describe":{"path":"src/tasks/task/describe.ts","handler":"describe"},"task:fingerprint":{"path":"src/tasks/task/fingerprint.ts","handler":"fingerprint"},"bundle:fingerprint":{"path":"src/tasks/bundle/fingerprint.ts","handler":"fingerprint"}},"runners":{}},"error":null}],"readFile":[{"input":["/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/download.ts"],"output":"// TASK: download\n// Run this task with:\n// forge task:run task:download --descriptorName [name] --uuid [task-uuid]\n\nimport { createTask } from '@forgehive/task'\nimport { Schema } from '@forgehive/schema'\nimport axios from 'axios'\nimport path from 'path'\nimport fs from 'fs/promises'\nimport { camelCase } from '../../utils/camelCase'\nimport { load as loadConf } from '../conf/load'\nimport { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'\nimport { Profile, type ForgeConf } from '../types'\n\nconst schema = new Schema({\n descriptorName: Schema.string(),\n uuid: Schema.string()\n})\n\nconst boundaries = {\n loadCurrentProfile: loadCurrentProfile.asBoundary(),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n downloadTask: async (uuid: string, profile: Profile): Promise<any> => {\n const downloadUrl = `${profile.url}/api/tasks/download`\n\n console.log(`Downloading task from ${downloadUrl}...`)\n\n const authToken = `${profile.apiKey}:${profile.apiSecret}`\n const response = await axios.post(downloadUrl, { uuid }, {\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json'\n }\n })\n\n return response.data\n },\n loadConf: loadConf.asBoundary(),\n getCwd: async (): Promise<string> => {\n return process.cwd()\n },\n parseTaskName: async (taskDescriptor: string): Promise<{\n descriptor: string\n taskName: string\n fileName: string\n dir?: string\n }> => {\n const res: string[] = taskDescriptor.split(':')\n\n if (res.length === 1) {\n return {\n descriptor: `${camelCase(res[0])}`,\n taskName: `${camelCase(res[0])}`,\n fileName: `${camelCase(res[0])}.ts`\n }\n }\n\n return {\n dir: res[0],\n descriptor: `${res[0]}:${camelCase(res[1])}`,\n taskName: `${camelCase(res[1])}`,\n fileName: `${camelCase(res[1])}.ts`\n }\n },\n persistTask: async (dir: string, fileName: string, content: string, cwd: string): Promise<{ path: string }> => {\n const dirPath = path.resolve(cwd, dir)\n const taskPath = path.resolve(dirPath, fileName)\n\n await fs.mkdir(dirPath, { recursive: true })\n await fs.writeFile(taskPath, content, 'utf-8')\n\n\n return {\n path: taskPath.toString()\n }\n },\n persistConf: async (forge: ForgeConf, cwd: string): Promise<void> => {\n const forgePath = path.join(cwd, 'forge.json')\n await fs.writeFile(forgePath, JSON.stringify(forge, null, 2))\n },\n checkTaskExists: async (dir: string, fileName: string): Promise<boolean> => {\n const taskPath = path.resolve(dir, fileName)\n\n try {\n await fs.access(taskPath)\n return true\n } catch {\n return false\n }\n }\n}\n\nexport const download = createTask(\n schema,\n boundaries,\n async function ({ descriptorName, uuid }, {\n downloadTask,\n getCwd,\n parseTaskName,\n persistTask,\n loadConf,\n persistConf,\n checkTaskExists,\n loadCurrentProfile\n }) {\n console.log(`Attempting to download task with descriptor: ${descriptorName} and uuid: ${uuid}`)\n\n // Parse descriptor name to get task details\n const { taskName, fileName, descriptor, dir } = await parseTaskName(descriptorName)\n const profile = await loadCurrentProfile({})\n const cwd = await getCwd()\n const forge = await loadConf({})\n\n let taskPath: string = forge.paths.tasks\n\n if (dir !== undefined) {\n taskPath = path.join(taskPath, dir)\n }\n\n // Check if task already exists\n const taskExists = await checkTaskExists(taskPath, fileName)\n if (taskExists) {\n console.log(`Task ${descriptor} already exists at ${taskPath}/${fileName}`)\n return {\n error: 'Task already exists',\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n // Download from hive api server\n let response\n try {\n response = await downloadTask(uuid, profile)\n } catch (e: unknown) {\n const error = e as { status: number, message: string }\n console.error('Error downloading task:', error.message, error.status)\n\n if (error.status === 404) {\n return {\n error: 'Task not found',\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n return {\n error: 'Failed to download task',\n message: error.message,\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n console.log(`\n ==================================================\n Starting task download!\n Creating: ${taskName}\n Dir: ${dir ?? ''}\n Into: ${taskPath}\n ==================================================\n `)\n\n console.log('Writing task file:', taskPath, fileName)\n console.log('Handler:', response.handler)\n console.log('Source code:', response.sourceCode)\n\n // Persist task with cwd\n await persistTask(taskPath, fileName, response.sourceCode, cwd)\n\n // Update forge.json with the new task\n if (forge.tasks === undefined) {\n forge.tasks = {}\n }\n\n forge.tasks[descriptor] = {\n path: `${taskPath}/${fileName}`,\n handler: response.handler\n }\n\n console.log('Forge:', forge)\n\n await persistConf(forge, cwd)\n\n return {\n taskPath,\n fileName,\n descriptor,\n handler: response.handler\n }\n }\n)\n","error":null}],"writeFile":[{"input":["/Users/danielzavaladlvega/.forge/task:download.task-fingerprint.json","{\n \"taskFingerprint\": {\n \"name\": \"download\",\n \"location\": {\n \"file\": \"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/download.ts\",\n \"line\": 93,\n \"column\": 25\n },\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"descriptorName\": {\n \"type\": \"string\"\n },\n \"uuid\": {\n \"type\": \"string\"\n }\n }\n },\n \"outputType\": {\n \"type\": \"object\",\n \"properties\": {\n \"taskPath\": {\n \"type\": \"string\"\n },\n \"fileName\": {\n \"type\": \"string\"\n },\n \"descriptor\": {\n \"type\": \"string\"\n },\n \"handler\": {\n \"type\": \"any\"\n }\n }\n },\n \"boundaries\": {\n \"loadCurrentProfile\": {\n \"inputTypes\": [\n \"any\"\n ],\n \"outputType\": \"any\"\n },\n \"downloadTask\": {\n \"inputTypes\": [\n \"uuid: string\",\n \"profile: Profile\"\n ],\n \"outputType\": \"any\"\n },\n \"loadConf\": {\n \"inputTypes\": [\n \"any\"\n ],\n \"outputType\": \"any\"\n },\n \"getCwd\": {\n \"inputTypes\": [],\n \"outputType\": \"string\"\n },\n \"parseTaskName\": {\n \"inputTypes\": [\n \"taskDescriptor: string\"\n ],\n \"outputType\": \"Promise<{\\n descriptor: string\\n taskName: string\\n fileName: string\\n dir?: string\\n }>\"\n },\n \"persistTask\": {\n \"inputTypes\": [\n \"dir: string\",\n \"fileName: string\",\n \"content: string\",\n \"cwd: string\"\n ],\n \"outputType\": \"{ path: string }\"\n },\n \"persistConf\": {\n \"inputTypes\": [\n \"forge: ForgeConf\",\n \"cwd: string\"\n ],\n \"outputType\": \"void\"\n },\n \"checkTaskExists\": {\n \"inputTypes\": [\n \"dir: string\",\n \"fileName: string\"\n ],\n \"outputType\": \"boolean\"\n }\n },\n \"hash\": \"dbhx5s\"\n }\n}"],"output":null,"error":null}],"ensureForgeFolder":[{"input":[],"output":"/Users/danielzavaladlvega/.forge","error":null}]},"context":{"environment":"cli"}}
2
+ {"name":"task:fingerprint","type":"success","input":{"descriptorName":"task:download"},"output":{"taskName":"download","fingerprint":{"name":"download","location":{"file":"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/download.ts","line":93,"column":25},"inputSchema":{"type":"object","properties":{"descriptorName":{"type":"string"},"uuid":{"type":"string"}}},"outputType":{"type":"object","properties":{"taskPath":{"type":"string"},"fileName":{"type":"string"},"descriptor":{"type":"string"},"handler":{"type":"any"}}},"boundaries":["loadCurrentProfile","downloadTask","loadConf","getCwd","parseTaskName","persistTask","persistConf","checkTaskExists"],"hash":"8f8oel"},"fingerprintFile":"/Users/danielzavaladlvega/.forge/task:download.task-fingerprint.json","hash":"8f8oel","analysis":{"inputSchemaProps":["descriptorName","uuid"],"boundaryCount":8,"hasDescription":false,"outputType":"object"}},"boundaries":{"getCwd":[{"input":[],"output":"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli","error":null}],"loadConf":[{"input":[{}],"output":{"project":{"name":"forge-cli"},"paths":{"logs":"logs/","fixtures":"fixtures","tasks":"src/tasks/","runners":"src/runners/","tests":"src/tests/"},"infra":{"region":"us-west-2","bucket":""},"tasks":{"bundle:create":{"path":"src/tasks/bundle/create.ts","handler":"create"},"bundle:load":{"path":"src/tasks/bundle/load.ts","handler":"load"},"task:run":{"path":"src/tasks/task/run.ts","handler":"run"},"task:remove":{"path":"src/tasks/task/remove.ts","handler":"remove"},"conf:info":{"path":"src/tasks/conf/info.ts","handler":"info"},"runner:create":{"path":"src/tasks/runner/create.ts","handler":"create"},"runner:remove":{"path":"src/tasks/runner/remove.ts","handler":"remove"},"runner:bundle":{"path":"src/tasks/runner/bundle.ts","handler":"bundle"},"task:publish":{"path":"src/tasks/task/publish.ts","handler":"publish"},"task:download":{"path":"src/tasks/task/download.ts","handler":"download"},"auth:add":{"path":"src/tasks/auth/add.ts","handler":"add"},"auth:load":{"path":"src/tasks/auth/load.ts","handler":"load"},"auth:loadCurrent":{"path":"src/tasks/auth/loadCurrent.ts","handler":"loadCurrent"},"auth:switch":{"path":"src/tasks/auth/switch.ts","handler":"switchProfile"},"auth:list":{"path":"src/tasks/auth/list.ts","handler":"list"},"auth:remove":{"path":"src/tasks/auth/remove.ts","handler":"remove"},"task:replay":{"path":"src/tasks/task/replay.ts","handler":"replay"},"fixture:download":{"path":"src/tasks/fixture/download.ts","handler":"download"},"bundle:zip":{"path":"src/tasks/bundle/zip.ts","handler":"zip"},"task:list":{"path":"src/tasks/task/list.ts","handler":"list"},"task:describe":{"path":"src/tasks/task/describe.ts","handler":"describe"},"task:fingerprint":{"path":"src/tasks/task/fingerprint.ts","handler":"fingerprint"},"bundle:fingerprint":{"path":"src/tasks/bundle/fingerprint.ts","handler":"fingerprint"}},"runners":{}},"error":null}],"readFile":[{"input":["/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/download.ts"],"output":"// TASK: download\n// Run this task with:\n// forge task:run task:download --descriptorName [name] --uuid [task-uuid]\n\nimport { createTask } from '@forgehive/task'\nimport { Schema } from '@forgehive/schema'\nimport axios from 'axios'\nimport path from 'path'\nimport fs from 'fs/promises'\nimport { camelCase } from '../../utils/camelCase'\nimport { load as loadConf } from '../conf/load'\nimport { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'\nimport { Profile, type ForgeConf } from '../types'\n\nconst schema = new Schema({\n descriptorName: Schema.string(),\n uuid: Schema.string()\n})\n\nconst boundaries = {\n loadCurrentProfile: loadCurrentProfile.asBoundary(),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n downloadTask: async (uuid: string, profile: Profile): Promise<any> => {\n const downloadUrl = `${profile.url}/api/tasks/download`\n\n console.log(`Downloading task from ${downloadUrl}...`)\n\n const authToken = `${profile.apiKey}:${profile.apiSecret}`\n const response = await axios.post(downloadUrl, { uuid }, {\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json'\n }\n })\n\n return response.data\n },\n loadConf: loadConf.asBoundary(),\n getCwd: async (): Promise<string> => {\n return process.cwd()\n },\n parseTaskName: async (taskDescriptor: string): Promise<{\n descriptor: string\n taskName: string\n fileName: string\n dir?: string\n }> => {\n const res: string[] = taskDescriptor.split(':')\n\n if (res.length === 1) {\n return {\n descriptor: `${camelCase(res[0])}`,\n taskName: `${camelCase(res[0])}`,\n fileName: `${camelCase(res[0])}.ts`\n }\n }\n\n return {\n dir: res[0],\n descriptor: `${res[0]}:${camelCase(res[1])}`,\n taskName: `${camelCase(res[1])}`,\n fileName: `${camelCase(res[1])}.ts`\n }\n },\n persistTask: async (dir: string, fileName: string, content: string, cwd: string): Promise<{ path: string }> => {\n const dirPath = path.resolve(cwd, dir)\n const taskPath = path.resolve(dirPath, fileName)\n\n await fs.mkdir(dirPath, { recursive: true })\n await fs.writeFile(taskPath, content, 'utf-8')\n\n\n return {\n path: taskPath.toString()\n }\n },\n persistConf: async (forge: ForgeConf, cwd: string): Promise<void> => {\n const forgePath = path.join(cwd, 'forge.json')\n await fs.writeFile(forgePath, JSON.stringify(forge, null, 2))\n },\n checkTaskExists: async (dir: string, fileName: string): Promise<boolean> => {\n const taskPath = path.resolve(dir, fileName)\n\n try {\n await fs.access(taskPath)\n return true\n } catch {\n return false\n }\n }\n}\n\nexport const download = createTask(\n schema,\n boundaries,\n async function ({ descriptorName, uuid }, {\n downloadTask,\n getCwd,\n parseTaskName,\n persistTask,\n loadConf,\n persistConf,\n checkTaskExists,\n loadCurrentProfile\n }) {\n console.log(`Attempting to download task with descriptor: ${descriptorName} and uuid: ${uuid}`)\n\n // Parse descriptor name to get task details\n const { taskName, fileName, descriptor, dir } = await parseTaskName(descriptorName)\n const profile = await loadCurrentProfile({})\n const cwd = await getCwd()\n const forge = await loadConf({})\n\n let taskPath: string = forge.paths.tasks\n\n if (dir !== undefined) {\n taskPath = path.join(taskPath, dir)\n }\n\n // Check if task already exists\n const taskExists = await checkTaskExists(taskPath, fileName)\n if (taskExists) {\n console.log(`Task ${descriptor} already exists at ${taskPath}/${fileName}`)\n return {\n error: 'Task already exists',\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n // Download from hive api server\n let response\n try {\n response = await downloadTask(uuid, profile)\n } catch (e: unknown) {\n const error = e as { status: number, message: string }\n console.error('Error downloading task:', error.message, error.status)\n\n if (error.status === 404) {\n return {\n error: 'Task not found',\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n return {\n error: 'Failed to download task',\n message: error.message,\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n console.log(`\n ==================================================\n Starting task download!\n Creating: ${taskName}\n Dir: ${dir ?? ''}\n Into: ${taskPath}\n ==================================================\n `)\n\n console.log('Writing task file:', taskPath, fileName)\n console.log('Handler:', response.handler)\n console.log('Source code:', response.sourceCode)\n\n // Persist task with cwd\n await persistTask(taskPath, fileName, response.sourceCode, cwd)\n\n // Update forge.json with the new task\n if (forge.tasks === undefined) {\n forge.tasks = {}\n }\n\n forge.tasks[descriptor] = {\n path: `${taskPath}/${fileName}`,\n handler: response.handler\n }\n\n console.log('Forge:', forge)\n\n await persistConf(forge, cwd)\n\n return {\n taskPath,\n fileName,\n descriptor,\n handler: response.handler\n }\n }\n)\n","error":null}],"writeFile":[{"input":["/Users/danielzavaladlvega/.forge/task:download.task-fingerprint.json","{\n \"taskFingerprint\": {\n \"name\": \"download\",\n \"location\": {\n \"file\": \"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/download.ts\",\n \"line\": 93,\n \"column\": 25\n },\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"descriptorName\": {\n \"type\": \"string\"\n },\n \"uuid\": {\n \"type\": \"string\"\n }\n }\n },\n \"outputType\": {\n \"type\": \"object\",\n \"properties\": {\n \"taskPath\": {\n \"type\": \"string\"\n },\n \"fileName\": {\n \"type\": \"string\"\n },\n \"descriptor\": {\n \"type\": \"string\"\n },\n \"handler\": {\n \"type\": \"any\"\n }\n }\n },\n \"boundaries\": [\n \"loadCurrentProfile\",\n \"downloadTask\",\n \"loadConf\",\n \"getCwd\",\n \"parseTaskName\",\n \"persistTask\",\n \"persistConf\",\n \"checkTaskExists\"\n ],\n \"hash\": \"8f8oel\"\n }\n}"],"output":null,"error":null}],"ensureForgeFolder":[{"input":[],"output":"/Users/danielzavaladlvega/.forge","error":null}]},"context":{"environment":"cli"}}
3
+ {"name":"task:fingerprint","type":"success","input":{"descriptorName":"task:download"},"output":{"taskName":"download","fingerprint":{"inputSchema":{"type":"object","properties":{"descriptorName":{"type":"string"},"uuid":{"type":"string"}}},"outputType":{"type":"object","properties":{"taskPath":{"type":"string"},"fileName":{"type":"string"},"descriptor":{"type":"string"},"handler":{"type":"any"}}},"boundaries":["loadCurrentProfile","downloadTask","loadConf","getCwd","parseTaskName","persistTask","persistConf","checkTaskExists"]},"fingerprintFile":"/Users/danielzavaladlvega/.forge/task:download.task-fingerprint.json","hash":"8f8oel","analysis":{"inputSchemaProps":["descriptorName","uuid"],"boundaryCount":8,"hasDescription":false,"outputType":"object"}},"boundaries":{"getCwd":[{"input":[],"output":"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli","error":null}],"loadConf":[{"input":[{}],"output":{"project":{"name":"forge-cli"},"paths":{"logs":"logs/","fixtures":"fixtures","tasks":"src/tasks/","runners":"src/runners/","tests":"src/tests/"},"infra":{"region":"us-west-2","bucket":""},"tasks":{"bundle:create":{"path":"src/tasks/bundle/create.ts","handler":"create"},"bundle:load":{"path":"src/tasks/bundle/load.ts","handler":"load"},"task:run":{"path":"src/tasks/task/run.ts","handler":"run"},"task:remove":{"path":"src/tasks/task/remove.ts","handler":"remove"},"conf:info":{"path":"src/tasks/conf/info.ts","handler":"info"},"runner:create":{"path":"src/tasks/runner/create.ts","handler":"create"},"runner:remove":{"path":"src/tasks/runner/remove.ts","handler":"remove"},"runner:bundle":{"path":"src/tasks/runner/bundle.ts","handler":"bundle"},"task:publish":{"path":"src/tasks/task/publish.ts","handler":"publish"},"task:download":{"path":"src/tasks/task/download.ts","handler":"download"},"auth:add":{"path":"src/tasks/auth/add.ts","handler":"add"},"auth:load":{"path":"src/tasks/auth/load.ts","handler":"load"},"auth:loadCurrent":{"path":"src/tasks/auth/loadCurrent.ts","handler":"loadCurrent"},"auth:switch":{"path":"src/tasks/auth/switch.ts","handler":"switchProfile"},"auth:list":{"path":"src/tasks/auth/list.ts","handler":"list"},"auth:remove":{"path":"src/tasks/auth/remove.ts","handler":"remove"},"task:replay":{"path":"src/tasks/task/replay.ts","handler":"replay"},"fixture:download":{"path":"src/tasks/fixture/download.ts","handler":"download"},"bundle:zip":{"path":"src/tasks/bundle/zip.ts","handler":"zip"},"task:list":{"path":"src/tasks/task/list.ts","handler":"list"},"task:describe":{"path":"src/tasks/task/describe.ts","handler":"describe"},"task:fingerprint":{"path":"src/tasks/task/fingerprint.ts","handler":"fingerprint"},"bundle:fingerprint":{"path":"src/tasks/bundle/fingerprint.ts","handler":"fingerprint"}},"runners":{}},"error":null}],"readFile":[{"input":["/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/download.ts"],"output":"// TASK: download\n// Run this task with:\n// forge task:run task:download --descriptorName [name] --uuid [task-uuid]\n\nimport { createTask } from '@forgehive/task'\nimport { Schema } from '@forgehive/schema'\nimport axios from 'axios'\nimport path from 'path'\nimport fs from 'fs/promises'\nimport { camelCase } from '../../utils/camelCase'\nimport { load as loadConf } from '../conf/load'\nimport { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'\nimport { Profile, type ForgeConf } from '../types'\n\nconst schema = new Schema({\n descriptorName: Schema.string(),\n uuid: Schema.string()\n})\n\nconst boundaries = {\n loadCurrentProfile: loadCurrentProfile.asBoundary(),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n downloadTask: async (uuid: string, profile: Profile): Promise<any> => {\n const downloadUrl = `${profile.url}/api/tasks/download`\n\n console.log(`Downloading task from ${downloadUrl}...`)\n\n const authToken = `${profile.apiKey}:${profile.apiSecret}`\n const response = await axios.post(downloadUrl, { uuid }, {\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json'\n }\n })\n\n return response.data\n },\n loadConf: loadConf.asBoundary(),\n getCwd: async (): Promise<string> => {\n return process.cwd()\n },\n parseTaskName: async (taskDescriptor: string): Promise<{\n descriptor: string\n taskName: string\n fileName: string\n dir?: string\n }> => {\n const res: string[] = taskDescriptor.split(':')\n\n if (res.length === 1) {\n return {\n descriptor: `${camelCase(res[0])}`,\n taskName: `${camelCase(res[0])}`,\n fileName: `${camelCase(res[0])}.ts`\n }\n }\n\n return {\n dir: res[0],\n descriptor: `${res[0]}:${camelCase(res[1])}`,\n taskName: `${camelCase(res[1])}`,\n fileName: `${camelCase(res[1])}.ts`\n }\n },\n persistTask: async (dir: string, fileName: string, content: string, cwd: string): Promise<{ path: string }> => {\n const dirPath = path.resolve(cwd, dir)\n const taskPath = path.resolve(dirPath, fileName)\n\n await fs.mkdir(dirPath, { recursive: true })\n await fs.writeFile(taskPath, content, 'utf-8')\n\n\n return {\n path: taskPath.toString()\n }\n },\n persistConf: async (forge: ForgeConf, cwd: string): Promise<void> => {\n const forgePath = path.join(cwd, 'forge.json')\n await fs.writeFile(forgePath, JSON.stringify(forge, null, 2))\n },\n checkTaskExists: async (dir: string, fileName: string): Promise<boolean> => {\n const taskPath = path.resolve(dir, fileName)\n\n try {\n await fs.access(taskPath)\n return true\n } catch {\n return false\n }\n }\n}\n\nexport const download = createTask(\n schema,\n boundaries,\n async function ({ descriptorName, uuid }, {\n downloadTask,\n getCwd,\n parseTaskName,\n persistTask,\n loadConf,\n persistConf,\n checkTaskExists,\n loadCurrentProfile\n }) {\n console.log(`Attempting to download task with descriptor: ${descriptorName} and uuid: ${uuid}`)\n\n // Parse descriptor name to get task details\n const { taskName, fileName, descriptor, dir } = await parseTaskName(descriptorName)\n const profile = await loadCurrentProfile({})\n const cwd = await getCwd()\n const forge = await loadConf({})\n\n let taskPath: string = forge.paths.tasks\n\n if (dir !== undefined) {\n taskPath = path.join(taskPath, dir)\n }\n\n // Check if task already exists\n const taskExists = await checkTaskExists(taskPath, fileName)\n if (taskExists) {\n console.log(`Task ${descriptor} already exists at ${taskPath}/${fileName}`)\n return {\n error: 'Task already exists',\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n // Download from hive api server\n let response\n try {\n response = await downloadTask(uuid, profile)\n } catch (e: unknown) {\n const error = e as { status: number, message: string }\n console.error('Error downloading task:', error.message, error.status)\n\n if (error.status === 404) {\n return {\n error: 'Task not found',\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n return {\n error: 'Failed to download task',\n message: error.message,\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n console.log(`\n ==================================================\n Starting task download!\n Creating: ${taskName}\n Dir: ${dir ?? ''}\n Into: ${taskPath}\n ==================================================\n `)\n\n console.log('Writing task file:', taskPath, fileName)\n console.log('Handler:', response.handler)\n console.log('Source code:', response.sourceCode)\n\n // Persist task with cwd\n await persistTask(taskPath, fileName, response.sourceCode, cwd)\n\n // Update forge.json with the new task\n if (forge.tasks === undefined) {\n forge.tasks = {}\n }\n\n forge.tasks[descriptor] = {\n path: `${taskPath}/${fileName}`,\n handler: response.handler\n }\n\n console.log('Forge:', forge)\n\n await persistConf(forge, cwd)\n\n return {\n taskPath,\n fileName,\n descriptor,\n handler: response.handler\n }\n }\n)\n","error":null}],"writeFile":[{"input":["/Users/danielzavaladlvega/.forge/task:download.task-fingerprint.json","{\n \"taskFingerprint\": {\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"descriptorName\": {\n \"type\": \"string\"\n },\n \"uuid\": {\n \"type\": \"string\"\n }\n }\n },\n \"outputType\": {\n \"type\": \"object\",\n \"properties\": {\n \"taskPath\": {\n \"type\": \"string\"\n },\n \"fileName\": {\n \"type\": \"string\"\n },\n \"descriptor\": {\n \"type\": \"string\"\n },\n \"handler\": {\n \"type\": \"any\"\n }\n }\n },\n \"boundaries\": [\n \"loadCurrentProfile\",\n \"downloadTask\",\n \"loadConf\",\n \"getCwd\",\n \"parseTaskName\",\n \"persistTask\",\n \"persistConf\",\n \"checkTaskExists\"\n ]\n }\n}"],"output":null,"error":null}],"ensureForgeFolder":[{"input":[],"output":"/Users/danielzavaladlvega/.forge","error":null}]},"context":{"environment":"cli"}}
4
+ {"name":"task:fingerprint","type":"error","input":{"descriptorName":"task:download"},"error":"Invalid input on: path: Required","boundaries":{"getCwd":[],"readFile":[],"writeFile":[],"ensureForgeFolder":[]},"context":{"environment":"cli"}}
5
+ {"name":"task:fingerprint","type":"error","input":{"descriptorName":"task:download"},"error":"Invalid input on: path: Required","boundaries":{"getCwd":[],"readFile":[],"writeFile":[],"ensureForgeFolder":[]},"context":{"environment":"cli"}}
6
+ {"name":"task:fingerprint","type":"success","input":{"descriptorName":"task:download"},"output":{"taskName":"task:download","fingerprint":{"inputSchema":{"type":"object","properties":{"descriptorName":{"type":"string"},"uuid":{"type":"string"}}},"outputType":{"type":"object","properties":{"taskPath":{"type":"string"},"fileName":{"type":"string"},"descriptor":{"type":"string"},"handler":{"type":"any"}}},"boundaries":["loadCurrentProfile","downloadTask","loadConf","getCwd","parseTaskName","persistTask","persistConf","checkTaskExists"]},"fingerprintFile":"/Users/danielzavaladlvega/.forge/task:download.task-fingerprint.json","analysis":{"inputSchemaProps":["descriptorName","uuid"],"boundaryCount":8,"hasDescription":false,"outputType":"object"}},"boundaries":{"getCwd":[{"input":[],"output":"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli","error":null}],"loadConf":[{"input":[{}],"output":{"project":{"name":"forge-cli"},"paths":{"logs":"logs/","fixtures":"fixtures","tasks":"src/tasks/","runners":"src/runners/","tests":"src/tests/"},"infra":{"region":"us-west-2","bucket":""},"tasks":{"bundle:create":{"path":"src/tasks/bundle/create.ts","handler":"create"},"bundle:load":{"path":"src/tasks/bundle/load.ts","handler":"load"},"task:run":{"path":"src/tasks/task/run.ts","handler":"run"},"task:remove":{"path":"src/tasks/task/remove.ts","handler":"remove"},"conf:info":{"path":"src/tasks/conf/info.ts","handler":"info"},"runner:create":{"path":"src/tasks/runner/create.ts","handler":"create"},"runner:remove":{"path":"src/tasks/runner/remove.ts","handler":"remove"},"runner:bundle":{"path":"src/tasks/runner/bundle.ts","handler":"bundle"},"task:publish":{"path":"src/tasks/task/publish.ts","handler":"publish"},"task:download":{"path":"src/tasks/task/download.ts","handler":"download"},"auth:add":{"path":"src/tasks/auth/add.ts","handler":"add"},"auth:load":{"path":"src/tasks/auth/load.ts","handler":"load"},"auth:loadCurrent":{"path":"src/tasks/auth/loadCurrent.ts","handler":"loadCurrent"},"auth:switch":{"path":"src/tasks/auth/switch.ts","handler":"switchProfile"},"auth:list":{"path":"src/tasks/auth/list.ts","handler":"list"},"auth:remove":{"path":"src/tasks/auth/remove.ts","handler":"remove"},"task:replay":{"path":"src/tasks/task/replay.ts","handler":"replay"},"fixture:download":{"path":"src/tasks/fixture/download.ts","handler":"download"},"bundle:zip":{"path":"src/tasks/bundle/zip.ts","handler":"zip"},"task:list":{"path":"src/tasks/task/list.ts","handler":"list"},"task:describe":{"path":"src/tasks/task/describe.ts","handler":"describe"},"task:fingerprint":{"path":"src/tasks/task/fingerprint.ts","handler":"fingerprint"},"bundle:fingerprint":{"path":"src/tasks/bundle/fingerprint.ts","handler":"fingerprint"}},"runners":{}},"error":null}],"readFile":[{"input":["/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/download.ts"],"output":"// TASK: download\n// Run this task with:\n// forge task:run task:download --descriptorName [name] --uuid [task-uuid]\n\nimport { createTask } from '@forgehive/task'\nimport { Schema } from '@forgehive/schema'\nimport axios from 'axios'\nimport path from 'path'\nimport fs from 'fs/promises'\nimport { camelCase } from '../../utils/camelCase'\nimport { load as loadConf } from '../conf/load'\nimport { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'\nimport { Profile, type ForgeConf } from '../types'\n\nconst schema = new Schema({\n descriptorName: Schema.string(),\n uuid: Schema.string()\n})\n\nconst boundaries = {\n loadCurrentProfile: loadCurrentProfile.asBoundary(),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n downloadTask: async (uuid: string, profile: Profile): Promise<any> => {\n const downloadUrl = `${profile.url}/api/tasks/download`\n\n console.log(`Downloading task from ${downloadUrl}...`)\n\n const authToken = `${profile.apiKey}:${profile.apiSecret}`\n const response = await axios.post(downloadUrl, { uuid }, {\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json'\n }\n })\n\n return response.data\n },\n loadConf: loadConf.asBoundary(),\n getCwd: async (): Promise<string> => {\n return process.cwd()\n },\n parseTaskName: async (taskDescriptor: string): Promise<{\n descriptor: string\n taskName: string\n fileName: string\n dir?: string\n }> => {\n const res: string[] = taskDescriptor.split(':')\n\n if (res.length === 1) {\n return {\n descriptor: `${camelCase(res[0])}`,\n taskName: `${camelCase(res[0])}`,\n fileName: `${camelCase(res[0])}.ts`\n }\n }\n\n return {\n dir: res[0],\n descriptor: `${res[0]}:${camelCase(res[1])}`,\n taskName: `${camelCase(res[1])}`,\n fileName: `${camelCase(res[1])}.ts`\n }\n },\n persistTask: async (dir: string, fileName: string, content: string, cwd: string): Promise<{ path: string }> => {\n const dirPath = path.resolve(cwd, dir)\n const taskPath = path.resolve(dirPath, fileName)\n\n await fs.mkdir(dirPath, { recursive: true })\n await fs.writeFile(taskPath, content, 'utf-8')\n\n\n return {\n path: taskPath.toString()\n }\n },\n persistConf: async (forge: ForgeConf, cwd: string): Promise<void> => {\n const forgePath = path.join(cwd, 'forge.json')\n await fs.writeFile(forgePath, JSON.stringify(forge, null, 2))\n },\n checkTaskExists: async (dir: string, fileName: string): Promise<boolean> => {\n const taskPath = path.resolve(dir, fileName)\n\n try {\n await fs.access(taskPath)\n return true\n } catch {\n return false\n }\n }\n}\n\nexport const download = createTask(\n schema,\n boundaries,\n async function ({ descriptorName, uuid }, {\n downloadTask,\n getCwd,\n parseTaskName,\n persistTask,\n loadConf,\n persistConf,\n checkTaskExists,\n loadCurrentProfile\n }) {\n console.log(`Attempting to download task with descriptor: ${descriptorName} and uuid: ${uuid}`)\n\n // Parse descriptor name to get task details\n const { taskName, fileName, descriptor, dir } = await parseTaskName(descriptorName)\n const profile = await loadCurrentProfile({})\n const cwd = await getCwd()\n const forge = await loadConf({})\n\n let taskPath: string = forge.paths.tasks\n\n if (dir !== undefined) {\n taskPath = path.join(taskPath, dir)\n }\n\n // Check if task already exists\n const taskExists = await checkTaskExists(taskPath, fileName)\n if (taskExists) {\n console.log(`Task ${descriptor} already exists at ${taskPath}/${fileName}`)\n return {\n error: 'Task already exists',\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n // Download from hive api server\n let response\n try {\n response = await downloadTask(uuid, profile)\n } catch (e: unknown) {\n const error = e as { status: number, message: string }\n console.error('Error downloading task:', error.message, error.status)\n\n if (error.status === 404) {\n return {\n error: 'Task not found',\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n return {\n error: 'Failed to download task',\n message: error.message,\n taskPath: `${taskPath}/${fileName}`,\n descriptor\n }\n }\n\n console.log(`\n ==================================================\n Starting task download!\n Creating: ${taskName}\n Dir: ${dir ?? ''}\n Into: ${taskPath}\n ==================================================\n `)\n\n console.log('Writing task file:', taskPath, fileName)\n console.log('Handler:', response.handler)\n console.log('Source code:', response.sourceCode)\n\n // Persist task with cwd\n await persistTask(taskPath, fileName, response.sourceCode, cwd)\n\n // Update forge.json with the new task\n if (forge.tasks === undefined) {\n forge.tasks = {}\n }\n\n forge.tasks[descriptor] = {\n path: `${taskPath}/${fileName}`,\n handler: response.handler\n }\n\n console.log('Forge:', forge)\n\n await persistConf(forge, cwd)\n\n return {\n taskPath,\n fileName,\n descriptor,\n handler: response.handler\n }\n }\n)\n","error":null}],"writeFile":[{"input":["/Users/danielzavaladlvega/.forge/task:download.task-fingerprint.json","{\n \"taskFingerprint\": {\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"descriptorName\": {\n \"type\": \"string\"\n },\n \"uuid\": {\n \"type\": \"string\"\n }\n }\n },\n \"outputType\": {\n \"type\": \"object\",\n \"properties\": {\n \"taskPath\": {\n \"type\": \"string\"\n },\n \"fileName\": {\n \"type\": \"string\"\n },\n \"descriptor\": {\n \"type\": \"string\"\n },\n \"handler\": {\n \"type\": \"any\"\n }\n }\n },\n \"boundaries\": [\n \"loadCurrentProfile\",\n \"downloadTask\",\n \"loadConf\",\n \"getCwd\",\n \"parseTaskName\",\n \"persistTask\",\n \"persistConf\",\n \"checkTaskExists\"\n ]\n }\n}"],"output":null,"error":null}],"ensureForgeFolder":[{"input":[],"output":"/Users/danielzavaladlvega/.forge","error":null}]},"context":{"environment":"cli"}}
7
+ {"name":"task:fingerprint","type":"success","input":{"descriptorName":"task:run"},"output":{"taskName":"task:run","fingerprint":{"inputSchema":{"type":"object","properties":{"descriptorName":{"type":"string"},"args":{"type":"unknown"}}},"outputType":{"type":"any"},"boundaries":["loadConf","loadCurrentProfile","bundleCreate","bundleLoad","verifyLogFolder","ensureBuildsFolder","sendLogToAPI"]},"fingerprintFile":"/Users/danielzavaladlvega/.forge/task:run.task-fingerprint.json","analysis":{"inputSchemaProps":["descriptorName","args"],"boundaryCount":7,"hasDescription":false,"outputType":"any"}},"boundaries":{"getCwd":[{"input":[],"output":"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli","error":null}],"loadConf":[{"input":[{}],"output":{"project":{"name":"forge-cli"},"paths":{"logs":"logs/","fixtures":"fixtures","tasks":"src/tasks/","runners":"src/runners/","tests":"src/tests/"},"infra":{"region":"us-west-2","bucket":""},"tasks":{"bundle:create":{"path":"src/tasks/bundle/create.ts","handler":"create"},"bundle:load":{"path":"src/tasks/bundle/load.ts","handler":"load"},"task:run":{"path":"src/tasks/task/run.ts","handler":"run"},"task:remove":{"path":"src/tasks/task/remove.ts","handler":"remove"},"conf:info":{"path":"src/tasks/conf/info.ts","handler":"info"},"runner:create":{"path":"src/tasks/runner/create.ts","handler":"create"},"runner:remove":{"path":"src/tasks/runner/remove.ts","handler":"remove"},"runner:bundle":{"path":"src/tasks/runner/bundle.ts","handler":"bundle"},"task:publish":{"path":"src/tasks/task/publish.ts","handler":"publish"},"task:download":{"path":"src/tasks/task/download.ts","handler":"download"},"auth:add":{"path":"src/tasks/auth/add.ts","handler":"add"},"auth:load":{"path":"src/tasks/auth/load.ts","handler":"load"},"auth:loadCurrent":{"path":"src/tasks/auth/loadCurrent.ts","handler":"loadCurrent"},"auth:switch":{"path":"src/tasks/auth/switch.ts","handler":"switchProfile"},"auth:list":{"path":"src/tasks/auth/list.ts","handler":"list"},"auth:remove":{"path":"src/tasks/auth/remove.ts","handler":"remove"},"task:replay":{"path":"src/tasks/task/replay.ts","handler":"replay"},"fixture:download":{"path":"src/tasks/fixture/download.ts","handler":"download"},"bundle:zip":{"path":"src/tasks/bundle/zip.ts","handler":"zip"},"task:list":{"path":"src/tasks/task/list.ts","handler":"list"},"task:describe":{"path":"src/tasks/task/describe.ts","handler":"describe"},"task:fingerprint":{"path":"src/tasks/task/fingerprint.ts","handler":"fingerprint"},"bundle:fingerprint":{"path":"src/tasks/bundle/fingerprint.ts","handler":"fingerprint"}},"runners":{}},"error":null}],"readFile":[{"input":["/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/run.ts"],"output":"// TASK: run\n// Run this task with:\n// shadow-cli task:run\n\nimport path from 'path'\nimport fs from 'fs/promises'\nimport os from 'os'\nimport axios from 'axios'\n\nimport { createTask } from '@forgehive/task'\nimport { Schema } from '@forgehive/schema'\nimport { RecordTape } from '@forgehive/record-tape'\n\nimport { create as bundleCreate } from '../bundle/create'\nimport { load as bundleLoad } from '../bundle/load'\nimport { load as loadConf } from '../conf/load'\nimport { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'\nimport { type ForgeConf, type Profile } from '../types'\n\n// For now, we'll use a simple schema without the record type\n// TODO: Use Schema.record once it's properly built and available\nconst schema = new Schema({\n descriptorName: Schema.string(),\n args: Schema.mixedRecord()\n // args will be passed directly without schema validation for now\n})\n\nconst boundaries = {\n loadConf: loadConf.asBoundary(),\n loadCurrentProfile: loadCurrentProfile.asBoundary(),\n bundleCreate: bundleCreate.asBoundary(),\n bundleLoad: bundleLoad.asBoundary(),\n verifyLogFolder: async (logsPath: string): Promise<boolean> => {\n // return true if the folder exists\n try {\n await fs.access(logsPath)\n } catch (error) {\n return false\n }\n\n return true\n },\n ensureBuildsFolder: async (): Promise<string> => {\n const buildsPath = path.join(os.homedir(), '.forge', 'builds')\n try {\n await fs.access(buildsPath)\n } catch {\n await fs.mkdir(buildsPath, { recursive: true })\n }\n\n return buildsPath\n },\n sendLogToAPI: async (profile: Profile, projectName: string, taskName: string, logItem: unknown): Promise<boolean> => {\n try {\n const logsUrl = `${profile.url}/api/tasks/log-ingest`\n const authToken = `${profile.apiKey}:${profile.apiSecret}`\n\n await axios.post(logsUrl, {\n projectName,\n taskName,\n logItem: JSON.stringify(logItem)\n }, {\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json'\n }\n })\n\n console.log('===============================================')\n console.log('Log sent to API... ', profile.name, profile.url)\n\n return true\n } catch (e) {\n const error = e as Error\n console.error('Failed to send log to API:', error.message)\n return false\n }\n }\n}\n\nexport const run = createTask(\n schema,\n boundaries,\n async function ({ descriptorName, args }, {\n loadConf,\n bundleCreate,\n bundleLoad,\n verifyLogFolder,\n ensureBuildsFolder,\n loadCurrentProfile,\n sendLogToAPI\n }) {\n // Load forge configuration\n const forge: ForgeConf = await loadConf({})\n const taskDescriptor = forge.tasks[descriptorName as keyof typeof forge.tasks]\n const projectName = forge.project.name\n\n if (taskDescriptor === undefined) {\n throw new Error('Task is not defined on forge.json')\n }\n\n // Try to load profile, but continue if not found\n let profile = null\n try {\n profile = await loadCurrentProfile({})\n } catch (error) {\n // Profile not found or not configured, continue without it\n console.log('No profile found, logs will not be sent to remote API')\n }\n\n // Verify if log folder exists\n const logFolderPath = path.join(process.cwd(), forge.paths.logs)\n const logFolderExists = await verifyLogFolder(logFolderPath)\n if (!logFolderExists) {\n throw new Error(`Log folder \"${logFolderPath}\" does not exist`)\n }\n\n // Prepare paths\n const logsPath = path.join(logFolderPath, descriptorName)\n const entryPoint = path.join(process.cwd(), taskDescriptor.path)\n const buildsPath = await ensureBuildsFolder()\n const outputFile = path.join(buildsPath, `${descriptorName}.js`)\n\n // Bundle the task\n await bundleCreate({\n entryPoint,\n outputFile\n })\n\n // Load the bundled task\n const bundle = await bundleLoad({\n bundlePath: outputFile\n })\n\n // Get the task handler\n const task = bundle[taskDescriptor.handler]\n\n if (!task) {\n throw new Error(`Handler \"${taskDescriptor.handler}\" not found in bundle`)\n }\n\n // Setup record tape\n let tape = new RecordTape({\n path: logsPath\n })\n\n // load record tape\n try {\n await tape.load()\n\n // Need to figure out how to handle the log length\n // and other options for the RecordTape\n // For now, we'll just keep the implementation simple\n const maxLogLength = 9\n const log = tape.getLog()\n\n if (log.length > maxLogLength) {\n const newTape = new RecordTape({\n path: logsPath,\n log: log.slice(-maxLogLength)\n })\n\n tape = newTape\n }\n } catch (_error) {\n // if the tape is not found, create a new one on saving\n }\n\n // Run the task with provided arguments\n const [result, error, record] = await task.safeRun(args)\n const logItem = tape.push(descriptorName, record, {\n environment: 'cli'\n })\n await tape.save()\n\n if (profile) {\n try {\n await sendLogToAPI(profile, projectName, descriptorName, logItem)\n } catch (e) {\n console.error('Failed to send log to API:', e)\n }\n }\n\n if (error) {\n throw error\n }\n\n return result\n }\n)\n","error":null}],"writeFile":[{"input":["/Users/danielzavaladlvega/.forge/task:run.task-fingerprint.json","{\n \"taskFingerprint\": {\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"descriptorName\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"unknown\"\n }\n }\n },\n \"outputType\": {\n \"type\": \"any\"\n },\n \"boundaries\": [\n \"loadConf\",\n \"loadCurrentProfile\",\n \"bundleCreate\",\n \"bundleLoad\",\n \"verifyLogFolder\",\n \"ensureBuildsFolder\",\n \"sendLogToAPI\"\n ]\n }\n}"],"output":null,"error":null}],"ensureForgeFolder":[{"input":[],"output":"/Users/danielzavaladlvega/.forge","error":null}]},"context":{"environment":"cli"}}
8
+ {"name":"task:fingerprint","type":"success","input":{"descriptorName":"task:run"},"output":{"taskName":"task:run","fingerprint":{"inputSchema":{"type":"object","properties":{"descriptorName":{"type":"string"},"args":{"type":"unknown"}}},"outputType":{"type":"unknown"},"boundaries":["loadConf","loadCurrentProfile","bundleCreate","bundleLoad","verifyLogFolder","ensureBuildsFolder","sendLogToAPI"]},"fingerprintFile":"/Users/danielzavaladlvega/.forge/task:run.task-fingerprint.json","analysis":{"inputSchemaProps":["descriptorName","args"],"boundaryCount":7,"hasDescription":false,"outputType":"unknown"}},"boundaries":{"getCwd":[{"input":[],"output":"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli","error":null}],"loadConf":[{"input":[{}],"output":{"project":{"name":"forge-cli"},"paths":{"logs":"logs/","fixtures":"fixtures","tasks":"src/tasks/","runners":"src/runners/","tests":"src/tests/"},"infra":{"region":"us-west-2","bucket":""},"tasks":{"bundle:create":{"path":"src/tasks/bundle/create.ts","handler":"create"},"bundle:load":{"path":"src/tasks/bundle/load.ts","handler":"load"},"task:run":{"path":"src/tasks/task/run.ts","handler":"run"},"task:remove":{"path":"src/tasks/task/remove.ts","handler":"remove"},"conf:info":{"path":"src/tasks/conf/info.ts","handler":"info"},"runner:create":{"path":"src/tasks/runner/create.ts","handler":"create"},"runner:remove":{"path":"src/tasks/runner/remove.ts","handler":"remove"},"runner:bundle":{"path":"src/tasks/runner/bundle.ts","handler":"bundle"},"task:publish":{"path":"src/tasks/task/publish.ts","handler":"publish"},"task:download":{"path":"src/tasks/task/download.ts","handler":"download"},"auth:add":{"path":"src/tasks/auth/add.ts","handler":"add"},"auth:load":{"path":"src/tasks/auth/load.ts","handler":"load"},"auth:loadCurrent":{"path":"src/tasks/auth/loadCurrent.ts","handler":"loadCurrent"},"auth:switch":{"path":"src/tasks/auth/switch.ts","handler":"switchProfile"},"auth:list":{"path":"src/tasks/auth/list.ts","handler":"list"},"auth:remove":{"path":"src/tasks/auth/remove.ts","handler":"remove"},"task:replay":{"path":"src/tasks/task/replay.ts","handler":"replay"},"fixture:download":{"path":"src/tasks/fixture/download.ts","handler":"download"},"bundle:zip":{"path":"src/tasks/bundle/zip.ts","handler":"zip"},"task:list":{"path":"src/tasks/task/list.ts","handler":"list"},"task:describe":{"path":"src/tasks/task/describe.ts","handler":"describe"},"task:fingerprint":{"path":"src/tasks/task/fingerprint.ts","handler":"fingerprint"},"bundle:fingerprint":{"path":"src/tasks/bundle/fingerprint.ts","handler":"fingerprint"}},"runners":{}},"error":null}],"readFile":[{"input":["/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/run.ts"],"output":"// TASK: run\n// Run this task with:\n// shadow-cli task:run\n\nimport path from 'path'\nimport fs from 'fs/promises'\nimport os from 'os'\nimport axios from 'axios'\n\nimport { createTask } from '@forgehive/task'\nimport { Schema } from '@forgehive/schema'\nimport { RecordTape } from '@forgehive/record-tape'\n\nimport { create as bundleCreate } from '../bundle/create'\nimport { load as bundleLoad } from '../bundle/load'\nimport { load as loadConf } from '../conf/load'\nimport { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'\nimport { type ForgeConf, type Profile } from '../types'\n\n// For now, we'll use a simple schema without the record type\n// TODO: Use Schema.record once it's properly built and available\nconst schema = new Schema({\n descriptorName: Schema.string(),\n args: Schema.mixedRecord()\n // args will be passed directly without schema validation for now\n})\n\nconst boundaries = {\n loadConf: loadConf.asBoundary(),\n loadCurrentProfile: loadCurrentProfile.asBoundary(),\n bundleCreate: bundleCreate.asBoundary(),\n bundleLoad: bundleLoad.asBoundary(),\n verifyLogFolder: async (logsPath: string): Promise<boolean> => {\n // return true if the folder exists\n try {\n await fs.access(logsPath)\n } catch (error) {\n return false\n }\n\n return true\n },\n ensureBuildsFolder: async (): Promise<string> => {\n const buildsPath = path.join(os.homedir(), '.forge', 'builds')\n try {\n await fs.access(buildsPath)\n } catch {\n await fs.mkdir(buildsPath, { recursive: true })\n }\n\n return buildsPath\n },\n sendLogToAPI: async (profile: Profile, projectName: string, taskName: string, logItem: unknown): Promise<boolean> => {\n try {\n const logsUrl = `${profile.url}/api/tasks/log-ingest`\n const authToken = `${profile.apiKey}:${profile.apiSecret}`\n\n await axios.post(logsUrl, {\n projectName,\n taskName,\n logItem: JSON.stringify(logItem)\n }, {\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json'\n }\n })\n\n console.log('===============================================')\n console.log('Log sent to API... ', profile.name, profile.url)\n\n return true\n } catch (e) {\n const error = e as Error\n console.error('Failed to send log to API:', error.message)\n return false\n }\n }\n}\n\nexport const run = createTask(\n schema,\n boundaries,\n async function ({ descriptorName, args }, {\n loadConf,\n bundleCreate,\n bundleLoad,\n verifyLogFolder,\n ensureBuildsFolder,\n loadCurrentProfile,\n sendLogToAPI\n }) {\n // Load forge configuration\n const forge: ForgeConf = await loadConf({})\n const taskDescriptor = forge.tasks[descriptorName as keyof typeof forge.tasks]\n const projectName = forge.project.name\n\n if (taskDescriptor === undefined) {\n throw new Error('Task is not defined on forge.json')\n }\n\n // Try to load profile, but continue if not found\n let profile = null\n try {\n profile = await loadCurrentProfile({})\n } catch (error) {\n // Profile not found or not configured, continue without it\n console.log('No profile found, logs will not be sent to remote API')\n }\n\n // Verify if log folder exists\n const logFolderPath = path.join(process.cwd(), forge.paths.logs)\n const logFolderExists = await verifyLogFolder(logFolderPath)\n if (!logFolderExists) {\n throw new Error(`Log folder \"${logFolderPath}\" does not exist`)\n }\n\n // Prepare paths\n const logsPath = path.join(logFolderPath, descriptorName)\n const entryPoint = path.join(process.cwd(), taskDescriptor.path)\n const buildsPath = await ensureBuildsFolder()\n const outputFile = path.join(buildsPath, `${descriptorName}.js`)\n\n // Bundle the task\n await bundleCreate({\n entryPoint,\n outputFile\n })\n\n // Load the bundled task\n const bundle = await bundleLoad({\n bundlePath: outputFile\n })\n\n // Get the task handler\n const task = bundle[taskDescriptor.handler]\n\n if (!task) {\n throw new Error(`Handler \"${taskDescriptor.handler}\" not found in bundle`)\n }\n\n // Setup record tape\n let tape = new RecordTape({\n path: logsPath\n })\n\n // load record tape\n try {\n await tape.load()\n\n // Need to figure out how to handle the log length\n // and other options for the RecordTape\n // For now, we'll just keep the implementation simple\n const maxLogLength = 9\n const log = tape.getLog()\n\n if (log.length > maxLogLength) {\n const newTape = new RecordTape({\n path: logsPath,\n log: log.slice(-maxLogLength)\n })\n\n tape = newTape\n }\n } catch (_error) {\n // if the tape is not found, create a new one on saving\n }\n\n // Run the task with provided arguments\n const [result, error, record] = await task.safeRun(args)\n const logItem = tape.push(descriptorName, record, {\n environment: 'cli'\n })\n await tape.save()\n\n if (profile) {\n try {\n await sendLogToAPI(profile, projectName, descriptorName, logItem)\n } catch (e) {\n console.error('Failed to send log to API:', e)\n }\n }\n\n if (error) {\n throw error\n }\n\n return result\n }\n)\n","error":null}],"writeFile":[{"input":["/Users/danielzavaladlvega/.forge/task:run.task-fingerprint.json","{\n \"taskFingerprint\": {\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"descriptorName\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"unknown\"\n }\n }\n },\n \"outputType\": {\n \"type\": \"unknown\"\n },\n \"boundaries\": [\n \"loadConf\",\n \"loadCurrentProfile\",\n \"bundleCreate\",\n \"bundleLoad\",\n \"verifyLogFolder\",\n \"ensureBuildsFolder\",\n \"sendLogToAPI\"\n ]\n }\n}"],"output":null,"error":null}],"ensureForgeFolder":[{"input":[],"output":"/Users/danielzavaladlvega/.forge","error":null}]},"context":{"environment":"cli"}}
9
+ {"name":"task:fingerprint","type":"error","input":{"descriptorName":"task:createTask"},"error":"Task \"task:createTask\" is not defined in forge.json","boundaries":{"getCwd":[{"input":[],"output":"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli","error":null}],"loadConf":[{"input":[{}],"output":{"project":{"name":"forge-cli"},"paths":{"logs":"logs/","fixtures":"fixtures","tasks":"src/tasks/","runners":"src/runners/","tests":"src/tests/"},"infra":{"region":"us-west-2","bucket":""},"tasks":{"bundle:create":{"path":"src/tasks/bundle/create.ts","handler":"create"},"bundle:load":{"path":"src/tasks/bundle/load.ts","handler":"load"},"task:run":{"path":"src/tasks/task/run.ts","handler":"run"},"task:remove":{"path":"src/tasks/task/remove.ts","handler":"remove"},"conf:info":{"path":"src/tasks/conf/info.ts","handler":"info"},"runner:create":{"path":"src/tasks/runner/create.ts","handler":"create"},"runner:remove":{"path":"src/tasks/runner/remove.ts","handler":"remove"},"runner:bundle":{"path":"src/tasks/runner/bundle.ts","handler":"bundle"},"task:publish":{"path":"src/tasks/task/publish.ts","handler":"publish"},"task:download":{"path":"src/tasks/task/download.ts","handler":"download"},"auth:add":{"path":"src/tasks/auth/add.ts","handler":"add"},"auth:load":{"path":"src/tasks/auth/load.ts","handler":"load"},"auth:loadCurrent":{"path":"src/tasks/auth/loadCurrent.ts","handler":"loadCurrent"},"auth:switch":{"path":"src/tasks/auth/switch.ts","handler":"switchProfile"},"auth:list":{"path":"src/tasks/auth/list.ts","handler":"list"},"auth:remove":{"path":"src/tasks/auth/remove.ts","handler":"remove"},"task:replay":{"path":"src/tasks/task/replay.ts","handler":"replay"},"fixture:download":{"path":"src/tasks/fixture/download.ts","handler":"download"},"bundle:zip":{"path":"src/tasks/bundle/zip.ts","handler":"zip"},"task:list":{"path":"src/tasks/task/list.ts","handler":"list"},"task:describe":{"path":"src/tasks/task/describe.ts","handler":"describe"},"task:fingerprint":{"path":"src/tasks/task/fingerprint.ts","handler":"fingerprint"},"bundle:fingerprint":{"path":"src/tasks/bundle/fingerprint.ts","handler":"fingerprint"}},"runners":{}},"error":null}],"readFile":[],"writeFile":[],"ensureForgeFolder":[]},"context":{"environment":"cli"}}
10
+ {"name":"task:fingerprint","type":"success","input":{"descriptorName":"task:createTask"},"output":{"taskName":"task:createTask","fingerprint":{"inputSchema":{"type":"object","properties":{"descriptorName":{"type":"string"}}},"outputType":{"type":"object","properties":{"taskPath":{"type":"string"},"fileName":{"type":"string"}}},"boundaries":["loadConf","loadTemplate","getCwd","parseTaskName","persistTask","persistConf"]},"fingerprintFile":"/Users/danielzavaladlvega/.forge/task:createTask.task-fingerprint.json","analysis":{"inputSchemaProps":["descriptorName"],"boundaryCount":6,"hasDescription":false,"outputType":"object"}},"boundaries":{"getCwd":[{"input":[],"output":"/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli","error":null}],"loadConf":[{"input":[{}],"output":{"project":{"name":"forge-cli"},"paths":{"logs":"logs/","fixtures":"fixtures","tasks":"src/tasks/","runners":"src/runners/","tests":"src/tests/"},"infra":{"region":"us-west-2","bucket":""},"tasks":{"task:createTask":{"path":"src/tasks/task/createTask.ts","handler":"createTask"},"bundle:create":{"path":"src/tasks/bundle/create.ts","handler":"create"},"bundle:load":{"path":"src/tasks/bundle/load.ts","handler":"load"},"task:run":{"path":"src/tasks/task/run.ts","handler":"run"},"task:remove":{"path":"src/tasks/task/remove.ts","handler":"remove"},"conf:info":{"path":"src/tasks/conf/info.ts","handler":"info"},"runner:create":{"path":"src/tasks/runner/create.ts","handler":"create"},"runner:remove":{"path":"src/tasks/runner/remove.ts","handler":"remove"},"runner:bundle":{"path":"src/tasks/runner/bundle.ts","handler":"bundle"},"task:publish":{"path":"src/tasks/task/publish.ts","handler":"publish"},"task:download":{"path":"src/tasks/task/download.ts","handler":"download"},"auth:add":{"path":"src/tasks/auth/add.ts","handler":"add"},"auth:load":{"path":"src/tasks/auth/load.ts","handler":"load"},"auth:loadCurrent":{"path":"src/tasks/auth/loadCurrent.ts","handler":"loadCurrent"},"auth:switch":{"path":"src/tasks/auth/switch.ts","handler":"switchProfile"},"auth:list":{"path":"src/tasks/auth/list.ts","handler":"list"},"auth:remove":{"path":"src/tasks/auth/remove.ts","handler":"remove"},"task:replay":{"path":"src/tasks/task/replay.ts","handler":"replay"},"fixture:download":{"path":"src/tasks/fixture/download.ts","handler":"download"},"bundle:zip":{"path":"src/tasks/bundle/zip.ts","handler":"zip"},"task:list":{"path":"src/tasks/task/list.ts","handler":"list"},"task:describe":{"path":"src/tasks/task/describe.ts","handler":"describe"},"task:fingerprint":{"path":"src/tasks/task/fingerprint.ts","handler":"fingerprint"},"bundle:fingerprint":{"path":"src/tasks/bundle/fingerprint.ts","handler":"fingerprint"}},"runners":{}},"error":null}],"readFile":[{"input":["/Users/danielzavaladlvega/forgehive/forge-mono-repo/apps/cli/src/tasks/task/createTask.ts"],"output":"import { createTask } from '@forgehive/task'\nimport { Schema } from '@forgehive/schema'\n\nimport Handlebars from 'handlebars'\nimport path from 'path'\nimport fs from 'fs/promises'\nimport { camelCase } from '../../utils/camelCase'\n\nimport { load } from '../conf/load'\nimport { type TaskName, type ForgeConf } from '../types'\n\n// Define the template content directly in the code\n// This eliminates the need to find and load an external file\nconst TASK_TEMPLATE = `// TASK: {{ taskName }}\n// Run this task with:\n// forge task:run {{ taskDescriptor }}\n\nimport { createTask } from '@forgehive/task'\nimport { Schema } from '@forgehive/schema'\n\nconst description = 'Add task description here'\n\nconst schema = new Schema({\n // Add your schema definitions here\n // example: myParam: Schema.string()\n})\n\nconst boundaries = {\n // Add your boundary functions here\n // example: readFile: async (path: string) => fs.readFile(path, 'utf-8')\n}\n\nexport const {{ taskName }} = createTask(\n schema,\n boundaries,\n async function (argv, boundaries) {\n console.log('input:', argv)\n console.log('boundaries:', boundaries)\n // Your task implementation goes here\n const status = { status: 'Ok' }\n\n return status\n }\n)\n\n{{ taskName }}.setDescription(description)\n`\n\nconst schema = new Schema({\n descriptorName: Schema.string()\n})\n\nconst boundaries = {\n // Load boundaries\n loadConf: load.asBoundary(),\n loadTemplate: async (): Promise<string> => {\n return TASK_TEMPLATE\n },\n getCwd: async (): Promise<string> => {\n return process.cwd()\n },\n parseTaskName: async (taskDescriptor: string): Promise<TaskName> => {\n const res: string[] = taskDescriptor.split(':')\n\n if (res.length === 1) {\n return {\n descriptor: `${camelCase(res[0])}`,\n taskName: `${camelCase(res[0])}`,\n fileName: `${camelCase(res[0])}.ts`\n }\n }\n\n return {\n dir: res[0],\n descriptor: `${res[0]}:${camelCase(res[1])}`,\n taskName: `${camelCase(res[1])}`,\n fileName: `${camelCase(res[1])}.ts`\n }\n },\n\n // Persist boundaries\n persistTask: async (dir: string, fileName: string, content: string, cwd: string): Promise<{ path: string }> => {\n const dirPath = path.resolve(cwd, dir)\n const taskPath = path.resolve(dirPath, fileName)\n\n let err\n try {\n await fs.stat(taskPath)\n } catch (e) {\n err = e\n }\n\n if (err === undefined) {\n throw new Error(`File '${taskPath}' already exists.`)\n }\n\n await fs.mkdir(dir, { recursive: true })\n await fs.writeFile(taskPath, content, 'utf-8')\n\n return {\n path: taskPath.toString()\n }\n },\n persistConf: async (forge: ForgeConf, cwd: string): Promise<void> => {\n const forgePath = path.join(cwd, 'forge.json')\n await fs.writeFile(forgePath, JSON.stringify(forge, null, 2))\n }\n}\n\nexport const createTaskCommand = createTask(\n schema,\n boundaries,\n async function ({ descriptorName }, {\n loadTemplate,\n persistTask,\n loadConf,\n persistConf,\n parseTaskName,\n getCwd\n }) {\n const { taskName, fileName, descriptor, dir } = await parseTaskName(descriptorName)\n const cwd = await getCwd()\n\n const forge = await loadConf({})\n let taskPath: string = forge.paths.tasks\n\n if (dir !== undefined) {\n taskPath = path.join(taskPath, dir)\n }\n\n console.log(`\n ==================================================\n Starting task creation!\n Creating: ${taskName}\n Dir: ${dir ?? ''}\n Into: ${taskPath}\n ==================================================\n `)\n\n const template = await loadTemplate()\n const comp = Handlebars.compile(template)\n const content = comp({\n taskName,\n taskDescriptor: descriptor\n })\n\n await persistTask(taskPath, fileName, content, cwd)\n\n if (forge.tasks === undefined) {\n forge.tasks = {}\n }\n\n forge.tasks[descriptor] = {\n path: `${taskPath}/${fileName}`,\n handler: taskName\n }\n\n await persistConf(forge, cwd)\n\n return { taskPath, fileName }\n }\n)\n","error":null}],"writeFile":[{"input":["/Users/danielzavaladlvega/.forge/task:createTask.task-fingerprint.json","{\n \"taskFingerprint\": {\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"descriptorName\": {\n \"type\": \"string\"\n }\n }\n },\n \"outputType\": {\n \"type\": \"object\",\n \"properties\": {\n \"taskPath\": {\n \"type\": \"string\"\n },\n \"fileName\": {\n \"type\": \"string\"\n }\n }\n },\n \"boundaries\": [\n \"loadConf\",\n \"loadTemplate\",\n \"getCwd\",\n \"parseTaskName\",\n \"persistTask\",\n \"persistConf\"\n ]\n }\n}"],"output":null,"error":null}],"ensureForgeFolder":[{"input":[],"output":"/Users/danielzavaladlvega/.forge","error":null}]},"context":{"environment":"cli"}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgehive/forge-cli",
3
- "version": "0.2.13",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript CLI application",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -10,10 +10,10 @@
10
10
  "publishConfig": {
11
11
  "access": "public",
12
12
  "dependencies": {
13
- "@forgehive/record-tape": "^0.1.4",
14
- "@forgehive/runner": "^0.1.10",
13
+ "@forgehive/record-tape": "^0.2.0",
14
+ "@forgehive/runner": "^0.2.0",
15
15
  "@forgehive/schema": "^0.1.4",
16
- "@forgehive/task": "^0.1.13",
16
+ "@forgehive/task": "^0.2.0",
17
17
  "esbuild": "^0.25.0",
18
18
  "handlebars": "^4.7.8",
19
19
  "minimist": "^1.2.8"
@@ -26,10 +26,10 @@
26
26
  "esbuild": "^0.25.0",
27
27
  "handlebars": "^4.7.8",
28
28
  "minimist": "^1.2.8",
29
- "@forgehive/record-tape": "0.1.6",
30
- "@forgehive/runner": "0.1.12",
31
- "@forgehive/task": "0.1.13",
32
- "@forgehive/schema": "0.1.4"
29
+ "@forgehive/record-tape": "0.2.0",
30
+ "@forgehive/task": "0.2.0",
31
+ "@forgehive/schema": "0.1.4",
32
+ "@forgehive/runner": "0.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/archiver": "^6.0.3",
@@ -0,0 +1,380 @@
1
+ # Task Fingerprinting System Specification
2
+
3
+ ## Overview
4
+
5
+ This specification describes a build-time code generation system that extracts comprehensive type information ("fingerprints") from TypeScript tasks during the esbuild bundling process. The system provides complete introspection capabilities for tasks created with the `@forgehive/task` library.
6
+
7
+ ## Problem Statement
8
+
9
+ Currently, TypeScript task functions created with `createTask` have limited runtime introspection capabilities. We need a way to:
10
+
11
+ 1. Extract complete type information about task inputs, outputs, and boundaries
12
+ 2. Generate comprehensive task fingerprints during build time
13
+ 3. Provide zero-runtime-overhead type introspection
14
+ 4. Enable tooling, documentation generation, and runtime validation
15
+ 5. Detect changes in task interfaces through hash-based fingerprinting
16
+
17
+ ## Architecture
18
+
19
+ ### Core Components
20
+
21
+ 1. **esbuild Plugin**: `taskFingerprintPlugin` - Analyzes TypeScript AST during bundling
22
+ 2. **Type Extractor**: Processes `createTask` calls and extracts type information
23
+ 3. **Fingerprint Generator**: Creates comprehensive task metadata
24
+ 4. **Bundle Task**: Enhanced version of existing bundle creation with fingerprinting
25
+
26
+ ### Data Flow
27
+
28
+ ```
29
+ TypeScript Source Files
30
+
31
+ esbuild Plugin (AST Analysis)
32
+
33
+ Task Discovery & Type Extraction
34
+
35
+ Fingerprint Generation
36
+
37
+ JSON Output + Bundle Creation
38
+ ```
39
+
40
+ ## Technical Requirements
41
+
42
+ ### Dependencies
43
+
44
+ ```typescript
45
+ import * as esbuild from 'esbuild'
46
+ import * as ts from 'typescript'
47
+ import * as fs from 'fs'
48
+ import * as path from 'path'
49
+ ```
50
+
51
+ ### Core Interfaces
52
+
53
+ #### TaskFingerprint
54
+ ```typescript
55
+ interface TaskFingerprint {
56
+ name: string
57
+ description?: string
58
+ location: {
59
+ file: string
60
+ line: number
61
+ column: number
62
+ }
63
+ inputSchema: {
64
+ type: string
65
+ properties: Record<string, any>
66
+ }
67
+ outputType: string
68
+ boundaries: Record<string, {
69
+ inputTypes: string[]
70
+ outputType: string
71
+ signature: string
72
+ }>
73
+ functionSource: string
74
+ hash: string
75
+ metadata: {
76
+ extractedAt: string
77
+ version: string
78
+ }
79
+ }
80
+ ```
81
+
82
+ #### FingerprintResult
83
+ ```typescript
84
+ interface FingerprintResult {
85
+ tasks: TaskFingerprint[]
86
+ buildInfo: {
87
+ entryPoint: string
88
+ outputFile: string
89
+ fingerprintsFile?: string
90
+ totalTasks: number
91
+ buildTimestamp: string
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## Implementation Specification
97
+
98
+ ### 1. esbuild Plugin (`taskFingerprintPlugin`)
99
+
100
+ **Purpose**: Integrate TypeScript AST analysis into the esbuild bundling process
101
+
102
+ **Key Functions**:
103
+ - `onLoad`: Analyze `.ts` files for `createTask` calls
104
+ - `onEnd`: Generate and write fingerprint JSON file
105
+ - Handle TypeScript compiler integration
106
+ - Extract comprehensive type information
107
+
108
+ **Configuration**:
109
+ ```typescript
110
+ interface PluginOptions {
111
+ tsConfigPath: string // Path to tsconfig.json
112
+ outputPath?: string // Output file for fingerprints
113
+ }
114
+ ```
115
+
116
+ ### 2. Task Discovery Algorithm
117
+
118
+ **Detection Pattern**: Look for these patterns in TypeScript AST:
119
+ ```typescript
120
+ // Direct calls
121
+ createTask(schema, boundaries, fn)
122
+
123
+ // Exported assignments
124
+ export const taskName = createTask(schema, boundaries, fn)
125
+ ```
126
+
127
+ **AST Node Types to Process**:
128
+ - `ts.CallExpression` with identifier "createTask"
129
+ - `ts.VariableStatement` with exported createTask assignments
130
+
131
+ ### 3. Type Extraction Specifications
132
+
133
+ #### Schema Analysis
134
+ - Detect `new Schema({ ... })` patterns
135
+ - Extract property definitions and types
136
+ - Handle Schema methods: `.string()`, `.number()`, `.boolean()`, `.optional()`, `.default()`
137
+ - Generate JSON Schema-compatible output
138
+
139
+ #### Boundary Analysis
140
+ - Extract function signatures from boundary object
141
+ - Parse parameter types and return types
142
+ - Handle async functions and Promise return types
143
+ - Truncate function source for readability (200 chars max)
144
+
145
+ #### Return Type Analysis
146
+ - Extract TypeScript return type annotations
147
+ - Parse Promise wrapper types
148
+ - Infer types from return statements when annotations missing
149
+ - Handle complex object return types
150
+
151
+ ### 4. Hash Generation Algorithm
152
+
153
+ ```typescript
154
+ function generateHash(input: string): string {
155
+ // Simple hash function for change detection
156
+ let hash = 0
157
+ for (let i = 0; i < input.length; i++) {
158
+ const char = input.charCodeAt(i)
159
+ hash = ((hash << 5) - hash) + char
160
+ hash = hash & hash // Convert to 32-bit integer
161
+ }
162
+ return Math.abs(hash).toString(36)
163
+ }
164
+ ```
165
+
166
+ **Hash Input**: Combination of task name, function source, and input schema
167
+
168
+ ## Task Configuration Schema
169
+
170
+ ### Enhanced Bundle Task Schema
171
+ ```typescript
172
+ const schema = new Schema({
173
+ entryPoint: Schema.string(),
174
+ outputFile: Schema.string(),
175
+ generateFingerprints: Schema.boolean().optional().default(true),
176
+ fingerprintsOutputFile: Schema.string().optional(),
177
+ tsConfigPath: Schema.string().optional().default('tsconfig.json')
178
+ })
179
+ ```
180
+
181
+ ### Boundaries
182
+ ```typescript
183
+ const boundaries = {
184
+ readFile: async (filePath: string): Promise<string> => { /* fs.readFile */ },
185
+ writeFile: async (filePath: string, content: string): Promise<void> => { /* fs.writeFile */ },
186
+ findTsFiles: async (dir: string): Promise<string[]> => { /* glob search */ }
187
+ }
188
+ ```
189
+
190
+ ## Output Specifications
191
+
192
+ ### Generated Fingerprint File Structure
193
+
194
+ ```json
195
+ {
196
+ "tasks": [
197
+ {
198
+ "name": "getPrice",
199
+ "description": "Fetches stock price for a given ticker",
200
+ "location": {
201
+ "file": "src/tasks/price.ts",
202
+ "line": 15,
203
+ "column": 23
204
+ },
205
+ "inputSchema": {
206
+ "type": "object",
207
+ "properties": {
208
+ "ticker": { "type": "string" }
209
+ }
210
+ },
211
+ "outputType": "Promise<{ ticker: string, price: number }>",
212
+ "boundaries": {
213
+ "fetchStockPrice": {
214
+ "inputTypes": ["string"],
215
+ "outputType": "Promise<{ price: number }>",
216
+ "signature": "async (ticker: string): Promise<{ price: number }> => {...}"
217
+ }
218
+ },
219
+ "functionSource": "async function ({ ticker }, { fetchStockPrice }) { ... }",
220
+ "hash": "abc123def",
221
+ "metadata": {
222
+ "extractedAt": "2025-01-01T12:00:00.000Z",
223
+ "version": "1.0.0"
224
+ }
225
+ }
226
+ ],
227
+ "buildInfo": {
228
+ "entryPoint": "src/index.ts",
229
+ "outputFile": "dist/bundle.js",
230
+ "fingerprintsFile": "dist/bundle.fingerprints.json",
231
+ "totalTasks": 5,
232
+ "buildTimestamp": "2025-01-01T12:00:00.000Z"
233
+ }
234
+ }
235
+ ```
236
+
237
+ ### Task Return Value
238
+ ```typescript
239
+ {
240
+ outputFile: string,
241
+ fingerprintsFile?: string,
242
+ errors: number,
243
+ warnings: number,
244
+ taskFingerprints?: {
245
+ totalTasks: number,
246
+ tasks: Array<{
247
+ name: string,
248
+ inputType: string,
249
+ outputType: string,
250
+ boundaryCount: number,
251
+ hash: string
252
+ }>
253
+ }
254
+ }
255
+ ```
256
+
257
+ ## Usage Patterns
258
+
259
+ ### CLI Integration
260
+ ```bash
261
+ # Generate bundle with fingerprints
262
+ shadow-cli bundle:createWithFingerprints \
263
+ --entryPoint src/index.ts \
264
+ --outputFile dist/bundle.js
265
+
266
+ # Custom fingerprints location
267
+ shadow-cli bundle:createWithFingerprints \
268
+ --entryPoint src/index.ts \
269
+ --outputFile dist/bundle.js \
270
+ --fingerprintsOutputFile dist/task-types.json
271
+
272
+ # Disable fingerprints
273
+ shadow-cli bundle:createWithFingerprints \
274
+ --entryPoint src/index.ts \
275
+ --outputFile dist/bundle.js \
276
+ --generateFingerprints false
277
+ ```
278
+
279
+ ### Programmatic Usage
280
+ ```typescript
281
+ import { createWithFingerprints } from './create-with-fingerprints'
282
+
283
+ const result = await createWithFingerprints.run({
284
+ entryPoint: 'src/index.ts',
285
+ outputFile: 'dist/bundle.js',
286
+ generateFingerprints: true
287
+ })
288
+
289
+ console.log(`Generated ${result.taskFingerprints?.totalTasks} task fingerprints`)
290
+ ```
291
+
292
+ ## Error Handling
293
+
294
+ ### Plugin Error Scenarios
295
+ 1. **TypeScript Parsing Errors**: Log warning, continue processing other files
296
+ 2. **Invalid createTask Calls**: Skip malformed calls, log warnings
297
+ 3. **File System Errors**: Fail gracefully, provide meaningful error messages
298
+ 4. **TypeScript Compiler Issues**: Use fallback type extraction methods
299
+
300
+ ### Graceful Degradation
301
+ - If TypeScript analysis fails, include raw source code snippets
302
+ - Provide partial fingerprints when complete type information unavailable
303
+ - Continue build process even if fingerprint generation fails
304
+
305
+ ## Performance Considerations
306
+
307
+ ### Optimization Strategies
308
+ 1. **File Filtering**: Only analyze files containing "createTask"
309
+ 2. **Caching**: Avoid re-processing identical files
310
+ 3. **Lazy Loading**: Import TypeScript compiler only when needed
311
+ 4. **Parallel Processing**: Process multiple files concurrently where possible
312
+
313
+ ### Memory Management
314
+ - Limit function source code storage (truncate long functions)
315
+ - Clean up TypeScript compiler instances after use
316
+ - Stream large file operations when possible
317
+
318
+ ## Testing Strategy
319
+
320
+ ### Unit Tests Required
321
+ 1. **AST Parsing**: Test extraction from various createTask patterns
322
+ 2. **Type Analysis**: Verify schema, boundary, and return type extraction
323
+ 3. **Hash Generation**: Ensure consistent hash generation
324
+ 4. **Plugin Integration**: Test esbuild plugin lifecycle
325
+
326
+ ### Integration Tests
327
+ 1. **End-to-End**: Full bundle creation with fingerprint generation
328
+ 2. **Real Projects**: Test against actual task codebases
329
+ 3. **Error Scenarios**: Verify graceful handling of malformed code
330
+
331
+ ### Test Data Requirements
332
+ ```typescript
333
+ // Sample task for testing
334
+ export const testTask = createTask(
335
+ new Schema({
336
+ input: Schema.string(),
337
+ count: Schema.number().optional()
338
+ }),
339
+ {
340
+ apiCall: async (data: string): Promise<{ result: number }> => ({ result: 42 })
341
+ },
342
+ async function ({ input, count }, { apiCall }): Promise<{ output: string }> {
343
+ const { result } = await apiCall(input)
344
+ return { output: `${input}-${result}` }
345
+ }
346
+ )
347
+ ```
348
+
349
+ ## Future Enhancements
350
+
351
+ ### Planned Features
352
+ 1. **Watch Mode**: Incremental fingerprint updates during development
353
+ 2. **Validation**: Runtime validation using generated fingerprints
354
+ 3. **Documentation**: Auto-generate API docs from fingerprints
355
+ 4. **IDE Integration**: TypeScript language service plugin
356
+ 5. **Diff Detection**: Compare fingerprints across versions
357
+
358
+ ### Extension Points
359
+ 1. **Custom Extractors**: Plugin system for custom type extraction
360
+ 2. **Output Formats**: Support for different output formats (YAML, XML)
361
+ 3. **Integration Hooks**: Webhooks for fingerprint updates
362
+ 4. **Analysis Tools**: CLI tools for fingerprint analysis and comparison
363
+
364
+ ## Implementation Checklist
365
+
366
+ - [ ] Implement `taskFingerprintPlugin` esbuild plugin
367
+ - [ ] Create TypeScript AST analysis functions
368
+ - [ ] Implement schema extraction logic
369
+ - [ ] Implement boundary analysis logic
370
+ - [ ] Implement return type extraction
371
+ - [ ] Create hash generation function
372
+ - [ ] Implement enhanced bundle task
373
+ - [ ] Add error handling and logging
374
+ - [ ] Write comprehensive tests
375
+ - [ ] Create CLI integration
376
+ - [ ] Generate documentation
377
+ - [ ] Performance optimization
378
+ - [ ] Add configuration validation
379
+
380
+ This specification provides a complete blueprint for implementing a sophisticated task fingerprinting system that operates at build time and provides comprehensive type introspection capabilities for TypeScript tasks.