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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/bin/hubspot-cms-sync.mjs +115 -0
  4. package/docs/CONFIGURATION.md +83 -0
  5. package/docs/GITHUB_ACTIONS.md +70 -0
  6. package/docs/MIGRATION_PLAN.md +361 -0
  7. package/docs/PLAN_REVIEW.md +42 -0
  8. package/docs/SKILL_DISTRIBUTION.md +79 -0
  9. package/examples/github-actions/ci.yml +56 -0
  10. package/examples/github-actions/preview.yml +71 -0
  11. package/examples/github-actions/publish.yml +82 -0
  12. package/examples/hubspot-cms-sync.config.mjs +45 -0
  13. package/examples/site.manifest.json +19 -0
  14. package/package.json +41 -0
  15. package/skill/SKILL.md +54 -0
  16. package/skill/references/commands.md +54 -0
  17. package/skill/references/config.md +25 -0
  18. package/skill/references/failures.md +58 -0
  19. package/skill/references/github-actions.md +56 -0
  20. package/skill/references/screenshots-and-fidelity.md +33 -0
  21. package/src/adapters/assets.mjs +576 -0
  22. package/src/adapters/blog.mjs +921 -0
  23. package/src/adapters/content.mjs +213 -0
  24. package/src/adapters/forms.mjs +569 -0
  25. package/src/adapters/pages.mjs +463 -0
  26. package/src/adapters/theme.mjs +503 -0
  27. package/src/config.mjs +113 -0
  28. package/src/corpus-scan.mjs +248 -0
  29. package/src/cta-inventory.mjs +352 -0
  30. package/src/index.mjs +3 -0
  31. package/src/lib/canonical.mjs +234 -0
  32. package/src/lib/hub.mjs +197 -0
  33. package/src/lib/orchestrate.mjs +141 -0
  34. package/src/lib/refs.mjs +398 -0
  35. package/src/lib/sync-state.mjs +86 -0
  36. package/src/manifest.mjs +353 -0
  37. package/src/preflight.mjs +385 -0
  38. package/src/pull.mjs +99 -0
  39. package/src/push.mjs +354 -0
  40. 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).