@usequark/quark-create-app 1.23.11

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 (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. package/templates/worker/src/lib/tokens.js +81 -0
@@ -0,0 +1,479 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const args = new Set(process.argv.slice(2));
9
+ const CHECK_MODE = args.has("--check");
10
+ const DEEP_MODE = args.has("--deep");
11
+ const SKIP_TASK_CLEAN = args.has("--skip-task-clean");
12
+ const DAY_MS = 24 * 60 * 60 * 1000;
13
+ const AUTO_CLEAN_INTERVAL_MS = DAY_MS;
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ export const ROOT = path.resolve(__dirname, "..");
17
+ export const AUTO_CLEAN_STATE_FILE = ".quark-auto-clean.json";
18
+ export const TARGET_DIR_NAMES = new Set([
19
+ ".turbo",
20
+ ".next",
21
+ "build",
22
+ "coverage",
23
+ "dist",
24
+ ]);
25
+ export const PROTECTED_PREFIXES = ["packages/cli/templates"];
26
+ export const DEEP_ONLY_ROOT_DIRS = ["tmp-npm-smoke", "tmp-test-project"];
27
+ export const AUTO_CLEAN_POLICIES = {
28
+ coverage: { ageSource: "mtimeMs", minAgeMs: 3 * DAY_MS },
29
+ build: { ageSource: "mtimeMs", minAgeMs: 3 * DAY_MS },
30
+ dist: { ageSource: "mtimeMs", minAgeMs: 3 * DAY_MS },
31
+ "tmp-test-project": { ageSource: "mtimeMs", minAgeMs: 3 * DAY_MS },
32
+ "tmp-npm-smoke": { ageSource: "mtimeMs", minAgeMs: 3 * DAY_MS },
33
+ ".next": {
34
+ ageSource: "birthtimeMs",
35
+ minAgeMs: 7 * DAY_MS,
36
+ minSizeBytes: 200 * 1024 * 1024,
37
+ },
38
+ ".turbo": {
39
+ ageSource: "birthtimeMs",
40
+ minAgeMs: 7 * DAY_MS,
41
+ minSizeBytes: 1024 * 1024 * 1024,
42
+ },
43
+ };
44
+
45
+ function toPosixPath(value) {
46
+ return value.split(path.sep).join("/");
47
+ }
48
+
49
+ function toRelativePath(targetPath) {
50
+ return toPosixPath(path.relative(ROOT, targetPath));
51
+ }
52
+
53
+ function pathExists(targetPath) {
54
+ return fs.existsSync(targetPath);
55
+ }
56
+
57
+ function isProtectedPath(relativePath) {
58
+ if (!relativePath || relativePath === ".") {
59
+ return false;
60
+ }
61
+
62
+ return PROTECTED_PREFIXES.some(
63
+ (prefix) =>
64
+ relativePath === prefix || relativePath.startsWith(`${prefix}/`),
65
+ );
66
+ }
67
+
68
+ function shouldSkipTraversal(entryName, relativePath) {
69
+ if (entryName === ".git" || entryName === "node_modules") {
70
+ return true;
71
+ }
72
+
73
+ return (
74
+ DEEP_ONLY_ROOT_DIRS.includes(relativePath) || isProtectedPath(relativePath)
75
+ );
76
+ }
77
+
78
+ function collectTargetPaths(currentDir = ROOT, targets = []) {
79
+ let entries = [];
80
+ try {
81
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
82
+ } catch {
83
+ return targets;
84
+ }
85
+
86
+ for (const entry of entries) {
87
+ if (!entry.isDirectory() || entry.isSymbolicLink()) {
88
+ continue;
89
+ }
90
+
91
+ const absolutePath = path.join(currentDir, entry.name);
92
+ const relativePath = toRelativePath(absolutePath);
93
+
94
+ if (shouldSkipTraversal(entry.name, relativePath)) {
95
+ continue;
96
+ }
97
+
98
+ if (TARGET_DIR_NAMES.has(entry.name)) {
99
+ targets.push(absolutePath);
100
+ continue;
101
+ }
102
+
103
+ collectTargetPaths(absolutePath, targets);
104
+ }
105
+
106
+ return targets;
107
+ }
108
+
109
+ function getPathSize(targetPath) {
110
+ let stats;
111
+ try {
112
+ stats = fs.lstatSync(targetPath);
113
+ } catch {
114
+ return 0;
115
+ }
116
+
117
+ if (stats.isSymbolicLink()) {
118
+ return 0;
119
+ }
120
+
121
+ if (stats.isFile()) {
122
+ return stats.size;
123
+ }
124
+
125
+ if (!stats.isDirectory()) {
126
+ return 0;
127
+ }
128
+
129
+ let total = 0;
130
+ let entries = [];
131
+ try {
132
+ entries = fs.readdirSync(targetPath, { withFileTypes: true });
133
+ } catch {
134
+ return 0;
135
+ }
136
+
137
+ for (const entry of entries) {
138
+ total += getPathSize(path.join(targetPath, entry.name));
139
+ }
140
+
141
+ return total;
142
+ }
143
+
144
+ function buildTargetDetails(paths) {
145
+ return [...new Set(paths.map((targetPath) => path.resolve(targetPath)))]
146
+ .filter((targetPath) => pathExists(targetPath))
147
+ .map((targetPath) => {
148
+ try {
149
+ const stats = fs.lstatSync(targetPath);
150
+
151
+ return {
152
+ birthtimeMs: stats.birthtimeMs,
153
+ mtimeMs: stats.mtimeMs,
154
+ name: path.basename(targetPath),
155
+ path: targetPath,
156
+ relativePath: toRelativePath(targetPath),
157
+ size: getPathSize(targetPath),
158
+ };
159
+ } catch {
160
+ return null;
161
+ }
162
+ })
163
+ .filter(Boolean)
164
+ .sort((left, right) => left.relativePath.localeCompare(right.relativePath));
165
+ }
166
+
167
+ export function collectTargets({ includeDeep = false } = {}) {
168
+ const targets = collectTargetPaths();
169
+
170
+ if (includeDeep) {
171
+ for (const relativePath of DEEP_ONLY_ROOT_DIRS) {
172
+ const absolutePath = path.join(ROOT, relativePath);
173
+ if (pathExists(absolutePath)) {
174
+ targets.push(absolutePath);
175
+ }
176
+ }
177
+ }
178
+
179
+ return buildTargetDetails(targets);
180
+ }
181
+
182
+ function printTargets(label, targets) {
183
+ console.log(label);
184
+ for (const target of targets) {
185
+ console.log(` ${target.relativePath}\t${humanSize(target.size)}`);
186
+ }
187
+ console.log(` Total\t${humanSize(sumSizes(targets))}`);
188
+ }
189
+
190
+ export function sumSizes(targets) {
191
+ return targets.reduce((total, target) => total + target.size, 0);
192
+ }
193
+
194
+ export function humanSize(bytes) {
195
+ if (bytes === 0) {
196
+ return "0 B";
197
+ }
198
+
199
+ const units = ["B", "KB", "MB", "GB", "TB"];
200
+ let value = bytes;
201
+ let unitIndex = 0;
202
+
203
+ while (value >= 1024 && unitIndex < units.length - 1) {
204
+ value /= 1024;
205
+ unitIndex += 1;
206
+ }
207
+
208
+ return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
209
+ }
210
+
211
+ function readRootPackageName() {
212
+ const packageJsonPath = path.join(ROOT, "package.json");
213
+ if (!pathExists(packageJsonPath)) {
214
+ return null;
215
+ }
216
+
217
+ try {
218
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
219
+ return typeof packageJson.name === "string" ? packageJson.name : null;
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+
225
+ function runTaskClean() {
226
+ if (
227
+ CHECK_MODE ||
228
+ SKIP_TASK_CLEAN ||
229
+ !pathExists(path.join(ROOT, "turbo.json"))
230
+ ) {
231
+ return;
232
+ }
233
+
234
+ const rootPackageName = readRootPackageName();
235
+ const npmExecPath = process.env.npm_execpath;
236
+ const command = npmExecPath
237
+ ? process.execPath
238
+ : process.platform === "win32"
239
+ ? "pnpm.cmd"
240
+ : "pnpm";
241
+ const commandArgs = npmExecPath
242
+ ? [npmExecPath, "exec", "turbo", "run", "clean"]
243
+ : ["exec", "turbo", "run", "clean"];
244
+
245
+ if (rootPackageName) {
246
+ commandArgs.push(`--filter=!${rootPackageName}`);
247
+ }
248
+
249
+ const result = spawnSync(command, commandArgs, {
250
+ cwd: ROOT,
251
+ stdio: "inherit",
252
+ });
253
+
254
+ if (result.status !== 0) {
255
+ process.exit(result.status ?? 1);
256
+ }
257
+ }
258
+
259
+ export function removeTargets(targets) {
260
+ const removed = [];
261
+ const failed = [];
262
+
263
+ for (const target of targets) {
264
+ try {
265
+ fs.rmSync(target.path, {
266
+ force: true,
267
+ maxRetries: 3,
268
+ recursive: true,
269
+ retryDelay: 100,
270
+ });
271
+ removed.push(target);
272
+ } catch (error) {
273
+ failed.push({ ...target, error });
274
+ }
275
+ }
276
+
277
+ return { failed, removed };
278
+ }
279
+
280
+ function diffRemovedTargets(beforeTargets, afterTargets) {
281
+ const afterPathSet = new Set(
282
+ afterTargets.map((target) => target.relativePath),
283
+ );
284
+ return beforeTargets.filter(
285
+ (target) => !afterPathSet.has(target.relativePath),
286
+ );
287
+ }
288
+
289
+ function readAutoCleanState() {
290
+ const statePath = path.join(ROOT, AUTO_CLEAN_STATE_FILE);
291
+ if (!pathExists(statePath)) {
292
+ return null;
293
+ }
294
+
295
+ try {
296
+ return JSON.parse(fs.readFileSync(statePath, "utf8"));
297
+ } catch {
298
+ return null;
299
+ }
300
+ }
301
+
302
+ function writeAutoCleanState(state) {
303
+ try {
304
+ fs.writeFileSync(
305
+ path.join(ROOT, AUTO_CLEAN_STATE_FILE),
306
+ `${JSON.stringify(state, null, "\t")}\n`,
307
+ "utf8",
308
+ );
309
+ return true;
310
+ } catch {
311
+ return false;
312
+ }
313
+ }
314
+
315
+ function formatAgeDays(ageMs) {
316
+ return `${Math.max(1, Math.floor(ageMs / DAY_MS))}d old`;
317
+ }
318
+
319
+ function formatAutoCleanReason(target) {
320
+ return `${formatAgeDays(target.ageMs)}, ${humanSize(target.size)}`;
321
+ }
322
+
323
+ export function getAutoCleanTargets({ now = Date.now() } = {}) {
324
+ return collectTargets({ includeDeep: true })
325
+ .map((target) => {
326
+ const policy = AUTO_CLEAN_POLICIES[target.name];
327
+ if (!policy) {
328
+ return null;
329
+ }
330
+
331
+ const ageSource = policy.ageSource ?? "mtimeMs";
332
+ const sourceValue =
333
+ ageSource === "birthtimeMs" && target.birthtimeMs > 0
334
+ ? target.birthtimeMs
335
+ : target.mtimeMs;
336
+ const ageMs = Math.max(0, now - sourceValue);
337
+
338
+ if (ageMs < policy.minAgeMs) {
339
+ return null;
340
+ }
341
+
342
+ if (policy.minSizeBytes && target.size < policy.minSizeBytes) {
343
+ return null;
344
+ }
345
+
346
+ return { ...target, ageMs };
347
+ })
348
+ .filter(Boolean);
349
+ }
350
+
351
+ export function maybeAutoClean({ logger = console, now = Date.now() } = {}) {
352
+ if (process.env.CI === "true") {
353
+ return { reason: "ci", status: "skipped" };
354
+ }
355
+
356
+ if (process.env.QUARK_SKIP_AUTO_CLEAN === "1") {
357
+ logger.log("Skipping Quark auto-clean (QUARK_SKIP_AUTO_CLEAN=1)");
358
+ return { reason: "explicit", status: "skipped" };
359
+ }
360
+
361
+ const state = readAutoCleanState();
362
+ if (
363
+ state?.lastCheckedAtMs &&
364
+ now - state.lastCheckedAtMs < AUTO_CLEAN_INTERVAL_MS
365
+ ) {
366
+ return { status: "throttled" };
367
+ }
368
+
369
+ const targets = getAutoCleanTargets({ now });
370
+ const statePayload = {
371
+ lastCheckedAtMs: now,
372
+ lastCleanedAtMs: state?.lastCleanedAtMs ?? null,
373
+ };
374
+
375
+ if (targets.length === 0) {
376
+ if (!writeAutoCleanState(statePayload)) {
377
+ logger.warn?.("Quark auto-clean could not update its local state file.");
378
+ }
379
+ return { status: "noop" };
380
+ }
381
+
382
+ logger.log("Auto-cleaning stale workspace artifacts...");
383
+
384
+ const { failed, removed } = removeTargets(targets);
385
+ if (removed.length > 0) {
386
+ logger.log(
387
+ `Auto-cleaned ${removed.length} path(s) (${humanSize(sumSizes(removed))} freed).`,
388
+ );
389
+ for (const target of removed) {
390
+ logger.log(` ${target.relativePath} (${formatAutoCleanReason(target)})`);
391
+ }
392
+ }
393
+
394
+ if (failed.length > 0) {
395
+ logger.error("Quark auto-clean could not remove:");
396
+ for (const target of failed) {
397
+ logger.error(` ${target.relativePath}: ${target.error.message}`);
398
+ }
399
+ }
400
+
401
+ if (removed.length > 0) {
402
+ statePayload.lastCleanedAtMs = now;
403
+ }
404
+
405
+ if (!writeAutoCleanState(statePayload)) {
406
+ logger.warn?.("Quark auto-clean could not update its local state file.");
407
+ }
408
+
409
+ return {
410
+ failed,
411
+ removed,
412
+ status: failed.length > 0 ? "partial" : "cleaned",
413
+ };
414
+ }
415
+
416
+ function main() {
417
+ const initialTargets = collectTargets({ includeDeep: DEEP_MODE });
418
+
419
+ if (CHECK_MODE) {
420
+ if (initialTargets.length === 0) {
421
+ console.log(
422
+ `No ${DEEP_MODE ? "deep " : ""}cleanup targets found in ${ROOT}.`,
423
+ );
424
+ return;
425
+ }
426
+
427
+ printTargets(
428
+ `Cleanup targets${DEEP_MODE ? " (deep)" : ""} in ${ROOT}:`,
429
+ initialTargets,
430
+ );
431
+ return;
432
+ }
433
+
434
+ runTaskClean();
435
+
436
+ const remainingTargets = collectTargets({ includeDeep: DEEP_MODE });
437
+ if (initialTargets.length === 0 && remainingTargets.length === 0) {
438
+ console.log(
439
+ `No ${DEEP_MODE ? "deep " : ""}cleanup targets found in ${ROOT}.`,
440
+ );
441
+ return;
442
+ }
443
+
444
+ const taskCleanedTargets = diffRemovedTargets(
445
+ initialTargets,
446
+ remainingTargets,
447
+ );
448
+ const { failed, removed } = removeTargets(remainingTargets);
449
+
450
+ if (taskCleanedTargets.length > 0) {
451
+ console.log(
452
+ `Workspace clean tasks removed ${taskCleanedTargets.length} path(s) totaling ${humanSize(sumSizes(taskCleanedTargets))}.`,
453
+ );
454
+ }
455
+
456
+ if (removed.length > 0) {
457
+ console.log(
458
+ `Removed ${removed.length} path(s) totaling ${humanSize(sumSizes(removed))}.`,
459
+ );
460
+ for (const target of removed) {
461
+ console.log(` ${target.relativePath}`);
462
+ }
463
+ }
464
+
465
+ if (failed.length > 0) {
466
+ console.error("Failed to remove:");
467
+ for (const target of failed) {
468
+ console.error(` ${target.relativePath}: ${target.error.message}`);
469
+ }
470
+ process.exit(1);
471
+ }
472
+ }
473
+
474
+ if (
475
+ process.argv[1] &&
476
+ path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
477
+ ) {
478
+ main();
479
+ }
@@ -0,0 +1,48 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import {
4
+ formatWebPreflightFailure,
5
+ runWebPreflight,
6
+ } from "../apps/web/src/preflight.js";
7
+ import { maybeAutoClean } from "./clean-workspace.mjs";
8
+
9
+ export async function runDevPreflight({ logger = console } = {}) {
10
+ if (process.env.QUARK_SKIP_DEV_PREFLIGHT === "1") {
11
+ logger.log("Skipping Quark dev preflight (QUARK_SKIP_DEV_PREFLIGHT=1)");
12
+ return { status: "skipped" };
13
+ }
14
+
15
+ logger.log("Running Quark dev preflight...");
16
+
17
+ const result = await runWebPreflight();
18
+
19
+ if (result.status === "ok") {
20
+ logger.log(`Database ready (${result.latencyMs}ms)`);
21
+ try {
22
+ maybeAutoClean({ logger });
23
+ } catch (error) {
24
+ logger.warn?.(
25
+ `Quark auto-clean failed: ${error?.message ?? String(error)}`,
26
+ );
27
+ }
28
+ return result;
29
+ }
30
+
31
+ logger.error(formatWebPreflightFailure(result));
32
+ process.exit(1);
33
+ }
34
+
35
+ async function main() {
36
+ try {
37
+ await runDevPreflight();
38
+ } catch (error) {
39
+ console.error(error?.message ?? String(error));
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ const __filename = fileURLToPath(import.meta.url);
45
+
46
+ if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
47
+ void main();
48
+ }