@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.
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }