Repository navigation
Expand file tree
/
Copy pathderive.ts
More file actions
80 lines (72 loc) · 2.72 KB
/
Copy pathderive.ts
File metadata and controls
80 lines (72 loc) · 2.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import type { Command } from 'commander'
import { V2_OPERATIONS, type V2OperationName } from '../generated/v2-api'
/**
* Trailing path segments that read as verbs rather than sub-resources, so
* `/tables/[id]/rows/upsert` derives `tables upsert` instead of
* `tables rows upsert create`.
*
* `execute` and `cancel` are deliberately absent: they are verbs, but their
* derived names read badly enough that the contract names them explicitly, and
* listing them here would produce `workflows execute` — close, but not the
* `workflows run` the contract asks for. Keeping them out means the contract is
* the only place that decision lives.
*/
const ACTION_SEGMENTS = new Set([
'upsert',
'query',
'search',
'export',
'import',
'deploy',
'rollback',
])
/**
* Derives a command path from an operation's route.
*
* `<resource> [sub-resource] <verb>`, where the verb comes from the method and
* whether the path ends in a parameter (an item) or not (a collection). This
* covers 41 of the 47 operations; the rest are named in the CLI contract.
*/
export function deriveCommandPath(operation: V2OperationName): string[] {
const spec = V2_OPERATIONS[operation]
const segments = spec.path.replace('/api/v2/', '').split('/')
const resource = segments[0]
const nouns = segments.slice(1).filter((segment) => !segment.startsWith('['))
const last = nouns[nouns.length - 1]
if (last && ACTION_SEGMENTS.has(last)) return [resource, last]
const isItem = spec.path.endsWith(']')
const verb =
spec.method === 'GET'
? isItem
? 'get'
: 'list'
: spec.method === 'POST'
? 'create'
: spec.method === 'DELETE'
? 'delete'
: 'update'
return last ? [resource, last, verb] : [resource, verb]
}
/** `conflictTarget` → `conflict-target`. */
export function kebab(value: string): string {
return value.replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)
}
/**
* `min-duration-ms` → `minDurationMs`, the key commander actually stores.
*
* Commander camelCases every multi-word flag when it builds its options object,
* so a lookup by the flag's own name finds nothing and the value is silently
* dropped — no error, the field just never reaches the API. Every read of a
* parsed flag has to go through this.
*/
export function camel(flag: string): string {
return flag.replace(/-([a-z])/g, (_match, character: string) => character.toUpperCase())
}
/** The command's own name and its ancestors', root excluded, in typing order. */
export function commandPath(command: Command): string[] {
const names: string[] = []
for (let current: Command | null = command; current?.parent; current = current.parent) {
names.unshift(current.name())
}
return names
}