@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.
@@ -0,0 +1,308 @@
1
+ import { nanoid } from 'nanoid'
2
+ import type { FlowyDb } from './db.ts'
3
+
4
+ type Db = FlowyDb
5
+
6
+ interface NodeRow {
7
+ id: string
8
+ type: string
9
+ title: string
10
+ description: string | null
11
+ status: string
12
+ metadata: string | null
13
+ created_at: string
14
+ updated_at: string
15
+ }
16
+
17
+ export interface NodeGql {
18
+ id: string
19
+ type: string
20
+ title: string
21
+ description: string | null
22
+ status: string
23
+ metadata: string | null
24
+ createdAt: string
25
+ updatedAt: string
26
+ }
27
+
28
+ const NODE_COLS =
29
+ 'id, type, title, description, status, metadata, created_at, updated_at'
30
+
31
+ const PREFIX_MAP: Record<string, string> = {
32
+ project: 'proj',
33
+ feature: 'feat',
34
+ task: 'task',
35
+ }
36
+
37
+ const VALID_STATUSES = new Set([
38
+ 'draft',
39
+ 'pending_review',
40
+ 'approved',
41
+ 'in_progress',
42
+ 'done',
43
+ 'blocked',
44
+ 'cancelled',
45
+ ])
46
+
47
+ function generateId(type: string): string {
48
+ const prefix = PREFIX_MAP[type] ?? type
49
+ return `${prefix}_${nanoid(12)}`
50
+ }
51
+
52
+ function rowToNode(row: NodeRow): NodeGql {
53
+ return {
54
+ id: row.id,
55
+ type: row.type,
56
+ title: row.title,
57
+ description: row.description,
58
+ status: row.status,
59
+ metadata: row.metadata,
60
+ createdAt: row.created_at,
61
+ updatedAt: row.updated_at,
62
+ }
63
+ }
64
+
65
+ function selectNode(db: Db, id: string): NodeGql | null {
66
+ const row = db.raw
67
+ .query(`SELECT ${NODE_COLS} FROM nodes WHERE id = ?`)
68
+ .get(id) as NodeRow | null
69
+ return row ? rowToNode(row) : null
70
+ }
71
+
72
+ function selectNodes(rows: NodeRow[]): NodeGql[] {
73
+ return rows.map(rowToNode)
74
+ }
75
+
76
+ function prefixedCols() {
77
+ return NODE_COLS.split(', ')
78
+ .map((c) => `n.${c}`)
79
+ .join(', ')
80
+ }
81
+
82
+ export function createResolvers(db: Db) {
83
+ return {
84
+ Query: {
85
+ node: (_: unknown, args: { id: string }) => {
86
+ return selectNode(db, args.id)
87
+ },
88
+
89
+ nodes: (_: unknown, args: { type?: string }) => {
90
+ const conditions: string[] = []
91
+ const params: string[] = []
92
+ if (args.type) {
93
+ conditions.push('type = ?')
94
+ params.push(args.type)
95
+ }
96
+ const where =
97
+ conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
98
+ const rows = db.raw
99
+ .query(`SELECT ${NODE_COLS} FROM nodes ${where}`)
100
+ .all(...params) as NodeRow[]
101
+ return selectNodes(rows)
102
+ },
103
+
104
+ descendants: (
105
+ _: unknown,
106
+ args: { nodeId: string; relation?: string; maxDepth?: number },
107
+ ) => {
108
+ const maxDepth = args.maxDepth ?? 100
109
+ if (maxDepth === 0) return []
110
+ let rows: NodeRow[]
111
+ if (args.relation) {
112
+ rows = db.raw
113
+ .query(
114
+ `WITH RECURSIVE tree(id, depth) AS (
115
+ SELECT source_id, 1 FROM edges WHERE target_id = ?1 AND relation = ?3
116
+ UNION ALL
117
+ SELECT e.source_id, t.depth + 1 FROM edges e
118
+ JOIN tree t ON e.target_id = t.id WHERE t.depth < ?2 AND e.relation = ?3
119
+ )
120
+ SELECT DISTINCT ${prefixedCols()} FROM nodes n JOIN tree t ON n.id = t.id`,
121
+ )
122
+ .all(args.nodeId, maxDepth, args.relation) as NodeRow[]
123
+ } else {
124
+ rows = db.raw
125
+ .query(
126
+ `WITH RECURSIVE tree(id, depth) AS (
127
+ SELECT source_id, 1 FROM edges WHERE target_id = ?1
128
+ UNION ALL
129
+ SELECT e.source_id, t.depth + 1 FROM edges e
130
+ JOIN tree t ON e.target_id = t.id WHERE t.depth < ?2
131
+ )
132
+ SELECT DISTINCT ${prefixedCols()} FROM nodes n JOIN tree t ON n.id = t.id`,
133
+ )
134
+ .all(args.nodeId, maxDepth) as NodeRow[]
135
+ }
136
+ return selectNodes(rows)
137
+ },
138
+
139
+ subtree: (_: unknown, args: { nodeId: string; maxDepth?: number }) => {
140
+ const maxDepth = args.maxDepth ?? 100
141
+ if (maxDepth === 0) return []
142
+ const rows = db.raw
143
+ .query(
144
+ `WITH RECURSIVE tree(id, depth) AS (
145
+ SELECT source_id, 1 FROM edges WHERE target_id = ?1
146
+ UNION ALL
147
+ SELECT e.source_id, t.depth + 1 FROM edges e
148
+ JOIN tree t ON e.target_id = t.id WHERE t.depth < ?2
149
+ )
150
+ SELECT DISTINCT ${prefixedCols()} FROM nodes n JOIN tree t ON n.id = t.id`,
151
+ )
152
+ .all(args.nodeId, maxDepth) as NodeRow[]
153
+ return selectNodes(rows)
154
+ },
155
+
156
+ search: (
157
+ _: unknown,
158
+ args: {
159
+ query: string
160
+ type?: string
161
+ status?: string
162
+ limit?: number
163
+ },
164
+ ) => {
165
+ if (args.query.trim().length < 3) {
166
+ throw new Error('Search query must be at least 3 characters')
167
+ }
168
+ const escaped = args.query.replace(/[%_\\]/g, '\\$&')
169
+ const conditions = [
170
+ "(title LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')",
171
+ ]
172
+ const params: (string | number)[] = [`%${escaped}%`, `%${escaped}%`]
173
+ if (args.type) {
174
+ conditions.push('type = ?')
175
+ params.push(args.type)
176
+ }
177
+ if (args.status) {
178
+ conditions.push('status = ?')
179
+ params.push(args.status)
180
+ }
181
+ const limit = args.limit ?? 50
182
+ const rows = db.raw
183
+ .query(
184
+ `SELECT ${NODE_COLS} FROM nodes WHERE ${conditions.join(' AND ')} LIMIT ?`,
185
+ )
186
+ .all(...params, limit) as NodeRow[]
187
+ return selectNodes(rows)
188
+ },
189
+ },
190
+
191
+ Mutation: {
192
+ createNode: (
193
+ _: unknown,
194
+ args: { type: string; title: string; description?: string },
195
+ ): NodeGql => {
196
+ if (!args.title.trim()) throw new Error('Title is required')
197
+ if (args.description != null && !args.description.trim()) {
198
+ throw new Error('Description cannot be empty')
199
+ }
200
+ const id = generateId(args.type)
201
+ const now = new Date().toISOString()
202
+ const description = args.description ?? null
203
+ db.raw.run(
204
+ 'INSERT INTO nodes (id, type, title, description, status, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
205
+ [id, args.type, args.title, description, 'draft', null, now, now],
206
+ )
207
+ return {
208
+ id,
209
+ type: args.type,
210
+ title: args.title,
211
+ description,
212
+ status: 'draft',
213
+ metadata: null,
214
+ createdAt: now,
215
+ updatedAt: now,
216
+ }
217
+ },
218
+
219
+ updateNode: (_: unknown, args: { id: string; status?: string }) => {
220
+ const existing = db.raw
221
+ .query('SELECT * FROM nodes WHERE id = ?')
222
+ .get(args.id) as NodeRow | null
223
+ if (!existing) throw new Error(`Node ${args.id} not found`)
224
+ const newStatus = args.status ?? existing.status
225
+ if (args.status && !VALID_STATUSES.has(args.status)) {
226
+ throw new Error(
227
+ `Invalid status: ${args.status}. Must be one of: ${[...VALID_STATUSES].join(', ')}`,
228
+ )
229
+ }
230
+ const now = new Date().toISOString()
231
+ db.raw.run('UPDATE nodes SET status = ?, updated_at = ? WHERE id = ?', [
232
+ newStatus,
233
+ now,
234
+ args.id,
235
+ ])
236
+ return rowToNode({ ...existing, status: newStatus, updated_at: now })
237
+ },
238
+
239
+ approveNode: (_: unknown, args: { id: string }) => {
240
+ const existing = db.raw
241
+ .query('SELECT * FROM nodes WHERE id = ?')
242
+ .get(args.id) as NodeRow | null
243
+ if (!existing) throw new Error(`Node ${args.id} not found`)
244
+ if (existing.status !== 'pending_review') {
245
+ throw new Error(
246
+ `Cannot approve node with status "${existing.status}", must be "pending_review"`,
247
+ )
248
+ }
249
+ const now = new Date().toISOString()
250
+ db.raw.run('UPDATE nodes SET status = ?, updated_at = ? WHERE id = ?', [
251
+ 'approved',
252
+ now,
253
+ args.id,
254
+ ])
255
+ return rowToNode({ ...existing, status: 'approved', updated_at: now })
256
+ },
257
+
258
+ createEdge: (
259
+ _: unknown,
260
+ args: { sourceId: string; targetId: string; relation: string },
261
+ ) => {
262
+ const validRelations = new Set(['part_of', 'blocks'])
263
+ if (!validRelations.has(args.relation)) {
264
+ throw new Error(
265
+ `Invalid relation: ${args.relation}. Must be 'part_of' or 'blocks'`,
266
+ )
267
+ }
268
+ const sourceExists = db.raw
269
+ .query('SELECT id FROM nodes WHERE id = ?')
270
+ .get(args.sourceId)
271
+ if (!sourceExists) {
272
+ throw new Error(`Source node ${args.sourceId} not found`)
273
+ }
274
+ const targetExists = db.raw
275
+ .query('SELECT id FROM nodes WHERE id = ?')
276
+ .get(args.targetId)
277
+ if (!targetExists) {
278
+ throw new Error(`Target node ${args.targetId} not found`)
279
+ }
280
+ if (args.relation === 'blocks' && args.sourceId === args.targetId) {
281
+ throw new Error('A node cannot block itself')
282
+ }
283
+ const now = new Date().toISOString()
284
+ db.raw.run(
285
+ 'INSERT INTO edges (source_id, target_id, relation, created_at) VALUES (?, ?, ?, ?)',
286
+ [args.sourceId, args.targetId, args.relation, now],
287
+ )
288
+ return {
289
+ sourceId: args.sourceId,
290
+ targetId: args.targetId,
291
+ relation: args.relation,
292
+ createdAt: now,
293
+ }
294
+ },
295
+
296
+ removeEdge: (
297
+ _: unknown,
298
+ args: { sourceId: string; targetId: string; relation: string },
299
+ ) => {
300
+ const result = db.raw.run(
301
+ 'DELETE FROM edges WHERE source_id = ? AND target_id = ? AND relation = ?',
302
+ [args.sourceId, args.targetId, args.relation],
303
+ )
304
+ return result.changes > 0
305
+ },
306
+ },
307
+ }
308
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { schema } from './schema.ts'
3
+
4
+ describe('schema', () => {
5
+ it('exports a valid GraphQL schema object', () => {
6
+ expect(schema).toBeDefined()
7
+ expect(typeof schema).toBe('object')
8
+ })
9
+
10
+ it('defines Node type with required fields', () => {
11
+ const nodeType = schema.getType(
12
+ 'Node',
13
+ ) as import('graphql').GraphQLObjectType
14
+ expect(nodeType).toBeDefined()
15
+ const fields = nodeType.getFields()
16
+ expect(fields.id).toBeDefined()
17
+ expect(fields.type).toBeDefined()
18
+ expect(fields.title).toBeDefined()
19
+ expect(fields.description).toBeDefined()
20
+ expect(fields.status).toBeDefined()
21
+ expect(fields.metadata).toBeDefined()
22
+ expect(fields.createdAt).toBeDefined()
23
+ expect(fields.updatedAt).toBeDefined()
24
+ })
25
+
26
+ it('uses String types instead of enums (no NodeType, Status, Relation enums)', () => {
27
+ expect(schema.getType('NodeType')).toBeUndefined()
28
+ expect(schema.getType('Status')).toBeUndefined()
29
+ expect(schema.getType('Relation')).toBeUndefined()
30
+ })
31
+
32
+ it('Node type does not have children, blockedBy, or blocking fields', () => {
33
+ const nodeType = schema.getType(
34
+ 'Node',
35
+ ) as import('graphql').GraphQLObjectType
36
+ const fields = nodeType.getFields()
37
+ expect(fields.children).toBeUndefined()
38
+ expect(fields.blockedBy).toBeUndefined()
39
+ expect(fields.blocking).toBeUndefined()
40
+ })
41
+
42
+ it('Edge type has sourceId, targetId, relation, createdAt as String fields', () => {
43
+ const edgeType = schema.getType(
44
+ 'Edge',
45
+ ) as import('graphql').GraphQLObjectType
46
+ const fields = edgeType.getFields()
47
+ expect(fields.sourceId).toBeDefined()
48
+ expect(fields.targetId).toBeDefined()
49
+ expect(fields.relation).toBeDefined()
50
+ expect(fields.createdAt).toBeDefined()
51
+ })
52
+
53
+ it('Query type has node, nodes, descendants, subtree, search', () => {
54
+ const queryType = schema.getType(
55
+ 'Query',
56
+ ) as import('graphql').GraphQLObjectType
57
+ const fields = queryType.getFields()
58
+ expect(fields.node).toBeDefined()
59
+ expect(fields.nodes).toBeDefined()
60
+ expect(fields.descendants).toBeDefined()
61
+ expect(fields.subtree).toBeDefined()
62
+ expect(fields.search).toBeDefined()
63
+ })
64
+
65
+ it('Query type does not have tree', () => {
66
+ const queryType = schema.getType(
67
+ 'Query',
68
+ ) as import('graphql').GraphQLObjectType
69
+ const fields = queryType.getFields()
70
+ expect(fields.tree).toBeUndefined()
71
+ })
72
+
73
+ it('Mutation type has createNode, updateNode, approveNode, createEdge, removeEdge', () => {
74
+ const mutationType = schema.getType(
75
+ 'Mutation',
76
+ ) as import('graphql').GraphQLObjectType
77
+ const fields = mutationType.getFields()
78
+ expect(fields.createNode).toBeDefined()
79
+ expect(fields.updateNode).toBeDefined()
80
+ expect(fields.approveNode).toBeDefined()
81
+ expect(fields.createEdge).toBeDefined()
82
+ expect(fields.removeEdge).toBeDefined()
83
+ })
84
+
85
+ it('Mutation type does not have deleteNode or deleteEdge', () => {
86
+ const mutationType = schema.getType(
87
+ 'Mutation',
88
+ ) as import('graphql').GraphQLObjectType
89
+ const fields = mutationType.getFields()
90
+ expect(fields.deleteNode).toBeUndefined()
91
+ expect(fields.deleteEdge).toBeUndefined()
92
+ })
93
+ })
@@ -0,0 +1,45 @@
1
+ import { createSchema } from 'graphql-yoga'
2
+
3
+ export const typeDefs = /* GraphQL */ `
4
+ type Node {
5
+ id: String!
6
+ type: String!
7
+ title: String!
8
+ description: String
9
+ status: String!
10
+ metadata: String
11
+ createdAt: String!
12
+ updatedAt: String!
13
+ }
14
+
15
+ type Edge {
16
+ sourceId: String!
17
+ targetId: String!
18
+ relation: String!
19
+ createdAt: String!
20
+ }
21
+
22
+ type Query {
23
+ node(id: String!): Node
24
+ nodes(type: String): [Node!]!
25
+ descendants(nodeId: String!, relation: String, maxDepth: Int): [Node!]!
26
+ subtree(nodeId: String!, maxDepth: Int): [Node!]!
27
+ search(query: String!, type: String, status: String, limit: Int): [Node!]!
28
+ }
29
+
30
+ type Mutation {
31
+ createNode(type: String!, title: String!, description: String): Node!
32
+ updateNode(id: String!, status: String): Node!
33
+ approveNode(id: String!): Node!
34
+ createEdge(sourceId: String!, targetId: String!, relation: String!): Edge!
35
+ removeEdge(sourceId: String!, targetId: String!, relation: String!): Boolean!
36
+ }
37
+ `
38
+
39
+ export const schema = createSchema({
40
+ typeDefs,
41
+ resolvers: {
42
+ Query: {},
43
+ Mutation: {},
44
+ },
45
+ })
@@ -0,0 +1,128 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Flowy — Agentic Persistent Planning
7
+
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.
9
+
10
+ ## Why Use Flowy
11
+
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.
13
+
14
+ ## First Time in a Project
15
+
16
+ ```bash
17
+ flowy init # auto-detects the git repo, creates a project, maps this directory
18
+ ```
19
+
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
+ ```
25
+
26
+ ## Core Workflow
27
+
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"
32
+
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"
36
+
37
+ # 3. Execute and track
38
+ flowy status <task-id> in_progress
39
+ # ... do the work ...
40
+ flowy status <task-id> done
41
+
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
+ ```
46
+
47
+ ## Entity Hierarchy
48
+
49
+ ```
50
+ project -> feature -> task
51
+ 1:many 1:many
52
+ ```
53
+
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
60
+ ```
61
+
62
+ Also: `blocked`, `cancelled`
63
+
64
+ Only `pending_review` entities can be approved via `flowy approve <id>`.
65
+
66
+ ## Commands
67
+
68
+ ### Project Context
69
+ ```bash
70
+ flowy init # Auto-detect repo, create + set project
71
+ flowy project list # List all projects
72
+ flowy project show [<id>] # Show project details
73
+ ```
74
+
75
+ ### Features (requires active project)
76
+ ```bash
77
+ flowy feature create --title "Title" --description "description or file.md"
78
+ flowy feature set "Title or ID" # Set active feature
79
+ flowy feature unset # Clear active feature
80
+ flowy feature list # List features in project
81
+ flowy feature show [<id>] # Show feature details
82
+ ```
83
+
84
+ ### Tasks (requires active feature)
85
+ ```bash
86
+ flowy task create --title "Title" --description "description or file.md"
87
+ flowy task list # List tasks in feature
88
+ flowy task show <id> # Show task details
89
+ flowy task block <blocker> <blocked> # Mark dependency
90
+ flowy task unblock <blocker> <blocked>
91
+ ```
92
+
93
+ ### Status and Approval
94
+ ```bash
95
+ flowy status <id> in_progress
96
+ flowy status <id> pending_review
97
+ flowy approve <id> # Only works on pending_review
98
+ flowy status <id> done
99
+ ```
100
+
101
+ ### Search and Explore
102
+ ```bash
103
+ flowy search "query" --type task --status draft --limit 10
104
+ flowy tree <project-id> --depth 3 # Show full subtree
105
+ ```
106
+
107
+ ## Validation Rules
108
+
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
116
+
117
+ ## Output Format
118
+
119
+ All commands output JSON to stdout. Errors go to stderr as `{ "error": "message" }`.
120
+
121
+ ## Environment Variables
122
+
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,40 @@
1
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import { resolve } from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest'
5
+
6
+ const CONFIG_PATH = resolve(homedir(), '.config', 'flowy', 'config.json')
7
+
8
+ describe('client command', () => {
9
+ let originalConfig: string | null = null
10
+
11
+ beforeEach(() => {
12
+ originalConfig = existsSync(CONFIG_PATH)
13
+ ? readFileSync(CONFIG_PATH, 'utf-8')
14
+ : null
15
+ })
16
+
17
+ afterEach(() => {
18
+ if (originalConfig !== null) {
19
+ writeFileSync(CONFIG_PATH, originalConfig)
20
+ } else if (existsSync(CONFIG_PATH)) {
21
+ rmSync(CONFIG_PATH)
22
+ }
23
+ })
24
+
25
+ test('exports a command group named client', async () => {
26
+ const { clientCommand } = await import('./client.ts')
27
+ expect(clientCommand.name()).toBe('client')
28
+ })
29
+
30
+ test('set name updates config', async () => {
31
+ const { clientCommand } = await import('./client.ts')
32
+ await clientCommand.parseAsync(['set', 'name', 'Acme Corp'], {
33
+ from: 'user',
34
+ })
35
+
36
+ const { loadConfig } = await import('../util/config.ts')
37
+ const config = loadConfig()
38
+ expect(config.client.name).toBe('Acme Corp')
39
+ })
40
+ })
@@ -0,0 +1,34 @@
1
+ import { Command } from 'commander'
2
+ import { loadConfig, saveConfig } from '../util/config.ts'
3
+ import { output, outputError } from '../util/format.ts'
4
+
5
+ const SUPPORTED_PROPERTIES = ['name'] as const
6
+
7
+ export const clientCommand = new Command('client').description(
8
+ 'Manage client settings',
9
+ )
10
+
11
+ clientCommand
12
+ .command('set')
13
+ .description('Set a client property')
14
+ .argument('<property>', 'Property to set')
15
+ .argument('<value>', 'Value to set')
16
+ .action((property: string, value: string) => {
17
+ try {
18
+ if (
19
+ !SUPPORTED_PROPERTIES.includes(
20
+ property as (typeof SUPPORTED_PROPERTIES)[number],
21
+ )
22
+ ) {
23
+ throw new Error(
24
+ `Unknown property "${property}". Supported: ${SUPPORTED_PROPERTIES.join(', ')}`,
25
+ )
26
+ }
27
+ const config = loadConfig()
28
+ config.client.name = value
29
+ saveConfig(config)
30
+ output({ property, value })
31
+ } catch (error) {
32
+ outputError(error)
33
+ }
34
+ })