@forgehive/forge-cli 0.3.11 → 0.3.13

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,12 @@ 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, projectUuid?: string) => Promise<{
21
+ success: boolean;
22
+ logUuid?: string;
23
+ taskUuid?: string;
24
+ skipRemoteLog?: boolean;
25
+ }>;
21
26
  }>) => Promise<any>, {
22
27
  loadConf: (args: {}) => Promise<Promise<ForgeConf>>;
23
28
  loadCurrentProfile: (args: {}) => Promise<Promise<Profile>>;
@@ -32,5 +37,10 @@ export declare const run: import("@forgehive/task").TaskInstanceType<(argv: {
32
37
  }) => Promise<Promise<any>>;
33
38
  ensureLogFolder: (logsPath: string) => Promise<void>;
34
39
  ensureBuildsFolder: () => Promise<string>;
35
- sendLogToAPI: (profile: Profile, projectName: string, record: ExecutionRecord) => Promise<boolean>;
40
+ sendLogToAPI: (profile: Profile, projectName: string, record: ExecutionRecord, taskUuid?: string, projectUuid?: string) => Promise<{
41
+ success: boolean;
42
+ logUuid?: string;
43
+ taskUuid?: string;
44
+ skipRemoteLog?: boolean;
45
+ }>;
36
46
  }>;
@@ -50,10 +50,27 @@ const boundaries = {
50
50
  }
51
51
  return buildsPath;
52
52
  },
