@sqldoc/sqldoc 0.0.1
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/package.json +37 -0
- package/src/__tests__/detect-pm.test.ts +67 -0
- package/src/__tests__/find-sqldoc.test.ts +60 -0
- package/src/__tests__/generate-config-types.test.ts +121 -0
- package/src/__tests__/init.test.ts +117 -0
- package/src/__tests__/prompt.test.ts +164 -0
- package/src/__tests__/scaffold.test.ts +115 -0
- package/src/commands/add.ts +48 -0
- package/src/commands/init.ts +210 -0
- package/src/commands/upgrade.ts +45 -0
- package/src/delegate.ts +44 -0
- package/src/detect-pm.ts +17 -0
- package/src/find-sqldoc.ts +22 -0
- package/src/generate-config-types.ts +77 -0
- package/src/index.ts +127 -0
- package/src/prompt.ts +92 -0
- package/src/runtime.ts +38 -0
- package/src/scaffold.ts +231 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import pc from 'picocolors'
|
|
4
|
+
import { detectPM } from '../detect-pm.ts'
|
|
5
|
+
import { generateConfigTypes } from '../generate-config-types.ts'
|
|
6
|
+
|
|
7
|
+
function isCompiledBinary(): boolean {
|
|
8
|
+
return typeof process.versions?.bun === 'string' && !process.execPath.match(/\/(bun|node)(\.exe)?$/)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Install packages into .sqldoc/node_modules.
|
|
13
|
+
*/
|
|
14
|
+
export function addCommand(sqldocDir: string, packages: string[]): void {
|
|
15
|
+
if (packages.length === 0) {
|
|
16
|
+
console.error(pc.red('Error: No packages specified'))
|
|
17
|
+
console.error(`Usage: ${pc.cyan('sqldoc add <package> [package...]')}`)
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const installArgs = isCompiledBinary()
|
|
22
|
+
? [process.execPath, 'install', ...packages]
|
|
23
|
+
: (() => {
|
|
24
|
+
const projectRoot = dirname(sqldocDir)
|
|
25
|
+
const pm = detectPM(projectRoot)
|
|
26
|
+
console.log(pc.dim(`Installing ${packages.join(', ')} with ${pm}...`))
|
|
27
|
+
return pm === 'yarn' ? ['yarn', 'add', ...packages] : [pm, 'install', ...packages]
|
|
28
|
+
})()
|
|
29
|
+
|
|
30
|
+
const env = isCompiledBinary() ? { ...process.env, BUN_BE_BUN: '1' } : process.env
|
|
31
|
+
|
|
32
|
+
if (isCompiledBinary()) {
|
|
33
|
+
console.log(pc.dim(`Installing ${packages.join(', ')} with built-in package manager...`))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = spawnSync(installArgs[0], installArgs.slice(1), {
|
|
37
|
+
cwd: sqldocDir,
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
env,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (result.status === 0) {
|
|
43
|
+
generateConfigTypes(sqldocDir)
|
|
44
|
+
console.log(pc.dim('Updated .sqldoc/config.d.ts'))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.exit(result.status ?? 1)
|
|
48
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { join, relative, resolve } from 'node:path'
|
|
4
|
+
import pc from 'picocolors'
|
|
5
|
+
import { detectPM } from '../detect-pm.ts'
|
|
6
|
+
import { generateConfigTypes } from '../generate-config-types.ts'
|
|
7
|
+
|
|
8
|
+
function isCompiledBinary(): boolean {
|
|
9
|
+
return typeof process.versions?.bun === 'string' && !process.execPath.match(/\/(bun|node)(\.exe)?$/)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function findLocalPackages(repoPath: string): Array<{ name: string; path: string }> {
|
|
13
|
+
const packagesDir = join(repoPath, 'packages')
|
|
14
|
+
if (!existsSync(packagesDir)) return []
|
|
15
|
+
|
|
16
|
+
const packages: Array<{ name: string; path: string }> = []
|
|
17
|
+
for (const dir of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
18
|
+
if (!dir.isDirectory()) continue
|
|
19
|
+
const pkgPath = join(packagesDir, dir.name, 'package.json')
|
|
20
|
+
if (!existsSync(pkgPath)) continue
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
23
|
+
if (pkg.name?.startsWith('@sqldoc/')) {
|
|
24
|
+
packages.push({ name: pkg.name, path: join(packagesDir, dir.name) })
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
return packages
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function installPackages(sqldocDir: string, targetDir: string, packages: string[]): boolean {
|
|
32
|
+
const installArgs = isCompiledBinary()
|
|
33
|
+
? [process.execPath, 'install', ...packages]
|
|
34
|
+
: (() => {
|
|
35
|
+
const pm = detectPM(targetDir)
|
|
36
|
+
console.log(pc.dim(`Using ${pm} to install ${packages.join(', ')}...`))
|
|
37
|
+
return pm === 'yarn' ? ['yarn', 'add', ...packages] : [pm, 'install', ...packages]
|
|
38
|
+
})()
|
|
39
|
+
|
|
40
|
+
const env = isCompiledBinary() ? { ...process.env, BUN_BE_BUN: '1' } : process.env
|
|
41
|
+
|
|
42
|
+
if (isCompiledBinary()) {
|
|
43
|
+
console.log(pc.dim('Using built-in package manager...'))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = spawnSync(installArgs[0], installArgs.slice(1), {
|
|
47
|
+
cwd: sqldocDir,
|
|
48
|
+
stdio: 'inherit',
|
|
49
|
+
env,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return result.status === 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULT_CONFIG = `import type { Config } from './.sqldoc/config'
|
|
56
|
+
|
|
57
|
+
export default {
|
|
58
|
+
dialect: 'postgres',
|
|
59
|
+
schema: 'schema/',
|
|
60
|
+
migrations: {
|
|
61
|
+
dir: 'migrations/',
|
|
62
|
+
format: 'plain',
|
|
63
|
+
},
|
|
64
|
+
namespaces: {
|
|
65
|
+
docs: {
|
|
66
|
+
format: 'html',
|
|
67
|
+
output: 'docs/schema.html',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
} satisfies Config
|
|
71
|
+
`
|
|
72
|
+
|
|
73
|
+
const EXAMPLE_SCHEMA = `-- @import '@sqldoc/ns-docs'
|
|
74
|
+
|
|
75
|
+
-- @docs.description('Application users')
|
|
76
|
+
CREATE TABLE users (
|
|
77
|
+
id BIGSERIAL PRIMARY KEY,
|
|
78
|
+
|
|
79
|
+
-- @docs.description('Login email address')
|
|
80
|
+
email TEXT NOT NULL UNIQUE,
|
|
81
|
+
|
|
82
|
+
-- @docs.description('Display name')
|
|
83
|
+
name TEXT,
|
|
84
|
+
|
|
85
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
86
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
-- @docs.description('User-created posts')
|
|
90
|
+
CREATE TABLE posts (
|
|
91
|
+
id BIGSERIAL PRIMARY KEY,
|
|
92
|
+
|
|
93
|
+
-- @docs.description('Author of the post')
|
|
94
|
+
user_id BIGINT NOT NULL REFERENCES users(id),
|
|
95
|
+
|
|
96
|
+
title TEXT NOT NULL,
|
|
97
|
+
body TEXT,
|
|
98
|
+
published BOOLEAN NOT NULL DEFAULT false,
|
|
99
|
+
|
|
100
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
101
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
102
|
+
);
|
|
103
|
+
`
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Initialize a .sqldoc/ project directory.
|
|
107
|
+
*
|
|
108
|
+
* --dev <path>: link all @sqldoc/* packages from a local monorepo.
|
|
109
|
+
* Otherwise: installs @sqldoc/cli and @sqldoc/ns-docs from npm.
|
|
110
|
+
*/
|
|
111
|
+
export async function initCommand(targetDir: string = process.cwd(), devPath?: string): Promise<void> {
|
|
112
|
+
const sqldocDir = join(targetDir, '.sqldoc')
|
|
113
|
+
|
|
114
|
+
if (existsSync(sqldocDir)) {
|
|
115
|
+
console.error(pc.red('Error: .sqldoc/ already exists in this directory'))
|
|
116
|
+
process.exit(1)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(pc.cyan('Initializing .sqldoc/ project...'))
|
|
120
|
+
console.log('')
|
|
121
|
+
|
|
122
|
+
mkdirSync(sqldocDir, { recursive: true })
|
|
123
|
+
writeFileSync(join(sqldocDir, '.gitignore'), 'node_modules/\n')
|
|
124
|
+
|
|
125
|
+
if (devPath) {
|
|
126
|
+
// Dev mode — link all @sqldoc/* packages from local repo
|
|
127
|
+
const repoPath = resolve(devPath)
|
|
128
|
+
if (!existsSync(repoPath)) {
|
|
129
|
+
console.error(pc.red(`Error: ${repoPath} does not exist`))
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const localPackages = findLocalPackages(repoPath)
|
|
134
|
+
if (localPackages.length === 0) {
|
|
135
|
+
console.error(pc.red(`No @sqldoc/* packages found in ${repoPath}/packages/`))
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const deps: Record<string, string> = {}
|
|
140
|
+
for (const pkg of localPackages) {
|
|
141
|
+
const relPath = relative(sqldocDir, pkg.path)
|
|
142
|
+
deps[pkg.name] = `file:${relPath}`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
writeFileSync(
|
|
146
|
+
join(sqldocDir, 'package.json'),
|
|
147
|
+
`${JSON.stringify({ name: 'sqldoc-local', private: true, workspaces: [], dependencies: deps }, null, 2)}\n`,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const nmDir = join(sqldocDir, 'node_modules', '@sqldoc')
|
|
151
|
+
mkdirSync(nmDir, { recursive: true })
|
|
152
|
+
for (const pkg of localPackages) {
|
|
153
|
+
const linkName = pkg.name.replace('@sqldoc/', '')
|
|
154
|
+
const linkPath = join(nmDir, linkName)
|
|
155
|
+
try {
|
|
156
|
+
symlinkSync(pkg.path, linkPath)
|
|
157
|
+
} catch (e: any) {
|
|
158
|
+
if (e.code !== 'EEXIST') throw e
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(pc.dim(`Linked ${localPackages.length} packages from ${repoPath}`))
|
|
163
|
+
} else {
|
|
164
|
+
// Production mode — install @sqldoc/cli and ns-docs
|
|
165
|
+
writeFileSync(
|
|
166
|
+
join(sqldocDir, 'package.json'),
|
|
167
|
+
`${JSON.stringify({ name: 'sqldoc-local', private: true, workspaces: [] }, null, 2)}\n`,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if (!installPackages(sqldocDir, targetDir, ['@sqldoc/cli', '@sqldoc/ns-docs'])) {
|
|
171
|
+
console.error(pc.red('Failed to install packages'))
|
|
172
|
+
process.exit(1)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Generate config types
|
|
177
|
+
generateConfigTypes(sqldocDir)
|
|
178
|
+
|
|
179
|
+
// Scaffold sqldoc.config.ts
|
|
180
|
+
const configPath = join(targetDir, 'sqldoc.config.ts')
|
|
181
|
+
if (!existsSync(configPath)) {
|
|
182
|
+
writeFileSync(configPath, DEFAULT_CONFIG)
|
|
183
|
+
console.log(` ${pc.green('+')} ${pc.bold('sqldoc.config.ts')}`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Scaffold example schema if schema/ doesn't exist
|
|
187
|
+
const schemaDir = join(targetDir, 'schema')
|
|
188
|
+
if (!existsSync(schemaDir)) {
|
|
189
|
+
mkdirSync(schemaDir, { recursive: true })
|
|
190
|
+
writeFileSync(join(schemaDir, 'schema.sql'), EXAMPLE_SCHEMA)
|
|
191
|
+
console.log(` ${pc.green('+')} ${pc.bold('schema/schema.sql')} ${pc.dim('(example schema with two tables)')}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log('')
|
|
195
|
+
console.log(pc.green('Project initialized!'))
|
|
196
|
+
console.log('')
|
|
197
|
+
console.log(`Edit ${pc.bold('sqldoc.config.ts')} to configure your project.`)
|
|
198
|
+
console.log(`We've created an example schema in ${pc.bold('schema/')} to get you started.`)
|
|
199
|
+
console.log('')
|
|
200
|
+
console.log('Try these commands:')
|
|
201
|
+
console.log(` ${pc.cyan('sqldoc codegen')} Generate HTML docs from your schema`)
|
|
202
|
+
console.log(` ${pc.cyan('sqldoc schema inspect')} View the parsed schema`)
|
|
203
|
+
console.log(` ${pc.cyan('sqldoc migrate')} Generate a migration file`)
|
|
204
|
+
console.log('')
|
|
205
|
+
console.log('Install more plugins:')
|
|
206
|
+
console.log(` ${pc.cyan('sqldoc add @sqldoc/ns-audit')} Audit trail triggers`)
|
|
207
|
+
console.log(` ${pc.cyan('sqldoc add @sqldoc/ns-validate')} CHECK constraints from tags`)
|
|
208
|
+
console.log(` ${pc.cyan('sqldoc add @sqldoc/ns-rls')} Row-level security policies`)
|
|
209
|
+
console.log(` ${pc.cyan('sqldoc add @sqldoc/templates')} Code generation (TypeScript, Go, etc.)`)
|
|
210
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import pc from 'picocolors'
|
|
4
|
+
import { detectPM } from '../detect-pm.ts'
|
|
5
|
+
|
|
6
|
+
function isCompiledBinary(): boolean {
|
|
7
|
+
return typeof process.versions?.bun === 'string' && !process.execPath.match(/\/(bun|node)(\.exe)?$/)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Upgrade all packages in .sqldoc/node_modules.
|
|
12
|
+
*/
|
|
13
|
+
export function upgradeCommand(sqldocDir: string): void {
|
|
14
|
+
const upgradeArgs = isCompiledBinary()
|
|
15
|
+
? [process.execPath, 'update']
|
|
16
|
+
: (() => {
|
|
17
|
+
const projectRoot = dirname(sqldocDir)
|
|
18
|
+
const pm = detectPM(projectRoot)
|
|
19
|
+
console.log(pc.dim(`Upgrading packages with ${pm}...`))
|
|
20
|
+
switch (pm) {
|
|
21
|
+
case 'pnpm':
|
|
22
|
+
return ['pnpm', 'update']
|
|
23
|
+
case 'yarn':
|
|
24
|
+
return ['yarn', 'upgrade']
|
|
25
|
+
case 'bun':
|
|
26
|
+
return ['bun', 'update']
|
|
27
|
+
default:
|
|
28
|
+
return ['npm', 'update']
|
|
29
|
+
}
|
|
30
|
+
})()
|
|
31
|
+
|
|
32
|
+
const env = isCompiledBinary() ? { ...process.env, BUN_BE_BUN: '1' } : process.env
|
|
33
|
+
|
|
34
|
+
if (isCompiledBinary()) {
|
|
35
|
+
console.log(pc.dim('Upgrading packages with built-in package manager...'))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const result = spawnSync(upgradeArgs[0], upgradeArgs.slice(1), {
|
|
39
|
+
cwd: sqldocDir,
|
|
40
|
+
stdio: 'inherit',
|
|
41
|
+
env,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
process.exit(result.status ?? 1)
|
|
45
|
+
}
|
package/src/delegate.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import pc from 'picocolors'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Delegate command execution to the project-local @sqldoc/cli.
|
|
8
|
+
* Spawns the local CLI with inherited stdio and SQLDOC_PROJECT_ROOT env var.
|
|
9
|
+
*/
|
|
10
|
+
export function delegate(sqldocDir: string, args: string[]): void {
|
|
11
|
+
// Try src/index.ts first (Bun runtime can execute .ts directly)
|
|
12
|
+
let localCli = join(sqldocDir, 'node_modules', '@sqldoc', 'cli', 'src', 'index.ts')
|
|
13
|
+
if (!existsSync(localCli)) {
|
|
14
|
+
// Fallback to dist/index.js (for pre-built npm packages)
|
|
15
|
+
localCli = join(sqldocDir, 'node_modules', '@sqldoc', 'cli', 'dist', 'index.js')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!existsSync(localCli)) {
|
|
19
|
+
console.error(pc.red('Error: @sqldoc/cli not found in .sqldoc/node_modules'))
|
|
20
|
+
console.error(`Run: ${pc.cyan('sqldoc init')}`)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const projectRoot = dirname(sqldocDir)
|
|
25
|
+
const child = spawn(process.execPath, [localCli, ...args], {
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
env: {
|
|
29
|
+
...process.env,
|
|
30
|
+
SQLDOC_PROJECT_ROOT: projectRoot,
|
|
31
|
+
BUN_BE_BUN: '1',
|
|
32
|
+
NODE_PATH: join(sqldocDir, 'node_modules'),
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
child.on('exit', (code) => {
|
|
37
|
+
process.exit(code ?? 1)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
child.on('error', (err) => {
|
|
41
|
+
console.error(pc.red(`Failed to start @sqldoc/cli: ${err.message}`))
|
|
42
|
+
process.exit(1)
|
|
43
|
+
})
|
|
44
|
+
}
|
package/src/detect-pm.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect the package manager used in a project by checking lockfiles.
|
|
8
|
+
* Priority: pnpm > yarn > bun > npm (fallback).
|
|
9
|
+
*/
|
|
10
|
+
export function detectPM(projectRoot: string): PackageManager {
|
|
11
|
+
if (existsSync(join(projectRoot, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
12
|
+
if (existsSync(join(projectRoot, 'yarn.lock'))) return 'yarn'
|
|
13
|
+
if (existsSync(join(projectRoot, 'bun.lockb'))) return 'bun'
|
|
14
|
+
if (existsSync(join(projectRoot, 'bun.lock'))) return 'bun'
|
|
15
|
+
if (existsSync(join(projectRoot, 'package-lock.json'))) return 'npm'
|
|
16
|
+
return 'npm'
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walk up from startDir looking for .sqldoc/ directory.
|
|
6
|
+
* Returns the absolute path to .sqldoc/ or null.
|
|
7
|
+
*/
|
|
8
|
+
export function findSqldocDir(startDir: string = process.cwd()): string | null {
|
|
9
|
+
let current = resolve(startDir)
|
|
10
|
+
while (true) {
|
|
11
|
+
const candidate = join(current, '.sqldoc')
|
|
12
|
+
if (existsSync(candidate)) {
|
|
13
|
+
return candidate
|
|
14
|
+
}
|
|
15
|
+
const parent = dirname(current)
|
|
16
|
+
if (parent === current) {
|
|
17
|
+
// Reached filesystem root
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
current = parent
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scan .sqldoc/node_modules for installed @sqldoc namespace plugins,
|
|
6
|
+
* and generate .sqldoc/config.d.ts with a typed SqldocConfig interface.
|
|
7
|
+
*
|
|
8
|
+
* Base config fields (devUrl, dialect, extensions, etc.) are imported from
|
|
9
|
+
* @sqldoc/core so they stay in sync. Only the namespaces block is generated.
|
|
10
|
+
*/
|
|
11
|
+
export function generateConfigTypes(sqldocDir: string): void {
|
|
12
|
+
const nodeModules = path.join(sqldocDir, 'node_modules', '@sqldoc')
|
|
13
|
+
if (!fs.existsSync(nodeModules)) return
|
|
14
|
+
|
|
15
|
+
const imports: string[] = ["import type { ProjectConfig as BaseProjectConfig } from '@sqldoc/core'"]
|
|
16
|
+
const namespaceFields: string[] = []
|
|
17
|
+
|
|
18
|
+
const dirs = fs.readdirSync(nodeModules).sort()
|
|
19
|
+
for (const dir of dirs) {
|
|
20
|
+
if (!dir.startsWith('ns-')) continue
|
|
21
|
+
|
|
22
|
+
const pkgPath = path.join(nodeModules, dir)
|
|
23
|
+
const pkgJsonPath = path.join(pkgPath, 'package.json')
|
|
24
|
+
if (!fs.existsSync(pkgJsonPath)) continue
|
|
25
|
+
|
|
26
|
+
const nsName = dir.replace('ns-', '')
|
|
27
|
+
const pkgName = `@sqldoc/${dir}`
|
|
28
|
+
|
|
29
|
+
const configTypeName = findConfigTypeName(pkgPath, nsName)
|
|
30
|
+
|
|
31
|
+
if (configTypeName) {
|
|
32
|
+
imports.push(`import type { ${configTypeName} } from '${pkgName}'`)
|
|
33
|
+
namespaceFields.push(` ${nsName}?: Partial<${configTypeName}>`)
|
|
34
|
+
} else {
|
|
35
|
+
namespaceFields.push(` ${nsName}?: Record<string, unknown>`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const namespacesBlock =
|
|
40
|
+
namespaceFields.length > 0
|
|
41
|
+
? ` namespaces?: {
|
|
42
|
+
${namespaceFields.join('\n')}
|
|
43
|
+
}`
|
|
44
|
+
: ` namespaces?: Record<string, Record<string, unknown>>`
|
|
45
|
+
|
|
46
|
+
const content = `// Auto-generated by sqldoc -- DO NOT EDIT
|
|
47
|
+
// Regenerated on: sqldoc init, sqldoc add, sqldoc codegen
|
|
48
|
+
${imports.join('\n')}
|
|
49
|
+
|
|
50
|
+
interface ProjectConfig extends BaseProjectConfig {
|
|
51
|
+
${namespacesBlock}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type SqldocConfig = ProjectConfig | ProjectConfig[]
|
|
55
|
+
export type Config = SqldocConfig
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
fs.writeFileSync(path.join(sqldocDir, 'config.d.ts'), content)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Look for an exported Config type in a namespace package.
|
|
63
|
+
* Checks for PascalCase(nsName)Config (e.g. DocsConfig, CodegenConfig).
|
|
64
|
+
*/
|
|
65
|
+
function findConfigTypeName(pkgPath: string, _nsName: string): string | null {
|
|
66
|
+
const candidates = ['src/index.ts', 'index.ts', 'src/index.js', 'index.js']
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
const filePath = path.join(pkgPath, candidate)
|
|
69
|
+
if (!fs.existsSync(filePath)) continue
|
|
70
|
+
|
|
71
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
72
|
+
const match = content.match(/export\s+(?:type\s+)?{\s*[^}]*\b(\w+Config)\b/)
|
|
73
|
+
if (match) return match[1]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null
|
|
77
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import * as fs from 'node:fs'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import pc from 'picocolors'
|
|
5
|
+
import { addCommand } from './commands/add.ts'
|
|
6
|
+
import { initCommand } from './commands/init.ts'
|
|
7
|
+
import { upgradeCommand } from './commands/upgrade.ts'
|
|
8
|
+
import { delegate } from './delegate.ts'
|
|
9
|
+
import { findSqldocDir } from './find-sqldoc.ts'
|
|
10
|
+
import { getPackageManagerCommand } from './runtime.ts'
|
|
11
|
+
|
|
12
|
+
// Version is set at build time — the compiled binary can't read package.json
|
|
13
|
+
const VERSION = '0.0.1'
|
|
14
|
+
|
|
15
|
+
const HELP = `
|
|
16
|
+
${pc.bold('sqldoc')} - SQL documentation and code generation
|
|
17
|
+
|
|
18
|
+
${pc.dim('Usage:')}
|
|
19
|
+
sqldoc <command> [options]
|
|
20
|
+
|
|
21
|
+
${pc.dim('Built-in commands:')}
|
|
22
|
+
init Initialize .sqldoc/ in the current directory
|
|
23
|
+
add <packages...> Install packages into .sqldoc/node_modules
|
|
24
|
+
upgrade Update all packages in .sqldoc/
|
|
25
|
+
|
|
26
|
+
${pc.dim('Options:')}
|
|
27
|
+
--version, -V Show version
|
|
28
|
+
--help, -h Show this help
|
|
29
|
+
|
|
30
|
+
All other commands are delegated to the project-local @sqldoc/cli
|
|
31
|
+
installed in .sqldoc/node_modules/@sqldoc/cli.
|
|
32
|
+
`.trim()
|
|
33
|
+
|
|
34
|
+
/** Run install in .sqldoc/ to ensure node_modules is up to date */
|
|
35
|
+
function ensureDeps(sqldocDir: string): void {
|
|
36
|
+
const nodeModules = path.join(sqldocDir, 'node_modules')
|
|
37
|
+
const pkgJson = path.join(sqldocDir, 'package.json')
|
|
38
|
+
|
|
39
|
+
// Skip if no package.json
|
|
40
|
+
if (!fs.existsSync(pkgJson)) return
|
|
41
|
+
|
|
42
|
+
// Dev mode uses symlinks — no install needed
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf-8'))
|
|
45
|
+
if (pkg.description?.includes('dev mode')) return
|
|
46
|
+
} catch {}
|
|
47
|
+
|
|
48
|
+
// Quick check: if node_modules exists and is newer than package.json, skip
|
|
49
|
+
if (fs.existsSync(nodeModules)) {
|
|
50
|
+
const pkgMtime = fs.statSync(pkgJson).mtimeMs
|
|
51
|
+
const nmMtime = fs.statSync(nodeModules).mtimeMs
|
|
52
|
+
if (nmMtime > pkgMtime) return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const pm = getPackageManagerCommand(sqldocDir)
|
|
56
|
+
try {
|
|
57
|
+
execSync(`${pm.cmd} install`, {
|
|
58
|
+
cwd: sqldocDir,
|
|
59
|
+
stdio: 'inherit',
|
|
60
|
+
env: { ...process.env, ...pm.env },
|
|
61
|
+
})
|
|
62
|
+
} catch {
|
|
63
|
+
console.error(pc.yellow('Warning: failed to sync .sqldoc/ dependencies'))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function requireSqldocDir(): string {
|
|
68
|
+
const sqldocDir = findSqldocDir()
|
|
69
|
+
if (!sqldocDir) {
|
|
70
|
+
console.error(pc.red('Error: No .sqldoc/ directory found'))
|
|
71
|
+
console.error(`Run ${pc.cyan('sqldoc init')} to create one.`)
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
74
|
+
return sqldocDir
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function main(): Promise<void> {
|
|
78
|
+
const args = process.argv.slice(2)
|
|
79
|
+
const command = args[0]
|
|
80
|
+
|
|
81
|
+
// Handle flags
|
|
82
|
+
if (command === '--version' || command === '-V') {
|
|
83
|
+
console.log(VERSION)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (command === '--help' || command === '-h') {
|
|
88
|
+
console.log(HELP)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle built-in commands
|
|
93
|
+
if (command === 'init') {
|
|
94
|
+
const devIdx = args.indexOf('--dev')
|
|
95
|
+
const devPath = devIdx !== -1 ? args[devIdx + 1] : undefined
|
|
96
|
+
await initCommand(process.cwd(), devPath)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (command === 'add') {
|
|
101
|
+
const sqldocDir = requireSqldocDir()
|
|
102
|
+
addCommand(sqldocDir, args.slice(1))
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (command === 'upgrade') {
|
|
107
|
+
const sqldocDir = requireSqldocDir()
|
|
108
|
+
upgradeCommand(sqldocDir)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// No command given → show help
|
|
113
|
+
if (!command) {
|
|
114
|
+
console.log(HELP)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Default: ensure dependencies are installed, then delegate
|
|
119
|
+
const sqldocDir = requireSqldocDir()
|
|
120
|
+
ensureDeps(sqldocDir)
|
|
121
|
+
delegate(sqldocDir, args)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main().catch((err) => {
|
|
125
|
+
console.error(pc.red(err.message))
|
|
126
|
+
process.exit(1)
|
|
127
|
+
})
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as readline from 'node:readline'
|
|
2
|
+
import pc from 'picocolors'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Low-level: ask a question, return the raw answer.
|
|
6
|
+
*/
|
|
7
|
+
function ask(rl: readline.Interface, question: string): Promise<string> {
|
|
8
|
+
return new Promise((resolve) => rl.question(question, resolve))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a readline interface on stdin/stdout.
|
|
13
|
+
*/
|
|
14
|
+
export function createRL(): readline.Interface {
|
|
15
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Prompt the user to select one option from a list.
|
|
20
|
+
* Shows numbered options; user types a number or presses Enter for default.
|
|
21
|
+
*/
|
|
22
|
+
export async function promptSelect<T extends string>(
|
|
23
|
+
rl: readline.Interface,
|
|
24
|
+
message: string,
|
|
25
|
+
options: Array<{ value: T; label: string }>,
|
|
26
|
+
defaultValue: T,
|
|
27
|
+
): Promise<T> {
|
|
28
|
+
console.log('')
|
|
29
|
+
console.log(pc.bold(message))
|
|
30
|
+
for (let i = 0; i < options.length; i++) {
|
|
31
|
+
const isDefault = options[i].value === defaultValue
|
|
32
|
+
const marker = isDefault ? pc.cyan('*') : ' '
|
|
33
|
+
console.log(` ${marker} ${i + 1}) ${options[i].label}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultIdx = options.findIndex((o) => o.value === defaultValue)
|
|
37
|
+
const answer = await ask(rl, `${pc.dim(`[${defaultIdx + 1}]`)} > `)
|
|
38
|
+
|
|
39
|
+
if (answer.trim() === '') return defaultValue
|
|
40
|
+
const idx = parseInt(answer.trim(), 10) - 1
|
|
41
|
+
if (idx >= 0 && idx < options.length) return options[idx].value
|
|
42
|
+
return defaultValue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Prompt the user to select multiple options from a list.
|
|
47
|
+
* Shows numbered options; user types comma-separated numbers.
|
|
48
|
+
* Enter with no input selects the defaults.
|
|
49
|
+
*/
|
|
50
|
+
export async function promptCheckbox<T extends string>(
|
|
51
|
+
rl: readline.Interface,
|
|
52
|
+
message: string,
|
|
53
|
+
options: Array<{ value: T; label: string; checked?: boolean }>,
|
|
54
|
+
): Promise<T[]> {
|
|
55
|
+
console.log('')
|
|
56
|
+
console.log(pc.bold(message))
|
|
57
|
+
console.log(pc.dim(' (enter comma-separated numbers, or press Enter for defaults)'))
|
|
58
|
+
for (let i = 0; i < options.length; i++) {
|
|
59
|
+
const marker = options[i].checked ? pc.green('[x]') : pc.dim('[ ]')
|
|
60
|
+
console.log(` ${marker} ${i + 1}) ${options[i].label}`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const defaults = options.filter((o) => o.checked).map((o) => o.value)
|
|
64
|
+
const answer = await ask(
|
|
65
|
+
rl,
|
|
66
|
+
`${pc.dim(
|
|
67
|
+
`[${options
|
|
68
|
+
.map((o, i) => (o.checked ? i + 1 : ''))
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join(',')}]`,
|
|
71
|
+
)} > `,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if (answer.trim() === '') return defaults
|
|
75
|
+
|
|
76
|
+
const indices = answer.split(',').map((s) => parseInt(s.trim(), 10) - 1)
|
|
77
|
+
const selected: T[] = []
|
|
78
|
+
for (const idx of indices) {
|
|
79
|
+
if (idx >= 0 && idx < options.length) {
|
|
80
|
+
selected.push(options[idx].value)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return selected.length > 0 ? selected : defaults
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Simple yes/no confirmation prompt. Enter = yes.
|
|
88
|
+
*/
|
|
89
|
+
export async function promptConfirm(rl: readline.Interface, message: string): Promise<boolean> {
|
|
90
|
+
const answer = await ask(rl, `${message} ${pc.dim('(Y/n)')} `)
|
|
91
|
+
return answer.trim() === '' || answer.trim().toLowerCase() === 'y'
|
|
92
|
+
}
|