@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.
- package/dist/runner.js +15 -7
- package/dist/tasks/auth/add.js +23 -19
- package/dist/tasks/auth/list.js +20 -16
- package/dist/tasks/auth/load.js +19 -15
- package/dist/tasks/auth/loadCurrent.js +13 -9
- package/dist/tasks/auth/remove.js +30 -26
- package/dist/tasks/auth/switch.js +19 -15
- package/dist/tasks/bundle/create.js +16 -12
- package/dist/tasks/bundle/fingerprint.d.ts +36 -0
- package/dist/tasks/bundle/fingerprint.js +164 -0
- package/dist/tasks/bundle/load.js +9 -5
- package/dist/tasks/bundle/zip.js +49 -45
- package/dist/tasks/conf/info.js +23 -19
- package/dist/tasks/conf/load.js +8 -4
- package/dist/tasks/fixture/download.js +40 -36
- package/dist/tasks/init.js +35 -31
- package/dist/tasks/runner/bundle.js +34 -30
- package/dist/tasks/runner/create.js +28 -24
- package/dist/tasks/runner/remove.js +22 -18
- package/dist/tasks/task/createTask.js +35 -28
- package/dist/tasks/task/describe.d.ts +35 -0
- package/dist/tasks/task/describe.js +130 -0
- package/dist/tasks/task/download.js +63 -59
- package/dist/tasks/task/fingerprint.d.ts +26 -0
- package/dist/tasks/task/fingerprint.js +87 -0
- package/dist/tasks/task/list.d.ts +12 -0
- package/dist/tasks/task/list.js +46 -0
- package/dist/tasks/task/publish.js +72 -68
- package/dist/tasks/task/remove.js +24 -20
- package/dist/tasks/task/replay.js +94 -90
- package/dist/tasks/task/run.js +84 -79
- package/dist/test/setup.d.ts +0 -0
- package/dist/test/setup.js +14 -0
- package/dist/test/tasks/create.test.js +6 -5
- package/dist/utils/taskAnalysis.d.ts +21 -0
- package/dist/utils/taskAnalysis.js +380 -0
- package/forge.json +20 -0
- package/jest.config.js +2 -1
- package/logs/task:fingerprint.log +10 -0
- package/package.json +8 -8
- package/specs/fingerprint.md +380 -0
- package/src/runner.ts +14 -5
- package/src/tasks/README.md +13 -13
- package/src/tasks/auth/add.ts +3 -3
- package/src/tasks/auth/list.ts +3 -3
- package/src/tasks/auth/load.ts +3 -3
- package/src/tasks/auth/loadCurrent.ts +3 -3
- package/src/tasks/auth/remove.ts +3 -3
- package/src/tasks/auth/switch.ts +3 -3
- package/src/tasks/bundle/README.md +7 -7
- package/src/tasks/bundle/create.ts +4 -4
- package/src/tasks/bundle/fingerprint.ts +218 -0
- package/src/tasks/bundle/load.ts +4 -4
- package/src/tasks/bundle/zip.ts +3 -3
- package/src/tasks/conf/info.ts +3 -3
- package/src/tasks/conf/load.ts +3 -3
- package/src/tasks/fixture/download.ts +3 -3
- package/src/tasks/init.ts +3 -3
- package/src/tasks/runner/bundle.ts +3 -3
- package/src/tasks/runner/create.ts +3 -3
- package/src/tasks/runner/remove.ts +3 -3
- package/src/tasks/task/createTask.ts +10 -7
- package/src/tasks/task/describe.ts +148 -0
- package/src/tasks/task/download.ts +3 -3
- package/src/tasks/task/fingerprint.ts +107 -0
- package/src/tasks/task/list.ts +58 -0
- package/src/tasks/task/publish.ts +3 -3
- package/src/tasks/task/remove.ts +3 -3
- package/src/tasks/task/replay.ts +3 -3
- package/src/tasks/task/run.ts +5 -4
- package/src/test/setup.ts +14 -0
- package/src/test/tasks/create.test.ts +9 -9
- package/src/utils/taskAnalysis.ts +419 -0
- package/dist/taskAdapter.d.ts +0 -34
- package/dist/taskAdapter.js +0 -85
- package/dist/templates/README.md +0 -23
- package/dist/templates/task.hbs +0 -27
- package/dist/test/utils.d.ts +0 -2
- package/dist/test/utils.js +0 -17
- package/logs/auth:list.log +0 -4
- package/logs/auth:load.log +0 -2
- package/logs/auth:loadCurrent.log +0 -1
- package/logs/conf:info.log +0 -2
- 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.
|
|
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.
|
|
14
|
-
"@forgehive/runner": "^0.
|
|
13
|
+
"@forgehive/record-tape": "^0.2.0",
|
|
14
|
+
"@forgehive/runner": "^0.2.0",
|
|
15
15
|
"@forgehive/schema": "^0.1.4",
|
|
16
|
-
"@forgehive/task": "^0.
|
|
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.
|
|
30
|
-
"@forgehive/
|
|
31
|
-
"@forgehive/
|
|
32
|
-
"@forgehive/
|
|
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.
|