@beignet/cli 0.0.3 → 0.0.4

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/README.md +379 -61
  3. package/dist/ansi.d.ts +10 -0
  4. package/dist/ansi.d.ts.map +1 -0
  5. package/dist/ansi.js +20 -0
  6. package/dist/ansi.js.map +1 -0
  7. package/dist/choices.d.ts +72 -0
  8. package/dist/choices.d.ts.map +1 -0
  9. package/dist/choices.js +88 -0
  10. package/dist/choices.js.map +1 -0
  11. package/dist/completion.d.ts +47 -0
  12. package/dist/completion.d.ts.map +1 -0
  13. package/dist/completion.js +123 -0
  14. package/dist/completion.js.map +1 -0
  15. package/dist/config.d.ts +8 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +8 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/create-prompts.d.ts +42 -0
  20. package/dist/create-prompts.d.ts.map +1 -0
  21. package/dist/create-prompts.js +136 -0
  22. package/dist/create-prompts.js.map +1 -0
  23. package/dist/create.d.ts +4 -1
  24. package/dist/create.d.ts.map +1 -1
  25. package/dist/create.js +16 -26
  26. package/dist/create.js.map +1 -1
  27. package/dist/db.d.ts +1 -0
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +37 -2
  30. package/dist/db.js.map +1 -1
  31. package/dist/github-annotations.d.ts +18 -0
  32. package/dist/github-annotations.d.ts.map +1 -0
  33. package/dist/github-annotations.js +22 -0
  34. package/dist/github-annotations.js.map +1 -0
  35. package/dist/index.d.ts +1 -9
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +657 -588
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts +21 -2
  40. package/dist/inspect.d.ts.map +1 -1
  41. package/dist/inspect.js +1938 -131
  42. package/dist/inspect.js.map +1 -1
  43. package/dist/lib.d.ts +20 -0
  44. package/dist/lib.d.ts.map +1 -0
  45. package/dist/lib.js +17 -0
  46. package/dist/lib.js.map +1 -0
  47. package/dist/lint.d.ts +10 -1
  48. package/dist/lint.d.ts.map +1 -1
  49. package/dist/lint.js +340 -33
  50. package/dist/lint.js.map +1 -1
  51. package/dist/make.d.ts +20 -3
  52. package/dist/make.d.ts.map +1 -1
  53. package/dist/make.js +1756 -394
  54. package/dist/make.js.map +1 -1
  55. package/dist/outbox.d.ts +24 -0
  56. package/dist/outbox.d.ts.map +1 -0
  57. package/dist/outbox.js +138 -0
  58. package/dist/outbox.js.map +1 -0
  59. package/dist/schedule.d.ts +36 -0
  60. package/dist/schedule.d.ts.map +1 -0
  61. package/dist/schedule.js +155 -0
  62. package/dist/schedule.js.map +1 -0
  63. package/dist/task.d.ts +26 -0
  64. package/dist/task.d.ts.map +1 -0
  65. package/dist/task.js +106 -0
  66. package/dist/task.js.map +1 -0
  67. package/dist/templates.d.ts +3 -32
  68. package/dist/templates.d.ts.map +1 -1
  69. package/dist/templates.js +1002 -527
  70. package/dist/templates.js.map +1 -1
  71. package/dist/version.d.ts +8 -0
  72. package/dist/version.d.ts.map +1 -0
  73. package/dist/version.js +18 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +9 -8
  76. package/src/ansi.ts +30 -0
  77. package/src/choices.ts +137 -0
  78. package/src/completion.ts +169 -0
  79. package/src/config.ts +16 -0
  80. package/src/create-prompts.ts +182 -0
  81. package/src/create.ts +24 -31
  82. package/src/db.ts +60 -4
  83. package/src/github-annotations.ts +37 -0
  84. package/src/index.ts +1067 -803
  85. package/src/inspect.ts +2859 -115
  86. package/src/lib.ts +45 -0
  87. package/src/lint.ts +493 -39
  88. package/src/make.ts +2181 -405
  89. package/src/outbox.ts +249 -0
  90. package/src/schedule.ts +272 -0
  91. package/src/task.ts +169 -0
  92. package/src/templates.ts +1073 -567
  93. package/src/version.ts +20 -0
  94. package/dist/create-bin.d.ts +0 -3
  95. package/dist/create-bin.d.ts.map +0 -1
  96. package/dist/create-bin.js +0 -9
  97. package/dist/create-bin.js.map +0 -1
  98. package/src/create-bin.ts +0 -11
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Interactive selection flow for `beignet create`.
3
+ *
4
+ * The decision helper is pure so the TTY gating stays unit-testable, and
5
+ * `@clack/prompts` is loaded lazily inside the prompt flow so non-interactive
6
+ * runs and CLI startup never pay for it.
7
+ */
8
+
9
+ import {
10
+ type FeatureName,
11
+ featureChoices,
12
+ type IntegrationName,
13
+ integrationChoices,
14
+ type PackageManager,
15
+ type PresetName,
16
+ packageManagerChoices,
17
+ presetIntegrationDefaults,
18
+ } from "./choices.js";
19
+
20
+ /**
21
+ * Selections shared by create flags and interactive prompt answers.
22
+ */
23
+ export type CreateSelections = {
24
+ directory?: string;
25
+ preset?: PresetName;
26
+ features?: readonly FeatureName[];
27
+ integrations?: readonly IntegrationName[];
28
+ packageManager?: PackageManager;
29
+ };
30
+
31
+ type CreatePromptFlags = CreateSelections & {
32
+ yes?: boolean;
33
+ };
34
+
35
+ type CreatePromptTty = {
36
+ stdin: boolean;
37
+ stdout: boolean;
38
+ };
39
+
40
+ /**
41
+ * Decide whether `beignet create` should run the interactive prompt flow.
42
+ *
43
+ * Prompts run only on a full TTY, only when `--yes` is absent, and only when
44
+ * no selection flag was passed. Any explicit preset, feature, integration, or
45
+ * package manager selection means the invocation is scripted, so the
46
+ * non-interactive path stays byte-identical for agents and CI.
47
+ */
48
+ export function shouldPromptInteractively(
49
+ flags: CreatePromptFlags,
50
+ tty: CreatePromptTty,
51
+ ): boolean {
52
+ if (!tty.stdin || !tty.stdout) return false;
53
+ if (flags.yes) return false;
54
+
55
+ const selectionFlagPassed =
56
+ flags.preset !== undefined ||
57
+ (flags.features?.length ?? 0) > 0 ||
58
+ (flags.integrations?.length ?? 0) > 0 ||
59
+ flags.packageManager !== undefined;
60
+
61
+ return !selectionFlagPassed;
62
+ }
63
+
64
+ const featureHints: Record<FeatureName, string> = {
65
+ client: "Typed Beignet HTTP client",
66
+ "react-query": "TanStack Query helpers for contracts",
67
+ forms: "React Hook Form wired to contract bodies",
68
+ openapi: "OpenAPI document route generated from contracts",
69
+ };
70
+
71
+ const integrationHints: Record<IntegrationName, string> = {
72
+ "better-auth": "Better Auth routes and auth provider wiring",
73
+ "drizzle-turso": "Drizzle/libSQL persistence with Turso-ready config",
74
+ inngest: "Inngest-backed background jobs",
75
+ pino: "Pino logger provider",
76
+ resend: "Resend-backed mail provider",
77
+ "upstash-rate-limit": "Upstash-backed rate limiting",
78
+ };
79
+
80
+ /**
81
+ * Run the interactive `beignet create` prompt flow.
82
+ *
83
+ * Returns the completed selections, or undefined when the user cancels.
84
+ */
85
+ export async function promptCreateSelections(
86
+ initial: CreateSelections,
87
+ ): Promise<CreateSelections | undefined> {
88
+ const prompts = await import("@clack/prompts");
89
+
90
+ prompts.intro("beignet create");
91
+
92
+ let directory = initial.directory;
93
+ if (directory === undefined) {
94
+ const answer = await prompts.text({
95
+ message: "Where should the app be created?",
96
+ placeholder: "my-app",
97
+ validate: (value) =>
98
+ value === undefined || value.trim() === ""
99
+ ? "Enter a project directory."
100
+ : undefined,
101
+ });
102
+ if (prompts.isCancel(answer)) return undefined;
103
+ directory = answer;
104
+ }
105
+
106
+ const preset = await prompts.select<PresetName>({
107
+ message: "Which starter preset?",
108
+ options: [
109
+ {
110
+ value: "standard",
111
+ label: "standard",
112
+ hint: "Full app layout with auth, database, jobs, and devtools",
113
+ },
114
+ {
115
+ value: "minimal",
116
+ label: "minimal",
117
+ hint: "Smallest contract/server/use-case loop",
118
+ },
119
+ ],
120
+ initialValue: "standard",
121
+ });
122
+ if (prompts.isCancel(preset)) return undefined;
123
+
124
+ // The standard preset already includes every starter feature, so the
125
+ // feature multiselect only appears when the preset leaves features open.
126
+ let features: readonly FeatureName[] | undefined;
127
+ if (preset === "minimal") {
128
+ const answer = await prompts.multiselect<FeatureName>({
129
+ message: "Add starter features? (press enter to skip)",
130
+ options: featureChoices.map((feature) => ({
131
+ value: feature,
132
+ label: feature,
133
+ hint: featureHints[feature],
134
+ })),
135
+ required: false,
136
+ });
137
+ if (prompts.isCancel(answer)) return undefined;
138
+ features = answer;
139
+ }
140
+
141
+ const presetIntegrations = new Set(presetIntegrationDefaults[preset]);
142
+ const integrationOptions = integrationChoices.filter(
143
+ (integration) => !presetIntegrations.has(integration),
144
+ );
145
+ let integrations: readonly IntegrationName[] | undefined;
146
+ if (integrationOptions.length > 0) {
147
+ const answer = await prompts.multiselect<IntegrationName>({
148
+ message:
149
+ presetIntegrations.size > 0
150
+ ? "Add provider integrations beyond the preset defaults? (press enter to skip)"
151
+ : "Add provider integrations? (press enter to skip)",
152
+ options: integrationOptions.map((integration) => ({
153
+ value: integration,
154
+ label: integration,
155
+ hint: integrationHints[integration],
156
+ })),
157
+ required: false,
158
+ });
159
+ if (prompts.isCancel(answer)) return undefined;
160
+ integrations = answer;
161
+ }
162
+
163
+ const packageManager = await prompts.select<PackageManager>({
164
+ message: "Which package manager?",
165
+ options: packageManagerChoices.map((manager) => ({
166
+ value: manager,
167
+ label: manager,
168
+ })),
169
+ initialValue: "bun",
170
+ });
171
+ if (prompts.isCancel(packageManager)) return undefined;
172
+
173
+ prompts.outro(`Creating ${directory}`);
174
+
175
+ return {
176
+ directory,
177
+ preset,
178
+ features,
179
+ integrations,
180
+ packageManager,
181
+ };
182
+ }
package/src/create.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { readFileSync } from "node:fs";
2
1
  import { mkdir, readdir, writeFile } from "node:fs/promises";
