@woodsportal/hubspot-kit 1.0.42-dev.0 → 1.0.42-dev.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/dist/{chunk-WHYTQEZX.js → chunk-4UFWXPQG.js} +2 -2
  3. package/dist/{chunk-4GOJPOW3.js → chunk-GXYUGXF5.js} +82 -23
  4. package/dist/chunk-GXYUGXF5.js.map +1 -0
  5. package/dist/cli.js +61 -26
  6. package/dist/cli.js.map +1 -1
  7. package/dist/{exec-VY6BGWF3.js → exec-NS4GKXZS.js} +2 -2
  8. package/dist/{scaffold-page-lifecycle-EHMPWPPE.js → scaffold-page-lifecycle-53AWWGFR.js} +3 -3
  9. package/examples/greenfield/module-hybrid-cdn/README.md +2 -1
  10. package/examples/greenfield/module-hybrid-cdn-auth-full/README.md +2 -1
  11. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/components/starter-login.tsx +2 -6
  12. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/sso-callback.ts +6 -0
  13. package/examples/greenfield/module-hybrid-cdn-auth-sso/README.md +2 -1
  14. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/sso-callback.ts +6 -0
  15. package/examples/greenfield/module-hybrid-cdn-auth-starter/README.md +2 -1
  16. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/README.md +2 -1
  17. package/package.json +3 -2
  18. package/scripts/build-cdn.mjs +6 -5
  19. package/scripts/dist-cli-pack-smoke.test.mjs +79 -0
  20. package/scripts/ensure-wphs-workspace.mjs +7 -2
  21. package/scripts/resolve-hubspot-cli.mjs +51 -0
  22. package/scripts/resolve-vite-config.mjs +2 -3
  23. package/scripts/resolve-vite-spawn.mjs +48 -0
  24. package/scripts/run-vite-build.mjs +5 -6
  25. package/scripts/watch-hubspot.mjs +11 -13
  26. package/templates/module-hybrid-cdn/README.md +2 -1
  27. package/templates/module-monolith/README.md +2 -1
  28. package/templates/scaffold/auth/_full/src/features/auth/components/starter-login.tsx +79 -0
  29. package/templates/scaffold/auth/_shared/src/app.tsx +8 -3
  30. package/templates/scaffold/auth/_shared/src/features/auth/components/starter-login.tsx +10 -3
  31. package/templates/scaffold/auth/_shared/src/integrations/woodsportal-sdk.ts +1 -0
  32. package/templates/scaffold/auth/_sso/src/features/auth/components/starter-login.tsx +10 -3
  33. package/templates/scaffold/auth/_sso/src/features/auth/sso-callback.ts +6 -0
  34. package/dist/chunk-4GOJPOW3.js.map +0 -1
  35. package/dist/ensure-wphs-workspace-5S3GTO6K.js +0 -234
  36. package/dist/ensure-wphs-workspace-5S3GTO6K.js.map +0 -1
  37. /package/dist/{chunk-WHYTQEZX.js.map → chunk-4UFWXPQG.js.map} +0 -0
  38. /package/dist/{exec-VY6BGWF3.js.map → exec-NS4GKXZS.js.map} +0 -0
  39. /package/dist/{scaffold-page-lifecycle-EHMPWPPE.js.map → scaffold-page-lifecycle-53AWWGFR.js.map} +0 -0
@@ -5,7 +5,7 @@ import {
5
5
  runNodeScript,
6
6
  runNodeScriptAsync,
7
7
  runNodeScriptLongRunning
8
- } from "./chunk-4GOJPOW3.js";
8
+ } from "./chunk-GXYUGXF5.js";
9
9
  export {
10
10
  runCommand,
11
11
  runCommandAsync,
@@ -13,4 +13,4 @@ export {
13
13
  runNodeScriptAsync,
14
14
  runNodeScriptLongRunning
15
15
  };
16
- //# sourceMappingURL=exec-VY6BGWF3.js.map
16
+ //# sourceMappingURL=exec-NS4GKXZS.js.map
@@ -8,11 +8,11 @@ import {
8
8
  resolveProjectNavConfig,
9
9
  slugToCamelCase,
10
10
  slugToPascalCase
11
- } from "./chunk-WHYTQEZX.js";
11
+ } from "./chunk-4UFWXPQG.js";
12
12
  import {
13
13
  ExitCode,
14
14
  WpHsError
15
- } from "./chunk-4GOJPOW3.js";
15
+ } from "./chunk-GXYUGXF5.js";
16
16
 