53
- sendLogToAPI: async (profile, projectName, record) => {
53
+ sendLogToAPI: async (profile, projectName, record, taskUuid, projectUuid) => {
54
+ // Check if we have required UUIDs for the new endpoint
55
+ if (!projectUuid || !taskUuid) {
56
+ console.log('===============================================');
57
+ console.log('⚠️ Remote logging skipped - missing UUIDs');
58
+ console.log('');
59
+ console.log('To enable remote logging with enhanced features:');
60
+ if (!projectUuid) {
61
+ console.log('• Use "forge project:create" to create a new project, or');
62
+ console.log('• Use "forge project:link" to connect to an existing project');
63
+ }
64
+ if (!taskUuid) {
65
+ console.log('• Use "forge project:sync" to get the task to have UUID');
66
+ }
67
+ console.log('===============================================');
68
+ return { success: true, skipRemoteLog: true };
69
+ }
54
70
  try {
55
71
  const config = {
56
72
  projectName,
73
+ projectUuid,
57
74
  apiKey: profile.apiKey,
58
75
  apiSecret: profile.apiSecret,
59
76
  host: profile.url,
@@ -62,22 +79,29 @@ const boundaries = {
62
79
  }
63
80
  };
64
81
  const client = new hive_sdk_1.HiveLogClient(config);
65
- const result = await client.sendLog(record);
66
- if (result === 'success') {
82
+ console.log('Sending execution log to Hive...');
83
+ const result = await client.sendLogByUuid(record, taskUuid);
84
+ if (result === 'success' || (typeof result === 'object' && 'uuid' in result)) {
67
85
  console.log('===============================================');
68
- console.log('Log sent to API... ', profile.name, profile.url);
69
- return true;
86
+ console.log('Log sent to Hive successfully');
87
+ console.log(` Profile: ${profile.name}`);
88
+ console.log(` Host: ${profile.url}`);
89
+ if (typeof result === 'object' && result && 'uuid' in result) {
90
+ const logResponse = result;
91
+ return { success: true, logUuid: logResponse.uuid, taskUuid };
92
+ }
93
+ return { success: true, taskUuid };
70
94
  }
71
95
  else {
72
- console.error('Failed to send log to API:', profile.url);
73
- return false;
96
+ console.error('Failed to send log to Hive:', profile.url);
97
+ return { success: false };
74
98
  }
75
99
  }
76
100
  catch (e) {
77
- console.error('Failed to send log to API:', profile.url);
101
+ console.error('Failed to send log to Hive:', profile.url);
78
102
  const error = e;
79
103
  console.error('Error:', error.message);
80
- return false;
104
+ return { success: false };
81
105
  }
82
106
  }
83
107
  };
@@ -89,6 +113,8 @@ exports.run = (0, task_1.createTask)({
89
113
  const forge = await loadConf({});
90
114
  const taskDescriptor = forge.tasks[descriptorName];
91
115
  const projectName = forge.project.name;
116
+ const projectUuid = forge.project.uuid;
117
+ const taskUuid = taskDescriptor?.uuid;
92
118
  if (taskDescriptor === undefined) {
93
119
  throw new Error('Task is not defined on forge.json');
94
120
  }
@@ -149,7 +175,10 @@ exports.run = (0, task_1.createTask)({
149
175
  await tape.save();
150
176
  if (profile) {
151
177
  try {
152
- await sendLogToAPI(profile, projectName, logItem);
178
+ const logResult = await sendLogToAPI(profile, projectName, logItem, taskUuid, projectUuid);
179
+ if (logResult.success && !logResult.skipRemoteLog && taskUuid) {
180
+ console.log(`🔗 View execution logs: ${profile.url}/tasks/${taskUuid}?tab=logs`);
181
+ }
153
182
  }
154
183
  catch (e) {
155
184
  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;
@@ -0,0 +1 @@
1
+ {"input":{},"boundaries":{"loadConf":[{"input":[{}],"output":{"project":{"name":"forge-cli"},"paths":{"logs":"logs/","fixtures":"fixtures","fingerprints":"fingerprints/","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"},"task:invoke":{"path":"src/tasks/task/invoke.ts","handler":"invoke"},"docs:download":{"path":"src/tasks/docs/download.ts","handler":"download"},"project:create":{"path":"src/tasks/project/create.ts","handler":"create"},"project:link":{"path":"src/tasks/project/link.ts","handler":"link","uuid":"cb9f82e1-d397-46d9-9f0d-2b0e3becbfa1"},"project:unlink":{"path":"src/tasks/project/unlink.ts","handler":"unlink","uuid":"414d37de-793c-4d01-899d-69515f5e0948"}},"runners":{}},"timing":{"startTime":1755627445111,"endTime":1755627445112,"duration":1}}]},"metadata":{"environment":"cli"},"metrics":[],"type":"success","output":{"taskCount":29},"timing":{"startTime":1755627445111,"endTime":1755627445112,"duration":1}}
@@ -0,0 +1 @@
1
+ {"input":{},"boundaries":{},"taskName":"test:guidance","metadata":{"environment":"cli"},"metrics":[],"type":"success","output":{},"timing":{"startTime":1755627427045,"endTime":1755627427045,"duration":0}}
@@ -0,0 +1 @@
1
+ {"uuid":"0198bfeb-69c0-77e8-882b-a76dcb9300f9","input":{},"boundaries":{},"taskName":"test:uuid","metadata":{"environment":"cli"},"metrics":[],"type":"success","output":{},"timing":{"startTime":1755566533056,"endTime":1755566533057,"duration":1}}
@@ -0,0 +1 @@
1
+ {"input":{},"boundaries":{},"taskName":"test:uuidCheck","metadata":{"environment":"cli"},"metrics":[],"type":"success","output":{},"timing":{"startTime":1755567492456,"endTime":1755567492456,"duration":0}}
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.13",
4
4
  "description": "TypeScript CLI application",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -10,11 +10,11 @@
10
10
  "publishConfig": {
11
11
  "access": "public",
12
12
  "dependencies": {
13
- "@forgehive/hive-sdk": "^0.1.2",
14
- "@forgehive/record-tape": "^0.2.5",
15
- "@forgehive/runner": "^0.2.5",
13
+ "@forgehive/hive-sdk": "^0.1.4",
14
+ "@forgehive/record-tape": "^0.2.6",
15
+ "@forgehive/runner": "^0.2.6",
16
16
  "@forgehive/schema": "^0.1.4",
17
- "@forgehive/task": "^0.2.5",
17
+ "@forgehive/task": "^0.2.6",
18
18
  "esbuild": "^0.25.0",
19
19
  "handlebars": "^4.7.8",
20
20
  "minimist": "^1.2.8",
@@ -30,11 +30,11 @@
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",
34
- "@forgehive/record-tape": "0.2.5",
35
- "@forgehive/runner": "0.2.5",
36
- "@forgehive/schema": "0.1.4",
37
- "@forgehive/task": "0.2.5"
33
+ "@forgehive/record-tape": "0.2.6",
34
+ "@forgehive/hive-sdk": "0.1.4",
35
+ "@forgehive/task": "0.2.6",
36
+ "@forgehive/runner": "0.2.6",
37
+ "@forgehive/schema": "0.1.4"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/archiver": "^6.0.3",
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
+ })