@forgehive/forge-cli 0.3.10 → 0.3.12

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 (38) hide show
  1. package/dist/runner.js +32 -6
  2. package/dist/tasks/auth/add.d.ts +16 -0
  3. package/dist/tasks/auth/add.js +56 -4
  4. package/dist/tasks/auth/clear.d.ts +16 -0
  5. package/dist/tasks/auth/clear.js +54 -0
  6. package/dist/tasks/auth/list.d.ts +7 -1
  7. package/dist/tasks/auth/list.js +28 -8
  8. package/dist/tasks/auth/switch.js +24 -8
  9. package/dist/tasks/project/create.d.ts +27 -0
  10. package/dist/tasks/project/create.js +96 -0
  11. package/dist/tasks/project/link.d.ts +22 -0
  12. package/dist/tasks/project/link.js +95 -0
  13. package/dist/tasks/project/sync.d.ts +116 -0
  14. package/dist/tasks/project/sync.js +152 -0
  15. package/dist/tasks/project/unlink.d.ts +11 -0
  16. package/dist/tasks/project/unlink.js +65 -0
  17. package/dist/tasks/task/createTask.d.ts +12 -0
  18. package/dist/tasks/task/createTask.js +55 -5
  19. package/dist/tasks/task/run.d.ts +10 -2
  20. package/dist/tasks/task/run.js +14 -6
  21. package/dist/tasks/types.d.ts +4 -0
  22. package/dist/test/tasks/create.test.js +4 -3
  23. package/forge.json +14 -0
  24. package/package.json +6 -5
  25. package/src/runner.ts +32 -7
  26. package/src/tasks/auth/add.ts +66 -4
  27. package/src/tasks/auth/clear.ts +63 -0
  28. package/src/tasks/auth/list.ts +30 -8
  29. package/src/tasks/auth/switch.ts +24 -8
  30. package/src/tasks/project/README.md +268 -0
  31. package/src/tasks/project/create.ts +111 -0
  32. package/src/tasks/project/link.ts +106 -0
  33. package/src/tasks/project/sync.ts +202 -0
  34. package/src/tasks/project/unlink.ts +74 -0
  35. package/src/tasks/task/createTask.ts +72 -5
  36. package/src/tasks/task/run.ts +17 -6
  37. package/src/tasks/types.ts +4 -0
  38. package/src/test/tasks/create.test.ts +4 -3
