@fiftth/fiftth-cli 0.1.0 → 1.0.0

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 (40) hide show
  1. package/.github/workflows/publish-npm.yml +62 -0
  2. package/README.md +123 -111
  3. package/dist/api/client.d.ts +1 -0
  4. package/dist/api/client.d.ts.map +1 -1
  5. package/dist/api/client.js +4 -0
  6. package/dist/api/client.js.map +1 -1
  7. package/dist/commands/checkout.d.ts +1 -1
  8. package/dist/commands/checkout.d.ts.map +1 -1
  9. package/dist/commands/checkout.js +63 -31
  10. package/dist/commands/checkout.js.map +1 -1
  11. package/dist/commands/tasks.d.ts.map +1 -1
  12. package/dist/commands/tasks.js +4 -1
  13. package/dist/commands/tasks.js.map +1 -1
  14. package/dist/config/configService.d.ts.map +1 -1
  15. package/dist/config/configService.js +17 -4
  16. package/dist/config/configService.js.map +1 -1
  17. package/dist/index.js +25 -25
  18. package/dist/index.js.map +1 -1
  19. package/dist/services/taskService.d.ts +1 -0
  20. package/dist/services/taskService.d.ts.map +1 -1
  21. package/dist/services/taskService.js +7 -0
  22. package/dist/services/taskService.js.map +1 -1
  23. package/package.json +56 -56
  24. package/src/api/client.ts +31 -26
  25. package/src/commands/checkout.ts +101 -68
  26. package/src/commands/login.ts +145 -145
  27. package/src/commands/repo.ts +113 -113
  28. package/src/commands/tasks.ts +86 -83
  29. package/src/commands/use.ts +149 -149
  30. package/src/config/configService.ts +56 -40
  31. package/src/context/runtimeContext.ts +42 -42
  32. package/src/git/gitService.ts +29 -29
  33. package/src/index.ts +133 -133
  34. package/src/services/taskContext.ts +32 -32
  35. package/src/services/taskService.ts +53 -45
  36. package/src/utils/api.ts +41 -41
  37. package/src/utils/config.ts +48 -48
  38. package/src/utils/ui.ts +46 -46
  39. package/tsconfig.json +18 -18
  40. package/vitest.config.ts +8 -8