3
2
  import path from "node:path";
3
+ import { presetFeatureDefaults, presetIntegrationDefaults } from "./choices.js";
4
4
  import {
5
5
  type FeatureName,
6
6
  featureChoices,
@@ -11,6 +11,7 @@ import {
11
11
  type PresetName,
12
12
  type TemplateName,
13
13
  } from "./templates.js";
14
+ import { getCliVersion } from "./version.js";
14
15
 
15
16
  /**
16
17
  * Options for creating a new Beignet app.
@@ -24,15 +25,18 @@ export type CreateOptions = {
24
25
  packageManager?: PackageManager;
25
26
  integrations?: IntegrationName[];
26
27
  force?: boolean;
28
+ dryRun?: boolean;
27
29
  beignetVersion?: string;
28
30
  };
29
31
 
30
32
  /**
31
- * Result returned after a new app is scaffolded.
33
+ * Result returned after a new app is scaffolded or previewed.
32
34
  */
33
35
  export type CreateResult = {
36
+ schemaVersion: 1;
34
37
  name: string;
35
38
  targetDir: string;
39
+ dryRun: boolean;
36
40
  files: string[];
37
41
  packageManager: PackageManager;
38
42
  };
@@ -58,8 +62,9 @@ export async function createProject(
58
62
  );
59
63
  }
