@stacksjs/buddy 0.70.59 → 0.70.60

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 (79) hide show
  1. package/dist/cli.js +162 -4
  2. package/dist/commands/about.js +35 -0
  3. package/dist/commands/add.d.ts +2 -0
  4. package/dist/commands/add.js +33 -0
  5. package/dist/commands/auth.js +68 -0
  6. package/dist/commands/build.js +153 -0
  7. package/dist/commands/cd.js +39 -0
  8. package/dist/commands/changelog.js +32 -0
  9. package/dist/commands/clean.js +37 -0
  10. package/dist/commands/cloud.js +503 -0
  11. package/dist/commands/commit.js +16 -0
  12. package/dist/commands/completion.js +143 -0
  13. package/dist/commands/config-migrate.js +94 -0
  14. package/dist/commands/configure.js +48 -0
  15. package/dist/commands/create.js +137 -0
  16. package/dist/commands/deploy.js +1404 -0
  17. package/dist/commands/dev.js +911 -0
  18. package/dist/commands/dns.js +62 -0
  19. package/dist/commands/doctor.js +280 -0
  20. package/dist/commands/domains.js +159 -0
  21. package/dist/commands/email.js +526 -0
  22. package/dist/commands/env.js +246 -0
  23. package/dist/commands/features.js +422 -0
  24. package/dist/commands/fresh.js +39 -0
  25. package/dist/commands/generate.js +104 -0
  26. package/dist/commands/http.js +30 -0
  27. package/dist/commands/index.js +53 -0
  28. package/dist/commands/install.js +23 -0
  29. package/dist/commands/key.js +23 -0
  30. package/dist/commands/lint.js +50 -0
  31. package/dist/commands/list.js +108 -0
  32. package/dist/commands/mail.js +693 -0
  33. package/dist/commands/maintenance.d.ts +2 -0
  34. package/dist/commands/maintenance.js +141 -0
  35. package/dist/commands/make.js +375 -0
  36. package/dist/commands/migrate-project.js +49 -0
  37. package/dist/commands/migrate.js +373 -0
  38. package/dist/commands/outdated.js +19 -0
  39. package/dist/commands/package.d.ts +2 -0
  40. package/dist/commands/package.js +17 -0
  41. package/dist/commands/phone.js +188 -0
  42. package/dist/commands/ports.js +118 -0
  43. package/dist/commands/prepublish.js +17 -0
  44. package/dist/commands/projects.js +29 -0
  45. package/dist/commands/publish.js +169 -0
  46. package/dist/commands/queue.js +249 -0
  47. package/dist/commands/release.js +30 -0
  48. package/dist/commands/route.js +21 -0
  49. package/dist/commands/saas.js +25 -0
  50. package/dist/commands/schedule.js +61 -0
  51. package/dist/commands/search.js +84 -0
  52. package/dist/commands/seed.js +71 -0
  53. package/dist/commands/serve.js +176 -0
  54. package/dist/commands/setup.js +203 -0
  55. package/dist/commands/share.d.ts +2 -0
  56. package/dist/commands/share.js +209 -0
  57. package/dist/commands/sms.js +328 -0
  58. package/dist/commands/stacks.d.ts +2 -0
  59. package/dist/commands/stacks.js +69 -0
  60. package/dist/commands/telemetry.js +74 -0
  61. package/dist/commands/test.js +130 -0
  62. package/dist/commands/tinker.js +37 -0
  63. package/dist/commands/types.js +18 -0
  64. package/dist/commands/upgrade.js +97 -0
  65. package/dist/commands/version.js +16 -0
  66. package/dist/config.d.ts +43 -0
  67. package/dist/config.js +223 -0
  68. package/dist/custom-cli.d.ts +1 -0
  69. package/dist/custom-cli.js +23 -0
  70. package/dist/index.js +1 -3424
  71. package/dist/lazy-commands.d.ts +61 -0
  72. package/dist/lazy-commands.js +182 -0
  73. package/dist/migrators/index.js +62 -0
  74. package/dist/migrators/laravel/index.js +148 -0
  75. package/dist/migrators/laravel/migrations.js +231 -0
  76. package/dist/migrators/laravel/models.js +132 -0
  77. package/dist/migrators/rails/index.js +11 -0
  78. package/dist/migrators/types.js +0 -0
  79. package/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ import process from "node:process";
