kalup 0.1.0 → 0.2.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 (78) hide show
  1. package/README.md +19 -11
  2. package/dist/{commands-C--Xsk1p.mjs → commands-Bk1w1oup.mjs} +10579 -9638
  3. package/dist/commands.d.mts +5 -1
  4. package/dist/commands.mjs +1 -1
  5. package/dist/{context-D10Syqfd.d.mts → context-8pARDYRR.d.mts} +74 -7
  6. package/dist/{host-BJWvo3sX.mjs → host-BoqS00po.mjs} +1 -1
  7. package/dist/host.d.mts +1 -1
  8. package/dist/host.mjs +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/schemas/blueprint-1.schema.json +42 -4
  11. package/dist/schemas/blueprints-lock-1.schema.json +2 -2
  12. package/dist/schemas/ir-1.schema.json +42 -5
  13. package/dist/schemas/plan-1.schema.json +1 -1
  14. package/docs/apply.md +3 -3
  15. package/docs/blueprints.md +3 -3
  16. package/docs/compare.md +1 -1
  17. package/docs/config.md +32 -16
  18. package/docs/errors/E_BAD_CHAIN.md +1 -1
  19. package/docs/errors/E_BIOME_CONFIG.md +2 -2
  20. package/docs/errors/E_BLUEPRINT_ADDED.md +2 -2
  21. package/docs/errors/E_BLUEPRINT_INTEGRITY.md +1 -1
  22. package/docs/errors/E_BLUEPRINT_LOCK.md +3 -3
  23. package/docs/errors/E_BLUEPRINT_ORIGINAL.md +3 -3
  24. package/docs/errors/E_BLUEPRINT_SOURCE.md +1 -1
  25. package/docs/errors/E_BLUEPRINT_UNKNOWN.md +2 -2
  26. package/docs/errors/E_DEFINITION_FIELD.md +29 -0
  27. package/docs/errors/E_DIR_AMBIGUOUS.md +17 -0
  28. package/docs/errors/E_DIR_IN_USE.md +17 -0
  29. package/docs/errors/E_DUPLICATE_ADDRESS.md +1 -1
  30. package/docs/errors/E_DUPLICATE_ALIAS.md +1 -1
  31. package/docs/errors/E_DUPLICATE_KEY.md +1 -1
  32. package/docs/errors/E_DUPLICATE_OPTION.md +1 -1
  33. package/docs/errors/E_HS_PREFIX.md +1 -1
  34. package/docs/errors/E_KEY_COLLISION.md +1 -1
  35. package/docs/errors/E_LIFECYCLE.md +3 -3
  36. package/docs/errors/E_LOCKED.md +1 -1
  37. package/docs/errors/E_MISSING_EXPORT.md +4 -4
  38. package/docs/errors/E_MISSING_KEY.md +1 -1
  39. package/docs/errors/E_NOT_DATA.md +1 -1
  40. package/docs/errors/E_OVERRIDE_DEFINITION.md +2 -2
  41. package/docs/errors/E_PENDING_TARGET.md +17 -0
  42. package/docs/errors/E_PLAN_DELETE.md +1 -1
  43. package/docs/errors/E_PORTAL_ID.md +2 -2
  44. package/docs/errors/E_PREVENT_DESTROY.md +1 -1
  45. package/docs/errors/E_PROJECT_WRITE.md +1 -1
  46. package/docs/errors/E_PROTECTED_SAVED_PLAN.md +4 -4
  47. package/docs/errors/E_PULL_INVALID.md +1 -1
  48. package/docs/errors/E_REFERENCE_DEFINITION.md +1 -1
  49. package/docs/errors/E_RM_DEPENDENTS.md +1 -1
  50. package/docs/errors/E_SETTING_VALUE.md +3 -1
  51. package/docs/errors/E_STANDARD_OBJECT.md +1 -1
  52. package/docs/errors/E_STRICT_WITHOUT_OPTIONS.md +1 -1
  53. package/docs/errors/E_TARGET_PORTAL_MISMATCH.md +1 -1
  54. package/docs/errors/E_TOMBSTONE_ADDRESS.md +3 -3
  55. package/docs/errors/E_TOMBSTONE_CONFLICT.md +3 -3
  56. package/docs/errors/E_TYPE_FIELDTYPE.md +2 -2
  57. package/docs/errors/E_UNKNOWN_BUILDER.md +2 -2
  58. package/docs/errors/E_UNKNOWN_GROUP.md +1 -1
  59. package/docs/errors/E_UNKNOWN_INCLUDE.md +3 -1
  60. package/docs/errors/E_UNSUPPORTED_FILE.md +4 -4
  61. package/docs/errors/E_USAGE.md +1 -1
  62. package/docs/errors/E_WRITE_IN_READ_MODE.md +1 -1
  63. package/docs/errors/W_CODEC_MISMATCH.md +1 -1
  64. package/docs/errors/W_JSON_FIELDTYPE.md +1 -1
  65. package/docs/errors/W_LARGE_SCOPE.md +5 -5
  66. package/docs/errors/W_LEGACY_DIR.md +17 -0
  67. package/docs/errors/W_PENDING_TARGET.md +23 -0
  68. package/docs/errors/W_PREFIX.md +1 -1
  69. package/docs/errors/W_STATE_NOT_MOVED.md +17 -0
  70. package/docs/errors/W_UNADDRESSABLE_NAME.md +1 -1
  71. package/docs/errors/W_UNSUPPORTED_TYPE.md +3 -3
  72. package/docs/plan.md +2 -2
  73. package/docs/pull.md +9 -9
  74. package/docs/rm.md +3 -3
  75. package/docs/state.md +4 -2
  76. package/docs/targets.md +3 -3
  77. package/package.json +2 -8
  78. package/docs/errors/E_FIRST_PULL.md +0 -18
