@forgehive/forge-cli 0.3.11 → 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.
@@ -17,7 +17,11 @@ export declare const run: import("@forgehive/task").TaskInstanceType<(argv: {
17
17
  }) => Promise<Promise<any>>;
18
18
  ensureLogFolder: (logsPath: string) => Promise<void>;
19
19
  ensureBuildsFolder: () => Promise<string>;
20
- sendLogToAPI: (profile: Profile, projectName: string, record: ExecutionRecord) => Promise<boolean>;
20
+ sendLogToAPI: (profile: Profile, projectName: string, record: ExecutionRecord, taskUuid?: string) => Promise<{
21
+ success: boolean;
22
+ logUuid?: string;
23
+ taskUuid?: string;
24
+ }>;
21
25
  }>) => Promise<any>, {
22
26
  loadConf: (args: {}) => Promise<Promise<ForgeConf>>;
23
27
  loadCurrentProfile: (args: {}) => Promise<Promise<Profile>>;
@@ -32,5 +36,9 @@ export declare const run: import("@forgehive/task").TaskInstanceType<(argv: {
32
36
  }) => Promise<Promise<any>>;
33
37
  ensureLogFolder: (logsPath: string) => Promise<void>;
34
38
  ensureBuildsFolder: () => Promise<string>;
35
- sendLogToAPI: (profile: Profile, projectName: string, record: ExecutionRecord) => Promise<boolean>;
39
+ sendLogToAPI: (profile: Profile, projectName: string, record: ExecutionRecord, taskUuid?: string) => Promise<{
40
+ success: boolean;
41
+ logUuid?: string;
42
+ taskUuid?: string;
43
+ }>;
36
44
  }>;
@@ -50,7 +50,7 @@ const boundaries = {
50
50
  }
51
51
  return buildsPath;
52
52
  },