60
64
 
65
+ const dryRun = Boolean(options.dryRun);
66
+
61
67
  await assertCanWrite(targetDir, Boolean(options.force));
62
- await mkdir(targetDir, { recursive: true });
63
68
 
64
69
  const files = getTemplateFiles({
65
70
  name,
@@ -70,15 +75,21 @@ export async function createProject(
70
75
  integrations,
71
76
  });
72
77
 
73
- for (const file of files) {
74
- const destination = path.join(targetDir, file.path);
75
- await mkdir(path.dirname(destination), { recursive: true });
76
- await writeFile(destination, file.content);
78
+ if (!dryRun) {
79
+ await mkdir(targetDir, { recursive: true });
80
+
81
+ for (const file of files) {
82
+ const destination = path.join(targetDir, file.path);
83
+ await mkdir(path.dirname(destination), { recursive: true });
84
+ await writeFile(destination, file.content);
85
+ }
77
86
  }
78
87
 
79
88
  return {
89
+ schemaVersion: 1,
80
90
  name,
81
91
  targetDir,
92
+ dryRun,
82
93
  files: files.map((file) => file.path),
83
94
  packageManager,
84
95
  };
@@ -88,13 +99,7 @@ function resolveFeatures(
88
99
  preset: PresetName,
89
100
  explicitFeatures: FeatureName[],
90
101
  ): FeatureName[] {
91
- const features = new Set<FeatureName>();
92
-
93
- if (preset === "standard") {
94
- for (const feature of featureChoices) {
95
- features.add(feature);
96
- }
97
- }
102
+ const features = new Set<FeatureName>(presetFeatureDefaults[preset]);
98
103
 
99
104
  for (const feature of explicitFeatures) {
100
105
  features.add(feature);
@@ -114,13 +119,9 @@ function resolveIntegrations(
114
119
  preset: PresetName,
115
120
  explicitIntegrations: IntegrationName[],
116
121
  ): IntegrationName[] {
117
- const integrations = new Set<IntegrationName>();
118
-
119
- if (preset === "standard") {
120
- integrations.add("better-auth");
121
- integrations.add("drizzle-turso");
122
- integrations.add("pino");
123
- }
122
+ const integrations = new Set<IntegrationName>(
123
+ presetIntegrationDefaults[preset],
124
+ );
124
125
 
125
126
  for (const integration of explicitIntegrations) {
126
127
  integrations.add(integration);
@@ -132,16 +133,8 @@ function resolveIntegrations(
132
133
  }
133
134
 
134
135
  function getDefaultBeignetVersion(): string {
135
- try {
136
- const packageJsonUrl = new URL("../package.json", import.meta.url);
137
- const packageJson = JSON.parse(readFileSync(packageJsonUrl, "utf8")) as {
138
- version?: string;
139
- };
140
-
141
- return packageJson.version ? `^${packageJson.version}` : "latest";
142
- } catch {
143
- return "latest";
144
- }
136
+ const version = getCliVersion();
137
+ return version === "0.0.0" ? "latest" : `^${version}`;
145
138
  }
146
139
 
147
140
  function normalizeProjectName(name: string): string {
package/src/db.ts CHANGED
@@ -21,6 +21,7 @@ export type RunDatabaseCommandOptions = {
21
21
  * Result returned by a database lifecycle command.
22
22
  */
23
23
  export type RunDatabaseCommandResult = {
24
+ schemaVersion: 1;
24
25
  command: DatabaseCommand;
25
26
  cwd: string;
26
27
  script: string;
@@ -56,18 +57,20 @@ export async function runDatabaseCommand(
56
57
  const cwd = path.resolve(options.cwd ?? process.cwd());
57
58
  const script = databaseScripts[options.command];
58
59
  const packageJson = await readPackageJson(cwd);
60
+ const scriptCommand = packageJson.scripts?.[script];
59
61
 
60
- if (!packageJson.scripts?.[script]) {
61
- throw new Error(
62
- `Missing package.json script "${script}". Add it to this Beignet app before running beignet db ${options.command}.`,
63
- );
62
+ if (!scriptCommand) {
63
+ throw new Error(missingDatabaseScriptMessage(options.command, script));
64
64
  }
65
65
 
66
+ await assertDatabaseCommandPreflight(cwd, options.command, scriptCommand);
67
+
66
68
  const runner = await detectPackageManager(cwd);
67
69
  const args = ["run", script];
68
70
 
69
71
  if (options.dryRun) {
70
72
  return {
73
+ schemaVersion: 1,
71
74
  command: options.command,
72
75
  cwd,
73
76
  script,
@@ -83,6 +86,7 @@ export async function runDatabaseCommand(
83
86
  });
84
87
 
85
88
  return {
89
+ schemaVersion: 1,
86
90
  command: options.command,
87
91
  cwd,
88
92
  script,
@@ -96,6 +100,58 @@ export async function runDatabaseCommand(
96
100
  };
97
101
  }
98
102
 
103
+ async function assertDatabaseCommandPreflight(
104
+ cwd: string,
105
+ command: DatabaseCommand,
106
+ scriptCommand: string,
107
+ ): Promise<void> {
108
+ if (
109
+ (command === "generate" || command === "migrate") &&
110
+ /\bdrizzle-kit\b/.test(scriptCommand) &&
111
+ !hasExplicitDrizzleConfigFlag(scriptCommand) &&
112
+ !(await exists(path.join(cwd, "drizzle.config.ts"))) &&
113
+ !(await exists(path.join(cwd, "drizzle.config.mts"))) &&
114
+ !(await exists(path.join(cwd, "drizzle.config.js"))) &&
115
+ !(await exists(path.join(cwd, "drizzle.config.mjs")))
116
+ ) {
117
+ throw new Error(
118
+ `Missing Drizzle config. beignet db ${command} runs "${scriptCommand}", but no drizzle.config.ts, drizzle.config.mts, drizzle.config.js, or drizzle.config.mjs exists in the app root.`,
119
+ );
120
+ }
121
+
122
+ const standardEntrypoints: Partial<Record<DatabaseCommand, string>> = {
123
+ seed: "infra/db/seed.ts",
124
+ reset: "infra/db/reset.ts",
125
+ };
126
+ const standardEntrypoint = standardEntrypoints[command];
127
+ if (
128
+ standardEntrypoint &&
129
+ scriptCommand.includes(standardEntrypoint) &&
130
+ !(await exists(path.join(cwd, standardEntrypoint)))
131
+ ) {
132
+ throw new Error(
133
+ `Missing database ${command} entrypoint ${standardEntrypoint}. beignet db ${command} runs "${scriptCommand}", so restore ${standardEntrypoint} or update package.json script "${databaseScripts[command]}".`,
134
+ );
135
+ }
136
+ }
137
+
138
+ function hasExplicitDrizzleConfigFlag(scriptCommand: string): boolean {
139
+ return /(?:^|\s)--config(?:=|\s)/.test(scriptCommand);
140
+ }
141
+
142
+ function missingDatabaseScriptMessage(
143
+ command: DatabaseCommand,
144
+ script: string,
145
+ ): string {
146
+ if (command === "generate" || command === "migrate") {
147
+ return `Missing package.json script "${script}". Standard Drizzle apps use "${script}": "drizzle-kit ${command}" with drizzle.config.ts at the app root.`;
148
+ }
149
+
150
+ const entrypoint =
151
+ command === "seed" ? "infra/db/seed.ts" : "infra/db/reset.ts";
152
+ return `Missing package.json script "${script}". Standard Drizzle apps use "${script}": "bun ${entrypoint}" so beignet db ${command} can run the app-owned ${command} entrypoint.`;
153
+ }
154
+
99
155
  async function readPackageJson(cwd: string): Promise<PackageJson> {
100
156
  try {
101
157
  return JSON.parse(
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Severity levels understood by GitHub Actions workflow commands.
3
+ */
4
+ export type GithubAnnotationSeverity = "error" | "warning" | "notice";
5
+
6
+ /**
7
+ * Format one GitHub Actions annotation workflow command.
8
+ *
9
+ * Absent properties are omitted and the message is percent-encoded so
10
+ * multiline messages survive the single-line command format.
11
+ */
12
+ export function formatGithubAnnotation(annotation: {
13
+ severity: GithubAnnotationSeverity;
14
+ message: string;
15
+ file?: string;
16
+ line?: number;
17
+ col?: number;
18
+ }): string {
19
+ const properties = [
20
+ annotation.file === undefined ? undefined : `file=${annotation.file}`,
21
+ annotation.line === undefined ? undefined : `line=${annotation.line}`,
22
+ annotation.col === undefined ? undefined : `col=${annotation.col}`,
23
+ ].filter((property): property is string => property !== undefined);
24
+ const propertySection =
25
+ properties.length > 0 ? ` ${properties.join(",")}` : "";
26
+
27
+ return `::${annotation.severity}${propertySection}::${encodeGithubAnnotationMessage(
28
+ annotation.message,
29
+ )}`;
30
+ }
31
+
32
+ function encodeGithubAnnotationMessage(message: string): string {
33
+ return message
34
+ .replaceAll("%", "%25")
35
+ .replaceAll("\r", "%0D")
36
+ .replaceAll("\n", "%0A");
37
+ }