@mars-stack/cli 8.0.3 → 8.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mars-stack/cli",
3
- "version": "8.0.3",
3
+ "version": "8.0.5",
4
4
  "description": "MARS CLI: scaffold, configure, and maintain SaaS apps",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,10 +48,10 @@
48
48
  "devDependencies": {
49
49
  "@types/fs-extra": "^11.0.0",
50
50
  "@types/js-yaml": "^4.0.9",
51
- "@types/node": "^25.5.0",
51
+ "@types/node": "^25.5.2",
52
52
  "@types/prompts": "^2.4.0",
53
53
  "tsup": "^8.0.0",
54
- "typescript": "^5.7.0",
54
+ "typescript": "^6.0.2",
55
55
  "vitest": "^4.1.2"
56
56
  }
57
57
  }
@@ -10,6 +10,8 @@ Browser-level tests for user-visible behaviour. Run: `yarn test:e2e` from the pr
10
10
 
11
11
  Monorepo CI runs this on pull requests via the `template-e2e` job in `.github/workflows/ci.yml` (Postgres service + `E2E_USE_CI_SERVER=1` + `scripts/start-e2e-server.mjs`).
12
12
 
13
+ **Local default web server:** `playwright-web-server.mjs` runs `ensure-db`, then **`npx prisma db seed`**, then `yarn dev` — same seeded-user assumption as CI (`user@example.com` / `Password123!` from `scripts/seed.ts`). You no longer need a separate manual `yarn db:seed` before `yarn test:e2e` on a fresh scaffold.
14
+
13
15
  ## Kitchen-sink CLI catalog (MARS-041, Option A)
14
16
 
15
17
  Full **`mars generate`** surface is exercised against a **materialized kitchen-sink fixture** (not the bare template tree):
@@ -32,15 +32,15 @@
32
32
  "@mars-stack/ui": "*",
33
33
  "@prisma/adapter-pg": "^7.6.0",
34
34
  "@prisma/client": "^7.6.0",
35
- "dotenv": "^17.3.1",
36
- "@react-email/components": "^1.0.10",
35
+ "dotenv": "^17.4.1",
36
+ "@react-email/components": "^1.0.11",
37
37
  "@sendgrid/mail": "^8.1.0",
38
38
  "@upstash/ratelimit": "^2.0.0",
39
39
  "@upstash/redis": "^1.37.0",
40
40
  "bcryptjs": "^3.0.3",
41
41
  "clsx": "^2.1.1",
42
42
  "jose": "^6.2.2",
43
- "next": "^16.2.1",
43
+ "next": "^16.2.2",
44
44
  "pino": "^9.6.0",
45
45
  "pino-pretty": "^13.0.0",
46
46
  "react": "^19.0.0",
@@ -56,17 +56,17 @@
56
56
  },
57
57
  "devDependencies": {
58
58
  "@eslint/eslintrc": "^3.3.5",
59
- "@playwright/test": "^1.50.0",
59
+ "@playwright/test": "^1.59.1",
60
60
  "@tailwindcss/postcss": "^4.2.2",
61
61
  "@testing-library/jest-dom": "^6.9.1",
62
62
  "@testing-library/react": "^16.0.0",
63
63
  "@types/bcryptjs": "^3.0.0",
64
- "@types/node": "^25.5.0",
64
+ "@types/node": "^25.5.2",
65
65
  "@types/react": "^19.0.0",
66
66
  "@types/react-dom": "^19.0.0",
67
67
  "@vitejs/plugin-react": "^5.1.4",
68
68
  "eslint": "^9.0.0",
69
- "eslint-config-next": "^16.2.1",
69
+ "eslint-config-next": "^16.2.2",
70
70
  "jsdom": "^29.0.1",
71
71
  "postcss": "^8.5.0",
72
72
  "prettier": "^3.5.0",
@@ -74,7 +74,7 @@
74
74
  "prisma": "^7.6.0",
75
75
  "tailwindcss": "^4.0.0",
76
76
  "tsx": "^4.0.0",
77
- "typescript": "^5.7.0",
77
+ "typescript": "^6.0.2",
78
78
  "vitest": "^4.1.2"
79
79
  }
80
80
  }
@@ -23,16 +23,13 @@ export default defineConfig({
23
23
  trace: 'on-first-retry',
24
24
  screenshot: 'only-on-failure',
25
25
  },
26
- projects: [
27
- {
28
- name: 'chromium',
29
- use: { ...devices['Desktop Chrome'] },
30
- },
31
- {
32
- name: 'mobile',
33
- use: { ...devices['iPhone 14'] },
34
- },
35
- ],
26
+ // CI (`template-e2e`) installs Chromium only — match that here so dogfood/automation does not fail on WebKit.
27
+ projects: process.env.CI
28
+ ? [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }]
29
+ : [
30
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
31
+ { name: 'mobile', use: { ...devices['iPhone 14'] } },
32
+ ],
36
33
  webServer:
37
34
  process.env.E2E_USE_CI_SERVER === '1'