53
- sendLogToAPI: async (profile, projectName, record) => {
53
+ sendLogToAPI: async (profile, projectName, record, taskUuid) => {
54
54
  try {
55
55
  const config = {
56
56
  projectName,
@@ -63,21 +63,25 @@ const boundaries = {
63
63
  };
64
64
  const client = new hive_sdk_1.HiveLogClient(config);
65
65
  const result = await client.sendLog(record);
66
- if (result === 'success') {
66
+ if (result === 'success' || (typeof result === 'object' && 'uuid' in result)) {
67
67
  console.log('===============================================');
68
68
  console.log('Log sent to API... ', profile.name, profile.url);
69
- return true;
69
+ if (typeof result === 'object' && result && 'uuid' in result) {
70
+ const logResponse = result;
71
+ return { success: true, logUuid: logResponse.uuid, taskUuid };
72
+ }
73
+ return { success: true, taskUuid };
70
74
  }
71
75
  else {
72
76
  console.error('Failed to send log to API:', profile.url);
73
- return false;
77
+ return { success: false };
74
78
  }
75
79
  }
76
80
  catch (e) {
77
81
  console.error('Failed to send log to API:', profile.url);
78
82
  const error = e;
79
83
  console.error('Error:', error.message);
80
- return false;
84
+ return { success: false };
81
85
  }
82
86
  }
83
87
  };
@@ -89,6 +93,7 @@ exports.run = (0, task_1.createTask)({
89
93
  const forge = await loadConf({});
90
94
  const taskDescriptor = forge.tasks[descriptorName];
91
95
  const projectName = forge.project.name;
96
+ const taskUuid = taskDescriptor?.uuid;
92
97
  if (taskDescriptor === undefined) {
93
98
  throw new Error('Task is not defined on forge.json');
94
99
  }
@@ -149,7 +154,10 @@ exports.run = (0, task_1.createTask)({
149
154
  await tape.save();
150
155
  if (profile) {
151
156
  try {
152
- await sendLogToAPI(profile, projectName, logItem);
157
+ const logResult = await sendLogToAPI(profile, projectName, logItem, taskUuid);
158
+ if (logResult.success && taskUuid) {
159
+ console.log(`šŸ”— View execution logs: ${profile.url}/tasks/${taskUuid}?tab=logs`);
160
+ }
153
161
  }
154
162
  catch (e) {
155
163
  console.error('Failed to send log to API:', e);
@@ -43,6 +43,9 @@ export interface Profile {
43
43
  apiKey: string;
44
44
  apiSecret: string;
45
45
  url: string;
46
+ teamName?: string;
47
+ teamUuid?: string;
48
+ userName?: string;
46
49
  }
47
50
  export interface Profiles {
48
51
  default: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgehive/forge-cli",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "TypeScript CLI application",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,7 @@
10
10
  "publishConfig": {
11
11
  "access": "public",
12
12
  "dependencies": {
13
- "@forgehive/hive-sdk": "^0.1.2",
13
+ "@forgehive/hive-sdk": "^0.1.3",
14
14
  "@forgehive/record-tape": "^0.2.5",
15
15
  "@forgehive/runner": "^0.2.5",
16
16
  "@forgehive/schema": "^0.1.4",
@@ -30,7 +30,7 @@
30
30
  "minimist": "^1.2.8",
31
31
  "typescript": "^5.3.3",
32
32
  "uuid": "^11.1.0",
33
- "@forgehive/hive-sdk": "0.1.2",
33
+ "@forgehive/hive-sdk": "0.1.3",
34
34
  "@forgehive/record-tape": "0.2.5",
35
35
  "@forgehive/runner": "0.2.5",
36
36
  "@forgehive/schema": "0.1.4",
package/src/runner.ts CHANGED
@@ -32,10 +32,12 @@ import { add as addProfile } from './tasks/auth/add'
32
32
  import { switchProfile } from './tasks/auth/switch'
33
33
  import { list as listProfiles } from './tasks/auth/list'
34
34
  import { remove as removeProfile } from './tasks/auth/remove'
35
+ import { clear as clearProfiles } from './tasks/auth/clear'
35
36
 
36
37
  import { create as createProject } from './tasks/project/create'
37
38
  import { link as linkProject } from './tasks/project/link'
38
39
  import { unlink as unlinkProject } from './tasks/project/unlink'
40
+ import { sync as syncProject } from './tasks/project/sync'
39
41
 
40
42
  interface CliParsedArguments extends RunnerParsedArguments {
41
43
  action: string;
@@ -83,11 +85,13 @@ runner.load('auth:add', addProfile)
83
85
  runner.load('auth:switch', switchProfile)
84
86
  runner.load('auth:list', listProfiles)
85
87
  runner.load('auth:remove', removeProfile)
88
+ runner.load('auth:clear', clearProfiles)
86
89
 
87
90
  // Project commands
88
91
  runner.load('project:create', createProject)
89
92
  runner.load('project:link', linkProject)
90
93
  runner.load('project:unlink', unlinkProject)
94
+ runner.load('project:sync', syncProject)
91
95
 
92
96
  // Set handler
93
97
  runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
@@ -101,7 +105,7 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
101
105
  let silent = false
102
106
  const task = runner.getTask(taskName)
103
107
  if (!task) {
104
- throw new Error(`Task "${taskName}" not found`)
108
+ throw new Error(`Forge command "${taskName}" not found`)
105
109
  }
106
110
 
107
111
  try {
@@ -109,6 +113,8 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
109
113
 
110
114
  const commandsWithDescriptor = ['task:create', 'task:remove', 'task:publish', 'task:describe', 'task:fingerprint']
111
115
  const commandsWithRunner = ['runner:create', 'runner:remove']
116
+ const commandsWithoutParams = ['project:unlink', 'project:sync', 'auth:clear']
117
+ const silentCommands = ['task:describe', 'task:list', 'auth:list', 'info']
112
118
 
113
119
  if (commandsWithDescriptor.includes(taskName)) {
114
120
  result = await task.run({ descriptorName: action })
@@ -195,17 +201,14 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
195
201
  result = await task.run({
196
202
  uuid
197
203
  })
198
- } else if (taskName === 'project:unlink') {
204
+ } else if (commandsWithoutParams.includes(taskName)) {
199
205
  result = await task.run({})
200
206
  } else {
201
207
  result = await task.run(args)
202
208
 
203
- if (taskName === 'info') {
204
- silent = true
205
- }
206
209
  }
207
210
 
208
- if (taskName === 'task:describe' || taskName === 'task:list' || taskName === 'auth:list') {
211
+ if (silentCommands.includes(taskName)) {
209
212
  silent = true
210
213
  }
211
214
 
@@ -24,23 +24,83 @@ const boundaries = {
24
24
  const buildsPath = path.join(os.homedir(), '.forge')
25
25
  const profilesPath = path.join(buildsPath, 'profiles.json')
26
26
  await fs.writeFile(profilesPath, JSON.stringify(profiles, null, 2))
27
+ },
28
+ fetchMeInfo: async (apiKey: string, apiSecret: string, url: string): Promise<{
29
+ success: boolean
30
+ teamName?: string
31
+ teamUuid?: string
32
+ userName?: string
33
+ error?: string
34
+ }> => {
35
+ try {
36
+ const response = await fetch(`${url}/api/me`, {
37
+ method: 'GET',
38
+ headers: {
39
+ 'Authorization': `Bearer ${apiKey}:${apiSecret}`,
40
+ 'Content-Type': 'application/json'
41
+ }
42
+ })
43
+
44
+ if (response.ok) {
45
+ const data = await response.json()
46
+ return {
47
+ success: true,
48
+ teamName: data.team?.name,
49
+ teamUuid: data.team?.uuid,
50
+ userName: data.user?.name
51
+ }
52
+ } else {
53
+ const errorData = await response.json().catch(() => ({ error: 'Unknown error' }))
54
+ return { success: false, error: errorData.error || `HTTP ${response.status}` }
55
+ }
56
+ } catch (error) {
57
+ return { success: false, error: error instanceof Error ? error.message : 'Network error' }
58
+ }
27
59
  }
28
60
  }
29
61
 
30
62
  export const add = createTask({
31
63
  schema,
32
64
  boundaries,
33
- fn: async function ({ name, apiKey, apiSecret, url }, { loadProfiles, persistProfiles }) {
65
+ fn: async function ({ name, apiKey, apiSecret, url }, { loadProfiles, persistProfiles, fetchMeInfo }) {
34
66
  const profiles = await loadProfiles({})
35
67
 
68
+ console.log('Verifying credentials...')
69
+
70
+ // Fetch team and user information from /me endpoint
71
+ const meInfo = await fetchMeInfo(apiKey, apiSecret, url)
72
+
73
+ if (!meInfo.success) {
74
+ throw new Error(`Failed to verify credentials: ${meInfo.error}`)
75
+ }
76
+
77
+ console.log('āœ… Credentials verified')
78
+ if (meInfo.userName) {
79
+ console.log(` User: ${meInfo.userName}`)
80
+ }
81
+ if (meInfo.teamName) {
82
+ console.log(` Team: ${meInfo.teamName}`)
83
+ }
84
+
85
+ // Create profile with team information
86
+ const profile = {
87
+ name,
88
+ apiKey,
89
+ apiSecret,
90
+ url,
91
+ teamName: meInfo.teamName,
92
+ teamUuid: meInfo.teamUuid,
93
+ userName: meInfo.userName
94
+ }
95
+
36
96
  // Check if profile with same name already exists
37
97
  const existingProfileIndex = profiles.profiles.findIndex(p => p.name === name)
38
98
  if (existingProfileIndex >= 0) {
39
99
  // Replace existing profile
40
- profiles.profiles[existingProfileIndex] = { name, apiKey, apiSecret, url }
100
+ profiles.profiles[existingProfileIndex] = profile
41
101
  } else {
42
102
  // Add new profile
43
- profiles.profiles.push({ name, apiKey, apiSecret, url })
103
+ profiles.profiles.push(profile)
44
104
  }
45
105
 
46
106
  // Set as default profile
@@ -51,7 +111,9 @@ export const add = createTask({
51
111
 
52
112
  return {
53
113
  status: 'Ok',
54
- message: `Profile '${name}' added and set as default`
114
+ message: `Profile '${name}' added and set as default`,
115
+ teamName: meInfo.teamName,
116
+ userName: meInfo.userName
55
117
  }
56
118
  }
57
119
  })
@@ -0,0 +1,63 @@
1
+ // TASK: clear
2
+ // Run this task with:
3
+ // forge task:run auth:clear
4
+
5
+ import { createTask } from '@forgehive/task'
6
+ import { Schema } from '@forgehive/schema'
7
+ import path from 'path'
8
+ import fs from 'fs/promises'
9
+ import os from 'os'
10
+
11
+ import { load as loadProfiles } from './load'
12
+ import { type Profiles } from '../types'
13
+
14
+ const schema = new Schema({})
15
+
16
+ const boundaries = {
17
+ loadProfiles: loadProfiles.asBoundary(),
18
+ clearProfiles: async (): Promise<void> => {
19
+ const buildsPath = path.join(os.homedir(), '.forge')
20
+ const profilesPath = path.join(buildsPath, 'profiles.json')
21
+
22
+ // Create empty profiles structure
23
+ const emptyProfiles: Profiles = {
24
+ default: '',
25
+ profiles: []
26
+ }
27
+
28
+ await fs.writeFile(profilesPath, JSON.stringify(emptyProfiles, null, 2))
29
+ }
30
+ }
31
+
32
+ export const clear = createTask({
33
+ schema,
34
+ boundaries,
35
+ fn: async function (_argv, { loadProfiles, clearProfiles }) {
36
+ const profiles = await loadProfiles({})
37
+
38
+ if (profiles.profiles.length === 0) {
39
+ console.log('No profiles found to clear.')
40
+ return { status: 'Ok', message: 'No profiles found' }
41
+ }
42
+
43
+ const profileCount = profiles.profiles.length
44
+ console.log(`Found ${profileCount} profile(s) to clear:`)
45
+
46
+ profiles.profiles.forEach(profile => {
47
+ console.log(` - ${profile.name} (${profile.teamName || 'Unknown team'})`)
48
+ })
49
+
50
+ console.log('\\nClearing all profiles...')
51
+
52
+ // Clear all profiles
53
+ await clearProfiles()
54
+
55
+ console.log('āœ… All profiles cleared successfully')
56
+
57
+ return {
58
+ status: 'Ok',
59
+ message: `Cleared ${profileCount} profile(s)`,
60
+ clearedCount: profileCount
61
+ }
62
+ }
63
+ })
@@ -32,6 +32,12 @@ export const list = createTask({
32
32
  console.log(` Name: ${currentProfile.name}`)
33
33
  console.log(` API Key: ${currentProfile.apiKey}`)
34
34
  console.log(` URL: ${currentProfile.url}`)
35
+ if (currentProfile.userName) {
36
+ console.log(` User: ${currentProfile.userName}`)
37
+ }
38
+ if (currentProfile.teamName) {
39
+ console.log(` Team: ${currentProfile.teamName}`)
40
+ }
35
41
  console.log('')
36
42
  }
37
43
 
@@ -40,10 +46,12 @@ export const list = createTask({
40
46
  const tableData = profiles.profiles.map(profile => ({
41
47
  Name: profile.name,
42
48
  'API Key': profile.apiKey,
43
- URL: profile.url
49
+ URL: profile.url,
50
+ Team: profile.teamName || 'Unknown',
51
+ User: profile.userName || 'Unknown'
44
52
  }))
45
53
 
46
- console.table(tableData, ['Name', 'API Key', 'URL'])
54
+ console.table(tableData, ['Name', 'API Key', 'URL', 'Team', 'User'])
47
55
 
48
56
  console.log('\nUse auth:add to create or update a profile')
49
57
  console.log('Use auth:switch [name] or auth:switch [index] to switch profiles')
@@ -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
+ })
@@ -8,6 +8,7 @@ import fs from 'fs/promises'
8
8
  import { camelCase } from '../../utils/camelCase'
9
9
 
10
10
  import { load } from '../conf/load'
11
+ import { loadCurrent } from '../auth/loadCurrent'
11
12
  import { type TaskName, type ForgeConf } from '../types'
12
13
 
13
14
  // Define the template content directly in the code
@@ -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,14 +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
197
  handler: taskName,
159
- uuid: uuidv4()
198
+ uuid: taskUuid
160
199
  }
161
200
 
162
201
  await persistConf(forge, cwd)
163
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
+
164
230
  return { taskPath, fileName }
165
231
  }
166
232
  })