@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
package/src/runner.ts CHANGED
@@ -20,7 +20,6 @@ import { describe as describeTask } from './tasks/task/describe'
20
20
  import { fingerprint as fingerprintTask } from './tasks/task/fingerprint'
21
21
  import { invoke as invokeTask } from './tasks/task/invoke'
22
22
 
23
-
24
23
  import { create as createRunner } from './tasks/runner/create'
25
24
  import { remove as removeRunner } from './tasks/runner/remove'
26
25
  import { bundle as bundleRunner } from './tasks/runner/bundle'
@@ -33,6 +32,12 @@ import { add as addProfile } from './tasks/auth/add'
33
32
  import { switchProfile } from './tasks/auth/switch'
34
33
  import { list as listProfiles } from './tasks/auth/list'
35
34
  import { remove as removeProfile } from './tasks/auth/remove'
35
+ import { clear as clearProfiles } from './tasks/auth/clear'
36
+
37
+ import { create as createProject } from './tasks/project/create'
38
+ import { link as linkProject } from './tasks/project/link'
39
+ import { unlink as unlinkProject } from './tasks/project/unlink'
40
+ import { sync as syncProject } from './tasks/project/sync'
36
41
 
37
42
  interface CliParsedArguments extends RunnerParsedArguments {
38
43
  action: string;
@@ -80,6 +85,13 @@ runner.load('auth:add', addProfile)
80
85
  runner.load('auth:switch', switchProfile)
81
86
  runner.load('auth:list', listProfiles)
82
87
  runner.load('auth:remove', removeProfile)
88
+ runner.load('auth:clear', clearProfiles)
89
+
90
+ // Project commands
91
+ runner.load('project:create', createProject)
92
+ runner.load('project:link', linkProject)
93
+ runner.load('project:unlink', unlinkProject)
94
+ runner.load('project:sync', syncProject)
83
95
 
84
96
  // Set handler
85
97
  runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
@@ -93,7 +105,7 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
93
105
  let silent = false
94
106
  const task = runner.getTask(taskName)
95
107
  if (!task) {
96
- throw new Error(`Task "${taskName}" not found`)
108
+ throw new Error(`Forge command "${taskName}" not found`)
97
109
  }
98
110
 
99
111
  try {
@@ -101,6 +113,8 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
101
113
 
102
114
  const commandsWithDescriptor = ['task:create', 'task:remove', 'task:publish', 'task:describe', 'task:fingerprint']
103
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']
104
118
 
105
119
  if (commandsWithDescriptor.includes(taskName)) {
106
120
  result = await task.run({ descriptorName: action })
@@ -167,7 +181,7 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
167
181
  })
168
182
  } else if (taskName === 'auth:switch' || taskName === 'auth:remove') {
169
183
  result = await task.run({
170
- profileName: action
184
+ profileName: String(action)
171
185
  })
172
186
  } else if (taskName === 'docs:download') {
173
187
  const { path } = args as { path?: string }
@@ -175,15 +189,26 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
175
189
  result = await task.run({
176
190
  path
177
191
  })
192
+ } else if (taskName === 'project:create') {
193
+ const { projectName, description } = args as { projectName?: string, description?: string }
194
+
195
+ result = await task.run({
196
+ projectName,
197
+ description
198
+ })
199
+ } else if (taskName === 'project:link') {
200
+ const { uuid } = args as { uuid: string }
201
+ result = await task.run({
202
+ uuid
203
+ })
204
+ } else if (commandsWithoutParams.includes(taskName)) {
205
+ result = await task.run({})
178
206
  } else {
179
207
  result = await task.run(args)
180
208
 
181
- if (taskName === 'info') {
182
- silent = true
183
- }
184
209
  }
185
210
 
186
- if (taskName === 'task:describe' || taskName === 'task:list') {
211
+ if (silentCommands.includes(taskName)) {
187
212
  silent = true
188
213
  }
189
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
+ })
@@ -25,19 +25,41 @@ export const list = createTask({
25
25
  return { status: 'Ok', profiles: [] }
26
26
  }
27
27
 
28
- console.log('Available profiles:')
28
+ // Show current profile
29
+ const currentProfile = profiles.profiles.find(profile => profile.name === profiles.default)
30
+ if (currentProfile) {
31
+ console.log('Current Profile:')
32
+ console.log(` Name: ${currentProfile.name}`)
33
+ console.log(` API Key: ${currentProfile.apiKey}`)
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
+ }
41
+ console.log('')
42
+ }
43
+
44
+ console.log('Available profiles:\n')
45
+
46
+ const tableData = profiles.profiles.map(profile => ({
47
+ Name: profile.name,
48
+ 'API Key': profile.apiKey,
49
+ URL: profile.url,
50
+ Team: profile.teamName || 'Unknown',
51
+ User: profile.userName || 'Unknown'
52
+ }))
29
53
 
