@sqaoss/flowy 1.7.0 → 1.8.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 +14 -0
- package/package.json +2 -1
- package/server/src/contract.test.ts +407 -0
- package/src/commands/approve.ts +2 -6
- package/src/commands/billing.test.ts +32 -0
- package/src/commands/billing.ts +4 -5
- package/src/commands/export.ts +8 -19
- package/src/commands/feature.ts +28 -48
- package/src/commands/import.ts +14 -24
- package/src/commands/init.ts +2 -5
- package/src/commands/key.test.ts +36 -1
- package/src/commands/key.ts +9 -9
- package/src/commands/project.ts +23 -43
- package/src/commands/search.ts +7 -13
- package/src/commands/setup.test.ts +48 -1
- package/src/commands/setup.ts +2 -10
- package/src/commands/status.ts +5 -8
- package/src/commands/task.ts +48 -87
- package/src/commands/tree.ts +5 -8
- package/src/commands/whoami.test.ts +34 -1
- package/src/commands/whoami.ts +8 -8
- package/src/util/config.test.ts +168 -1
- package/src/util/config.ts +188 -18
- package/src/util/operations.test.ts +114 -0
- package/src/util/operations.ts +331 -0
package/src/commands/feature.ts
CHANGED
|
@@ -7,6 +7,15 @@ import {
|
|
|
7
7
|
} from '../util/config.ts'
|
|
8
8
|
import { resolveDescription } from '../util/description.ts'
|
|
9
9
|
import { output, outputError } from '../util/format.ts'
|
|
10
|
+
import {
|
|
11
|
+
CREATE_EDGE,
|
|
12
|
+
CREATE_NODE,
|
|
13
|
+
DELETE_NODE,
|
|
14
|
+
DESCENDANTS,
|
|
15
|
+
DESCENDANTS_BRIEF,
|
|
16
|
+
GET_NODE,
|
|
17
|
+
UPDATE_NODE,
|
|
18
|
+
} from '../util/operations.ts'
|
|
10
19
|
|
|
11
20
|
export const featureCommand = new Command('feature').description(
|
|
12
21
|
'Manage features in the active project',
|
|
@@ -32,22 +41,15 @@ featureCommand
|
|
|
32
41
|
descriptionFile: opts.descriptionFile,
|
|
33
42
|
})
|
|
34
43
|
const nodeData = await graphql<{ createNode: { id: string } }>(
|
|
35
|
-
|
|
36
|
-
createNode(type: $type, title: $title, description: $description) {
|
|
37
|
-
id type title description status createdAt updatedAt
|
|
38
|
-
}
|
|
39
|
-
}`,
|
|
44
|
+
CREATE_NODE,
|
|
40
45
|
{ type: 'feature', title: opts.title, description },
|
|
41
46
|
)
|
|
42
47
|
const featureId = nodeData.createNode.id
|
|
43
|
-
await graphql(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}`,
|
|
49
|
-
{ sourceId: featureId, targetId: project.id, relation: 'part_of' },
|
|
50
|
-
)
|
|
48
|
+
await graphql(CREATE_EDGE, {
|
|
49
|
+
sourceId: featureId,
|
|
50
|
+
targetId: project.id,
|
|
51
|
+
relation: 'part_of',
|
|
52
|
+
})
|
|
51
53
|
output(nodeData.createNode)
|
|
52
54
|
} catch (error) {
|
|
53
55
|
outputError(error)
|
|
@@ -63,14 +65,11 @@ featureCommand
|
|
|
63
65
|
const project = requireProject()
|
|
64
66
|
const data = await graphql<{
|
|
65
67
|
descendants: Array<{ id: string; type: string; title: string }>
|
|
66
|
-
}>(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}`,
|
|
72
|
-
{ nodeId: project.id, relation: 'part_of', maxDepth: 1 },
|
|
73
|
-
)
|
|
68
|
+
}>(DESCENDANTS_BRIEF, {
|
|
69
|
+
nodeId: project.id,
|
|
70
|
+
relation: 'part_of',
|
|
71
|
+
maxDepth: 1,
|
|
72
|
+
})
|
|
74
73
|
const features = data.descendants.filter((n) => n.type === 'feature')
|
|
75
74
|
const match = features.find(
|
|
76
75
|
(f) => f.id === nameOrId || f.title === nameOrId,
|
|
@@ -111,14 +110,7 @@ featureCommand
|
|
|
111
110
|
const project = requireProject()
|
|
112
111
|
const data = await graphql<{
|
|
113
112
|
descendants: Array<{ id: string; type: string }>
|
|
114
|
-
}>(
|
|
115
|
-
`query Descendants($nodeId: String!, $relation: String, $maxDepth: Int) {
|
|
116
|
-
descendants(nodeId: $nodeId, relation: $relation, maxDepth: $maxDepth) {
|
|
117
|
-
id type title description status createdAt updatedAt
|
|
118
|
-
}
|
|
119
|
-
}`,
|
|
120
|
-
{ nodeId: project.id, relation: 'part_of', maxDepth: 1 },
|
|
121
|
-
)
|
|
113
|
+
}>(DESCENDANTS, { nodeId: project.id, relation: 'part_of', maxDepth: 1 })
|
|
122
114
|
const features = data.descendants.filter((n) => n.type === 'feature')
|
|
123
115
|
output(features)
|
|
124
116
|
} catch (error) {
|
|
@@ -158,11 +150,7 @@ featureCommand
|
|
|
158
150
|
}
|
|
159
151
|
if (opts.metadata != null) variables.metadata = opts.metadata
|
|
160
152
|
const data = await graphql<{ updateNode: unknown }>(
|
|
161
|
-
|
|
162
|
-
updateNode(id: $id, title: $title, description: $description, metadata: $metadata) {
|
|
163
|
-
id type title description status metadata createdAt updatedAt
|
|
164
|
-
}
|
|
165
|
-
}`,
|
|
153
|
+
UPDATE_NODE,
|
|
166
154
|
variables,
|
|
167
155
|
)
|
|
168
156
|
output(data.updateNode)
|
|
@@ -183,12 +171,9 @@ featureCommand
|
|
|
183
171
|
'No feature specified. Pass an ID or set an active feature.',
|
|
184
172
|
)
|
|
185
173
|
}
|
|
186
|
-
const data = await graphql<{ deleteNode: boolean }>(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}`,
|
|
190
|
-
{ id: featureId },
|
|
191
|
-
)
|
|
174
|
+
const data = await graphql<{ deleteNode: boolean }>(DELETE_NODE, {
|
|
175
|
+
id: featureId,
|
|
176
|
+
})
|
|
192
177
|
output({ deleted: data.deleteNode })
|
|
193
178
|
} catch (error) {
|
|
194
179
|
outputError(error)
|
|
@@ -207,14 +192,9 @@ featureCommand
|
|
|
207
192
|
'No feature specified. Pass an ID or set an active feature.',
|
|
208
193
|
)
|
|
209
194
|
}
|
|
210
|
-
const data = await graphql<{ node: unknown }>(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
id type title description status metadata createdAt updatedAt
|
|
214
|
-
}
|
|
215
|
-
}`,
|
|
216
|
-
{ id: featureId },
|
|
217
|
-
)
|
|
195
|
+
const data = await graphql<{ node: unknown }>(GET_NODE, {
|
|
196
|
+
id: featureId,
|
|
197
|
+
})
|
|
218
198
|
output(data.node)
|
|
219
199
|
} catch (error) {
|
|
220
200
|
outputError(error)
|
package/src/commands/import.ts
CHANGED
|
@@ -9,6 +9,13 @@ import {
|
|
|
9
9
|
parseManifest,
|
|
10
10
|
readClientKey,
|
|
11
11
|
} from '../util/manifest.ts'
|
|
12
|
+
import {
|
|
13
|
+
IMPORT_CREATE,
|
|
14
|
+
IMPORT_EDGE,
|
|
15
|
+
IMPORT_EDGES,
|
|
16
|
+
IMPORT_EXISTING,
|
|
17
|
+
IMPORT_UPDATE,
|
|
18
|
+
} from '../util/operations.ts'
|
|
12
19
|
|
|
13
20
|
const NODE_TYPES = ['project', 'feature', 'task'] as const
|
|
14
21
|
|
|
@@ -53,12 +60,9 @@ function desiredEdges(manifest: Manifest): DesiredEdge[] {
|
|
|
53
60
|
async function loadExisting(): Promise<Map<string, string>> {
|
|
54
61
|
const idByKey = new Map<string, string>()
|
|
55
62
|
for (const type of NODE_TYPES) {
|
|
56
|
-
const data = await graphql<{ nodes: ServerNode[] }>(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}`,
|
|
60
|
-
{ type },
|
|
61
|
-
)
|
|
63
|
+
const data = await graphql<{ nodes: ServerNode[] }>(IMPORT_EXISTING, {
|
|
64
|
+
type,
|
|
65
|
+
})
|
|
62
66
|
for (const node of data.nodes) {
|
|
63
67
|
const key = readClientKey(node.metadata)
|
|
64
68
|
if (key) idByKey.set(key, node.id)
|
|
@@ -78,9 +82,7 @@ async function loadExistingEdges(nodeIds: string[]): Promise<Set<string>> {
|
|
|
78
82
|
for (const nodeId of nodeIds) {
|
|
79
83
|
for (const relation of RELATIONS) {
|
|
80
84
|
const data = await graphql<{ edges: Array<{ id: string }> }>(
|
|
81
|
-
|
|
82
|
-
edges(nodeId: $nodeId, relation: $relation, direction: "outgoing") { id }
|
|
83
|
-
}`,
|
|
85
|
+
IMPORT_EDGES,
|
|
84
86
|
{ nodeId, relation },
|
|
85
87
|
)
|
|
86
88
|
for (const target of data.edges) {
|
|
@@ -91,25 +93,13 @@ async function loadExistingEdges(nodeIds: string[]): Promise<Set<string>> {
|
|
|
91
93
|
return existing
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
const CREATE_NODE = `mutation ImportCreate($type: String!, $title: String!, $description: String, $status: String, $metadata: String) {
|
|
95
|
-
createNode(type: $type, title: $title, description: $description, status: $status, metadata: $metadata) { id }
|
|
96
|
-
}`
|
|
97
|
-
|
|
98
|
-
const UPDATE_NODE = `mutation ImportUpdate($id: String!, $title: String, $description: String, $status: String, $metadata: String) {
|
|
99
|
-
updateNode(id: $id, title: $title, description: $description, status: $status, metadata: $metadata) { id }
|
|
100
|
-
}`
|
|
101
|
-
|
|
102
|
-
const CREATE_EDGE = `mutation ImportEdge($sourceId: String!, $targetId: String!, $relation: String!) {
|
|
103
|
-
createEdge(sourceId: $sourceId, targetId: $targetId, relation: $relation) { sourceId targetId relation }
|
|
104
|
-
}`
|
|
105
|
-
|
|
106
96
|
async function upsertNode(
|
|
107
97
|
node: ManifestNode,
|
|
108
98
|
existingId: string | undefined,
|
|
109
99
|
): Promise<string> {
|
|
110
100
|
const metadata = buildNodeMetadata(node.key, node.metadata)
|
|
111
101
|
if (existingId) {
|
|
112
|
-
await graphql<{ updateNode: { id: string } }>(
|
|
102
|
+
await graphql<{ updateNode: { id: string } }>(IMPORT_UPDATE, {
|
|
113
103
|
id: existingId,
|
|
114
104
|
title: node.title,
|
|
115
105
|
description: node.description ?? null,
|
|
@@ -118,7 +108,7 @@ async function upsertNode(
|
|
|
118
108
|
})
|
|
119
109
|
return existingId
|
|
120
110
|
}
|
|
121
|
-
const data = await graphql<{ createNode: { id: string } }>(
|
|
111
|
+
const data = await graphql<{ createNode: { id: string } }>(IMPORT_CREATE, {
|
|
122
112
|
type: node.type,
|
|
123
113
|
title: node.title,
|
|
124
114
|
description: node.description ?? null,
|
|
@@ -162,7 +152,7 @@ export const importCommand = new Command('import')
|
|
|
162
152
|
const targetId = idByKey.get(edge.targetKey)
|
|
163
153
|
if (!sourceId || !targetId) continue
|
|
164
154
|
if (present.has(edgeKey(sourceId, targetId, edge.relation))) continue
|
|
165
|
-
await graphql(
|
|
155
|
+
await graphql(IMPORT_EDGE, {
|
|
166
156
|
sourceId,
|
|
167
157
|
targetId,
|
|
168
158
|
relation: edge.relation,
|
package/src/commands/init.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Command } from 'commander'
|
|
|
4
4
|
import { graphql } from '../util/client.ts'
|
|
5
5
|
import { loadConfig, saveConfig } from '../util/config.ts'
|
|
6
6
|
import { output, outputError } from '../util/format.ts'
|
|
7
|
+
import { CREATE_PROJECT } from '../util/operations.ts'
|
|
7
8
|
|
|
8
9
|
export const initCommand = new Command('init')
|
|
9
10
|
.description('Initialize Flowy for the current git repository')
|
|
@@ -30,11 +31,7 @@ export const initCommand = new Command('init')
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const data = await graphql<{ createNode: { id: string; title: string } }>(
|
|
33
|
-
|
|
34
|
-
createNode(type: $type, title: $title) {
|
|
35
|
-
id type title description status metadata createdAt updatedAt
|
|
36
|
-
}
|
|
37
|
-
}`,
|
|
34
|
+
CREATE_PROJECT,
|
|
38
35
|
{ type: 'project', title: repoName },
|
|
39
36
|
)
|
|
40
37
|
|
package/src/commands/key.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ let mockOutputError: ReturnType<typeof vi.fn>
|
|
|
7
7
|
|
|
8
8
|
beforeEach(() => {
|
|
9
9
|
mockLoadConfig = vi.fn(() => ({
|
|
10
|
-
mode: '
|
|
10
|
+
mode: 'remote',
|
|
11
11
|
apiUrl: 'https://flowy-ai.fly.dev/graphql',
|
|
12
12
|
apiKey: 'old-key',
|
|
13
13
|
client: { name: '' },
|
|
@@ -26,6 +26,16 @@ beforeEach(() => {
|
|
|
26
26
|
loadConfig: mockLoadConfig,
|
|
27
27
|
saveConfig: mockSaveConfig,
|
|
28
28
|
fingerprintKey: actual.fingerprintKey,
|
|
29
|
+
requireRemoteMode: (commandName: string) => {
|
|
30
|
+
const cfg = (mockLoadConfig as unknown as () => { mode: string })()
|
|
31
|
+
if (cfg.mode === 'local') {
|
|
32
|
+
const err = new Error(
|
|
33
|
+
`"flowy ${commandName}" is only available in remote mode. The active mode is local mode.`,
|
|
34
|
+
) as Error & { code?: string }
|
|
35
|
+
err.code = 'LOCAL_MODE'
|
|
36
|
+
throw err
|
|
37
|
+
}
|
|
38
|
+
},
|
|
29
39
|
}
|
|
30
40
|
})
|
|
31
41
|
|
|
@@ -160,4 +170,29 @@ describe('key command', () => {
|
|
|
160
170
|
expect(mockSaveConfig).not.toHaveBeenCalled()
|
|
161
171
|
expect(mockOutput).not.toHaveBeenCalled()
|
|
162
172
|
})
|
|
173
|
+
|
|
174
|
+
test('rotate errors cleanly in local mode without hitting the server', async () => {
|
|
175
|
+
mockLoadConfig.mockReturnValue({
|
|
176
|
+
mode: 'local',
|
|
177
|
+
apiUrl: 'http://localhost:4000/graphql',
|
|
178
|
+
apiKey: 'old-key',
|
|
179
|
+
client: { name: '' },
|
|
180
|
+
projects: {},
|
|
181
|
+
})
|
|
182
|
+
const mockGraphql = vi.fn()
|
|
183
|
+
vi.doMock('../util/client.ts', () => ({ graphql: mockGraphql }))
|
|
184
|
+
|
|
185
|
+
const { keyCommand } = await import('./key.ts')
|
|
186
|
+
await keyCommand.parseAsync(['rotate'], { from: 'user' })
|
|
187
|
+
|
|
188
|
+
expect(mockGraphql).not.toHaveBeenCalled()
|
|
189
|
+
expect(mockSaveConfig).not.toHaveBeenCalled()
|
|
190
|
+
expect(mockOutput).not.toHaveBeenCalled()
|
|
191
|
+
expect(mockOutputError).toHaveBeenCalledWith(
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
message: expect.stringMatching(/local mode/i),
|
|
194
|
+
code: 'LOCAL_MODE',
|
|
195
|
+
}),
|
|
196
|
+
)
|
|
197
|
+
})
|
|
163
198
|
})
|
package/src/commands/key.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { graphql } from '../util/client.ts'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
fingerprintKey,
|
|
5
|
+
loadConfig,
|
|
6
|
+
requireRemoteMode,
|
|
7
|
+
saveConfig,
|
|
8
|
+
} from '../util/config.ts'
|
|
4
9
|
import { output, outputError } from '../util/format.ts'
|
|
10
|
+
import { ROTATE_API_KEY } from '../util/operations.ts'
|
|
5
11
|
|
|
6
12
|
export const keyCommand = new Command('key').description('API key management')
|
|
7
13
|
|
|
@@ -14,6 +20,7 @@ keyCommand
|
|
|
14
20
|
)
|
|
15
21
|
.action(async (opts) => {
|
|
16
22
|
try {
|
|
23
|
+
requireRemoteMode('key rotate')
|
|
17
24
|
const data = await graphql<{
|
|
18
25
|
rotateApiKey: {
|
|
19
26
|
user: {
|
|
@@ -25,14 +32,7 @@ keyCommand
|
|
|
25
32
|
}
|
|
26
33
|
apiKey: string
|
|
27
34
|
}
|
|
28
|
-
}>(
|
|
29
|
-
`mutation RotateApiKey {
|
|
30
|
-
rotateApiKey {
|
|
31
|
-
user { id email tier createdAt graceEndsAt }
|
|
32
|
-
apiKey
|
|
33
|
-
}
|
|
34
|
-
}`,
|
|
35
|
-
)
|
|
35
|
+
}>(ROTATE_API_KEY)
|
|
36
36
|
|
|
37
37
|
const { user, apiKey } = data.rotateApiKey
|
|
38
38
|
const config = loadConfig()
|
package/src/commands/project.ts
CHANGED
|
@@ -3,6 +3,14 @@ import { graphql } from '../util/client.ts'
|
|
|
3
3
|
import { loadConfig, requireProject, saveConfig } from '../util/config.ts'
|
|
4
4
|
import { resolveDescription } from '../util/description.ts'
|
|
5
5
|
import { output, outputError } from '../util/format.ts'
|
|
6
|
+
import {
|
|
7
|
+
CREATE_PROJECT,
|
|
8
|
+
DELETE_NODE,
|
|
9
|
+
GET_PROJECT,
|
|
10
|
+
LIST_PROJECTS,
|
|
11
|
+
LIST_PROJECTS_FOR_SET,
|
|
12
|
+
UPDATE_NODE,
|
|
13
|
+
} from '../util/operations.ts'
|
|
6
14
|
|
|
7
15
|
export const projectCommand = new Command('project').description(
|
|
8
16
|
'Manage projects',
|
|
@@ -14,14 +22,10 @@ projectCommand
|
|
|
14
22
|
.argument('<name>', 'Project name')
|
|
15
23
|
.action(async (name: string) => {
|
|
16
24
|
try {
|
|
17
|
-
const data = await graphql<{ createNode: unknown }>(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
}`,
|
|
23
|
-
{ type: 'project', title: name },
|
|
24
|
-
)
|
|
25
|
+
const data = await graphql<{ createNode: unknown }>(CREATE_PROJECT, {
|
|
26
|
+
type: 'project',
|
|
27
|
+
title: name,
|
|
28
|
+
})
|
|
25
29
|
output(data.createNode)
|
|
26
30
|
} catch (error) {
|
|
27
31
|
outputError(error)
|
|
@@ -44,14 +48,7 @@ projectCommand
|
|
|
44
48
|
try {
|
|
45
49
|
const data = await graphql<{
|
|
46
50
|
nodes: Array<{ id: string; title: string }>
|
|
47
|
-
}>(
|
|
48
|
-
`query ListProjects($type: String) {
|
|
49
|
-
nodes(type: $type) {
|
|
50
|
-
id title
|
|
51
|
-
}
|
|
52
|
-
}`,
|
|
53
|
-
{ type: 'project' },
|
|
54
|
-
)
|
|
51
|
+
}>(LIST_PROJECTS_FOR_SET, { type: 'project' })
|
|
55
52
|
const project = data.nodes.find((n) => n.title === name)
|
|
56
53
|
if (!project) {
|
|
57
54
|
throw new Error(`Project "${name}" not found.`)
|
|
@@ -67,14 +64,9 @@ projectCommand
|
|
|
67
64
|
.description('List all projects')
|
|
68
65
|
.action(async () => {
|
|
69
66
|
try {
|
|
70
|
-
const data = await graphql<{ nodes: unknown[] }>(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
id type title description status createdAt updatedAt
|
|
74
|
-
}
|
|
75
|
-
}`,
|
|
76
|
-
{ type: 'project' },
|
|
77
|
-
)
|
|
67
|
+
const data = await graphql<{ nodes: unknown[] }>(LIST_PROJECTS, {
|
|
68
|
+
type: 'project',
|
|
69
|
+
})
|
|
78
70
|
output(data.nodes)
|
|
79
71
|
} catch (error) {
|
|
80
72
|
outputError(error)
|
|
@@ -84,14 +76,9 @@ projectCommand
|
|
|
84
76
|
export async function showProject(id?: string): Promise<void> {
|
|
85
77
|
try {
|
|
86
78
|
const projectId = id ?? requireProject().id
|
|
87
|
-
const data = await graphql<{ node: unknown }>(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
id type title description status metadata createdAt updatedAt
|
|
91
|
-
}
|
|
92
|
-
}`,
|
|
93
|
-
{ id: projectId },
|
|
94
|
-
)
|
|
79
|
+
const data = await graphql<{ node: unknown }>(GET_PROJECT, {
|
|
80
|
+
id: projectId,
|
|
81
|
+
})
|
|
95
82
|
output(data.node)
|
|
96
83
|
} catch (error) {
|
|
97
84
|
outputError(error)
|
|
@@ -131,11 +118,7 @@ projectCommand
|
|
|
131
118
|
}
|
|
132
119
|
if (opts.metadata != null) variables.metadata = opts.metadata
|
|
133
120
|
const data = await graphql<{ updateNode: unknown }>(
|
|
134
|
-
|
|
135
|
-
updateNode(id: $id, title: $title, description: $description, metadata: $metadata) {
|
|
136
|
-
id type title description status metadata createdAt updatedAt
|
|
137
|
-
}
|
|
138
|
-
}`,
|
|
121
|
+
UPDATE_NODE,
|
|
139
122
|
variables,
|
|
140
123
|
)
|
|
141
124
|
output(data.updateNode)
|
|
@@ -151,12 +134,9 @@ projectCommand
|
|
|
151
134
|
.action(async (id?: string) => {
|
|
152
135
|
try {
|
|
153
136
|
const projectId = id ?? requireProject().id
|
|
154
|
-
const data = await graphql<{ deleteNode: boolean }>(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}`,
|
|
158
|
-
{ id: projectId },
|
|
159
|
-
)
|
|
137
|
+
const data = await graphql<{ deleteNode: boolean }>(DELETE_NODE, {
|
|
138
|
+
id: projectId,
|
|
139
|
+
})
|
|
160
140
|
output({ deleted: data.deleteNode })
|
|
161
141
|
} catch (error) {
|
|
162
142
|
outputError(error)
|
package/src/commands/search.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { graphql } from '../util/client.ts'
|
|
3
3
|
import { output, outputError } from '../util/format.ts'
|
|
4
|
+
import { SEARCH } from '../util/operations.ts'
|
|
4
5
|
|
|
5
6
|
export const searchCommand = new Command('search')
|
|
6
7
|
.description('Search nodes by text')
|
|
@@ -10,19 +11,12 @@ export const searchCommand = new Command('search')
|
|
|
10
11
|
.option('--limit <n>', 'Limit results', '50')
|
|
11
12
|
.action(async (query: string, opts) => {
|
|
12
13
|
try {
|
|
13
|
-
const data = await graphql<{ search: unknown[] }>(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
query,
|
|
21
|
-
type: opts.type,
|
|
22
|
-
status: opts.status,
|
|
23
|
-
limit: opts.limit ? Number.parseInt(opts.limit, 10) : undefined,
|
|
24
|
-
},
|
|
25
|
-
)
|
|
14
|
+
const data = await graphql<{ search: unknown[] }>(SEARCH, {
|
|
15
|
+
query,
|
|
16
|
+
type: opts.type,
|
|
17
|
+
status: opts.status,
|
|
18
|
+
limit: opts.limit ? Number.parseInt(opts.limit, 10) : undefined,
|
|
19
|
+
})
|
|
26
20
|
output(data.search)
|
|
27
21
|
} catch (error) {
|
|
28
22
|
outputError(error)
|
|
@@ -8,7 +8,7 @@ let mockSpawnSync: ReturnType<typeof vi.fn>
|
|
|
8
8
|
|
|
9
9
|
beforeEach(() => {
|
|
10
10
|
mockLoadConfig = vi.fn(() => ({
|
|
11
|
-
mode: '
|
|
11
|
+
mode: 'remote',
|
|
12
12
|
apiUrl: 'https://flowy-ai.fly.dev/graphql',
|
|
13
13
|
apiKey: '',
|
|
14
14
|
client: { name: '' },
|
|
@@ -315,6 +315,53 @@ describe('setup command', () => {
|
|
|
315
315
|
errSpy.mockRestore()
|
|
316
316
|
})
|
|
317
317
|
|
|
318
|
+
test('setup remote persists the API key before any later step (save-after-register)', async () => {
|
|
319
|
+
const mockGraphql = vi.fn().mockResolvedValue({
|
|
320
|
+
register: {
|
|
321
|
+
user: {
|
|
322
|
+
id: 'user_1',
|
|
323
|
+
email: 'a@b.com',
|
|
324
|
+
tier: null,
|
|
325
|
+
createdAt: '2026-06-13T00:00:00Z',
|
|
326
|
+
graceEndsAt: null,
|
|
327
|
+
},
|
|
328
|
+
apiKey: 'flowy_key_persisted',
|
|
329
|
+
checkoutUrl: null,
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
vi.doMock('../util/client.ts', () => ({ graphql: mockGraphql }))
|
|
333
|
+
|
|
334
|
+
// The skill install (a step AFTER the key is obtained) blows up hard.
|
|
335
|
+
// The key must already be on disk by then so the user is never stranded.
|
|
336
|
+
let keySavedBeforeSkillInstall = false
|
|
337
|
+
mockSaveConfig.mockImplementation((cfg: { apiKey?: string }) => {
|
|
338
|
+
if (cfg.apiKey === 'flowy_key_persisted')
|
|
339
|
+
keySavedBeforeSkillInstall = true
|
|
340
|
+
})
|
|
341
|
+
mockSpawnSync.mockImplementation((cmd: string) => {
|
|
342
|
+
if (cmd === 'npx') {
|
|
343
|
+
// By the time the (post-register) skill install runs, the key is saved.
|
|
344
|
+
expect(keySavedBeforeSkillInstall).toBe(true)
|
|
345
|
+
throw new Error('npx exploded')
|
|
346
|
+
}
|
|
347
|
+
return { status: 0, stdout: Buffer.from('') }
|
|
348
|
+
})
|
|
349
|
+
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
350
|
+
|
|
351
|
+
const { setupCommand } = await import('./setup.ts')
|
|
352
|
+
await setupCommand.parseAsync(['remote', '--email', 'a@b.com'], {
|
|
353
|
+
from: 'user',
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// The key was saved at least once with the real value before the failure.
|
|
357
|
+
expect(
|
|
358
|
+
mockSaveConfig.mock.calls.some(
|
|
359
|
+
(c) => (c[0] as { apiKey?: string }).apiKey === 'flowy_key_persisted',
|
|
360
|
+
),
|
|
361
|
+
).toBe(true)
|
|
362
|
+
errSpy.mockRestore()
|
|
363
|
+
})
|
|
364
|
+
|
|
318
365
|
test('setup remote warns when the skill install fails', async () => {
|
|
319
366
|
const mockGraphql = vi.fn().mockResolvedValue({
|
|
320
367
|
register: {
|
package/src/commands/setup.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
2
2
|
import { Command, Option } from 'commander'
|
|
3
3
|
import { fingerprintKey, loadConfig, saveConfig } from '../util/config.ts'
|
|
4
4
|
import { output, outputError } from '../util/format.ts'
|
|
5
|
+
import { REGISTER } from '../util/operations.ts'
|
|
5
6
|
import { pinnedInstallSpec } from './serve.ts'
|
|
6
7
|
|
|
7
8
|
export const setupCommand = new Command('setup').description(
|
|
@@ -104,16 +105,7 @@ setupCommand
|
|
|
104
105
|
apiKey: string
|
|
105
106
|
checkoutUrl: string
|
|
106
107
|
}
|
|
107
|
-
}>(
|
|
108
|
-
`mutation Register($email: String!, $tier: String) {
|
|
109
|
-
register(email: $email, tier: $tier) {
|
|
110
|
-
user { id email tier createdAt graceEndsAt }
|
|
111
|
-
apiKey
|
|
112
|
-
checkoutUrl
|
|
113
|
-
}
|
|
114
|
-
}`,
|
|
115
|
-
{ email: opts.email, tier: opts.tier },
|
|
116
|
-
)
|
|
108
|
+
}>(REGISTER, { email: opts.email, tier: opts.tier })
|
|
117
109
|
|
|
118
110
|
config.apiKey = data.register.apiKey
|
|
119
111
|
saveConfig(config)
|
package/src/commands/status.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { graphql } from '../util/client.ts'
|
|
3
3
|
import { output, outputError } from '../util/format.ts'
|
|
4
|
+
import { UPDATE_STATUS } from '../util/operations.ts'
|
|
4
5
|
|
|
5
6
|
export const statusCommand = new Command('status')
|
|
6
7
|
.description('Update a node status (shorthand)')
|
|
@@ -11,14 +12,10 @@ export const statusCommand = new Command('status')
|
|
|
11
12
|
)
|
|
12
13
|
.action(async (id: string, status: string) => {
|
|
13
14
|
try {
|
|
14
|
-
const data = await graphql<{ updateNode: unknown }>(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
}`,
|
|
20
|
-
{ id, status },
|
|
21
|
-
)
|
|
15
|
+
const data = await graphql<{ updateNode: unknown }>(UPDATE_STATUS, {
|
|
16
|
+
id,
|
|
17
|
+
status,
|
|
18
|
+
})
|
|
22
19
|
output(data.updateNode)
|
|
23
20
|
} catch (error) {
|
|
24
21
|
outputError(error)
|