bsmnt 0.6.4 → 0.7.1

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 (108) hide show
  1. package/dist/application/add-integration/index.d.ts.map +1 -1
  2. package/dist/application/add-integration/index.js +30 -4
  3. package/dist/application/add-integration/index.js.map +1 -1
  4. package/dist/application/shared/node-version.d.ts +8 -0
  5. package/dist/application/shared/node-version.d.ts.map +1 -0
  6. package/dist/application/shared/node-version.js +31 -0
  7. package/dist/application/shared/node-version.js.map +1 -0
  8. package/dist/application/shared/strip-ansi.d.ts +3 -0
  9. package/dist/application/shared/strip-ansi.d.ts.map +1 -0
  10. package/dist/application/shared/strip-ansi.js +7 -0
  11. package/dist/application/shared/strip-ansi.js.map +1 -0
  12. package/dist/core/create/execute-plan.d.ts +3 -0
  13. package/dist/core/create/execute-plan.d.ts.map +1 -1
  14. package/dist/core/create/execute-plan.js +3 -0
  15. package/dist/core/create/execute-plan.js.map +1 -1
  16. package/dist/domain/cms.d.ts +2 -0
  17. package/dist/domain/cms.d.ts.map +1 -1
  18. package/dist/domain/cms.js +1 -0
  19. package/dist/domain/cms.js.map +1 -1
  20. package/dist/domain/forms.d.ts +2 -2
  21. package/dist/domain/forms.js +1 -1
  22. package/dist/domain/forms.js.map +1 -1
  23. package/dist/domain/integration.d.ts +7 -3
  24. package/dist/domain/integration.d.ts.map +1 -1
  25. package/dist/domain/integration.js.map +1 -1
  26. package/dist/domain/skills.d.ts +26 -8
  27. package/dist/domain/skills.d.ts.map +1 -1
  28. package/dist/domain/skills.js +30 -9
  29. package/dist/domain/skills.js.map +1 -1
  30. package/dist/index.js +132 -16
  31. package/dist/index.js.map +1 -1
  32. package/dist/infrastructure/create/executor.d.ts.map +1 -1
  33. package/dist/infrastructure/create/executor.js +2 -1
  34. package/dist/infrastructure/create/executor.js.map +1 -1
  35. package/dist/infrastructure/create/init-git-repo.d.ts +11 -7
  36. package/dist/infrastructure/create/init-git-repo.d.ts.map +1 -1
  37. package/dist/infrastructure/create/init-git-repo.js +21 -18
  38. package/dist/infrastructure/create/init-git-repo.js.map +1 -1
  39. package/dist/infrastructure/create/sanity-auth.d.ts +11 -0
  40. package/dist/infrastructure/create/sanity-auth.d.ts.map +1 -0
  41. package/dist/infrastructure/create/sanity-auth.js +72 -0
  42. package/dist/infrastructure/create/sanity-auth.js.map +1 -0
  43. package/dist/infrastructure/create/setup-agent.d.ts +4 -3
  44. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  45. package/dist/infrastructure/create/setup-agent.js +51 -30
  46. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  47. package/dist/infrastructure/create/setup-dotenvx.d.ts.map +1 -1
  48. package/dist/infrastructure/create/setup-dotenvx.js +9 -4
  49. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
  50. package/dist/infrastructure/create/setup-remote.d.ts +14 -0
  51. package/dist/infrastructure/create/setup-remote.d.ts.map +1 -0
  52. package/dist/infrastructure/create/setup-remote.js +139 -0
  53. package/dist/infrastructure/create/setup-remote.js.map +1 -0
  54. package/dist/infrastructure/create/setup-sanity.d.ts +3 -2
  55. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  56. package/dist/infrastructure/create/setup-sanity.js +34 -49
  57. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  58. package/dist/modules/features/env/dotenvx/config.d.ts.map +1 -1
  59. package/dist/modules/features/env/dotenvx/config.js +6 -0
  60. package/dist/modules/features/env/dotenvx/config.js.map +1 -1
  61. package/dist/paths.d.ts +0 -2
  62. package/dist/paths.d.ts.map +1 -1
  63. package/dist/paths.js +0 -2
  64. package/dist/paths.js.map +1 -1
  65. package/package.json +4 -3
  66. package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +160 -0
  67. package/src/agent-skills/manage-env/SKILL.md +5 -3
  68. package/src/agent-skills/rss-feed/SKILL.md +158 -0
  69. package/src/modules/features/cms/sanity/files/app/blog/[slug]/page.tsx +10 -9
  70. package/src/modules/features/cms/sanity/files/app/blog/page.tsx +5 -5
  71. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.config.ts +3 -0
  72. package/src/modules/features/cms/sanity/files/lib/scripts/copy-sanity-mcp.ts +6 -6
  73. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +4 -6
  74. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.config.ts +3 -0
  75. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +5 -4
  76. package/src/modules/features/cms/sanity-pagebuilder/files/lib/scripts/sanity-typegen.ts +17 -26
  77. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/json-ld.tsx +1 -0
  78. package/src/modules/features/env/dotenvx/files/decrypt-env.mjs +12 -12
  79. package/src/modules/features/env/dotenvx/files/push-keys.mjs +9 -9
  80. package/src/modules/features/env/dotenvx/files/save-key.mjs +3 -3
  81. package/src/modules/features/env/dotenvx/files/setup-remote.ts +182 -0
  82. package/src/modules/features/env/dotenvx/files/write-env.mjs +6 -6
  83. package/src/modules/features/forms/hubspot/files/_sanity-adapter/form-block.ts +60 -0
  84. package/src/modules/features/forms/hubspot/files/_sanity-adapter/form-field.ts +30 -0
  85. package/src/modules/features/forms/hubspot/files/_sanity-adapter/form-select-input.tsx +169 -0
  86. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/README.md +114 -0
  87. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/embed.tsx +165 -0
  88. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/form-selection.ts +40 -0
  89. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/form.tsx +29 -0
  90. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/forms.ts +142 -0
  91. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/hubspot-form.css +636 -0
  92. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/skeleton.tsx +24 -0
  93. package/src/templates/next-default/README.md +1 -0
  94. package/src/templates/next-default/biome.json +1 -1
  95. package/src/templates/next-default/components/basement.svg +1 -1
  96. package/src/templates/next-default/knip.json +9 -1
  97. package/src/templates/next-experiments/README.md +1 -0
  98. package/src/templates/next-experiments/biome.json +1 -1
  99. package/src/templates/next-experiments/components/basement.svg +1 -1
  100. package/src/templates/next-experiments/knip.json +9 -1
  101. package/src/templates/next-pagebuilder/README.md +1 -0
  102. package/src/templates/next-pagebuilder/biome.json +1 -1
  103. package/src/templates/next-pagebuilder/knip.json +7 -2
  104. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +1197 -176
  105. package/src/templates/next-webgl/README.md +1 -0
  106. package/src/templates/next-webgl/biome.json +1 -1
  107. package/src/templates/next-webgl/components/basement.svg +1 -1
  108. package/src/templates/next-webgl/knip.json +9 -1