38
35
  ? {
@@ -10,7 +10,8 @@ import { fileURLToPath } from 'node:url';
10
10
 
11
11
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
12
12
 
13
- const child = spawn('sh', ['-c', 'node scripts/ensure-db.mjs && yarn dev'], {
13
+ /** Match CI `start-e2e-server.mjs`: seed so smoke tests find `user@example.com` without a manual `db:seed`. */
14
+ const child = spawn('sh', ['-c', 'node scripts/ensure-db.mjs && npx prisma db seed && yarn dev'], {
14
15
  cwd: ROOT,
15
16
  stdio: 'inherit',
16
17
  shell: false,
@@ -1,4 +1,4 @@
1
- import { PrismaClient } from '../prisma/generated/prisma/client.js';
1
+ import { PrismaClient } from '@db';
2
2
  import { PrismaPg } from '@prisma/adapter-pg';
3
3
  import { hashPassword } from '@mars-stack/core/auth/password-hash';
4
4
 
@@ -35,8 +35,9 @@ function parseUserAgent(ua: string | null): string {
35
35
  }
36
36
 
37
37
  export default function Settings() {
38
- const { getCSRFHeaders } = useCSRFContext();
38
+ const { getCSRFHeaders, csrfToken, isLoading: csrfLoading } = useCSRFContext();
39
39
  const { user, isLoading: authLoading, updateUser } = useAuth();
40
+ const csrfReady = !csrfLoading && csrfToken !== null;
40
41
 
41
42
  const [name, setName] = useState('');
42
43
  const [nameInitialized, setNameInitialized] = useState(false);
@@ -77,10 +78,12 @@ export default function Settings() {
77
78
  }
78
79
  }, [user, fetchSessions]);
79
80
 
80
- if (!nameInitialized && user?.name) {
81
- setName(user.name);
82
- setNameInitialized(true);
83
- }
81
+ useEffect(() => {
82
+ if (!nameInitialized && user?.name) {
83
+ setName(user.name);
84
+ setNameInitialized(true);
85
+ }
86
+ }, [nameInitialized, user?.name]);
84
87
 
85
88
  if (authLoading) {
86
89
  return (
@@ -96,6 +99,7 @@ export default function Settings() {
96
99
 
97
100
  async function handleNameSubmit(event: FormEvent<HTMLFormElement>) {
98
101
  event.preventDefault();
102
+ if (!csrfReady) return;
99
103
  setNameStatus(null);
100
104
  setNameSaving(true);
101
105
 
@@ -124,6 +128,7 @@ export default function Settings() {
124
128
  }
125
129
 
126
130
  async function handleRevokeSession(sessionId: string) {
131
+ if (!csrfReady) return;
127
132
  setRevokingId(sessionId);
128
133
  setSessionsStatus(null);
129
134
  try {
@@ -147,6 +152,7 @@ export default function Settings() {
147
152
  }
148
153
 
149
154
  async function handleRevokeAllSessions() {
155
+ if (!csrfReady) return;
150
156
  setRevokingAll(true);
151
157
  setSessionsStatus(null);
152
158
  try {
@@ -171,6 +177,7 @@ export default function Settings() {
171
177
 
172
178
  async function handlePasswordSubmit(event: FormEvent<HTMLFormElement>) {
173
179
  event.preventDefault();
180
+ if (!csrfReady) return;
174
181
  setPasswordStatus(null);
175
182
 
176
183
  if (newPassword !== confirmPassword) {
@@ -236,7 +243,11 @@ export default function Settings() {
236
243
  </p>
237
244
  )}
238
245
 
239
- <Button type="submit" loading={nameSaving} disabled={!name.trim()}>
246
+ <Button
247
+ type="submit"
248
+ loading={nameSaving}
249
+ disabled={!name.trim() || !csrfReady}
250
+ >
240
251
  Save Name
241
252
  </Button>
242
253
  </form>
@@ -299,7 +310,9 @@ export default function Settings() {
299
310
  <Button
300
311
  type="submit"
301
312
  loading={passwordSaving}
302
- disabled={!currentPassword || !newPassword || !confirmPassword}
313
+ disabled={
314
+ !currentPassword || !newPassword || !confirmPassword || !csrfReady
315
+ }
303
316
  >
304
317
  Change Password
305
318
  </Button>
@@ -314,6 +327,7 @@ export default function Settings() {
314
327
  variant="subtle"
315
328
  size="sm"
316
329
  loading={revokingAll}
330
+ disabled={!csrfReady}
317
331
  onClick={handleRevokeAllSessions}
318
332
  >
319
333
  Revoke All
@@ -359,6 +373,7 @@ export default function Settings() {
359
373
  variant="subtle"
360
374
  size="sm"
361
375
  loading={revokingId === session.id}
376
+ disabled={!csrfReady}
362
377
  onClick={() => handleRevokeSession(session.id)}
363
378
  >
364
379
  Revoke
@@ -1,11 +1,13 @@
1
1
  // @vitest-environment node
2
2
  import { describe, it, expect, vi, beforeEach } from 'vitest';
3
3
 
4
- const { mockRequireCSRFToken } = vi.hoisted(() => ({
4
+ const { mockRequireCSRFToken, mockGetSession } = vi.hoisted(() => ({
5
5
  mockRequireCSRFToken: vi.fn(),
6
+ mockGetSession: vi.fn(),
6
7
  }));
7
8
 
8
9
  vi.mock('@/lib/mars', () => ({
10
+ getSession: mockGetSession,
9
11
  requireCSRFToken: mockRequireCSRFToken,
10
12
  }));
11
13
 
@@ -14,6 +16,7 @@ import { GET } from './route';
14
16
  describe('GET /api/csrf', () => {
15
17
  beforeEach(() => {
16
18
  vi.clearAllMocks();
19
+ mockGetSession.mockResolvedValueOnce(null);
17
20
  });
18
21
 
19
22
  it('returns token and csrfToken with the same value', async () => {
@@ -1,9 +1,10 @@
1
- import { requireCSRFToken } from '@/lib/mars';
1
+ import { getSession, requireCSRFToken } from '@/lib/mars';
2
2
  import { NextResponse } from 'next/server';
3
3
 
4
4
  export async function GET() {
5
5
  try {
6
- const token = await requireCSRFToken();
6
+ const session = await getSession();
7
+ const token = await requireCSRFToken(session?.fingerprint);
7
8
  return NextResponse.json({ token, csrfToken: token }, { status: 200 });
8
9
  } catch (error) {
9
10
  console.error('CSRF token generation error:', error);