@@ -0,0 +1,111 @@
1
+ // TASK: create
2
+ // Run this task with:
3
+ // forge task:run project:create
4
+
5
+ import { createTask } from '@forgehive/task'
6
+ import { Schema } from '@forgehive/schema'
7
+ import { v4 as uuidv4 } from 'uuid'
8
+ import fs from 'fs/promises'
9
+ import path from 'path'
10
+
11
+ import { load as loadConf } from '../conf/load'
12
+ import { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'
13
+ import { type ForgeConf, type Profile } from '../types'
14
+
15
+ const name = 'project:create'
16
+ const description = 'Create a new project in ForgeHive'
17
+
18
+ const schema = new Schema({
19
+ projectName: Schema.string().optional(),
20
+ description: Schema.string().optional()
21
+ })
22
+
23
+ const boundaries = {
24
+ loadConf: loadConf.asBoundary(),
25
+ loadCurrentProfile: loadCurrentProfile.asBoundary(),
26
+ writeFile: async (filePath: string, content: string): Promise<void> => {
27
+ await fs.writeFile(filePath, content, 'utf-8')
28
+ },
29
+ createProject: async (profile: Profile, payload: { projectName: string; description: string; uuid: string }): Promise<Response> => {
30
+ const authToken = `${profile.apiKey}:${profile.apiSecret}`
31
+ return await fetch(`${profile.url}/api/projects`, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'Authorization': `Bearer ${authToken}`
36
+ },
37
+ body: JSON.stringify(payload)
38
+ })
39
+ }
40
+ }
41
+
42
+ export const create = createTask({
43
+ name,
44
+ description,
45
+ schema,
46
+ boundaries,
47
+ fn: async function (argv, { loadConf, loadCurrentProfile, writeFile, createProject }) {
48
+ const { projectName: inputProjectName, description } = argv
49
+
50
+ // Load current configuration
51
+ const conf = await loadConf({})
52
+
53
+ // Use provided projectName or fall back to forge.json project name
54
+ const projectName = inputProjectName || conf.project.name
55
+
56
+ if (!projectName) {
57
+ throw new Error('Project name is required. Provide --projectName or ensure forge.json has a project name.')
58
+ }
59
+
60
+ // Check if project already has a UUID, generate one if not
61
+ let projectUuid = conf.project.uuid
62
+ if (!projectUuid) {
63
+ projectUuid = uuidv4()
64
+
65
+ // Update forge.json with the new UUID
66
+ const forgePath = path.join(process.cwd(), 'forge.json')
67
+ const updatedConf: ForgeConf = {
68
+ ...conf,
69
+ project: {
70
+ ...conf.project,
71
+ uuid: projectUuid
72
+ }
73
+ }
74
+
75
+ await writeFile(forgePath, JSON.stringify(updatedConf, null, 2))
76
+ console.log(`Generated and saved project UUID: ${projectUuid}`)
77
+ }
78
+
79
+ // Load current profile for API authentication
80
+ const profile = await loadCurrentProfile({})
81
+
82
+ // Prepare API request payload
83
+ const payload = {
84
+ projectName,
85
+ description: description || '',
86
+ uuid: projectUuid
87
+ }
88
+
89
+ // Make API request to create project
90
+ const response = await createProject(profile, payload)
91
+
92
+ if (!response.ok) {
93
+ const errorText = await response.text()
94
+ throw new Error(`Failed to create project: ${response.status} ${response.statusText} - ${errorText}`)
95
+ }
96
+
97
+ const result = await response.json()
98
+
99
+ console.log('Project created successfully!')
100
+ console.log(`Project UUID: ${result.project.uuid}`)
101
+ console.log(`Project Name: ${result.project.projectName}`)
102
+ console.log(`\n🌐 View your project on the dashboard: ${profile.url}/dashboard/projects/${result.project.uuid}`)
103
+
104
+ return {
105
+ success: true,
106
+ project: result.project,
107
+ localUuid: projectUuid
108
+ }
109
+ }
110
+ })
111
+
@@ -0,0 +1,106 @@
1
+ // TASK: link
2
+ // Run this task with:
3
+ // forge project:link [uuid]
4
+
5
+ import { createTask } from '@forgehive/task'
6
+ import { Schema } from '@forgehive/schema'
7
+ import fs from 'fs/promises'
8
+ import path from 'path'
9
+
10
+ import { load as loadConf } from '../conf/load'
11
+ import { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'
12
+ import { type ForgeConf, type Profile } from '../types'
13
+
14
+ const name = 'project:link'
15
+ const description = 'Link an existing remote project to the local project by UUID'
16
+
17
+ const schema = new Schema({
18
+ uuid: Schema.string()
19
+ })
20
+
21
+ const boundaries = {
22
+ loadConf: loadConf.asBoundary(),
23
+ loadCurrentProfile: loadCurrentProfile.asBoundary(),
24
+ writeFile: async (filePath: string, content: string): Promise<void> => {
25
+ await fs.writeFile(filePath, content, 'utf-8')
26
+ },
27
+ fetchProject: async (profile: Profile, uuid: string): Promise<Response> => {
28
+ const authToken = `${profile.apiKey}:${profile.apiSecret}`
29
+ console.log(`Fetching project ${uuid} from ${profile.url}...`)
30
+ return await fetch(`${profile.url}/api/projects/${uuid}`, {
31
+ method: 'GET',
32
+ headers: {
33
+ 'Authorization': `Bearer ${authToken}`
34
+ }
35
+ })
36
+ }
37
+ }
38
+
39
+ export const link = createTask({
40
+ name,
41
+ description,
42
+ schema,
43
+ boundaries,
44
+ fn: async function ({ uuid }, { loadConf, loadCurrentProfile, writeFile, fetchProject }) {
45
+ // Load current configuration and profile
46
+ const conf = await loadConf({})
47
+
48
+ // Check if project already has a UUID
49
+ if (conf.project.uuid) {
50
+ throw new Error(`Project is already linked to UUID: ${conf.project.uuid}. Use a different project or remove the existing UUID from forge.json first.`)
51
+ }
52
+
53
+ const profile = await loadCurrentProfile({})
54
+
55
+ console.log(`Checking if project ${uuid} exists on ${profile.url}...`)
56
+
57
+ // Check if project exists on remote
58
+ const response = await fetchProject(profile, uuid)
59
+
60
+ if (!response.ok) {
61
+ if (response.status === 404) {
62
+ throw new Error(`Project with UUID ${uuid} not found on ${profile.url}. Please verify the UUID is correct.`)
63
+ } else if (response.status === 401) {
64
+ throw new Error('Authentication failed. Please check your profile credentials with \'forge auth:list\'.')
65
+ } else {
66
+ const errorText = await response.text()
67
+ throw new Error(`Failed to verify project: ${response.status} ${response.statusText} - ${errorText}`)
68
+ }
69
+ }
70
+
71
+ const projectData = await response.json()
72
+ const project = projectData.project
73
+
74
+ console.log(`āœ“ Found project: ${project.projectName}`)
75
+ console.log(` Description: ${project.description || 'No description'}`)
76
+ console.log(` Tasks: ${project.tasks.length} task(s)`)
77
+
78
+ // Update forge.json with the UUID
79
+ const forgePath = path.join(process.cwd(), 'forge.json')
80
+ const updatedConf: ForgeConf = {
81
+ ...conf,
82
+ project: {
83
+ ...conf.project,
84
+ uuid: uuid
85
+ }
86
+ }
87
+
88
+ await writeFile(forgePath, JSON.stringify(updatedConf, null, 2))
89
+
90
+ console.log(`\nāœ“ Successfully linked project ${uuid} to local forge.json`)
91
+ console.log(` Local project name: ${conf.project.name}`)
92
+ console.log(` Remote project name: ${project.projectName}`)
93
+ console.log(`\n🌐 View your project on the dashboard: ${profile.url}/dashboard/projects/${uuid}`)
94
+
95
+ return {
96
+ success: true,
97
+ linkedProject: {
98
+ uuid: project.uuid,
99
+ name: project.projectName,
100
+ description: project.description,
101
+ tasksCount: project.tasks.length
102
+ }
103
+ }
104
+ }
105
+ })
106
+
@@ -0,0 +1,202 @@
1
+ // TASK: sync
2
+ // Run this task with:
3
+ // forge task:run project:sync
4
+
5
+ import { createTask } from '@forgehive/task'
6
+ import { Schema } from '@forgehive/schema'
7
+ import { v4 as uuidv4 } from 'uuid'
8
+
9
+ import { load } from '../conf/load'
10
+ import { loadCurrent } from '../auth/loadCurrent'
11
+ import { type ForgeConf } from '../types'
12
+ import path from 'path'
13
+ import fs from 'fs/promises'
14
+
15
+ const schema = new Schema({})
16
+
17
+ const boundaries = {
18
+ loadConf: load.asBoundary(),
19
+ loadCurrentProfile: loadCurrent.asBoundary(),
20
+ getCwd: async (): Promise<string> => {
21
+ return process.cwd()
22
+ },
23
+ persistConf: async (forge: ForgeConf, cwd: string): Promise<void> => {
24
+ const forgePath = path.join(cwd, 'forge.json')
25
+ await fs.writeFile(forgePath, JSON.stringify(forge, null, 2))
26
+ },
27
+ syncTasksToHive: async (
28
+ projectUuid: string,
29
+ tasks: Array<{ uuid: string; name: string }>,
30
+ apiKey: string,
31
+ apiSecret: string,
32
+ baseUrl: string
33
+ ): Promise<{
34
+ success: boolean
35
+ error?: string
36
+ data?: {
37
+ projectUuid: string
38
+ projectName: string
39
+ summary: {
40
+ total: number
41
+ created: number
42
+ updated: number
43
+ errors: number
44
+ }
45
+ results: {
46
+ created: Array<{ uuid: string; taskName: string; action: string }>
47
+ updated: Array<{ uuid: string; taskName: string; previousName: string; action: string }>
48
+ errors: Array<{ uuid: string; taskName: string; error: string }>
49
+ }
50
+ }
51
+ }> => {
52
+ try {
53
+ const url = `${baseUrl}/api/projects/${projectUuid}/sync`
54
+ const response = await fetch(url, {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ 'Authorization': `Bearer ${apiKey}:${apiSecret}`
59
+ },
60
+ body: JSON.stringify({ tasks })
61
+ })
62
+
63
+ if (response.ok) {
64
+ const data = await response.json()
65
+ return { success: true, data }
66
+ } else {
67
+ const errorData = await response.json().catch(() => ({ error: `HTTP ${response.status} - ${response.statusText}` }))
68
+ return { success: false, error: errorData.error || `HTTP ${response.status} - ${response.statusText}` }
69
+ }
70
+ } catch (error) {
71
+ return { success: false, error: error instanceof Error ? error.message : 'Network error' }
72
+ }
73
+ }
74
+ }
75
+
76
+ export const sync = createTask({
77
+ schema,
78
+ boundaries,
79
+ fn: async function (_argv, {
80
+ loadConf,
81
+ loadCurrentProfile,
82
+ getCwd,
83
+ persistConf,
84
+ syncTasksToHive
85
+ }) {
86
+ const cwd = await getCwd()
87
+ const forge = await loadConf({})
88
+
89
+ // Check if project has UUID
90
+ if (!forge.project.uuid) {
91
+ throw new Error('Project does not have a UUID. Please run "forge project:link" to connect to a Hive project.')
92
+ }
93
+
94
+ console.log(`
95
+ ==================================================
96
+ Starting project sync to Hive!
97
+ Project: ${forge.project.name}
98
+ UUID: ${forge.project.uuid}
99
+ ==================================================
100
+ `)
101
+
102
+ // Ensure all tasks have UUIDs and collect them for sync
103
+ let configUpdated = false
104
+ const tasksToSync: Array<{ uuid: string; name: string }> = []
105
+
106
+ if (!forge.tasks) {
107
+ forge.tasks = {}
108
+ }
109
+
110
+ for (const [taskDescriptor, taskData] of Object.entries(forge.tasks)) {
111
+ if (!taskData.uuid) {
112
+ taskData.uuid = uuidv4()
113
+ configUpdated = true
114
+ console.log(` āž• Generated UUID for task: ${taskDescriptor}`)
115
+ }
116
+
117
+ // Use the task descriptor (key) as the name for the API
118
+ const taskName = taskDescriptor
119
+ tasksToSync.push({
120
+ uuid: taskData.uuid,
121
+ name: taskName
122
+ })
123
+ }
124
+
125
+ // Save config if we added UUIDs
126
+ if (configUpdated) {
127
+ await persistConf(forge, cwd)
128
+ console.log(' šŸ’¾ Updated forge.json with new UUIDs')
129
+ }
130
+
131
+ if (tasksToSync.length === 0) {
132
+ console.log(' ā„¹ļø No tasks found to sync')
133
+ return { status: 'no-tasks', message: 'No tasks found in project' }
134
+ }
135
+
136
+ console.log(` šŸ“Š Found ${tasksToSync.length} tasks to sync`)
137
+
138
+ try {
139
+ const profile = await loadCurrentProfile({})
140
+ const result = await syncTasksToHive(
141
+ forge.project.uuid,
142
+ tasksToSync,
143
+ profile.apiKey,
144
+ profile.apiSecret,
145
+ profile.url
146
+ )
147
+
148
+ if (result.success && result.data) {
149
+ const { summary, results } = result.data
150
+
151
+ console.log('\\n āœ… Sync completed successfully!')
152
+ console.log(` Total tasks: ${summary.total}`)
153
+ console.log(` Created: ${summary.created}`)
154
+ console.log(` Updated: ${summary.updated}`)
155
+ console.log(` Errors: ${summary.errors}`)
156
+
157
+ if (results.created.length > 0) {
158
+ console.log('\\n šŸ†• Created tasks:')
159
+ results.created.forEach(task => {
160
+ console.log(` • ${task.taskName} (${task.uuid})`)
161
+ })
162
+ }
163
+
164
+ if (results.updated.length > 0) {
165
+ console.log('\\n šŸ”„ Updated tasks:')
166
+ results.updated.forEach(task => {
167
+ console.log(` • ${task.taskName} (was: ${task.previousName}) (${task.uuid})`)
168
+ })
169
+ }
170
+
171
+ if (results.errors.length > 0) {
172
+ console.log('\\n āŒ Tasks with errors:')
173
+ results.errors.forEach(task => {
174
+ console.log(` • ${task.taskName}: ${task.error} (${task.uuid})`)
175
+ })
176
+ }
177
+
178
+ const projectUrl = `${profile.url}/dashboard/projects/${forge.project.uuid}`
179
+ console.log(`\\n šŸ”— View your project: ${projectUrl}`)
180
+
181
+ return {
182
+ status: 'success',
183
+ summary,
184
+ results,
185
+ projectUrl
186
+ }
187
+ } else {
188
+ throw new Error(result.error || 'Unknown sync error')
189
+ }
190
+ } catch (error) {
191
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
192
+
193
+ if (errorMessage.includes('No default profile')) {
194
+ console.log('\\n āš ļø No authentication profile found. Run "forge auth:add" to configure.')
195
+ return { status: 'no-auth', message: 'No authentication profile configured' }
196
+ } else {
197
+ console.log(`\\n āŒ Sync failed: ${errorMessage}`)
198
+ return { status: 'error', message: errorMessage }
199
+ }
200
+ }
201
+ }
202
+ })
@@ -0,0 +1,74 @@
1
+ // TASK: unlink
2
+ // Run this task with:
3
+ // forge project:unlink
4
+
5
+ import { createTask } from '@forgehive/task'
6
+ import { Schema } from '@forgehive/schema'
7
+ import fs from 'fs/promises'
8
+ import path from 'path'
9
+
10
+ import { load as loadConf } from '../conf/load'
11
+ import { type ForgeConf } from '../types'
12
+
13
+ const name = 'project:unlink'
14
+ const description = 'Remove the project UUID link from local forge.json'
15
+
16
+ const schema = new Schema({
17
+ // No parameters needed for unlink
18
+ })
19
+
20
+ const boundaries = {
21
+ loadConf: loadConf.asBoundary(),
22
+ writeFile: async (filePath: string, content: string): Promise<void> => {
23
+ await fs.writeFile(filePath, content, 'utf-8')
24
+ }
25
+ }
26
+
27
+ export const unlink = createTask({
28
+ name,
29
+ description,
30
+ schema,
31
+ boundaries,
32
+ fn: async function (argv, { loadConf, writeFile }) {
33
+ // Load current configuration
34
+ const conf = await loadConf({})
35
+
36
+ // Check if project has a UUID to unlink
37
+ if (!conf.project.uuid) {
38
+ throw new Error('No project UUID found in forge.json. The project is not currently linked to a remote project.')
39
+ }
40
+
41
+ const currentUuid = conf.project.uuid
42
+
43
+ // Remove the UUID from the project configuration
44
+ const updatedConf: ForgeConf = {
45
+ ...conf,
46
+ project: {
47
+ ...conf.project,
48
+ uuid: undefined
49
+ }
50
+ }
51
+
52
+ // Clean up undefined values by creating a new object without the uuid field
53
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
54
+ const { uuid, ...projectWithoutUuid } = updatedConf.project
55
+ const finalConf: ForgeConf = {
56
+ ...updatedConf,
57
+ project: projectWithoutUuid
58
+ }
59
+
60
+ // Write the updated configuration back to forge.json
61
+ const forgePath = path.join(process.cwd(), 'forge.json')
62
+ await writeFile(forgePath, JSON.stringify(finalConf, null, 2))
63
+
64
+ console.log(`āœ“ Successfully unlinked project from UUID: ${currentUuid}`)
65
+ console.log(' The project is no longer linked to a remote project.')
66
+ console.log(' You can now link to a different project using \'forge project:link [uuid]\'')
67
+
68
+ return {
69
+ success: true,
70
+ unlinkedUuid: currentUuid
71
+ }
72
+ }
73
+ })
74
+
@@ -1,5 +1,6 @@
1
1
  import { createTask } from '@forgehive/task'
