hubspot-cms-sync 0.1.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/LICENSE +21 -0
- package/README.md +52 -0
- package/bin/hubspot-cms-sync.mjs +115 -0
- package/docs/CONFIGURATION.md +83 -0
- package/docs/GITHUB_ACTIONS.md +70 -0
- package/docs/MIGRATION_PLAN.md +361 -0
- package/docs/PLAN_REVIEW.md +42 -0
- package/docs/SKILL_DISTRIBUTION.md +79 -0
- package/examples/github-actions/ci.yml +56 -0
- package/examples/github-actions/preview.yml +71 -0
- package/examples/github-actions/publish.yml +82 -0
- package/examples/hubspot-cms-sync.config.mjs +45 -0
- package/examples/site.manifest.json +19 -0
- package/package.json +41 -0
- package/skill/SKILL.md +54 -0
- package/skill/references/commands.md +54 -0
- package/skill/references/config.md +25 -0
- package/skill/references/failures.md +58 -0
- package/skill/references/github-actions.md +56 -0
- package/skill/references/screenshots-and-fidelity.md +33 -0
- package/src/adapters/assets.mjs +576 -0
- package/src/adapters/blog.mjs +921 -0
- package/src/adapters/content.mjs +213 -0
- package/src/adapters/forms.mjs +569 -0
- package/src/adapters/pages.mjs +463 -0
- package/src/adapters/theme.mjs +503 -0
- package/src/config.mjs +113 -0
- package/src/corpus-scan.mjs +248 -0
- package/src/cta-inventory.mjs +352 -0
- package/src/index.mjs +3 -0
- package/src/lib/canonical.mjs +234 -0
- package/src/lib/hub.mjs +197 -0
- package/src/lib/orchestrate.mjs +141 -0
- package/src/lib/refs.mjs +398 -0
- package/src/lib/sync-state.mjs +86 -0
- package/src/manifest.mjs +353 -0
- package/src/preflight.mjs +385 -0
- package/src/pull.mjs +99 -0
- package/src/push.mjs +354 -0
- package/src/republish.mjs +102 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# hubspot-cms-sync
|
|
2
|
+
|
|
3
|
+
Git-backed bidirectional HubSpot CMS sync for themes, site pages, page module
|
|
4
|
+
content, blogs, forms, and assets.
|
|
5
|
+
|
|
6
|
+
The package provides the `hubspot-cms-sync` CLI plus the short `hcms` alias.
|
|
7
|
+
It reads account and site settings from `hubspot-cms-sync.config.mjs`, stores
|
|
8
|
+
per-account identity in a gitignored `.sync-state/` directory, and refuses to
|
|
9
|
+
push to configured read-only portal ids.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install --save-dev hubspot-cms-sync
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For local development from a sibling checkout:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install --save-dev ../hubspot-cms-sync
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
hcms doctor
|
|
27
|
+
hubspot-cms-sync pull prod
|
|
28
|
+
hubspot-cms-sync preflight dev
|
|
29
|
+
hubspot-cms-sync push dev --publish
|
|
30
|
+
hcms push dev --dry-run
|
|
31
|
+
hubspot-cms-sync republish dev --all --blog
|
|
32
|
+
hubspot-cms-sync corpus
|
|
33
|
+
hcms manifest validate
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Copy `examples/hubspot-cms-sync.config.mjs` into the consuming repo, then set
|
|
39
|
+
the theme name, manifest path, read-only portal ids, and account registry path.
|
|
40
|
+
Account keys live outside git at `$HUBSPOT_KEY_DIR/<portalId>.key` or
|
|
41
|
+
`~/.hubspot/<portalId>.key`.
|
|
42
|
+
|
|
43
|
+
## Tests
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm test
|
|
47
|
+
npm run lint
|
|
48
|
+
npm pack --dry-run
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Live HubSpot round-trip tests are skipped by default and require
|
|
52
|
+
`RUN_INTEGRATION=1` plus sandbox credentials.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { loadConfig } from '../src/config.mjs';
|
|
5
|
+
import { pull } from '../src/pull.mjs';
|
|
6
|
+
import { push } from '../src/push.mjs';
|
|
7
|
+
import { main as preflightMain } from '../src/preflight.mjs';
|
|
8
|
+
import { main as republishMain } from '../src/republish.mjs';
|
|
9
|
+
import { main as manifestMain } from '../src/manifest.mjs';
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
console.log(`usage: hcms [--root <path>] <command> [args]
|
|
13
|
+
|
|
14
|
+
commands:
|
|
15
|
+
pull <account>
|
|
16
|
+
push <account> [--publish] [--dry-run]
|
|
17
|
+
preflight <account>
|
|
18
|
+
republish <account|portalId> [--all] [--blog] [slugs...]
|
|
19
|
+
corpus [paths...]
|
|
20
|
+
manifest [args...]
|
|
21
|
+
doctor
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseGlobal(argv) {
|
|
26
|
+
const out = { root: process.cwd(), args: [] };
|
|
27
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
28
|
+
const a = argv[i];
|
|
29
|
+
if (a === '--root') {
|
|
30
|
+
out.root = argv[++i];
|
|
31
|
+
} else {
|
|
32
|
+
out.args.push(a);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runNodeScript(script, args, { cwd }) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const child = spawn(process.execPath, [script, ...args], { cwd, stdio: 'inherit' });
|
|
41
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function main(argv = process.argv.slice(2)) {
|
|
46
|
+
const { root, args } = parseGlobal(argv);
|
|
47
|
+
const [cmd, ...rest] = args;
|
|
48
|
+
if (!cmd || cmd === '-h' || cmd === '--help') {
|
|
49
|
+
usage();
|
|
50
|
+
return cmd ? 0 : 2;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const config = await loadConfig({ root });
|
|
54
|
+
|
|
55
|
+
if (cmd === 'doctor') {
|
|
56
|
+
console.log(`root: ${config.root}`);
|
|
57
|
+
console.log(`accounts: ${config.accountsPath}`);
|
|
58
|
+
console.log(`content: ${config.contentDirPath}`);
|
|
59
|
+
console.log(`manifest: ${config.manifestFilePath}`);
|
|
60
|
+
console.log(`sync state: ${config.syncStateDirPath}`);
|
|
61
|
+
console.log(`theme: ${config.theme.name}`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (cmd === 'pull') {
|
|
66
|
+
const account = rest[0];
|
|
67
|
+
if (!account) throw new Error('pull requires <account>');
|
|
68
|
+
await pull(account, { config });
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (cmd === 'push') {
|
|
73
|
+
const publish = rest.includes('--publish');
|
|
74
|
+
const dryRun = rest.includes('--dry-run');
|
|
75
|
+
const account = rest.find((a) => !a.startsWith('--'));
|
|
76
|
+
if (!account) throw new Error('push requires <account>');
|
|
77
|
+
if (dryRun) {
|
|
78
|
+
// Current engine preflight is the no-write plan surface. A future plan command
|
|
79
|
+
// can add per-adapter write summaries.
|
|
80
|
+
const { preflightRefs } = await import('../src/push.mjs');
|
|
81
|
+
preflightRefs(config.contentDirPath);
|
|
82
|
+
console.log(`dry-run push preflight passed for ${account}`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
await push(account, { publish, config });
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (cmd === 'preflight') {
|
|
90
|
+
return preflightMain(rest, { config });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (cmd === 'republish') {
|
|
94
|
+
return republishMain(rest, { config });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (cmd === 'manifest') {
|
|
98
|
+
return manifestMain(rest, { config });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (cmd === 'corpus') {
|
|
102
|
+
const script = new URL('../src/corpus-scan.mjs', import.meta.url).pathname;
|
|
103
|
+
return runNodeScript(script, rest, { cwd: config.root });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
usage();
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().then((code) => {
|
|
111
|
+
process.exitCode = code;
|
|
112
|
+
}).catch((e) => {
|
|
113
|
+
console.error(`hcms failed: ${e.message}`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Configuration Plan
|
|
2
|
+
|
|
3
|
+
The package should be driven by `hubspot-cms-sync.config.mjs` in the consuming
|
|
4
|
+
repo root.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
export default {
|
|
10
|
+
accountsFile: 'sync/accounts.json',
|
|
11
|
+
keyDirEnv: 'HUBSPOT_KEY_DIR',
|
|
12
|
+
contentDir: 'content',
|
|
13
|
+
syncStateDir: '.sync-state',
|
|
14
|
+
manifestPath: 'site.manifest.json',
|
|
15
|
+
readOnlyPortalIds: ['529456'],
|
|
16
|
+
knownPortalIds: ['529456', '246389711'],
|
|
17
|
+
assetHosts: {
|
|
18
|
+
canonicalizeHostPatterns: [
|
|
19
|
+
'hubfs',
|
|
20
|
+
'hubspotusercontent',
|
|
21
|
+
'cdn\\d*\\.hubspot\\.net'
|
|
22
|
+
],
|
|
23
|
+
legacySiteHosts: []
|
|
24
|
+
},
|
|
25
|
+
adapters: {
|
|
26
|
+
externalDirs: []
|
|
27
|
+
},
|
|
28
|
+
theme: {
|
|
29
|
+
name: 'seventh-sense-theme',
|
|
30
|
+
dirs: ['templates', 'modules', 'css', 'js', 'images'],
|
|
31
|
+
files: ['theme.json', 'fields.json']
|
|
32
|
+
},
|
|
33
|
+
blog: {
|
|
34
|
+
slug: 'blog',
|
|
35
|
+
itemTemplate: 'seventh-sense-theme/templates/blog-post.html',
|
|
36
|
+
listingTemplate: 'seventh-sense-theme/templates/blog.html'
|
|
37
|
+
},
|
|
38
|
+
uiGated: [
|
|
39
|
+
'blogContainerCreate',
|
|
40
|
+
'domainConnect',
|
|
41
|
+
'homepageDesignation',
|
|
42
|
+
'themeSettingsValues',
|
|
43
|
+
'nativeMenus'
|
|
44
|
+
],
|
|
45
|
+
verification: {
|
|
46
|
+
baseUrlEnv: 'SITE_BASE_URL',
|
|
47
|
+
commands: {
|
|
48
|
+
unit: 'npm run test:unit',
|
|
49
|
+
corpus: 'hcms corpus',
|
|
50
|
+
playwright: 'npx playwright test verify/fidelity.spec.mjs verify/forms.spec.mjs verify/links.spec.mjs'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Design Requirements
|
|
57
|
+
|
|
58
|
+
- Config paths are relative to `--root` / `process.cwd()`.
|
|
59
|
+
- The CLI resolves config once, derives absolute paths, and passes that object
|
|
60
|
+
explicitly. Avoid hidden module-level global state.
|
|
61
|
+
- Package defaults are safe and minimal.
|
|
62
|
+
- No consumer-specific portal IDs are hardcoded in source.
|
|
63
|
+
- The config loader validates shape and prints remediation, not stack traces.
|
|
64
|
+
- `site.manifest.json` remains the deploy surface for content, pages, forms, and
|
|
65
|
+
blog.
|
|
66
|
+
- `hubspot-cms-sync.config.mjs` controls environment policy and filesystem
|
|
67
|
+
layout.
|
|
68
|
+
|
|
69
|
+
## Open Config Questions
|
|
70
|
+
|
|
71
|
+
- Should the package support TypeScript configs or only ESM?
|
|
72
|
+
- Should CTA/menu adapters be optional plugins or built-in later?
|
|
73
|
+
|
|
74
|
+
## Decisions From Plan Review
|
|
75
|
+
|
|
76
|
+
- Project-specific adapters should be supported only after core extraction works.
|
|
77
|
+
If supported, they should come from explicit `adapters.externalDirs` entries and
|
|
78
|
+
participate in the same `dependsOn` validation as built-in adapters.
|
|
79
|
+
- `knownPortalIds` cannot remain hardcoded. For v1, require explicit config or
|
|
80
|
+
derive from `accountsFile` plus any registry portal IDs; tests must cover both
|
|
81
|
+
behaviors before generic publication.
|
|
82
|
+
- `readOnlyPortalIds` must be an array. Push/preflight checks use membership,
|
|
83
|
+
not a single hardcoded production portal.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# GitHub Actions Examples
|
|
2
|
+
|
|
3
|
+
This package is currently a planning scaffold. The workflows in
|
|
4
|
+
`examples/github-actions/` are distribution templates for future consuming
|
|
5
|
+
repositories after the `hubspot-cms-sync` CLI is published.
|
|
6
|
+
|
|
7
|
+
Copy the examples into a consuming repo's `.github/workflows/` directory and
|
|
8
|
+
adjust portal names, credentials, branch policy, and verification commands to
|
|
9
|
+
match that repo's `hubspot-cms-sync.config.mjs`.
|
|
10
|
+
|
|
11
|
+
## Shared Assumptions
|
|
12
|
+
|
|
13
|
+
- The consuming repo has a committed `hubspot-cms-sync.config.mjs`.
|
|
14
|
+
- The consuming repo has a committed `site.manifest.json`.
|
|
15
|
+
- The CLI is available through either a project dependency or
|
|
16
|
+
`npx --yes hubspot-cms-sync@latest`.
|
|
17
|
+
- `HUBSPOT_KEY_DIR` points at credentials hydrated during the workflow.
|
|
18
|
+
- Workflow targets such as `dev`, `preview`, and `prod` are examples. Use the
|
|
19
|
+
names defined by the consuming repo's accounts file and config.
|
|
20
|
+
- Production portals should be protected by GitHub Environments and should not
|
|
21
|
+
be used for pull request previews.
|
|
22
|
+
|
|
23
|
+
## Secrets
|
|
24
|
+
|
|
25
|
+
Use repository or environment secrets. Exact secret names can vary, but the
|
|
26
|
+
workflow should hydrate whatever credential files the consuming repo's
|
|
27
|
+
`accountsFile` expects.
|
|
28
|
+
|
|
29
|
+
Suggested baseline:
|
|
30
|
+
|
|
31
|
+
- `HUBSPOT_DEV_PRIVATE_APP_TOKEN`
|
|
32
|
+
- `HUBSPOT_PREVIEW_PRIVATE_APP_TOKEN`
|
|
33
|
+
- `HUBSPOT_PROD_PRIVATE_APP_TOKEN`
|
|
34
|
+
- `SITE_BASE_URL` or an environment-specific equivalent
|
|
35
|
+
|
|
36
|
+
Do not commit generated credential files or `.sync-state` mutations from CI
|
|
37
|
+
unless the consuming repo intentionally uses a reviewed pull flow for state
|
|
38
|
+
updates.
|
|
39
|
+
|
|
40
|
+
## Example Workflows
|
|
41
|
+
|
|
42
|
+
- `examples/github-actions/ci.yml`: read-only checks for pull requests and
|
|
43
|
+
pushes.
|
|
44
|
+
- `examples/github-actions/preview.yml`: deploy a pull request to a non-prod
|
|
45
|
+
preview portal and run verification.
|
|
46
|
+
- `examples/github-actions/publish.yml`: manually publish to a protected target.
|
|
47
|
+
|
|
48
|
+
## Command Policy
|
|
49
|
+
|
|
50
|
+
The examples prefer this sequence for write-capable operations:
|
|
51
|
+
|
|
52
|
+
1. `hcms doctor`
|
|
53
|
+
2. `hcms corpus`
|
|
54
|
+
3. `hcms preflight <target>`
|
|
55
|
+
4. `hcms push <target> --dry-run`
|
|
56
|
+
5. `hcms push <target> --publish`
|
|
57
|
+
6. `the consuming repo verification commands`
|
|
58
|
+
|
|
59
|
+
For read-only CI, omit write steps and keep production credentials unavailable.
|
|
60
|
+
|
|
61
|
+
## Guardrails
|
|
62
|
+
|
|
63
|
+
- Do not bypass `readOnlyPortalIds`.
|
|
64
|
+
- Do not use production credentials in `pull_request` workflows.
|
|
65
|
+
- Keep publish workflows behind `workflow_dispatch` and a protected GitHub
|
|
66
|
+
Environment.
|
|
67
|
+
- Treat surviving `@cta:*` and `@menu:*` references as closed failures until
|
|
68
|
+
producer adapters exist.
|
|
69
|
+
- Treat HubSpot writes as rerun-to-convergence operations, not transactional
|
|
70
|
+
rollbacks.
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# Migration Plan
|
|
2
|
+
|
|
3
|
+
This plan extracts the HubSpot CMS sync system from
|
|
4
|
+
`../7thsense-website/sync` into this standalone npm package.
|
|
5
|
+
|
|
6
|
+
The goal is not just moving files. The goal is to turn a repo-specific set of
|
|
7
|
+
working scripts into a reusable product surface:
|
|
8
|
+
|
|
9
|
+
- npm CLI for deterministic local and CI execution.
|
|
10
|
+
- Config-driven adapters instead of Seventh Sense constants in code.
|
|
11
|
+
- Clean import path from a consuming website repo.
|
|
12
|
+
- Optional Codex skill that uses the npm CLI rather than duplicating the engine.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Current Source Inventory
|
|
17
|
+
|
|
18
|
+
Move or port these from `../7thsense-website/sync`:
|
|
19
|
+
|
|
20
|
+
| Source file | Package target | Notes |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `sync/pull.mjs` | `src/commands/pull.mjs` | Convert CLI parsing to shared command runner |
|
|
23
|
+
| `sync/push.mjs` | `src/commands/push.mjs` | Keep prod guard, make read-only portals config-driven |
|
|
24
|
+
| `sync/preflight.mjs` | `src/commands/preflight.mjs` | Make theme/blog expectations config-driven |
|
|
25
|
+
| `sync/republish.mjs` | `src/commands/republish.mjs` | Convert raw portal arg to account resolution |
|
|
26
|
+
| `sync/manifest.mjs` | `src/manifest.mjs` | Generalize theme name and page filters |
|
|
27
|
+
| `sync/cta-inventory.mjs` | `src/cta-inventory.mjs` | Keep as optional read-only CTA normalization helper |
|
|
28
|
+
| `sync/lib/*.mjs` | `src/lib/*.mjs` | Remove hardcoded repo roots and known portals |
|
|
29
|
+
| `sync/adapters/*.mjs` | `src/adapters/*.mjs` | Make paths and constants config-driven |
|
|
30
|
+
| `scripts/corpus-scan.mjs` | `src/commands/corpus.mjs` | Required for `hcms corpus`; currently outside `sync/` |
|
|
31
|
+
| `test/unit/**/*.test.mjs` | `test/unit/` | Engine tests move with the engine and import from `src/` |
|
|
32
|
+
| `test/integration/**/*.test.mjs` | `test/integration/` | Live/dev-account engine integration tests move with package |
|
|
33
|
+
|
|
34
|
+
Leave these behind or archive as legacy in the website repo:
|
|
35
|
+
|
|
36
|
+
| Legacy file | Action |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `sync/blog-sync.mjs` | Remove after adapter parity is confirmed |
|
|
39
|
+
| `sync/cms-pull.mjs` | Remove after unified `pull` covers page definitions |
|
|
40
|
+
| `sync/forms-sync.mjs` | Remove after forms adapter handles push/pull |
|
|
41
|
+
| `sync/page-content.mjs` | Remove after content adapter covers widgets |
|
|
42
|
+
|
|
43
|
+
Keep these in the website repo:
|
|
44
|
+
|
|
45
|
+
| Website-local files | Reason |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `verify/**/*.spec.mjs` | Site-specific Playwright fidelity/forms/links gates |
|
|
48
|
+
| `verify/**/*-snapshots/**` | Site-specific visual baselines |
|
|
49
|
+
| `content/**`, `templates/**`, `modules/**`, `css/**`, `js/**`, `images/**` | The consuming site source of truth |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Target Package Layout
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
hubspot-cms-sync/
|
|
57
|
+
├── bin/
|
|
58
|
+
│ └── hubspot-cms-sync.mjs
|
|
59
|
+
├── src/
|
|
60
|
+
│ ├── index.mjs
|
|
61
|
+
│ ├── cli.mjs
|
|
62
|
+
│ ├── config.mjs
|
|
63
|
+
│ ├── manifest.mjs
|
|
64
|
+
│ ├── commands/
|
|
65
|
+
│ │ ├── init.mjs
|
|
66
|
+
│ │ ├── pull.mjs
|
|
67
|
+
│ │ ├── push.mjs
|
|
68
|
+
│ │ ├── preflight.mjs
|
|
69
|
+
│ │ ├── republish.mjs
|
|
70
|
+
│ │ ├── corpus.mjs
|
|
71
|
+
│ │ └── verify.mjs
|
|
72
|
+
│ ├── adapters/
|
|
73
|
+
│ │ ├── assets.mjs
|
|
74
|
+
│ │ ├── blog.mjs
|
|
75
|
+
│ │ ├── content.mjs
|
|
76
|
+
│ │ ├── forms.mjs
|
|
77
|
+
│ │ ├── pages.mjs
|
|
78
|
+
│ │ └── theme.mjs
|
|
79
|
+
│ └── lib/
|
|
80
|
+
│ ├── canonical.mjs
|
|
81
|
+
│ ├── hub.mjs
|
|
82
|
+
│ ├── orchestrate.mjs
|
|
83
|
+
│ ├── refs.mjs
|
|
84
|
+
│ └── sync-state.mjs
|
|
85
|
+
├── examples/
|
|
86
|
+
│ ├── hubspot-cms-sync.config.mjs
|
|
87
|
+
│ └── site.manifest.json
|
|
88
|
+
├── test/
|
|
89
|
+
└── docs/
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Phase 1: Extract Without Generalizing Too Much
|
|
95
|
+
|
|
96
|
+
Objective: prove the package can run the Seventh Sense workflow from outside the
|
|
97
|
+
website repo.
|
|
98
|
+
|
|
99
|
+
Steps:
|
|
100
|
+
|
|
101
|
+
1. Copy unified sync files into `src/`.
|
|
102
|
+
2. Add a CLI wrapper with these commands:
|
|
103
|
+
- `pull <account>`
|
|
104
|
+
- `preflight <account>`
|
|
105
|
+
- `push <account> [--publish]`
|
|
106
|
+
- `republish <account|portalId> [--all] [--blog]`
|
|
107
|
+
- `corpus [paths...]`
|
|
108
|
+
3. Add `--root <path>` and default it to `process.cwd()`.
|
|
109
|
+
4. Replace package-relative repo roots with an explicit resolved config object,
|
|
110
|
+
built once in `src/cli.mjs` and passed into commands/libs/adapters. Do **not**
|
|
111
|
+
use module-level `configure()` global state.
|
|
112
|
+
Modules that currently need root/config threading:
|
|
113
|
+
- `hub.mjs`: accounts path currently resolves from `__dirname`.
|
|
114
|
+
- `manifest.mjs`: repo root and manifest path currently resolve from `__dirname`.
|
|
115
|
+
- `sync-state.mjs`: `content/` and `.sync-state/` currently resolve from package source.
|
|
116
|
+
- `theme.mjs`: theme root and temp `.sync-state` build path currently resolve from package source.
|
|
117
|
+
- `preflight.mjs`: repo root and theme name currently resolve from package source.
|
|
118
|
+
- `refs.mjs`: known portal IDs are currently hardcoded.
|
|
119
|
+
5. Keep the current adapter names and behavior.
|
|
120
|
+
6. Run the package against `../7thsense-website` using `node ../hubspot-cms-sync/bin/... --root .`.
|
|
121
|
+
7. Do not delete website repo scripts yet.
|
|
122
|
+
|
|
123
|
+
Acceptance:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
cd ../7thsense-website
|
|
127
|
+
node ../hubspot-cms-sync/bin/hubspot-cms-sync.mjs corpus
|
|
128
|
+
node ../hubspot-cms-sync/bin/hubspot-cms-sync.mjs preflight dev
|
|
129
|
+
node ../hubspot-cms-sync/bin/hubspot-cms-sync.mjs push dev --dry-run
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The first extraction is allowed to still require a Seventh Sense-shaped config.
|
|
133
|
+
It must also prove the engine works when `cwd` is the website repo and package
|
|
134
|
+
source lives elsewhere; no command may accidentally read/write inside
|
|
135
|
+
`node_modules/hubspot-cms-sync` or `../hubspot-cms-sync`.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Phase 2: Config-Drive Repo-Specific Values
|
|
140
|
+
|
|
141
|
+
Objective: make the package reusable for another HubSpot CMS site.
|
|
142
|
+
|
|
143
|
+
Replace hardcoded values with `hubspot-cms-sync.config.mjs`:
|
|
144
|
+
|
|
145
|
+
- Account registry path.
|
|
146
|
+
- Key directory env var.
|
|
147
|
+
- Read-only portal IDs.
|
|
148
|
+
- Known portal IDs for canonicalization.
|
|
149
|
+
- Theme name.
|
|
150
|
+
- Theme roots and files.
|
|
151
|
+
- Content directory.
|
|
152
|
+
- Sync-state directory.
|
|
153
|
+
- Manifest path.
|
|
154
|
+
- Blog slug and template paths.
|
|
155
|
+
- UI-gated prerequisites.
|
|
156
|
+
- Asset host canonicalization policy.
|
|
157
|
+
- Forms desired-state path.
|
|
158
|
+
- CTA inventory behavior.
|
|
159
|
+
- Optional external adapter search paths, if plugin adapters are supported.
|
|
160
|
+
|
|
161
|
+
Acceptance:
|
|
162
|
+
|
|
163
|
+
- No `seventh-sense`, `theseventhsense`, `246389711`, or `529456` hardcoded in
|
|
164
|
+
package source except examples/tests.
|
|
165
|
+
- Tests cover config loading and defaults.
|
|
166
|
+
- Package can run from a fixture project with a different theme name.
|
|
167
|
+
- `READ_ONLY_PORTAL` is replaced by `config.readOnlyPortalIds` membership checks.
|
|
168
|
+
- `KNOWN_PORTALS` is replaced by config-derived known portal IDs, or by a
|
|
169
|
+
documented discovery rule from accounts + registry.
|
|
170
|
+
- Config loader behavior is tested: missing config, invalid config, missing
|
|
171
|
+
accounts file, missing key file, and invalid manifest all print remediation
|
|
172
|
+
and exit non-zero.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Phase 3: Public CLI Surface
|
|
177
|
+
|
|
178
|
+
Objective: make the tool understandable and safe for normal users.
|
|
179
|
+
|
|
180
|
+
Add:
|
|
181
|
+
|
|
182
|
+
- `init`: writes example config and manifest.
|
|
183
|
+
- `doctor`: checks Node version, config, accounts, keys, manifest, and theme paths.
|
|
184
|
+
- `plan`: dry-run pull or push and print a resource summary.
|
|
185
|
+
- `push --dry-run`: resolves refs and preflights without writes.
|
|
186
|
+
- `verify`: orchestrates configured local/remote verification commands.
|
|
187
|
+
- `preview`: optional wrapper around push-to-dev plus verification.
|
|
188
|
+
|
|
189
|
+
Target command set:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
hcms init
|
|
193
|
+
hcms doctor
|
|
194
|
+
hcms pull prod
|
|
195
|
+
hcms corpus
|
|
196
|
+
hcms preflight dev
|
|
197
|
+
hcms push dev --dry-run
|
|
198
|
+
hcms push dev --publish
|
|
199
|
+
hcms republish dev --all --blog
|
|
200
|
+
repo verification dev
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Guardrails:
|
|
204
|
+
|
|
205
|
+
- Read-only portal IDs refuse push and preflight-by-default.
|
|
206
|
+
- Any unsupported logical ref fails before network writes.
|
|
207
|
+
- Missing UI-gated prerequisites produce remediation text.
|
|
208
|
+
- `--force-read-only` should not exist.
|
|
209
|
+
- Command outputs that agents/CI need to parse should have a stable JSON mode,
|
|
210
|
+
e.g. `--json` for `doctor`, `plan`, `preflight`, and `verify`.
|
|
211
|
+
|
|
212
|
+
### Engine limitations carried into v1
|
|
213
|
+
|
|
214
|
+
The first public CLI should be honest about known limits:
|
|
215
|
+
|
|
216
|
+
- CTA and menu producer adapters do not exist yet. Any surviving `@cta:*` or
|
|
217
|
+
`@menu:*` token fails closed at push preflight.
|
|
218
|
+
- Legacy image host canonicalization is best-effort and may need project policy
|
|
219
|
+
for non-HubSpot or unrecoverable asset URLs.
|
|
220
|
+
- HubSpot writes are not globally transactional. A transient API failure after
|
|
221
|
+
earlier writes can leave a partial target state; rerun-to-convergence is the
|
|
222
|
+
recovery model.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Phase 4: Move Website Repo To The Package
|
|
227
|
+
|
|
228
|
+
Objective: remove local sync implementation from `../7thsense-website`.
|
|
229
|
+
|
|
230
|
+
Website repo changes:
|
|
231
|
+
|
|
232
|
+
1. Add package dependency:
|
|
233
|
+
```json
|
|
234
|
+
"devDependencies": {
|
|
235
|
+
"hubspot-cms-sync": "file:../hubspot-cms-sync"
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
2. Add `hubspot-cms-sync.config.mjs`.
|
|
239
|
+
3. Update `package.json` scripts:
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"sync:pull": "hcms pull",
|
|
243
|
+
"sync:push": "hcms push",
|
|
244
|
+
"sync:preflight": "hcms preflight",
|
|
245
|
+
"sync:republish": "hcms republish",
|
|
246
|
+
"corpus": "hcms corpus"
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
4. Delete local unified sync files after parity:
|
|
250
|
+
- `sync/pull.mjs`
|
|
251
|
+
- `sync/push.mjs`
|
|
252
|
+
- `sync/preflight.mjs`
|
|
253
|
+
- `sync/republish.mjs`
|
|
254
|
+
- `sync/manifest.mjs`
|
|
255
|
+
- `sync/cta-inventory.mjs`
|
|
256
|
+
- `sync/lib/**`
|
|
257
|
+
- `sync/adapters/**`
|
|
258
|
+
5. Delete legacy scripts:
|
|
259
|
+
- `sync/blog-sync.mjs`
|
|
260
|
+
- `sync/cms-pull.mjs`
|
|
261
|
+
- `sync/forms-sync.mjs`
|
|
262
|
+
- `sync/page-content.mjs`
|
|
263
|
+
6. Keep only project-local config, content, theme, tests, and docs.
|
|
264
|
+
7. Repoint or remove website unit tests before deleting `sync/**`. Engine tests
|
|
265
|
+
should have moved to the package; website repo tests should import package
|
|
266
|
+
exports only if they are validating website-specific integration behavior.
|
|
267
|
+
|
|
268
|
+
Acceptance in website repo:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npm ci
|
|
272
|
+
npm run test:unit
|
|
273
|
+
npm run corpus
|
|
274
|
+
npm run sync:preflight -- dev
|
|
275
|
+
npm run sync:push -- dev --dry-run
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
This gate is only valid after test ownership is resolved. Do not delete
|
|
279
|
+
`sync/**` while website tests still import `../../sync/...`.
|
|
280
|
+
|
|
281
|
+
If credentials are present and the operator intends to write:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npm run sync:push -- dev --publish
|
|
285
|
+
npm test
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Phase 5: CI And PR Gates
|
|
291
|
+
|
|
292
|
+
Objective: make preview and deploy flows package-owned.
|
|
293
|
+
|
|
294
|
+
Package should provide reusable CI examples:
|
|
295
|
+
|
|
296
|
+
- `examples/github-actions/ci.yml`
|
|
297
|
+
- `examples/github-actions/publish.yml`
|
|
298
|
+
- `examples/github-actions/preview.yml`
|
|
299
|
+
|
|
300
|
+
Website repo should use:
|
|
301
|
+
|
|
302
|
+
- Unit/corpus checks on every PR.
|
|
303
|
+
- Optional `hcms preview dev --publish --verify` for preview environment.
|
|
304
|
+
- Manual `Publish` workflow for deployment.
|
|
305
|
+
- Environment-scoped `HUBSPOT_PORTAL_KEY`.
|
|
306
|
+
|
|
307
|
+
PR gates should include:
|
|
308
|
+
|
|
309
|
+
- `hcms corpus`
|
|
310
|
+
- `hcms push dev --dry-run`
|
|
311
|
+
- `npm run test:unit` or package-provided unit fixtures
|
|
312
|
+
- Playwright fidelity/forms/links checks against the preview URL
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Phase 6: npm Publication
|
|
317
|
+
|
|
318
|
+
Objective: publish as an installable CLI.
|
|
319
|
+
|
|
320
|
+
Before publication:
|
|
321
|
+
|
|
322
|
+
- Rename package if needed (`@seventhsense/hubspot-cms-sync` vs `hubspot-cms-sync`).
|
|
323
|
+
- Set `"private": false`.
|
|
324
|
+
- Add `LICENSE`.
|
|
325
|
+
- Add full README.
|
|
326
|
+
- Add fixture tests.
|
|
327
|
+
- Add provenance-capable GitHub release workflow.
|
|
328
|
+
- Add semver policy.
|
|
329
|
+
- Scrub private portal IDs from `examples/` and docs shipped in the npm tarball,
|
|
330
|
+
or exclude those docs from `package.json#files`.
|
|
331
|
+
- Flip `"private": false`.
|
|
332
|
+
- Bump from `0.0.0` to an intentional prerelease version.
|
|
333
|
+
- Add `prepublishOnly` that runs the real test suite, lint, and `npm pack --dry-run`.
|
|
334
|
+
- Confirm package tarball contents contain no keys, `.sync-state`, private portal
|
|
335
|
+
IDs, or customer-specific docs.
|
|
336
|
+
|
|
337
|
+
Release commands:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
npm pack --dry-run
|
|
341
|
+
npm publish --provenance
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Phase 7: Codex Skill Companion
|
|
347
|
+
|
|
348
|
+
Objective: create a `hubspot-cms-sync` skill that tells Codex how to operate the
|
|
349
|
+
npm CLI safely.
|
|
350
|
+
|
|
351
|
+
The skill should not duplicate the sync engine. It should:
|
|
352
|
+
|
|
353
|
+
- Detect whether the npm CLI is installed.
|
|
354
|
+
- Read `hubspot-cms-sync.config.mjs`.
|
|
355
|
+
- Run `hcms doctor`, `hcms corpus`, `hcms preflight`, `hcms push --dry-run`, `hcms push`,
|
|
356
|
+
and `repo verification`.
|
|
357
|
+
- Interpret common HubSpot errors.
|
|
358
|
+
- Guide PR preview/deployment workflows.
|
|
359
|
+
- Capture screenshots via the consuming repo's Playwright commands.
|
|
360
|
+
|
|
361
|
+
See [SKILL_DISTRIBUTION.md](./SKILL_DISTRIBUTION.md).
|