@stacksjs/actions 0.70.87 → 0.70.90

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 (159) hide show
  1. package/dist/action.js +78 -2
  2. package/dist/actions.js +2 -2
  3. package/dist/add.js +20 -2
  4. package/dist/ai/request-model-access.js +10 -2
  5. package/dist/auth/client.js +44 -3
  6. package/dist/auth/prune.js +35 -2
  7. package/dist/auth/setup.js +152 -36
  8. package/dist/auth/token.js +10 -2
  9. package/dist/blog-admin.d.ts +55 -0
  10. package/dist/blog-admin.js +156 -0
  11. package/dist/blog.d.ts +11 -0
  12. package/dist/blog.js +306 -56
  13. package/dist/build/cli.js +6 -2
  14. package/dist/build/component-libs.js +7 -2
  15. package/dist/build/core.js +55 -2
  16. package/dist/build/desktop.js +5 -2
  17. package/dist/build/docs.js +5 -2
  18. package/dist/build/frontend-static.js +123 -8
  19. package/dist/build/server.js +6 -2
  20. package/dist/build/stacks.js +8 -2
  21. package/dist/build/views.js +11 -2
  22. package/dist/build.js +64 -2
  23. package/dist/bump.js +162 -7
  24. package/dist/cdn/invalidate-cache.js +0 -1
  25. package/dist/changelog.js +27 -3
  26. package/dist/check/ports.js +16 -2
  27. package/dist/clean.js +4 -2
  28. package/dist/commit.js +12 -2
  29. package/dist/copy-types.js +10 -3
  30. package/dist/database/seed.js +26 -2
  31. package/dist/deploy/create-receipt-rule.js +36 -2
  32. package/dist/deploy/index.js +1318 -44
  33. package/dist/dev/api.js +94 -6
  34. package/dist/dev/components.js +7 -2
  35. package/dist/dev/dashboard-globals.js +89 -3
  36. package/dist/dev/dashboard-utils.js +380 -2
  37. package/dist/dev/dashboard.js +435 -8
  38. package/dist/dev/desktop.js +5 -2
  39. package/dist/dev/docs.js +24 -2
  40. package/dist/dev/index.js +29 -2
  41. package/dist/dev/migrate-watcher.js +74 -2
  42. package/dist/dev/system-tray.js +5 -2
  43. package/dist/dev/views.js +147 -2
  44. package/dist/discover-packages.js +32 -2
  45. package/dist/domains/add.js +61 -2
  46. package/dist/domains/purchase.js +66 -2
  47. package/dist/domains/remove.js +24 -2
  48. package/dist/examples.js +34 -2
  49. package/dist/find-projects.js +16 -2
  50. package/dist/fresh.js +17 -2
  51. package/dist/generate/action-types.js +9 -4
  52. package/dist/generate/component-meta.js +9 -3
  53. package/dist/generate/custom-cli-file.js +20 -3
  54. package/dist/generate/env-files.js +46 -6
  55. package/dist/generate/ide-helpers.js +8 -3
  56. package/dist/generate/index.js +158 -2
  57. package/dist/generate/lib-entries.js +16 -3
  58. package/dist/generate/pantry-file.js +46 -2
  59. package/dist/generate/symlink-core.js +2 -2
  60. package/dist/generate/types.js +3 -2
  61. package/dist/generate/vscode-custom-data.js +9 -3
  62. package/dist/generate/web-types.js +0 -1
  63. package/dist/helpers/component-meta.js +295 -2
  64. package/dist/helpers/index.js +1 -2
  65. package/dist/helpers/lib-entries.js +73 -2
  66. package/dist/helpers/package-json.js +45 -14
  67. package/dist/helpers/utils.js +105 -2
  68. package/dist/helpers/vscode-custom-data.js +30 -2
  69. package/dist/index.d.ts +13 -0
  70. package/dist/index.js +50 -2
  71. package/dist/key-generate.js +24 -2
  72. package/dist/lint/fix.js +4 -3
  73. package/dist/lint/index.js +4 -3
  74. package/dist/lint/lint.js +34 -2
  75. package/dist/make-command.js +64 -14
  76. package/dist/make-job.js +41 -16
  77. package/dist/make-policy.js +74 -26
  78. package/dist/make-resource.js +50 -21
  79. package/dist/make.js +328 -2
  80. package/dist/migrate/database.js +16 -2
  81. package/dist/migrate/fresh.js +31 -2
  82. package/dist/orm/base.js +0 -2
  83. package/dist/orm/test.js +0 -1
  84. package/dist/prepublish.js +16 -2
  85. package/dist/queue/clear.js +42 -2
  86. package/dist/queue/dlq-purge.js +28 -2
  87. package/dist/queue/dlq-retry.js +35 -2
  88. package/dist/queue/dlq.js +55 -5
  89. package/dist/queue/failed.js +69 -5
  90. package/dist/queue/flush.js +49 -3
  91. package/dist/queue/inspect.js +154 -8
  92. package/dist/queue/list.js +69 -5
  93. package/dist/queue/monitor.js +94 -6
  94. package/dist/queue/pause.js +28 -2
  95. package/dist/queue/quarantine.js +42 -3
  96. package/dist/queue/resume.js +27 -2
  97. package/dist/queue/retry.js +41 -2
  98. package/dist/queue/schedule-list.js +33 -5
  99. package/dist/queue/schedule.js +12 -2
  100. package/dist/queue/status.js +68 -5
  101. package/dist/queue/table.js +10 -2
  102. package/dist/queue/unquarantine.js +27 -2
  103. package/dist/queue/work.js +57 -2
  104. package/dist/release.js +21 -3
  105. package/dist/route/list.js +78 -3
  106. package/dist/run-action.js +1 -2
  107. package/dist/saas/setup.js +9 -2
  108. package/dist/scaffold-crud.js +164 -40
  109. package/dist/schedule/run.js +7 -2
  110. package/dist/search/flush.js +10 -2
  111. package/dist/search/import.js +10 -2
  112. package/dist/search/settings-list.js +10 -2
  113. package/dist/search/settings.js +9 -2
  114. package/dist/serve/api.js +35 -2
  115. package/dist/setup/index.js +1 -2
  116. package/dist/setup/ssl.js +271 -2
  117. package/dist/stacks.js +265 -2
  118. package/dist/templates.js +301 -2
  119. package/dist/test/feature.js +11 -2
  120. package/dist/test/index.js +11 -2
  121. package/dist/test/ui.js +4 -2
  122. package/dist/test/unit.js +11 -2
  123. package/dist/typecheck.js +4 -2
  124. package/dist/types.js +16 -2
  125. package/dist/upgrade/binary.js +29 -2
  126. package/dist/upgrade/bun.js +0 -1
  127. package/dist/upgrade/dependencies.js +0 -2
  128. package/dist/upgrade/framework-utils.js +193 -2
  129. package/dist/upgrade/framework.js +310 -7
  130. package/dist/upgrade/index.js +17 -2
  131. package/dist/upgrade/packages.js +99 -12
  132. package/dist/upgrade/shell.js +40 -4
  133. package/dist/upgrade.js +38 -2
  134. package/package.json +16 -16
  135. package/dist/chunk-0c8sd1t6.js +0 -2
  136. package/dist/chunk-0g2nzhx9.js +0 -240
  137. package/dist/chunk-0gadhvn7.js +0 -3
  138. package/dist/chunk-0h27aa4r.js +0 -93
  139. package/dist/chunk-3peh8kqs.js +0 -851
  140. package/dist/chunk-62gdfav8.js +0 -3
  141. package/dist/chunk-6bk53y02.js +0 -3
  142. package/dist/chunk-78c5n3s2.js +0 -3
  143. package/dist/chunk-c74qv38e.js +0 -17
  144. package/dist/chunk-cdk2r2jd.js +0 -1
  145. package/dist/chunk-dbz9dp2h.js +0 -3
  146. package/dist/chunk-e142dheq.js +0 -3
  147. package/dist/chunk-ec4gky40.js +0 -3
  148. package/dist/chunk-h3d81wrg.js +0 -3
  149. package/dist/chunk-m3q0x4h0.js +0 -688
  150. package/dist/chunk-mqgbyhph.js +0 -5
  151. package/dist/chunk-n24m8prb.js +0 -3
  152. package/dist/chunk-n5eqwhck.js +0 -7
  153. package/dist/chunk-nsvnqv8y.js +0 -5
  154. package/dist/chunk-nty6mmx4.js +0 -283
  155. package/dist/chunk-s735m9mg.js +0 -1
  156. package/dist/chunk-tpb8jgsk.js +0 -7
  157. package/dist/chunk-tsbk5vne.js +0 -3
  158. package/dist/chunk-xg9xscqz.js +0 -1
  159. package/dist/chunk-y6e448he.js +0 -7
