create-dstack 0.1.0 → 0.1.2
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.
- package/README.md +2 -0
- package/dist/index.js +41 -147
- package/package.json +2 -2
- package/templates/CLAUDE.md +0 -2
- package/templates/agents.md +0 -2
- package/templates/.cursor/skills/api/SKILL.md +0 -198
- package/templates/.cursor/skills/api-timing-logs/SKILL.md +0 -77
- package/templates/.cursor/skills/brand-styling/SKILL.md +0 -104
- package/templates/.cursor/skills/create-pr/SKILL.md +0 -138
- package/templates/.cursor/skills/frontend-design/SKILL.md +0 -45
- package/templates/.cursor/skills/make-interfaces-feel-better/SKILL.md +0 -122
- package/templates/.cursor/skills/make-interfaces-feel-better/animations.md +0 -381
- package/templates/.cursor/skills/make-interfaces-feel-better/performance.md +0 -88
- package/templates/.cursor/skills/make-interfaces-feel-better/surfaces.md +0 -245
- package/templates/.cursor/skills/make-interfaces-feel-better/typography.md +0 -125
- package/templates/.cursor/skills/react-doctor/SKILL.md +0 -19
- package/templates/.cursor/skills/ux-writing/SKILL.md +0 -453
- package/templates/convex/auth.config.ts +0 -7
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
#!/usr/bin/env node
|
|
4
3
|
var __create = Object.create;
|
|
5
4
|
var __getProtoOf = Object.getPrototypeOf;
|
|
6
5
|
var __defProp = Object.defineProperty;
|
|
@@ -163,15 +162,15 @@ var require_picocolors = __commonJS((exports, module) => {
|
|
|
163
162
|
|
|
164
163
|
// src/index.ts
|
|
165
164
|
var {$: $2 } = globalThis.Bun;
|
|
166
|
-
import { existsSync, cpSync
|
|
165
|
+
import { existsSync, cpSync } from "fs";
|
|
167
166
|
import { join, resolve, dirname } from "path";
|
|
168
167
|
import { fileURLToPath } from "url";
|
|
169
168
|
|
|
170
169
|
// node_modules/@clack/core/dist/index.mjs
|
|
171
|
-
import { styleText as y } from "
|
|
172
|
-
import { stdout as S, stdin as $ } from "
|
|
173
|
-
import * as _ from "
|
|
174
|
-
import P from "
|
|
170
|
+
import { styleText as y } from "util";
|
|
171
|
+
import { stdout as S, stdin as $ } from "process";
|
|
172
|
+
import * as _ from "readline";
|
|
173
|
+
import P from "readline";
|
|
175
174
|
|
|
176
175
|
// node_modules/fast-string-truncated-width/dist/utils.js
|
|
177
176
|
var isAmbiguous = (x) => {
|
|
@@ -350,7 +349,7 @@ var dist_default2 = fastStringWidth;
|
|
|
350
349
|
|
|
351
350
|
// node_modules/fast-wrap-ansi/lib/main.js
|
|
352
351
|
var ESC = "\x1B";
|
|
353
|
-
var CSI = "
|
|
352
|
+
var CSI = "\x9B";
|
|
354
353
|
var END_CODE = 39;
|
|
355
354
|
var ANSI_ESCAPE_BELL = "\x07";
|
|
356
355
|
var ANSI_CSI = "[";
|
|
@@ -559,7 +558,7 @@ function wrapAnsi(string, columns, options) {
|
|
|
559
558
|
|
|
560
559
|
// node_modules/@clack/core/dist/index.mjs
|
|
561
560
|
var import_sisteransi = __toESM(require_src(), 1);
|
|
562
|
-
import { ReadStream as D } from "
|
|
561
|
+
import { ReadStream as D } from "tty";
|
|
563
562
|
function d(r, t, e) {
|
|
564
563
|
if (!e.some((o) => !o.disabled))
|
|
565
564
|
return r;
|
|
@@ -620,12 +619,6 @@ function z({ input: r = $, output: t = S, overwrite: e = true, hideCursor: s = t
|
|
|
620
619
|
}
|
|
621
620
|
var O = (r) => ("columns" in r) && typeof r.columns == "number" ? r.columns : 80;
|
|
622
621
|
var A = (r) => ("rows" in r) && typeof r.rows == "number" ? r.rows : 20;
|
|
623
|
-
function R(r, t, e, s = e) {
|
|
624
|
-
const i = O(r ?? S);
|
|
625
|
-
return wrapAnsi(t, i - e.length, { hard: true, trim: false }).split(`
|
|
626
|
-
`).map((n, o) => `${o === 0 ? s : e}${n}`).join(`
|
|
627
|
-
`);
|
|
628
|
-
}
|
|
629
622
|
var p = class {
|
|
630
623
|
input;
|
|
631
624
|
output;
|
|
@@ -784,7 +777,7 @@ var H = class extends p {
|
|
|
784
777
|
if (!this.userInput)
|
|
785
778
|
return y(["inverse", "hidden"], "_");
|
|
786
779
|
if (this._cursor >= this.userInput.length)
|
|
787
|
-
return `${this.userInput}
|
|
780
|
+
return `${this.userInput}\u2588`;
|
|
788
781
|
const t = this.userInput.slice(0, this._cursor), [e, ...s] = this.userInput.slice(this._cursor);
|
|
789
782
|
return `${t}${y("inverse", e)}${s.join("")}`;
|
|
790
783
|
}
|
|
@@ -832,24 +825,6 @@ var H = class extends p {
|
|
|
832
825
|
}
|
|
833
826
|
}
|
|
834
827
|
};
|
|
835
|
-
|
|
836
|
-
class Q extends p {
|
|
837
|
-
get cursor() {
|
|
838
|
-
return this.value ? 0 : 1;
|
|
839
|
-
}
|
|
840
|
-
get _value() {
|
|
841
|
-
return this.cursor === 0;
|
|
842
|
-
}
|
|
843
|
-
constructor(t) {
|
|
844
|
-
super(t, false), this.value = !!t.initialValue, this.on("userInput", () => {
|
|
845
|
-
this.value = this._value;
|
|
846
|
-
}), this.on("confirm", (e) => {
|
|
847
|
-
this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = e, this.state = "submit", this.close();
|
|
848
|
-
}), this.on("cursor", () => {
|
|
849
|
-
this.value = !this.value;
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
828
|
var X = { Y: { type: "year", len: 4 }, M: { type: "month", len: 2 }, D: { type: "day", len: 2 } };
|
|
854
829
|
function L(r) {
|
|
855
830
|
return [...r].map((t) => X[t]);
|
|
@@ -954,7 +929,7 @@ class et extends p {
|
|
|
954
929
|
}
|
|
955
930
|
}
|
|
956
931
|
#f(t, e) {
|
|
957
|
-
if (e?.name === "backspace" || e?.sequence === "
|
|
932
|
+
if (e?.name === "backspace" || e?.sequence === "\x7F" || e?.sequence === "\b" || t === "\x7F" || t === "\b") {
|
|
958
933
|
this.inlineError = "";
|
|
959
934
|
const s = this.#u();
|
|
960
935
|
if (!s)
|
|
@@ -1086,7 +1061,7 @@ class at extends p {
|
|
|
1086
1061
|
return this.userInput;
|
|
1087
1062
|
const t = this.userInput;
|
|
1088
1063
|
if (this.cursor >= t.length)
|
|
1089
|
-
return `${this.userInput}
|
|
1064
|
+
return `${this.userInput}\u2588`;
|
|
1090
1065
|
const e = t.slice(0, this.cursor), [s, ...i] = t.slice(this.cursor);
|
|
1091
1066
|
return `${e}${y("inverse", s)}${i.join("")}`;
|
|
1092
1067
|
}
|
|
@@ -1103,8 +1078,8 @@ class at extends p {
|
|
|
1103
1078
|
}
|
|
1104
1079
|
|
|
1105
1080
|
// node_modules/@clack/prompts/dist/index.mjs
|
|
1106
|
-
import { styleText as t, stripVTControlCharacters as ne } from "
|
|
1107
|
-
import P2 from "
|
|
1081
|
+
import { styleText as t, stripVTControlCharacters as ne } from "util";
|
|
1082
|
+
import P2 from "process";
|
|
1108
1083
|
var import_sisteransi2 = __toESM(require_src(), 1);
|
|
1109
1084
|
function Ze() {
|
|
1110
1085
|
return P2.platform !== "win32" ? P2.env.TERM !== "linux" : !!P2.env.CI || !!P2.env.WT_SESSION || !!P2.env.TERMINUS_SUBLIME || P2.env.ConEmuTask === "{cmd::Cmder}" || P2.env.TERM_PROGRAM === "Terminus-Sublime" || P2.env.TERM_PROGRAM === "vscode" || P2.env.TERM === "xterm-256color" || P2.env.TERM === "alacritty" || P2.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
|
|
@@ -1112,31 +1087,31 @@ function Ze() {
|
|
|
1112
1087
|
var ee = Ze();
|
|
1113
1088
|
var ae = () => process.env.CI === "true";
|
|
1114
1089
|
var w2 = (e, i) => ee ? e : i;
|
|
1115
|
-
var _e = w2("
|
|
1116
|
-
var oe = w2("
|
|
1117
|
-
var ue = w2("
|
|
1118
|
-
var F2 = w2("
|
|
1119
|
-
var le = w2("
|
|
1120
|
-
var d2 = w2("
|
|
1121
|
-
var E2 = w2("
|
|
1122
|
-
var Ie = w2("
|
|
1123
|
-
var Ee = w2("
|
|
1124
|
-
var z2 = w2("
|
|
1125
|
-
var H2 = w2("
|
|
1126
|
-
var te = w2("
|
|
1127
|
-
var U = w2("
|
|
1128
|
-
var J2 = w2("
|
|
1129
|
-
var xe = w2("
|
|
1130
|
-
var se = w2("
|
|
1131
|
-
var ce = w2("
|
|
1132
|
-
var Ge = w2("
|
|
1133
|
-
var $e = w2("
|
|
1134
|
-
var de = w2("
|
|
1135
|
-
var Oe = w2("
|
|
1136
|
-
var he = w2("
|
|
1137
|
-
var pe = w2("
|
|
1138
|
-
var me = w2("
|
|
1139
|
-
var ge = w2("
|
|
1090
|
+
var _e = w2("\u25C6", "*");
|
|
1091
|
+
var oe = w2("\u25A0", "x");
|
|
1092
|
+
var ue = w2("\u25B2", "x");
|
|
1093
|
+
var F2 = w2("\u25C7", "o");
|
|
1094
|
+
var le = w2("\u250C", "T");
|
|
1095
|
+
var d2 = w2("\u2502", "|");
|
|
1096
|
+
var E2 = w2("\u2514", "\u2014");
|
|
1097
|
+
var Ie = w2("\u2510", "T");
|
|
1098
|
+
var Ee = w2("\u2518", "\u2014");
|
|
1099
|
+
var z2 = w2("\u25CF", ">");
|
|
1100
|
+
var H2 = w2("\u25CB", " ");
|
|
1101
|
+
var te = w2("\u25FB", "[\u2022]");
|
|
1102
|
+
var U = w2("\u25FC", "[+]");
|
|
1103
|
+
var J2 = w2("\u25FB", "[ ]");
|
|
1104
|
+
var xe = w2("\u25AA", "\u2022");
|
|
1105
|
+
var se = w2("\u2500", "-");
|
|
1106
|
+
var ce = w2("\u256E", "+");
|
|
1107
|
+
var Ge = w2("\u251C", "+");
|
|
1108
|
+
var $e = w2("\u256F", "+");
|
|
1109
|
+
var de = w2("\u2570", "+");
|
|
1110
|
+
var Oe = w2("\u256D", "+");
|
|
1111
|
+
var he = w2("\u25CF", "\u2022");
|
|
1112
|
+
var pe = w2("\u25C6", "*");
|
|
1113
|
+
var me = w2("\u25B2", "!");
|
|
1114
|
+
var ge = w2("\u25A0", "x");
|
|
1140
1115
|
var V2 = (e) => {
|
|
1141
1116
|
switch (e) {
|
|
1142
1117
|
case "initial":
|
|
@@ -1150,61 +1125,6 @@ var V2 = (e) => {
|
|
|
1150
1125
|
return t("green", F2);
|
|
1151
1126
|
}
|
|
1152
1127
|
};
|
|
1153
|
-
var ot2 = (e) => {
|
|
1154
|
-
const i = e.active ?? "Yes", s = e.inactive ?? "No";
|
|
1155
|
-
return new Q({ active: i, inactive: s, signal: e.signal, input: e.input, output: e.output, initialValue: e.initialValue ?? true, render() {
|
|
1156
|
-
const r = e.withGuide ?? u.withGuide, u2 = `${V2(this.state)} `, n = r ? `${t("gray", d2)} ` : "", o = R(e.output, e.message, n, u2), c2 = `${r ? `${t("gray", d2)}
|
|
1157
|
-
` : ""}${o}
|
|
1158
|
-
`, a = this.value ? i : s;
|
|
1159
|
-
switch (this.state) {
|
|
1160
|
-
case "submit": {
|
|
1161
|
-
const l = r ? `${t("gray", d2)} ` : "";
|
|
1162
|
-
return `${c2}${l}${t("dim", a)}`;
|
|
1163
|
-
}
|
|
1164
|
-
case "cancel": {
|
|
1165
|
-
const l = r ? `${t("gray", d2)} ` : "";
|
|
1166
|
-
return `${c2}${l}${t(["strikethrough", "dim"], a)}${r ? `
|
|
1167
|
-
${t("gray", d2)}` : ""}`;
|
|
1168
|
-
}
|
|
1169
|
-
default: {
|
|
1170
|
-
const l = r ? `${t("cyan", d2)} ` : "", $2 = r ? t("cyan", E2) : "";
|
|
1171
|
-
return `${c2}${l}${this.value ? `${t("green", z2)} ${i}` : `${t("dim", H2)} ${t("dim", i)}`}${e.vertical ? r ? `
|
|
1172
|
-
${t("cyan", d2)} ` : `
|
|
1173
|
-
` : ` ${t("dim", "/")} `}${this.value ? `${t("dim", H2)} ${t("dim", s)}` : `${t("green", z2)} ${s}`}
|
|
1174
|
-
${$2}
|
|
1175
|
-
`;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
} }).prompt();
|
|
1179
|
-
};
|
|
1180
|
-
var O2 = { message: (e = [], { symbol: i = t("gray", d2), secondarySymbol: s = t("gray", d2), output: r = process.stdout, spacing: u2 = 1, withGuide: n } = {}) => {
|
|
1181
|
-
const o = [], c2 = n ?? u.withGuide, a = c2 ? s : "", l = c2 ? `${i} ` : "", $2 = c2 ? `${s} ` : "";
|
|
1182
|
-
for (let p2 = 0;p2 < u2; p2++)
|
|
1183
|
-
o.push(a);
|
|
1184
|
-
const y2 = Array.isArray(e) ? e : e.split(`
|
|
1185
|
-
`);
|
|
1186
|
-
if (y2.length > 0) {
|
|
1187
|
-
const [p2, ...m] = y2;
|
|
1188
|
-
p2.length > 0 ? o.push(`${l}${p2}`) : o.push(c2 ? i : "");
|
|
1189
|
-
for (const g of m)
|
|
1190
|
-
g.length > 0 ? o.push(`${$2}${g}`) : o.push(c2 ? s : "");
|
|
1191
|
-
}
|
|
1192
|
-
r.write(`${o.join(`
|
|
1193
|
-
`)}
|
|
1194
|
-
`);
|
|
1195
|
-
}, info: (e, i) => {
|
|
1196
|
-
O2.message(e, { ...i, symbol: t("blue", he) });
|
|
1197
|
-
}, success: (e, i) => {
|
|
1198
|
-
O2.message(e, { ...i, symbol: t("green", pe) });
|
|
1199
|
-
}, step: (e, i) => {
|
|
1200
|
-
O2.message(e, { ...i, symbol: t("green", F2) });
|
|
1201
|
-
}, warn: (e, i) => {
|
|
1202
|
-
O2.message(e, { ...i, symbol: t("yellow", me) });
|
|
1203
|
-
}, warning: (e, i) => {
|
|
1204
|
-
O2.warn(e, i);
|
|
1205
|
-
}, error: (e, i) => {
|
|
1206
|
-
O2.message(e, { ...i, symbol: t("red", ge) });
|
|
1207
|
-
} };
|
|
1208
1128
|
var pt = (e = "", i) => {
|
|
1209
1129
|
const s = i?.output ?? process.stdout, r = i?.withGuide ?? u.withGuide ? `${t("gray", E2)} ` : "";
|
|
1210
1130
|
s.write(`${r}${t("red", e)}
|
|
@@ -1224,7 +1144,7 @@ ${t("gray", E2)} ` : "";
|
|
|
1224
1144
|
`);
|
|
1225
1145
|
};
|
|
1226
1146
|
var Ct = (e) => t("magenta", e);
|
|
1227
|
-
var fe = ({ indicator: e = "dots", onCancel: i, output: s = process.stdout, cancelMessage: r, errorMessage: u2, frames: n = ee ? ["
|
|
1147
|
+
var fe = ({ indicator: e = "dots", onCancel: i, output: s = process.stdout, cancelMessage: r, errorMessage: u2, frames: n = ee ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], delay: o = ee ? 80 : 120, signal: c2, ...a } = {}) => {
|
|
1228
1148
|
const l = ae();
|
|
1229
1149
|
let $2, y2, p2 = false, m = false, g = "", S2, h = performance.now();
|
|
1230
1150
|
const f = O(s), v = a?.styleFrame ?? Ct, T2 = (_2) => {
|
|
@@ -1281,7 +1201,7 @@ var fe = ({ indicator: e = "dots", onCancel: i, output: s = process.stdout, canc
|
|
|
1281
1201
|
return m;
|
|
1282
1202
|
} };
|
|
1283
1203
|
};
|
|
1284
|
-
var Ve = { light: w2("
|
|
1204
|
+
var Ve = { light: w2("\u2500", "-"), heavy: w2("\u2501", "="), block: w2("\u2588", "#") };
|
|
1285
1205
|
var je = `${t("gray", d2)} `;
|
|
1286
1206
|
var Ot = (e) => new at({ validate: e.validate, placeholder: e.placeholder, defaultValue: e.defaultValue, initialValue: e.initialValue, output: e.output, signal: e.signal, input: e.input, render() {
|
|
1287
1207
|
const i = e?.withGuide ?? u.withGuide, s = `${`${i ? `${t("gray", d2)}
|
|
@@ -1338,11 +1258,6 @@ async function main() {
|
|
|
1338
1258
|
}
|
|
1339
1259
|
projectName = input;
|
|
1340
1260
|
}
|
|
1341
|
-
const useStackAuth = await ot2({ message: "Add Stack Auth?" });
|
|
1342
|
-
if (q(useStackAuth)) {
|
|
1343
|
-
pt("Cancelled.");
|
|
1344
|
-
process.exit(0);
|
|
1345
|
-
}
|
|
1346
1261
|
const projectDir = resolve(process.cwd(), projectName);
|
|
1347
1262
|
if (existsSync(projectDir)) {
|
|
1348
1263
|
pt(`Directory "${projectName}" already exists.`);
|
|
@@ -1350,7 +1265,7 @@ async function main() {
|
|
|
1350
1265
|
}
|
|
1351
1266
|
const s = fe();
|
|
1352
1267
|
s.start("preheating the oven...");
|
|
1353
|
-
await $2`bunx create-next-app@latest ${projectName} --typescript --tailwind --no-eslint --app --src-dir --import-alias "@/*" --use-bun`.quiet();
|
|
1268
|
+
await $2`bunx create-next-app@latest ${projectName} --typescript --tailwind --no-eslint --app --no-src-dir --import-alias "@/*" --use-bun`.quiet();
|
|
1354
1269
|
s.stop("next.js is in the chat");
|
|
1355
1270
|
s.start("calling convex off the bench...");
|
|
1356
1271
|
await $2`bun add convex`.cwd(projectDir).quiet();
|
|
@@ -1361,29 +1276,8 @@ async function main() {
|
|
|
1361
1276
|
s.start("adding the drip (shadcn)...");
|
|
1362
1277
|
await $2`bunx shadcn@latest init -d`.cwd(projectDir).quiet();
|
|
1363
1278
|
s.stop("looking fire already");
|
|
1364
|
-
if (useStackAuth) {
|
|
1365
|
-
s.start("sliding stack auth into the dm...");
|
|
1366
|
-
await $2`bun add @stackframe/stack`.cwd(projectDir).quiet();
|
|
1367
|
-
s.stop("auth is cooked and ready");
|
|
1368
|
-
mkdirSync(join(projectDir, "convex"), { recursive: true });
|
|
1369
|
-
copyTemplate(join(TEMPLATES_DIR, "convex", "auth.config.ts"), join(projectDir, "convex", "auth.config.ts"));
|
|
1370
|
-
appendFileSync(join(projectDir, ".env.local"), [
|
|
1371
|
-
"",
|
|
1372
|
-
"# Stack Auth \u2014 fill in from https://app.stack-auth.com",
|
|
1373
|
-
"NEXT_PUBLIC_STACK_PROJECT_ID=",
|
|
1374
|
-
"NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=",
|
|
1375
|
-
"STACK_SECRET_SERVER_KEY=",
|
|
1376
|
-
""
|
|
1377
|
-
].join(`
|
|
1378
|
-
`));
|
|
1379
|
-
O2.warn(`Stack Auth needs manual steps:
|
|
1380
|
-
` + ` 1. Create a project at ${import_picocolors.default.cyan("https://app.stack-auth.com")}
|
|
1381
|
-
` + ` 2. Fill in ${import_picocolors.default.dim(".env.local")} with your keys
|
|
1382
|
-
` + ` 3. Set the same vars in your Convex dashboard
|
|
1383
|
-
` + ` 4. Run ${import_picocolors.default.cyan("bunx @stackframe/stack-cli@latest init")} inside the project to finish wiring up the provider`);
|
|
1384
|
-
}
|
|
1385
1279
|
s.start("putting the secret sauce on it...");
|
|
1386
|
-
copyTemplate(join(TEMPLATES_DIR, "globals.css"), join(projectDir, "
|
|
1280
|
+
copyTemplate(join(TEMPLATES_DIR, "globals.css"), join(projectDir, "app", "globals.css"));
|
|
1387
1281
|
copyTemplate(join(TEMPLATES_DIR, "oxlint.json"), join(projectDir, "oxlint.json"));
|
|
1388
1282
|
copyTemplate(join(TEMPLATES_DIR, "oxfmt.json"), join(projectDir, "oxfmt.json"));
|
|
1389
1283
|
if (existsSync(join(TEMPLATES_DIR, ".claude"))) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-dstack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Create a new dstack project — Next.js, Bun, Convex, Shadcn, Oxlint, Oxfmt",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"create-dstack": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"build": "bun build ./src/index.ts --outfile ./dist/index.js --target
|
|
14
|
+
"build": "bun build ./src/index.ts --outfile ./dist/index.js --target bun",
|
|
15
15
|
"start": "bun run ./src/index.ts",
|
|
16
16
|
"prepublishOnly": "bun run build"
|
|
17
17
|
},
|
package/templates/CLAUDE.md
CHANGED
|
@@ -19,7 +19,6 @@ bunx oxfmt --check . # Check formatting
|
|
|
19
19
|
- React 19
|
|
20
20
|
- TypeScript 5 with strict mode
|
|
21
21
|
- Convex for DB
|
|
22
|
-
- StackAuth for auth, teams, user metadata
|
|
23
22
|
- All database types in Convex
|
|
24
23
|
|
|
25
24
|
## Conventions
|
|
@@ -49,7 +48,6 @@ API routes: mirror domains (app/api/mail/, app/api/profile/)
|
|
|
49
48
|
|
|
50
49
|
## Authentication & Security
|
|
51
50
|
|
|
52
|
-
- User endpoints: StackAuth server session for API routes and Server Actions; `useUser` only in client components
|
|
53
51
|
- System/cron: Bearer token
|
|
54
52
|
- Error handling: `throwOnError` or `if (error)` patterns
|
|
55
53
|
|
package/templates/agents.md
CHANGED
|
@@ -19,7 +19,6 @@ bunx oxfmt --check . # Check formatting
|
|
|
19
19
|
- React 19
|
|
20
20
|
- TypeScript 5 with strict mode
|
|
21
21
|
- Convex for DB
|
|
22
|
-
- StackAuth for auth, teams, user metadata
|
|
23
22
|
- All database types in Convex
|
|
24
23
|
|
|
25
24
|
## Conventions
|
|
@@ -49,7 +48,6 @@ API routes: mirror domains (app/api/mail/, app/api/profile/)
|
|
|
49
48
|
|
|
50
49
|
## Authentication & Security
|
|
51
50
|
|
|
52
|
-
- User endpoints: StackAuth server session for API routes and Server Actions; `useUser` only in client components
|
|
53
51
|
- System/cron: Bearer token
|
|
54
52
|
- Error handling: `throwOnError` or `if (error)` patterns
|
|
55
53
|
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: api
|
|
3
|
-
description: Whenever designing or working on API routes
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# API Development Rules
|
|
7
|
-
|
|
8
|
-
Every API route is either user-authenticated or system-authenticated. No exceptions.
|
|
9
|
-
|
|
10
|
-
## 1. Route Handlers
|
|
11
|
-
|
|
12
|
-
- Use standard Next.js `NextRequest` and `NextResponse` types only
|
|
13
|
-
- No custom middleware that wraps handlers
|
|
14
|
-
|
|
15
|
-
## 2. Authentication
|
|
16
|
-
|
|
17
|
-
### User APIs (browser/app requests)
|
|
18
|
-
|
|
19
|
-
Resolve the signed-in user with **StackAuth on the server**. In Route Handlers and Server Actions, use Stack's server-side session APIs—**not** the React `useUser` hook (that is client-only).
|
|
20
|
-
|
|
21
|
-
After auth, read and write data through **Convex** (`fetchQuery`, `fetchMutation`, `fetchAction` from `convex/nextjs`) so authorization can rely on `ctx.auth` inside Convex functions. Pass the Convex auth token when your setup requires it (same pattern as other OIDC providers: token option on `fetch*`).
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
import { fetchMutation } from "convex/nextjs";
|
|
25
|
-
import { api } from "@/convex/_generated/api";
|
|
26
|
-
import type { NextRequest } from "next/server";
|
|
27
|
-
|
|
28
|
-
export async function PATCH(request: NextRequest) {
|
|
29
|
-
// 1. Resolve user with StackAuth server APIs; return 401 if missing
|
|
30
|
-
// 2. Build Convex token if your Stack+Convex integration uses JWT forwarding
|
|
31
|
-
const token = await getConvexAuthTokenFromRequest(request);
|
|
32
|
-
const args = await request.json();
|
|
33
|
-
await fetchMutation(api.example.updateThing, args, { token });
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Cron/System APIs
|
|
38
|
-
|
|
39
|
-
Use Bearer token + trusted Convex entry points:
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
const authHeader = request.headers.get("authorization");
|
|
43
|
-
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
44
|
-
return new Response("Unauthorized", { status: 401 });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
await fetchMutation(api.jobs.runScheduled, payload);
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Use **deployment-appropriate** Convex access for system jobs (e.g. internal mutations or HTTP actions that validate the same secret). Never reuse a "god mode" client for user-initiated work.
|
|
51
|
-
|
|
52
|
-
### Access shape
|
|
53
|
-
|
|
54
|
-
| Caller | Auth | Data layer |
|
|
55
|
-
| ----------------- | ----------------- | ----------------------------------- |
|
|
56
|
-
| User API route | StackAuth session | Convex via `fetch*` + user token |
|
|
57
|
-
| System / cron API | Bearer secret | Convex mutation/action for batch/system work |
|
|
58
|
-
|
|
59
|
-
Never bypass user-level Convex auth for operations that should be scoped to a single user.
|
|
60
|
-
|
|
61
|
-
## 3. Validation
|
|
62
|
-
|
|
63
|
-
**Every API route MUST validate with Zod.** No exceptions.
|
|
64
|
-
|
|
65
|
-
### Validation Order: Fail Fast
|
|
66
|
-
|
|
67
|
-
1. Authenticate
|
|
68
|
-
2. Parse and validate request body immediately
|
|
69
|
-
3. Then do expensive operations (Convex calls, external APIs)
|
|
70
|
-
|
|
71
|
-
### Schema Patterns
|
|
72
|
-
|
|
73
|
-
- Use `.trim()` for strings, `.email()` for emails
|
|
74
|
-
- Use `.refine()` for custom business logic
|
|
75
|
-
- Use `z.discriminatedUnion()` for conditional validation
|
|
76
|
-
|
|
77
|
-
## 4. Error Response Format
|
|
78
|
-
|
|
79
|
-
All errors use Stripe-style format:
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
{
|
|
83
|
-
error: {
|
|
84
|
-
type: string; // Required: validation_error, authentication_error, api_error, etc.
|
|
85
|
-
message: string; // Required: Human-readable
|
|
86
|
-
code?: string; // Optional: MISSING_EMAIL, QUOTA_EXCEEDED, etc.
|
|
87
|
-
param?: string; // Optional: Field name for validation errors
|
|
88
|
-
details?: unknown; // Optional: Zod errors array
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Error Types by Status
|
|
94
|
-
|
|
95
|
-
- `400` validation_error - Invalid input
|
|
96
|
-
- `401` authentication_error - Not authenticated
|
|
97
|
-
- `403` authorization_error - Not authorized
|
|
98
|
-
- `404` not_found_error - Resource missing
|
|
99
|
-
- `429` rate_limit_error - Quota exceeded
|
|
100
|
-
- `500` api_error - Internal error
|
|
101
|
-
- `503` service_unavailable_error - External service down
|
|
102
|
-
|
|
103
|
-
### Forbidden Formats
|
|
104
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
// NEVER use these:
|
|
107
|
-
{ success: false, error: "message" }
|
|
108
|
-
{ error: "string" } // Must be object
|
|
109
|
-
{ message: "..." } // Must use error.message
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## 5. Standard API Template
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
116
|
-
import { z } from "zod";
|
|
117
|
-
import { fetchMutation } from "convex/nextjs";
|
|
118
|
-
import { api } from "@/convex/_generated/api";
|
|
119
|
-
|
|
120
|
-
const schema = z.object({
|
|
121
|
-
email: z.string().email(),
|
|
122
|
-
name: z.string().min(1),
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
export async function POST(request: NextRequest) {
|
|
126
|
-
const user = await getStackAuthUserFromRequest(request);
|
|
127
|
-
if (!user) {
|
|
128
|
-
return NextResponse.json(
|
|
129
|
-
{
|
|
130
|
-
error: { type: "authentication_error", message: "Unauthorized" },
|
|
131
|
-
},
|
|
132
|
-
{ status: 401 },
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const rawBody = await request.json();
|
|
138
|
-
const data = schema.parse(rawBody);
|
|
139
|
-
|
|
140
|
-
const token = await getConvexAuthTokenFromRequest(request, user);
|
|
141
|
-
const result = await fetchMutation(api.example.createProfile, data, { token });
|
|
142
|
-
|
|
143
|
-
return NextResponse.json({ success: true, data: result });
|
|
144
|
-
} catch (error) {
|
|
145
|
-
if (error instanceof z.ZodError) {
|
|
146
|
-
return NextResponse.json(
|
|
147
|
-
{
|
|
148
|
-
error: {
|
|
149
|
-
type: "validation_error",
|
|
150
|
-
message: "Validation failed",
|
|
151
|
-
details: error.issues,
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
{ status: 400 },
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
console.error("API Error:", error);
|
|
158
|
-
return NextResponse.json(
|
|
159
|
-
{
|
|
160
|
-
error: { type: "api_error", message: "Internal server error" },
|
|
161
|
-
},
|
|
162
|
-
{ status: 500 },
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Replace `getStackAuthUserFromRequest` / `getConvexAuthTokenFromRequest` with the project's StackAuth server helpers and Convex token bridge.
|
|
169
|
-
|
|
170
|
-
## 6. Security
|
|
171
|
-
|
|
172
|
-
- Always sanitize inputs: `.trim()`, `.toLowerCase()` for emails
|
|
173
|
-
- Check user quotas before expensive operations via `user.clientReadOnlyMetadata`
|
|
174
|
-
- Use Supabase query builders, never raw SQL with user input
|
|
175
|
-
- Validate environment variables exist before using
|
|
176
|
-
- There is no SQL layer; do not concatenate user input into query strings for external services
|
|
177
|
-
|
|
178
|
-
## 7. Database Best Practices (Convex)
|
|
179
|
-
|
|
180
|
-
- Define schema and indexes in `convex/schema.ts`; query with indexed fields
|
|
181
|
-
- Limit list reads (e.g. `.take(n)` / bounded queries); avoid unbounded scans
|
|
182
|
-
- Use Convex argument validators on every function; keep Zod at the HTTP boundary
|
|
183
|
-
- Use `Promise.allSettled()` for concurrent operations that can fail independently
|
|
184
|
-
- Use `pLimit` for controlled concurrency against external APIs
|
|
185
|
-
|
|
186
|
-
## 8. Logging
|
|
187
|
-
|
|
188
|
-
- Use `console.error()` for errors with context (userId, error message, stack)
|
|
189
|
-
- Use `console.log()` sparingly for important business events
|
|
190
|
-
- Never log sensitive data (passwords, tokens, PII)
|
|
191
|
-
- Logs are auto-captured by Vercel
|
|
192
|
-
|
|
193
|
-
## 9. Testing
|
|
194
|
-
|
|
195
|
-
- Test files in `__tests__/` within API route folders
|
|
196
|
-
- Mock StackAuth server user resolution and Convex `fetch*` helpers when testing handlers in isolation
|
|
197
|
-
- Mock `request.json()` for body parsing
|
|
198
|
-
- Use `{} as NextRequest` if handler doesn't use request object
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: api-timing-logs
|
|
3
|
-
description: Adds readable one-line phase timing logs for API routes and server handlers (local debugging and production correlation). Use when instrumenting slow routes, debugging latency, adding request tracing, or when the user asks for timing logs, speed stats, or structured server logging.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# API phase timing logs (gold standard)
|
|
7
|
-
|
|
8
|
-
## Goals
|
|
9
|
-
|
|
10
|
-
- **Scannable in a terminal**: one line per phase, aligned columns, no nested objects on the happy path.
|
|
11
|
-
- **Grep-friendly**: fixed bracket tag per feature (e.g. `[ck-gen]`, `[mail-send]`).
|
|
12
|
-
- **Minimal PII**: log `user=…` **once** at start; repeat **only** on error lines that need correlation.
|
|
13
|
-
|
|
14
|
-
## Log line shape
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
[TAG] begin user=<full-user-id>
|
|
18
|
-
[TAG] <step> <ms>ms <optional key=value ...>
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
- **`TAG`**: short, stable, unique within the codebase (2–8 chars after the bracket is enough if unambiguous).
|
|
22
|
-
- **`step`**: lowercase, use **dots** for sub-phases (`db.company_row`, `storage.sign`, `llm`). Pad `step` to a fixed width (e.g. 18 chars) so `ms` columns line up.
|
|
23
|
-
- **`ms`**: wall time for **that phase only** (`Date.now() - phaseStart`), not cumulative unless the step name says `total`.
|
|
24
|
-
- **Detail tail**: space-separated `key=value`. Prefer counts and compact units over raw dumps.
|
|
25
|
-
|
|
26
|
-
## Helpers (TypeScript pattern)
|
|
27
|
-
|
|
28
|
-
Copy/adapt per route; keep helpers **file-local** unless three+ routes need the same tag.
|
|
29
|
-
|
|
30
|
-
```ts
|
|
31
|
-
const TAG = "[feature-tag]";
|
|
32
|
-
|
|
33
|
-
function elapsedMs(start: number) {
|
|
34
|
-
return Date.now() - start;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function phaseLog(step: string, ms: number, detail?: string) {
|
|
38
|
-
const tail = detail ? ` ${detail}` : "";
|
|
39
|
-
console.info(`${TAG} ${step.padEnd(18)} ${String(ms).padStart(5)}ms${tail}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function phaseWarn(bucket: string, detail: string) {
|
|
43
|
-
console.warn(`${TAG} ${bucket} ${detail}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function phaseErr(step: string, userId: string, detail: string) {
|
|
47
|
-
console.error(`${TAG} ${step} user=${userId} ${detail}`);
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
After auth (or once `userId` is known): `console.info(\`${TAG} begin user=${userId}\`);`then use`phaseLog`for every subsequent phase **without** repeating`userId`.
|
|
52
|
-
|
|
53
|
-
## What to log per phase
|
|
54
|
-
|
|
55
|
-
- **External I/O**: hub pull, DB reads/writes, storage sign/download, HTTP fetches, LLM calls—each gets its own line with duration.
|
|
56
|
-
- **CPU-ish work** only if it can be large (parse/format of huge payloads); otherwise merge with the phase that produced the bytes.
|
|
57
|
-
- **Final line**: `done` or `total` with end-to-end `ms` plus 1–3 summary stats (`docs=`, `llmIn=34500ch`, etc.).
|
|
58
|
-
|
|
59
|
-
## Units and compaction
|
|
60
|
-
|
|
61
|
-
- Prefer **`kch`** (thousands of **characters**) when approximating payload size from string length. Do **not** label char counts as `kB` (misleading).
|
|
62
|
-
- Rough token hints are OK: `~8676tok` with a comment in code that it is approximate (e.g. chars/4).
|
|
63
|
-
- For multiple similar items, one compact tail beats an array of objects:
|
|
64
|
-
`ok=3/3 raw~1200kch [a.json:400kch@800ms, b.pdf:…]`
|
|
65
|
-
- Cap list length in logs (e.g. first 3 files + `+2 more`) if noise gets large.
|
|
66
|
-
|
|
67
|
-
## Errors
|
|
68
|
-
|
|
69
|
-
- Use **`phaseErr`** for failures where you need to find the user in logs: include `user=<id>` and a **short** reason (message string, not full stack objects).
|
|
70
|
-
- Keep generic `console.error` for unexpected exceptions if you already logged `begin` with `user=`.
|
|
71
|
-
|
|
72
|
-
## Anti-patterns
|
|
73
|
-
|
|
74
|
-
- Repeating `userId` on every `info` line.
|
|
75
|
-
- Dumping **full prompts**, document bodies, or tokens in logs.
|
|
76
|
-
- Large **JSON blobs** for routine success paths—use one line + counts.
|
|
77
|
-
- Inconsistent tags (e.g. mixing `console.info("feature: …")` and `[tag] …` styles) in the same route.
|