30
- profiles.profiles.forEach(profile => {
31
- const isDefault = profile.name === profiles.default
32
- const prefix = isDefault ? '* ' : ' '
33
- console.log(`${prefix}${profile.name} - API Key: ${profile.apiKey}`)
34
- })
54
+ console.table(tableData, ['Name', 'API Key', 'URL', 'Team', 'User'])
35
55
 
36
56
  console.log('\nUse auth:add to create or update a profile')
37
- console.log('\nUse auth:switch to switch to a profile')
57
+ console.log('Use auth:switch [name] or auth:switch [index] to switch profiles')
58
+ console.log('========================================')
38
59
 
39
60
  return {
40
- default: profiles.default
61
+ default: profiles.default,
62
+ profiles: tableData
41
63
  }
42
64
  }
43
65
  })
@@ -1,6 +1,6 @@
1
1
  // TASK: switch
2
2
  // Run this task with:
3
- // forge task:run auth:switch --profileName [name]
3
+ // forge auth:switch [name] or forge auth:switch [index]
4
4
 
5
5
  import { createTask } from '@forgehive/task'
6
6
  import { Schema } from '@forgehive/schema'
@@ -31,23 +31,39 @@ export const switchProfile = createTask({
31
31
  // Load profiles
32
32
  const profiles = await loadProfiles({})
33
33
 
34
- // Check if profile exists
35
- const profileExists = profiles.profiles.some(profile => profile.name === profileName)
34
+ if (profiles.profiles.length === 0) {
35
+ throw new Error('No profiles found. Use auth:add to create one.')
36
+ }
37
+
38
+ let targetProfile: string
36
39
 
37
- if (!profileExists) {
38
- throw new Error(`Profile "${profileName}" not found. Use auth:list to see available profiles.`)
40
+ // Check if profileName is a number (index)
41
+ const indexInput = parseInt(profileName, 10)
42
+ if (!isNaN(indexInput)) {
43
+ // Using index
44
+ if (indexInput < 0 || indexInput >= profiles.profiles.length) {
45
+ throw new Error(`Profile index ${indexInput} is out of range. Use auth:list to see available profiles (0-${profiles.profiles.length - 1}).`)
46
+ }
47
+ targetProfile = profiles.profiles[indexInput].name
48
+ } else {
49
+ // Using profile name
50
+ const profileExists = profiles.profiles.some(profile => profile.name === profileName)
51
+ if (!profileExists) {
52
+ throw new Error(`Profile "${profileName}" not found. Use auth:list to see available profiles.`)
53
+ }
54
+ targetProfile = profileName
39
55
  }
40
56
 
41
57
  // Update default profile
42
- profiles.default = profileName
58
+ profiles.default = targetProfile
43
59
 
44
60
  // Save updated profiles
45
61
  await persistProfiles(profiles)
46
62
 
47
- console.log(`Switched to profile: ${profileName}`)
63
+ console.log(`Switched to profile: ${targetProfile}`)
48
64
 
49
65
  return {
50
- default: profileName
66
+ default: targetProfile
51
67
  }
52
68
  }
53
69
  })