2
2
  import { Schema } from '@forgehive/schema'
3
+ import { v4 as uuidv4 } from 'uuid'
3
4
 
4
5
  import Handlebars from 'handlebars'
5
6
  import path from 'path'
@@ -7,6 +8,7 @@ import fs from 'fs/promises'
7
8
  import { camelCase } from '../../utils/camelCase'
8
9
 
9
10
  import { load } from '../conf/load'
11
+ import { loadCurrent } from '../auth/loadCurrent'
10
12
  import { type TaskName, type ForgeConf } from '../types'
11
13
 
12
14
  // Define the template content directly in the code
@@ -38,11 +40,10 @@ export const {{ taskName }} = createTask({
38
40
  boundaries,
39
41
  fn: async function (argv, boundaries) {
40
42
  console.log('input:', argv)
41
- console.log('boundaries:', boundaries)
43
+ console.log('boundaries:', Object.keys(boundaries))
42
44
  // Your task implementation goes here
43
- const status = { status: 'Ok' }
44
45
 
45
- return status
46
+ return {}
46
47
  }
47
48
  })
48
49
 
@@ -55,6 +56,7 @@ const schema = new Schema({
55
56
  const boundaries = {
56
57
  // Load boundaries
57
58
  loadConf: load.asBoundary(),
59
+ loadCurrentProfile: loadCurrent.asBoundary(),
58
60
  loadTemplate: async (): Promise<string> => {
59
61
  return TASK_TEMPLATE
60
62
  },
@@ -106,6 +108,40 @@ const boundaries = {
106
108
  persistConf: async (forge: ForgeConf, cwd: string): Promise<void> => {
107
109
  const forgePath = path.join(cwd, 'forge.json')
108
110
  await fs.writeFile(forgePath, JSON.stringify(forge, null, 2))
111
+ },
112
+ createTaskInHive: async (
113
+ projectUuid: string,
114
+ taskUuid: string,
115
+ taskName: string,
116
+ description: string,
117
+ apiKey: string,
118
+ apiSecret: string,
119
+ baseUrl: string
120
+ ): Promise<{ success: boolean; taskUrl?: string; error?: string }> => {
121
+ try {
122
+ const url = `${baseUrl}/api/projects/${projectUuid}/tasks/${taskUuid}`
123
+ const response = await fetch(url, {
124
+ method: 'POST',
125
+ headers: {
126
+ 'Content-Type': 'application/json',
127
+ 'Authorization': `Bearer ${apiKey}:${apiSecret}`
128
+ },
129
+ body: JSON.stringify({
130
+ taskName,
131
+ description
132
+ })
133
+ })
134
+
135
+ if (response.ok) {
136
+ const taskUrl = `${baseUrl}/tasks/${taskUuid}`
137
+ return { success: true, taskUrl }
138
+ } else {
139
+ const errorData = await response.json().catch(() => ({ error: 'Unknown error' }))
140
+ return { success: false, error: errorData.error || `HTTP ${response.status}` }
141
+ }
142
+ } catch (error) {
143
+ return { success: false, error: error instanceof Error ? error.message : 'Network error' }
144
+ }
109
145
  }
110
146
  }
111
147
 
@@ -118,7 +154,9 @@ export const createTaskCommand = createTask({
118
154
  loadConf,
119
155
  persistConf,
120
156
  parseTaskName,
121
- getCwd
157
+ getCwd,
158
+ loadCurrentProfile,
159
+ createTaskInHive
122
160
  }) {
123
161
  const { taskName, fileName, descriptor, dir } = await parseTaskName(descriptorName)
124
162
  const cwd = await getCwd()
@@ -153,13 +191,42 @@ export const createTaskCommand = createTask({
153
191
  forge.tasks = {}
154
192
  }
155
193
 
194
+ const taskUuid = uuidv4()
156
195
  forge.tasks[descriptor] = {
157
196
  path: `${taskPath}/${fileName}`,
158
- handler: taskName
197
+ handler: taskName,
198
+ uuid: taskUuid
159
199
  }
160
200
 
161
201
  await persistConf(forge, cwd)
162
202
 
203
+ // Try to create task in Hive if user has profile and project UUID
204
+ if (forge.project.uuid) {
205
+ try {
206
+ const profile = await loadCurrentProfile({})
207
+ const result = await createTaskInHive(
208
+ forge.project.uuid,
209
+ taskUuid,
210
+ descriptor,
211
+ 'Add task description here',
212
+ profile.apiKey,
213
+ profile.apiSecret,
214
+ profile.url
215
+ )
216
+
217
+ if (result.success && result.taskUrl) {
218
+ console.log('\\nāœ… Task created successfully in Hive!')
219
+ console.log(`šŸ”— View your task: ${result.taskUrl}`)
220
+ } else {
221
+ console.log(`\nāš ļø Task created locally but could not sync to Hive: ${result.error}`)
222
+ console.log(`šŸ”— Host: ${profile.url}`)
223
+ }
224
+ } catch (error) {
225
+ // Silently continue if no profile is configured
226
+ console.log('\\nšŸ“ Task created locally. Configure a profile with \'forge auth:add\' to sync with Hive.')
227
+ }
228
+ }
229
+
163
230
  return { taskPath, fileName }
164
231
  }
165
232
  })
@@ -49,7 +49,7 @@ const boundaries = {
49
49
 
50
50
  return buildsPath
51
51
  },
52
- sendLogToAPI: async (profile: Profile, projectName: string, record: ExecutionRecord): Promise<boolean> => {
52
+ sendLogToAPI: async (profile: Profile, projectName: string, record: ExecutionRecord, taskUuid?: string): Promise<{ success: boolean; logUuid?: string; taskUuid?: string }> => {
53
53
  try {
54
54
  const config = {
55
55
  projectName,
@@ -64,19 +64,25 @@ const boundaries = {
64
64
  const client = new HiveLogClient(config)
65
65
  const result = await client.sendLog(record)
66
66
 
67
- if (result === 'success') {
67
+ if (result === 'success' || (typeof result === 'object' && 'uuid' in result)) {
68
68
  console.log('===============================================')
69
69
  console.log('Log sent to API... ', profile.name, profile.url)
70
- return true
70
+
71
+ if (typeof result === 'object' && result && 'uuid' in result) {
72
+ const logResponse = result as { uuid: string }
73
+ return { success: true, logUuid: logResponse.uuid, taskUuid }
74
+ }
75
+
76
+ return { success: true, taskUuid }
71
77
  } else {
72
78
  console.error('Failed to send log to API:', profile.url)
73
- return false
79
+ return { success: false }
74
80
  }
75
81
  } catch (e) {
76
82
  console.error('Failed to send log to API:', profile.url)
77
83
  const error = e as Error
78
84
  console.error('Error:', error.message)
79
- return false
85
+ return { success: false }
80
86
  }
81
87
  }
82
88
  }
@@ -97,6 +103,7 @@ export const run = createTask({
97
103
  const forge: ForgeConf = await loadConf({})
98
104
  const taskDescriptor = forge.tasks[descriptorName as keyof typeof forge.tasks]
99
105
  const projectName = forge.project.name
106
+ const taskUuid = taskDescriptor?.uuid
100
107
 
101
108
  if (taskDescriptor === undefined) {
102
109
  throw new Error('Task is not defined on forge.json')
@@ -169,7 +176,11 @@ export const run = createTask({
169
176
 
170
177
  if (profile) {
171
178
  try {
172
- await sendLogToAPI(profile, projectName, logItem)
179
+ const logResult = await sendLogToAPI(profile, projectName, logItem, taskUuid)
180
+
181
+ if (logResult.success && taskUuid) {
182
+ console.log(`šŸ”— View execution logs: ${profile.url}/tasks/${taskUuid}?tab=logs`)
183
+ }
173
184
  } catch (e) {
174
185
  console.error('Failed to send log to API:', e)
175
186
  }
@@ -10,6 +10,7 @@ export interface RunnerDescriptor {
10
10
  export interface TaskDescriptor {
11
11
  path: string
12
12
  handler: string
13
+ uuid?: string
13
14
  }
14
15
 
15
16
  export interface Infra {
@@ -47,6 +48,9 @@ export interface Profile {
47
48
  apiKey: string
48
49
  apiSecret: string
49
50
  url: string
51
+ teamName?: string
52
+ teamUuid?: string
53
+ userName?: string
50
54
  }
51
55
 
52
56
  export interface Profiles {