@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.
- package/dist/runner.js +32 -6
- package/dist/tasks/auth/add.d.ts +16 -0
- package/dist/tasks/auth/add.js +56 -4
- package/dist/tasks/auth/clear.d.ts +16 -0
- package/dist/tasks/auth/clear.js +54 -0
- package/dist/tasks/auth/list.d.ts +7 -1
- package/dist/tasks/auth/list.js +28 -8
- package/dist/tasks/auth/switch.js +24 -8
- package/dist/tasks/project/create.d.ts +27 -0
- package/dist/tasks/project/create.js +96 -0
- package/dist/tasks/project/link.d.ts +22 -0
- package/dist/tasks/project/link.js +95 -0
- package/dist/tasks/project/sync.d.ts +116 -0
- package/dist/tasks/project/sync.js +152 -0
- package/dist/tasks/project/unlink.d.ts +11 -0
- package/dist/tasks/project/unlink.js +65 -0
- package/dist/tasks/task/createTask.d.ts +12 -0
- package/dist/tasks/task/createTask.js +55 -5
- package/dist/tasks/task/run.d.ts +10 -2
- package/dist/tasks/task/run.js +14 -6
- package/dist/tasks/types.d.ts +4 -0
- package/dist/test/tasks/create.test.js +4 -3
- package/forge.json +14 -0
- package/package.json +6 -5
- package/src/runner.ts +32 -7
- package/src/tasks/auth/add.ts +66 -4
- package/src/tasks/auth/clear.ts +63 -0
- package/src/tasks/auth/list.ts +30 -8
- package/src/tasks/auth/switch.ts +24 -8
- package/src/tasks/project/README.md +268 -0
- package/src/tasks/project/create.ts +111 -0
- package/src/tasks/project/link.ts +106 -0
- package/src/tasks/project/sync.ts +202 -0
- package/src/tasks/project/unlink.ts +74 -0
- package/src/tasks/task/createTask.ts +72 -5
- package/src/tasks/task/run.ts +17 -6
- package/src/tasks/types.ts +4 -0
- 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
|
|
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
|
})
|
package/src/tasks/task/run.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/tasks/types.ts
CHANGED
|
@@ -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 {
|