@@ -34,11 +34,12 @@ export default async function BlogArticlePage({ params }: Props) {
34
34
  if (!article) return notFound();
35
35
 
36
36
  const articleUrl = `/blog/${slug}`;
37
- const articleImage = article.featuredImage
38
- ? urlForImage(article.featuredImage).url()
39
- : article.metadata?.image
40
- ? urlForImage(article.metadata.image).url()
41
- : undefined;
37
+ let articleImage: string | undefined;
38
+ if (article.featuredImage) {
39
+ articleImage = urlForImage(article.featuredImage).url();
40
+ } else if (article.metadata?.image) {
41
+ articleImage = urlForImage(article.metadata.image).url();
42
+ }
42
43
  const pageJsonLd = buildPageJsonLd(article.metadata?.jsonLd);
43
44
 
44
45
  return (
@@ -57,13 +58,13 @@ export default async function BlogArticlePage({ params }: Props) {
57
58
  {pageJsonLd.map(({ key, data }) => (
58
59
  <JsonLd key={key} data={data} />
59
60
  ))}
60
- <article className="container mx-auto px-4 py-8 max-w-4xl">
61
+ <article className="container mx-auto max-w-4xl px-4 py-8">
61
62
  <header className="mb-8">
62
- <h1 className="text-4xl font-bold mb-4">{article.title}</h1>
63
+ <h1 className="mb-4 font-bold text-4xl">{article.title}</h1>
63
64
  {article.excerpt && (
64
- <p className="text-lg text-gray-600">{article.excerpt}</p>
65
+ <p className="text-gray-600 text-lg">{article.excerpt}</p>
65
66
  )}
66
- <div className="flex gap-4 mt-4 text-sm text-gray-500">
67
+ <div className="mt-4 flex gap-4 text-gray-500 text-sm">
67
68
  {article.author && <span>By {article.author}</span>}
68
69
  {article.publishedAt && (
69
70
  <time dateTime={article.publishedAt}>
@@ -13,8 +13,8 @@ export default async function BlogIndexPage() {
13
13
  const { data: articles } = await sanityFetch({ query: ALL_ARTICLES_QUERY });
14
14
 
15
15
  return (
16
- <main className="container mx-auto px-4 py-8 max-w-4xl">
17
- <h1 className="text-4xl font-bold mb-8">Blog</h1>
16
+ <main className="container mx-auto max-w-4xl px-4 py-8">
17
+ <h1 className="mb-8 font-bold text-4xl">Blog</h1>
18
18
 
19
19
  {articles && articles.length > 0 ? (
20
20
  <ul className="flex flex-col gap-8">
@@ -25,15 +25,15 @@ export default async function BlogIndexPage() {
25
25
  return (
26
26
  <li key={article._id}>
27
27
  <article>
28
- <h2 className="text-2xl font-semibold mb-2">
28
+ <h2 className="mb-2 font-semibold text-2xl">
29
29
  <Link href={`/blog/${slug}`} className="hover:underline">
30
30
  {article.title}
31
31
  </Link>
32
32
  </h2>
33
33
  {article.excerpt && (
34
- <p className="text-gray-600 mb-2">{article.excerpt}</p>
34
+ <p className="mb-2 text-gray-600">{article.excerpt}</p>
35
35
  )}
36
- <div className="flex gap-4 text-sm text-gray-500">
36
+ <div className="flex gap-4 text-gray-500 text-sm">
37
37
  {article.author && <span>By {article.author}</span>}
38
38
  {article.publishedAt && (
39
39
  <time dateTime={article.publishedAt}>
@@ -36,6 +36,9 @@ export default defineConfig({
36
36
  projectId,
37
37
  dataset,
38
38
  schema,
39
+ // Content Releases is an Enterprise-only feature; hide it from the Studio.
40
+ releases: { enabled: false },
41
+ scheduledDrafts: { enabled: false },
39
42
  plugins: [
40
43
  structureTool({ structure }),
41
44
  // Presentation tool for visual editing
@@ -10,14 +10,14 @@ const link =
10
10
 
11
11
  const copied = await copyToClipboard(link);
12
12
 
13
- console.log("\n🔗 Sanity MCP Setup for Cursor\n");
13
+ console.info("\n🔗 Sanity MCP Setup for Cursor\n");
14
14
 
15
15
  if (copied) {
16
- console.log("✅ Link copied to clipboard!");
17
- console.log(" Paste it in your browser or Cursor address bar.\n");
16
+ console.info("✅ Link copied to clipboard!");
17
+ console.info(" Paste it in your browser or Cursor address bar.\n");
18
18
  } else {
19
- console.log("📋 Copy this link manually:\n");
20
- console.log(` ${link}\n`);
19
+ console.info("📋 Copy this link manually:\n");
20
+ console.info(` ${link}\n`);
21
21
  }
22
22
 
23
- console.log("This will install the Sanity MCP server in Cursor.");
23
+ console.info("This will install the Sanity MCP server in Cursor.");
@@ -198,12 +198,10 @@ export const PageTreePane = ({ options }: PageTreePaneProps) => {
198
198
  {showPinned && (
199
199
  <Stack>
200
200
  {PINNED_PAGE_ENTRIES.map((entry) => {
201
- const activeState: ActiveState =
202
- activeChildId === entry.id
203
- ? isLeafPane
204
- ? "leaf"
205
- : "ancestor"
206
- : "none";
201
+ let activeState: ActiveState = "none";
202
+ if (activeChildId === entry.id) {
203
+ activeState = isLeafPane ? "leaf" : "ancestor";
204
+ }
207
205
  return (
208
206
  <PinnedRow
209
207
  key={entry.id}
@@ -28,6 +28,9 @@ export default defineConfig({
28
28
  basePath: "/studio",
29
29
  projectId,
30
30
  dataset,
31
+ // Content Releases is an Enterprise-only feature; hide it from the Studio.
32
+ releases: { enabled: false },
33
+ scheduledDrafts: { enabled: false },
31
34
  schema: {
32
35
  ...schema,
33
36
  templates: (previousTemplates) => [
@@ -106,11 +106,12 @@ export const redirect = defineType({
106
106
  permanent: "permanent",
107
107
  },
108
108
  prepare({ source, destination, permanent }) {
109
- const target = destination
110
- ? /^https?:\/\//.test(destination)
109
+ let target = "?";
110
+ if (destination) {
111
+ target = /^https?:\/\//.test(destination)
111
112
  ? destination
112
- : `/${destination}`
113
- : "?";
113
+ : `/${destination}`;
114
+ }
114
115
  return {
115
116
  title: source ? `/${source}` : "Untitled redirect",
116
117
  subtitle: `→ ${target} (${permanent ? "308" : "307"})`,
@@ -1,43 +1,34 @@
1
1
  /**
2
- * Wrapper around `sanity schema extract && sanity typegen generate`
3
- * that gracefully skips when Sanity is not configured.
2
+ * Wrapper around `sanity schema extract && sanity typegen generate`; skips when
3
+ * Sanity isn't configured. Reads the project id from `process.env` — the
4
+ * dotenvx-wrapped script already decrypted `.env` into it — so it runs on Vercel
5
+ * too, which has no `.env.local`.
4
6
  */
5
7
  import { execSync } from "node:child_process";
6
- import { existsSync } from "node:fs";
7
8
  import { resolve } from "node:path";
8
9
 
9
10
  const sanityDir = resolve(import.meta.dir, "../integrations/sanity");
10
- const envFile = resolve(import.meta.dir, "../../.env.local");
11
11
 
12
- // Check if Sanity project ID is configured
13
- const hasEnvFile = existsSync(envFile);
14
-
15
- if (!hasEnvFile) {
16
- console.log(" Skipping sanity:typegen — .env.local not found");
17
- process.exit(0);
18
- }
19
-
20
- // Read .env.local to check for project ID
21
- const envContent = Bun.file(envFile);
22
- const text = await envContent.text();
23
- const hasProjectId = text
24
- .split("\n")
25
- .some(
26
- (line) =>
27
- line.startsWith("NEXT_PUBLIC_SANITY_PROJECT_ID=") &&
28
- line.split("=")[1]?.trim() !== "",
29
- );
30
-
31
- if (!hasProjectId) {
32
- console.log(
12
+ if (!process.env.NEXT_PUBLIC_SANITY_PROJECT_ID) {
13
+ console.info(
33
14
  " Skipping sanity:typegen — NEXT_PUBLIC_SANITY_PROJECT_ID not set",
34
15
  );
35
16
  process.exit(0);
36
17
  }
37
18
 
19
+ // Format the generated file to the project's biome style so the committed types
20
+ // match lint — but only locally. In CI/Vercel the build discards the formatted
21
+ // result, so running biome there is pure per-deploy waste. (Scoped to the one
22
+ // file; linting the whole dir would rewrite unrelated source.)
23
+ const format =
24
+ process.env.CI || process.env.VERCEL
25
+ ? ""
26
+ : " && bun biome check --write --unsafe sanity.types.ts";
27
+
38
28
  try {
29
+ // No `--env-file` — dotenvx already injected the env into process.env.
39
30
  execSync(
40
- `cd ${sanityDir} && bun --env-file ../../../.env.local sanity schema extract && bun --env-file ../../../.env.local sanity typegen generate && bun biome check --write --unsafe`,
31
+ `cd ${sanityDir} && bun sanity schema extract && bun sanity typegen generate${format}`,
41
32
  { stdio: "inherit" },
42
33
  );
43
34
  } catch {
@@ -27,6 +27,7 @@ const serializeJsonLd = <T extends Thing>(data: WithContext<T>) =>
27
27
  export const JsonLd = <T extends Thing>({ data }: JsonLdProps<T>) => (
28
28
  <script
29
29
  type="application/ld+json"
30
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: serialized JSON-LD, not user HTML; `<` is escaped
30
31
  dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
31
32
  />
32
33
  );
@@ -17,21 +17,21 @@ if (process.env.CI || process.env.VERCEL) {
17
17
  }
18
18
 
19
19
  const args = process.argv.slice(2);
20
- const file =
21
- args.includes("--prod") || args.includes("--production")
22
- ? ".env.production"
23
- : args.includes("--preview")
24
- ? ".env.preview"
25
- : ".env";
20
+ let file = ".env";
21
+ if (args.includes("--prod") || args.includes("--production")) {
22
+ file = ".env.production";
23
+ } else if (args.includes("--preview")) {
24
+ file = ".env.preview";
25
+ }
26
26
 
27
27
  // Friendlier than dotenvx's MISSING_ENV_FILE error when the file isn't created yet.
28
28
  if (!existsSync(file)) {
29
- const flag =
30
- file === ".env.production"
31
- ? " --prod"
32
- : file === ".env.preview"
33
- ? " --preview"
34
- : "";
29
+ let flag = "";
30
+ if (file === ".env.production") {
31
+ flag = " --prod";
32
+ } else if (file === ".env.preview") {
33
+ flag = " --preview";
34
+ }
35
35
  console.error(
36
36
  `No ${file} yet. Add values with: bun run env:set KEY value${flag}`,
37
37
  );
@@ -25,13 +25,12 @@ export function parsePrivateKeys(content) {
25
25
  return keys;
26
26
  }
27
27
 
28
- // The base key decrypts the shared .env (loaded in every environment); a
29
- // suffixed key (…_PRODUCTION / …_PREVIEW) decrypts only that environment's file.
28
+ // Base key Production + Preview (where the server decrypts .env); a suffixed
29
+ // key (…_PRODUCTION / …_PREVIEW) targets just that env. No Development: local
30
+ // dev decrypts with .env.keys, not a Vercel var.
30
31
  export function vercelEnvsFor(name) {
31
32
  const suffix = name.slice("DOTENV_PRIVATE_KEY".length).replace(/^_/, "");
32
- return suffix
33
- ? [suffix.toLowerCase()]
34
- : ["production", "preview", "development"];
33
+ return suffix ? [suffix.toLowerCase()] : ["production", "preview"];
35
34
  }
36
35
 
37
36
  // Ask which remote when no arg was given. Never hang a non-interactive shell.
@@ -97,12 +96,13 @@ async function main() {
97
96
  let ok = true;
98
97
  for (const { name, value } of keys) {
99
98
  if (target === "github") {
100
- console.log(`→ gh secret set ${name}`);
99
+ console.info(`→ gh secret set ${name}`);
101
100
  ok = pipe("gh", ["secret", "set", name], value) && ok;
102
101
  } else {
103
102
  for (const env of vercelEnvsFor(name)) {
104
- console.log(`→ vercel env add ${name} ${env}`);
105
- ok = pipe("vercel", ["env", "add", name, env], value) && ok;
103
+ console.info(`→ vercel env add ${name} ${env} (sensitive)`);
104
+ // --sensitive: write-only, never shown in the dashboard.
105
+ ok = pipe("vercel", ["env", "add", name, env, "--sensitive"], value) && ok;
106
106
  }
107
107
  }
108
108
  }
@@ -116,7 +116,7 @@ async function main() {
116
116
  process.exit(1);
117
117
  }
118
118
 
119
- console.log(
119
+ console.info(
120
120
  target === "vercel"
121
121
  ? "\nDone. Redeploy for the new env vars to take effect."
122
122
  : "\nDone.",
@@ -131,6 +131,6 @@ try {
131
131
  const ref = `op://${VAULT}/${itemRef}/private-key`;
132
132
  setOpRef(ref);
133
133
 
134
- console.log(`\n✓ Saved to 1Password (${VAULT} / "${title}").`);
135
- console.log(` Set DOTENV_PRIVATE_KEY_OP_REF=${ref} in .env.`);
136
- console.log(" Teammates can now restore with `bun run env:setup`.");
134
+ console.info(`\n✓ Saved to 1Password (${VAULT} / "${title}").`);
135
+ console.info(` Set DOTENV_PRIVATE_KEY_OP_REF=${ref} in .env.`);
136
+ console.info(" Teammates can now restore with `bun run env:setup`.");
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Create the GitHub repo, link a Vercel project to it, and push the dotenvx key
3
+ * to Vercel env — before the git connection, so the first push-to-deploy can
4
+ * decrypt the committed `.env`. No GitHub secret: CI reads the key from
5
+ * 1Password via OP_SERVICE_ACCOUNT_TOKEN.
6
+ *
7
+ * bun setup-remote # private repo under your account (default)
8
+ * bun setup-remote --public # public repo
9
+ * bun setup-remote --org my-org # create under an org
10
+ *
11
+ * The CLI runs the same sequence inline during `bsmnt create`
12
+ * (src/infrastructure/create/setup-remote.ts); keep the two in sync.
13
+ */
14
+ import { spawnSync } from "node:child_process";
15
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
16
+ import path from "node:path";
17
+
18
+ const GH_HINT =
19
+ "gh CLI not found — install it (https://cli.github.com) and run `gh auth login`.";
20
+ const VERCEL_HINT =
21
+ "vercel CLI not found — install it (`bun add -g vercel`) and try again.";
22
+
23
+ /** Run an interactive command, letting the user answer its prompts directly. */
24
+ function run(cmd: string, args: string[], notFoundHint: string): boolean {
25
+ const res = spawnSync(cmd, args, { stdio: "inherit" });
26
+ if ((res.error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
27
+ console.error(`\n${notFoundHint}`);
28
+ process.exit(1);
29
+ }
30
+ return res.status === 0;
31
+ }
32
+
33
+ /**
34
+ * Push the private keys to Vercel env via the tested push-keys path, rather
35
+ * than re-implementing the per-key (stdin-piped) upload here.
36
+ */
37
+ function pushVercelKeys(): boolean {
38
+ return (
39
+ spawnSync("bun", ["run", "scripts/push-keys.mjs", "vercel"], {
40
+ stdio: "inherit",
41
+ }).status === 0
42
+ );
43
+ }
44
+
45
+ function hasOriginRemote(): boolean {
46
+ return (
47
+ spawnSync("git", ["remote", "get-url", "origin"], { stdio: "ignore" })
48
+ .status === 0
49
+ );
50
+ }
51
+
52
+ function ghReady(): boolean {
53
+ const res = spawnSync("gh", ["auth", "status"], { stdio: "ignore" });
54
+ return (
55
+ (res.error as NodeJS.ErrnoException | undefined)?.code !== "ENOENT" &&
56
+ res.status === 0
57
+ );
58
+ }
59
+
60
+ /** True when `.env.keys` holds at least one non-empty DOTENV_PRIVATE_KEY*. */
61
+ function hasPrivateKey(): boolean {
62
+ if (!existsSync(".env.keys")) return false;
63
+ for (const line of readFileSync(".env.keys", "utf8").split("\n")) {
64
+ const value = line
65
+ .match(/^\s*DOTENV_PRIVATE_KEY[A-Z_]*\s*=\s*(.+?)\s*$/)?.[1]
66
+ ?.replace(/^["']|["']$/g, "");
67
+ if (value) return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ function main(): void {
73
+ // No key means a deploy can't decrypt `.env` — nothing to wire up yet.
74
+ if (!hasPrivateKey()) {
75
+ console.error(
76
+ "No DOTENV_PRIVATE_KEY* in .env.keys — run `bun run env:setup` first so there's a key to push.",
77
+ );
78
+ process.exit(1);
79
+ }
80
+
81
+ // GitHub: ensure the repo exists so Vercel can git-connect to it.
82
+ let connected = hasOriginRemote();
83
+ if (!connected) {
84
+ if (ghReady()) {
85
+ // `--source` forces non-interactive mode, which requires a visibility flag.
86
+ const visibility = process.argv.includes("--public")
87
+ ? "--public"
88
+ : "--private";
89
+ const orgIndex = process.argv.indexOf("--org");
90
+ const owner = orgIndex !== -1 ? process.argv[orgIndex + 1] : undefined;
91
+ const nameArg = owner ? [`${owner}/${path.basename(process.cwd())}`] : [];
92
+ console.info(`→ gh repo create (${visibility})`);
93
+ // No `--push` here: the push must come after `vercel git connect`, or
94
+ // the git integration never sees it and skips the first deployment.
95
+ if (
96
+ run(
97
+ "gh",
98
+ ["repo", "create", ...nameArg, "--source=.", "--remote=origin", visibility],
99
+ GH_HINT,
100
+ )
101
+ ) {
102
+ connected = hasOriginRemote();
103
+ }
104
+ } else {
105
+ console.info(
106
+ "\nNo `origin` remote and the gh CLI isn't ready — skipping GitHub setup.\n" +
107
+ " Install gh (https://cli.github.com), run `gh auth login`, then re-run\n" +
108
+ " (or create the repo manually and re-run).",
109
+ );
110
+ }
111
+ }
112
+
113
+ // Vercel: link the project and push the keys as env vars.
114
+ if (spawnSync("vercel", ["whoami"], { stdio: "ignore" }).status !== 0) {
115
+ console.info("→ vercel login");
116
+ if (!run("vercel", ["login"], VERCEL_HINT)) process.exit(1);
117
+ }
118
+ // Pick the scope via Vercel's team picker, then link with `--yes` so the
119
+ // validated lowercase dir name is used (the `Name?` prompt rejects uppercase).
120
+ console.info("→ vercel switch + link");
121
+ run("vercel", ["switch"], VERCEL_HINT);
122
+ if (!run("vercel", ["link", "--yes"], VERCEL_HINT)) {
123
+ console.error("\nLink cancelled — re-run `bun setup-remote` when ready.");
124
+ process.exit(1);
125
+ }
126
+ // Pin the Next.js framework so Vercel resolves the .next output (it sometimes
127
+ // creates the project as "Other"); commit it so the push below ships it.
128
+ if (!existsSync("vercel.json")) {
129
+ writeFileSync(
130
+ "vercel.json",
131
+ '{\n "$schema": "https://openapi.vercel.sh/vercel.json",\n "framework": "nextjs"\n}\n',
132
+ );
133
+ spawnSync("git", ["add", "vercel.json"], { stdio: "ignore" });
134
+ spawnSync(
135
+ "git",
136
+ [
137
+ "commit",
138
+ "-m",
139
+ "chore: pin Next.js framework for Vercel",
140
+ "--no-verify",
141
+ "--",
142
+ "vercel.json",
143
+ ],
144
+ { stdio: "ignore" },
145
+ );
146
+ }
147
+ console.info("→ uploading private keys to Vercel env");
148
+ if (!pushVercelKeys()) process.exit(1);
149
+
150
+ // Connect the two so pushes build from a clone (keys already in place).
151
+ if (!connected) {
152
+ console.info(
153
+ "\n✓ Vercel linked and keys uploaded. No GitHub repo connected yet — once\n" +
154
+ " one exists, run `vercel git connect` for push-to-deploy.",
155
+ );
156
+ return;
157
+ }
158
+ console.info("→ vercel git connect");
159
+ if (!run("vercel", ["git", "connect"], VERCEL_HINT)) {
160
+ console.error(
161
+ "\nKeys uploaded, but `vercel git connect` failed. If the Git integration\n" +
162
+ "isn't installed, add it once from the Vercel dashboard (Account/Team →\n" +
163
+ "Integrations → GitHub), then re-run `vercel git connect`.",
164
+ );
165
+ process.exit(1);
166
+ }
167
+
168
+ // Push after connecting so the git integration builds the first deployment.
169
+ console.info("→ git push");
170
+ if (!run("git", ["push", "-u", "origin", "HEAD"], "git not found.")) {
171
+ console.error(
172
+ "\nConnected, but the push failed — run `git push` to trigger the first deploy.",
173
+ );
174
+ process.exit(1);
175
+ }
176
+
177
+ console.info(
178
+ "\n✓ GitHub + Vercel ready. Pushed — Vercel is building the first deployment.",
179
+ );
180
+ }
181
+
182
+ main();
@@ -12,12 +12,12 @@ const [mode, ...rest] = process.argv.slice(2);
12
12
 
13
13
  // --prod / --preview pick the per-environment file; default is the shared .env.
14
14
  const ENV_FLAGS = new Set(["--prod", "--production", "--preview"]);
15
- const file =
16
- rest.includes("--prod") || rest.includes("--production")
17
- ? ".env.production"
18
- : rest.includes("--preview")
19
- ? ".env.preview"
20
- : ".env";
15
+ let file = ".env";
16
+ if (rest.includes("--prod") || rest.includes("--production")) {
17
+ file = ".env.production";
18
+ } else if (rest.includes("--preview")) {
19
+ file = ".env.preview";
20
+ }
21
21
 
22
22
  // On bulk encrypt, keep browser-public NEXT_PUBLIC_* and the 1Password pointer
23
23
  // plaintext (same exclusions the scaffold uses).
@@ -0,0 +1,60 @@
1
+ import { EnvelopeIcon } from "@sanity/icons"
2
+ import { defineField, defineType } from "sanity"
3
+ import {
4
+ decodeHubSpotFormSelection,
5
+ getHubSpotFormSelectionLabel,
6
+ } from "../form-selection"
7
+ import { hubspotFormField } from "./form-field"
8
+
9
+ /**
10
+ * Sample reusable HubSpot form block. Register it in your schema's document
11
+ * list (and page-builder config, if you use one), then render its `form` with
12
+ * <HubSpotForm /> from the integration core:
13
+ *
14
+ * @example
15
+ * import { HubSpotForm } from "@/lib/integrations/hubspot/form"
16
+ * import { decodeHubSpotFormSelection } from "@/lib/integrations/hubspot/form-selection"
17
+ *
18
+ * const selection = decodeHubSpotFormSelection(block.form)
19
+ * if (!selection) return null
20
+ * return <HubSpotForm {...selection} />
21
+ */
22
+ export const hubspotFormBlock = defineType({
23
+ name: "hubspotFormBlock",
24
+ title: "HubSpot Form Block",
25
+ type: "document",
26
+ icon: EnvelopeIcon,
27
+ fields: [
28
+ defineField({
29
+ name: "name",
30
+ title: "Name",
31
+ type: "string",
32
+ description: "Used to identify this block in the Studio.",
33
+ validation: (Rule) => Rule.required(),
34
+ }),
35
+ hubspotFormField,
36
+ ],
37
+ preview: {
38
+ select: {
39
+ name: "name",
40
+ form: "form",
41
+ },
42
+ prepare({ name, form }) {
43
+ const selectedForm = decodeHubSpotFormSelection(form)
44
+
45
+ return {
46
+ title: name || "Untitled",
47
+ ...(selectedForm
48
+ ? { subtitle: getHubSpotFormSelectionLabel(selectedForm) }
49
+ : {}),
50
+ }
51
+ },
52
+ },
53
+ })
54
+
55
+ export const hubspotFormBlockReference = defineType({
56
+ name: "hubspotFormBlockReference",
57
+ title: "HubSpot Form Block",
58
+ type: "reference",
59
+ to: [{ type: "hubspotFormBlock" }],
60
+ })
@@ -0,0 +1,30 @@
1
+ import { defineField } from "sanity"
2
+ import { decodeHubSpotFormSelection } from "../form-selection"
3
+ import { HubSpotFormSelectInput } from "./form-select-input"
4
+
5
+ /**
6
+ * Shared HubSpot form picker field. Stores the selected form as one encoded
7
+ * string and validates it decodes to a real selection. Reuse it in any block
8
+ * or document that embeds a HubSpot form.
9
+ *
10
+ * @example
11
+ * defineType({
12
+ * name: "contactBlock",
13
+ * type: "object",
14
+ * fields: [hubspotFormField],
15
+ * })
16
+ */
17
+ export const hubspotFormField = defineField({
18
+ name: "form",
19
+ title: "HubSpot Form",
20
+ type: "string",
21
+ components: {
22
+ input: HubSpotFormSelectInput,
23
+ },
24
+ description:
25
+ "Fetches HubSpot forms when the dropdown is opened. The selected form is stored as one string.",
26
+ validation: (Rule) =>
27
+ Rule.required().custom((value) =>
28
+ decodeHubSpotFormSelection(value) ? true : "Select a valid HubSpot form."
29
+ ),
30
+ })