2
+ import { log, onUnknownSubcommand } from "@stacksjs/cli";
3
+ import { decryptEnv, encryptEnv, getEnv, getKeypair, rotateKeypair, setEnv } from "@stacksjs/env";
4
+ import { ExitCode } from "@stacksjs/types";
5
+ export function env(buddy) {
6
+ const descriptions = {
7
+ env: "Interact with your environment variables",
8
+ get: "Get an environment variable",
9
+ set: "Set an environment variable",
10
+ encrypt: "Encrypt a value",
11
+ decrypt: "Decrypt a value",
12
+ check: "Check environment configuration and validate setup",
13
+ pretty: "Pretty print the result",
14
+ stdout: "Output the result to stdout",
15
+ keypair: "Generate a keypair",
16
+ fileKeys: "The path to the file containing the keys",
17
+ excludeKey: "The key to exclude from encryption",
18
+ rotate: "Rotate a keypair",
19
+ format: "The format to output the result (json, shell, eval)",
20
+ all: "Get all environment variables",
21
+ file: "The environment file to use",
22
+ project: "Target a specific project",
23
+ verbose: "Enable verbose output"
24
+ };
25
+ buddy.command("env:get [key]", descriptions.get).option("-f, --file [file]", descriptions.file, { default: "" }).option("--format [format]", descriptions.format, { default: "json" }).option("-a, --all", descriptions.all, { default: !1 }).option("-p, --pretty", descriptions.pretty, { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).example("buddy env:get SECRET").example("buddy env:get SECRET --file .env.production").example("buddy env:get --all --pretty").example("buddy env:get --format shell").example("buddy env:get --format eval").example("buddy env:get --format json").action(async (key, options) => {
26
+ log.debug("Running `buddy env:get` ...", options);
27
+ const result = getEnv(key, {
28
+ file: options.file,
29
+ keysFile: options.fileKeys,
30
+ all: options.all,
31
+ format: options.format,
32
+ prettyPrint: options.pretty
33
+ });
34
+ if (result.success) {
35
+ console.log(result.output);
36
+ process.exit(ExitCode.Success);
37
+ } else {
38
+ console.error(result.error);
39
+ process.exit(ExitCode.FatalError);
40
+ }
41
+ });
42
+ buddy.command("env:set [key] [value]", descriptions.set).option("-f, --file [file]", descriptions.file, { default: "" }).option("-fk, --file-keys [fileKeys]", descriptions.fileKeys, { default: "" }).option("--plain", "Don't encrypt the value", { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).example("buddy env:set SECRET=value").example("buddy env:set SECRET value").example("buddy env:set SECRET value --file .env.production").example("buddy env:set SECRET value --plain").action(async (key, value, options) => {
43
+ log.debug("Running `buddy env:set` ...", options);
44
+ if (!key || !value) {
45
+ console.error("Both key and value are required");
46
+ process.exit(ExitCode.FatalError);
47
+ }
48
+ const result = setEnv(key, value, {
49
+ file: options.file,
50
+ keysFile: options.fileKeys,
51
+ plain: options.plain
52
+ });
53
+ if (result.success) {
54
+ console.log(result.output);
55
+ process.exit(ExitCode.Success);
56
+ } else {
57
+ console.error(result.error);
58
+ process.exit(ExitCode.FatalError);
59
+ }
60
+ });
61
+ buddy.command("env:encrypt [key]", descriptions.encrypt).option("-f, --file [file]", descriptions.file, { default: "" }).option("-fk, --file-keys [fileKeys]", descriptions.fileKeys, { default: "" }).option("-o, --stdout", descriptions.stdout, { default: !1 }).option("-ek, --exclude-key [excludeKey]", descriptions.excludeKey, { default: "" }).example("buddy env:encrypt").example("buddy env:encrypt --file .env.production").example('buddy env:encrypt -k "SECRET_*"').action(async (key, options) => {
62
+ log.debug("Running `buddy env:encrypt` ...", options);
63
+ const result = encryptEnv({
64
+ file: options.file,
65
+ keysFile: options.fileKeys,
66
+ key,
67
+ excludeKey: options.excludeKey,
68
+ stdout: options.stdout
69
+ });
70
+ if (result.success) {
71
+ console.log(result.output);
72
+ process.exit(ExitCode.Success);
73
+ } else {
74
+ console.error(result.error);
75
+ process.exit(ExitCode.FatalError);
76
+ }
77
+ });
78
+ buddy.command("env:decrypt [key]", descriptions.decrypt).option("-f, --file [file]", descriptions.file, { default: "" }).option("-fk, --file-keys [fileKeys]", descriptions.fileKeys, { default: "" }).option("-o, --stdout", descriptions.stdout, { default: !1 }).example("buddy env:decrypt").example("buddy env:decrypt --file .env.production").example('buddy env:decrypt -k "SECRET_*"').action(async (key, options) => {
79
+ log.debug("Running `buddy env:decrypt` ...", options);
80
+ const result = decryptEnv({
81
+ file: options.file,
82
+ keysFile: options.fileKeys,
83
+ key,
84
+ stdout: options.stdout
85
+ });
86
+ if (result.success) {
87
+ console.log(result.output);
88
+ process.exit(ExitCode.Success);
89
+ } else {
90
+ console.error(result.error);
91
+ process.exit(ExitCode.FatalError);
92
+ }
93
+ });
94
+ buddy.command("env:keypair [key]", descriptions.keypair).option("-f, --file [file]", descriptions.file, { default: "" }).option("-fk, --file-keys [fileKeys]", descriptions.fileKeys, { default: "" }).option("--format [format]", descriptions.format, { default: "json" }).example("buddy env:keypair").example("buddy env:keypair --file .env.production").example("buddy env:keypair DOTENV_PRIVATE_KEY").action(async (key, options) => {
95
+ log.debug("Running `buddy env:keypair` ...", options);
96
+ const result = getKeypair(key, {
97
+ file: options.file,
98
+ keysFile: options.fileKeys,
99
+ format: options.format
100
+ });
101
+ if (result.success) {
102
+ console.log(result.output);
103
+ process.exit(ExitCode.Success);
104
+ } else {
105
+ console.error(result.error);
106
+ process.exit(ExitCode.FatalError);
107
+ }
108
+ });
109
+ buddy.command("env:rotate [key]", descriptions.rotate).option("-f, --file [file]", descriptions.file, { default: "" }).option("-fk, --file-keys [fileKeys]", descriptions.fileKeys, { default: "" }).option("-o, --stdout", descriptions.stdout, { default: !1 }).option("-ek, --exclude-key [excludeKey]", descriptions.excludeKey, { default: "" }).example("buddy env:rotate").example("buddy env:rotate --file .env.production").action(async (key, options) => {
110
+ log.debug("Running `buddy env:rotate` ...", options);
111
+ const result = rotateKeypair({
112
+ file: options.file,
113
+ keysFile: options.fileKeys,
114
+ key,
115
+ excludeKey: options.excludeKey,
116
+ stdout: options.stdout
117
+ });
118
+ if (result.success) {
119
+ console.log(result.output);
120
+ process.exit(ExitCode.Success);
121
+ } else {
122
+ console.error(result.error);
123
+ process.exit(ExitCode.FatalError);
124
+ }
125
+ });
126
+ buddy.command("env:check", descriptions.check).option("-f, --file [file]", descriptions.file, { default: "" }).option("--verbose", descriptions.verbose, { default: !1 }).example("buddy env:check").example("buddy env:check --file .env.production").action(async (options) => {
127
+ log.debug("Running `buddy env:check` ...", options);
128
+ const { bold, dim, green, red, yellow, intro } = await import("@stacksjs/cli"), { storage } = await import("@stacksjs/storage"), { existsSync } = await import("node:fs"), { resolve } = await import("node:path");
129
+ await intro("buddy env:check");
130
+ const checks = [], envFile = options.file || ".env", envPath = resolve(process.cwd(), envFile);
131
+ if (existsSync(envPath)) {
132
+ checks.push({
133
+ name: `${envFile} file`,
134
+ status: "pass",
135
+ message: "Found"
136
+ });
137
+ try {
138
+ const envContent = await storage.readTextFile(envPath), lines = String(envContent).split(`
139
+ `).filter((line) => line.trim() && !line.startsWith("#")), varCount = lines.length;
140
+ checks.push({
141
+ name: "Environment variables",
142
+ status: "pass",
143
+ message: `${varCount} variables defined`
144
+ });
145
+ if (lines.some((line) => line.startsWith("APP_KEY="))) {
146
+ const appKeyLine = lines.find((line) => line.startsWith("APP_KEY=")), eq = appKeyLine?.indexOf("=") ?? -1, appKeyValue = (eq >= 0 ? appKeyLine.slice(eq + 1) : "").trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
147
+ if (appKeyValue && appKeyValue.length > 0)
148
+ checks.push({
149
+ name: "APP_KEY",
150
+ status: "pass",
151
+ message: "Set"
152
+ });
153
+ else
154
+ checks.push({
155
+ name: "APP_KEY",
156
+ status: "warn",
157
+ message: "Empty (run: buddy key:generate)"
158
+ });
159
+ } else
160
+ checks.push({
161
+ name: "APP_KEY",
162
+ status: "warn",
163
+ message: "Not found (run: buddy key:generate)"
164
+ });
165
+ const hasPublicKey = lines.some((line) => line.startsWith("DOTENV_PUBLIC_KEY=")), hasPrivateKey = lines.some((line) => line.startsWith("DOTENV_PRIVATE_KEY="));
166
+ if (hasPublicKey && hasPrivateKey)
167
+ checks.push({
168
+ name: "Encryption keys",
169
+ status: "pass",
170
+ message: "Public and private keys configured"
171
+ });
172
+ else if (hasPublicKey || hasPrivateKey)
173
+ checks.push({
174
+ name: "Encryption keys",
175
+ status: "warn",
176
+ message: "Incomplete keypair (run: buddy env:keypair)"
177
+ });
178
+ else
179
+ checks.push({
180
+ name: "Encryption keys",
181
+ status: "warn",
182
+ message: "Not configured (optional)"
183
+ });
184
+ } catch (error) {
185
+ checks.push({
186
+ name: `${envFile} content`,
187
+ status: "fail",
188
+ message: `Cannot read file: ${error}`
189
+ });
190
+ }
191
+ } else
192
+ checks.push({
193
+ name: `${envFile} file`,
194
+ status: "fail",
195
+ message: "Not found"
196
+ });
197
+ const keysPath = resolve(process.cwd(), ".env.keys");
198
+ if (existsSync(keysPath))
199
+ checks.push({
200
+ name: ".env.keys file",
201
+ status: "pass",
202
+ message: "Found"
203
+ });
204
+ else
205
+ checks.push({
206
+ name: ".env.keys file",
207
+ status: "warn",
208
+ message: "Not found (optional for encryption)"
209
+ });
210
+ console.log("");
211
+ console.log(bold("Environment Configuration Check:"));
212
+ console.log(dim("\u2500".repeat(60)));
213
+ console.log("");
214
+ let hasFailures = !1, hasWarnings = !1;
215
+ for (const check of checks) {
216
+ let statusIcon = "", statusColor = (text) => text;
217
+ if (check.status === "pass") {
218
+ statusIcon = "\u2713";
219
+ statusColor = green;
220
+ } else if (check.status === "warn") {
221
+ statusIcon = "\u26A0";
222
+ statusColor = yellow;
223
+ hasWarnings = !0;
224
+ } else {
225
+ statusIcon = "\u2717";
226
+ statusColor = red;
227
+ hasFailures = !0;
228
+ }
229
+ console.log(`${statusColor(statusIcon)} ${bold(check.name.padEnd(25))} ${dim(check.message)}`);
230
+ }
231
+ console.log("");
232
+ console.log(dim("\u2500".repeat(60)));
233
+ console.log("");
234
+ if (hasFailures) {
235
+ console.log(red("\u2717 Some critical checks failed. Please address the issues above."));
236
+ process.exit(ExitCode.FatalError);
237
+ } else if (hasWarnings) {
238
+ console.log(yellow("\u26A0 Some checks have warnings. Your environment should work but may have issues."));
239
+ process.exit(ExitCode.Success);
240
+ } else {
241
+ console.log(green("\u2713 All checks passed! Your environment configuration looks healthy."));
242
+ process.exit(ExitCode.Success);
243
+ }
244
+ });
245
+ onUnknownSubcommand(buddy, "env");
246
+ }
@@ -0,0 +1,422 @@
1
+ import { existsSync } from "node:fs";
2
+ import { cp, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import process from "node:process";
5
+ import { frameworkPath, projectPath } from "@stacksjs/path";
6
+ import { ExitCode } from "@stacksjs/types";
7
+ export const FEATURE_NAMES = [
8
+ "dashboard",
9
+ "commerce",
10
+ "cms",
11
+ "marketing",
12
+ "monitoring",
13
+ "realtime",
14
+ "queue"
15
+ ], FEATURE_FILES = {
16
+ cms: [
17
+ "app/Actions/Cms/",
18
+ "app/Actions/Dashboard/Content/",
19
+ "app/Models/Content/",
20
+ "app/Models/Tag.ts",
21
+ "app/Models/Comment.ts",
22
+ "resources/views/dashboard/content/"
23
+ ],
24
+ commerce: [
25
+ "app/Actions/Commerce/",
26
+ "app/Actions/Dashboard/Commerce/",
27
+ "app/Models/commerce/",
28
+ "resources/components/Dashboard/Commerce/",
29
+ "resources/views/dashboard/commerce/"
30
+ ],
31
+ dashboard: [
32
+ "app/Actions/Dashboard/",
33
+ "resources/components/Dashboard/",
34
+ "resources/views/dashboard/",
35
+ "routes/dashboard.ts",
36
+ "routes/dashboard-api.ts"
37
+ ],
38
+ marketing: [
39
+ "app/Actions/Dashboard/Marketing/",
40
+ "app/Models/Campaign.ts",
41
+ "app/Models/CampaignSend.ts",
42
+ "app/Models/EmailList.ts",
43
+ "app/Models/EmailListSubscriber.ts",
44
+ "app/Models/SocialPost.ts",
45
+ "resources/components/Marketing/",
46
+ "resources/views/dashboard/marketing/"
47
+ ],
48
+ monitoring: [
49
+ "app/Actions/Monitoring/",
50
+ "app/Actions/TestErrorAction.ts",
51
+ "app/Models/Error.ts",
52
+ "functions/monitoring/",
53
+ "resources/views/dashboard/monitoring/",
54
+ "resources/views/dashboard/errors/"
55
+ ],
56
+ realtime: [
57
+ "app/Actions/Realtime/",
58
+ "app/Actions/Dashboard/Realtime/",
59
+ "app/Models/realtime/",
60
+ "app/Broadcasts/",
61
+ "functions/realtime/",
62
+ "resources/views/dashboard/realtime/"
63
+ ],
64
+ queue: [
65
+ "app/Actions/Queue/",
66
+ "app/Actions/Dashboard/Jobs/",
67
+ "app/Jobs/",
68
+ "app/Models/Job.ts",
69
+ "app/Models/FailedJob.ts",
70
+ "functions/jobs.ts",
71
+ "resources/views/dashboard/queue/",
72
+ "resources/views/dashboard/jobs/"
73
+ ]
74
+ }, FEATURE_TABLES = {
75
+ cms: ["posts", "pages", "comments", "tags", "authors", "categories"],
76
+ commerce: [
77
+ "products",
78
+ "product_variants",
79
+ "product_units",
80
+ "manufacturers",
81
+ "orders",
82
+ "order_items",
83
+ "carts",
84
+ "cart_items",
85
+ "payments",
86
+ "payment_methods",
87
+ "payment_products",
88
+ "payment_transactions",
89
+ "customers",
90
+ "subscribers",
91
+ "subscriber_emails",
92
+ "subscriptions",
93
+ "gift_cards",
94
+ "coupons",
95
+ "transactions",
96
+ "reviews",
97
+ "drivers",
98
+ "delivery_routes",
99
+ "digital_deliveries",
100
+ "shipping_methods",
101
+ "shipping_rates",
102
+ "shipping_zones",
103
+ "license_keys",
104
+ "loyalty_points",
105
+ "loyalty_rewards",
106
+ "print_devices",
107
+ "receipts",
108
+ "tax_rates",
109
+ "waitlist_products",
110
+ "waitlist_restaurants"
111
+ ],
112
+ dashboard: [
113
+ "boards",
114
+ "board_columns",
115
+ "cards",
116
+ "card_labels",
117
+ "card_assignees",
118
+ "card_comments",
119
+ "labels",
120
+ "ci_run_states",
121
+ "ci_runner_samples",
122
+ "ci_runner_alert_states",
123
+ "requests",
124
+ "logs"
125
+ ],
126
+ marketing: [],
127
+ monitoring: ["errors"],
128
+ realtime: ["websockets"],
129
+ queue: ["jobs", "failed_jobs"]
130
+ };
131
+ export function migrationTable(filename) {
132
+ const inMatch = filename.match(/-in-([a-z0-9_]+)\.sql$/i);
133
+ if (inMatch)
134
+ return inMatch[1];
135
+ const createMatch = filename.match(/-create-([a-z0-9_]+)-table\.sql$/i);
136
+ if (createMatch)
137
+ return createMatch[1];
138
+ const alterMatch = filename.match(/-alter-([a-z0-9_]+)-/i);
139
+ if (alterMatch)
140
+ return alterMatch[1];
141
+ return null;
142
+ }
143
+ export function migrationFeature(filename) {
144
+ const table = migrationTable(filename);
145
+ if (!table)
146
+ return null;
147
+ for (const f of FEATURE_NAMES)
148
+ if (FEATURE_TABLES[f].includes(table))
149
+ return f;
150
+ return null;
151
+ }
152
+ export function featurePathsPresent(feature, root = projectPath()) {
153
+ return FEATURE_FILES[feature].filter((rel) => existsSync(`${root}/${rel}`));
154
+ }
155
+ export async function deleteFeatureFiles(feature, root = projectPath()) {
156
+ const removed = [];
157
+ for (const rel of FEATURE_FILES[feature]) {
158
+ const full = `${root}/${rel}`;
159
+ if (!existsSync(full))
160
+ continue;
161
+ await rm(full, { recursive: !0, force: !0 });
162
+ removed.push(rel);
163
+ }
164
+ return removed;
165
+ }
166
+ export async function copyFeatureFiles(feature, options = {}) {
167
+ const source = options.source ?? frameworkPath("defaults"), target = options.target ?? projectPath(), force = options.force === !0, copied = [], skipped = [];
168
+ for (const rel of FEATURE_FILES[feature]) {
169
+ const sourceFull = join(source, rel);
170
+ if (!existsSync(sourceFull)) {
171
+ skipped.push(rel);
172
+ continue;
173
+ }
174
+ const targetFull = join(target, rel);
175
+ if (existsSync(targetFull) && !force) {
176
+ skipped.push(rel);
177
+ continue;
178
+ }
179
+ await cp(sourceFull, targetFull, { recursive: !0, force });
180
+ copied.push(rel);
181
+ }
182
+ return { copied, skipped };
183
+ }
184
+ const FEATURE_DESCRIPTIONS = {
185
+ dashboard: "Admin SPA shell + Activity/Log/Request/Deployment/Notification dashboards.",
186
+ commerce: "Order/Cart/Product/Customer/Coupon/GiftCard/Shipping + storefront API.",
187
+ cms: "Post/Page/Author/Comment/Tag models + content edit dashboards.",
188
+ marketing: "/api/email/subscribe, /api/contact, Campaign/EmailList/SocialPost.",
189
+ monitoring: "Error model + error-tracking views and actions.",
190
+ realtime: "WebSocket broadcaster + Websocket model + realtime-stats actions.",
191
+ queue: "Job + FailedJob models + queue dashboard pages."
192
+ }, STARTER_TEMPLATES = {
193
+ dashboard: `import type { DashboardConfig } from '@stacksjs/types'
194
+
195
+ /**
196
+ * **Dashboard Configuration**
197
+ *
198
+ * Top-level feature gate plus per-section visibility toggles for the
199
+ * \`buddy dev --dashboard\` sidebar. Userland models in \`app/Models/\`
200
+ * always appear regardless of these flags.
201
+ */
202
+ export default {
203
+ enabled: true,
204
+
205
+ sections: {
206
+ library: { enabled: true },
207
+ content: { enabled: true },
208
+ commerce: { enabled: true },
209
+ marketing: { enabled: true },
210
+ analytics: { enabled: true },
211
+ management: { enabled: true },
212
+ utilities: { enabled: true },
213
+ data: {
214
+ dashboard: { enabled: true },
215
+ activity: { enabled: true },
216
+ users: { enabled: true },
217
+ teams: { enabled: true },
218
+ subscribers: { enabled: true },
219
+ allModels: { enabled: true },
220
+ },
221
+ },
222
+ } satisfies DashboardConfig
223
+ `,
224
+ commerce: `import type { CommerceConfig } from '@stacksjs/types'
225
+
226
+ /**
227
+ * **Commerce Configuration**
228
+ *
229
+ * Controls the commerce feature bundle (Order, Cart, Product, Customer,
230
+ * Coupon, GiftCard, Receipt, Shipping models + storefront API).
231
+ */
232
+ export default {
233
+ enabled: true,
234
+
235
+ /** Default storefront currency (ISO 4217). */
236
+ currency: 'USD',
237
+
238
+ /** Default tax rate applied when a product/region rule doesn't override. */
239
+ defaultTaxRate: 0,
240
+ } satisfies CommerceConfig
241
+ `,
242
+ cms: `import type { CmsConfig } from '@stacksjs/types'
243
+
244
+ /**
245
+ * **CMS Configuration**
246
+ *
247
+ * Controls the CMS feature bundle (Post, Page, Author, Comment, Tag,
248
+ * Category models + content-edit dashboards).
249
+ */
250
+ export default {
251
+ enabled: true,
252
+ } satisfies CmsConfig
253
+ `,
254
+ marketing: `import type { MarketingConfig } from '@stacksjs/types'
255
+
256
+ /**
257
+ * **Marketing Configuration**
258
+ *
259
+ * Controls the marketing feature bundle (\`/api/email/subscribe\`,
260
+ * \`/api/contact\`, Campaign / EmailList / SocialPost).
261
+ */
262
+ export default {
263
+ enabled: true,
264
+ } satisfies MarketingConfig
265
+ `,
266
+ monitoring: `import type { MonitoringConfig } from '@stacksjs/types'
267
+
268
+ /**
269
+ * **Monitoring Configuration**
270
+ *
271
+ * Controls the monitoring feature bundle (Error model + error-tracking
272
+ * views and actions).
273
+ */
274
+ export default {
275
+ enabled: true,
276
+ } satisfies MonitoringConfig
277
+ `,
278
+ realtime: `import type { RealtimeConfig } from '@stacksjs/types'
279
+
280
+ /**
281
+ * **Realtime Configuration**
282
+ *
283
+ * Controls the realtime feature bundle (WebSocket broadcaster + Websocket
284
+ * model + realtime-stats actions). The full set of options (driver, server,
285
+ * channels, etc.) is documented at @stacksjs/realtime; this starter
286
+ * template just flips the feature on.
287
+ */
288
+ export default {
289
+ enabled: true,
290
+ } satisfies RealtimeConfig
291
+ `,
292
+ queue: `import type { QueueConfig } from '@stacksjs/types'
293
+
294
+ /**
295
+ * **Queue Configuration**
296
+ *
297
+ * Controls the queue feature bundle (Job + FailedJob models + queue
298
+ * dashboard pages). The full set of options (drivers, connections, worker
299
+ * concurrency, etc.) is documented at @stacksjs/queue; this starter
300
+ * template ships the \`sync\` driver so jobs execute immediately in dev.
301
+ */
302
+ export default {
303
+ enabled: true,
304
+
305
+ default: 'sync',
306
+
307
+ connections: {
308
+ sync: { driver: 'sync' },
309
+ },
310
+
311
+ failed: {
312
+ driver: 'database',
313
+ table: 'failed_jobs',
314
+ },
315
+ } satisfies QueueConfig
316
+ `
317
+ };
318
+ export async function setFeatureEnabled(feature, enabled, options) {
319
+ const path = options.root ? join(options.root, `config/${feature}.ts`) : projectPath(`config/${feature}.ts`), file = Bun.file(path);
320
+ if (!await file.exists()) {
321
+ if (!options.createIfMissing)
322
+ return "missing";
323
+ await Bun.write(path, STARTER_TEMPLATES[feature]);
324
+ return "created";
325
+ }
326
+ const src = await file.text(), enabledRegex = /(^|\{)(\s*)enabled\s*:\s*(true|false)(\s*,?)/m;
327
+ if (enabledRegex.test(src)) {
328
+ const replaced = src.replace(enabledRegex, (_full, anchor, ws, current, trailing) => {
329
+ if (current === String(enabled))
330
+ return `${anchor}${ws}enabled: ${current}${trailing}`;
331
+ return `${anchor}${ws}enabled: ${enabled}${trailing}`;
332
+ });
333
+ if (replaced === src)
334
+ return "unchanged";
335
+ await Bun.write(path, replaced);
336
+ return "flipped";
337
+ }
338
+ const insertRegex = /(export default\s*\{)/;
339
+ if (!insertRegex.test(src))
340
+ throw Error(`Could not locate \`export default {\` in ${path} \u2014 please add \`enabled: ${enabled}\` manually.`);
341
+ const next = src.replace(insertRegex, `$1
342
+ enabled: ${enabled},`);
343
+ await Bun.write(path, next);
344
+ return "flipped";
345
+ }
346
+ export async function uninstallAllFeatures(options = {}) {
347
+ const root = options.root ?? projectPath(), results = [];
348
+ for (const feature of FEATURE_NAMES) {
349
+ const configOutcome = await setFeatureEnabled(feature, !1, { createIfMissing: !1, root }), filesRemoved = await deleteFeatureFiles(feature, root);
350
+ results.push({ feature, configOutcome, filesRemoved });
351
+ }
352
+ return results;
353
+ }
354
+ function registerInstallPair(buddy, feature) {
355
+ const desc = FEATURE_DESCRIPTIONS[feature], configRel = `config/${feature}.ts`;
356
+ buddy.command(`${feature}:install`, `Activate the ${feature} feature bundle. ${desc}`).option("--force", `Overwrite any existing ${feature} files in the project (default skips existing paths so the install is idempotent).`).action(async (options) => {
357
+ try {
358
+ switch (await setFeatureEnabled(feature, !0, { createIfMissing: !0 })) {
359
+ case "created":
360
+ console.log(`\u2713 Created ${configRel} with '${feature}' enabled.`);
361
+ break;
362
+ case "flipped":
363
+ console.log(`\u2713 Enabled '${feature}' in ${configRel}.`);
364
+ break;
365
+ case "unchanged":
366
+ console.log(`\u2713 '${feature}' is already enabled in ${configRel}.`);
367
+ break;
368
+ }
369
+ const { copied } = await copyFeatureFiles(feature, { force: options.force === !0 });
370
+ if (copied.length > 0) {
371
+ console.log(`\u2713 Copied ${copied.length} stamped path(s) from framework defaults:`);
372
+ for (const path of copied)
373
+ console.log(` - ${path}`);
374
+ } else if (featurePathsPresent(feature).length > 0)
375
+ console.log(` \u2192 ${feature} scaffolding already in place \u2014 use --force to overwrite.`);
376
+ console.log(` \u2192 next ./buddy dev will boot with ${feature} loaded.`);
377
+ process.exit(ExitCode.Success);
378
+ } catch (err) {
379
+ console.error(`\u2717 Failed to install ${feature}:`, err);
380
+ process.exit(ExitCode.FatalError);
381
+ }
382
+ });
383
+ buddy.command(`${feature}:uninstall`, `Deactivate the ${feature} feature bundle.`).option("--keep-files", `Don't delete the ${feature} scaffolding (action/model/view files). Flip the flag only.`).action(async (options) => {
384
+ try {
385
+ switch (await setFeatureEnabled(feature, !1, { createIfMissing: !1 })) {
386
+ case "missing":
387
+ console.log(`\u2713 '${feature}' is already disabled (${configRel} is absent).`);
388
+ break;
389
+ case "flipped":
390
+ console.log(`\u2713 Disabled '${feature}' in ${configRel}. Custom config preserved.`);
391
+ break;
392
+ case "unchanged":
393
+ console.log(`\u2713 '${feature}' is already disabled in ${configRel}.`);
394
+ break;
395
+ case "created":
396
+ console.log(`\u2717 Unexpected create on uninstall for ${feature}; please re-run with --force or report this bug.`);
397
+ break;
398
+ }
399
+ if (options.keepFiles) {
400
+ const stillPresent = featurePathsPresent(feature);
401
+ if (stillPresent.length > 0)
402
+ console.log(` \u2192 ${stillPresent.length} stamped path(s) preserved (--keep-files).`);
403
+ } else {
404
+ const removed = await deleteFeatureFiles(feature);
405
+ if (removed.length > 0) {
406
+ console.log(`\u2713 Removed ${removed.length} stamped path(s):`);
407
+ for (const path of removed)
408
+ console.log(` - ${path}`);
409
+ }
410
+ }
411
+ console.log(` \u2192 next ./buddy dev will boot without ${feature}.`);
412
+ process.exit(ExitCode.Success);
413
+ } catch (err) {
414
+ console.error(`\u2717 Failed to uninstall ${feature}:`, err);
415
+ process.exit(ExitCode.FatalError);
416
+ }
417
+ });
418
+ }
419
+ export function features(buddy) {
420
+ for (const feature of FEATURE_NAMES)
421
+ registerInstallPair(buddy, feature);
422
+ }