@@ -4,7 +4,7 @@ A warning from `pull`: the file's builder does not match the portal's property t
4
4
 
5
5
  ## When
6
6
 
7
- The file has `p.string` for a `number` in the portal, say, or `p.enum` where the portal fieldType is `checkbox`, which only `p.multiEnum` takes. Pull keeps the property as written and refreshes nothing on it, so the app's types hold and the file still validates. A fieldType no builder takes, such as `calculation_equation`, is not a mismatch.
7
+ The file has `p.string` for a `number` in the portal, say, or `p.enum` where the portal fieldType is `checkbox`, which only `p.multiEnum` takes. Pull keeps the property as written and refreshes nothing on it, so the app's types hold and the file still validates. A fieldType no builder takes, such as `calculation_rollup`, is not a mismatch. A custom HubSpot user property in the portal is managed by `p.owner` only, so another builder over it is a mismatch.
8
8
 
9
9
  ## Fix
10
10
 
@@ -13,5 +13,5 @@ Set `fieldType: 'textarea'`.
13
13
  ## Example
14
14
 
15
15
  ```
16
- kalup/objects/companies.ts:24: W_JSON_FIELDTYPE: p.json 'orch_row_meta' has fieldType 'text'; JSON text belongs in a textarea (fix: set fieldType: 'textarea') (docs: errors/W_JSON_FIELDTYPE.md)
16
+ hubspot/objects/companies.ts:24: W_JSON_FIELDTYPE: p.json 'orch_row_meta' has fieldType 'text'; JSON text belongs in a textarea (fix: set fieldType: 'textarea') (docs: errors/W_JSON_FIELDTYPE.md)
17
17
  ```
@@ -1,23 +1,23 @@
1
1
  # W_LARGE_SCOPE
2
2
 
3
- A warning from `init`: the first pull wrote more than 200 properties for one object. Exit stays 0.
3
+ A warning from `pull`: it wrote more than 200 properties into a new object file. Exit stays 0.
4
4
 
5
5
  ## When
6
6
 
7
- `init` writes `{}` for each object, so every custom property is in the pull scope.
7
+ `init` writes `{}` for each object, so every custom property is in the pull scope, and the first pull writes each object file.
8
8
 
9
9
  ## Fix
10
10
 
11
- If the app needs only some of them, set `custom: false` for that object and list the ones it needs under `include`. Properties already in the file stay; pull never removes a property.
11
+ If the app needs only some of them, set `custom: false` for that object, then delete the properties it does not need from the object file. Every property the file keeps stays in the pull scope; with `custom: false` pull adds no other custom property, and removing one from a file never deletes it in HubSpot. `include` names any other property the app needs.
12
12
 
13
13
  ## Example
14
14
 
15
15
  ```ts
16
16
  objects: {
17
- companies: { custom: false, include: ['plot_count', 'soil_type'] },
17
+ companies: { custom: false, include: ['domain'] },
18
18
  },
19
19
  ```
20
20
 
21
21
  ```
22
- W_LARGE_SCOPE: the first pull wrote 312 properties for companies: every custom property is in the pull scope (fix: set objects.companies.custom to false and list the properties the app needs under objects.companies.include) (docs: errors/W_LARGE_SCOPE.md)
22
+ W_LARGE_SCOPE: the pull wrote 312 properties into the new file for companies: every custom property is in the pull scope (fix: set objects.companies.custom to false, then delete the properties the app does not need from hubspot/objects/companies.ts) (docs: errors/W_LARGE_SCOPE.md)
23
23
  ```
