@sqaoss/flowy 1.6.1 → 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 +81 -8
- package/src/commands/key.ts +22 -12
- package/src/commands/project.ts +23 -43
- package/src/commands/search.ts +7 -13
- package/src/commands/setup.test.ts +95 -7
- package/src/commands/setup.ts +14 -12
- 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 +64 -2
- package/src/commands/whoami.ts +14 -8
- package/src/util/config.test.ts +218 -3
- package/src/util/config.ts +224 -21
- 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: '' },
|
|
@@ -17,10 +17,27 @@ beforeEach(() => {
|
|
|
17
17
|
mockOutput = vi.fn()
|
|
18
18
|
mockOutputError = vi.fn()
|
|
19
19
|
|
|
20
|
-
vi.doMock('../util/config.ts', () =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
vi.doMock('../util/config.ts', async () => {
|
|
21
|
+
const actual =
|
|
22
|
+
await vi.importActual<typeof import('../util/config.ts')>(
|
|
23
|
+
'../util/config.ts',
|
|
24
|
+
)
|
|
25
|
+
return {
|
|
26
|
+
loadConfig: mockLoadConfig,
|
|
27
|
+
saveConfig: mockSaveConfig,
|
|
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
|
+
},
|
|
39
|
+
}
|
|
40
|
+
})
|
|
24
41
|
|
|
25
42
|
vi.doMock('../util/format.ts', () => ({
|
|
26
43
|
output: mockOutput,
|
|
@@ -46,7 +63,7 @@ describe('key command', () => {
|
|
|
46
63
|
expect(keyCommand.commands).toHaveLength(1)
|
|
47
64
|
})
|
|
48
65
|
|
|
49
|
-
test('rotate calls rotateApiKey mutation, saves new key to config, and outputs
|
|
66
|
+
test('rotate calls rotateApiKey mutation, saves new key to config, and outputs a fingerprint (not the secret)', async () => {
|
|
50
67
|
const mockGraphql = vi.fn().mockResolvedValue({
|
|
51
68
|
rotateApiKey: {
|
|
52
69
|
user: {
|
|
@@ -74,12 +91,43 @@ describe('key command', () => {
|
|
|
74
91
|
apiKey: 'flowy_new_key_456',
|
|
75
92
|
}),
|
|
76
93
|
)
|
|
77
|
-
|
|
94
|
+
|
|
95
|
+
// Default output must NOT leak the full secret.
|
|
96
|
+
const outputArg = mockOutput.mock.calls[0]![0]
|
|
97
|
+
expect(JSON.stringify(outputArg)).not.toContain('flowy_new_key_456')
|
|
98
|
+
expect(outputArg).toEqual(
|
|
78
99
|
expect.objectContaining({
|
|
79
100
|
user: expect.objectContaining({ email: 'test@example.com' }),
|
|
80
|
-
|
|
101
|
+
keyFingerprint: expect.stringMatching(/sha256:[0-9a-f]{12}/),
|
|
81
102
|
}),
|
|
82
103
|
)
|
|
104
|
+
expect(outputArg).not.toHaveProperty('apiKey')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('rotate --show-key reveals the full secret', async () => {
|
|
108
|
+
const mockGraphql = vi.fn().mockResolvedValue({
|
|
109
|
+
rotateApiKey: {
|
|
110
|
+
user: {
|
|
111
|
+
id: 'user_1',
|
|
112
|
+
email: 'test@example.com',
|
|
113
|
+
tier: 'free',
|
|
114
|
+
createdAt: '2025-01-01T00:00:00Z',
|
|
115
|
+
graceEndsAt: null,
|
|
116
|
+
},
|
|
117
|
+
apiKey: 'flowy_new_key_456',
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
vi.doMock('../util/client.ts', () => ({
|
|
121
|
+
graphql: mockGraphql,
|
|
122
|
+
}))
|
|
123
|
+
|
|
124
|
+
const { keyCommand } = await import('./key.ts')
|
|
125
|
+
await keyCommand.parseAsync(['rotate', '--show-key'], { from: 'user' })
|
|
126
|
+
|
|
127
|
+
const outputArg = mockOutput.mock.calls[0]![0]
|
|
128
|
+
expect(outputArg).toEqual(
|
|
129
|
+
expect.objectContaining({ apiKey: 'flowy_new_key_456' }),
|
|
130
|
+
)
|
|
83
131
|
})
|
|
84
132
|
|
|
85
133
|
test('rotate saves the exact new apiKey to config', async () => {
|
|
@@ -122,4 +170,29 @@ describe('key command', () => {
|
|
|
122
170
|
expect(mockSaveConfig).not.toHaveBeenCalled()
|
|
123
171
|
expect(mockOutput).not.toHaveBeenCalled()
|
|
124
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
|
+
})
|
|
125
198
|
})
|
package/src/commands/key.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
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
|
|
|
8
14
|
keyCommand
|
|
9
15
|
.command('rotate')
|
|
10
16
|
.description('Rotate API key')
|
|
11
|
-
.
|
|
17
|
+
.option(
|
|
18
|
+
'--show-key',
|
|
19
|
+
'Print the full API key instead of a non-reversible fingerprint',
|
|
20
|
+
)
|
|
21
|
+
.action(async (opts) => {
|
|
12
22
|
try {
|
|
23
|
+
requireRemoteMode('key rotate')
|
|
13
24
|
const data = await graphql<{
|
|
14
25
|
rotateApiKey: {
|
|
15
26
|
user: {
|
|
@@ -21,20 +32,19 @@ keyCommand
|
|
|
21
32
|
}
|
|
22
33
|
apiKey: string
|
|
23
34
|
}
|
|
24
|
-
}>(
|
|
25
|
-
`mutation RotateApiKey {
|
|
26
|
-
rotateApiKey {
|
|
27
|
-
user { id email tier createdAt graceEndsAt }
|
|
28
|
-
apiKey
|
|
29
|
-
}
|
|
30
|
-
}`,
|
|
31
|
-
)
|
|
35
|
+
}>(ROTATE_API_KEY)
|
|
32
36
|
|
|
37
|
+
const { user, apiKey } = data.rotateApiKey
|
|
33
38
|
const config = loadConfig()
|
|
34
|
-
config.apiKey =
|
|
39
|
+
config.apiKey = apiKey
|
|
35
40
|
saveConfig(config)
|
|
36
41
|
|
|
37
|
-
output(
|
|
42
|
+
// Default output never leaks the secret; --show-key opts in (F35).
|
|
43
|
+
output(
|
|
44
|
+
opts.showKey
|
|
45
|
+
? { user, apiKey }
|
|
46
|
+
: { user, keyFingerprint: fingerprintKey(apiKey) },
|
|
47
|
+
)
|
|
38
48
|
} catch (error) {
|
|
39
49
|
outputError(error)
|
|
40
50
|
}
|
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)
|