rahman-resources 1.9.2 → 1.10.0
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/lib/manifest.json +548 -1105
- package/lib/slice-schema.json +5 -0
- package/lib/starter/_env.example +11 -2
- package/lib/starter/_package.json +4 -1
- package/lib/starter/components/convex-provider.tsx +15 -5
- package/lib/starter/convex/schema.ts +19 -0
- package/lib/starter/convex/settings.ts +51 -0
- package/lib/starter/convex/setup.ts +22 -0
- package/lib/starter/convex/update.ts +47 -0
- package/lib/starter/lib/headless-core/index.ts +5 -0
- package/lib/starter/lib/headless-core/settings.ts +20 -0
- package/lib/starter/lib/headless-core/version.ts +31 -0
- package/lib/starter/scripts/setup-auth.mjs +53 -0
- package/lib/starter/scripts/smoke-test.mjs +67 -0
- package/lib/starter/vercel.json +4 -0
- package/lib/starter/version.json +5 -0
- package/package.json +1 -1
package/lib/slice-schema.json
CHANGED
|
@@ -135,6 +135,11 @@
|
|
|
135
135
|
"reason": { "type": "string" }
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
},
|
|
139
|
+
"sharedFiles": {
|
|
140
|
+
"type": "array",
|
|
141
|
+
"items": { "type": "string" },
|
|
142
|
+
"description": "Repo-root shared files (relative to project root) this slice imports — the CLI copies them alongside the slice on add."
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
145
|
},
|
package/lib/starter/_env.example
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
# Convex (self-hosted or cloud)
|
|
1
|
+
# Convex (self-hosted or cloud) — set BOTH in your host (e.g. Vercel).
|
|
2
2
|
NEXT_PUBLIC_CONVEX_URL=
|
|
3
|
+
# Production deploy key from Convex. Lets the build push functions+schema to
|
|
4
|
+
# Convex (build:auto) and auto-provision the auth keys below.
|
|
5
|
+
CONVEX_DEPLOY_KEY=
|
|
3
6
|
|
|
4
|
-
# @convex-dev/auth — required for auth signing
|
|
7
|
+
# @convex-dev/auth — required for auth signing. AUTO-SET during build by
|
|
8
|
+
# scripts/setup-auth.mjs when CONVEX_DEPLOY_KEY is present (you usually don't
|
|
9
|
+
# touch these). Listed here for local dev / manual fallback.
|
|
5
10
|
JWKS=
|
|
6
11
|
JWT_PRIVATE_KEY=
|
|
7
12
|
SITE_URL=http://localhost:3000
|
|
8
13
|
|
|
14
|
+
# In-app update channel (optional) — set on your Convex deployment to enable the
|
|
15
|
+
# admin "Rebuild now" button. Create at Vercel -> Settings -> Git -> Deploy Hooks.
|
|
16
|
+
VERCEL_DEPLOY_HOOK_URL=
|
|
17
|
+
|
|
9
18
|
# Server actions encryption (multi-instance — pin once, share across instances)
|
|
10
19
|
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=
|
|
11
20
|
|
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
"lint": "next lint",
|
|
11
11
|
"typecheck": "tsc --noEmit",
|
|
12
12
|
"convex:dev": "convex dev",
|
|
13
|
-
"convex:codegen": "convex dev --once"
|
|
13
|
+
"convex:codegen": "convex dev --once",
|
|
14
|
+
"build:auto": "if [ -n \"$CONVEX_DEPLOY_KEY\" ]; then node scripts/setup-auth.mjs && convex deploy --cmd 'next build'; else next build; fi",
|
|
15
|
+
"smoke": "node scripts/smoke-test.mjs"
|
|
14
16
|
},
|
|
15
17
|
"dependencies": {
|
|
16
18
|
"@auth/core": "^0.37.0",
|
|
17
19
|
"@convex-dev/auth": "^0.0.84",
|
|
20
|
+
"jose": "^5.9.6",
|
|
18
21
|
"@radix-ui/react-label": "^2.1.0",
|
|
19
22
|
"@radix-ui/react-slot": "^1.1.0",
|
|
20
23
|
"class-variance-authority": "^0.7.1",
|
|
@@ -10,15 +10,18 @@
|
|
|
10
10
|
// work even when the client hasn't established its WS yet.
|
|
11
11
|
|
|
12
12
|
import { ConvexAuthProvider } from "@convex-dev/auth/react";
|
|
13
|
-
import { ConvexReactClient } from "convex/react";
|
|
13
|
+
import { ConvexReactClient, ConvexProvider } from "convex/react";
|
|
14
14
|
import { ConvexHttpClient } from "convex/browser";
|
|
15
15
|
import { useEffect, useState, type ReactNode } from "react";
|
|
16
16
|
|
|
17
17
|
export function ConvexClientProvider({ children }: { children: ReactNode }) {
|
|
18
18
|
const [mounted, setMounted] = useState(false);
|
|
19
19
|
const [convex] = useState(() => {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Always construct a client so `useQuery` ALWAYS has a ConvexProvider above
|
|
21
|
+
// it and can never throw "Could not find Convex client". If the env var is
|
|
22
|
+
// missing (misconfig), fall back to a non-connecting placeholder — queries
|
|
23
|
+
// stay in the loading state instead of crashing the page.
|
|
24
|
+
const url = process.env.NEXT_PUBLIC_CONVEX_URL || "https://placeholder.convex.cloud";
|
|
22
25
|
const client = new ConvexReactClient(url);
|
|
23
26
|
const http = new ConvexHttpClient(url);
|
|
24
27
|
const orig = client.action.bind(client);
|
|
@@ -32,6 +35,13 @@ export function ConvexClientProvider({ children }: { children: ReactNode }) {
|
|
|
32
35
|
});
|
|
33
36
|
|
|
34
37
|
useEffect(() => setMounted(true), []);
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
// Outer ConvexProvider ALWAYS supplies the client, so `useQuery` can never
|
|
39
|
+
// throw "Could not find Convex client" — during SSR/prerender, during the
|
|
40
|
+
// mount transition, or under the auth provider. ConvexAuthProvider nests
|
|
41
|
+
// client-side only (it errors during static prerender).
|
|
42
|
+
return (
|
|
43
|
+
<ConvexProvider client={convex}>
|
|
44
|
+
{mounted ? <ConvexAuthProvider client={convex}>{children}</ConvexAuthProvider> : children}
|
|
45
|
+
</ConvexProvider>
|
|
46
|
+
);
|
|
37
47
|
}
|
|
@@ -4,6 +4,25 @@ import { v } from "convex/values";
|
|
|
4
4
|
|
|
5
5
|
export default defineSchema({
|
|
6
6
|
...authTables,
|
|
7
|
+
|
|
8
|
+
// Singleton site config — owner identity + branding, written by the admin
|
|
9
|
+
// Settings UI / onboarding, read by the public site. One row. Part of the
|
|
10
|
+
// headless-core engine (type: lib/headless-core/settings.ts).
|
|
11
|
+
siteSettings: defineTable({
|
|
12
|
+
siteName: v.optional(v.string()),
|
|
13
|
+
tagline: v.optional(v.string()),
|
|
14
|
+
ownerName: v.optional(v.string()),
|
|
15
|
+
contactEmail: v.optional(v.string()),
|
|
16
|
+
brandColor: v.optional(v.string()),
|
|
17
|
+
themeDefault: v.optional(v.string()), // "light" | "dark" | "system"
|
|
18
|
+
logoUrl: v.optional(v.string()),
|
|
19
|
+
faviconUrl: v.optional(v.string()),
|
|
20
|
+
socials: v.optional(v.string()), // JSON string
|
|
21
|
+
seoDescription: v.optional(v.string()),
|
|
22
|
+
analyticsId: v.optional(v.string()),
|
|
23
|
+
onboardedAt: v.optional(v.number()),
|
|
24
|
+
}),
|
|
25
|
+
|
|
7
26
|
// Add app tables here. Example:
|
|
8
27
|
// notes: defineTable({
|
|
9
28
|
// userId: v.id("users"),
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { v, ConvexError } from "convex/values";
|
|
2
|
+
import { mutation, query } from "./_generated/server";
|
|
3
|
+
import { getAuthUserId } from "@convex-dev/auth/server";
|
|
4
|
+
|
|
5
|
+
// Public read of site config — used by the public site (title/favicon/brand) and
|
|
6
|
+
// the admin Settings UI. One singleton row.
|
|
7
|
+
export const get = query({
|
|
8
|
+
args: {},
|
|
9
|
+
handler: async (ctx) => {
|
|
10
|
+
return await ctx.db.query("siteSettings").first();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const FIELDS = {
|
|
15
|
+
siteName: v.optional(v.string()),
|
|
16
|
+
tagline: v.optional(v.string()),
|
|
17
|
+
ownerName: v.optional(v.string()),
|
|
18
|
+
contactEmail: v.optional(v.string()),
|
|
19
|
+
brandColor: v.optional(v.string()),
|
|
20
|
+
themeDefault: v.optional(v.string()),
|
|
21
|
+
logoUrl: v.optional(v.string()),
|
|
22
|
+
faviconUrl: v.optional(v.string()),
|
|
23
|
+
socials: v.optional(v.string()),
|
|
24
|
+
seoDescription: v.optional(v.string()),
|
|
25
|
+
analyticsId: v.optional(v.string()),
|
|
26
|
+
// set true on the final onboarding step so the wizard never shows again
|
|
27
|
+
markOnboarded: v.optional(v.boolean()),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Upsert the singleton settings row. Admin-only.
|
|
31
|
+
export const upsert = mutation({
|
|
32
|
+
args: FIELDS,
|
|
33
|
+
handler: async (ctx, args) => {
|
|
34
|
+
const userId = await getAuthUserId(ctx);
|
|
35
|
+
if (!userId) throw new ConvexError("Harus login sebagai admin.");
|
|
36
|
+
|
|
37
|
+
const { markOnboarded, ...fields } = args;
|
|
38
|
+
const patch: Record<string, unknown> = {};
|
|
39
|
+
for (const [k, val] of Object.entries(fields)) {
|
|
40
|
+
if (val !== undefined) patch[k] = val;
|
|
41
|
+
}
|
|
42
|
+
if (markOnboarded) patch.onboardedAt = Date.now();
|
|
43
|
+
|
|
44
|
+
const existing = await ctx.db.query("siteSettings").first();
|
|
45
|
+
if (existing) {
|
|
46
|
+
await ctx.db.patch(existing._id, patch);
|
|
47
|
+
return existing._id;
|
|
48
|
+
}
|
|
49
|
+
return ctx.db.insert("siteSettings", patch);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { query } from "./_generated/server";
|
|
2
|
+
|
|
3
|
+
// Onboarding state for the admin UI. Public (no PII) so the login form and the
|
|
4
|
+
// dashboard setup banner can adapt before/after sign-in. Part of headless-core.
|
|
5
|
+
export const status = query({
|
|
6
|
+
args: {},
|
|
7
|
+
handler: async (ctx) => {
|
|
8
|
+
const owner = await ctx.db.query("users").first();
|
|
9
|
+
const settings = await ctx.db.query("siteSettings").first();
|
|
10
|
+
const keyConfigured = !!process.env.ADMIN_SIGNUP_KEY;
|
|
11
|
+
const ownerClaimed = !!owner;
|
|
12
|
+
return {
|
|
13
|
+
ownerClaimed,
|
|
14
|
+
// Onboarding wizard is done once the owner finishes it (onboardedAt set).
|
|
15
|
+
onboarded: !!settings?.onboardedAt,
|
|
16
|
+
// Signup is open if no owner has claimed yet, or a key is configured (invites).
|
|
17
|
+
signupOpen: !ownerClaimed || keyConfigured,
|
|
18
|
+
// The "Setup key" field is only needed when a key is configured.
|
|
19
|
+
signupKeyRequired: keyConfigured,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { action } from "./_generated/server";
|
|
2
|
+
import { getAuthUserId } from "@convex-dev/auth/server";
|
|
3
|
+
|
|
4
|
+
// In-app update channel (headless-core). A clone can't "git pull" itself, but it
|
|
5
|
+
// CAN tell the owner when the upstream template shipped a newer version and (if
|
|
6
|
+
// they wired a Vercel deploy hook) rebuild on demand. Version comparison happens
|
|
7
|
+
// client-side (the app imports CORE_VERSION); these actions only do what the
|
|
8
|
+
// browser can't: fetch the upstream manifest, and POST the deploy hook.
|
|
9
|
+
|
|
10
|
+
// Upstream manifest on the template's default branch. Hardcoded (not client-
|
|
11
|
+
// supplied) so this can't be pointed at an arbitrary URL. EDIT the slug to your
|
|
12
|
+
// own repo if you fork away from the template.
|
|
13
|
+
const UPSTREAM_VERSION_URL =
|
|
14
|
+
"https://raw.githubusercontent.com/rahmanef63/template-__APP_SLUG__/main/version.json";
|
|
15
|
+
|
|
16
|
+
export const fetchUpstreamVersion = action({
|
|
17
|
+
args: {},
|
|
18
|
+
handler: async () => {
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(UPSTREAM_VERSION_URL, { cache: "no-store" } as RequestInit);
|
|
21
|
+
if (!res.ok) return null;
|
|
22
|
+
const json = (await res.json()) as { version?: string; core?: string; channel?: string };
|
|
23
|
+
if (!json?.version) return null;
|
|
24
|
+
return { version: json.version, core: json.core ?? json.version, channel: json.channel ?? "stable" };
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Rebuild the live site by hitting a Vercel Deploy Hook the owner pasted into
|
|
32
|
+
// Convex env (VERCEL_DEPLOY_HOOK_URL). Admin-gated. No-ops cleanly if unset.
|
|
33
|
+
export const triggerDeploy = action({
|
|
34
|
+
args: {},
|
|
35
|
+
handler: async (ctx) => {
|
|
36
|
+
const userId = await getAuthUserId(ctx);
|
|
37
|
+
if (!userId) return { ok: false as const, reason: "unauthorized" as const };
|
|
38
|
+
const hook = process.env.VERCEL_DEPLOY_HOOK_URL;
|
|
39
|
+
if (!hook) return { ok: false as const, reason: "no-hook" as const };
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(hook, { method: "POST" });
|
|
42
|
+
return { ok: res.ok, reason: res.ok ? ("triggered" as const) : ("hook-failed" as const) };
|
|
43
|
+
} catch {
|
|
44
|
+
return { ok: false as const, reason: "hook-failed" as const };
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// headless-core — the engine every scaffold inherits: version/update plumbing +
|
|
2
|
+
// the site-settings contract. Kept inside the app (not a separate npm package)
|
|
3
|
+
// so a clone stays a single self-contained unit.
|
|
4
|
+
export * from "./version";
|
|
5
|
+
export * from "./settings";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The site-settings contract — the shape the onboarding/admin Settings UI writes
|
|
2
|
+
// and the public surfaces read. Mirrors the Convex `siteSettings` singleton (all
|
|
3
|
+
// optional; one row). Centralised so every surface shares one definition.
|
|
4
|
+
export type SiteSettings = {
|
|
5
|
+
siteName?: string;
|
|
6
|
+
tagline?: string;
|
|
7
|
+
ownerName?: string;
|
|
8
|
+
contactEmail?: string;
|
|
9
|
+
brandColor?: string;
|
|
10
|
+
themeDefault?: string; // "light" | "dark" | "system"
|
|
11
|
+
logoUrl?: string;
|
|
12
|
+
faviconUrl?: string;
|
|
13
|
+
socials?: string; // JSON string
|
|
14
|
+
seoDescription?: string;
|
|
15
|
+
analyticsId?: string;
|
|
16
|
+
onboardedAt?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// The editable subset (everything except the server-managed onboardedAt).
|
|
20
|
+
export type SiteSettingsInput = Omit<SiteSettings, "onboardedAt">;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Single source of version truth for this app + its headless core.
|
|
2
|
+
//
|
|
3
|
+
// `version.json` (repo root) is the machine-readable manifest the in-app update
|
|
4
|
+
// channel reads on BOTH sides: the running app imports CORE_VERSION here, and
|
|
5
|
+
// the upstream check fetches the raw version.json from GitHub to compare.
|
|
6
|
+
//
|
|
7
|
+
// Bump rule: edit version.json (this file imports it — one number to change).
|
|
8
|
+
import manifest from "@/version.json";
|
|
9
|
+
|
|
10
|
+
export const CORE_VERSION: string = manifest.core;
|
|
11
|
+
export const TEMPLATE_VERSION: string = manifest.version;
|
|
12
|
+
export const RELEASE_CHANNEL: string = manifest.channel;
|
|
13
|
+
|
|
14
|
+
// Upstream repo this clone updates from. Defaults to the template repo the CLI
|
|
15
|
+
// scaffolded from; EDIT these to YOUR repo so "check for updates" points at the
|
|
16
|
+
// source you actually pull from (or leave as-is if you track the template).
|
|
17
|
+
export const UPSTREAM_OWNER = "rahmanef63";
|
|
18
|
+
export const UPSTREAM_REPO = "template-__APP_SLUG__";
|
|
19
|
+
export const UPSTREAM_REPO_URL = `https://github.com/${UPSTREAM_OWNER}/${UPSTREAM_REPO}`;
|
|
20
|
+
export const UPSTREAM_VERSION_URL = `https://raw.githubusercontent.com/${UPSTREAM_OWNER}/${UPSTREAM_REPO}/main/version.json`;
|
|
21
|
+
|
|
22
|
+
/** semver-ish compare: returns >0 if a>b, <0 if a<b, 0 if equal. Tolerates "1.2.3". */
|
|
23
|
+
export function compareVersions(a: string, b: string): number {
|
|
24
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
25
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
26
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
27
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
28
|
+
if (d !== 0) return d > 0 ? 1 : -1;
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Build-time auth bootstrap — so a cloner NEVER touches Convex env for login.
|
|
2
|
+
// Runs only when CONVEX_DEPLOY_KEY is present (i.e. a real clone build); the
|
|
3
|
+
// deploy key lets us set the deployment's env. Idempotent: generates the
|
|
4
|
+
// @convex-dev/auth JWT keys once and skips if they already exist (so sessions
|
|
5
|
+
// aren't invalidated on every deploy).
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
if (!process.env.CONVEX_DEPLOY_KEY) {
|
|
9
|
+
console.log("[setup-auth] no CONVEX_DEPLOY_KEY — skipping (demo/local).");
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const npx = (args, capture = false) =>
|
|
14
|
+
execFileSync("npx", args, {
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
stdio: capture ? ["ignore", "pipe", "ignore"] : "inherit",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function envGet(name) {
|
|
20
|
+
try {
|
|
21
|
+
return npx(["convex", "env", "get", name], true).trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (envGet("JWT_PRIVATE_KEY")) {
|
|
28
|
+
console.log("[setup-auth] JWT keys already set — skip.");
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log("[setup-auth] generating auth keys…");
|
|
33
|
+
// Same format as `npx @convex-dev/auth` (jose RS256, newlines → spaces).
|
|
34
|
+
const { generateKeyPair, exportPKCS8, exportJWK } = await import("jose");
|
|
35
|
+
const keys = await generateKeyPair("RS256", { extractable: true });
|
|
36
|
+
const privateKey = (await exportPKCS8(keys.privateKey)).trimEnd().replace(/\n/g, " ");
|
|
37
|
+
const jwk = await exportJWK(keys.publicKey);
|
|
38
|
+
const jwks = JSON.stringify({ keys: [{ use: "sig", ...jwk }] });
|
|
39
|
+
|
|
40
|
+
const site =
|
|
41
|
+
process.env.SITE_URL ||
|
|
42
|
+
(process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
43
|
+
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
44
|
+
: process.env.VERCEL_URL
|
|
45
|
+
? `https://${process.env.VERCEL_URL}`
|
|
46
|
+
: "");
|
|
47
|
+
|
|
48
|
+
// NAME=value form: the value starts with "-----BEGIN", which the CLI would else
|
|
49
|
+
// parse as a flag.
|
|
50
|
+
npx(["convex", "env", "set", `JWT_PRIVATE_KEY=${privateKey}`]);
|
|
51
|
+
npx(["convex", "env", "set", `JWKS=${jwks}`]);
|
|
52
|
+
if (site) npx(["convex", "env", "set", `SITE_URL=${site}`]);
|
|
53
|
+
console.log("[setup-auth] auth keys provisioned ✔");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Clone smoke-test — verifies a fresh clone has everything needed to deploy.
|
|
3
|
+
// Runs LOCALLY (or in a pre-push hook) — no GitHub Actions cloud minutes.
|
|
4
|
+
//
|
|
5
|
+
// node scripts/smoke-test.mjs # full (invariants + tsc + build)
|
|
6
|
+
// node scripts/smoke-test.mjs --no-build # skip next build (fast)
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
const noBuild = process.argv.includes("--no-build");
|
|
11
|
+
let failed = 0;
|
|
12
|
+
const ok = (m) => console.log(` \x1b[32m✓\x1b[0m ${m}`);
|
|
13
|
+
const bad = (m) => { console.log(` \x1b[31m✗ ${m}\x1b[0m`); failed++; };
|
|
14
|
+
|
|
15
|
+
console.log("\n● Required files");
|
|
16
|
+
const required = [
|
|
17
|
+
"version.json",
|
|
18
|
+
"vercel.json",
|
|
19
|
+
"package.json",
|
|
20
|
+
".env.example",
|
|
21
|
+
"convex/auth.ts",
|
|
22
|
+
"convex/schema.ts",
|
|
23
|
+
"convex/settings.ts",
|
|
24
|
+
"convex/setup.ts",
|
|
25
|
+
"scripts/setup-auth.mjs",
|
|
26
|
+
"lib/headless-core/version.ts",
|
|
27
|
+
];
|
|
28
|
+
for (const f of required) (existsSync(f) ? ok(f) : bad(`missing ${f}`));
|
|
29
|
+
|
|
30
|
+
console.log("\n● Manifest + scripts");
|
|
31
|
+
try {
|
|
32
|
+
const v = JSON.parse(readFileSync("version.json", "utf8"));
|
|
33
|
+
v.version && v.core ? ok(`version.json (v${v.version})`) : bad("version.json missing version/core");
|
|
34
|
+
} catch { bad("version.json invalid JSON"); }
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
|
37
|
+
pkg.scripts?.["build:auto"] ? ok("package.json build:auto present") : bad("package.json missing build:auto");
|
|
38
|
+
} catch { bad("package.json invalid"); }
|
|
39
|
+
try {
|
|
40
|
+
const vc = JSON.parse(readFileSync("vercel.json", "utf8"));
|
|
41
|
+
/build:auto/.test(vc.buildCommand ?? "") ? ok("vercel.json buildCommand -> build:auto") : bad("vercel.json buildCommand wrong");
|
|
42
|
+
} catch { bad("vercel.json invalid"); }
|
|
43
|
+
|
|
44
|
+
console.log("\n● Env documentation");
|
|
45
|
+
try {
|
|
46
|
+
const env = readFileSync(".env.example", "utf8");
|
|
47
|
+
for (const k of ["NEXT_PUBLIC_CONVEX_URL", "CONVEX_DEPLOY_KEY"])
|
|
48
|
+
(env.includes(k) ? ok(`.env.example documents ${k}`) : bad(`.env.example missing ${k}`));
|
|
49
|
+
} catch { bad(".env.example unreadable"); }
|
|
50
|
+
|
|
51
|
+
function run(label, cmd) {
|
|
52
|
+
process.stdout.write(`\n● ${label}\n`);
|
|
53
|
+
try {
|
|
54
|
+
execSync(cmd, { stdio: "pipe" });
|
|
55
|
+
ok(`${label} passed`);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
bad(`${label} failed`);
|
|
58
|
+
const out = (e.stdout?.toString() || "") + (e.stderr?.toString() || "");
|
|
59
|
+
console.log(out.split("\n").slice(-25).join("\n"));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
run("Typecheck", "npx tsc --noEmit");
|
|
64
|
+
if (!noBuild) run("Build", "npm run build");
|
|
65
|
+
|
|
66
|
+
console.log(`\n${failed === 0 ? "\x1b[32mSMOKE PASS\x1b[0m" : `\x1b[31mSMOKE FAIL — ${failed} issue(s)\x1b[0m`}\n`);
|
|
67
|
+
process.exit(failed === 0 ? 0 : 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rahman-resources",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Rahman Resources (rr) — shadcn-style installer for vertical slices. `npx resources add <slug>` copies slice into your project's `slices/<slug>/`. You own the files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|