@@ -1,29 +1,29 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
- import { simpleGit } from 'simple-git'
4
-
5
- export function checkRepoExists(repoPath: string): boolean {
6
- return fs.existsSync(repoPath) && fs.statSync(repoPath).isDirectory()
7
- }
8
-
9
- export function checkGitRepo(repoPath: string): boolean {
10
- const gitPath = path.join(repoPath, '.git')
11
- return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory()
12
- }
13
-
14
- export async function checkUncommittedChanges(repoPath: string): Promise<boolean> {
15
- const git = simpleGit(repoPath)
16
- const status = await git.status()
17
-
18
- return !status.isClean()
19
- }
20
-
21
- export async function fetch(repoPath: string): Promise<void> {
22
- const git = simpleGit(repoPath)
23
- await git.fetch()
24
- }
25
-
26
- export async function checkoutBranch(repoPath: string, branch: string): Promise<void> {
27
- const git = simpleGit(repoPath)
28
- await git.checkout(branch)
29
- }
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { simpleGit } from 'simple-git'
4
+
5
+ export function checkRepoExists(repoPath: string): boolean {
6
+ return fs.existsSync(repoPath) && fs.statSync(repoPath).isDirectory()
7
+ }
8
+
9
+ export function checkGitRepo(repoPath: string): boolean {
10
+ const gitPath = path.join(repoPath, '.git')
11
+ return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory()
12
+ }
13
+
14
+ export async function checkUncommittedChanges(repoPath: string): Promise<boolean> {
15
+ const git = simpleGit(repoPath)
16
+ const status = await git.status()
17
+
18
+ return !status.isClean()
19
+ }
20
+
21
+ export async function fetch(repoPath: string): Promise<void> {
22
+ const git = simpleGit(repoPath)
23
+ await git.fetch()
24
+ }
25
+
26
+ export async function checkoutBranch(repoPath: string, branch: string): Promise<void> {
27
+ const git = simpleGit(repoPath)
28
+ await git.checkout(branch)
29
+ }
package/src/index.ts CHANGED
@@ -1,133 +1,133 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander'
3
- import chalk from 'chalk'
4
- import { loginCommand, logoutCommand, loginStatusCommand } from './commands/login.js'
5
- import { useCommand } from './commands/use.js'
6
- import { registerRepoCommands } from './commands/repo.js'
7
- import { tasksCommand } from './commands/tasks.js'
8
- import { checkoutCommand } from './commands/checkout.js'
9
- import { loadConfig, getConfigPath } from './utils/config.js'
10
- import { cmd, fail, kv, muted, ok, section, title } from './utils/ui.js'
11
-
12
- const program = new Command()
13
-
14
- program
15
- .name('fiftth')
16
- .description('Startup-grade CLI for autonomous software teams')
17
- .version('0.1.0', '-v, --version', 'Output the current version')
18
- .addHelpText(
19
- 'before',
20
- `\n${title('Fiftth CLI')}\n${muted('start orchestrating your AI engineering workflows.')}\n`,
21
- )
22
- .addHelpText(
23
- 'after',
24
- `
25
- ${chalk.dim('Examples:')}
26
- ${cmd('$ fiftth login')} Authenticate with Fiftth
27
- ${cmd('$ fiftth login --token <tok>')} Authenticate using a token directly
28
- ${cmd('$ fiftth use my-workspace')} Switch to a workspace
29
- ${cmd('$ fiftth use')} Interactively select a workspace
30
- ${cmd('$ fiftth use --list')} List all available workspaces
31
- ${cmd('$ fiftth repo add api ~/dev/api')} Register a local git repository
32
- ${cmd('$ fiftth repo list')} List configured repositories
33
- ${cmd('$ fiftth tasks')} Select an active task and inspect repos
34
- ${cmd('$ fiftth checkout owner/repo')} Checkout selected task branch
35
- `,
36
- )
37
-
38
- // ─── fiftth login ──────────────────────────────────────────────────────────
39
- const loginCmd = program
40
- .command('login')
41
- .description('Authenticate with the Fiftth platform')
42
- .option('-t, --token <token>', 'Access token (for non-interactive / CI use)')
43
- .option('--host <host>', 'API host (defaults to https://api.fiftth.io)')
44
- .addHelpText(
45
- 'after',
46
- `
47
- ${chalk.dim('Examples:')}
48
- ${chalk.cyan('$ fiftth login')}
49
- ${chalk.cyan('$ fiftth login --token ghp_xxxxxxxxxxxx')}
50
- ${chalk.cyan('$ fiftth login --host https://selfhosted.example.com')}
51
- `,
52
- )
53
-
54
- loginCmd
55
- .command('status')
56
- .description('Show current authentication status')
57
- .action(async () => {
58
- await loginStatusCommand().catch(handleError)
59
- })
60
-
61
- loginCmd
62
- .command('logout')
63
- .description('Log out and remove stored credentials')
64
- .action(async () => {
65
- await logoutCommand().catch(handleError)
66
- })
67
-
68
- loginCmd.action(async (options: { token?: string; host?: string }) => {
69
- await loginCommand(options).catch(handleError)
70
- })
71
-
72
- // ─── fiftth use ────────────────────────────────────────────────────────────
73
- program
74
- .command('use [workspace]')
75
- .description('Select the active workspace')
76
- .option('-l, --list', 'List all available workspaces')
77
- .addHelpText(
78
- 'after',
79
- `
80
- ${chalk.dim('Examples:')}
81
- ${chalk.cyan('$ fiftth use')} Interactively select a workspace
82
- ${chalk.cyan('$ fiftth use my-workspace')} Switch directly to a workspace by slug or name
83
- ${chalk.cyan('$ fiftth use --list')} List all workspaces
84
- `,
85
- )
86
- .action(async (workspace: string | undefined, options: { list?: boolean }) => {
87
- await useCommand(workspace, options).catch(handleError)
88
- })
89
-
90
- // ─── fiftth config ─────────────────────────────────────────────────────────
91
- program
92
- .command('config')
93
- .description('Show current CLI configuration')
94
- .action(() => {
95
- const config = loadConfig()
96
- console.log(`\n${section('CLI configuration')}\n`)
97
- console.log(kv('Config file', cmd(getConfigPath())))
98
- console.log(kv('Host', cmd(config.host)))
99
- console.log(kv('Workspace', config.workspace ? cmd(config.workspace) : muted('(none)')))
100
- console.log(kv('Logged in', config.token ? ok('yes') : fail('no')))
101
- console.log()
102
- })
103
-
104
- // ─── fiftth repo ───────────────────────────────────────────────────────────
105
- registerRepoCommands(program)
106
-
107
- // ─── fiftth tasks ──────────────────────────────────────────────────────────
108
- program
109
- .command('tasks')
110
- .description('List and select active tasks')
111
- .action(async () => {
112
- await tasksCommand().catch(handleError)
113
- })
114
-
115
- // ─── fiftth checkout ───────────────────────────────────────────────────────
116
- program
117
- .command('checkout <repoName>')
118
- .description('Checkout the selected task branch for a repository')
119
- .action(async (repoName: string) => {
120
- await checkoutCommand(repoName).catch(handleError)
121
- })
122
-
123
- // ─── Error handling ────────────────────────────────────────────────────────
124
- function handleError(err: unknown): void {
125
- if (err instanceof Error) {
126
- console.error(`\n${fail(`Error: ${err.message}`)}\n`)
127
- } else {
128
- console.error(`\n${fail('An unexpected error occurred.')}\n`)
129
- }
130
- process.exit(1)
131
- }
132
-
133
- program.parse()
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander'
3
+ import chalk from 'chalk'
4
+ import { loginCommand, logoutCommand, loginStatusCommand } from './commands/login.js'
5
+ import { useCommand } from './commands/use.js'
6
+ import { registerRepoCommands } from './commands/repo.js'
7
+ import { tasksCommand } from './commands/tasks.js'
8
+ import { checkoutCommand } from './commands/checkout.js'
9
+ import { loadConfig, getConfigPath } from './utils/config.js'
10
+ import { cmd, fail, kv, muted, ok, section, title } from './utils/ui.js'
11
+
12
+ const program = new Command()
13
+
14
+ program
15
+ .name('fiftth')
16
+ .description('Startup-grade CLI for autonomous software teams')
17
+ .version('0.1.0', '-v, --version', 'Output the current version')
18
+ .addHelpText(
19
+ 'before',
20
+ `\n${title('Fiftth CLI')}\n${muted('start orchestrating your AI engineering workflows.')}\n`,
21
+ )
22
+ .addHelpText(
23
+ 'after',
24
+ `
25
+ ${chalk.dim('Examples:')}
26
+ ${cmd('$ fiftth login')} Authenticate with Fiftth
27
+ ${cmd('$ fiftth login --token <tok>')} Authenticate using a token directly
28
+ ${cmd('$ fiftth use my-workspace')} Switch to a workspace
29
+ ${cmd('$ fiftth use')} Interactively select a workspace
30
+ ${cmd('$ fiftth use --list')} List all available workspaces
31
+ ${cmd('$ fiftth repo add api ~/dev/api')} Register a local git repository
32
+ ${cmd('$ fiftth repo list')} List configured repositories
33
+ ${cmd('$ fiftth tasks')} Select an active task and inspect repos
34
+ ${cmd('$ fiftth checkout <taskId>')} Checkout task branches in all linked repositories
35
+ `,
36
+ )
37
+
38
+ // ─── fiftth login ──────────────────────────────────────────────────────────
39
+ const loginCmd = program
40
+ .command('login')
41
+ .description('Authenticate with the Fiftth platform')
42
+ .option('-t, --token <token>', 'Access token (for non-interactive / CI use)')
43
+ .option('--host <host>', 'API host (defaults to https://api.fiftth.io)')
44
+ .addHelpText(
45
+ 'after',
46
+ `
47
+ ${chalk.dim('Examples:')}
48
+ ${chalk.cyan('$ fiftth login')}
49
+ ${chalk.cyan('$ fiftth login --token ghp_xxxxxxxxxxxx')}
50
+ ${chalk.cyan('$ fiftth login --host https://selfhosted.example.com')}
51
+ `,
52
+ )
53
+
54
+ loginCmd
55
+ .command('status')
56
+ .description('Show current authentication status')
57
+ .action(async () => {
58
+ await loginStatusCommand().catch(handleError)
59
+ })
60
+
61
+ loginCmd
62
+ .command('logout')
63
+ .description('Log out and remove stored credentials')
64
+ .action(async () => {
65
+ await logoutCommand().catch(handleError)
66
+ })
67
+
68
+ loginCmd.action(async (options: { token?: string; host?: string }) => {
69
+ await loginCommand(options).catch(handleError)
70
+ })
71
+
72
+ // ─── fiftth use ────────────────────────────────────────────────────────────
73
+ program
74
+ .command('use [workspace]')
75
+ .description('Select the active workspace')
76
+ .option('-l, --list', 'List all available workspaces')
77
+ .addHelpText(
78
+ 'after',
79
+ `
80
+ ${chalk.dim('Examples:')}
81
+ ${chalk.cyan('$ fiftth use')} Interactively select a workspace
82
+ ${chalk.cyan('$ fiftth use my-workspace')} Switch directly to a workspace by slug or name
83
+ ${chalk.cyan('$ fiftth use --list')} List all workspaces
84
+ `,
85
+ )
86
+ .action(async (workspace: string | undefined, options: { list?: boolean }) => {
87
+ await useCommand(workspace, options).catch(handleError)
88
+ })
89
+
90
+ // ─── fiftth config ─────────────────────────────────────────────────────────
91
+ program
92
+ .command('config')
93
+ .description('Show current CLI configuration')
94
+ .action(() => {
95
+ const config = loadConfig()
96
+ console.log(`\n${section('CLI configuration')}\n`)
97
+ console.log(kv('Config file', cmd(getConfigPath())))
98
+ console.log(kv('Host', cmd(config.host)))
99
+ console.log(kv('Workspace', config.workspace ? cmd(config.workspace) : muted('(none)')))
100
+ console.log(kv('Logged in', config.token ? ok('yes') : fail('no')))
101
+ console.log()
102
+ })
103
+
104
+ // ─── fiftth repo ───────────────────────────────────────────────────────────
105
+ registerRepoCommands(program)
106
+
107
+ // ─── fiftth tasks ──────────────────────────────────────────────────────────
108
+ program
109
+ .command('tasks')
110
+ .description('List and select active tasks')
111
+ .action(async () => {
112
+ await tasksCommand().catch(handleError)
113
+ })
114
+
115
+ // ─── fiftth checkout ───────────────────────────────────────────────────────
116
+ program
117
+ .command('checkout <taskId>')
118
+ .description('Checkout the selected task branches for all linked repositories')
119
+ .action(async (taskId: string) => {
120
+ await checkoutCommand(taskId).catch(handleError)
121
+ })
122
+
123
+ // ─── Error handling ────────────────────────────────────────────────────────
124
+ function handleError(err: unknown): void {
125
+ if (err instanceof Error) {
126
+ console.error(`\n${fail(`Error: ${err.message}`)}\n`)
127
+ } else {
128
+ console.error(`\n${fail('An unexpected error occurred.')}\n`)
129
+ }
130
+ process.exit(1)
131
+ }
132
+
133
+ program.parse()
@@ -1,32 +1,32 @@
1
- export interface SelectedTaskRepository {
2
- repoName: string
3
- branch: string
4
- }
5
-
6
- export interface SelectedTaskContext {
7
- taskId: string
8
- title: string
9
- repositories: SelectedTaskRepository[]
10
- }
11
-
12
- let selectedTaskContext: SelectedTaskContext | null = null
13
-
14
- export function setSelectedTaskContext(task: SelectedTaskContext): void {
15
- selectedTaskContext = task
16
- }
17
-
18
- export function getSelectedTaskContext(): SelectedTaskContext | null {
19
- return selectedTaskContext
20
- }
21
-
22
- export function getSelectedTaskBranch(repoName: string): string | undefined {
23
- const repository = selectedTaskContext?.repositories.find(
24
- (entry) => entry.repoName === repoName,
25
- )
26
-
27
- return repository?.branch
28
- }
29
-
30
- export function clearSelectedTaskContext(): void {
31
- selectedTaskContext = null
32
- }
1
+ export interface SelectedTaskRepository {
2
+ repoName: string
3
+ branch: string
4
+ }
5
+
6
+ export interface SelectedTaskContext {
7
+ taskId: string
8
+ title: string
9
+ repositories: SelectedTaskRepository[]
10
+ }
11
+
12
+ let selectedTaskContext: SelectedTaskContext | null = null
13
+
14
+ export function setSelectedTaskContext(task: SelectedTaskContext): void {
15
+ selectedTaskContext = task
16
+ }
17
+
18
+ export function getSelectedTaskContext(): SelectedTaskContext | null {
19
+ return selectedTaskContext
20
+ }
21
+
22
+ export function getSelectedTaskBranch(repoName: string): string | undefined {
23
+ const repository = selectedTaskContext?.repositories.find(
24
+ (entry) => entry.repoName === repoName,
25
+ )
26
+
27
+ return repository?.branch
28
+ }
29
+
30
+ export function clearSelectedTaskContext(): void {
31
+ selectedTaskContext = null
32
+ }
@@ -1,45 +1,53 @@
1
- import { createApiClient } from '../api/client.js'
2
-
3
- const ALLOWED_COLUMNS = new Set(['IN_PROGRESS', 'HUMAN_REVIEW'])
4
-
5
- interface ApiTaskRepository {
6
- fullName: string
7
- githubPrBranch: string
8
- }
9
-
10
- interface ApiTask {
11
- id: string
12
- title: string
13
- column: string
14
- repositories?: ApiTaskRepository[]
15
- }
16
-
17
- export interface TaskRepository {
18
- fullName: string
19
- branch: string
20
- }
21
-
22
- export interface Task {
23
- id: string
24
- title: string
25
- column: string
26
- repositories: TaskRepository[]
27
- }
28
-
29
- export async function fetchActiveTasks(workspaceId: string): Promise<Task[]> {
30
- const client = createApiClient()
31
- const query = new URLSearchParams({ workspaceId }).toString()
32
- const tasks = await client.get<ApiTask[]>(`/tasks?${query}`)
33
-
34
- return tasks
35
- .filter((task) => ALLOWED_COLUMNS.has(task.column))
36
- .map((task) => ({
37
- id: task.id,
38
- title: task.title,
39
- column: task.column,
40
- repositories: (task.repositories ?? []).map((repository) => ({
41
- fullName: repository.fullName,
42
- branch: repository.githubPrBranch,
43
- })),
44
- }))
45
- }
1
+ import { createApiClient } from '../api/client.js'
2
+
3
+ const ALLOWED_COLUMNS = new Set(['IN_PROGRESS', 'HUMAN_REVIEW'])
4
+
5
+ interface ApiTaskRepository {
6
+ fullName: string
7
+ githubPrBranch: string
8
+ }
9
+
10
+ interface ApiTask {
11
+ id: string
12
+ title: string
13
+ column: string
14
+ repositories?: ApiTaskRepository[]
15
+ }
16
+
17
+ export interface TaskRepository {
18
+ fullName: string
19
+ branch: string
20
+ }
21
+
22
+ export interface Task {
23
+ id: string
24
+ title: string
25
+ column: string
26
+ repositories: TaskRepository[]
27
+ }
28
+
29
+ export async function moveTaskToHumanReview(taskId: string): Promise<void> {
30
+ const client = createApiClient()
31
+ await client.post(`/tasks/${taskId}/move`, {
32
+ targetColumn: 'HUMAN_REVIEW',
33
+ targetOrder: 0,
34
+ })
35
+ }
36
+
37
+ export async function fetchActiveTasks(workspaceId: string): Promise<Task[]> {
38
+ const client = createApiClient()
39
+ const query = new URLSearchParams({ workspaceId }).toString()
40
+ const tasks = await client.get<ApiTask[]>(`/tasks?${query}`)
41
+
42
+ return tasks
43
+ .filter((task) => ALLOWED_COLUMNS.has(task.column))
44
+ .map((task) => ({
45
+ id: task.id,
46
+ title: task.title,
47
+ column: task.column,
48
+ repositories: (task.repositories ?? []).map((repository) => ({
49
+ fullName: repository.fullName,
50
+ branch: repository.githubPrBranch,
51
+ })),
52
+ }))
53
+ }
package/src/utils/api.ts CHANGED
@@ -1,41 +1,41 @@
1
- import { loadConfig } from './config.js'
2
-
3
- export interface Workspace {
4
- id: string
5
- name: string
6
- slug: string
7
- }
8
-
9
- export interface ApiClient {
10
- getWorkspaces(): Promise<Workspace[]>
11
- }
12
-
13
- export function createApiClient(): ApiClient {
14
- const config = loadConfig()
15
-
16
- async function request<T>(endpoint: string): Promise<T> {
17
- const url = `${config.host}${endpoint}`
18
- const response = await fetch(url, {
19
- headers: {
20
- Authorization: `Bearer ${config.token}`,
21
- 'Content-Type': 'application/json',
22
- 'User-Agent': 'fiftth-cli/0.1.0',
23
- },
24
- })
25
-
26
- if (!response.ok) {
27
- const body = await response.text().catch(() => '')
28
- throw new Error(
29
- `API request failed: ${response.status} ${response.statusText}${body ? ` — ${body}` : ''}`,
30
- )
31
- }
32
-
33
- return response.json() as Promise<T>
34
- }
35
-
36
- return {
37
- getWorkspaces(): Promise<Workspace[]> {
38
- return request<Workspace[]>('/workspaces')
39
- },
40
- }
41
- }
1
+ import { loadConfig } from './config.js'
2
+
3
+ export interface Workspace {
4
+ id: string
5
+ name: string
6
+ slug: string
7
+ }
8
+
9
+ export interface ApiClient {
10
+ getWorkspaces(): Promise<Workspace[]>
11
+ }
12
+
13
+ export function createApiClient(): ApiClient {
14
+ const config = loadConfig()
15
+
16
+ async function request<T>(endpoint: string): Promise<T> {
17
+ const url = `${config.host}${endpoint}`
18
+ const response = await fetch(url, {
19
+ headers: {
20
+ Authorization: `Bearer ${config.token}`,
21
+ 'Content-Type': 'application/json',
22
+ 'User-Agent': 'fiftth-cli/0.1.0',
23
+ },
24
+ })
25
+
26
+ if (!response.ok) {
27
+ const body = await response.text().catch(() => '')
28
+ throw new Error(
29
+ `API request failed: ${response.status} ${response.statusText}${body ? ` — ${body}` : ''}`,
30
+ )
31
+ }
32
+
33
+ return response.json() as Promise<T>
34
+ }
35
+
36
+ return {
37
+ getWorkspaces(): Promise<Workspace[]> {
38
+ return request<Workspace[]>('/workspaces')
39
+ },
40
+ }
41
+ }