@sqaoss/flowy 1.0.2 → 1.1.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.
- package/README.md +61 -70
- package/package.json +4 -4
- package/skills/using-flowy/SKILL.md +78 -103
- package/src/commands/init.test.ts +174 -0
- package/src/commands/init.ts +50 -0
- package/src/commands/setup.test.ts +36 -2
- package/src/commands/setup.ts +31 -5
- package/src/index.ts +2 -0
package/README.md
CHANGED
|
@@ -1,62 +1,89 @@
|
|
|
1
1
|
# Flowy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Agentic persistent planning
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@sqaoss/flowy)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/sqaoss/flowy/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Jira, Linear, Trello are built for humans clicking boards. AI agents don't click boards. When your agent needs to plan work, track progress, and close tickets, those tools add friction, load context, and get in the way.
|
|
10
10
|
|
|
11
|
-
Flowy is
|
|
11
|
+
Flowy is where agents store plans and flow through execution. Features are master plans. Tasks are execution steps. Everything persists in a database, not as files cluttering your git history. Your agent flows through work without friction.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
You get full observability on what every agent planned, built, and shipped.
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Get Started
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
### Install (once)
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
npm i -g @sqaoss/flowy
|
|
21
|
+
flowy setup remote --email you@example.com
|
|
22
|
+
```
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
flowy setup local
|
|
24
|
+
### Initialize a project
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
```bash
|
|
27
|
+
cd my-project
|
|
28
|
+
flowy init # auto-detects repo, creates project
|
|
29
|
+
```
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
flowy project create "Auth System"
|
|
31
|
-
flowy project set "Auth System"
|
|
31
|
+
### Start planning
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
flowy feature create --title "
|
|
35
|
-
flowy feature set "
|
|
33
|
+
```bash
|
|
34
|
+
flowy feature create --title "User Auth" --description auth-spec.md
|
|
35
|
+
flowy feature set "User Auth"
|
|
36
36
|
|
|
37
|
-
# Create tasks
|
|
38
37
|
flowy task create --title "Implement OAuth" --description oauth.md
|
|
39
|
-
flowy task create --title "Write
|
|
38
|
+
flowy task create --title "Write tests" --description "Unit + integration"
|
|
40
39
|
|
|
41
|
-
# Track progress
|
|
42
40
|
flowy status <task-id> in_progress
|
|
43
41
|
flowy status <task-id> done
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Every command outputs JSON. Your agent reads it, acts on it, moves to the next task.
|
|
45
|
+
|
|
46
|
+
## Agent Skill
|
|
47
|
+
|
|
48
|
+
Flowy installs an agent skill during setup. Your AI agent automatically knows every command. No manual configuration needed.
|
|
49
|
+
|
|
50
|
+
Or install the skill manually: `npx skills add sqaoss/flowy`
|
|
51
|
+
|
|
52
|
+
See [skills/using-flowy/SKILL.md](skills/using-flowy/SKILL.md) for the full skill reference.
|
|
53
|
+
|
|
54
|
+
## Data Model
|
|
44
55
|
|
|
45
|
-
# Search and explore
|
|
46
|
-
flowy search "OAuth" --type task
|
|
47
|
-
flowy tree <project-id> --depth 3
|
|
48
56
|
```
|
|
57
|
+
project -> feature -> task
|
|
58
|
+
1:many 1:many
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Every task belongs to a feature. Every feature belongs to a project. No orphans.
|
|
49
62
|
|
|
50
|
-
|
|
63
|
+
### Status Flow
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
```
|
|
66
|
+
draft -> pending_review -> approved -> in_progress -> done
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Also: `blocked`, `cancelled`. Only `pending_review` entities can be approved.
|
|
70
|
+
|
|
71
|
+
## Self-Hosted
|
|
72
|
+
|
|
73
|
+
Run Flowy on your own machine with SQLite and Docker. Same CLI, same commands.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
flowy setup local # starts a local server via Docker
|
|
77
|
+
flowy init # auto-detects repo
|
|
78
|
+
```
|
|
53
79
|
|
|
54
80
|
## Command Reference
|
|
55
81
|
|
|
56
82
|
| Command | Description |
|
|
57
83
|
|---------|-------------|
|
|
84
|
+
| `setup remote --email <email>` | Register and connect to the hosted server |
|
|
58
85
|
| `setup local` | Start a local Docker server and configure the CLI |
|
|
59
|
-
| `
|
|
86
|
+
| `init` | Auto-detect repo and create/map project |
|
|
60
87
|
| `whoami` | Show current user |
|
|
61
88
|
| `client set name <name>` | Set client display name |
|
|
62
89
|
| `project create <name>` | Create project |
|
|
@@ -80,63 +107,27 @@ Remote mode connects to the hosted Flowy SaaS for multi-agent collaboration, sha
|
|
|
80
107
|
|
|
81
108
|
All commands output JSON to stdout.
|
|
82
109
|
|
|
83
|
-
## Data Model
|
|
84
|
-
|
|
85
|
-
### Entity Hierarchy
|
|
86
|
-
|
|
87
|
-
```
|
|
88
|
-
client -> project -> feature -> task
|
|
89
|
-
1:many 1:many 1:many
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Every entity belongs to its parent. No orphans.
|
|
93
|
-
|
|
94
|
-
### Status Flow
|
|
95
|
-
|
|
96
|
-
```
|
|
97
|
-
draft -> pending_review -> approved -> in_progress -> done
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Also: `blocked`, `cancelled`
|
|
101
|
-
|
|
102
|
-
### Description Field
|
|
103
|
-
|
|
104
|
-
`--description` accepts a file path or an inline string:
|
|
105
|
-
- `--description spec.md` -- reads file content
|
|
106
|
-
- `--description "Do the thing"` -- sends string as-is
|
|
107
|
-
|
|
108
110
|
## Configuration
|
|
109
111
|
|
|
110
112
|
Config is stored at `~/.config/flowy/config.json`.
|
|
111
113
|
|
|
112
114
|
| Variable | Description | Default |
|
|
113
115
|
|----------|-------------|---------|
|
|
114
|
-
| `FLOWY_API_URL` | GraphQL endpoint | `
|
|
115
|
-
| `FLOWY_API_KEY` | API key (remote mode
|
|
116
|
+
| `FLOWY_API_URL` | GraphQL endpoint | `https://flowy-ai.fly.dev/graphql` |
|
|
117
|
+
| `FLOWY_API_KEY` | API key (remote mode) | -- |
|
|
116
118
|
| `FLOWY_PROJECT` | Override active project by name | -- |
|
|
117
119
|
| `FLOWY_FEATURE` | Override active feature by ID | -- |
|
|
118
120
|
|
|
119
|
-
## For AI Agents
|
|
120
|
-
|
|
121
|
-
Flowy integrates with [TanStack Intent](https://github.com/TanStack/intent) for automatic tool discovery. Run:
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
npx @tanstack/intent install
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
This auto-discovers the Flowy skill and makes it available to your agent. See [`skills/using-flowy/SKILL.md`](skills/using-flowy/SKILL.md) for the full skill reference.
|
|
128
|
-
|
|
129
121
|
## Development
|
|
130
122
|
|
|
131
123
|
```bash
|
|
132
|
-
bun run test #
|
|
133
|
-
bun run check #
|
|
134
|
-
bun run typecheck # TypeScript
|
|
124
|
+
bun run test # CLI tests
|
|
125
|
+
bun run check # Lint + format
|
|
126
|
+
bun run typecheck # TypeScript
|
|
135
127
|
|
|
136
|
-
# Server tests
|
|
137
|
-
cd server && bunx --bun vitest run
|
|
128
|
+
cd server && bunx --bun vitest run # Server tests
|
|
138
129
|
```
|
|
139
130
|
|
|
140
131
|
## License
|
|
141
132
|
|
|
142
|
-
Apache-2.0
|
|
133
|
+
Apache-2.0. Copyright 2026 SQA & Automation SRL.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqaoss/flowy",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Agentic persistent planning",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"flowy": "./src/index.ts"
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"ai-agents",
|
|
43
43
|
"graphql",
|
|
44
44
|
"bun",
|
|
45
|
-
"
|
|
45
|
+
"agentic",
|
|
46
|
+
"planning"
|
|
46
47
|
],
|
|
47
48
|
"scripts": {
|
|
48
49
|
"cli": "bun src/index.ts",
|
|
@@ -60,7 +61,6 @@
|
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@biomejs/biome": "2.4.4",
|
|
63
|
-
"@tanstack/intent": "^0.0.23",
|
|
64
64
|
"@commitlint/cli": "^20.4.2",
|
|
65
65
|
"@commitlint/config-conventional": "^20.4.2",
|
|
66
66
|
"husky": "^9.1.7",
|
|
@@ -1,153 +1,128 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description:
|
|
4
|
-
metadata:
|
|
5
|
-
sources:
|
|
6
|
-
- src/commands/project.ts
|
|
7
|
-
- src/commands/feature.ts
|
|
8
|
-
- src/commands/task.ts
|
|
9
|
-
- src/commands/status.ts
|
|
10
|
-
- src/commands/approve.ts
|
|
11
|
-
- src/commands/search.ts
|
|
12
|
-
- src/commands/tree.ts
|
|
13
|
-
- src/commands/setup.ts
|
|
14
|
-
- src/util/config.ts
|
|
2
|
+
name: flowy
|
|
3
|
+
description: Store plans and track execution with Flowy CLI. Use when you need to create features, break work into tasks, track progress, or manage project structure. Trigger on any planning, task tracking, or work organization request.
|
|
15
4
|
---
|
|
16
5
|
|
|
17
|
-
# Flowy —
|
|
6
|
+
# Flowy — Agentic Persistent Planning
|
|
18
7
|
|
|
19
|
-
Flowy
|
|
8
|
+
Flowy gives you a persistent store for plans and execution tracking. Features are your master plans. Tasks are your execution steps. Everything persists across sessions, no files in git, no context lost.
|
|
20
9
|
|
|
21
|
-
##
|
|
10
|
+
## Why Use Flowy
|
|
22
11
|
|
|
23
|
-
|
|
24
|
-
# Install globally
|
|
25
|
-
bun add -g @sqaoss/flowy # or: npm i -g @sqaoss/flowy
|
|
12
|
+
Without Flowy, your plans live in markdown files that clutter git history, get deleted when done, and leave no record of what you accomplished. With Flowy, plans persist in a database. You flow through work without friction. Your human gets full observability.
|
|
26
13
|
|
|
27
|
-
|
|
28
|
-
flowy setup --mode saas --email you@example.com
|
|
29
|
-
export FLOWY_API_KEY=flowy_xxx_yyy
|
|
14
|
+
## First Time in a Project
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
flowy
|
|
16
|
+
```bash
|
|
17
|
+
flowy init # auto-detects the git repo, creates a project, maps this directory
|
|
33
18
|
```
|
|
34
19
|
|
|
35
|
-
|
|
20
|
+
If Flowy isn't set up yet, the human needs to run:
|
|
21
|
+
```bash
|
|
22
|
+
npm i -g @sqaoss/flowy
|
|
23
|
+
flowy setup remote --email their@email.com
|
|
24
|
+
```
|
|
36
25
|
|
|
37
|
-
|
|
26
|
+
## Core Workflow
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
```bash
|
|
29
|
+
# 1. Plan a feature (master plan)
|
|
30
|
+
flowy feature create --title "User Auth" --description auth-spec.md
|
|
31
|
+
flowy feature set "User Auth"
|
|
42
32
|
|
|
43
|
-
|
|
33
|
+
# 2. Break into tasks (execution steps)
|
|
34
|
+
flowy task create --title "Implement OAuth" --description oauth.md
|
|
35
|
+
flowy task create --title "Write tests" --description "Unit + integration tests"
|
|
44
36
|
|
|
45
|
-
|
|
37
|
+
# 3. Execute and track
|
|
38
|
+
flowy status <task-id> in_progress
|
|
39
|
+
# ... do the work ...
|
|
40
|
+
flowy status <task-id> done
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
# 4. Move to next task or feature
|
|
43
|
+
flowy feature create --title "API Rate Limiting" --description rate-limit.md
|
|
44
|
+
flowy feature set "API Rate Limiting"
|
|
45
|
+
```
|
|
49
46
|
|
|
50
|
-
|
|
47
|
+
## Entity Hierarchy
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
flowy whoami # Show current user
|
|
49
|
+
```
|
|
50
|
+
project -> feature -> task
|
|
51
|
+
1:many 1:many
|
|
57
52
|
```
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
Every task belongs to a feature. Every feature belongs to a project. No orphans. The project is set automatically by `flowy init`.
|
|
55
|
+
|
|
56
|
+
## Status Flow
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
draft -> pending_review -> approved -> in_progress -> done
|
|
62
60
|
```
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
Also: `blocked`, `cancelled`
|
|
63
|
+
|
|
64
|
+
Only `pending_review` entities can be approved via `flowy approve <id>`.
|
|
65
|
+
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
### Project Context
|
|
65
69
|
```bash
|
|
66
|
-
flowy
|
|
67
|
-
flowy project set "Auth System" # Map current dir to project
|
|
70
|
+
flowy init # Auto-detect repo, create + set project
|
|
68
71
|
flowy project list # List all projects
|
|
69
|
-
flowy project show
|
|
70
|
-
flowy project show <id> # Show specific project
|
|
72
|
+
flowy project show [<id>] # Show project details
|
|
71
73
|
```
|
|
72
74
|
|
|
73
75
|
### Features (requires active project)
|
|
74
76
|
```bash
|
|
75
|
-
flowy feature create --title "
|
|
76
|
-
flowy feature
|
|
77
|
-
flowy feature set "SSO Support" # Set active feature
|
|
77
|
+
flowy feature create --title "Title" --description "description or file.md"
|
|
78
|
+
flowy feature set "Title or ID" # Set active feature
|
|
78
79
|
flowy feature unset # Clear active feature
|
|
79
80
|
flowy feature list # List features in project
|
|
80
|
-
flowy feature show
|
|
81
|
+
flowy feature show [<id>] # Show feature details
|
|
81
82
|
```
|
|
82
83
|
|
|
83
84
|
### Tasks (requires active feature)
|
|
84
85
|
```bash
|
|
85
|
-
flowy task create --title "
|
|
86
|
-
flowy task create --title "Write tests" --description "Unit tests for auth"
|
|
86
|
+
flowy task create --title "Title" --description "description or file.md"
|
|
87
87
|
flowy task list # List tasks in feature
|
|
88
88
|
flowy task show <id> # Show task details
|
|
89
|
-
flowy task block <
|
|
90
|
-
flowy task unblock <
|
|
89
|
+
flowy task block <blocker> <blocked> # Mark dependency
|
|
90
|
+
flowy task unblock <blocker> <blocked>
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
### Status
|
|
93
|
+
### Status and Approval
|
|
94
94
|
```bash
|
|
95
95
|
flowy status <id> in_progress
|
|
96
|
-
flowy status <id> done
|
|
97
96
|
flowy status <id> pending_review
|
|
98
|
-
flowy approve <id> #
|
|
97
|
+
flowy approve <id> # Only works on pending_review
|
|
98
|
+
flowy status <id> done
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
### Search
|
|
101
|
+
### Search and Explore
|
|
102
102
|
```bash
|
|
103
|
-
flowy search "
|
|
104
|
-
flowy
|
|
105
|
-
flowy tree <id> --depth 3 # Show subtree
|
|
103
|
+
flowy search "query" --type task --status draft --limit 10
|
|
104
|
+
flowy tree <project-id> --depth 3 # Show full subtree
|
|
106
105
|
```
|
|
107
106
|
|
|
108
|
-
##
|
|
109
|
-
- `client` — a client or company
|
|
110
|
-
- `project` — a codebase or product
|
|
111
|
-
- `feature` — a unit of work (replaces epic)
|
|
112
|
-
- `task` — an individual work item
|
|
113
|
-
|
|
114
|
-
## Status Flow
|
|
115
|
-
`draft` → `pending_review` → `approved` → `in_progress` → `done`
|
|
116
|
-
|
|
117
|
-
Also: `blocked`, `cancelled`
|
|
118
|
-
|
|
119
|
-
## Description Field
|
|
120
|
-
`--description` accepts a file path or an inline string:
|
|
121
|
-
- `--description spec.md` — reads file, sends raw content
|
|
122
|
-
- `--description "Do the thing"` — sends string as-is
|
|
123
|
-
|
|
124
|
-
## Workflow Example
|
|
107
|
+
## Validation Rules
|
|
125
108
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
flowy project set "Auth System"
|
|
109
|
+
- **Title is required** and cannot be empty
|
|
110
|
+
- **Description** is optional, but if provided cannot be empty
|
|
111
|
+
- **--description** accepts a file path (reads content) or an inline string
|
|
112
|
+
- **Search** requires at least 3 characters
|
|
113
|
+
- **Status** must be one of: draft, pending_review, approved, in_progress, done, blocked, cancelled
|
|
114
|
+
- **Blocking**: a task cannot block itself
|
|
115
|
+
- **Edges**: both source and target nodes must exist
|
|
134
116
|
|
|
135
|
-
|
|
136
|
-
flowy feature create --title "SSO Support" --description sso-spec.md
|
|
137
|
-
flowy feature set "SSO Support"
|
|
117
|
+
## Output Format
|
|
138
118
|
|
|
139
|
-
|
|
140
|
-
flowy task create --title "Implement OAuth" --description oauth.md
|
|
141
|
-
flowy task create --title "Write auth tests" --description tests.md
|
|
119
|
+
All commands output JSON to stdout. Errors go to stderr as `{ "error": "message" }`.
|
|
142
120
|
|
|
143
|
-
|
|
144
|
-
flowy status <task-id> in_progress
|
|
145
|
-
flowy status <task-id> done
|
|
121
|
+
## Environment Variables
|
|
146
122
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
All commands output JSON. Parse with `jq` or directly in your agent code.
|
|
123
|
+
| Variable | Description |
|
|
124
|
+
|----------|-------------|
|
|
125
|
+
| `FLOWY_PROJECT` | Override active project by name |
|
|
126
|
+
| `FLOWY_FEATURE` | Override active feature by ID |
|
|
127
|
+
| `FLOWY_API_URL` | GraphQL endpoint |
|
|
128
|
+
| `FLOWY_API_KEY` | API key (from setup) |
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
let mockGraphql: ReturnType<typeof vi.fn>
|
|
4
|
+
let mockLoadConfig: ReturnType<typeof vi.fn>
|
|
5
|
+
let mockSaveConfig: ReturnType<typeof vi.fn>
|
|
6
|
+
let mockOutput: ReturnType<typeof vi.fn>
|
|
7
|
+
let mockOutputError: ReturnType<typeof vi.fn>
|
|
8
|
+
let mockSpawnSync: ReturnType<typeof vi.fn>
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockGraphql = vi.fn()
|
|
12
|
+
mockLoadConfig = vi.fn(() => ({
|
|
13
|
+
mode: 'saas',
|
|
14
|
+
apiUrl: 'https://flowy-ai.fly.dev/graphql',
|
|
15
|
+
apiKey: 'test-key',
|
|
16
|
+
client: { name: '' },
|
|
17
|
+
projects: {},
|
|
18
|
+
}))
|
|
19
|
+
mockSaveConfig = vi.fn()
|
|
20
|
+
mockOutput = vi.fn()
|
|
21
|
+
mockOutputError = vi.fn()
|
|
22
|
+
mockSpawnSync = vi.fn()
|
|
23
|
+
|
|
24
|
+
vi.doMock('../util/client.ts', () => ({
|
|
25
|
+
graphql: mockGraphql,
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
vi.doMock('../util/config.ts', () => ({
|
|
29
|
+
loadConfig: mockLoadConfig,
|
|
30
|
+
saveConfig: mockSaveConfig,
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
vi.doMock('../util/format.ts', () => ({
|
|
34
|
+
output: mockOutput,
|
|
35
|
+
outputError: mockOutputError,
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
vi.doMock('node:child_process', () => ({
|
|
39
|
+
spawnSync: mockSpawnSync,
|
|
40
|
+
}))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.resetModules()
|
|
45
|
+
vi.restoreAllMocks()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('init command', () => {
|
|
49
|
+
test('exports a command named "init" with no subcommands', async () => {
|
|
50
|
+
const { initCommand } = await import('./init.ts')
|
|
51
|
+
expect(initCommand.name()).toBe('init')
|
|
52
|
+
expect(initCommand.commands).toHaveLength(0)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('detects repo name from SSH git remote URL', async () => {
|
|
56
|
+
mockSpawnSync
|
|
57
|
+
.mockReturnValueOnce({
|
|
58
|
+
status: 0,
|
|
59
|
+
stdout: '/home/user/my-repo\n',
|
|
60
|
+
})
|
|
61
|
+
.mockReturnValueOnce({
|
|
62
|
+
status: 0,
|
|
63
|
+
stdout: 'git@github.com:sqaoss/flowy.git\n',
|
|
64
|
+
})
|
|
65
|
+
mockGraphql.mockResolvedValue({
|
|
66
|
+
createNode: { id: 'proj_123', title: 'flowy' },
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const { initCommand } = await import('./init.ts')
|
|
70
|
+
await initCommand.parseAsync([], { from: 'user' })
|
|
71
|
+
|
|
72
|
+
expect(mockGraphql).toHaveBeenCalledWith(
|
|
73
|
+
expect.stringContaining('createNode'),
|
|
74
|
+
expect.objectContaining({ type: 'project', title: 'flowy' }),
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('detects repo name from HTTPS git remote URL', async () => {
|
|
79
|
+
mockSpawnSync
|
|
80
|
+
.mockReturnValueOnce({
|
|
81
|
+
status: 0,
|
|
82
|
+
stdout: '/home/user/my-repo\n',
|
|
83
|
+
})
|
|
84
|
+
.mockReturnValueOnce({
|
|
85
|
+
status: 0,
|
|
86
|
+
stdout: 'https://github.com/sqaoss/flowy.git\n',
|
|
87
|
+
})
|
|
88
|
+
mockGraphql.mockResolvedValue({
|
|
89
|
+
createNode: { id: 'proj_123', title: 'flowy' },
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const { initCommand } = await import('./init.ts')
|
|
93
|
+
await initCommand.parseAsync([], { from: 'user' })
|
|
94
|
+
|
|
95
|
+
expect(mockGraphql).toHaveBeenCalledWith(
|
|
96
|
+
expect.stringContaining('createNode'),
|
|
97
|
+
expect.objectContaining({ type: 'project', title: 'flowy' }),
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('falls back to directory name when no remote', async () => {
|
|
102
|
+
mockSpawnSync
|
|
103
|
+
.mockReturnValueOnce({
|
|
104
|
+
status: 0,
|
|
105
|
+
stdout: '/home/user/my-cool-project\n',
|
|
106
|
+
})
|
|
107
|
+
.mockReturnValueOnce({
|
|
108
|
+
status: 1,
|
|
109
|
+
stdout: '',
|
|
110
|
+
})
|
|
111
|
+
mockGraphql.mockResolvedValue({
|
|
112
|
+
createNode: { id: 'proj_456', title: 'my-cool-project' },
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const { initCommand } = await import('./init.ts')
|
|
116
|
+
await initCommand.parseAsync([], { from: 'user' })
|
|
117
|
+
|
|
118
|
+
expect(mockGraphql).toHaveBeenCalledWith(
|
|
119
|
+
expect.stringContaining('createNode'),
|
|
120
|
+
expect.objectContaining({
|
|
121
|
+
type: 'project',
|
|
122
|
+
title: 'my-cool-project',
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('calls graphql to create project and maps directory via config', async () => {
|
|
128
|
+
mockSpawnSync
|
|
129
|
+
.mockReturnValueOnce({
|
|
130
|
+
status: 0,
|
|
131
|
+
stdout: '/home/user/flowy\n',
|
|
132
|
+
})
|
|
133
|
+
.mockReturnValueOnce({
|
|
134
|
+
status: 0,
|
|
135
|
+
stdout: 'git@github.com:sqaoss/flowy.git\n',
|
|
136
|
+
})
|
|
137
|
+
mockGraphql.mockResolvedValue({
|
|
138
|
+
createNode: { id: 'proj_789', title: 'flowy' },
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const { initCommand } = await import('./init.ts')
|
|
142
|
+
await initCommand.parseAsync([], { from: 'user' })
|
|
143
|
+
|
|
144
|
+
expect(mockGraphql).toHaveBeenCalledOnce()
|
|
145
|
+
expect(mockSaveConfig).toHaveBeenCalledWith(
|
|
146
|
+
expect.objectContaining({
|
|
147
|
+
projects: expect.objectContaining({
|
|
148
|
+
[process.cwd()]: { id: 'proj_789', name: 'flowy' },
|
|
149
|
+
}),
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
expect(mockOutput).toHaveBeenCalledWith({
|
|
153
|
+
id: 'proj_789',
|
|
154
|
+
name: 'flowy',
|
|
155
|
+
directory: process.cwd(),
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('throws when not in a git repo', async () => {
|
|
160
|
+
mockSpawnSync.mockReturnValueOnce({
|
|
161
|
+
status: 128,
|
|
162
|
+
stdout: '',
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const { initCommand } = await import('./init.ts')
|
|
166
|
+
await initCommand.parseAsync([], { from: 'user' })
|
|
167
|
+
|
|
168
|
+
expect(mockOutputError).toHaveBeenCalledWith(
|
|
169
|
+
expect.objectContaining({
|
|
170
|
+
message: expect.stringContaining('Not a git repository'),
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { basename } from 'node:path'
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
import { graphql } from '../util/client.ts'
|
|
5
|
+
import { loadConfig, saveConfig } from '../util/config.ts'
|
|
6
|
+
import { output, outputError } from '../util/format.ts'
|
|
7
|
+
|
|
8
|
+
export const initCommand = new Command('init')
|
|
9
|
+
.description('Initialize Flowy for the current git repository')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const toplevel = spawnSync('git', ['rev-parse', '--show-toplevel'])
|
|
13
|
+
if (toplevel.status !== 0) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'Not a git repository. Run flowy init from inside a git project.',
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let repoName: string
|
|
20
|
+
const remote = spawnSync('git', ['remote', 'get-url', 'origin'])
|
|
21
|
+
if (remote.status === 0) {
|
|
22
|
+
const url = String(remote.stdout).trim()
|
|
23
|
+
repoName =
|
|
24
|
+
url
|
|
25
|
+
.split('/')
|
|
26
|
+
.pop()
|
|
27
|
+
?.replace(/\.git$/, '') ?? ''
|
|
28
|
+
} else {
|
|
29
|
+
repoName = basename(String(toplevel.stdout).trim())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = await graphql<{ createNode: { id: string; title: string } }>(
|
|
33
|
+
`mutation CreateProject($type: String!, $title: String!) {
|
|
34
|
+
createNode(type: $type, title: $title) {
|
|
35
|
+
id type title description status metadata createdAt updatedAt
|
|
36
|
+
}
|
|
37
|
+
}`,
|
|
38
|
+
{ type: 'project', title: repoName },
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const { id, title } = data.createNode
|
|
42
|
+
const config = loadConfig()
|
|
43
|
+
const cwd = process.cwd()
|
|
44
|
+
config.projects[cwd] = { id, name: title }
|
|
45
|
+
saveConfig(config)
|
|
46
|
+
output({ id, name: title, directory: cwd })
|
|
47
|
+
} catch (error) {
|
|
48
|
+
outputError(error)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
@@ -88,13 +88,47 @@ describe('setup command', () => {
|
|
|
88
88
|
)
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
test('setup remote
|
|
91
|
+
test('setup remote requires --email', async () => {
|
|
92
92
|
const { setupCommand } = await import('./setup.ts')
|
|
93
93
|
await setupCommand.parseAsync(['remote'], { from: 'user' })
|
|
94
94
|
|
|
95
|
+
expect(mockOutputError).toHaveBeenCalledWith(
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
message: expect.stringContaining('--email is required'),
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('setup remote registers, saves API key, and outputs result', async () => {
|
|
103
|
+
const mockGraphql = vi.fn().mockResolvedValue({
|
|
104
|
+
register: {
|
|
105
|
+
user: { id: 'user_1', email: 'test@example.com', tier: 'free' },
|
|
106
|
+
apiKey: 'flowy_test_key_123',
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
vi.doMock('../util/client.ts', () => ({
|
|
110
|
+
graphql: mockGraphql,
|
|
111
|
+
}))
|
|
112
|
+
mockSpawnSync.mockReturnValue({
|
|
113
|
+
status: 0,
|
|
114
|
+
stdout: Buffer.from(''),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const { setupCommand } = await import('./setup.ts')
|
|
118
|
+
await setupCommand.parseAsync(['remote', '--email', 'test@example.com'], {
|
|
119
|
+
from: 'user',
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
expect(mockSaveConfig).toHaveBeenCalledWith(
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
mode: 'remote',
|
|
125
|
+
apiKey: 'flowy_test_key_123',
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
95
128
|
expect(mockOutput).toHaveBeenCalledWith(
|
|
96
129
|
expect.objectContaining({
|
|
97
|
-
|
|
130
|
+
user: expect.objectContaining({ email: 'test@example.com' }),
|
|
131
|
+
apiKey: 'flowy_test_key_123',
|
|
98
132
|
}),
|
|
99
133
|
)
|
|
100
134
|
})
|
package/src/commands/setup.ts
CHANGED
|
@@ -70,13 +70,39 @@ setupCommand
|
|
|
70
70
|
|
|
71
71
|
setupCommand
|
|
72
72
|
.command('remote')
|
|
73
|
-
.description('
|
|
74
|
-
.
|
|
73
|
+
.description('Connect to the hosted Flowy service')
|
|
74
|
+
.option('--email <email>', 'Email address for registration')
|
|
75
|
+
.action(async (opts) => {
|
|
75
76
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
if (!opts.email) {
|
|
78
|
+
throw new Error('--email is required for registration')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { graphql } = await import('../util/client.ts')
|
|
82
|
+
|
|
83
|
+
const config = loadConfig()
|
|
84
|
+
config.mode = 'remote'
|
|
85
|
+
config.apiUrl = 'https://flowy-ai.fly.dev/graphql'
|
|
86
|
+
saveConfig(config)
|
|
87
|
+
|
|
88
|
+
const data = await graphql(
|
|
89
|
+
`mutation Register($email: String!) {
|
|
90
|
+
register(email: $email) {
|
|
91
|
+
user { id email tier }
|
|
92
|
+
apiKey
|
|
93
|
+
}
|
|
94
|
+
}`,
|
|
95
|
+
{ email: opts.email },
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
config.apiKey = data.register.apiKey
|
|
99
|
+
saveConfig(config)
|
|
100
|
+
|
|
101
|
+
spawnSync('npx', ['skills', 'add', 'sqaoss/flowy', '--yes'], {
|
|
102
|
+
stdio: 'inherit',
|
|
79
103
|
})
|
|
104
|
+
|
|
105
|
+
output(data.register)
|
|
80
106
|
} catch (error) {
|
|
81
107
|
outputError(error)
|
|
82
108
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from 'commander'
|
|
|
3
3
|
import { approveCommand } from './commands/approve.ts'
|
|
4
4
|
import { clientCommand } from './commands/client.ts'
|
|
5
5
|
import { featureCommand } from './commands/feature.ts'
|
|
6
|
+
import { initCommand } from './commands/init.ts'
|
|
6
7
|
import { projectCommand } from './commands/project.ts'
|
|
7
8
|
import { searchCommand } from './commands/search.ts'
|
|
8
9
|
import { setupCommand } from './commands/setup.ts'
|
|
@@ -16,6 +17,7 @@ const program = new Command()
|
|
|
16
17
|
.description('Project management for AI coding agents')
|
|
17
18
|
.version('0.2.0')
|
|
18
19
|
|
|
20
|
+
program.addCommand(initCommand)
|
|
19
21
|
program.addCommand(setupCommand)
|
|
20
22
|
program.addCommand(clientCommand)
|
|
21
23
|
program.addCommand(projectCommand)
|