@@ -0,0 +1,17 @@
1
+ # W_LEGACY_DIR
2
+
3
+ A warning from every command that reads the project: the object files are in `kalup/`. Exit stays 0.
4
+
5
+ ## When
6
+
7
+ Kalup 0.2 keeps the object files in the folder `dir` in `kalup.config.ts` names, `hubspot/` by default. When `dir` is not set, `kalup/` holds .ts files and `hubspot/` holds none, Kalup keeps reading and writing `kalup/` and warns once per command. When both hold .ts files it stops with `E_DIR_AMBIGUOUS` instead.
8
+
9
+ ## Fix
10
+
11
+ Add `dir: 'kalup'` to `kalup.config.ts` to keep the folder, or move `kalup/` to `hubspot/` (`git mv kalup hubspot`) and change the imports of `./kalup` in the app. If the folder holds `blueprints.lock.json`, replace `kalup/.blueprints/` with `hubspot/.blueprints/` in it. Update the formatter ignore `init` wrote (`!kalup` in biome, `kalup/` in `.prettierignore`) to the folder you keep.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ kalup.config.ts: W_LEGACY_DIR: the object files are in kalup/, the folder Kalup 0.1 used; the default is now hubspot/ (fix: add dir: 'kalup' to kalup.config.ts, or move kalup/ to hubspot/) (docs: errors/W_LEGACY_DIR.md)
17
+ ```
@@ -0,0 +1,23 @@
1
+ # W_PENDING_TARGET
2
+
3
+ A warning from `validate` and every command that validates: a target has no `portalId` yet. Exit stays 0.
4
+
5
+ ## When
6
+
7
+ `kalup init` without `--portal` writes a pending target, since init never asks HubSpot. Commands that only read the files work; one that needs the portal refuses the target with `E_PENDING_TARGET`. A pending target is left out of the IR, and it pins no portal.
8
+
9
+ ## Fix
10
+
11
+ Set `portalId` on the target to the Hub ID from the HubSpot account menu.
12
+
13
+ ## Example
14
+
15
+ ```ts
16
+ targets: {
17
+ production: { credentials: { read: { env: 'HUBSPOT_SERVICE_KEY' } } },
18
+ },
19
+ ```
20
+
21
+ ```
22
+ kalup.config.ts:7: W_PENDING_TARGET: target 'production' has no portalId yet, so no command reads or writes its portal (fix: set targets.production.portalId to the Hub ID from the HubSpot account menu) (docs: errors/W_PENDING_TARGET.md)
23
+ ```
@@ -13,5 +13,5 @@ Rename the property to carry the prefix, or clear `prefix`. A property already i
13
13
  ## Example
14
14
 
15
15
  ```
16
- kalup/objects/companies.ts:14: W_PREFIX: 'soil_type' does not carry the project prefix 'orch_' (fix: rename it to orch_soil_type, or clear prefix in kalup.config.ts) (docs: errors/W_PREFIX.md)
16
+ hubspot/objects/companies.ts:14: W_PREFIX: 'soil_type' does not carry the project prefix 'orch_' (fix: rename it to orch_soil_type, or clear prefix in kalup.config.ts) (docs: errors/W_PREFIX.md)
17
17
  ```
@@ -0,0 +1,17 @@
1
+ # W_STATE_NOT_MOVED
2
+
3
+ A warning from `status`, `pull`, `plan` and `apply`: with `state: 'repo'` the portal has no state file beside the object files, but `.kalup/state/` holds one. Exit stays 0.
4
+
5
+ ## When
6
+
7
+ `state: 'repo'` moves where Kalup reads and writes state, from `.kalup/state/` to `<dir>/state/`. Kalup does not move the file for you. Until it is moved, every command starts from no state: what Kalup created plans as adopt steps, and a value you changed in a file is held as diverged instead of planned as an update.
8
+
9
+ ## Fix
10
+
11
+ Move the file the warning names before the next apply, for example `mkdir -p hubspot/state && mv .kalup/state/portal-2222222.json hubspot/state/portal-2222222.json`, then commit it. If a `pull` already wrote a new file there, the move replaces it, which is what you want: the old file keeps what Kalup created and the values it last applied.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ hubspot/state/portal-2222222.json: W_STATE_NOT_MOVED: state: 'repo' reads hubspot/state/portal-2222222.json, which does not exist, but .kalup/state/portal-2222222.json holds the state from before the switch; this command starts from no state (fix: move it before the next apply: mkdir -p hubspot/state && mv .kalup/state/portal-2222222.json hubspot/state/portal-2222222.json) (docs: errors/W_STATE_NOT_MOVED.md)
17
+ ```
@@ -4,7 +4,7 @@ A warning from `compare`, `plan` and `snapshot`: a portal group or property has
4
4
 
5
5
  ## When
6
6
 
7
- An address is `<type>:<path>` with no whitespace. HubSpot names its groups and properties without spaces, but its API does not promise it. A group whose name holds whitespace is not captured, nor is a property whose own name or group name holds it; the property is listed as out of scope.
7
+ An address is `<type>:<path>` with no whitespace. HubSpot names its groups and properties without spaces, but its API does not promise it. A group whose name holds whitespace is not captured, nor is a property whose own name or group name holds it; the property is listed as out of scope. A property outside the pull scope that config does not name gets no warning.
8
8
 
9
9
  A property config names in such a group is `unaddressable` in coverage instead: unknown, never absent, so `plan` never creates it. `compare` reports it `unknown` (`E_INCOMPLETE`, exit 1), `plan` blocks it, and the read is incomplete (`W_INCOMPLETE` in `snapshot`).
10
10
 
@@ -1,12 +1,12 @@
1
1
  # W_UNSUPPORTED_TYPE
2
2
 
3
- A warning from any command that reads a portal: a property Kalup does not write. It reads as a `p.string` reference. Exit stays 0.
3
+ A warning from any command that reads a portal: a property in the pull scope or the object files that Kalup does not write. It reads as a `p.string` reference. Exit stays 0.
4
4
 
5
5
  ## When
6
6
 