17
17
  // src/lib/scaffold-page-lifecycle.ts
18
18
  import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -191,4 +191,4 @@ export {
191
191
  listScaffoldedPages,
192
192
  removeScaffoldedPage
193
193
  };
194
- //# sourceMappingURL=scaffold-page-lifecycle-EHMPWPPE.js.map
194
+ //# sourceMappingURL=scaffold-page-lifecycle-53AWWGFR.js.map
@@ -5,7 +5,8 @@ This project was scaffolded with **WoodsCLI** (`@woodsportal/hubspot-kit`).
5
5
  ## Daily dev
6
6
 
7
7
  ```bash
8
- yarn install
8
+ npm install
9
+ wp setup
9
10
  wp doctor
10
11
  wp dev
11
12
  ```
@@ -5,7 +5,8 @@ This project was scaffolded with **WoodsCLI** (`@woodsportal/hubspot-kit`).
5
5
  ## Daily dev
6
6
 
7
7
  ```bash
8
- yarn install
8
+ npm install
9
+ wp setup
9
10
  wp doctor
10
11
  wp dev
11
12
  ```
@@ -6,7 +6,7 @@ import { Form, FormField } from '@/components/ui/Form'
6
6
  import { Input } from '@/components/ui/Input'
7
7
  import { SsoLoginButton } from '@/components/ui/auth/SsoLoginButton'
8
8
  import { api } from '@/integrations/woodsportal-sdk'
9
- import { handleAuthLoginResponse, MFA_REQUIRED_SCAFFOLD_MESSAGE } from '@/features/auth/login-session-bridge'
9
+ import { handleAuthLoginResponse } from '@/features/auth/login-session-bridge'
10
10
  import { resolveAuthLoginErrorMessage } from '@/features/auth/resolve-auth-login-error-message'