@@ -0,0 +1,268 @@
1
+ # Project Commands
2
+
3
+ This directory contains ForgeHive CLI commands for project management operations.
4
+
5
+ ## Commands
6
+
7
+ ### `project:create`
8
+
9
+ Creates a new project in ForgeHive with automatic UUID generation and API integration.
10
+
11
+ #### Usage
12
+
13
+ ```bash
14
+ forge project:create [--projectName="My Project"] [--description="Project description"]
15
+ ```
16
+
17
+ #### Parameters
18
+
19
+ | Parameter | Type | Required | Description |
20
+ |-----------|------|----------|-------------|
21
+ | `projectName` | string | No | Name of the project to create. If not provided, uses the project name from `forge.json` |
22
+ | `description` | string | No | Optional description for the project |
23
+
24
+ #### Behavior
25
+
26
+ 1. **UUID Management**: Checks if the local `forge.json` has a project UUID
27
+ - If no UUID exists, generates a new UUID v4 and saves it to `forge.json`
28
+ - If UUID already exists, uses the existing one
29
+
30
+ 2. **Authentication**: Uses the current profile from `auth:list` for API authentication
31
+ - Requires a valid profile set via `auth:add` and `auth:switch`
32
+ - Uses profile's `apiKey`, `apiSecret`, and `url` for the request
33
+
34
+ 3. **API Request**: Makes a POST request to `/api/projects` with:
35
+ - `projectName`: The provided project name
36
+ - `description`: The provided description (or empty string if not provided)
37
+ - `uuid`: The project UUID (generated or existing)
38
+
39
+ 4. **Response**: Returns the created project details including:
40
+ - Project UUID
41
+ - Project name
42
+ - Team information
43
+ - Creation timestamps
44
+
45
+ #### Examples
46
+
47
+ ```bash
48
+ # Create a project using the name from forge.json
49
+ forge project:create
50
+
51
+ # Create a project with custom name
52
+ forge project:create --projectName="My New Project"
53
+
54
+ # Create a project with custom name and description
55
+ forge project:create --projectName="Analytics Dashboard" --description="Customer analytics and reporting platform"
56
+
57
+ # Create a project using forge.json name but with description
58
+ forge project:create --description="Using the default project name from configuration"
59
+ ```
60
+
61
+ #### Success Output
62
+
63
+ ```
64
+ Generated and saved project UUID: 550e8400-e29b-41d4-a716-446655440000
65
+ Project created successfully!
66
+ Project UUID: 550e8400-e29b-41d4-a716-446655440000
67
+ Project Name: My New Project
68
+
69
+ 🌐 View your project on the dashboard: https://api.forgehive.com/dashboard/projects/550e8400-e29b-41d4-a716-446655440000
70
+ ```
71
+
72
+ #### Error Cases
73
+
74
+ - **No Project Name**: If no `--projectName` is provided and `forge.json` doesn't contain a project name
75
+ - **No Profile**: If no authentication profile is configured
76
+ - **Invalid Profile**: If the current profile is invalid or expired
77
+ - **API Errors**: Network issues, server errors, or validation failures
78
+ - **Duplicate Names**: If a project with the same name already exists in the team
79
+
80
+ #### File Changes
81
+
82
+ When run, this command may modify:
83
+ - `forge.json`: Adds or updates the project UUID if one doesn't exist
84
+
85
+ #### Dependencies
86
+
87
+ - Requires `@forgehive/task` framework
88
+ - Requires `@forgehive/schema` for validation
89
+ - Uses `uuid` package for UUID generation
90
+ - Integrates with `auth:loadCurrent` for profile management
91
+ - Integrates with `conf:load` for configuration management
92
+
93
+ #### API Integration
94
+
95
+ This command integrates with the ForgeHive Projects API. See the [Projects API Documentation](../../../../../../../hive/docs/projects-api.md) for detailed API specifications.
96
+
97
+ ---
98
+
99
+ ## Project Configuration
100
+
101
+ Projects are configured via the `forge.json` file in your project root:
102
+
103
+ ```json
104
+ {
105
+ "project": {
106
+ "name": "My Project",
107
+ "uuid": "550e8400-e29b-41d4-a716-446655440000"
108
+ }
109
+ }
110
+ ```
111
+
112
+ The UUID is automatically generated and managed by the CLI commands.
113
+
114
+ ---
115
+
116
+ ### `project:link`
117
+
118
+ Links an existing remote project to the local project by validating the UUID and updating forge.json.
119
+
120
+ #### Usage
121
+
122
+ ```bash
123
+ forge project:link [uuid]
124
+ ```
125
+
126
+ #### Parameters
127
+
128
+ | Parameter | Type | Required | Description |
129
+ |-----------|------|----------|-------------|
130
+ | `uuid` | string (UUID) | Yes | UUID of the existing remote project to link |
131
+
132
+ #### Behavior
133
+
134
+ 1. **UUID Validation**: Validates the provided UUID format using regex
135
+ 2. **Remote Verification**: Makes a GET request to `/api/projects/{uuid}` to verify the project exists
136
+ 3. **Authentication**: Uses the current profile for API authentication
137
+ 4. **Project Details**: Displays project information (name, description, task count) if found
138
+ 5. **Local Update**: Updates the local `forge.json` with the verified UUID
139
+
140
+ #### Examples
141
+
142
+ ```bash
143
+ # Link to an existing remote project
144
+ forge project:link 550e8400-e29b-41d4-a716-446655440000
145
+ ```
146
+
147
+ #### Success Output
148
+
149
+ ```
150
+ Checking if project 550e8400-e29b-41d4-a716-446655440000 exists on https://api.forgehive.com...
151
+ ✓ Found project: Customer Analytics
152
+ Description: Analytics platform for customer behavior analysis
153
+ Tasks: 3 task(s)
154
+
155
+ ✓ Successfully linked project 550e8400-e29b-41d4-a716-446655440000 to local forge.json
156
+ Local project name: My Local Project
157
+ Remote project name: Customer Analytics
158
+
159
+ 🌐 View your project on the dashboard: https://api.forgehive.com/dashboard/projects/550e8400-e29b-41d4-a716-446655440000
160
+ ```
161
+
162
+ #### Error Cases
163
+
164
+ - **Invalid UUID Format**: If the provided UUID doesn't match the expected format
165
+ - **Project Already Linked**: If the local project already has a UUID in forge.json
166
+ - **Project Not Found**: If the UUID doesn't exist on the remote server (404)
167
+ - **Authentication Failed**: If the current profile credentials are invalid (401)
168
+ - **No Profile**: If no authentication profile is configured
169
+ - **Network Issues**: Connection problems or server errors
170
+
171
+ #### Error Examples
172
+
173
+ ```bash
174
+ # Invalid UUID format
175
+ forge project:link invalid-uuid
176
+ # Error: Invalid UUID format: invalid-uuid. Please provide a valid UUID.
177
+
178
+ # Project already linked
179
+ forge project:link 550e8400-e29b-41d4-a716-446655440000
180
+ # Error: Project is already linked to UUID: 123e4567-e89b-12d3-a456-426614174000. Use a different project or remove the existing UUID from forge.json first.
181
+
182
+ # Project not found
183
+ forge project:link 00000000-0000-0000-0000-000000000000
184
+ # Error: Project with UUID 00000000-0000-0000-0000-000000000000 not found on https://api.forgehive.com. Please verify the UUID is correct.
185
+
186
+ # Authentication failed
187
+ forge project:link 550e8400-e29b-41d4-a716-446655440000
188
+ # Error: Authentication failed. Please check your profile credentials with 'forge auth:list'.
189
+ ```
190
+
191
+ #### File Changes
192
+
193
+ When run successfully, this command modifies:
194
+ - `forge.json`: Updates the `project.uuid` field with the verified remote project UUID
195
+
196
+ #### Dependencies
197
+
198
+ - Requires `@forgehive/task` framework
199
+ - Requires `@forgehive/schema` for validation
200
+ - Integrates with `auth:loadCurrent` for profile management
201
+ - Integrates with `conf:load` for configuration management
202
+ - Uses the ForgeHive Projects API `/api/projects/{uuid}` endpoint
203
+
204
+ ---
205
+
206
+ ### `project:unlink`
207
+
208
+ Removes the project UUID link from the local forge.json, allowing the project to be linked to a different remote project.
209
+
210
+ #### Usage
211
+
212
+ ```bash
213
+ forge project:unlink
214
+ ```
215
+
216
+ #### Parameters
217
+
218
+ This command takes no parameters.
219
+
220
+ #### Behavior
221
+
222
+ 1. **UUID Check**: Verifies that a project UUID exists in forge.json
223
+ 2. **Local Update**: Removes the UUID field from the project configuration
224
+ 3. **Confirmation**: Shows the UUID that was removed and next steps
225
+
226
+ #### Examples
227
+
228
+ ```bash
229
+ # Unlink the current project
230
+ forge project:unlink
231
+ ```
232
+
233
+ #### Success Output
234
+
235
+ ```
236
+ ✓ Successfully unlinked project from UUID: 550e8400-e29b-41d4-a716-446655440000
237
+ The project is no longer linked to a remote project.
238
+ You can now link to a different project using 'forge project:link [uuid]'
239
+ ```
240
+
241
+ #### Error Cases
242
+
243
+ - **No UUID Found**: If the project is not currently linked (no UUID in forge.json)
244
+
245
+ #### Error Examples
246
+
247
+ ```bash
248
+ # No project linked
249
+ forge project:unlink
250
+ # Error: No project UUID found in forge.json. The project is not currently linked to a remote project.
251
+ ```
252
+
253
+ #### File Changes
254
+
255
+ When run successfully, this command modifies:
256
+ - `forge.json`: Removes the `project.uuid` field completely
257
+
258
+ #### Dependencies
259
+
260
+ - Requires `@forgehive/task` framework
261
+ - Requires `@forgehive/schema` for validation
262
+ - Integrates with `conf:load` for configuration management
263
+
264
+ **Note**: This command only affects the local forge.json file. It does not make any API calls or modify anything on the remote server.
265
+
266
+ ---
267
+
268
+ ## Project Configuration