package/dist/setup/ssl.js CHANGED
@@ -1,2 +1,271 @@
1
- // @bun
2
- import{Fa as a,Ga as b,Ha as c,Ia as d,Ja as e,Ka as f,La as g,Ma as h}from"../chunk-n5eqwhck.js";import"../chunk-h3d81wrg.js";export{e as trustCertificate,c as sslCertificatesExist,g as setupSSL,h as isSSLSetupComplete,a as isDomainInHosts,d as isCertificateTrusted,f as generateCertificates,b as addDomainToHosts};
1
+ import { execSync, spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { log } from "@stacksjs/cli";
7
+ const HOSTS_FILE = process.platform === "win32" ? path.join(process.env.windir || "C:\\Windows", "System32", "drivers", "etc", "hosts") : "/etc/hosts", SSL_DIR = path.join(os.homedir(), ".stacks", "ssl");
8
+ function isInteractiveTerminal() {
9
+ return process.stdin.isTTY === !0 && process.stdout.isTTY === !0;
10
+ }
11
+ function getSudoPassword() {
12
+ return process.env.SUDO_PASSWORD;
13
+ }
14
+ function canRunSudo() {
15
+ return isInteractiveTerminal() || !!getSudoPassword();
16
+ }
17
+ export function isDomainInHosts(domain) {
18
+ try {
19
+ const hostsContent = fs.readFileSync(HOSTS_FILE, "utf-8");
20
+ return hostsContent.includes(`127.0.0.1 ${domain}`) || hostsContent.includes(`::1 ${domain}`);
21
+ } catch {
22
+ if (process.platform !== "win32") {
23
+ const sudoPassword = getSudoPassword();
24
+ if (sudoPassword)
25
+ try {
26
+ const result = execSync(`echo '${sudoPassword}' | sudo -S cat "${HOSTS_FILE}" 2>/dev/null`, { encoding: "utf-8" });
27
+ return result.includes(`127.0.0.1 ${domain}`) || result.includes(`::1 ${domain}`);
28
+ } catch {
29
+ return !1;
30
+ }
31
+ }
32
+ return !1;
33
+ }
34
+ }
35
+ export async function addDomainToHosts(domain, verbose) {
36
+ if (isDomainInHosts(domain)) {
37
+ if (verbose)
38
+ log.info(`${domain} already in hosts file`);
39
+ return !0;
40
+ }
41
+ const hostsEntry = `
42
+ # Added by Stacks
43
+ 127.0.0.1 ${domain}
44
+ ::1 ${domain}
45
+ `;
46
+ if (process.platform === "win32") {
47
+ log.warn("Please add this to your hosts file manually:");
48
+ log.warn(`127.0.0.1 ${domain}`);
49
+ log.warn(`::1 ${domain}`);
50
+ log.warn(`File location: ${HOSTS_FILE}`);
51
+ return !1;
52
+ }
53
+ if (!canRunSudo()) {
54
+ if (verbose) {
55
+ log.warn(`Cannot add ${domain} to hosts file (non-interactive mode and no SUDO_PASSWORD set)`);
56
+ log.info("Run `buddy setup:ssl` in a terminal or set SUDO_PASSWORD environment variable");
57
+ }
58
+ return !1;
59
+ }
60
+ const sudoPassword = getSudoPassword();
61
+ return new Promise((resolve) => {
62
+ let child;
63
+ if (sudoPassword)
64
+ child = spawn("sh", ["-c", `echo '${sudoPassword}' | sudo -S sh -c "echo '${hostsEntry}' >> '${HOSTS_FILE}'"`], {
65
+ stdio: ["pipe", "inherit", "inherit"]
66
+ });
67
+ else
68
+ child = spawn("sudo", ["sh", "-c", `echo '${hostsEntry}' >> "${HOSTS_FILE}"`], {
69
+ stdio: "inherit"
70
+ });
71
+ child.on("close", (code) => {
72
+ if (code === 0) {
73
+ if (verbose)
74
+ log.success(`Added ${domain} to hosts file`);
75
+ resolve(!0);
76
+ } else {
77
+ if (verbose) {
78
+ log.error(`Failed to add ${domain} to hosts file`);
79
+ log.warn(`Manually add: 127.0.0.1 ${domain}`);
80
+ }
81
+ resolve(!1);
82
+ }
83
+ });
84
+ child.on("error", () => {
85
+ log.error("Failed to run sudo command");
86
+ resolve(!1);
87
+ });
88
+ });
89
+ }
90
+ export function sslCertificatesExist(domain) {
91
+ const certPath = path.join(SSL_DIR, `${domain}.crt`), keyPath = path.join(SSL_DIR, `${domain}.key`), caPath = path.join(SSL_DIR, `${domain}.ca.crt`);
92
+ return fs.existsSync(certPath) && fs.existsSync(keyPath) && fs.existsSync(caPath);
93
+ }
94
+ export function isCertificateTrusted(domain) {
95
+ if (process.platform !== "darwin")
96
+ return !0;
97
+ const certPath = path.join(SSL_DIR, `${domain}.crt`);
98
+ if (!fs.existsSync(certPath))
99
+ return !1;
100
+ try {
101
+ const fingerprint = execSync(`openssl x509 -noout -fingerprint -sha256 -in "${certPath}"`, { encoding: "utf-8" }), eqIdx = fingerprint.indexOf("="), fingerprintValue = eqIdx >= 0 ? fingerprint.slice(eqIdx + 1).trim() : "";
102
+ if (!fingerprintValue)
103
+ return !1;
104
+ return execSync("security find-certificate -a -Z -p | openssl x509 -noout -fingerprint -sha256 2>/dev/null || true", { encoding: "utf-8" }).includes(fingerprintValue);
105
+ } catch {
106
+ return !1;
107
+ }
108
+ }
109
+ export async function trustCertificate(domain, verbose) {
110
+ const caCertPath = path.join(SSL_DIR, `${domain}.ca.crt`), certPath = path.join(SSL_DIR, `${domain}.crt`);
111
+ if (!fs.existsSync(caCertPath)) {
112
+ log.error(`CA certificate not found: ${caCertPath}`);
113
+ return !1;
114
+ }
115
+ if (isCertificateTrusted(domain)) {
116
+ if (verbose)
117
+ log.info("Certificate is already trusted, skipping trust step");
118
+ return !0;
119
+ }
120
+ if (!canRunSudo()) {
121
+ if (verbose) {
122
+ log.warn("Cannot trust certificate (non-interactive mode and no SUDO_PASSWORD set)");
123
+ log.info("Run `buddy setup:ssl` in a terminal or set SUDO_PASSWORD environment variable");
124
+ }
125
+ return !1;
126
+ }
127
+ const sudoPassword = getSudoPassword();
128
+ if (process.platform === "darwin")
129
+ return new Promise((resolve) => {
130
+ const combinedCommand = `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain '${caCertPath}' && security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain '${certPath}'`;
131
+ let child;
132
+ if (sudoPassword)
133
+ child = spawn("sh", ["-c", `echo '${sudoPassword}' | sudo -S sh -c "${combinedCommand}"`], {
134
+ stdio: ["pipe", "inherit", "inherit"]
135
+ });
136
+ else
137
+ child = spawn("sudo", ["sh", "-c", combinedCommand], {
138
+ stdio: "inherit"
139
+ });
140
+ child.on("close", (code) => {
141
+ if (code === 0) {
142
+ try {
143
+ execSync(`security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db "${certPath}" 2>/dev/null || true`);
144
+ } catch {}
145
+ resolve(!0);
146
+ } else {
147
+ if (verbose) {
148
+ log.warn("Could not trust certificate automatically");
149
+ log.info(` sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${caCertPath}"`);
150
+ }
151
+ resolve(!1);
152
+ }
153
+ });
154
+ child.on("error", () => {
155
+ log.error("Failed to run sudo command");
156
+ resolve(!1);
157
+ });
158
+ });
159
+ else if (process.platform === "linux")
160
+ return new Promise((resolve) => {
161
+ const linuxCommands = `mkdir -p /usr/local/share/ca-certificates/stacks && cp '${caCertPath}' /usr/local/share/ca-certificates/stacks/ && update-ca-certificates`;
162
+ let child;
163
+ if (sudoPassword)
164
+ child = spawn("sh", ["-c", `echo '${sudoPassword}' | sudo -S sh -c "${linuxCommands}"`], {
165
+ stdio: ["pipe", "inherit", "inherit"]
166
+ });
167
+ else
168
+ child = spawn("sudo", ["sh", "-c", linuxCommands], {
169
+ stdio: "inherit"
170
+ });
171
+ child.on("close", (code) => {
172
+ if (code === 0) {
173
+ log.success("Certificate trusted");
174
+ resolve(!0);
175
+ } else {
176
+ log.warn("Could not trust certificate automatically");
177
+ resolve(!1);
178
+ }
179
+ });
180
+ child.on("error", () => resolve(!1));
181
+ });
182
+ else if (process.platform === "win32") {
183
+ log.warn("Please manually trust the certificate in Windows:");
184
+ log.warn(` 1. Double-click: ${caCertPath}`);
185
+ log.warn(' 2. Click "Install Certificate"');
186
+ log.warn(' 3. Select "Local Machine" and place in "Trusted Root Certification Authorities"');
187
+ return !1;
188
+ }
189
+ return !1;
190
+ }
191
+ export async function generateCertificates(domain, verbose) {
192
+ try {
193
+ const tlsxModule = await import("@stacksjs/tlsx"), { generateCertificate, createRootCA } = tlsxModule, _httpsConfig = tlsxModule.httpsConfig;
194
+ if (!fs.existsSync(SSL_DIR))
195
+ fs.mkdirSync(SSL_DIR, { recursive: !0 });
196
+ const config = {
197
+ domain,
198
+ hostCertCN: domain,
199
+ basePath: SSL_DIR,
200
+ caCertPath: path.join(SSL_DIR, `${domain}.ca.crt`),
201
+ certPath: path.join(SSL_DIR, `${domain}.crt`),
202
+ keyPath: path.join(SSL_DIR, `${domain}.key`),
203
+ altNameIPs: ["127.0.0.1", "::1"],
204
+ altNameURIs: ["localhost", domain],
205
+ organizationName: "Stacks Development",
206
+ countryName: "US",
207
+ stateName: "California",
208
+ localityName: "Playa Vista",
209
+ commonName: domain,
210
+ validityDays: 825,
211
+ verbose: verbose || !1,
212
+ subjectAltNames: [
213
+ { type: 2, value: domain },
214
+ { type: 2, value: `*.${domain.split(".").slice(1).join(".")}` },
215
+ { type: 2, value: "localhost" },
216
+ { type: 2, value: "*.localhost" }
217
+ ]
218
+ };
219
+ if (verbose)
220
+ log.info("Generating Root CA certificate...");
221
+ const caCert = await createRootCA(config);
222
+ if (verbose)
223
+ log.info(`Generating host certificate for ${domain}...`);
224
+ const hostCert = await generateCertificate({
225
+ ...config,
226
+ rootCA: {
227
+ certificate: caCert.certificate,
228
+ privateKey: caCert.privateKey
229
+ }
230
+ });
231
+ fs.writeFileSync(config.caCertPath, caCert.certificate);
232
+ fs.writeFileSync(config.certPath, hostCert.certificate);
233
+ fs.writeFileSync(config.keyPath, hostCert.privateKey);
234
+ if (verbose)
235
+ log.success(`SSL certificates generated for ${domain}`);
236
+ return !0;
237
+ } catch (error) {
238
+ log.error(`Failed to generate certificates: ${error}`);
239
+ return !1;
240
+ }
241
+ }
242
+ export async function setupSSL(options = {}) {
243
+ const domain = options.domain || process.env.APP_URL?.replace(/^https?:\/\//, "") || "stacks.localhost";
244
+ if (domain === "localhost" || domain.startsWith("localhost:")) {
245
+ log.info("Skipping SSL setup for localhost");
246
+ return !0;
247
+ }
248
+ let success = !0;
249
+ if (!options.skipHosts) {
250
+ if (!await addDomainToHosts(domain, options.verbose) && options.verbose)
251
+ log.warn("Hosts file not updated - you may need to add the entry manually");
252
+ }
253
+ if (!sslCertificatesExist(domain)) {
254
+ if (options.verbose)
255
+ log.info("Generating SSL certificates...");
256
+ if (!await generateCertificates(domain, options.verbose)) {
257
+ log.error("Failed to generate SSL certificates");
258
+ return !1;
259
+ }
260
+ }
261
+ if (!options.skipTrust && !isCertificateTrusted(domain)) {
262
+ if (!await trustCertificate(domain, options.verbose)) {
263
+ log.warn("Certificate not trusted - you may see browser warnings");
264
+ success = !1;
265
+ }
266
+ }
267
+ return success;
268
+ }
269
+ export function isSSLSetupComplete(domain) {
270
+ return isDomainInHosts(domain) && sslCertificatesExist(domain);
271
+ }
package/dist/stacks.js CHANGED
@@ -1,2 +1,265 @@
1
- // @bun
2
- import"./chunk-h3d81wrg.js";import{log as H}from"@stacksjs/logging";import{dirname as T,join as Z,projectPath as A,relative as m,stacksBackupPath as u,stacksLockPath as b}from"@stacksjs/path";import{copyFile as K,createFolder as R,deleteFile as g,deleteEmptyFolders as d,doesFolderExist as P,fs as J}from"@stacksjs/storage";var F=["app","config","database","resources","routes","public","locales"];async function L(){let z=b();try{if(J.existsSync(z)){let q=await Bun.file(z).text();return JSON.parse(q)}}catch{}return{version:1,generatedAt:new Date().toISOString(),stacks:{}}}async function D(z){let q=b(),W=T(q);if(!J.existsSync(W))await R(W);z.generatedAt=new Date().toISOString(),await Bun.write(q,JSON.stringify(z,null,2))}function y(z){let q=A("pantry"),W=Z(q,z);if(J.existsSync(Z(W,"package.json")))return W;let X=Z(q,"@stacksjs",z);if(J.existsSync(Z(X,"package.json")))return X;if(z.startsWith("@")){let Q=Z(q,z);if(J.existsSync(Z(Q,"package.json")))return Q}let M=["*/package.json","@*/*/package.json"];for(let Q of M){let Y=new Bun.Glob(Q);for(let $ of Y.scanSync({cwd:q,absolute:!0,onlyFiles:!0}))try{if(JSON.parse(J.readFileSync($,"utf-8"))?.stacks?.name===z)return T($)}catch{}}return null}function C(z,q){let W=[];if(!J.existsSync(z))return W;let X=J.readdirSync(z,{withFileTypes:!0});for(let M of X){let Q=Z(z,M.name);if(M.isDirectory())W.push(...C(Q,q));else W.push(m(q,Q))}return W}async function n(z){let{name:q,force:W=!1,dryRun:X=!1,verbose:M=!1}=z,Q=W?"overwrite":z.conflict??"skip";H.info(`Installing stack "${q}"...`);let Y=y(q);if(!Y)return H.error(`Stack "${q}" not found in pantry. Install it first with: bun add <package-name>`),null;let $,G;try{let V=await Bun.file(Z(Y,"package.json")).text();if($=JSON.parse(V),G=$.stacks,!G?.name)return H.error(`Package at "${Y}" does not have a valid "stacks" field in package.json`),null}catch(V){return H.error(`Failed to read stack package.json: ${V}`),null}let O=await L(),U=$.name||q;if(O.stacks[U]&&!W)return H.warn(`Stack "${G.name}" is already installed. Use --force to reinstall.`),O.stacks[U];let B=G.directories??F,w=[];for(let V of B){let E=Z(Y,V);if(J.existsSync(E)){let _=C(E,Y);w.push(..._)}}if(w.length===0)return H.warn(`Stack "${G.name}" has no files to install`),null;if(M)H.info(`Found ${w.length} file(s) to install`);let x=[],I=[];for(let V of w){let E=Z(Y,V),_=A(V);if(X){if(J.existsSync(_))if(H.info(`[dry-run] Would ${Q}: ${V}`),Q==="skip")I.push(V);else x.push(V);else H.info(`[dry-run] Would copy: ${V}`),x.push(V);continue}if(J.existsSync(_))switch(Q){case"skip":if(M)H.warn(`Skipping (file exists): ${V}`);I.push(V);continue;case"backup":{let f=u(G.name),v=Z(f,V),j=T(v);if(!J.existsSync(j))await R(j);if(K(_,v),M)H.info(`Backed up: ${V}`);break}case"overwrite":if(M)H.warn(`Overwriting: ${V}`);break}let S=T(_);if(!J.existsSync(S))await R(S);if(K(E,_),x.push(V),M)H.info(`Copied: ${V}`)}if(X)return H.info(`[dry-run] Would install ${x.length} file(s), skip ${I.length} file(s)`),null;let N={packageName:U,name:G.name,version:$.version||"0.0.0",installedAt:new Date().toISOString(),files:x,skipped:I,conflictStrategy:Q};return O.stacks[U]=N,await D(O),H.success(`Installed stack "${G.name}" (${x.length} file(s) copied, ${I.length} skipped)`),N}async function c(z){let{name:q,force:W=!1,verbose:X=!1}=z;H.info(`Uninstalling stack "${q}"...`);let M=await L(),Q=null,Y=null;for(let[O,U]of Object.entries(M.stacks))if(U.name===q||O===q){Q=O,Y=U;break}if(!Y||!Q)return H.error(`Stack "${q}" is not installed`),!1;let $=0;for(let O of Y.files){let U=A(O);if(!J.existsSync(U)){if(X)H.info(`Already removed: ${O}`);continue}if(!W){let B=y(q);if(B){let w=Z(B,O);if(J.existsSync(w)){let x=await Bun.file(w).text(),I=await Bun.file(U).text();if(x!==I){H.warn(`Skipping modified file: ${O} (use --force to remove)`);continue}}}}if(await g(U),$++,X)H.info(`Removed: ${O}`)}let G=u(Y.name);if(J.existsSync(G)&&P(G)){let O=C(G,G);for(let U of O){let B=Z(G,U),w=A(U),x=T(w);if(!J.existsSync(x))await R(x);if(K(B,w),X)H.info(`Restored from backup: ${U}`)}J.rmSync(G,{recursive:!0,force:!0})}for(let O of F){let U=A(O);if(J.existsSync(U))await d(U)}return delete M.stacks[Q],await D(M),H.success(`Uninstalled stack "${Y.name}" (${$} file(s) removed)`),!0}async function i(){let z=[],q=await L(),W=A("pantry");if(J.existsSync(W)){let X=["*/package.json","@*/*/package.json"];for(let M of X){let Q=new Bun.Glob(M);for(let Y of Q.scanSync({cwd:W,absolute:!0,onlyFiles:!0}))try{let $=J.readFileSync(Y,"utf-8"),G=JSON.parse($);if(!G?.stacks?.name)continue;let O=G.name,U=!!q.stacks[O],B=q.stacks[O];z.push({name:G.stacks.name,packageName:O,version:G.version||"0.0.0",description:G.stacks.description,installed:U,installedAt:B?.installedAt,fileCount:B?.files.length})}catch{}}}for(let[X,M]of Object.entries(q.stacks))if(!z.some((Q)=>Q.packageName===X))z.push({name:M.name,packageName:X,version:M.version,installed:!0,installedAt:M.installedAt,fileCount:M.files.length});return z}export{c as uninstallStack,i as listStacks,n as installStack};
1
+ import { log } from "@stacksjs/logging";
2
+ import { dirname, join, projectPath, relative, stacksBackupPath, stacksLockPath } from "@stacksjs/path";
3
+ import { copyFile, createFolder, deleteFile, deleteEmptyFolders, doesFolderExist, fs } from "@stacksjs/storage";
4
+ const STACK_DIRS = ["app", "config", "database", "resources", "routes", "public", "locales"];
5
+ async function readStackLock() {
6
+ const lockPath = stacksLockPath();
7
+ try {
8
+ if (fs.existsSync(lockPath)) {
9
+ const content = await Bun.file(lockPath).text();
10
+ return JSON.parse(content);
11
+ }
12
+ } catch {}
13
+ return {
14
+ version: 1,
15
+ generatedAt: new Date().toISOString(),
16
+ stacks: {}
17
+ };
18
+ }
19
+ async function writeStackLock(lock) {
20
+ const lockPath = stacksLockPath(), lockDir = dirname(lockPath);
21
+ if (!fs.existsSync(lockDir))
22
+ await createFolder(lockDir);
23
+ lock.generatedAt = new Date().toISOString();
24
+ await Bun.write(lockPath, JSON.stringify(lock, null, 2));
25
+ }
26
+ function resolveStackPackagePath(name) {
27
+ const pantryDir = projectPath("pantry"), directPath = join(pantryDir, name);
28
+ if (fs.existsSync(join(directPath, "package.json")))
29
+ return directPath;
30
+ const scopedPath = join(pantryDir, "@stacksjs", name);
31
+ if (fs.existsSync(join(scopedPath, "package.json")))
32
+ return scopedPath;
33
+ if (name.startsWith("@")) {
34
+ const fullPath = join(pantryDir, name);
35
+ if (fs.existsSync(join(fullPath, "package.json")))
36
+ return fullPath;
37
+ }
38
+ const patterns = ["*/package.json", "@*/*/package.json"];
39
+ for (const pattern of patterns) {
40
+ const glob = new Bun.Glob(pattern);
41
+ for (const file of glob.scanSync({ cwd: pantryDir, absolute: !0, onlyFiles: !0 }))
42
+ try {
43
+ if (JSON.parse(fs.readFileSync(file, "utf-8"))?.stacks?.name === name)
44
+ return dirname(file);
45
+ } catch {}
46
+ }
47
+ return null;
48
+ }
49
+ function collectFiles(dir, baseDir) {
50
+ const files = [];
51
+ if (!fs.existsSync(dir))
52
+ return files;
53
+ const entries = fs.readdirSync(dir, { withFileTypes: !0 });
54
+ for (const entry of entries) {
55
+ const fullPath = join(dir, entry.name);
56
+ if (entry.isDirectory())
57
+ files.push(...collectFiles(fullPath, baseDir));
58
+ else
59
+ files.push(relative(baseDir, fullPath));
60
+ }
61
+ return files;
62
+ }
63
+ export async function installStack(options) {
64
+ const { name, force = !1, dryRun = !1, verbose = !1 } = options, conflict = force ? "overwrite" : options.conflict ?? "skip";
65
+ log.info(`Installing stack "${name}"...`);
66
+ const stackPath = resolveStackPackagePath(name);
67
+ if (!stackPath) {
68
+ log.error(`Stack "${name}" not found in pantry. Install it first with: bun add <package-name>`);
69
+ return null;
70
+ }
71
+ let pkg, meta;
72
+ try {
73
+ const pkgContent = await Bun.file(join(stackPath, "package.json")).text();
74
+ pkg = JSON.parse(pkgContent);
75
+ meta = pkg.stacks;
76
+ if (!meta?.name) {
77
+ log.error(`Package at "${stackPath}" does not have a valid "stacks" field in package.json`);
78
+ return null;
79
+ }
80
+ } catch (error) {
81
+ log.error(`Failed to read stack package.json: ${error}`);
82
+ return null;
83
+ }
84
+ const lock = await readStackLock(), packageName = pkg.name || name;
85
+ if (lock.stacks[packageName] && !force) {
86
+ log.warn(`Stack "${meta.name}" is already installed. Use --force to reinstall.`);
87
+ return lock.stacks[packageName];
88
+ }
89
+ const dirsToScan = meta.directories ?? STACK_DIRS, allFiles = [];
90
+ for (const dir of dirsToScan) {
91
+ const dirPath = join(stackPath, dir);
92
+ if (fs.existsSync(dirPath)) {
93
+ const files = collectFiles(dirPath, stackPath);
94
+ allFiles.push(...files);
95
+ }
96
+ }
97
+ if (allFiles.length === 0) {
98
+ log.warn(`Stack "${meta.name}" has no files to install`);
99
+ return null;
100
+ }
101
+ if (verbose)
102
+ log.info(`Found ${allFiles.length} file(s) to install`);
103
+ const copiedFiles = [], skippedFiles = [];
104
+ for (const relPath of allFiles) {
105
+ const src = join(stackPath, relPath), dest = projectPath(relPath);
106
+ if (dryRun) {
107
+ if (fs.existsSync(dest)) {
108
+ log.info(`[dry-run] Would ${conflict}: ${relPath}`);
109
+ if (conflict === "skip")
110
+ skippedFiles.push(relPath);
111
+ else
112
+ copiedFiles.push(relPath);
113
+ } else {
114
+ log.info(`[dry-run] Would copy: ${relPath}`);
115
+ copiedFiles.push(relPath);
116
+ }
117
+ continue;
118
+ }
119
+ if (fs.existsSync(dest))
120
+ switch (conflict) {
121
+ case "skip":
122
+ if (verbose)
123
+ log.warn(`Skipping (file exists): ${relPath}`);
124
+ skippedFiles.push(relPath);
125
+ continue;
126
+ case "backup": {
127
+ const backupDir = stacksBackupPath(meta.name), backupDest = join(backupDir, relPath), backupDestDir = dirname(backupDest);
128
+ if (!fs.existsSync(backupDestDir))
129
+ await createFolder(backupDestDir);
130
+ copyFile(dest, backupDest);
131
+ if (verbose)
132
+ log.info(`Backed up: ${relPath}`);
133
+ break;
134
+ }
135
+ case "overwrite":
136
+ if (verbose)
137
+ log.warn(`Overwriting: ${relPath}`);
138
+ break;
139
+ }
140
+ const destDir = dirname(dest);
141
+ if (!fs.existsSync(destDir))
142
+ await createFolder(destDir);
143
+ copyFile(src, dest);
144
+ copiedFiles.push(relPath);
145
+ if (verbose)
146
+ log.info(`Copied: ${relPath}`);
147
+ }
148
+ if (dryRun) {
149
+ log.info(`[dry-run] Would install ${copiedFiles.length} file(s), skip ${skippedFiles.length} file(s)`);
150
+ return null;
151
+ }
152
+ const entry = {
153
+ packageName,
154
+ name: meta.name,
155
+ version: pkg.version || "0.0.0",
156
+ installedAt: new Date().toISOString(),
157
+ files: copiedFiles,
158
+ skipped: skippedFiles,
159
+ conflictStrategy: conflict
160
+ };
161
+ lock.stacks[packageName] = entry;
162
+ await writeStackLock(lock);
163
+ log.success(`Installed stack "${meta.name}" (${copiedFiles.length} file(s) copied, ${skippedFiles.length} skipped)`);
164
+ return entry;
165
+ }
166
+ export async function uninstallStack(options) {
167
+ const { name, force = !1, verbose = !1 } = options;
168
+ log.info(`Uninstalling stack "${name}"...`);
169
+ const lock = await readStackLock();
170
+ let entryKey = null, entry = null;
171
+ for (const [key, value] of Object.entries(lock.stacks))
172
+ if (value.name === name || key === name) {
173
+ entryKey = key;
174
+ entry = value;
175
+ break;
176
+ }
177
+ if (!entry || !entryKey) {
178
+ log.error(`Stack "${name}" is not installed`);
179
+ return !1;
180
+ }
181
+ let removedCount = 0;
182
+ for (const relPath of entry.files) {
183
+ const filePath = projectPath(relPath);
184
+ if (!fs.existsSync(filePath)) {
185
+ if (verbose)
186
+ log.info(`Already removed: ${relPath}`);
187
+ continue;
188
+ }
189
+ if (!force) {
190
+ const stackPath = resolveStackPackagePath(name);
191
+ if (stackPath) {
192
+ const srcPath = join(stackPath, relPath);
193
+ if (fs.existsSync(srcPath)) {
194
+ const srcContent = await Bun.file(srcPath).text(), destContent = await Bun.file(filePath).text();
195
+ if (srcContent !== destContent) {
196
+ log.warn(`Skipping modified file: ${relPath} (use --force to remove)`);
197
+ continue;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ await deleteFile(filePath);
203
+ removedCount++;
204
+ if (verbose)
205
+ log.info(`Removed: ${relPath}`);
206
+ }
207
+ const backupDir = stacksBackupPath(entry.name);
208
+ if (fs.existsSync(backupDir) && doesFolderExist(backupDir)) {
209
+ const backupFiles = collectFiles(backupDir, backupDir);
210
+ for (const relPath of backupFiles) {
211
+ const backupSrc = join(backupDir, relPath), dest = projectPath(relPath), destDir = dirname(dest);
212
+ if (!fs.existsSync(destDir))
213
+ await createFolder(destDir);
214
+ copyFile(backupSrc, dest);
215
+ if (verbose)
216
+ log.info(`Restored from backup: ${relPath}`);
217
+ }
218
+ fs.rmSync(backupDir, { recursive: !0, force: !0 });
219
+ }
220
+ for (const dir of STACK_DIRS) {
221
+ const dirPath = projectPath(dir);
222
+ if (fs.existsSync(dirPath))
223
+ await deleteEmptyFolders(dirPath);
224
+ }
225
+ delete lock.stacks[entryKey];
226
+ await writeStackLock(lock);
227
+ log.success(`Uninstalled stack "${entry.name}" (${removedCount} file(s) removed)`);
228
+ return !0;
229
+ }
230
+ export async function listStacks() {
231
+ const entries = [], lock = await readStackLock(), pantryDir = projectPath("pantry");
232
+ if (fs.existsSync(pantryDir)) {
233
+ const patterns = ["*/package.json", "@*/*/package.json"];
234
+ for (const pattern of patterns) {
235
+ const glob = new Bun.Glob(pattern);
236
+ for (const file of glob.scanSync({ cwd: pantryDir, absolute: !0, onlyFiles: !0 }))
237
+ try {
238
+ const pkgContent = fs.readFileSync(file, "utf-8"), pkg = JSON.parse(pkgContent);
239
+ if (!pkg?.stacks?.name)
240
+ continue;
241
+ const packageName = pkg.name, installed = !!lock.stacks[packageName], lockEntry = lock.stacks[packageName];
242
+ entries.push({
243
+ name: pkg.stacks.name,
244
+ packageName,
245
+ version: pkg.version || "0.0.0",
246
+ description: pkg.stacks.description,
247
+ installed,
248
+ installedAt: lockEntry?.installedAt,
249
+ fileCount: lockEntry?.files.length
250
+ });
251
+ } catch {}
252
+ }
253
+ }
254
+ for (const [packageName, entry] of Object.entries(lock.stacks))
255
+ if (!entries.some((e) => e.packageName === packageName))
256
+ entries.push({
257
+ name: entry.name,
258
+ packageName,
259
+ version: entry.version,
260
+ installed: !0,
261
+ installedAt: entry.installedAt,
262
+ fileCount: entry.files.length
263
+ });
264
+ return entries;
265
+ }