11
11
  import {
12
12
  getStoredLoginEmail,
@@ -28,11 +28,7 @@ export function StarterLogin() {
28
28
  const response = await login({ username: email.trim(), password })
29
29
  const result = await handleAuthLoginResponse(response, router)
30
30
  if (!result.success) {
31
- if (result.reason === 'mfa_required') {
32
- toast.error(MFA_REQUIRED_SCAFFOLD_MESSAGE)
33
- } else {
34
- toast.error('Login failed — no access token returned.')
35
- }
31
+ toast.error('Login failed — no access token returned.')
36
32
  return
37
33
  }
38
34
  if (!result.mfaRequired) {
@@ -1,6 +1,7 @@
1
1
  import { Routes } from '@/config/routes'
2
2
  import { readHashPathname } from '@/features/auth/auth-route-paths'
3
3
  import { normalizeRouterPath } from '@/utils/normalize-router-path'
4
+ import { WP_STORAGE_KEYS } from 'woodsportal-client-sdk'
4
5
 
5
6
  export type SsoCallbackParams = {
6
7
  code: string
@@ -60,6 +61,11 @@ export function clearPreHashQueryParams(): void {
60
61
  window.history.replaceState(null, '', `${window.location.pathname}${window.location.hash}`)
61
62
  }
62
63
 
64
+ /** Legacy sessionStorage inflight — clear on boot so stale keys never block exchange. */
65
+ export function clearLegacySsoCallbackInFlight(): void {
66
+ sessionStorage.removeItem(`${WP_STORAGE_KEYS.c.sso.cb}:inflight`)
67
+ }
68
+
63
69
  export function isSsoCallbackAlreadyProcessed(key: string): boolean {
64
70
  return processedKeys.has(key)
65
71
  }
@@ -5,7 +5,8 @@ This project was scaffolded with **WoodsCLI** (`@woodsportal/hubspot-kit`).
5
5
  ## Daily dev
6
6
 
7
7
  ```bash
8
- yarn install
8
+ npm install
9
+ wp setup
9
10
  wp doctor
10
11
  wp dev
11
12
  ```
@@ -1,6 +1,7 @@
1
1
  import { Routes } from '@/config/routes'
2
2
  import { readHashPathname } from '@/features/auth/auth-route-paths'
3
3
  import { normalizeRouterPath } from '@/utils/normalize-router-path'
4
+ import { WP_STORAGE_KEYS } from 'woodsportal-client-sdk'
4
5
 
5
6
  export type SsoCallbackParams = {
6
7
  code: string
@@ -60,6 +61,11 @@ export function clearPreHashQueryParams(): void {
60
61
  window.history.replaceState(null, '', `${window.location.pathname}${window.location.hash}`)
61
62
  }
62
63
 
64
+ /** Legacy sessionStorage inflight — clear on boot so stale keys never block exchange. */
65
+ export function clearLegacySsoCallbackInFlight(): void {
66
+ sessionStorage.removeItem(`${WP_STORAGE_KEYS.c.sso.cb}:inflight`)
67
+ }
68
+
63
69
  export function isSsoCallbackAlreadyProcessed(key: string): boolean {
64
70
  return processedKeys.has(key)
65
71
  }
@@ -5,7 +5,8 @@ This project was scaffolded with **WoodsCLI** (`@woodsportal/hubspot-kit`).
5
5
  ## Daily dev
6
6
 
7
7
  ```bash
8
- yarn install
8
+ npm install
9
+ wp setup
9
10
  wp doctor
10
11
  wp dev
11
12
  ```
@@ -5,7 +5,8 @@ This project was scaffolded with **WoodsCLI** (`@woodsportal/hubspot-kit`).
5
5
  ## Daily dev
6
6
 
7
7
  ```bash
8
- yarn install
8
+ npm install
9
+ wp setup
9
10
  wp doctor
10
11
  wp dev
11
12
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@woodsportal/hubspot-kit",
3
- "version": "1.0.42-dev.0",
3
+ "version": "1.0.42-dev.2",
4
4
  "description": "WoodsCLI — HubSpot CMS dev kit by WoodsPortal. wp CLI, Vite presets, module-config overlay, and theme/module build pipelines.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,8 +53,9 @@
53
53
  "typecheck": "tsc --noEmit",
54
54
  "test": "node --import tsx --test 'src/lib/**/*.test.ts' 'src/cli/**/*.test.ts' 'src/schema/**/*.test.ts' 'src/vite/**/*.test.ts' 'scripts/portal-build-id.test.mjs' 'scripts/resolve-consumer-dep.test.mjs' 'scripts/wp-cli-smoke.test.mjs' 'scripts/vite/ensure-dev-kit-shims.test.mjs' 'scripts/vite/consumer-react-aliases.test.mjs' 'scripts/vite/resolve-from-project-root.test.mjs' && vitest run src/dev-module-config/__tests__/normalize-repeater-items.test.ts src/dev-module-config/__tests__/module-config-transform.test.ts src/dev-module-config/__tests__/flatten-visible-fields.test.ts",
55
55
  "test:examples": "node examples/smoke/run-matrix.mjs --tier static",
56
+ "test:dist-pack": "node --test scripts/dist-cli-pack-smoke.test.mjs",
56
57
  "lint": "eslint src",
57
- "verify": "npm run typecheck && npm run test && npm run test:examples && npm run build",
58
+ "verify": "npm run typecheck && npm run test && npm run test:examples && npm run build && npm run test:dist-pack",
58
59
  "prepublishOnly": "npm run verify"
59
60
  },
60
61
  "engines": {
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * Build CDN vendor + app IIFE chunks into dist/cdn/
4
4
  */
5
- import { spawnSync } from 'node:child_process'
6
- import { resolve, dirname } from 'node:path'
5
+ import { resolve } from 'node:path'
7
6
  import { fileURLToPath } from 'node:url'
7
+
8
8
  import { findProjectRoot } from './project-root.mjs'
9
9
 
10
10
  import { envWithPortalBuildId } from './resolve-portal-build-id.mjs'
@@ -17,6 +17,7 @@ import {
17
17
  portalSubprocessStdio,
18
18
  portalViteLogArgs,
19
19
  } from './portal-subprocess-stdio.mjs'
20
+ import { spawnViteSync } from './resolve-vite-spawn.mjs'
20
21
 
21
22
  const root = findProjectRoot()
22
23
 
@@ -31,9 +32,9 @@ const env = envForPortalBuild(mode, root, {
31
32
  })
32
33
 
33
34
  function runWithEnv(config, buildMode) {
34
- const result = spawnSync(
35
- 'yarn',
36
- ['vite', 'build', '--config', config, '--mode', buildMode, ...portalViteLogArgs()],
35
+ const result = spawnViteSync(
36
+ root,
37
+ ['build', '--config', config, '--mode', buildMode, ...portalViteLogArgs()],
37
38
  { cwd: root, stdio: portalSubprocessStdio(), env, encoding: 'utf8' },
38
39
  )
39
40
  exitOnSpawnFailure(result)
@@ -0,0 +1,79 @@
1
+ import assert from 'node:assert/strict'
2
+ import { spawnSync } from 'node:child_process'
3
+ import { existsSync, mkdtempSync, rmSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join, resolve } from 'node:path'
6
+ import { test } from 'node:test'
7
+
8
+ import { copyFixture, kitRoot } from './cli-smoke-helpers.mjs'
9
+
10
+ const distCli = join(kitRoot, 'dist/cli.js')
11
+
12
+ test('dist cli: doctor scaffolds .wphs without SyntaxError', () => {
13
+ if (!existsSync(distCli)) {
14
+ console.log('skip: dist/cli.js missing — run npm run build first')
15
+ return
16
+ }
17
+
18
+ const dir = mkdtempSync(join(tmpdir(), 'wphs-dist-pack-'))
19
+ try {
20
+ copyFixture('greenfield/module-hybrid-cdn', dir)
21
+ const result = spawnSync(
22
+ process.execPath,
23
+ [distCli, '--project', dir, '--ci', '-y', 'doctor'],
24
+ {
25
+ cwd: kitRoot,
26
+ encoding: 'utf8',
27
+ env: { ...process.env, NO_COLOR: '1', WP_SKIP_VERSION_CHECK: '1' },
28
+ },
29
+ )
30
+
31
+ const combined = `${result.stdout}\n${result.stderr}`
32
+ assert.doesNotMatch(combined, /Invalid or unexpected token/)
33
+ assert.ok(
34
+ result.status === 0 || result.status === 2,
35
+ `unexpected exit ${result.status}: ${combined}`,
36
+ )
37
+ assert.ok(existsSync(resolve(dir, '.wphs', 'config')), 'expected .wphs/config after doctor')
38
+ } finally {
39
+ rmSync(dir, { recursive: true, force: true })
40
+ }
41
+ })
42
+
43
+ test('dist cli: init dry-run completes without SyntaxError', () => {
44
+ if (!existsSync(distCli)) {
45
+ console.log('skip: dist/cli.js missing — run npm run build first')
46
+ return
47
+ }
48
+
49
+ const targetDir = mkdtempSync(join(tmpdir(), 'wphs-dist-init-'))
50
+ try {
51
+ const result = spawnSync(
52
+ process.execPath,
53
+ [
54
+ distCli,
55
+ '--ci',
56
+ '-y',
57
+ '--dry-run',
58
+ 'init',
59
+ '-t',
60
+ 'module-hybrid-cdn',
61
+ '-n',
62
+ 'dist-init-smoke',
63
+ '--directory',
64
+ targetDir,
65
+ ],
66
+ {
67
+ cwd: kitRoot,
68
+ encoding: 'utf8',
69
+ env: { ...process.env, NO_COLOR: '1', WP_SKIP_VERSION_CHECK: '1' },
70
+ },
71
+ )
72
+
73
+ const combined = `${result.stdout}\n${result.stderr}`
74
+ assert.doesNotMatch(combined, /Invalid or unexpected token/)
75
+ assert.equal(result.status, 0, combined)
76
+ } finally {
77
+ rmSync(targetDir, { recursive: true, force: true })
78
+ }
79
+ })
@@ -1,8 +1,9 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * Scaffold WoodsCLI `.wphs/` workspace + kit symlinks.
4
3
  */
5
4
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
5
+ import { resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
6
7
 
7
8
  import { findProjectRoot, getKitRoot } from './project-root.mjs'
8
9
  import { wphsPaths, wphsWorkspaceSubdirs } from './wphs-paths.mjs'
@@ -48,7 +49,11 @@ export function ensureWphsWorkspace(projectRoot, kitRoot) {
48
49
  return paths
49
50
  }
50
51
 
51
- if (import.meta.url === `file://${process.argv[1]}`) {
52
+ const invokedDirectly =
53
+ process.argv[1] &&
54
+ resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1])
55
+
56
+ if (invokedDirectly) {
52
57
  const paths = ensureWphsWorkspace()
53
58
  console.log(`[wphs] workspace ready at ${paths.workspaceRoot}`)
54
59
  }
@@ -0,0 +1,51 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { platform } from 'node:os'
3
+ import { resolve } from 'node:path'
4
+
5
+ /**
6
+ * @param {string} [projectRoot]
7
+ * @returns {{ command: string, prefixArgs: string[], shell?: boolean }}
8
+ */
9
+ export function resolveHubSpotCliSpawn(projectRoot) {
10
+ const roots = []
11
+ if (projectRoot) roots.push(projectRoot)
12
+ if (!roots.includes(process.cwd())) roots.push(process.cwd())
13
+
14
+ for (const root of roots) {
15
+ const binDir = resolve(root, 'node_modules', '.bin')
16
+ if (platform() === 'win32') {
17
+ const cmd = resolve(binDir, 'hs.cmd')
18
+ if (existsSync(cmd)) return { command: cmd, prefixArgs: [], shell: false }
19
+ }
20
+ const shim = resolve(binDir, 'hs')
21
+ if (existsSync(shim)) return { command: shim, prefixArgs: [], shell: false }
22
+ }
23
+
24
+ if (platform() === 'win32') {
25
+ return { command: 'hs', prefixArgs: [], shell: true }
26
+ }
27
+ return { command: 'hs', prefixArgs: [], shell: false }
28
+ }
29
+
30
+ /**
31
+ * @param {string} projectRoot
32
+ * @param {string | undefined} kitCliPath
33
+ */
34
+ export function resolveWpBinSpawn(projectRoot, kitCliPath) {
35
+ if (kitCliPath && existsSync(kitCliPath)) {
36
+ return { command: process.execPath, prefixArgs: [kitCliPath], shell: false }
37
+ }
38
+
39
+ const binDir = resolve(projectRoot, 'node_modules', '.bin')
40
+ if (platform() === 'win32') {
41
+ const cmd = resolve(binDir, 'wp.cmd')
42
+ if (existsSync(cmd)) return { command: cmd, prefixArgs: [], shell: false }
43
+ }
44
+ const shim = resolve(binDir, 'wp')
45
+ if (existsSync(shim)) return { command: shim, prefixArgs: [], shell: false }
46
+
47
+ if (platform() === 'win32') {
48
+ return { command: 'wp', prefixArgs: [], shell: true }
49
+ }
50
+ return { command: 'wp', prefixArgs: [], shell: false }
51
+ }
@@ -2,7 +2,7 @@
2
2
  * Resolve a Vite config path: prefer project root, fall back to kit vite presets.
3
3
  */
4
4
  import { existsSync } from 'node:fs'
5
- import { basename, resolve } from 'node:path'
5
+ import { basename, resolve, isAbsolute } from 'node:path'
6
6
 
7
7
  import { findProjectRoot, getKitRoot } from './project-root.mjs'
8
8
 
@@ -11,8 +11,7 @@ import { findProjectRoot, getKitRoot } from './project-root.mjs'
11
11
  * @param {string} [projectRoot]
12
12
  */
13
13
  export function resolveViteConfig(configArg, projectRoot = findProjectRoot()) {
14
- const isAbsolute = configArg.startsWith('/')
15
- const localPath = isAbsolute ? configArg : resolve(projectRoot, configArg)
14
+ const localPath = isAbsolute(configArg) ? configArg : resolve(projectRoot, configArg)
16
15
  if (existsSync(localPath)) {
17
16
  return localPath
18
17
  }
@@ -0,0 +1,48 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import { platform } from 'node:os'
4
+ import { resolve } from 'node:path'
5
+
6
+ const VITE_JS_SEGMENTS = ['node_modules', 'vite', 'bin', 'vite.js']
7
+
8
+ /**
9
+ * @param {string} projectRoot
10
+ * @returns {{ command: string, prefixArgs: string[], shell?: boolean } | null}
11
+ */
12
+ export function resolveViteSpawn(projectRoot) {
13
+ const viteJs = resolve(projectRoot, ...VITE_JS_SEGMENTS)
14
+ if (existsSync(viteJs)) {
15
+ return { command: process.execPath, prefixArgs: [viteJs], shell: false }
16
+ }
17
+
18
+ const binDir = resolve(projectRoot, 'node_modules', '.bin')
19
+ if (platform() === 'win32') {
20
+ const winCmd = resolve(binDir, 'vite.cmd')
21
+ if (existsSync(winCmd)) {
22
+ return { command: winCmd, prefixArgs: [], shell: false }
23
+ }
24
+ }
25
+
26
+ const posixShim = resolve(binDir, 'vite')
27
+ if (existsSync(posixShim)) {
28
+ return { command: posixShim, prefixArgs: [], shell: false }
29
+ }
30
+
31
+ return null
32
+ }
33
+
34
+ /**
35
+ * @param {string} projectRoot
36
+ * @param {string[]} viteArgs
37
+ * @param {import('node:child_process').SpawnSyncOptions} options
38
+ */
39
+ export function spawnViteSync(projectRoot, viteArgs, options) {
40
+ const spawn = resolveViteSpawn(projectRoot)
41
+ if (!spawn) {
42
+ throw new Error('vite not installed — run npm install or yarn install in the project root')
43
+ }
44
+ return spawnSync(spawn.command, [...spawn.prefixArgs, ...viteArgs], {
45
+ ...options,
46
+ shell: spawn.shell ?? false,
47
+ })
48
+ }
@@ -4,8 +4,6 @@
4
4
  *
5
5
  * Usage: node scripts/run-vite-build.mjs <vite-config.mjs> [--local | --dev | --stg | --prod]
6
6
  */
7
- import { spawnSync } from 'node:child_process'
8
-
9
7
  import { defaultDatadogFlag, envForPortalBuild } from './portal-vite-env.mjs'
10
8
  import { resolvePortalViteMode } from './portal-env-mode.mjs'
11
9
  import { findProjectRoot } from './project-root.mjs'
@@ -15,6 +13,7 @@ import {
15
13
  portalSubprocessStdio,
16
14
  portalViteLogArgs,
17
15
  } from './portal-subprocess-stdio.mjs'
16
+ import { spawnViteSync } from './resolve-vite-spawn.mjs'
18
17
 
19
18
  const root = findProjectRoot()
20
19
  const configArg = process.argv[2]
@@ -32,10 +31,10 @@ const env = envForPortalBuild(mode, root, {
32
31
  WP_HS_PROJECT_ROOT: root,
33
32
  })
34
33
 
35
- const result = spawnSync(
36
- 'npx',
37
- ['vite', 'build', '--config', config, '--mode', mode, ...portalViteLogArgs()],
38
- { cwd: root, stdio: portalSubprocessStdio(), env, shell: true, encoding: 'utf8' },
34
+ const result = spawnViteSync(
35
+ root,
36
+ ['build', '--config', config, '--mode', mode, ...portalViteLogArgs()],
37
+ { cwd: root, stdio: portalSubprocessStdio(), env, encoding: 'utf8' },
39
38
  )
40
39
 
41
40
  exitOnSpawnFailure(result)
@@ -8,6 +8,7 @@ import { dirname, resolve } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
9
9
 
10
10
  import { moduleOutDir, moduleUploadPath, readWoodsportalConfig, themeUploadPath } from './read-woodsportal-config.mjs'
11
+ import { resolveHubSpotCliSpawn, resolveWpBinSpawn } from './resolve-hubspot-cli.mjs'
11
12
 
12
13
  const kitScriptsDir = dirname(fileURLToPath(import.meta.url))
13
14
  const kitRoot = resolve(kitScriptsDir, '..')
@@ -87,23 +88,15 @@ process.on('SIGTERM', () => shutdown('SIGTERM'))
87
88
  function runWpBuild() {
88
89
  const env = { ...process.env, WP_HS_PROJECT_ROOT: root }
89
90
 
90
- let command = process.execPath
91
- let args = []
92
-
93
- if (existsSync(kitCli)) {
94
- args = [kitCli, 'build', '--tier', tier, '--quiet']
95
- } else {
96
- const localWp = resolve(root, 'node_modules', '.bin', 'wp')
97
- command = existsSync(localWp) ? localWp : 'wp'
98
- args = ['build', '--tier', tier, '--quiet']
99
- }
91
+ const wpSpawn = resolveWpBinSpawn(root, existsSync(kitCli) ? kitCli : undefined)
92
+ const args = [...wpSpawn.prefixArgs, 'build', '--tier', tier, '--quiet']
100
93
 
101
94
  log(`Running wp build --tier ${tier} …`)
102
- const result = spawnSync(command, args, {
95
+ const result = spawnSync(wpSpawn.command, args, {
103
96
  cwd: root,
104
97
  stdio: 'inherit',
105
98
  env,
106
- shell: command === 'wp',
99
+ shell: wpSpawn.shell ?? false,
107
100
  })
108
101
  if (result.status !== 0) {
109
102
  process.exit(result.status ?? 1)
@@ -122,7 +115,12 @@ function startCmsWatch() {
122
115
  const src = isTheme ? theme.srcDir : hubspotModulePath
123
116
  const target = isTheme ? theme.uploadPath : hubspotUploadTarget
124
117
  log(`Starting hs cms watch ${src} → ${target}`)
125
- cmsChild = spawn('hs', ['cms', 'watch', src, target], { cwd: root, stdio: 'inherit' })
118
+ const hsSpawn = resolveHubSpotCliSpawn(root)
119
+ cmsChild = spawn(hsSpawn.command, [...hsSpawn.prefixArgs, 'cms', 'watch', src, target], {
120
+ cwd: root,
121
+ stdio: 'inherit',
122
+ shell: hsSpawn.shell ?? false,
123
+ })
126
124
  cmsChild.on('exit', (code) => {
127
125
  if (shuttingDown) return
128
126
  process.exit(code ?? 0)
@@ -5,7 +5,8 @@ This project was scaffolded with **WoodsCLI** (`@woodsportal/hubspot-kit`).
5
5
  ## Daily dev
6
6
 
7
7
  ```bash
8
- yarn install
8
+ npm install
9
+ wp setup
9
10
  wp doctor
10
11
  wp dev
11
12
  ```
@@ -5,7 +5,8 @@ This project was scaffolded with **WoodsCLI** (`@woodsportal/hubspot-kit`).
5
5
  ## Daily dev
6
6
 
7
7
  ```bash
8
- yarn install
8
+ npm install
9
+ wp setup
9
10
  wp doctor
10
11
  wp dev
11
12
  ```
@@ -0,0 +1,79 @@
1
+ import { useState } from 'react'
2
+ import { useRouter } from '@tanstack/react-router'
3
+ import { toast } from 'sonner'
4
+ import { Button } from '@/components/ui/Button'
5
+ import { Form, FormField } from '@/components/ui/Form'
6
+ import { Input } from '@/components/ui/Input'
7
+ import { SsoLoginButton } from '@/components/ui/auth/SsoLoginButton'
8
+ import { api } from '@/integrations/woodsportal-sdk'
9
+ import { handleAuthLoginResponse } from '@/features/auth/login-session-bridge'
10
+ import { resolveAuthLoginErrorMessage } from '@/features/auth/resolve-auth-login-error-message'
11
+ import {
12
+ getStoredLoginEmail,
13
+ setStoredLoginEmail,
14
+ } from '@/features/auth/login-email-storage'
15
+
16
+ export function StarterLogin() {
17
+ const router = useRouter()
18
+ const [email, setEmail] = useState(getStoredLoginEmail() ?? '')
19
+ const [password, setPassword] = useState('')
20
+ const [isSubmitting, setIsSubmitting] = useState(false)
21
+
22
+ const onSubmit = async (event: React.FormEvent) => {
23
+ event.preventDefault()
24
+ setIsSubmitting(true)
25
+ try {
26
+ setStoredLoginEmail(email)
27
+ const { login } = api.auth.login({})
28
+ const response = await login({ username: email.trim(), password })
29
+ const result = await handleAuthLoginResponse(response, router)
30
+ if (!result.success) {
31
+ toast.error('Login failed — no access token returned.')
32
+ return
33
+ }
34
+ if (!result.mfaRequired) {
35
+ toast.success('Signed in')
36
+ }
37
+ } catch (error) {
38
+ toast.error(resolveAuthLoginErrorMessage(error))
39
+ } finally {
40
+ setIsSubmitting(false)
41
+ }
42
+ }
43
+
44
+ return (
45
+ <div className="w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
46
+ <h1 className="mb-2 text-xl font-semibold text-gray-900">Sign in</h1>
47
+ <p className="mb-6 text-sm text-gray-600">Use your portal email and password or SSO.</p>
48
+ <Form onSubmit={onSubmit}>
49
+ <FormField label="Email">
50
+ <Input
51
+ type="email"
52
+ autoComplete="email"
53
+ required
54
+ value={email}
55
+ onChange={(e) => setEmail(e.target.value)}
56
+ />
57
+ </FormField>
58
+ <FormField label="Password">
59
+ <Input
60
+ type="password"
61
+ autoComplete="current-password"
62
+ required
63
+ value={password}
64
+ onChange={(e) => setPassword(e.target.value)}
65
+ />
66
+ </FormField>
67
+ <Button type="submit" className="w-full" disabled={isSubmitting}>
68
+ {isSubmitting ? 'Signing in…' : 'Sign in'}
69
+ </Button>
70
+ </Form>
71
+ <div className="my-4 flex items-center gap-3 text-xs text-gray-500">
72
+ <span className="h-px flex-1 bg-gray-200" />
73
+ or
74
+ <span className="h-px flex-1 bg-gray-200" />
75
+ </div>
76
+ <SsoLoginButton />
77
+ </div>
78
+ )
79
+ }
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
2
2
  import { RouterProvider } from '@tanstack/react-router'
3
3
  import { installCrossTabSessionListener } from 'woodsportal-client-sdk'
4
4
 
5
- import { api } from '@/integrations/woodsportal-sdk'
5
+ import { api, isSessionRefreshDeferredError } from '@/integrations/woodsportal-sdk'
6
6
  import * as TanStackQueryProvider from '@/integrations/tanstack-query/root-provider'
7
7
  import { env } from '@/env'
8
8
  import Loader from '@/components/ui/loader/loader'
@@ -123,7 +123,10 @@ export function App({ router }: AppProps) {
123
123
 
124
124
  try {
125
125
  await session.refreshSession({ force: true })
126
- } catch {
126
+ } catch (error) {
127
+ if (isSessionRefreshDeferredError(error)) {
128
+ return
129
+ }
127
130
  if (!inCmsEditor) {
128
131
  await teardownAndRedirectToLogin()
129
132
  }
@@ -147,7 +150,9 @@ export function App({ router }: AppProps) {
147
150
  }),
148
151
  ])
149
152
  } catch (error) {
150
- if (!inCmsEditor && skipCurrentPublicPath()) {
153
+ if (isSessionRefreshDeferredError(error)) {
154
+ // API temporarily unavailable — keep session cookies intact.
155
+ } else if (!inCmsEditor && skipCurrentPublicPath()) {
151
156
  await teardownAndRedirectToLogin()
152
157
  } else if (error instanceof Error && error.message !== 'bootstrap_timeout') {
153
158
  throw error