@baseworks/organization 0.2.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/dist/chunk-5UCSEIJS.js +64 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +534 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +335 -0
- package/dist/schema/pg/index.d.ts +562 -0
- package/dist/schema/pg/index.js +62 -0
- package/dist/schema/sqlite/index.d.ts +604 -0
- package/dist/schema/sqlite/index.js +12 -0
- package/package.json +37 -0
- package/src/__tests__/cli-env.test.ts +158 -0
- package/src/__tests__/cli-org.test.ts +154 -0
- package/src/__tests__/cli-proj.test.ts +157 -0
- package/src/__tests__/cli-ws.test.ts +156 -0
- package/src/__tests__/helpers.ts +29 -0
- package/src/cli.ts +682 -0
- package/src/index.ts +5 -0
- package/src/operations/bootstrap.ts +50 -0
- package/src/repo/environments.ts +82 -0
- package/src/repo/index.ts +9 -0
- package/src/repo/organizations.ts +96 -0
- package/src/repo/projects.ts +106 -0
- package/src/repo/workspaces.ts +87 -0
- package/src/schema/environments.ts +14 -0
- package/src/schema/index.ts +5 -0
- package/src/schema/organizations.ts +11 -0
- package/src/schema/pg/environments.ts +14 -0
- package/src/schema/pg/index.ts +4 -0
- package/src/schema/pg/organizations.ts +11 -0
- package/src/schema/pg/projects.ts +16 -0
- package/src/schema/pg/workspaces.ts +15 -0
- package/src/schema/projects.ts +16 -0
- package/src/schema/sqlite/environments.ts +14 -0
- package/src/schema/sqlite/index.ts +4 -0
- package/src/schema/sqlite/organizations.ts +11 -0
- package/src/schema/sqlite/projects.ts +16 -0
- package/src/schema/sqlite/workspaces.ts +15 -0
- package/src/schema/workspaces.ts +15 -0
- package/src/types.ts +88 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/// <reference types="vitest/globals" />
|
|
2
|
+
import { beforeAll, afterEach, afterAll, describe, it, expect } from 'vitest'
|
|
3
|
+
import { run, mocks, server } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
|
|
6
|
+
afterEach(() => server.resetHandlers())
|
|
7
|
+
afterAll(() => server.close())
|
|
8
|
+
|
|
9
|
+
const ORG_SLUG = 'dotlabs'
|
|
10
|
+
const WS_SLUG = 'staging'
|
|
11
|
+
const PROJ_SLUG = 'api'
|
|
12
|
+
const OPT = ['--org', ORG_SLUG, '--ws', WS_SLUG, '--proj', PROJ_SLUG]
|
|
13
|
+
const BASE = `/v1/orgs/${ORG_SLUG}/workspaces/${WS_SLUG}/projects/${PROJ_SLUG}/envs`
|
|
14
|
+
|
|
15
|
+
const ENV = {
|
|
16
|
+
id: '019f08dd-5555-7db6-956d-f323dfadba50',
|
|
17
|
+
shortId: 'ev1abc',
|
|
18
|
+
slug: 'production',
|
|
19
|
+
name: 'Production',
|
|
20
|
+
isDefault: true,
|
|
21
|
+
createdAt: 1782507760630,
|
|
22
|
+
updatedAt: 1782507760630,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ENV2 = {
|
|
26
|
+
id: '019f08de-6666-7db6-956d-f323dfadba50',
|
|
27
|
+
shortId: 'ev2xyz',
|
|
28
|
+
slug: 'preview',
|
|
29
|
+
name: 'Preview',
|
|
30
|
+
isDefault: false,
|
|
31
|
+
createdAt: 1782507760630,
|
|
32
|
+
updatedAt: 1782507760630,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── list ─────────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
describe('flect env (list)', () => {
|
|
38
|
+
it('default columns: ID → SLUG → NAME → DEFAULT', async () => {
|
|
39
|
+
mocks.mockGet(BASE, { envs: [ENV, ENV2] })
|
|
40
|
+
const r = await run(['env', ...OPT])
|
|
41
|
+
expect(r.exitCode).toBe(0)
|
|
42
|
+
expect(r.stdout).toContain('ev1abc')
|
|
43
|
+
expect(r.stdout).toContain('production')
|
|
44
|
+
expect(r.stdout).toContain('Production')
|
|
45
|
+
expect(r.stdout.indexOf('ID')).toBeLessThan(r.stdout.indexOf('SLUG'))
|
|
46
|
+
expect(r.stdout.indexOf('SLUG')).toBeLessThan(r.stdout.indexOf('NAME'))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('does not show full UUID in default view', async () => {
|
|
50
|
+
mocks.mockGet(BASE, { envs: [ENV] })
|
|
51
|
+
const r = await run(['env', ...OPT])
|
|
52
|
+
expect(r.stdout).not.toContain(ENV.id)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('-o wide adds FULL ID column', async () => {
|
|
56
|
+
mocks.mockGet(BASE, { envs: [ENV] })
|
|
57
|
+
const r = await run(['env', '-o', 'wide', ...OPT])
|
|
58
|
+
expect(r.exitCode).toBe(0)
|
|
59
|
+
expect(r.stdout).toContain('FULL ID')
|
|
60
|
+
expect(r.stdout).toContain(ENV.id)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('-o json returns raw JSON array', async () => {
|
|
64
|
+
mocks.mockGet(BASE, { envs: [ENV] })
|
|
65
|
+
const r = await run(['env', '-o', 'json', ...OPT])
|
|
66
|
+
const parsed = JSON.parse(r.stdout)
|
|
67
|
+
expect(Array.isArray(parsed)).toBe(true)
|
|
68
|
+
expect(parsed[0].shortId).toBe('ev1abc')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// ── create ───────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
describe('flect env create', () => {
|
|
75
|
+
it('sends only name when no slug given', async () => {
|
|
76
|
+
const captured = mocks.mockPostCapture(BASE, { env: ENV })
|
|
77
|
+
const r = await run(['env', 'create', '--name', 'Production', ...OPT])
|
|
78
|
+
expect(r.exitCode).toBe(0)
|
|
79
|
+
const body = captured.body as Record<string, unknown>
|
|
80
|
+
expect(body.name).toBe('Production')
|
|
81
|
+
expect(body.slug).toBeUndefined()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('sends explicit slug when provided', async () => {
|
|
85
|
+
const captured = mocks.mockPostCapture(BASE, { env: ENV })
|
|
86
|
+
await run(['env', 'create', '--name', 'Production', '--slug', 'production', ...OPT])
|
|
87
|
+
expect((captured.body as Record<string, unknown>).slug).toBe('production')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('shows shortId in output, not full UUID', async () => {
|
|
91
|
+
mocks.mockPost(BASE, { env: ENV })
|
|
92
|
+
const r = await run(['env', 'create', '--name', 'Production', ...OPT])
|
|
93
|
+
expect(r.stdout).toContain('ev1abc')
|
|
94
|
+
expect(r.stdout).not.toContain(ENV.id)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// ── get ───────────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe('flect env get', () => {
|
|
101
|
+
it('works with slug', async () => {
|
|
102
|
+
mocks.mockGet(`${BASE}/production`, { env: ENV })
|
|
103
|
+
const r = await run(['env', 'get', 'production', ...OPT])
|
|
104
|
+
expect(r.exitCode).toBe(0)
|
|
105
|
+
expect(r.stdout).toContain('Production')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('works with shortId', async () => {
|
|
109
|
+
mocks.mockGet(`${BASE}/ev1abc`, { env: ENV })
|
|
110
|
+
const r = await run(['env', 'get', 'ev1abc', ...OPT])
|
|
111
|
+
expect(r.exitCode).toBe(0)
|
|
112
|
+
expect(r.stdout).toContain('Production')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('id field shows shortId, full_id field shows UUID', async () => {
|
|
116
|
+
mocks.mockGet(`${BASE}/production`, { env: ENV })
|
|
117
|
+
const r = await run(['env', 'get', 'production', ...OPT])
|
|
118
|
+
const lines = r.stdout.split('\n')
|
|
119
|
+
const idLine = lines.find(l => /^\s+id\s/.test(l))
|
|
120
|
+
expect(idLine).toContain('ev1abc')
|
|
121
|
+
expect(idLine).not.toContain(ENV.id)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// ── update ────────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
describe('flect env update', () => {
|
|
128
|
+
it('works with slug', async () => {
|
|
129
|
+
const captured = mocks.mockPatchCapture(`${BASE}/production`, { env: { ...ENV, name: 'Production v2' } })
|
|
130
|
+
const r = await run(['env', 'update', 'production', '--name', 'Production v2', ...OPT])
|
|
131
|
+
expect(r.exitCode).toBe(0)
|
|
132
|
+
expect((captured.body as Record<string, unknown>).name).toBe('Production v2')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('works with shortId', async () => {
|
|
136
|
+
const captured = mocks.mockPatchCapture(`${BASE}/ev1abc`, { env: { ...ENV, name: 'Production v2' } })
|
|
137
|
+
const r = await run(['env', 'update', 'ev1abc', '--name', 'Production v2', ...OPT])
|
|
138
|
+
expect(r.exitCode).toBe(0)
|
|
139
|
+
expect((captured.body as Record<string, unknown>).name).toBe('Production v2')
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// ── delete ────────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
describe('flect env delete', () => {
|
|
146
|
+
it('works with slug', async () => {
|
|
147
|
+
mocks.mockDelete(`${BASE}/production`)
|
|
148
|
+
const r = await run(['env', 'delete', 'production', ...OPT])
|
|
149
|
+
expect(r.exitCode).toBe(0)
|
|
150
|
+
expect(r.stdout).toContain('production')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('works with shortId', async () => {
|
|
154
|
+
mocks.mockDelete(`${BASE}/ev1abc`)
|
|
155
|
+
const r = await run(['env', 'delete', 'ev1abc', ...OPT])
|
|
156
|
+
expect(r.exitCode).toBe(0)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/// <reference types="vitest/globals" />
|
|
2
|
+
import { beforeAll, afterEach, afterAll, describe, it, expect } from 'vitest'
|
|
3
|
+
import { run, mocks, server } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
|
|
6
|
+
afterEach(() => server.resetHandlers())
|
|
7
|
+
afterAll(() => server.close())
|
|
8
|
+
|
|
9
|
+
const ORG = {
|
|
10
|
+
id: '019f05bd-ebf7-7db6-956d-f323dfadba50',
|
|
11
|
+
shortId: 'n2shs8',
|
|
12
|
+
slug: 'dotlabs',
|
|
13
|
+
name: 'Dotlabs',
|
|
14
|
+
metadata: null,
|
|
15
|
+
createdAt: 1782507760630,
|
|
16
|
+
updatedAt: 1782507760630,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ORG2 = {
|
|
20
|
+
id: '019f05bc-8f7c-716d-8530-3a3683ea2411',
|
|
21
|
+
shortId: 'kf5wnb',
|
|
22
|
+
slug: 'acme',
|
|
23
|
+
name: 'Acme Inc',
|
|
24
|
+
metadata: null,
|
|
25
|
+
createdAt: 1782507671419,
|
|
26
|
+
updatedAt: 1782507671419,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── list ─────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
describe('flect org (list)', () => {
|
|
32
|
+
it('default columns: ID (shortId) → SLUG → NAME', async () => {
|
|
33
|
+
mocks.mockGet('/v1/orgs', { orgs: [ORG, ORG2] })
|
|
34
|
+
const r = await run(['org'])
|
|
35
|
+
expect(r.exitCode).toBe(0)
|
|
36
|
+
expect(r.stdout).toContain('n2shs8')
|
|
37
|
+
expect(r.stdout).toContain('dotlabs')
|
|
38
|
+
expect(r.stdout).toContain('Dotlabs')
|
|
39
|
+
expect(r.stdout.indexOf('ID')).toBeLessThan(r.stdout.indexOf('SLUG'))
|
|
40
|
+
expect(r.stdout.indexOf('SLUG')).toBeLessThan(r.stdout.indexOf('NAME'))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('does not show full UUID in default view', async () => {
|
|
44
|
+
mocks.mockGet('/v1/orgs', { orgs: [ORG] })
|
|
45
|
+
const r = await run(['org'])
|
|
46
|
+
expect(r.stdout).not.toContain(ORG.id)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('-o wide adds CREATED and FULL ID columns', async () => {
|
|
50
|
+
mocks.mockGet('/v1/orgs', { orgs: [ORG] })
|
|
51
|
+
const r = await run(['org', '-o', 'wide'])
|
|
52
|
+
expect(r.exitCode).toBe(0)
|
|
53
|
+
expect(r.stdout).toContain('CREATED')
|
|
54
|
+
expect(r.stdout).toContain('FULL ID')
|
|
55
|
+
expect(r.stdout).toContain(ORG.id)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('-o json returns raw JSON array', async () => {
|
|
59
|
+
mocks.mockGet('/v1/orgs', { orgs: [ORG] })
|
|
60
|
+
const r = await run(['org', '-o', 'json'])
|
|
61
|
+
expect(r.exitCode).toBe(0)
|
|
62
|
+
const parsed = JSON.parse(r.stdout)
|
|
63
|
+
expect(Array.isArray(parsed)).toBe(true)
|
|
64
|
+
expect(parsed[0].shortId).toBe('n2shs8')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// ── create ───────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
describe('flect org create', () => {
|
|
71
|
+
it('sends only name when no slug given (API generates slug from name)', async () => {
|
|
72
|
+
const captured = mocks.mockPostCapture('/v1/orgs', { org: { ...ORG, slug: 'my-org', shortId: 'abc123' } })
|
|
73
|
+
const r = await run(['org', 'create', '--name', 'My Org'])
|
|
74
|
+
expect(r.exitCode).toBe(0)
|
|
75
|
+
const body = captured.body as Record<string, unknown>
|
|
76
|
+
expect(body.name).toBe('My Org')
|
|
77
|
+
expect(body.slug).toBeUndefined()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('sends explicit slug when provided', async () => {
|
|
81
|
+
const captured = mocks.mockPostCapture('/v1/orgs', { org: ORG })
|
|
82
|
+
await run(['org', 'create', '--name', 'Dotlabs', '--slug', 'dotlabs'])
|
|
83
|
+
expect((captured.body as Record<string, unknown>).slug).toBe('dotlabs')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('shows shortId (not full UUID) in output', async () => {
|
|
87
|
+
mocks.mockPost('/v1/orgs', { org: ORG })
|
|
88
|
+
const r = await run(['org', 'create', '--name', 'Dotlabs'])
|
|
89
|
+
expect(r.stdout).toContain('n2shs8')
|
|
90
|
+
expect(r.stdout).not.toContain(ORG.id)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ── get ───────────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe('flect org get', () => {
|
|
97
|
+
it('works with slug', async () => {
|
|
98
|
+
mocks.mockGet('/v1/orgs/dotlabs', { org: ORG })
|
|
99
|
+
const r = await run(['org', 'get', 'dotlabs'])
|
|
100
|
+
expect(r.exitCode).toBe(0)
|
|
101
|
+
expect(r.stdout).toContain('Dotlabs')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('works with shortId', async () => {
|
|
105
|
+
mocks.mockGet('/v1/orgs/n2shs8', { org: ORG })
|
|
106
|
+
const r = await run(['org', 'get', 'n2shs8'])
|
|
107
|
+
expect(r.exitCode).toBe(0)
|
|
108
|
+
expect(r.stdout).toContain('Dotlabs')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('id field shows shortId, full_id field shows UUID', async () => {
|
|
112
|
+
mocks.mockGet('/v1/orgs/dotlabs', { org: ORG })
|
|
113
|
+
const r = await run(['org', 'get', 'dotlabs'])
|
|
114
|
+
const lines = r.stdout.split('\n')
|
|
115
|
+
const idLine = lines.find(l => /^\s+id\s/.test(l))
|
|
116
|
+
expect(idLine).toContain('n2shs8')
|
|
117
|
+
expect(idLine).not.toContain(ORG.id)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// ── update ────────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe('flect org update', () => {
|
|
124
|
+
it('works with slug', async () => {
|
|
125
|
+
const captured = mocks.mockPatchCapture('/v1/orgs/dotlabs', { org: { ...ORG, name: 'Dotlabs HQ' } })
|
|
126
|
+
const r = await run(['org', 'update', 'dotlabs', '--name', 'Dotlabs HQ'])
|
|
127
|
+
expect(r.exitCode).toBe(0)
|
|
128
|
+
expect((captured.body as Record<string, unknown>).name).toBe('Dotlabs HQ')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('works with shortId', async () => {
|
|
132
|
+
const captured = mocks.mockPatchCapture('/v1/orgs/n2shs8', { org: { ...ORG, name: 'Dotlabs HQ' } })
|
|
133
|
+
const r = await run(['org', 'update', 'n2shs8', '--name', 'Dotlabs HQ'])
|
|
134
|
+
expect(r.exitCode).toBe(0)
|
|
135
|
+
expect((captured.body as Record<string, unknown>).name).toBe('Dotlabs HQ')
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ── delete ────────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
describe('flect org delete', () => {
|
|
142
|
+
it('works with slug', async () => {
|
|
143
|
+
mocks.mockDelete('/v1/orgs/dotlabs')
|
|
144
|
+
const r = await run(['org', 'delete', 'dotlabs'])
|
|
145
|
+
expect(r.exitCode).toBe(0)
|
|
146
|
+
expect(r.stdout).toContain('dotlabs')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('works with shortId', async () => {
|
|
150
|
+
mocks.mockDelete('/v1/orgs/n2shs8')
|
|
151
|
+
const r = await run(['org', 'delete', 'n2shs8'])
|
|
152
|
+
expect(r.exitCode).toBe(0)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/// <reference types="vitest/globals" />
|
|
2
|
+
import { beforeAll, afterEach, afterAll, describe, it, expect } from 'vitest'
|
|
3
|
+
import { run, mocks, server } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
|
|
6
|
+
afterEach(() => server.resetHandlers())
|
|
7
|
+
afterAll(() => server.close())
|
|
8
|
+
|
|
9
|
+
const ORG_SLUG = 'dotlabs'
|
|
10
|
+
const WS_SLUG = 'staging'
|
|
11
|
+
const OPT = ['--org', ORG_SLUG, '--ws', WS_SLUG]
|
|
12
|
+
const BASE = `/v1/orgs/${ORG_SLUG}/workspaces/${WS_SLUG}/projects`
|
|
13
|
+
|
|
14
|
+
const PROJ = {
|
|
15
|
+
id: '019f07cc-3333-7db6-956d-f323dfadba50',
|
|
16
|
+
shortId: 'pj9abc',
|
|
17
|
+
slug: 'api',
|
|
18
|
+
name: 'API',
|
|
19
|
+
isDefault: true,
|
|
20
|
+
createdAt: 1782507760630,
|
|
21
|
+
updatedAt: 1782507760630,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PROJ2 = {
|
|
25
|
+
id: '019f07cd-4444-7db6-956d-f323dfadba50',
|
|
26
|
+
shortId: 'pjxyz1',
|
|
27
|
+
slug: 'web',
|
|
28
|
+
name: 'Web',
|
|
29
|
+
isDefault: false,
|
|
30
|
+
createdAt: 1782507760630,
|
|
31
|
+
updatedAt: 1782507760630,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── list ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
describe('flect proj (list)', () => {
|
|
37
|
+
it('default columns: ID → SLUG → NAME → DEFAULT', async () => {
|
|
38
|
+
mocks.mockGet(BASE, { projects: [PROJ, PROJ2] })
|
|
39
|
+
const r = await run(['proj', ...OPT])
|
|
40
|
+
expect(r.exitCode).toBe(0)
|
|
41
|
+
expect(r.stdout).toContain('pj9abc')
|
|
42
|
+
expect(r.stdout).toContain('api')
|
|
43
|
+
expect(r.stdout).toContain('API')
|
|
44
|
+
expect(r.stdout.indexOf('ID')).toBeLessThan(r.stdout.indexOf('SLUG'))
|
|
45
|
+
expect(r.stdout.indexOf('SLUG')).toBeLessThan(r.stdout.indexOf('NAME'))
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('does not show full UUID in default view', async () => {
|
|
49
|
+
mocks.mockGet(BASE, { projects: [PROJ] })
|
|
50
|
+
const r = await run(['proj', ...OPT])
|
|
51
|
+
expect(r.stdout).not.toContain(PROJ.id)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('-o wide adds FULL ID column', async () => {
|
|
55
|
+
mocks.mockGet(BASE, { projects: [PROJ] })
|
|
56
|
+
const r = await run(['proj', '-o', 'wide', ...OPT])
|
|
57
|
+
expect(r.exitCode).toBe(0)
|
|
58
|
+
expect(r.stdout).toContain('FULL ID')
|
|
59
|
+
expect(r.stdout).toContain(PROJ.id)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('-o json returns raw JSON array', async () => {
|
|
63
|
+
mocks.mockGet(BASE, { projects: [PROJ] })
|
|
64
|
+
const r = await run(['proj', '-o', 'json', ...OPT])
|
|
65
|
+
const parsed = JSON.parse(r.stdout)
|
|
66
|
+
expect(Array.isArray(parsed)).toBe(true)
|
|
67
|
+
expect(parsed[0].shortId).toBe('pj9abc')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// ── create ───────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
describe('flect proj create', () => {
|
|
74
|
+
it('sends only name when no slug given', async () => {
|
|
75
|
+
const captured = mocks.mockPostCapture(BASE, { project: PROJ })
|
|
76
|
+
const r = await run(['proj', 'create', '--name', 'API', ...OPT])
|
|
77
|
+
expect(r.exitCode).toBe(0)
|
|
78
|
+
const body = captured.body as Record<string, unknown>
|
|
79
|
+
expect(body.name).toBe('API')
|
|
80
|
+
expect(body.slug).toBeUndefined()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('sends explicit slug when provided', async () => {
|
|
84
|
+
const captured = mocks.mockPostCapture(BASE, { project: PROJ })
|
|
85
|
+
await run(['proj', 'create', '--name', 'API', '--slug', 'api', ...OPT])
|
|
86
|
+
expect((captured.body as Record<string, unknown>).slug).toBe('api')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('shows shortId in output, not full UUID', async () => {
|
|
90
|
+
mocks.mockPost(BASE, { project: PROJ })
|
|
91
|
+
const r = await run(['proj', 'create', '--name', 'API', ...OPT])
|
|
92
|
+
expect(r.stdout).toContain('pj9abc')
|
|
93
|
+
expect(r.stdout).not.toContain(PROJ.id)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// ── get ───────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
describe('flect proj get', () => {
|
|
100
|
+
it('works with slug', async () => {
|
|
101
|
+
mocks.mockGet(`${BASE}/api`, { project: PROJ })
|
|
102
|
+
const r = await run(['proj', 'get', 'api', ...OPT])
|
|
103
|
+
expect(r.exitCode).toBe(0)
|
|
104
|
+
expect(r.stdout).toContain('API')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('works with shortId', async () => {
|
|
108
|
+
mocks.mockGet(`${BASE}/pj9abc`, { project: PROJ })
|
|
109
|
+
const r = await run(['proj', 'get', 'pj9abc', ...OPT])
|
|
110
|
+
expect(r.exitCode).toBe(0)
|
|
111
|
+
expect(r.stdout).toContain('API')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('id field shows shortId, full_id field shows UUID', async () => {
|
|
115
|
+
mocks.mockGet(`${BASE}/api`, { project: PROJ })
|
|
116
|
+
const r = await run(['proj', 'get', 'api', ...OPT])
|
|
117
|
+
const lines = r.stdout.split('\n')
|
|
118
|
+
const idLine = lines.find(l => /^\s+id\s/.test(l))
|
|
119
|
+
expect(idLine).toContain('pj9abc')
|
|
120
|
+
expect(idLine).not.toContain(PROJ.id)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// ── update ────────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
describe('flect proj update', () => {
|
|
127
|
+
it('works with slug', async () => {
|
|
128
|
+
const captured = mocks.mockPatchCapture(`${BASE}/api`, { project: { ...PROJ, name: 'API v2' } })
|
|
129
|
+
const r = await run(['proj', 'update', 'api', '--name', 'API v2', ...OPT])
|
|
130
|
+
expect(r.exitCode).toBe(0)
|
|
131
|
+
expect((captured.body as Record<string, unknown>).name).toBe('API v2')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('works with shortId', async () => {
|
|
135
|
+
const captured = mocks.mockPatchCapture(`${BASE}/pj9abc`, { project: { ...PROJ, name: 'API v2' } })
|
|
136
|
+
const r = await run(['proj', 'update', 'pj9abc', '--name', 'API v2', ...OPT])
|
|
137
|
+
expect(r.exitCode).toBe(0)
|
|
138
|
+
expect((captured.body as Record<string, unknown>).name).toBe('API v2')
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// ── delete ────────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
describe('flect proj delete', () => {
|
|
145
|
+
it('works with slug', async () => {
|
|
146
|
+
mocks.mockDelete(`${BASE}/api`)
|
|
147
|
+
const r = await run(['proj', 'delete', 'api', ...OPT])
|
|
148
|
+
expect(r.exitCode).toBe(0)
|
|
149
|
+
expect(r.stdout).toContain('api')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('works with shortId', async () => {
|
|
153
|
+
mocks.mockDelete(`${BASE}/pj9abc`)
|
|
154
|
+
const r = await run(['proj', 'delete', 'pj9abc', ...OPT])
|
|
155
|
+
expect(r.exitCode).toBe(0)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/// <reference types="vitest/globals" />
|
|
2
|
+
import { beforeAll, afterEach, afterAll, describe, it, expect } from 'vitest'
|
|
3
|
+
import { run, mocks, server } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
|
|
6
|
+
afterEach(() => server.resetHandlers())
|
|
7
|
+
afterAll(() => server.close())
|
|
8
|
+
|
|
9
|
+
// --org flag ile active org'u override ediyoruz (context boş olduğu için)
|
|
10
|
+
const ORG_SLUG = 'dotlabs'
|
|
11
|
+
const OPT = ['--org', ORG_SLUG]
|
|
12
|
+
|
|
13
|
+
const WS = {
|
|
14
|
+
id: '019f06aa-1111-7db6-956d-f323dfadba50',
|
|
15
|
+
shortId: 'ws1234',
|
|
16
|
+
slug: 'staging',
|
|
17
|
+
name: 'Staging',
|
|
18
|
+
isDefault: true,
|
|
19
|
+
createdAt: 1782507760630,
|
|
20
|
+
updatedAt: 1782507760630,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const WS2 = {
|
|
24
|
+
id: '019f06ab-2222-7db6-956d-f323dfadba50',
|
|
25
|
+
shortId: 'ws5678',
|
|
26
|
+
slug: 'production',
|
|
27
|
+
name: 'Production',
|
|
28
|
+
isDefault: false,
|
|
29
|
+
createdAt: 1782507760630,
|
|
30
|
+
updatedAt: 1782507760630,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── list ─────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe('flect ws (list)', () => {
|
|
36
|
+
it('default columns: ID → SLUG → NAME → DEFAULT', async () => {
|
|
37
|
+
mocks.mockGet(`/v1/orgs/${ORG_SLUG}/workspaces`, { workspaces: [WS, WS2] })
|
|
38
|
+
const r = await run(['ws', ...OPT])
|
|
39
|
+
expect(r.exitCode).toBe(0)
|
|
40
|
+
expect(r.stdout).toContain('ws1234')
|
|
41
|
+
expect(r.stdout).toContain('staging')
|
|
42
|
+
expect(r.stdout).toContain('Staging')
|
|
43
|
+
expect(r.stdout.indexOf('ID')).toBeLessThan(r.stdout.indexOf('SLUG'))
|
|
44
|
+
expect(r.stdout.indexOf('SLUG')).toBeLessThan(r.stdout.indexOf('NAME'))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('does not show full UUID in default view', async () => {
|
|
48
|
+
mocks.mockGet(`/v1/orgs/${ORG_SLUG}/workspaces`, { workspaces: [WS] })
|
|
49
|
+
const r = await run(['ws', ...OPT])
|
|
50
|
+
expect(r.stdout).not.toContain(WS.id)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('-o wide adds FULL ID column', async () => {
|
|
54
|
+
mocks.mockGet(`/v1/orgs/${ORG_SLUG}/workspaces`, { workspaces: [WS] })
|
|
55
|
+
const r = await run(['ws', '-o', 'wide', ...OPT])
|
|
56
|
+
expect(r.exitCode).toBe(0)
|
|
57
|
+
expect(r.stdout).toContain('FULL ID')
|
|
58
|
+
expect(r.stdout).toContain(WS.id)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('-o json returns raw JSON array', async () => {
|
|
62
|
+
mocks.mockGet(`/v1/orgs/${ORG_SLUG}/workspaces`, { workspaces: [WS] })
|
|
63
|
+
const r = await run(['ws', '-o', 'json', ...OPT])
|
|
64
|
+
const parsed = JSON.parse(r.stdout)
|
|
65
|
+
expect(Array.isArray(parsed)).toBe(true)
|
|
66
|
+
expect(parsed[0].shortId).toBe('ws1234')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// ── create ───────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe('flect ws create', () => {
|
|
73
|
+
it('sends only name when no slug given', async () => {
|
|
74
|
+
const captured = mocks.mockPostCapture(`/v1/orgs/${ORG_SLUG}/workspaces`, { workspace: WS })
|
|
75
|
+
const r = await run(['ws', 'create', '--name', 'Staging', ...OPT])
|
|
76
|
+
expect(r.exitCode).toBe(0)
|
|
77
|
+
const body = captured.body as Record<string, unknown>
|
|
78
|
+
expect(body.name).toBe('Staging')
|
|
79
|
+
expect(body.slug).toBeUndefined()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('sends explicit slug when provided', async () => {
|
|
83
|
+
const captured = mocks.mockPostCapture(`/v1/orgs/${ORG_SLUG}/workspaces`, { workspace: WS })
|
|
84
|
+
await run(['ws', 'create', '--name', 'Staging', '--slug', 'staging', ...OPT])
|
|
85
|
+
expect((captured.body as Record<string, unknown>).slug).toBe('staging')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('shows shortId in output, not full UUID', async () => {
|
|
89
|
+
mocks.mockPost(`/v1/orgs/${ORG_SLUG}/workspaces`, { workspace: WS })
|
|
90
|
+
const r = await run(['ws', 'create', '--name', 'Staging', ...OPT])
|
|
91
|
+
expect(r.stdout).toContain('ws1234')
|
|
92
|
+
expect(r.stdout).not.toContain(WS.id)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// ── get ───────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe('flect ws get', () => {
|
|
99
|
+
it('works with slug', async () => {
|
|
100
|
+
mocks.mockGet(`/v1/orgs/${ORG_SLUG}/workspaces/staging`, { workspace: WS })
|
|
101
|
+
const r = await run(['ws', 'get', 'staging', ...OPT])
|
|
102
|
+
expect(r.exitCode).toBe(0)
|
|
103
|
+
expect(r.stdout).toContain('Staging')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('works with shortId', async () => {
|
|
107
|
+
mocks.mockGet(`/v1/orgs/${ORG_SLUG}/workspaces/ws1234`, { workspace: WS })
|
|
108
|
+
const r = await run(['ws', 'get', 'ws1234', ...OPT])
|
|
109
|
+
expect(r.exitCode).toBe(0)
|
|
110
|
+
expect(r.stdout).toContain('Staging')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('id field shows shortId, full_id field shows UUID', async () => {
|
|
114
|
+
mocks.mockGet(`/v1/orgs/${ORG_SLUG}/workspaces/staging`, { workspace: WS })
|
|
115
|
+
const r = await run(['ws', 'get', 'staging', ...OPT])
|
|
116
|
+
const lines = r.stdout.split('\n')
|
|
117
|
+
const idLine = lines.find(l => /^\s+id\s/.test(l))
|
|
118
|
+
expect(idLine).toContain('ws1234')
|
|
119
|
+
expect(idLine).not.toContain(WS.id)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// ── update ────────────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
describe('flect ws update', () => {
|
|
126
|
+
it('works with slug', async () => {
|
|
127
|
+
const captured = mocks.mockPatchCapture(`/v1/orgs/${ORG_SLUG}/workspaces/staging`, { workspace: { ...WS, name: 'Staging Env' } })
|
|
128
|
+
const r = await run(['ws', 'update', 'staging', '--name', 'Staging Env', ...OPT])
|
|
129
|
+
expect(r.exitCode).toBe(0)
|
|
130
|
+
expect((captured.body as Record<string, unknown>).name).toBe('Staging Env')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('works with shortId', async () => {
|
|
134
|
+
const captured = mocks.mockPatchCapture(`/v1/orgs/${ORG_SLUG}/workspaces/ws1234`, { workspace: { ...WS, name: 'Staging Env' } })
|
|
135
|
+
const r = await run(['ws', 'update', 'ws1234', '--name', 'Staging Env', ...OPT])
|
|
136
|
+
expect(r.exitCode).toBe(0)
|
|
137
|
+
expect((captured.body as Record<string, unknown>).name).toBe('Staging Env')
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// ── delete ────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
describe('flect ws delete', () => {
|
|
144
|
+
it('works with slug', async () => {
|
|
145
|
+
mocks.mockDelete(`/v1/orgs/${ORG_SLUG}/workspaces/staging`)
|
|
146
|
+
const r = await run(['ws', 'delete', 'staging', ...OPT])
|
|
147
|
+
expect(r.exitCode).toBe(0)
|
|
148
|
+
expect(r.stdout).toContain('staging')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('works with shortId', async () => {
|
|
152
|
+
mocks.mockDelete(`/v1/orgs/${ORG_SLUG}/workspaces/ws1234`)
|
|
153
|
+
const r = await run(['ws', 'delete', 'ws1234', ...OPT])
|
|
154
|
+
expect(r.exitCode).toBe(0)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { buildOrgCommand, buildWsCommand, buildProjCommand, buildEnvCommand, addGlobalCommands } from '../cli.js'
|
|
3
|
+
import { createApiClient } from '@baseworks/cli/client'
|
|
4
|
+
import { createContextManager } from '@baseworks/cli/context'
|
|
5
|
+
import { runCommand, createMocks, server } from '@baseworks/cli/testing'
|
|
6
|
+
|
|
7
|
+
export { runCommand, server }
|
|
8
|
+
|
|
9
|
+
export const TEST_BASE = 'http://flect.test'
|
|
10
|
+
|
|
11
|
+
export const mocks = createMocks(TEST_BASE)
|
|
12
|
+
|
|
13
|
+
export function makeProgram(token = 'test-token') {
|
|
14
|
+
const ctx = createContextManager('org-test')
|
|
15
|
+
const http = createApiClient(() => token, () => TEST_BASE)
|
|
16
|
+
const deps = { http, ctx, appBase: TEST_BASE, cliName: 'flect' }
|
|
17
|
+
|
|
18
|
+
const program = new Command('flect').exitOverride().enablePositionalOptions()
|
|
19
|
+
addGlobalCommands(program, deps)
|
|
20
|
+
program.addCommand(buildOrgCommand(deps))
|
|
21
|
+
program.addCommand(buildWsCommand(deps))
|
|
22
|
+
program.addCommand(buildProjCommand(deps))
|
|
23
|
+
program.addCommand(buildEnvCommand(deps))
|
|
24
|
+
return program
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function run(args: string[]) {
|
|
28
|
+
return runCommand(makeProgram(), args)
|
|
29
|
+
}
|