7
- Its HubSpot `type` has no builder (`phone_number`, `object_coordinates`, `json`, or a type Kalup does not know), it is a custom property whose `fieldType` its builder does not allow (a `string` with fieldType `html`, rich text), or it is a custom owner or `externalOptions` property, whose options HubSpot fills. Pull writes it as a `p.string` reference, with `.readonly()` when HubSpot marks its value read-only. A file entry that is already a reference keeps its builder; a managed one becomes a `p.string` reference. `plan` never creates, changes or archives it, and blocks a managed entry; `compare` compares the fields it has.
7
+ Its HubSpot `type` has no builder (`object_coordinates`, `json`, or a type Kalup does not know), it is a custom property whose `fieldType` its builder does not allow (a `calculation_rollup`), or it is a custom `externalOptions` property that is not an owner select or radio (a multi-owner checkbox, or options from elsewhere), whose options HubSpot fills. An owner select or radio is `p.owner`. Pull writes it as a `p.string` reference, with `.readonly()` when HubSpot marks its value read-only. A file entry that is already a reference keeps its builder; a managed one becomes a `p.string` reference. `plan` never creates, changes or archives it, and blocks a managed entry; `compare` compares the fields it has.
8
8
 
9
- A HubSpot-defined or calculated owner or `externalOptions` property raises no warning: it is a `p.string` reference like any HubSpot-defined property.
9
+ A HubSpot-defined or HubSpot-calculated `externalOptions` property raises no warning: it is a reference like any HubSpot-defined property. Nor does a property outside the pull scope that no object file names: the project does not use it.
10
10
 
11
11
  ## Fix
12
12
 
package/docs/plan.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Plan
2
2
 
3
- `kalup plan [--target <name>] [--take config <address[#unit]>] [--exit-code]` shows what apply would do to one target: a step per object, group and property config manages, `definition` overrides applied (config.md), then the releases and deletes tombstones ask for. It writes neither portal nor state.
3
+ `kalup plan [--target <name>] [--take config <address[#unit]>] [--out [<file>]] [--exit-code]` shows what apply would do to one target: a step per object, group and property config manages, `definition` overrides applied (config.md), then the releases and deletes tombstones ask for. It writes neither portal nor state. `--out <file>` saves the plan/1 document; `--out` alone saves it as `.kalup/plans/<target>-<planId>.json` and prints the path. `kalup apply <file>` applies either.
4
4
 
