@sqaoss/flowy 0.1.1 → 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/LICENSE +201 -661
- package/README.md +90 -56
- package/docker-compose.yml +14 -0
- package/package.json +24 -7
- package/server/Dockerfile +14 -0
- package/server/package.json +25 -0
- package/server/src/db.test.ts +93 -0
- package/server/src/db.ts +47 -0
- package/server/src/index.test.ts +25 -0
- package/server/src/index.ts +45 -0
- package/server/src/resolvers.test.ts +855 -0
- package/server/src/resolvers.ts +308 -0
- package/server/src/schema.test.ts +93 -0
- package/server/src/schema.ts +45 -0
- package/skills/using-flowy/SKILL.md +128 -0
- package/src/commands/client.test.ts +40 -0
- package/src/commands/client.ts +34 -0
- package/src/commands/feature.test.ts +71 -0
- package/src/commands/feature.ts +143 -0
- package/src/commands/init.test.ts +174 -0
- package/src/commands/init.ts +50 -0
- package/src/commands/project.test.ts +83 -0
- package/src/commands/project.ts +104 -0
- package/src/commands/setup.test.ts +135 -0
- package/src/commands/setup.ts +109 -0
- package/src/commands/task.test.ts +83 -0
- package/src/commands/task.ts +127 -0
- package/src/commands/tree.test.ts +9 -0
- package/src/commands/tree.ts +2 -59
- package/src/index.ts +14 -8
- package/src/util/config.test.ts +151 -0
- package/src/util/config.ts +107 -2
- package/src/util/description.test.ts +29 -0
- package/src/util/description.ts +8 -0
- package/src/commands/edge.ts +0 -84
- package/src/commands/node.ts +0 -134
- package/src/commands/register.ts +0 -25
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
afterAll,
|
|
6
|
+
afterEach,
|
|
7
|
+
beforeAll,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
test,
|
|
12
|
+
vi,
|
|
13
|
+
} from 'vitest'
|
|
14
|
+
|
|
15
|
+
const CONFIG_PATH = resolve(homedir(), '.config', 'flowy', 'config.json')
|
|
16
|
+
|
|
17
|
+
describe('config', () => {
|
|
18
|
+
let originalConfig: string | null = null
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
originalConfig = existsSync(CONFIG_PATH)
|
|
22
|
+
? readFileSync(CONFIG_PATH, 'utf-8')
|
|
23
|
+
: null
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
if (originalConfig !== null) {
|
|
28
|
+
writeFileSync(CONFIG_PATH, originalConfig)
|
|
29
|
+
} else if (existsSync(CONFIG_PATH)) {
|
|
30
|
+
rmSync(CONFIG_PATH)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
if (existsSync(CONFIG_PATH)) rmSync(CONFIG_PATH)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
delete process.env.FLOWY_PROJECT
|
|
40
|
+
delete process.env.FLOWY_FEATURE
|
|
41
|
+
vi.resetModules()
|
|
42
|
+
})
|
|
43
|
+
test('loadConfig returns defaults when no config file exists', async () => {
|
|
44
|
+
const { loadConfig } = await import('./config.ts')
|
|
45
|
+
const config = loadConfig()
|
|
46
|
+
expect(config.mode).toBe('saas')
|
|
47
|
+
expect(config.apiUrl).toBe('https://flowy-ai.fly.dev/graphql')
|
|
48
|
+
expect(config.apiKey).toBe('')
|
|
49
|
+
expect(config.client.name).toBe('')
|
|
50
|
+
expect(config.projects).toEqual({})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('getConfig uses env var overrides', async () => {
|
|
54
|
+
process.env.FLOWY_API_URL = 'http://localhost:4000/graphql'
|
|
55
|
+
process.env.FLOWY_API_KEY = 'test-key'
|
|
56
|
+
const { getConfig } = await import('./config.ts')
|
|
57
|
+
const { apiUrl, apiKey } = getConfig()
|
|
58
|
+
expect(apiUrl).toBe('http://localhost:4000/graphql')
|
|
59
|
+
expect(apiKey).toBe('test-key')
|
|
60
|
+
delete process.env.FLOWY_API_URL
|
|
61
|
+
delete process.env.FLOWY_API_KEY
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('saveConfig writes and loadConfig reads from disk', async () => {
|
|
65
|
+
const { saveConfig, loadConfig } = await import('./config.ts')
|
|
66
|
+
const config = loadConfig()
|
|
67
|
+
config.client.name = 'Test Client'
|
|
68
|
+
saveConfig(config)
|
|
69
|
+
const reloaded = loadConfig()
|
|
70
|
+
expect(reloaded.client.name).toBe('Test Client')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('resolveProject returns null when no project configured', async () => {
|
|
74
|
+
const { resolveProject } = await import('./config.ts')
|
|
75
|
+
expect(resolveProject()).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('resolveProject matches cwd against project paths', async () => {
|
|
79
|
+
const { saveConfig, loadConfig, resolveProject } = await import(
|
|
80
|
+
'./config.ts'
|
|
81
|
+
)
|
|
82
|
+
const config = loadConfig()
|
|
83
|
+
const cwd = process.cwd()
|
|
84
|
+
config.projects[cwd] = { id: 'proj_1', name: 'Test Project' }
|
|
85
|
+
saveConfig(config)
|
|
86
|
+
const project = resolveProject()
|
|
87
|
+
expect(project).not.toBeNull()
|
|
88
|
+
expect(project?.id).toBe('proj_1')
|
|
89
|
+
expect(project?.name).toBe('Test Project')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('resolveProject uses FLOWY_PROJECT env var', async () => {
|
|
93
|
+
const { saveConfig, loadConfig, resolveProject } = await import(
|
|
94
|
+
'./config.ts'
|
|
95
|
+
)
|
|
96
|
+
const config = loadConfig()
|
|
97
|
+
config.projects['/other/path'] = { id: 'proj_2', name: 'Env Project' }
|
|
98
|
+
saveConfig(config)
|
|
99
|
+
process.env.FLOWY_PROJECT = 'Env Project'
|
|
100
|
+
const project = resolveProject()
|
|
101
|
+
expect(project?.name).toBe('Env Project')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('requireProject throws when no project configured', async () => {
|
|
105
|
+
const { requireProject } = await import('./config.ts')
|
|
106
|
+
expect(() => requireProject()).toThrow('No active project')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('resolveFeature returns FLOWY_FEATURE env var', async () => {
|
|
110
|
+
process.env.FLOWY_FEATURE = 'feat_123'
|
|
111
|
+
const { resolveFeature } = await import('./config.ts')
|
|
112
|
+
expect(resolveFeature()).toBe('feat_123')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('resolveFeature falls back to activeFeature from project config', async () => {
|
|
116
|
+
const { saveConfig, loadConfig, resolveFeature } = await import(
|
|
117
|
+
'./config.ts'
|
|
118
|
+
)
|
|
119
|
+
const config = loadConfig()
|
|
120
|
+
const cwd = process.cwd()
|
|
121
|
+
config.projects[cwd] = {
|
|
122
|
+
id: 'proj_1',
|
|
123
|
+
name: 'Test',
|
|
124
|
+
activeFeature: 'feat_abc',
|
|
125
|
+
}
|
|
126
|
+
saveConfig(config)
|
|
127
|
+
expect(resolveFeature()).toBe('feat_abc')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('requireFeature throws when no feature set', async () => {
|
|
131
|
+
const { requireFeature } = await import('./config.ts')
|
|
132
|
+
expect(() => requireFeature()).toThrow('No active feature')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('updateProjectConfig modifies project entry for cwd', async () => {
|
|
136
|
+
const { saveConfig, loadConfig, updateProjectConfig } = await import(
|
|
137
|
+
'./config.ts'
|
|
138
|
+
)
|
|
139
|
+
const config = loadConfig()
|
|
140
|
+
const cwd = process.cwd()
|
|
141
|
+
config.projects[cwd] = { id: 'proj_1', name: 'Test' }
|
|
142
|
+
saveConfig(config)
|
|
143
|
+
updateProjectConfig((p) => {
|
|
144
|
+
p.activeFeature = 'feat_999'
|
|
145
|
+
})
|
|
146
|
+
const updated = loadConfig()
|
|
147
|
+
expect(
|
|
148
|
+
(updated.projects[cwd] as { activeFeature?: string }).activeFeature,
|
|
149
|
+
).toBe('feat_999')
|
|
150
|
+
})
|
|
151
|
+
})
|
package/src/util/config.ts
CHANGED
|
@@ -1,5 +1,110 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = resolve(homedir(), '.config', 'flowy')
|
|
6
|
+
const CONFIG_PATH = resolve(CONFIG_DIR, 'config.json')
|
|
7
|
+
|
|
8
|
+
export function loadConfig() {
|
|
9
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
10
|
+
return {
|
|
11
|
+
mode: 'saas' as const,
|
|
12
|
+
apiUrl: 'https://flowy-ai.fly.dev/graphql',
|
|
13
|
+
apiKey: '',
|
|
14
|
+
client: { name: '' },
|
|
15
|
+
projects: {} as Record<string, unknown>,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveConfig(config: ReturnType<typeof loadConfig>): void {
|
|
22
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
23
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ProjectConfig {
|
|
27
|
+
id: string
|
|
28
|
+
name: string
|
|
29
|
+
activeFeature?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveProject(): ProjectConfig | null {
|
|
33
|
+
const envProject = process.env.FLOWY_PROJECT
|
|
34
|
+
const config = loadConfig()
|
|
35
|
+
|
|
36
|
+
if (envProject) {
|
|
37
|
+
return (
|
|
38
|
+
(Object.values(config.projects).find(
|
|
39
|
+
(p) => (p as ProjectConfig).name === envProject,
|
|
40
|
+
) as ProjectConfig) ?? null
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cwd = process.cwd()
|
|
45
|
+
let bestMatch: ProjectConfig | null = null
|
|
46
|
+
let bestLength = 0
|
|
47
|
+
|
|
48
|
+
for (const [path, project] of Object.entries(config.projects)) {
|
|
49
|
+
if (
|
|
50
|
+
(cwd === path || cwd.startsWith(`${path}/`)) &&
|
|
51
|
+
path.length > bestLength
|
|
52
|
+
) {
|
|
53
|
+
bestMatch = project as ProjectConfig
|
|
54
|
+
bestLength = path.length
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return bestMatch
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveFeature(): string | null {
|
|
62
|
+
const envFeature = process.env.FLOWY_FEATURE
|
|
63
|
+
if (envFeature) return envFeature
|
|
64
|
+
const project = resolveProject()
|
|
65
|
+
return project?.activeFeature ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function requireFeature(): string {
|
|
69
|
+
const feature = resolveFeature()
|
|
70
|
+
if (!feature) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'No active feature. Run "flowy feature set <name-or-id>" or set FLOWY_FEATURE.',
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
return feature
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function requireProject(): ProjectConfig {
|
|
79
|
+
const project = resolveProject()
|
|
80
|
+
if (!project) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
'No active project. Run "flowy project set <name>" or set FLOWY_PROJECT.',
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
return project
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function updateProjectConfig(
|
|
89
|
+
updater: (project: ProjectConfig) => void,
|
|
90
|
+
): void {
|
|
91
|
+
const config = loadConfig()
|
|
92
|
+
const cwd = process.cwd()
|
|
93
|
+
|
|
94
|
+
for (const [path, project] of Object.entries(config.projects)) {
|
|
95
|
+
if (cwd === path || cwd.startsWith(`${path}/`)) {
|
|
96
|
+
updater(project as ProjectConfig)
|
|
97
|
+
saveConfig(config)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error('No directory mapping. Run "flowy project set <name>" first.')
|
|
103
|
+
}
|
|
104
|
+
|
|
1
105
|
export function getConfig() {
|
|
2
|
-
const
|
|
3
|
-
const
|
|
106
|
+
const config = loadConfig()
|
|
107
|
+
const apiUrl = process.env.FLOWY_API_URL ?? config.apiUrl
|
|
108
|
+
const apiKey = process.env.FLOWY_API_KEY ?? config.apiKey
|
|
4
109
|
return { apiUrl, apiKey }
|
|
5
110
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
describe('resolveDescription', () => {
|
|
9
|
+
const TEST_FILE = resolve(__dirname, '../../.test-desc.md')
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
try {
|
|
13
|
+
rmSync(TEST_FILE)
|
|
14
|
+
} catch {}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('returns file content when path exists', async () => {
|
|
18
|
+
writeFileSync(TEST_FILE, '# Test\nSome content')
|
|
19
|
+
const { resolveDescription } = await import('./description.ts')
|
|
20
|
+
const result = await resolveDescription(TEST_FILE)
|
|
21
|
+
expect(result).toBe('# Test\nSome content')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('returns value as-is when path does not exist', async () => {
|
|
25
|
+
const { resolveDescription } = await import('./description.ts')
|
|
26
|
+
const result = await resolveDescription('Just a plain description')
|
|
27
|
+
expect(result).toBe('Just a plain description')
|
|
28
|
+
})
|
|
29
|
+
})
|
package/src/commands/edge.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander'
|
|
2
|
-
import { graphql } from '../util/client.ts'
|
|
3
|
-
import { output, outputError } from '../util/format.ts'
|
|
4
|
-
|
|
5
|
-
export const edgeCommand = new Command('edge').description(
|
|
6
|
-
'Manage edges between nodes',
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
edgeCommand
|
|
10
|
-
.command('create')
|
|
11
|
-
.description('Create an edge between two nodes')
|
|
12
|
-
.requiredOption('--source <id>', 'Source node ID')
|
|
13
|
-
.requiredOption('--target <id>', 'Target node ID')
|
|
14
|
-
.requiredOption(
|
|
15
|
-
'--relation <rel>',
|
|
16
|
-
'Relation type (part_of, depends_on, blocks, informs)',
|
|
17
|
-
)
|
|
18
|
-
.action(async (opts) => {
|
|
19
|
-
try {
|
|
20
|
-
const data = await graphql<{ createEdge: unknown }>(
|
|
21
|
-
`mutation CreateEdge($sourceId: String!, $targetId: String!, $relation: String!) {
|
|
22
|
-
createEdge(sourceId: $sourceId, targetId: $targetId, relation: $relation) {
|
|
23
|
-
sourceId targetId relation createdAt
|
|
24
|
-
}
|
|
25
|
-
}`,
|
|
26
|
-
{
|
|
27
|
-
sourceId: opts.source,
|
|
28
|
-
targetId: opts.target,
|
|
29
|
-
relation: opts.relation,
|
|
30
|
-
},
|
|
31
|
-
)
|
|
32
|
-
output(data.createEdge)
|
|
33
|
-
} catch (error) {
|
|
34
|
-
outputError(error)
|
|
35
|
-
}
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
edgeCommand
|
|
39
|
-
.command('remove')
|
|
40
|
-
.description('Remove an edge')
|
|
41
|
-
.requiredOption('--source <id>', 'Source node ID')
|
|
42
|
-
.requiredOption('--target <id>', 'Target node ID')
|
|
43
|
-
.requiredOption('--relation <rel>', 'Relation type')
|
|
44
|
-
.action(async (opts) => {
|
|
45
|
-
try {
|
|
46
|
-
const data = await graphql<{ removeEdge: boolean }>(
|
|
47
|
-
`mutation RemoveEdge($sourceId: String!, $targetId: String!, $relation: String!) {
|
|
48
|
-
removeEdge(sourceId: $sourceId, targetId: $targetId, relation: $relation)
|
|
49
|
-
}`,
|
|
50
|
-
{
|
|
51
|
-
sourceId: opts.source,
|
|
52
|
-
targetId: opts.target,
|
|
53
|
-
relation: opts.relation,
|
|
54
|
-
},
|
|
55
|
-
)
|
|
56
|
-
output({ removed: data.removeEdge })
|
|
57
|
-
} catch (error) {
|
|
58
|
-
outputError(error)
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
edgeCommand
|
|
63
|
-
.command('list')
|
|
64
|
-
.description('List edges')
|
|
65
|
-
.option('--node <id>', 'Filter by node ID')
|
|
66
|
-
.option('--relation <rel>', 'Filter by relation type')
|
|
67
|
-
.action(async (opts) => {
|
|
68
|
-
try {
|
|
69
|
-
const data = await graphql<{ edges: unknown[] }>(
|
|
70
|
-
`query ListEdges($nodeId: String, $relation: String) {
|
|
71
|
-
edges(nodeId: $nodeId, relation: $relation) {
|
|
72
|
-
sourceId targetId relation createdAt
|
|
73
|
-
}
|
|
74
|
-
}`,
|
|
75
|
-
{
|
|
76
|
-
nodeId: opts.node,
|
|
77
|
-
relation: opts.relation,
|
|
78
|
-
},
|
|
79
|
-
)
|
|
80
|
-
output(data.edges)
|
|
81
|
-
} catch (error) {
|
|
82
|
-
outputError(error)
|
|
83
|
-
}
|
|
84
|
-
})
|
package/src/commands/node.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander'
|
|
2
|
-
import { graphql } from '../util/client.ts'
|
|
3
|
-
import { output, outputError } from '../util/format.ts'
|
|
4
|
-
|
|
5
|
-
export const nodeCommand = new Command('node').description('Manage work nodes')
|
|
6
|
-
|
|
7
|
-
nodeCommand
|
|
8
|
-
.command('create')
|
|
9
|
-
.description('Create a new node')
|
|
10
|
-
.requiredOption(
|
|
11
|
-
'--type <type>',
|
|
12
|
-
'Node type (client, project, feature, epic, task)',
|
|
13
|
-
)
|
|
14
|
-
.requiredOption('--title <title>', 'Node title')
|
|
15
|
-
.option('--description <desc>', 'Node description')
|
|
16
|
-
.option('--status <status>', 'Initial status')
|
|
17
|
-
.option('--metadata <json>', 'Metadata as JSON string')
|
|
18
|
-
.action(async (opts) => {
|
|
19
|
-
try {
|
|
20
|
-
const data = await graphql<{ createNode: unknown }>(
|
|
21
|
-
`mutation CreateNode($type: String!, $title: String!, $description: String, $status: String, $metadata: String) {
|
|
22
|
-
createNode(type: $type, title: $title, description: $description, status: $status, metadata: $metadata) {
|
|
23
|
-
id type title description status metadata createdAt updatedAt
|
|
24
|
-
}
|
|
25
|
-
}`,
|
|
26
|
-
{
|
|
27
|
-
type: opts.type,
|
|
28
|
-
title: opts.title,
|
|
29
|
-
description: opts.description,
|
|
30
|
-
status: opts.status,
|
|
31
|
-
metadata: opts.metadata,
|
|
32
|
-
},
|
|
33
|
-
)
|
|
34
|
-
output(data.createNode)
|
|
35
|
-
} catch (error) {
|
|
36
|
-
outputError(error)
|
|
37
|
-
}
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
nodeCommand
|
|
41
|
-
.command('get')
|
|
42
|
-
.description('Get a node by ID')
|
|
43
|
-
.requiredOption('--id <id>', 'Node ID')
|
|
44
|
-
.action(async (opts) => {
|
|
45
|
-
try {
|
|
46
|
-
const data = await graphql<{ node: unknown }>(
|
|
47
|
-
`query GetNode($id: String!) {
|
|
48
|
-
node(id: $id) {
|
|
49
|
-
id type title description status metadata createdAt updatedAt
|
|
50
|
-
}
|
|
51
|
-
}`,
|
|
52
|
-
{ id: opts.id },
|
|
53
|
-
)
|
|
54
|
-
output(data.node)
|
|
55
|
-
} catch (error) {
|
|
56
|
-
outputError(error)
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
nodeCommand
|
|
61
|
-
.command('list')
|
|
62
|
-
.description('List nodes')
|
|
63
|
-
.option('--type <type>', 'Filter by type')
|
|
64
|
-
.option('--status <status>', 'Filter by status')
|
|
65
|
-
.option('--limit <n>', 'Limit results', '50')
|
|
66
|
-
.option('--offset <n>', 'Offset results', '0')
|
|
67
|
-
.action(async (opts) => {
|
|
68
|
-
try {
|
|
69
|
-
const data = await graphql<{ nodes: unknown[] }>(
|
|
70
|
-
`query ListNodes($type: String, $status: String, $limit: Int, $offset: Int) {
|
|
71
|
-
nodes(type: $type, status: $status, limit: $limit, offset: $offset) {
|
|
72
|
-
id type title description status metadata createdAt updatedAt
|
|
73
|
-
}
|
|
74
|
-
}`,
|
|
75
|
-
{
|
|
76
|
-
type: opts.type,
|
|
77
|
-
status: opts.status,
|
|
78
|
-
limit: Number.parseInt(opts.limit, 10),
|
|
79
|
-
offset: Number.parseInt(opts.offset, 10),
|
|
80
|
-
},
|
|
81
|
-
)
|
|
82
|
-
output(data.nodes)
|
|
83
|
-
} catch (error) {
|
|
84
|
-
outputError(error)
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
nodeCommand
|
|
89
|
-
.command('update')
|
|
90
|
-
.description('Update a node')
|
|
91
|
-
.requiredOption('--id <id>', 'Node ID')
|
|
92
|
-
.option('--title <title>', 'New title')
|
|
93
|
-
.option('--description <desc>', 'New description')
|
|
94
|
-
.option('--status <status>', 'New status')
|
|
95
|
-
.option('--metadata <json>', 'New metadata as JSON string')
|
|
96
|
-
.action(async (opts) => {
|
|
97
|
-
try {
|
|
98
|
-
const data = await graphql<{ updateNode: unknown }>(
|
|
99
|
-
`mutation UpdateNode($id: String!, $title: String, $description: String, $status: String, $metadata: String) {
|
|
100
|
-
updateNode(id: $id, title: $title, description: $description, status: $status, metadata: $metadata) {
|
|
101
|
-
id type title description status metadata createdAt updatedAt
|
|
102
|
-
}
|
|
103
|
-
}`,
|
|
104
|
-
{
|
|
105
|
-
id: opts.id,
|
|
106
|
-
title: opts.title,
|
|
107
|
-
description: opts.description,
|
|
108
|
-
status: opts.status,
|
|
109
|
-
metadata: opts.metadata,
|
|
110
|
-
},
|
|
111
|
-
)
|
|
112
|
-
output(data.updateNode)
|
|
113
|
-
} catch (error) {
|
|
114
|
-
outputError(error)
|
|
115
|
-
}
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
nodeCommand
|
|
119
|
-
.command('delete')
|
|
120
|
-
.description('Delete a node')
|
|
121
|
-
.requiredOption('--id <id>', 'Node ID')
|
|
122
|
-
.action(async (opts) => {
|
|
123
|
-
try {
|
|
124
|
-
const data = await graphql<{ deleteNode: boolean }>(
|
|
125
|
-
`mutation DeleteNode($id: String!) {
|
|
126
|
-
deleteNode(id: $id)
|
|
127
|
-
}`,
|
|
128
|
-
{ id: opts.id },
|
|
129
|
-
)
|
|
130
|
-
output({ deleted: data.deleteNode })
|
|
131
|
-
} catch (error) {
|
|
132
|
-
outputError(error)
|
|
133
|
-
}
|
|
134
|
-
})
|
package/src/commands/register.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander'
|
|
2
|
-
import { graphql } from '../util/client.ts'
|
|
3
|
-
import { output, outputError } from '../util/format.ts'
|
|
4
|
-
|
|
5
|
-
export const registerCommand = new Command('register')
|
|
6
|
-
.description('Register a new account')
|
|
7
|
-
.requiredOption('--email <email>', 'Your email address')
|
|
8
|
-
.action(async (opts) => {
|
|
9
|
-
try {
|
|
10
|
-
const data = await graphql<{
|
|
11
|
-
register: { user: { id: string; email: string }; apiKey: string }
|
|
12
|
-
}>(
|
|
13
|
-
`mutation Register($email: String!) {
|
|
14
|
-
register(email: $email) {
|
|
15
|
-
user { id email tier createdAt }
|
|
16
|
-
apiKey
|
|
17
|
-
}
|
|
18
|
-
}`,
|
|
19
|
-
{ email: opts.email },
|
|
20
|
-
)
|
|
21
|
-
output(data.register)
|
|
22
|
-
} catch (error) {
|
|
23
|
-
outputError(error)
|
|
24
|
-
}
|
|
25
|
-
})
|