Skip to content

Commit cd01495

Browse files
committed
feat(cli): expand init guidance
- add schema command and schema-builder tooling - rerun generate after init and improve prompts - ignore TypeScript build info artifacts
1 parent 61b9772 commit cd01495

10 files changed

Lines changed: 3533 additions & 83 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ generated
44
.idea
55
dist
66
packages/pg-sourcerer/coverage
7+
*.tsbuildinfo
78

89
# Local/temp files
910
.worktrees/

packages/pg-sourcerer/src/cli.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Console, Effect, Option } from "effect";
77
import { runGenerate } from "./generate.js";
88
import { runInit } from "./init.js";
99
import { ConfigWithFallback, FileConfigProvider } from "./services/config.js";
10+
import { schemaCommand } from "./commands/schema.js";
1011
import packageJson from "../package.json" with { type: "json" };
1112

1213
const configPath = Options.file("config").pipe(
@@ -54,14 +55,25 @@ const runGenerateCommand = (args: GenerateArgs) => {
5455
FileConfigProvider(configOpts),
5556
);
5657

57-
return runGenerate(opts).pipe(
58+
const runGenerateOnce = runGenerate(opts).pipe(
5859
Effect.provide(configLayer),
5960
Effect.tap(logSuccess),
61+
);
62+
63+
return runGenerateOnce.pipe(
6064
Effect.catchTags({
6165
ConfigNotFound: () =>
6266
Console.log("No config file found. Running init...").pipe(
6367
Effect.zipRight(runInit),
64-
Effect.tap(() => Console.log("\nRun 'pgsourcerer' again to generate code.")),
68+
Effect.zipRight(
69+
runGenerateOnce.pipe(
70+
Effect.catchTags({
71+
ConfigNotFound: () => Console.error("No config file found after init."),
72+
ConfigInvalid: error =>
73+
Effect.forEach(error.errors, e => Console.error(` - ${e}`)),
74+
}),
75+
),
76+
),
6577
),
6678
ConfigInvalid: error =>
6779
Effect.forEach(error.errors, e => Console.error(` - ${e}`)),
@@ -85,7 +97,7 @@ const rootCommand = Command.make(
8597
"pgsourcerer",
8698
{ configPath, outputDir, dryRun },
8799
runGenerateCommand,
88-
).pipe(Command.withSubcommands([generateCommand, initCommand]));
100+
).pipe(Command.withSubcommands([generateCommand, initCommand, schemaCommand]));
89101

90102
const cli = Command.run(rootCommand, {
91103
name: "pgsourcerer",
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* Schema CLI Commands
3+
*
4+
* Commands for interactive schema/DDL management.
5+
*/
6+
import { Command, Options } from "@effect/cli";
7+
import { Console, Effect, Option } from "effect";
8+
import * as fs from "node:fs";
9+
import * as path from "node:path";
10+
import pg from "pg";
11+
import { runSchemaBuilder } from "../schema-builder/index.js";
12+
import { ConfigService, FileConfigProvider, ConfigWithFallback } from "../services/config.js";
13+
import { runInit } from "../init.js";
14+
15+
// =============================================================================
16+
// Options
17+
// =============================================================================
18+
19+
const outputPath = Options.file("output").pipe(
20+
Options.withAlias("o"),
21+
Options.withDescription("Output file path for generated DDL"),
22+
Options.optional,
23+
);
24+
25+
const schemaName = Options.text("schema").pipe(
26+
Options.withAlias("s"),
27+
Options.withDescription("Default schema name"),
28+
Options.withDefault("public"),
29+
);
30+
31+
const configPath = Options.file("config").pipe(
32+
Options.withAlias("c"),
33+
Options.withDescription("Path to config file"),
34+
Options.optional,
35+
);
36+
37+
// =============================================================================
38+
// schema new - Create new table interactively
39+
// =============================================================================
40+
41+
interface SchemaNewArgs {
42+
readonly outputPath: Option.Option<string>;
43+
readonly schemaName: string;
44+
readonly configPath: Option.Option<string>;
45+
}
46+
47+
const runSchemaNew = (args: SchemaNewArgs) => {
48+
const runNew = Effect.gen(function* () {
49+
yield* Console.log("Loading configuration...");
50+
51+
const config = yield* ConfigService;
52+
const pool = new pg.Pool({ connectionString: config.connectionString });
53+
54+
try {
55+
yield* Console.log("Starting Schema Builder...\n");
56+
57+
// Run the TUI (this is async, not Effect-based)
58+
const result = yield* Effect.tryPromise({
59+
try: () =>
60+
runSchemaBuilder({
61+
defaultSchema: args.schemaName,
62+
db: pool,
63+
}),
64+
catch: (error) => new Error(`Schema builder failed: ${error}`),
65+
});
66+
67+
// Small delay to let terminal settle after alternate screen exit
68+
yield* Effect.sleep("100 millis");
69+
70+
if (!result) {
71+
// Use sync write to ensure output after alternate screen
72+
process.stdout.write("\nCancelled.\n");
73+
return;
74+
}
75+
76+
// Use sync writes to ensure output appears after alternate screen
77+
process.stdout.write("\n--- Generated DDL ---\n\n");
78+
process.stdout.write(result.ddl + "\n");
79+
80+
// Determine output path
81+
const outPath = Option.getOrUndefined(args.outputPath);
82+
83+
if (outPath) {
84+
// Write to specified path
85+
const dir = path.dirname(outPath);
86+
if (dir && dir !== ".") {
87+
fs.mkdirSync(dir, { recursive: true });
88+
}
89+
fs.writeFileSync(outPath, result.ddl + "\n");
90+
process.stdout.write(`\n✓ Saved to: ${outPath}\n`);
91+
} else {
92+
// Suggest a filename
93+
const suggestedName = `${result.state.tableName}.sql`;
94+
process.stdout.write(`\nTo save, run again with: --output ${suggestedName}\n`);
95+
process.stdout.write("Or copy the DDL above.\n");
96+
}
97+
} finally {
98+
yield* Effect.promise(() => pool.end());
99+
}
100+
});
101+
102+
const configOpts = { configPath: Option.getOrUndefined(args.configPath) };
103+
const configLayer = ConfigWithFallback(
104+
FileConfigProvider(configOpts),
105+
() => FileConfigProvider(configOpts),
106+
);
107+
108+
const runNewOnce = runNew.pipe(Effect.provide(configLayer));
109+
110+
return runNewOnce.pipe(
111+
Effect.catchTags({
112+
ConfigNotFound: () =>
113+
Console.log("No config file found. Running init...").pipe(
114+
Effect.zipRight(runInit),
115+
Effect.zipRight(
116+
runNewOnce.pipe(
117+
Effect.catchTags({
118+
ConfigNotFound: () => Console.error("No config file found after init."),
119+
ConfigInvalid: (error) =>
120+
Effect.forEach(error.errors, (e) => Console.error(` - ${e}`)),
121+
}),
122+
),
123+
),
124+
),
125+
ConfigInvalid: (error) => Effect.forEach(error.errors, (e) => Console.error(` - ${e}`)),
126+
}),
127+
);
128+
};
129+
130+
const schemaNewCommand = Command.make(
131+
"new",
132+
{ outputPath, schemaName, configPath },
133+
runSchemaNew,
134+
).pipe(Command.withDescription("Create a new table interactively"));
135+
136+
// =============================================================================
137+
// schema command (parent)
138+
// =============================================================================
139+
140+
export const schemaCommand = Command.make("schema", {}, () =>
141+
Console.log("Schema commands:\n new Create a new table interactively\n\nRun 'pgsourcerer schema <command> --help' for details."),
142+
).pipe(
143+
Command.withDescription("Schema/DDL management commands"),
144+
Command.withSubcommands([schemaNewCommand]),
145+
);

0 commit comments

Comments
 (0)