5
5
  This page is the reference. For the walk-through with examples, see [kalup plan](https://kalup.dev/docs/commands/plan) and [Drift](https://kalup.dev/docs/concepts/drift) on the website.
6
6
 
@@ -43,7 +43,7 @@ The first rule that matches: a `skip` override (no step, `coverage.excluded`); a
43
43
 
44
44
  ## Tombstones, missing and orphans
45
45
 
46
- `kalup/removed.ts` tombstones name properties and groups:
46
+ `hubspot/removed.ts` tombstones name properties and groups:
47
47
 
48
48
  - `release`: a `release` step drops the entry, even one naming another portal name; nothing is sent.
49
49
  - `destroy`, present: a `delete`, risk `destructive`, labelled `existed-before-kalup` for an adopted resource, expecting every base unit's live value. Blocked with `policy` without `allowDestroy: true`, `unsupported` when it is not archivable or a group still holds properties (active or archived) the plan does not delete, `not-owned` without an owning entry.
package/docs/pull.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pull
2
2
 
3
- `kalup pull [--target <name>]` reads one target and merges it into `kalup/objects/*.ts` and the target's `definition` overrides. It never writes to the portal (`E_WRITE_IN_READ_MODE`), records in state what the files and the portal agree on (below), and sanitizes portal strings it prints.
3
+ `kalup pull [--target <name>]` reads one target and merges it into `hubspot/objects/*.ts` and the target's `definition` overrides. It never writes to the portal (`E_WRITE_IN_READ_MODE`), records in state what the files and the portal agree on (below), and sanitizes portal strings it prints.
4
4
 
5
5
  This page is the reference. For the walk-through with examples, see [kalup pull](https://kalup.dev/docs/commands/pull) on the website.
6
6
 
@@ -9,22 +9,22 @@ This page is the reference. For the walk-through with examples, see [kalup pull]
9
9
  1. Validate, then pick the target (targets.md).
10
10
  2. The read key, then the portal guard (targets.md).
11
11
  3. The read: custom object schemas when `objects` names one (or with `--discover`), then each object's properties (sensitive ones too) and groups. A 403 on a list is `E_SCOPE`: that object is skipped and pull ends with `E_INCOMPLETE`.
12
- 4. Normalize. A `hubspotDefined` or `calculated` property becomes a reference. A property Kalup does not write becomes a `p.string` reference, with `W_UNSUPPORTED_TYPE`: a `type` or custom `fieldType` no builder carries (`phone_number`, rich text `html`), or a custom owner or `externalOptions` property. A HubSpot-defined owner property is a `p.string` reference with no warning. Options are ordered by `displayOrder`, missing or negative last.
12
+ 4. Normalize. A `hubspotDefined` property, or one HubSpot calculates with a field type other than `calculation_equation`, becomes a reference; a custom `calculation_equation` property is managed with its `calculationFormula`. An owner property (select or radio) is `p.owner`, a `phone_number` one `p.phoneNumber`, rich text a `p.string` with `fieldType: 'html'`. A property Kalup does not write becomes a `p.string` reference, with `W_UNSUPPORTED_TYPE`: a `type` or custom `fieldType` no builder carries (`object_coordinates`, a rollup), or a custom `externalOptions` property that is no owner select or radio. A display field is kept only on the types that show it, and a field holding HubSpot's default is left out. Options are ordered by `displayOrder`, missing or negative last.
13
13
  5. Merge, with state where it owns a resource (below), then validate: an issue is `E_PULL_INVALID`, even with `--check`, and nothing is written.
14
- 6. Write the changed files and `kalup/index.ts` as one, each first copied to `.kalup/history/<timestamp>/`; a failure puts all back (`E_PROJECT_WRITE`).
14
+ 6. Write the changed files and `hubspot/index.ts` as one, each first copied to `.kalup/history/<timestamp>/`; a failure puts all back (`E_PROJECT_WRITE`).
15
15
 
16
16
  ## Scope
17
17
 
18
18
  `objects.<key>` in `kalup.config.ts` decides what pull writes:
19
19
 
20
20
  - `custom` (default `true`): every property that is not HubSpot-defined.
21
- - `include: [...]`: properties by internal name, on top of `custom`; the only way in for HubSpot-defined ones.
21
+ - `include: [...]`: properties by internal name, on top of `custom`; the only way in for HubSpot-defined ones the files do not define.
22
22
  - `exclude: [...]`: internal names left out, `*` matching any run: never pulled, never archived by takeover. `include` wins over a pattern; a name in both is `E_SETTING_VALUE`, so `--discover` and the plan's notes say to take a listed name out of `exclude` rather than add it to `include`.
23
23
  - `as`: the export name for the first pull, by default PascalCase singular (`line_items` to `LineItem`).
24
24
 
25
25
  Under takeover (config.md), pull ends with a line naming what a plan for the target would archive once the files are as pull leaves them, such as what `--only` left out.
26
26
 
27
- A portal property in scope is written unless `kalup/removed.ts` names it or its group, printed `in kalup/removed.ts, not written back` or `its group is in kalup/removed.ts, not written`, never a difference. A file property HubSpot moved into such a group keeps its group, a difference. Pull removes nothing. A file property outside the scope is kept, printed `out of scope, not refreshed`.
27
+ A portal property in scope is written unless `hubspot/removed.ts` names it or its group, printed `in hubspot/removed.ts, not written back` or `its group is in hubspot/removed.ts, not written`, never a difference. A file property HubSpot moved into such a group keeps its group, a difference. Pull removes nothing. Every property an object file defines is in scope, whatever `custom`, `include` and `exclude` say: pull refreshes it, and one the portal lacks is kept and printed `missing in portal` (plan creates it). `E_UNKNOWN_INCLUDE` is only for an `include` name that neither the portal nor the files have.
28
28
 
29
29
  ## Merge rules
30
30
 
@@ -36,7 +36,7 @@ A portal property in scope is written unless `kalup/removed.ts` names it or its
36
36
  - Kalup does not write it (step 4): a reference stays as written, whatever its builder; a full definition becomes a `p.string` reference, a `definition` change.
37
37
  - Builder conflicts with the portal `type` or `fieldType` (`checkbox` on `p.enum`): kept, `W_CODEC_MISMATCH`.
38
38
  - Portal says reference: the definition becomes options-only (`value`, `label`, the file's `as`), or none; `.managed(false)` stays as written.
39
- - Portal says managed: `label`, `group`, `fieldType`, `description`, `hasUniqueValue` and `formField` take the portal value. A field the file states stays, even `description: ''`.
39
+ - Portal says managed: `label`, `group`, `fieldType`, `description`, `hasUniqueValue`, `formField`, `hidden`, `displayOrder`, the display hints, `calculationFormula` and `dataSensitivity` take the portal value. A field the file states stays, even `description: ''`.
40
40
  - Key, builder kind, chain, comments, the `p.json` validator and `lifecycle` come from the file. Pull adds `.readonly()` where HubSpot marks the value read-only (`modificationMetadata.readOnlyValue`), and never removes one.
41
41
 
42
42
  **Options** merge by `value`. A member in both takes the portal's `label`, `hidden` and `description` and keeps the file's `as`. Members follow portal order. A portal-only member is added; a file-only one is kept, printed `only in config`.
@@ -57,7 +57,7 @@ Where state holds agreed values for a resource (its base), each unit is compared
57
57
 
58
58
  `--accept <address[#unit]>` (repeatable, `*` as in `--only`) takes the portal side of those units, as each kept line prints; one matching nothing is `E_ACCEPT_UNMATCHED`.
59
59
 
60
- After the files are written, and only after a complete read, pull records in state the base of every unit the files and the portal agree on, for the addresses `--only` selects: under the portal lock (`E_LOCKED`) with the serial check, as apply saves. An owned entry keeps its origin; an address no entry owns gets origin `pulled`, which owns nothing: the next plan still adopts it, but compares against that base, so a later file edit is a `config-change`, not `diverged`. A field the files leave out because HubSpot holds its default (an empty description, `formField` off, an option with no description) is recorded at that default, so adding it to the file later is a `config-change` too. A unit that still differs keeps its base. `--check` and `--discover` record nothing and take no lock. A plan saved before the pull no longer applies (`E_STATE_CHANGED`).
60
+ After the files are written, and only after a complete read, pull records in state the base of every unit the files and the portal agree on, for the addresses `--only` selects: under the portal lock (`E_LOCKED`) with the serial check, as apply saves. An owned entry keeps its origin; an address no entry owns gets origin `pulled`, which owns nothing: the next plan still adopts it, but compares against that base, so a later file edit is a `config-change`, not `diverged`. A field the files leave out because HubSpot holds its default (an empty description, `formField` off, an option with no description) is recorded at that default, so adding it to the file later is a `config-change` too. A unit that still differs keeps its base. `--check` and `--discover` record nothing and take no lock. A plan saved before the pull no longer applies (`E_STATE_CHANGED`). The pull that creates the state file prints its path. A new object file that gets more than 200 properties warns `W_LARGE_SCOPE`: the scope `init` writes takes every custom property.
61
61
 
62
62
  ## Target overrides
63
63
 
@@ -69,8 +69,8 @@ After the files are written, and only after a complete read, pull records in sta
69
69
 
70
70
  - `--only <glob>`: merge only matching addresses. `*` matches any characters, `/` included: `property:companies/*`.
71
71
  - `--discover`: list what is outside the scope, write nothing.
72
- - `--check`: print the changes and `would write <file>`, write nothing. With `--exit-code`, exit 2 on any difference: a change line but `out of scope`, `skipped`, `in kalup/removed.ts`, a new property in a removed group, `config change kept` or `ignored on this target`, a `W_CODEC_MISMATCH`, or a file to rewrite.
73
- - `--json`: `data` holds `target`, `portalId`, `objects` (counts and `changes[]` per object: `kind`, `address`, and `field`, `before` and `after` when one field differs; a kept value has the file's side in `before`, the portal's in `after`; `kind` is `added`, `changed`, `missing`, `local-only`, `out-of-scope`, `excluded`, `shadowed`, `removed`, `removed-group`, `kept`, `conflict`, `removed-in-hubspot`, `ignored` or `override-group`), `files` and `state` (`recorded`, the resources whose base changed, and `serial`; absent with `--check` or after an incomplete read); with `--discover`, what is outside the scope.
72
+ - `--check`: print the changes and `would write <file>`, write nothing. With `--exit-code`, exit 2 on any difference: a change line but `skipped`, `in hubspot/removed.ts`, a new property in a removed group, `config change kept` or `ignored on this target`, a `W_CODEC_MISMATCH`, or a file to rewrite.
73
+ - `--json`: `data` holds `target`, `portalId`, `objects` (counts and `changes[]` per object: `kind`, `address`, and `field`, `before` and `after` when one field differs; a kept value has the file's side in `before`, the portal's in `after`; `kind` is `added`, `changed`, `missing`, `local-only`, `excluded`, `shadowed`, `removed`, `removed-group`, `kept`, `conflict`, `removed-in-hubspot`, `ignored` or `override-group`), `files` and `state` (`path`, `recorded`, the resources whose base changed, and `serial`; absent with `--check` or after an incomplete read); with `--discover`, what is outside the scope.
74
74
 
75
75
  ## Exit codes
76
76
 
package/docs/rm.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Remove
2
2
 
3
- `kalup rm <address> [--release] [--json]` takes a property or property group out of config and writes its tombstone in `kalup/removed.ts`. Absence never deletes: a resource dropped from an object file by hand stays in the portal and comes back on the next pull. `rm` is the only way to ask for a delete, and it works offline: it reads no key, sends no request and never touches state.
3
+ `kalup rm <address> [--release] [--json]` takes a property or property group out of config and writes its tombstone in `hubspot/removed.ts`. Absence never deletes: a resource dropped from an object file by hand stays in the portal and comes back on the next pull. `rm` is the only way to ask for a delete, and it works offline: it reads no key, sends no request and never touches state.
4
4
 
5
5
  This page is the reference. For the walk-through with examples, see [kalup rm](https://kalup.dev/docs/commands/rm) on the website.
6
6
 
@@ -8,9 +8,9 @@ This page is the reference. For the walk-through with examples, see [kalup rm](h
8
8
 
9
9
  - The address must be `property:<object>/<name>` or `group:<object>/<name>` (`E_TOMBSTONE_ADDRESS`, exit 3). Custom objects are not removed in this release.
10
10
  - The property, or the group entry, leaves the export that defines it, in whichever file holds it. An export left with no properties keeps its groups; `defineObject` accepts it.
11
- - `kalup/removed.ts` gets `'<address>': { action: 'destroy' }`, or `'release'` with `--release`. An address already there gets its action changed; the same action writes nothing.
11
+ - `hubspot/removed.ts` gets `'<address>': { action: 'destroy' }`, or `'release'` with `--release`. An address already there gets its action changed; the same action writes nothing.
12
12
  - An address config does not define (an orphan the plan lists) gets the tombstone alone.
13
- - `kalup/index.ts` is written again.
13
+ - `hubspot/index.ts` is written again.
14
14
 
15
15
  Before writing, rm validates the project as it would leave it; any issue is exit 3 and nothing is written. Each file is copied to `.kalup/history/<timestamp>/`, then all of them are written through one staged write: temporary files first, then a rename each, and on a failure every file is put back (`E_PROJECT_WRITE`).
16
16
 
package/docs/state.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # State
2
2
 
3
- Kalup keeps what it knows about each portal in `.kalup/state/portal-<portalId>.json`: which resources it created or adopted there, and per unit the value config and the portal last agreed on (the base). `plan` reads it; `apply`, `state rebuild --write` and `target rebind` write it, and `pull` (so `init`) records the units the files and the portal agree on (pull.md): an address no entry owns gets origin `pulled`, which owns nothing. Every worktree of one clone shares it; `KALUP_STATE_DIR` moves it. Never edit it by hand. `kalup status` prints its path, lineage, serial and last apply per target; "an apply did not finish" means run `kalup plan`.
3
+ Kalup keeps what it knows about each portal in `.kalup/state/portal-<portalId>.json`: which resources it created or adopted there, and per unit the value config and the portal last agreed on (the base). `plan` reads it; `apply`, `state rebuild --write` and `target rebind` write it, and `pull` records the units the files and the portal agree on (pull.md): an address no entry owns gets origin `pulled`, which owns nothing. The first pull that creates the file prints its path. Every worktree of one clone shares it, when the main checkout holds the project and ignores its `.kalup/`; otherwise a worktree keeps its own. `KALUP_STATE_DIR` moves it.
4
+
5
+ `state: 'repo'` in `kalup.config.ts` keeps the files in `state/` inside the folder of object files (`hubspot/state/portal-<portalId>.json`), committed, so teammates and CI share bases and ownership. The trade-off: every apply, and every pull that records a base, changes a committed file, so two branches applying to one portal conflict in git. Kalup does not detect a stale or wrongly merged file: one from an older commit, or a conflict resolved to the wrong side, passes every command, and what it no longer records comes back as adopt steps. After a state merge conflict, keep the side that applied last, then run `kalup state rebuild` to see what the file lacks (`--write` rebuilds it from the portal). Before the first run with `state: 'repo'`, move `.kalup/state/portal-<id>.json` to `<dir>/state/`; until you do, commands warn `W_STATE_NOT_MOVED` and start from no state. The file holds no key and no record data. The portal lock, the journal and archived files stay local, and a save keeps no `.bak`: git holds the previous version. Never edit it by hand. `kalup status` prints its path, lineage, serial and last apply per target; "an apply did not finish" means run `kalup plan`.
4
6
 
5
7
  This page is the reference. For the walk-through with examples, see [kalup state](https://kalup.dev/docs/commands/state) on the website.
6
8
 
@@ -15,7 +17,7 @@ Without `--write` it is read-only: it checks the read key's portal, reads the ta
15
17
  - `stale`: current entries that record another portal name, whose resource the portal no longer holds, or whose address is no longer in config.
16
18
  - `excluded`: tombstoned addresses, skipped ones, and ones the read could not see or Kalup does not write.
17
19
 
18
- With `--write`, only a person at a terminal may run it (else `E_APPROVAL_REQUIRED`, exit 4); `--yes` and `--approve` are refused. It checks the write key's portal and refuses an incomplete read (`E_INCOMPLETE`: a rebuild would drop what it could not check). It shows the report and what the current file loses for good (the `created` origin, agreed values, entries not in config), asks for the target name, takes the portal lock, refuses a state file changed since the report (`E_STATE_CHANGED`), archives the current file under `.kalup/state/archive/` (ending its lineage), and writes a new lineage at serial 1: an `adopted` entry for every found resource, with a base for the units that agree. A tombstoned address is never adopted. Nothing is sent to the portal.
20
+ With `--write`, only a person at a terminal may run it (else `E_APPROVAL_REQUIRED`, exit 4); `--yes` and `--approve` are refused. It checks the write key's portal and refuses an incomplete read (`E_INCOMPLETE`: a rebuild would drop what it could not check). It shows the report and what the current file loses for good (the `created` origin, agreed values, entries not in config), asks for the target name, takes the portal lock, refuses a state file changed since the report (`E_STATE_CHANGED`), archives the current file under `.kalup/state/archive/` (ending its lineage; also with `state: 'repo'`), and writes a new lineage at serial 1: an `adopted` entry for every found resource, with a base for the units that agree. A tombstoned address is never adopted. Nothing is sent to the portal.
19
21
 
20
22
  Plans saved before a rebuild are refused by apply (`E_STATE_CHANGED`); plan again.
21
23
 
package/docs/targets.md CHANGED
@@ -32,11 +32,11 @@ The first of several targets is never chosen, and no choice is remembered. Text
32
32
 
33
33
  ## Portal pin and guard
34
34
 
35
- `portalId` is required, a positive integer (`E_PORTAL_ID`): the Hub ID from the HubSpot account menu. Two targets may not pin one portal (`E_DUPLICATE_PORTAL`). Every command that reads a target first checks the account-info of its key against the pin. A mismatch is `E_TARGET_PORTAL_MISMATCH`, exit 4, and nothing more is sent with that key.
35
+ `portalId` is a positive integer (`E_PORTAL_ID`): the Hub ID from the HubSpot account menu. A target without it is pending, as `init` writes it without `--portal`: `validate` warns `W_PENDING_TARGET`, the IR leaves it out, offline commands work, and every command that would read or write its portal refuses it before any request (`E_PENDING_TARGET`, exit 3). `status` lists it as pending. Two targets may not pin one portal (`E_DUPLICATE_PORTAL`). Every command that reads a target first checks the account-info of its key against the pin. A mismatch is `E_TARGET_PORTAL_MISMATCH`, exit 4, and nothing more is sent with that key.
36
36
 
37
37
  ## Keys
38
38
 
39
- `credentials.read.env` names the variable that holds the read key. Without `credentials` it is `HUBSPOT_SERVICE_KEY`, which `init` always reads. The key comes from the environment, else from the project's `.env`. A missing key is `E_MISSING_KEY`. No output carries a key.
39
+ `credentials.read.env` names the variable that holds the read key. Without `credentials` it is `HUBSPOT_SERVICE_KEY`, the variable the target `init` writes names. `init` itself needs no key. The key comes from the environment, else from the project's `.env`. A missing key is `E_MISSING_KEY`. No output carries a key.
40
40
 
41
41
  The key goes out as `Authorization: Bearer`. `init` prints the read scopes the pull scope needs; `status` checks each. Both recommend one `crm.objects.<object>.read` scope too: Limits Tracking answered 403 to `crm.schemas.*` scopes alone and 200 with `crm.objects.companies.read` added (developer test account, 2026-09-29); without it `plan` cannot check the property limit (`W_LIMIT_UNREADABLE`). `credentials.write` names the key apply, `state rebuild --write` and `target rebind` use (apply.md); without it they use the read key. That key needs the read scopes and `crm.schemas.<object>.write` per object (`crm.schemas.custom.write` for custom objects), which `init` and `status` list. Neither checks them: read commands never resolve the write key, and no request can check a write scope.
42
42
 
@@ -51,7 +51,7 @@ The key goes out as `Authorization: Bearer`. `init` prints the read scopes the p
51
51
 
52
52
  ## Policy
53
53
 
54
- `protected: true` marks a portal that accepts only saved plans, not covered by `--yes`. `init` writes it for a `STANDARD` account; with config silent, only test portals, sandboxes and app developer accounts are unprotected. `drift` (`'hold'` or `'overwrite'`, default hold) acts only where state holds a base; `adopt` (same values, default hold) decides a unit with no base, as on a first adoption (plan.md). `allowDestroy` (default false) lets a destroy tombstone or takeover delete in this portal. `yesLimit` (0 to 1000, default 25) caps what `--yes` covers; `0` turns it off. `mode` and `objects: { <object>: { mode } }` set takeover per target (config.md). All of them enter the plan's approval digest, and none is inherited from the project or an object but `mode`.
54
+ `protected: true` marks a portal `--yes` never covers: a person at a terminal applies, a saved plan or one made in the same run, or a reviewed CI job with `--approve` (apply.md). `init` does not write it; with config silent, only test portals, sandboxes and app developer accounts are unprotected. `drift` (`'hold'` or `'overwrite'`, default hold) acts only where state holds a base; `adopt` (same values, default hold) decides a unit with no base, as on a first adoption (plan.md). `allowDestroy` (default false) lets a destroy tombstone or takeover delete in this portal. `yesLimit` (0 to 1000, default 25) caps what `--yes` covers; `0` turns it off. `mode` and `objects: { <object>: { mode } }` set takeover per target (config.md). All of them enter the plan's approval digest, and none is inherited from the project or an object but `mode`.
55
55
 
56
56
  ## Rebind
57
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kalup",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Configuration as code for HubSpot: pull, compare, plan and apply properties and property groups",
5
5
  "keywords": [
6
6
  "hubspot",
@@ -63,13 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@oclif/core": "^5.0.0",
66
- "@kalup/core": "0.1.0"
67
- },
68
- "devDependencies": {
69
- "tsdown": "^0.23.0",
70
- "vitest": "^5.0.1",
71
- "@kalup/tsconfig": "0.0.0",
72
- "@kalup/engine": "0.0.0"
66
+ "@kalup/core": "0.2.0"
73
67
  },
74
68
  "engines": {
75
69
  "node": ">=22.13.1"
@@ -1,18 +0,0 @@
1
- # E_FIRST_PULL
2
-
3
- `init` wrote the project files, but the first pull failed. The exit code is the pull's.
4
-
5
- ## When
6
-
7
- `init` checks the portal, writes `kalup.config.ts`, `.gitignore`, `AGENTS.md`, `CLAUDE.md` and, when it finds a formatter, its ignore entries, then runs a pull. The issue before this one says why the pull failed.
8
-
9
- ## Fix
10
-
11
- Fix that issue, then run the pull yourself. Do not run `init` again: the config exists now, so it would stop with `E_CONFIG_EXISTS`.
12
-
13
- ## Example
14
-
15
- ```
16
- E_AUTH: HubSpot rejected the key (401). (fix: Check that the key is valid and not expired. It needs the scope crm.schemas.companies.read.) (docs: errors/E_AUTH.md)
17
- E_FIRST_PULL: The project files are written, but the first pull failed. (fix: fix the issue above, then run npx kalup pull --target sandbox) (docs: errors/E_FIRST_PULL.md)
18
- ```