kalup 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 (145) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +68 -0
  4. package/bin/kalup.mjs +13 -0
  5. package/dist/commands-C--Xsk1p.mjs +16145 -0
  6. package/dist/commands.d.mts +191 -0
  7. package/dist/commands.mjs +2 -0
  8. package/dist/context-D10Syqfd.d.mts +1265 -0
  9. package/dist/host-BJWvo3sX.mjs +390 -0
  10. package/dist/host.d.mts +44 -0
  11. package/dist/host.mjs +2 -0
  12. package/dist/index.d.mts +1 -0
  13. package/dist/index.mjs +14 -0
  14. package/dist/schemas/blueprint-1.schema.json +151 -0
  15. package/dist/schemas/blueprints-lock-1.schema.json +76 -0
  16. package/dist/schemas/ir-1.schema.json +379 -0
  17. package/dist/schemas/plan-1.schema.json +868 -0
  18. package/dist/schemas/state-1.schema.json +98 -0
  19. package/docs/apply.md +64 -0
  20. package/docs/blueprints.md +86 -0
  21. package/docs/compare.md +73 -0
  22. package/docs/config.md +88 -0
  23. package/docs/dictionary.md +43 -0
  24. package/docs/errors/E_ACCEPT_UNMATCHED.md +17 -0
  25. package/docs/errors/E_APPROVAL_REQUIRED.md +17 -0
  26. package/docs/errors/E_APPROVE_CREDENTIAL.md +17 -0
  27. package/docs/errors/E_APPROVE_MISMATCH.md +17 -0
  28. package/docs/errors/E_AUTH.md +17 -0
  29. package/docs/errors/E_BAD_CHAIN.md +21 -0
  30. package/docs/errors/E_BINDING_CHANGED.md +17 -0
  31. package/docs/errors/E_BIOME_CONFIG.md +17 -0
  32. package/docs/errors/E_BLUEPRINT_ADDED.md +17 -0
  33. package/docs/errors/E_BLUEPRINT_COLLISION.md +17 -0
  34. package/docs/errors/E_BLUEPRINT_INTEGRITY.md +17 -0
  35. package/docs/errors/E_BLUEPRINT_LOCK.md +17 -0
  36. package/docs/errors/E_BLUEPRINT_ORIGINAL.md +17 -0
  37. package/docs/errors/E_BLUEPRINT_REF.md +17 -0
  38. package/docs/errors/E_BLUEPRINT_REQUIRES.md +17 -0
  39. package/docs/errors/E_BLUEPRINT_SCHEMA.md +17 -0
  40. package/docs/errors/E_BLUEPRINT_SOURCE.md +17 -0
  41. package/docs/errors/E_BLUEPRINT_UNKNOWN.md +17 -0
  42. package/docs/errors/E_BUDGET.md +17 -0
  43. package/docs/errors/E_CANCELLED.md +21 -0
  44. package/docs/errors/E_CONFIG_EXISTS.md +17 -0
  45. package/docs/errors/E_DAILY_LIMIT.md +17 -0
  46. package/docs/errors/E_DEFAULT_TARGET.md +17 -0
  47. package/docs/errors/E_DUPLICATE_ADDRESS.md +17 -0
  48. package/docs/errors/E_DUPLICATE_ALIAS.md +24 -0
  49. package/docs/errors/E_DUPLICATE_KEY.md +24 -0
  50. package/docs/errors/E_DUPLICATE_OPTION.md +25 -0
  51. package/docs/errors/E_DUPLICATE_PORTAL.md +24 -0
  52. package/docs/errors/E_FIRST_PULL.md +18 -0
  53. package/docs/errors/E_HS_PREFIX.md +21 -0
  54. package/docs/errors/E_HTTP.md +17 -0
  55. package/docs/errors/E_INCOMPLETE.md +23 -0
  56. package/docs/errors/E_IR_SCHEMA.md +18 -0
  57. package/docs/errors/E_JOURNAL_WRITE.md +17 -0
  58. package/docs/errors/E_KEY_COLLISION.md +22 -0
  59. package/docs/errors/E_KEY_INVALID.md +19 -0
  60. package/docs/errors/E_LIFECYCLE.md +23 -0
  61. package/docs/errors/E_LOCKED.md +17 -0
  62. package/docs/errors/E_LOCK_DIR.md +17 -0
  63. package/docs/errors/E_MISSING_EXPORT.md +17 -0
  64. package/docs/errors/E_MISSING_KEY.md +17 -0
  65. package/docs/errors/E_NOT_DATA.md +21 -0
  66. package/docs/errors/E_NO_CONFIG.md +17 -0
  67. package/docs/errors/E_NO_TARGETS.md +17 -0
  68. package/docs/errors/E_OVERRIDE_AMBIGUOUS.md +21 -0
  69. package/docs/errors/E_OVERRIDE_DEFINITION.md +26 -0
  70. package/docs/errors/E_OVERRIDE_NAME.md +23 -0
  71. package/docs/errors/E_PLAN_DELETE.md +17 -0
  72. package/docs/errors/E_PLAN_DESTINATION.md +17 -0
  73. package/docs/errors/E_PLAN_DIGEST.md +17 -0
  74. package/docs/errors/E_PLAN_INVALID.md +17 -0
  75. package/docs/errors/E_PLAN_RISK.md +17 -0
  76. package/docs/errors/E_PLAN_SCHEMA.md +17 -0
  77. package/docs/errors/E_PLAN_STALE.md +17 -0
  78. package/docs/errors/E_PLAN_VERSION.md +18 -0
  79. package/docs/errors/E_POLICY_CHANGED.md +17 -0
  80. package/docs/errors/E_PORTAL_ID.md +17 -0
  81. package/docs/errors/E_PREVENT_DESTROY.md +17 -0
  82. package/docs/errors/E_PROJECT_WRITE.md +17 -0
  83. package/docs/errors/E_PROTECTED_SAVED_PLAN.md +17 -0
  84. package/docs/errors/E_PULL_INVALID.md +20 -0
  85. package/docs/errors/E_RATE_LIMIT.md +17 -0
  86. package/docs/errors/E_REBIND_STANDARD.md +17 -0
  87. package/docs/errors/E_REFERENCE_DEFINITION.md +21 -0
  88. package/docs/errors/E_RM_DEPENDENTS.md +17 -0
  89. package/docs/errors/E_SCOPE.md +17 -0
  90. package/docs/errors/E_SETTING_LEVEL.md +21 -0
  91. package/docs/errors/E_SETTING_VALUE.md +23 -0
  92. package/docs/errors/E_SNAPSHOT.md +17 -0
  93. package/docs/errors/E_STANDARD_OBJECT.md +21 -0
  94. package/docs/errors/E_STATE_CHANGED.md +19 -0
  95. package/docs/errors/E_STATE_CONFLICT.md +17 -0
  96. package/docs/errors/E_STATE_INVALID.md +17 -0
  97. package/docs/errors/E_STATE_SCHEMA.md +17 -0
  98. package/docs/errors/E_STATE_WRITE.md +19 -0
  99. package/docs/errors/E_STRICT_WITHOUT_OPTIONS.md +21 -0
  100. package/docs/errors/E_TAKE_UNMATCHED.md +19 -0
  101. package/docs/errors/E_TARGET_NAME.md +17 -0
  102. package/docs/errors/E_TARGET_PORTAL_MISMATCH.md +19 -0
  103. package/docs/errors/E_TARGET_REQUIRED.md +17 -0
  104. package/docs/errors/E_TOMBSTONE_ADDRESS.md +23 -0
  105. package/docs/errors/E_TOMBSTONE_CONFLICT.md +17 -0
  106. package/docs/errors/E_TYPE_FIELDTYPE.md +21 -0
  107. package/docs/errors/E_UNCERTAIN_WRITE.md +17 -0
  108. package/docs/errors/E_UNEXPECTED.md +17 -0
  109. package/docs/errors/E_UNKNOWN_BUILDER.md +21 -0
  110. package/docs/errors/E_UNKNOWN_GROUP.md +21 -0
  111. package/docs/errors/E_UNKNOWN_INCLUDE.md +17 -0
  112. package/docs/errors/E_UNKNOWN_OBJECT.md +17 -0
  113. package/docs/errors/E_UNKNOWN_OVERRIDE.md +17 -0
  114. package/docs/errors/E_UNKNOWN_TARGET.md +17 -0
  115. package/docs/errors/E_UNREACHABLE.md +17 -0
  116. package/docs/errors/E_UNSUPPORTED_FILE.md +17 -0
  117. package/docs/errors/E_USAGE.md +17 -0
  118. package/docs/errors/E_WRITE_IN_READ_MODE.md +17 -0
  119. package/docs/errors/E_WRITE_NOT_ALLOWED.md +17 -0
  120. package/docs/errors/W_BLUEPRINT_DOWNGRADE.md +17 -0
  121. package/docs/errors/W_CODEC_MISMATCH.md +18 -0
  122. package/docs/errors/W_INCOMPLETE.md +19 -0
  123. package/docs/errors/W_JSON_FIELDTYPE.md +17 -0
  124. package/docs/errors/W_KEY_COLLISION.md +17 -0
  125. package/docs/errors/W_LARGE_SCOPE.md +23 -0
  126. package/docs/errors/W_LIMIT_HEADROOM.md +19 -0
  127. package/docs/errors/W_LIMIT_UNREADABLE.md +17 -0
  128. package/docs/errors/W_MODE_SHADOWED.md +17 -0
  129. package/docs/errors/W_OVERRIDE_OPTION.md +17 -0
  130. package/docs/errors/W_PIN_EXPIRES.md +17 -0
  131. package/docs/errors/W_PREFIX.md +17 -0
  132. package/docs/errors/W_RATE_HEADERS.md +21 -0
  133. package/docs/errors/W_RATE_LIMIT.md +17 -0
  134. package/docs/errors/W_UNADDRESSABLE_NAME.md +21 -0
  135. package/docs/errors/W_UNFINISHED_APPLY.md +19 -0
  136. package/docs/errors/W_UNRESOLVED.md +17 -0
  137. package/docs/errors/W_UNSUPPORTED_TYPE.md +19 -0
  138. package/docs/errors/W_UNVERIFIED.md +17 -0
  139. package/docs/plan.md +85 -0
  140. package/docs/pull.md +83 -0
  141. package/docs/rm.md +44 -0
  142. package/docs/snapshot.md +52 -0
  143. package/docs/state.md +37 -0
  144. package/docs/targets.md +58 -0
  145. package/package.json +86 -0
@@ -0,0 +1,98 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kalup.dev/schemas/state-1.schema.json",
4
+ "title": "Kalup portal state, version 1",
5
+ "description": ".kalup/state/portal-<portalId>.json. Describes one verified portal, not the code, and holds no target name. Covered by the compatibility policy (docs/compatibility.md): closed at every level apart from the free-form values in an entry's base and rewrites, so a new field is a new format version. No tokens, record data, unowned fields or unmanaged resources.",
6
+ "type": "object",
7
+ "required": ["format", "lineage", "serial", "portalId", "resources"],
8
+ "properties": {
9
+ "format": { "const": "kalup.state/1" },
10
+ "lineage": {
11
+ "description": "16 lowercase hex characters, new on rebuild and rebind. A plan made against another lineage is refused.",
12
+ "type": "string",
13
+ "pattern": "^[0-9a-f]{16}$"
14
+ },
15
+ "serial": {
16
+ "description": "Increases on every save. A plan binds it, and a save compares it.",
17
+ "type": "integer",
18
+ "minimum": 0
19
+ },
20
+ "portalId": {
21
+ "description": "The verified portal. Every read checks it against the portal the key belongs to.",
22
+ "type": "integer",
23
+ "minimum": 1
24
+ },
25
+ "lastApply": {
26
+ "type": "object",
27
+ "required": ["planId", "writesHash", "actor", "at", "outcome"],
28
+ "properties": {
29
+ "planId": { "type": "string", "pattern": "^pl_[0-9a-f]{12}$" },
30
+ "writesHash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
31
+ "actor": { "type": "string" },
32
+ "at": { "type": "string" },
33
+ "outcome": { "enum": ["running", "done", "partial", "uncertain"] }
34
+ },
35
+ "additionalProperties": false
36
+ },
37
+ "resources": {
38
+ "type": "object",
39
+ "patternProperties": { "^[a-z]+:\\S+$": { "$ref": "#/$defs/resourceState" } },
40
+ "additionalProperties": false
41
+ }
42
+ },
43
+ "additionalProperties": false,
44
+ "$defs": {
45
+ "resourceState": {
46
+ "type": "object",
47
+ "required": ["origin", "id"],
48
+ "properties": {
49
+ "origin": {
50
+ "description": "created and adopted are owned. pulled owns nothing: pull recorded the base of a resource no entry owned, and a plan still adopts it.",
51
+ "enum": ["created", "adopted", "reference", "pulled"]
52
+ },
53
+ "id": {
54
+ "description": "The portal name the entry owns. It owns the resource only while this equals the name the address resolves to. null for runbook-only types.",
55
+ "type": ["string", "null"]
56
+ },
57
+ "via": { "description": "Transport of the last write.", "type": "string" },
58
+ "normVersion": { "description": "Normalizer version of the type that wrote base.", "type": "integer" },
59
+ "base": { "$ref": "#/$defs/base" },
60
+ "baseHash": { "description": "Replaces base for opaque payloads and runbook types.", "type": "string" },
61
+ "attested": {
62
+ "type": "object",
63
+ "required": ["by", "at"],
64
+ "properties": { "by": { "type": "string" }, "at": { "type": "string" } },
65
+ "additionalProperties": false
66
+ },
67
+ "rewrites": {
68
+ "description": "Units whose read-back showed HubSpot storing another value than the one sent, by unit.",
69
+ "type": "object",
70
+ "additionalProperties": {
71
+ "type": "object",
72
+ "required": ["sent", "stored"],
73
+ "properties": { "sent": {}, "stored": {} },
74
+ "additionalProperties": false
75
+ }
76
+ }
77
+ },
78
+ "additionalProperties": false
79
+ },
80
+ "base": {
81
+ "description": "Per owned unit, the value config and portal last agreed on; possibly partial. Scalar units by field name. options: a map keyed by option value whose members hold the agreed label, hidden and description; a member with no fields means only its membership is agreed. optionsOrder: the agreed order of the members both sides held.",
82
+ "type": "object",
83
+ "properties": {
84
+ "options": { "type": "object", "additionalProperties": { "$ref": "#/$defs/baseOption" } },
85
+ "optionsOrder": { "type": "array", "items": { "type": "string" } }
86
+ }
87
+ },
88
+ "baseOption": {
89
+ "type": "object",
90
+ "properties": {
91
+ "label": { "type": "string" },
92
+ "hidden": { "type": "boolean" },
93
+ "description": { "type": "string" }
94
+ },
95
+ "additionalProperties": false
96
+ }
97
+ }
98
+ }
package/docs/apply.md ADDED
@@ -0,0 +1,64 @@
1
+ # Apply
2
+
3
+ `kalup apply <plan-file> [--yes | --approve <writesHash>]` applies a plan saved by `kalup plan --out` to the target it names. `kalup apply [--target <name>] [--take config <selector>] [--yes]` plans an unprotected target now and applies that plan through the same checks. Apply writes property groups and properties, on custom objects too, and never a custom object schema.
4
+
5
+ This page is the reference. For the walk-through with examples, see [kalup apply](https://kalup.dev/docs/commands/apply) on the website.
6
+
7
+ ## Approval
8
+
9
+ A plan with any effect (a write, adoption, release, delete or state-only update) needs one approval:
10
+
11
+ - **A person at a terminal**: stdin and stderr are terminals, no `--json`, `CI` unset. Apply prints the target, portal, account type, protection, each step in words from its data (never the plan's titles, with a differing portal name) and the counts, then asks for the target name and, for deletes and takeover option removals, the number of destructive steps. A wrong answer or the end of input is `E_CANCELLED`.
12
+ - **`--yes`**: an unprotected target, no step Kalup derives as risky or destructive, at most the target's `yesLimit` (default 25; `0` turns `--yes` off) writes, adoptions and releases. It trusts the file once its digest matches, never past the risk or delete rules.
13
+ - **`--approve <writesHash>`**: a reviewed CI job. The digest must equal the file's recomputed `writesHash` (`E_APPROVE_MISMATCH`), and the target must name its own `credentials.write`, read from the process environment, with no `.env` defining it (`E_APPROVE_CREDENTIAL`). It covers protected targets and risky steps, and shows only that the writes equal a reviewed digest, not that a review happened. A write key exported in a workstation shell satisfies it too, so keep that key only in CI. Agents never pass it.
14
+
15
+ Every delete, and every option removal takeover asks for, needs the person at a terminal. Otherwise apply stops with `E_APPROVAL_REQUIRED`, exit 4, printing the command. A protected target accepts only a saved plan (`E_PROTECTED_SAVED_PLAN`).
16
+
17
+ The terminal question stops an over-eager agent, not a hostile one: anything with a shell on the machine can read its keys.
18
+
19
+ ## What apply checks
20
+
21
+ Before any write, in order:
22
+
23
+ 1. The file is `plan/1` from this release line (`E_PLAN_VERSION`, `E_PLAN_INVALID`), and `writesHash` and `planId` recomputed from it match (`E_PLAN_DIGEST`). A plan with nothing to apply exits 0 with no request.
24
+ 2. `kalup.config.ts`: the target is declared and pins the plan's portal (`E_PLAN_DESTINATION`). Later config edits change nothing it writes.
25
+ 3. The write key (`credentials.write`, else the read key) passes the portal guard. Every request, reads included, uses it.
26
+ 4. The policy equals the plan's: `protected`, `drift`, `adopt`, `allowDestroy`, `yesLimit` and the objects in takeover (`E_POLICY_CHANGED`); each step's API version is current and unexpired, and the normalizer versions match (`E_PLAN_VERSION`).
27
+ 5. Every step but a release is on an object `kalup.config.ts` declares, the name bindings match the target's name overrides, and no two steps but releases resolve to one portal resource (`E_BINDING_CHANGED`).
28
+ 6. Each delete has a `destroy` tombstone in `kalup/removed.ts` or takeover's leave (takeover mode, in the pull scope, not excluded), is gone from config, and no address in config names its portal resource, read as data (`E_PLAN_DELETE`). The object files also tell takeover's option removals from config's own.
29
+ 7. Approval, then the portal lock (`E_LOCKED`).
30
+ 8. State: a plan already applied with outcome `done` exits 0 ("Already applied"); otherwise lineage and serial equal the plan's (`E_STATE_CHANGED`).
31
+ 9. A fresh read of each object the plan changes, and of the schemas list for a custom object (`E_INCOMPLETE` on a 403, `E_BINDING_CHANGED` for another type ID). Every `expect` must hold (`E_PLAN_STALE`), a delete's covering each field its base holds. Kalup derives each step's risk, labels and blocked status again (`E_PLAN_RISK` when the plan states less); a takeover removal needs `allowDestroy`, and never takes a HubSpot-defined property.
32
+ 10. Three calls per write plus the reads use at most half of HubSpot's daily remainder (`E_BUDGET`).
33
+
34
+ ## Running the steps
35
+
36
+ Apply records `lastApply.outcome: running`, then runs the steps one at a time: groups, properties, releases, then deletes, properties before groups. Each write reads the resource again and compares it with `expect`, builds the request from that read, sends it once, and reads it back for up to 60 seconds, saying so on stderr after a few seconds.
37
+
38
+ - A property update sends the approved fields with the live `type` and `fieldType`; options go as the full live list with the approved changes, new ones last.
39
+ - A 429, 423 or 477 is waited out three times, reading again before each resend. A daily 429 stops the run.
40
+ - A timeout, network failure or 5xx is `uncertain` and never resent: HubSpot documents no idempotency keys. Only reading back the approved values settles it (`E_UNCERTAIN_WRITE`).
41
+ - A value HubSpot stores differently is `W_UNVERIFIED`: state records both, and the next plan notes it instead of writing again.
42
+ - A delete runs only once every earlier step verified. HubSpot keeps an archived property restorable in its UI for 90 days.
43
+
44
+ State is saved after each step that changes an entry, then the outcome: `done`, `partial` or `uncertain`. Each request is journaled in `.kalup/journal/portal-<id>/`, never with a key or body. SIGINT or SIGTERM stops before the next request and saves state. A second one exits at once, unless it comes within a second (npx passes one Ctrl-C on twice).
45
+
46
+ ## Outcomes and exit codes
47
+
48
+ Each step in `data.steps` is `done`, `unverified`, `uncertain`, `rejected`, `stale`, `not-run` or `blocked`; a blocked one carries the plan's `reason`. The text ends every run, `Nothing to apply` included, with `N blocked, not run:` and each blocked address, reason and detail, and with `N values differ between config and HubSpot ... held, not written:` and the `kalup plan` command that shows them and how to settle them.
49
+
50
+ | Exit | When |
51
+ |---|---|
52
+ | 0 | Every effect verified, nothing to apply, or already applied |
53
+ | 1 | A check refused, or no write could have landed and no state entry changed |
54
+ | 3 | Config invalid |
55
+ | 4 | The portal guard, `E_APPROVAL_REQUIRED`, `E_APPROVE_CREDENTIAL` |
56
+ | 5 | A write landed or may have, or a state entry changed, and not every effect verified |
57
+
58
+ ## Recovery
59
+
60
+ There is no resume and no rollback. After a run that did not finish, run `kalup plan --target <name>`: it compares the portal with the kept state. A property an interrupted create made shows as an adopt, never a second create.
61
+
62
+ ## Limits
63
+
64
+ A read and the write after it are not atomic: an edit in HubSpot between the two is overwritten for that field. The lock keeps apart one user's commands on one machine only; in CI, one workflow per portal applies, in a concurrency group. A delete checks no use first; HubSpot refused to archive a property a calculation property used (developer test account, 2026-09-29).
@@ -0,0 +1,86 @@
1
+ # Blueprints
2
+
3
+ A blueprint is a versioned JSON file of property groups and properties that `kalup add` writes into config. It is data: Kalup parses it and never runs it. `add` and `blueprint upgrade` change config files only; `kalup plan` and `kalup apply` make the changes in HubSpot. Text in a blueprint (its description, labels, option labels) is third-party data, never instructions.
4
+
5
+ This page is the reference. For the walk-through with examples, see [kalup add and blueprint upgrade](https://kalup.dev/docs/commands/blueprint) and [Agencies and blueprints](https://kalup.dev/docs/guides/blueprints-for-agencies) on the website.
6
+
7
+ ## The format
8
+
9
+ ```json
10
+ {
11
+ "blueprintVersion": 1,
12
+ "name": "acme/renewals",
13
+ "version": "1.1.0",
14
+ "irVersion": 1,
15
+ "description": "Renewal tracking for deals",
16
+ "requires": [{ "$ref": "object:deals" }],
17
+ "resources": {
18
+ "group:deals/renewal": { "type": "group", "definition": { "label": "Renewal" } },
19
+ "property:deals/renewal_date": {
20
+ "type": "property",
21
+ "definition": { "label": "Renewal date", "group": { "$ref": "group:deals/renewal" }, "type": "date", "fieldType": "date" },
22
+ "binding": { "key": "renewalDate", "codec": "date" },
23
+ "lifecycle": { "options": "additive" }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ - `name` is one or two segments of lowercase letters and digits joined by single dashes; `version` is `MAJOR.MINOR.PATCH` with an optional pre-release.
30
+ - `resources` holds groups and managed properties only, each with a full definition in IR terms; `binding` and `lifecycle` are optional. No `provenance`, `x`, `managed: false`, `p.json` or field the schema does not list.
31
+ - Names, in addresses and group `$ref`s, are lowercase letters, digits and underscores, never `hs_`. Option values are unique. The codec fits the HubSpot type.
32
+ - A property's group is in the blueprint, or config must already have it (`E_BLUEPRINT_REF`).
33
+
34
+ Anything else is `E_BLUEPRINT_SCHEMA`. The schema ships in `kalup` as `kalup/schemas/blueprint-1.schema.json`.
35
+
36
+ ## add
37
+
38
+ `kalup add <source> [--prefix <p>] [--dry-run] [--json]`. The source is a path relative to the current directory, or an `https://` URL: no key is sent, redirects are followed only to https, and the fetch stops after 30 seconds or 1 MB (`E_BLUEPRINT_SOURCE`). A URL with credentials or a query string is refused: the lock records it.
39
+
40
+ 1. The bytes are hashed (`sha256:`), parsed as JSON and checked. The prefix is applied (below).
41
+ 2. A new resource is added. One config already has with the same definition and binding is recorded under the blueprint, not rewritten. Any other difference is `E_BLUEPRINT_COLLISION`, listing the differing units; so is a `.managed(false)` entry, or an address another blueprint provides.
42
+ 3. Resources go into the export that holds their object, or a new `kalup/objects/<object>.ts` for a standard object. A custom object must already be in config (`E_BLUEPRINT_REQUIRES`). An object missing from `objects` in `kalup.config.ts` is added as `{}`.
43
+ 4. The lock entry goes to `kalup/blueprints.lock.json`, and the bytes, unchanged, to `kalup/.blueprints/<name with / as -->@<version>.json`. `.gitattributes` gets `kalup/.blueprints/** -text`, so git never changes their line endings.
44
+
45
+ The project as add would leave it is validated first: any issue, such as a binding key another property uses, is exit 3. Files are copied to `.kalup/history/`, then written as one change (`E_PROJECT_WRITE` restores them on a failure).
46
+
47
+ The output lists each resource (added, already in config, colliding), the objects added, each file, and the next step, `kalup plan`. The description is printed only at a terminal, labelled as the blueprint's own text. `--dry-run` writes nothing.
48
+
49
+ ## Prefix
50
+
51
+ `--prefix acme_`, else `prefix` in `kalup.config.ts`, else none. It goes before every group and property name, in addresses and every `$ref`: `property:deals/renewal_date` becomes `property:deals/acme_renewal_date`. Labels, option values, binding keys and object keys stay, so the app code is the same in every project. HubSpot names are permanent, so a prefix is too. Upgrades reuse the recorded prefix.
52
+
53
+ ## blueprint upgrade
54
+
55
+ `kalup blueprint upgrade <name> <source> [--take remote <address[#unit]>]... [--dry-run] [--exit-code] [--json]` merges three ways: the stored original is the base, config is local, the new version is remote.
56
+
57
+ - Units: each definition field, each option (by value, then `label`, `hidden`, `description`), the options order, each lifecycle field, and the binding's `key`, `codec`, `aliases`, `required` and `readonly`.
58
+ - Config unchanged since the base takes upstream's value. Upstream unchanged keeps config's. Both changed alike is fine. Both changed differently is a conflict: config keeps its value, the output shows both and the command that takes upstream's, and the lock records it under `held`. It stays held at later versions until taken or settled in config.
59
+ - A resource new upstream is added; one config already has differently is a conflict per differing unit.
60
+ - An option added upstream is added. One removed upstream that config still has stays, with its `as` alias and a note. An option or resource the client removed stays removed.
61
+ - A resource removed upstream is detached: config keeps it, with no provenance.
62
+
63
+ The same version and hash prints "Already at" and the conflicts config still holds, dropping settled ones from the lock; add `--take remote <address#unit>` to take upstream's side of one. A lower version warns `W_BLUEPRINT_DOWNGRADE`. The new original replaces the old one.
64
+
65
+ A field a version owns for the first time, where the portal holds another value, is held by `plan` as `diverged` (plan.md); `plan --take config` writes it.
66
+
67
+ ## Provenance and the lock
68
+
69
+ These two commands write `kalup/blueprints.lock.json`, never a person (`E_BLUEPRINT_LOCK`, exit 3). Per blueprint it records `version`, `source`, `hash`, `prefix`, `original`, `resources` (local address to blueprint address) and `held`. The loader adds `provenance` to each config resource the lock lists; `kalup ir` shows it. Commit the lock, `kalup/.blueprints/` and `.gitattributes` with the config.
70
+
71
+ ## Integrity
72
+
73
+ The lock's `sources` keeps the hash of every source and version ever recorded. The same source and version with other bytes is `E_BLUEPRINT_INTEGRITY`: new content needs a new version number. A stored original that is missing or edited is `E_BLUEPRINT_ORIGINAL`; restore it from git.
74
+
75
+ ## What upgrade never does
76
+
77
+ Write to a portal, delete or tombstone a resource, overwrite a value the client changed without `--take remote`, or touch overrides, state or another blueprint's resources.
78
+
79
+ ## Exit codes
80
+
81
+ | Exit | When |
82
+ |---|---|
83
+ | 0 | Written, or nothing to do |
84
+ | 1 | `E_USAGE`, `E_TAKE_UNMATCHED`, `E_PROJECT_WRITE` and every `E_BLUEPRINT_` code but `E_BLUEPRINT_LOCK` |
85
+ | 2 | `upgrade --exit-code` when the lock holds conflicts |
86
+ | 3 | Config invalid, before or after the change; `E_BLUEPRINT_LOCK` |
@@ -0,0 +1,73 @@
1
+ # Compare
2
+
3
+ `kalup compare <a> <b>` reports what would change in `b` to match `a`. It never writes, to the portal or to disk.
4
+
5
+ This page is the reference. For the walk-through with examples, see [kalup compare](https://kalup.dev/docs/commands/compare) on the website.
6
+
7
+ ## Sides
8
+
9
+ Each side is, in this order:
10
+
11
+ 1. `config`: the config files, as `kalup ir` derives them. Compared with a target, that target's `definition` overrides apply.
12
+ 2. A target declared in `kalup.config.ts`, read now. Each target has its own key, client and portal guard, and every guard runs before the first read: a key for another portal is exit 4.
13
+ 3. Otherwise a snapshot file, relative to the current directory. No such file is `E_SNAPSHOT`, exit 1.
14
+
15
+ The project must load and validate only when a side is `config` or a target (exit 3 otherwise). Two snapshot files compare anywhere and send no request. A target is read as `pull` reads it: the same scope, three properties lists per object, `skip` and `name` overrides applied.
16
+
17
+ ## Direction
18
+
19
+ `a` is desired, `b` observed. In `changes[]`, `before` is `b`'s value and `after` is `a`'s.
20
+
21
+ - `compare config production`: what a plan for production would change, without planning.
22
+ - `compare <snapshot> production`: drift since the snapshot. `before` is the portal now, `after` the snapshot.
23
+ - `compare sandbox production`: two portals.
24
+
25
+ ## What is compared
26
+
27
+ Every address present on either side, including properties Kalup does not write, and every config address. Each gets a status:
28
+
29
+ - Equal: every unit converged. Counted, not listed.
30
+ - `differs`: `changes[]` lists options to add or remove; `held[]` lists units that differ, `diverged` since there is no base; `notes[]` lists options only `b` holds, kept because options are additive. When `b` is a portal side, the note names the pull command that brings the option into config. `pull` does not write a resource that names a portal name a `name` override shadows (`shadowed:<name>`), so on such a resource the note says to correct or remove that override instead. Nor does it write a property outside its object's pull scope, so there the note says to add the name to `objects.<object>.include`. A snapshot does not record whether HubSpot defines a property, so on a reference `include` does not name, a snapshot's note says it may be outside the scope and gives the same advice.
31
+ - `only-a` or `only-b`: on one side only.
32
+ - `unmanaged`: only a portal side holds it and the other side is config. Listed and counted, never a difference: absence never deletes.
33
+ - `unknown`: a side could not read its object, never read it, left out a property config names because its group's name holds whitespace (`W_UNADDRESSABLE_NAME`), or a target side has a `lookup` override, since this version manages no lookup resources. `reason` says which side and why.
34
+ - `excluded`: a `skip` override, or outside a side's read scope. Listed, not a difference.
35
+
36
+ Config owns the fields it states, less `ignoreChanges`; a reference owns nothing, so only its presence compares. A portal side owns every field it captured; a field HubSpot left out takes its default, or `null` when it has none. With config as `b`, only the fields config states are compared. Options are compared when the config side states them, and always between two portal sides. Config managing what the portal holds as HubSpot-defined or calculated differs in the unit `managed`.
37
+
38
+ ## Complete
39
+
40
+ A comparison is complete when no address is unknown and every object either side names was read on both sides (read, absent from the portal, or skipped). Otherwise the exit is 1 with `E_INCOMPLETE`, naming what was not compared and what to do: add scopes or an object key under `objects`, or rename in HubSpot a portal group whose name holds whitespace, with or without `--exit-code`. `data` is still printed, with `ok: false`. An incomplete comparison is never a clean result.
41
+
42
+ ## Flags
43
+
44
+ - `--exit-code`: exit 2 when anything `differs` or is `only-a` or `only-b`. An option only `b` holds counts. Unmanaged and excluded addresses do not.
45
+ - `--json`: one `envelope/1`. `data` holds `a` and `b` (kind, name, portal ID, file, `observedAt`), `complete`, `counts` and `differences[]` with `address`, `status`, `changes`, `held`, `notes` and `reason`.
46
+
47
+ Structured values keep portal strings exact. The text output is sanitized.
48
+
49
+ ## Output
50
+
51
+ The example project after config added an option and a property, and the portal renamed a label and added an option:
52
+
53
+ ```
54
+ a: config
55
+ b: target sandbox, portal 1111111
56
+ 12 equal, 1 differs, 1 only in a, 0 only in b, 1 unmanaged, 0 unknown, 0 skipped
57
+ unmanaged: group:companies/companyinformation
58
+ differs: property:companies/billing_status
59
+ add options[trial]: null -> {"value":"trial","label":"Trial"}
60
+ label differs: a "Billing status", b "Billing state"
61
+ kept options[paused]: {"value":"paused","label":"Paused","hidden":false,"description":""}
62
+ only in a: property:companies/churn_reason
63
+ ```
64
+
65
+ ## Exit codes
66
+
67
+ | Exit | When |
68
+ |---|---|
69
+ | 0 | Complete, differences included without `--exit-code` |
70
+ | 1 | `E_INCOMPLETE`, `E_USAGE`, `E_NO_CONFIG`, `E_SNAPSHOT` (a missing file, not JSON), `E_MISSING_KEY`, `E_OVERRIDE_AMBIGUOUS`, a failed request |
71
+ | 2 | `--exit-code` and a difference, with `ok: true` |
72
+ | 3 | Config invalid, `E_UNKNOWN_OBJECT`, or a file that is not a valid snapshot |
73
+ | 4 | `E_TARGET_PORTAL_MISMATCH` |
package/docs/config.md ADDED
@@ -0,0 +1,88 @@
1
+ # Config files
2
+
3
+ Kalup reads `kalup.config.ts` and every `.ts` file under `kalup/` except `kalup/index.ts` as data. It parses a small grammar and never runs them. The app imports the same files and runs them for types and codecs.
4
+
5
+ This page is the reference. For the walk-through with examples, see [Config files](https://kalup.dev/docs/config/config-files), [kalup.config.ts](https://kalup.dev/docs/config/kalup-config) and [Property builders](https://kalup.dev/docs/config/property-builders) on the website.
6
+
7
+ ## Files
8
+
9
+ - `kalup.config.ts`: one `export default defineConfig({...})` and nothing after it. Fields: `name` (default: the directory name), `prefix`, `defaultTarget` (targets.md), `mode` (below), `objects` (the pull scope, pull.md) and `targets` (targets.md). A setting at a level that does not take it is `E_SETTING_LEVEL`, whose fix lists the levels that do; a value it does not take is `E_SETTING_VALUE`, with the nearest allowed one.
10
+ - `kalup/objects/<object>.ts`: one or more `export const <Name> = defineObject('<object>', {...})` or `defineCustomObject('<name>', {...})`. The writer adds an `export type <Name>Data = ...` line after each. A file with no such export is `E_MISSING_EXPORT`.
11
+ - `kalup/index.ts`: the barrel, written by `pull` and `fmt`. It imports each object file as `./objects/<object>.js`, which resolves under TypeScript `NodeNext`, `Node16` and `Bundler` resolution, bundlers such as Vite and Next.js, and plain Node running `tsc` output. Under `NodeNext`, import it as `./kalup/index.js`.
12
+ - `kalup/removed.ts`: tombstones, below.
13
+ - `kalup/pipelines/*`, and a `defineConfig` or `defineRemoved` file elsewhere under `kalup/`, are `E_UNSUPPORTED_FILE`.
14
+
15
+ ## The grammar
16
+
17
+ Anything else is `E_NOT_DATA`.
18
+
19
+ - `import` lines. Imports from `@kalup/core` and `kalup` are rewritten; others are kept, for `p.json` validators.
20
+ - Object literals of `key: value` entries, arrays, strings in single or double quotes on one line, numbers, `true` and `false`. No template strings, identifiers as values, spreads, computed keys, shorthand, or calls other than the builders.
21
+ - A `//` comment on its own line above an export, a group entry or a property entry, and a comment block above the imports (the file header). Every other comment is an error, including any in `kalup.config.ts` but the header.
22
+ - `p.<kind>('<internal name>')` or `p.<kind>('<internal name>', {...})`, then any of `.strict()` (`p.enum` and `p.multiEnum` only), `.required()`, `.readonly()` and `.managed(false)`, each once. Any other chain call is `E_BAD_CHAIN`. A kind not listed below is `E_UNKNOWN_BUILDER`.
23
+ - `p.json('<name>', <validator>, {...})`. The validator is opaque text and may not hold a `//` comment.
24
+ - A custom object needs `labels: { singular, plural }` and `primaryDisplayProperty`, and may set `requiredProperties`, `searchableProperties` and `secondaryDisplayProperties`.
25
+
26
+ `E_DUPLICATE_KEY`: a key twice in one literal, an export name twice in one file, or one internal name under two keys. `E_DUPLICATE_ADDRESS`: one address from two files or two exports.
27
+
28
+ ## Builders
29
+
30
+ Each builder sets the HubSpot `type`, which config never states, and allows these `fieldType` values. Any other is `E_TYPE_FIELDTYPE`.
31
+
32
+ - `p.string`, `p.stringArray`, `p.json`: `string`; text, textarea, file, phonenumber.
33
+ - `p.number`: `number`; number.
34
+ - `p.boolean`: `bool`; booleancheckbox.
35
+ - `p.date` (`YYYY-MM-DD`) and `p.datetime` (ISO 8601): `date` and `datetime`; date.
36
+ - `p.enum`: `enumeration`; select, radio, booleancheckbox.
37
+ - `p.multiEnum`: `enumeration`; checkbox.
38
+
39
+ In the app every value can be `null`, and a blank one reads as `null`. `p.enum` gives one alias, `p.multiEnum` an alias array (`;`-separated on the wire); a stored value the options do not list reads as `Unlisted`, a branded string `set` writes back unchanged. `.strict()` makes both throw on it instead and drops `Unlisted` from the type; it needs options (`E_STRICT_WITHOUT_OPTIONS`). A bare `p.enum` reference is `Unlisted | null`. `p.stringArray` a `string[]` (split on `,` or `;`, written `,`-joined), `p.json` the validator's output. `.required()` drops `null` and makes `get` throw on a missing value. `.readonly()` removes `set` from the type. `pull` never writes `.required()`, `p.stringArray` or `p.json`, and keeps them.
40
+
41
+ ## Managed, reference, options-only
42
+
43
+ A definition with `label`, `group` and `fieldType` is managed: the fields present are owned, and an omitted `description`, `options`, `hasUniqueValue` or `formField` belongs to the portal. `group` must name a group declared under `groups` for the same object, in any export or file (`E_UNKNOWN_GROUP`). A managed internal name starting with `hs_` is `E_HS_PREFIX`.
44
+
45
+ No definition makes a reference: never created, changed or removed. `p.enum` or `p.multiEnum` with `options` and nothing else is a reference with typed options, which pull refreshes.
46
+
47
+ A definition missing one of the three fields, options-only on another builder, or `.managed(false)` on a reference is `E_REFERENCE_DEFINITION`.
48
+
49
+ `.managed(false)` keeps a full definition for typing while nothing owns it.
50
+
51
+ ## Options and aliases
52
+
53
+ `options: [{ value, label, as?, hidden?, description? }]`. Array order is display order. `as` is the app-side name: the TypeScript type is the union of `as ?? value`, `get` returns the alias and `set` takes it. `as` never goes to HubSpot. Values and aliases must each be unique (`E_DUPLICATE_OPTION`, `E_DUPLICATE_ALIAS`).
54
+
55
+ ## Keys
56
+
57
+ The object key is the app's name for the property. Two exports of one object using the same key is `E_KEY_COLLISION`. Rename keys freely; HubSpot only knows the internal name.
58
+
59
+ ## Lifecycle
60
+
61
+ `lifecycle: { options: 'additive' | 'exact', removedOptions: [...], ignoreChanges: [...], preventDestroy: true }` inside a full definition. `options` defaults to `additive`, and to `exact` under takeover, unless the property or the target's override states it. `removedOptions` may not name a value still in `options`, and `ignoreChanges` may name only definition fields (`E_LIFECYCLE`). `plan` and `compare` apply the others (plan.md); `preventDestroy` blocks a `rm` destroy.
62
+
63
+ ## Per-target definitions
64
+
65
+ A target's override `definition` (targets.md) replaces each field it states there, whole, and owns it, empty values included: a property's `label`, `description`, `group`, `fieldType`, `formField`, `options` (no `as`) and lifecycle but `preventDestroy`; a group's `label`. Else `E_OVERRIDE_DEFINITION`. `pull` writes these fields into the override.
66
+
67
+ ## Mode: addon and takeover
68
+
69
+ `mode: 'addon' | 'takeover'` at the top level, under `objects.<object>`, under `targets.<target>`, or under `targets.<target>.objects.<object>`. The most specific wins, in that order from the last, and the default is `addon`: Kalup manages only what config names. A target `mode` that differs from an object's `mode` the target says nothing more about is `W_MODE_SHADOWED`.
70
+
71
+ Under `takeover`, `plan` archives every custom property and group in the object's pull scope that config lacks and `kalup/removed.ts` does not name (a group only once every property in it goes, and after them), and removes enum options only the portal holds. Never a HubSpot-defined or calculated property, a kind Kalup does not write, anything an object file lists, a name `exclude` covers, or a property a custom object schema names. Every takeover removal is destructive: it needs `allowDestroy: true` on the target and a person at a terminal, and `--yes` and `--approve` never cover it. Without `allowDestroy` it is blocked, reason `policy`; after an incomplete read, reason `scope`.
72
+
73
+ ## Removed resources
74
+
75
+ `kalup/removed.ts` holds `export default defineRemoved({...})`, keyed by address:
76
+
77
+ ```ts
78
+ export default defineRemoved({
79
+ 'property:companies/legacy_score': { action: 'destroy', reason: 'Replaced by lead_score' },
80
+ 'group:companies/old_billing': { action: 'release' },
81
+ })
82
+ ```
83
+
84
+ `destroy` deletes the resource, only on a target with `allowDestroy: true` (default false). `release` stops managing it, leaving it there. Keys are property or group addresses (`E_TOMBSTONE_ADDRESS`) that config no longer defines (`E_TOMBSTONE_CONFLICT`).
85
+
86
+ ## Canonical form
87
+
88
+ `kalup fmt` validates, then rewrites `kalup.config.ts`, `kalup/removed.ts`, every object file and the barrel: groups and properties sorted by internal name, tombstones by address, fields in a fixed order, options in display order, quotes as biome writes them, 120 columns. It keeps every value you wrote, `description: ''`, `options: []`, `false` and an empty `lifecycle` included: a present field is owned. `fmt --check` lists the files it would change; `--exit-code` exits 2 then. Old files go to `.kalup/history/<timestamp>/` first; the last 20 runs are kept.
@@ -0,0 +1,43 @@
1
+ # Data dictionary
2
+
3
+ `kalup docs [<source>]` prints a Markdown data dictionary. The source is `config`, the default, or a snapshot file relative to the current directory. It sends no request.
4
+
5
+ This page is the reference. For the walk-through with examples, see [kalup docs](https://kalup.dev/docs/commands/docs) on the website.
6
+
7
+ - `config`: the project must validate (exit 3). The page describes the config files: a field a definition leaves out belongs to the portal and is not listed.
8
+ - A snapshot file needs no project. The page describes the read, with HubSpot's defaults filled in, such as `Hidden: no`.
9
+
10
+ ## Layout
11
+
12
+ 1. `# <project> data dictionary` and a line naming the source: the config files, or the target, portal ID and `observedAt` of the snapshot.
13
+ 2. `## Coverage`. For a snapshot: whether the read was complete, the objects not read with the missing scope, objects absent from the portal, what `skip` overrides left out, unsupported properties (Kalup does not write them), schemas without a label, `name` overrides, config properties in a group no address can hold, counts out of scope and shadowed, custom objects config does not name, and the fields Kalup does not capture. Reference properties record only their options.
14
+ 3. One `## <object>` section per object key, sorted: a custom object's labels, display property and property lists; a groups table (internal name, label); a properties table (internal name, label, type, field type, group, managed or reference, description); and one options table per enumeration (value, label, hidden, description), in display order. Config adds the key, codec, required and alias columns. Unsupported properties appear only under Coverage.
15
+ 4. Config with `definition` overrides: `## Per-target overrides`, one row per address, field and target with its value, sorted.
16
+
17
+ ## Escaping
18
+
19
+ Every string from a file or a portal is shown as text: newlines become spaces, control and bidirectional formatting characters are removed, every ASCII punctuation character is backslash-escaped, and an invisible word joiner (U+2060) breaks a bare URL, `www.` name or email address, which GFM renderers such as remark-gfm link even when escaped. So no label can form a link, HTML, emphasis, code, a heading or a table cell. Descriptions are cut at 500 characters with an ellipsis.
20
+
21
+ ## Deterministic
22
+
23
+ The same source gives the same bytes: objects, groups and properties sorted, options in display order, and no timestamp but a snapshot's own `observedAt`. Commit the dictionary and check it in CI:
24
+
25
+ ```sh
26
+ npx --no-install kalup docs --out DATA-DICTIONARY.md
27
+ git diff --exit-code DATA-DICTIONARY.md
28
+ ```
29
+
30
+ ## Flags
31
+
32
+ - `--out <file>`: write the Markdown there, relative to the current directory, replacing an older file, and print `Wrote <file>`.
33
+ - `--json`: one `envelope/1` with `data: { markdown }`, or `data: { file }` with `--out`.
34
+
35
+ An incomplete snapshot gives `W_INCOMPLETE`: what was not read is unknown, and the Coverage section lists it.
36
+
37
+ ## Exit codes
38
+
39
+ | Exit | When |
40
+ |---|---|
41
+ | 0 | Written, warnings included |
42
+ | 1 | `E_USAGE`, `E_NO_CONFIG`, `E_SNAPSHOT` (a missing file, not JSON) |
43
+ | 3 | Config invalid, or a file that is not a valid snapshot |
@@ -0,0 +1,17 @@
1
+ # E_ACCEPT_UNMATCHED
2
+
3
+ A `pull --accept` selector matches nothing pull keeps. Exit 1. Nothing was written.
4
+
5
+ ## When
6
+
7
+ Where state owns a resource, pull keeps the file's value for a unit config changed, for a conflict, for an option config added, and for an option HubSpot removed that config still holds. `--accept <address[#unit]>` takes the portal's side of those units. A selector that matches none of them (a unit that agrees, one pull takes from the portal anyway, a typo) is refused. The message lists what pull keeps there. The run's warnings follow it, so an unread list (`E_SCOPE`, `E_INCOMPLETE`) that explains an empty match is shown too.
8
+
9
+ ## Fix
10
+
11
+ Run `kalup pull --target <name> --check`, pick a unit it lists as a config change kept, a conflict or removed in HubSpot, and pass that. A selector without `#unit` takes every such unit on the address, and `*` in the address works as in `--only`.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ E_ACCEPT_UNMATCHED: --accept property:companies/soil_ph#description matches no config change, conflict or option removed in HubSpot; pull keeps: property:companies/soil_ph#label (conflict, config kept). Nothing was written. (fix: accept a unit that kalup pull --target sandbox --check lists as kept, a conflict or removed in HubSpot, or leave the selector out) (docs: errors/E_ACCEPT_UNMATCHED.md)
17
+ ```
@@ -0,0 +1,17 @@
1
+ # E_APPROVAL_REQUIRED
2
+
3
+ Nothing approved this plan. Exit 4, `humanRequired: true`. Nothing was written.
4
+
5
+ ## When
6
+
7
+ A plan with any effect needs one approval. A person at a terminal (stdin and stderr are terminals, no `--json`, `CI` unset) confirms it by typing the target name. `--yes` covers only an unprotected target, with no risky or destructive step, and at most as many writes, adoptions and releases as `yesLimit` on the target allows (25 by default; `yesLimit: 0` turns `--yes` off). Every delete, and every option removal takeover asks for, on every host, needs a person at a terminal who also types the number of destructive steps. Otherwise apply stops here, and the message says which condition failed. `kalup state rebuild --write` and `kalup target rebind` run only for a person at a terminal, and stop here otherwise.
8
+
9
+ ## Fix
10
+
11
+ Stop. Hand the command in the fix to the user, who runs it in a terminal and confirms it there. Agents never approve on the user's behalf.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ E_APPROVAL_REQUIRED: --yes does not cover this plan: s1 (risky) is not safe. (fix: ask the user to run kalup apply plan.json in a terminal, where they confirm it) (docs: errors/E_APPROVAL_REQUIRED.md)
17
+ ```
@@ -0,0 +1,17 @@
1
+ # E_APPROVE_CREDENTIAL
2
+
3
+ `--approve` was refused because the write key is not held apart. Exit 4, `humanRequired: true`. Nothing was sent.
4
+
5
+ ## When
6
+
7
+ `--approve <writesHash>` lets a reviewed CI job apply a plan. It rests on custody: only that CI environment holds the write key. So the target must name its own `credentials.write`, and the key is read from the process environment only. Kalup refuses when the target has no `credentials.write`, or one that names the read credential's variable, or when `.env` in the project directory defines that variable at all, whatever its value, because the key is then on this machine. The message never includes a value.
8
+
9
+ ## Fix
10
+
11
+ Stop. A person decides. Give the target `credentials.write` with a variable only the reviewed CI environment holds. Keep it out of `.env` and every workstation shell: exported there, it satisfies `--approve` too. Or a person applies the plan at a terminal. Agents: hand this to the user.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ E_APPROVE_CREDENTIAL: --approve needs a write key that only the reviewed CI environment holds, and .env in the project directory defines HUBSPOT_PROD_WRITE_KEY. (fix: Remove HUBSPOT_PROD_WRITE_KEY from .env, or have a person apply the plan at a terminal.) (docs: errors/E_APPROVE_CREDENTIAL.md)
17
+ ```
@@ -0,0 +1,17 @@
1
+ # E_APPROVE_MISMATCH
2
+
3
+ The digest given to `--approve` is not this plan's. Exit 1. Nothing was written.
4
+
5
+ ## When
6
+
7
+ `--approve <writesHash>` is for a reviewed CI job: the digest of the plan a reviewer saw. `kalup apply` recomputes the digest of the plan file it was given and refuses when the two differ. The plan changed after the review, or the digest belongs to another plan.
8
+
9
+ ## Fix
10
+
11
+ Review the plan file again, and approve the digest of the plan that will run. A person can also apply it with `kalup apply <plan-file>` in a terminal.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ E_APPROVE_MISMATCH: The digest given to --approve is not the writesHash of this plan: the plan changed after the review, or the digest belongs to another plan. Nothing was written. (fix: review this plan again; a person can apply it with kalup apply plan.json in a terminal) (docs: errors/E_APPROVE_MISMATCH.md)
17
+ ```
@@ -0,0 +1,17 @@
1
+ # E_AUTH
2
+
3
+ HubSpot rejected the read key with a 401. Exit 1.
4
+
5
+ ## When
6
+
7
+ A request from a command that reads a portal came back 401: the key is wrong, revoked or expired. `status` reports it for that target and checks the others.
8
+
9
+ ## Fix
10
+
11
+ Check the key in the variable the target reads (`credentials.read.env`, or `HUBSPOT_SERVICE_KEY` when there is none). If it was revoked, a person creates a new one in HubSpot. When the request needed a scope, the fix names it.
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
+ ```
@@ -0,0 +1,21 @@
1
+ # E_BAD_CHAIN
2
+
3
+ A builder call is followed by a chain call Kalup does not accept. Exit 3.
4
+
5
+ ## When
6
+
7
+ After `p.<kind>(...)` only `.strict()`, `.required()`, `.readonly()` and `.managed(false)` are allowed, each once, and `.strict()` only after `p.enum` or `p.multiEnum`. `.optional()`, `.managed(true)`, `.required` without parentheses, `.required()` twice or `.strict()` on `p.string` are errors.
8
+
9
+ ## Fix
10
+
11
+ Use one of the four calls, once each. A property is nullable unless it has `.required()`, so there is no `.optional()`. Drop `.strict()` from a builder other than `p.enum` and `p.multiEnum`.
12
+
13
+ ## Example
14
+
15
+ ```ts
16
+ plotCount: p.number('plot_count').optional(),
17
+ ```
18
+
19
+ ```
20
+ kalup/objects/companies.ts:5: E_BAD_CHAIN: .optional() is not a chain call (fix: use .strict(), .required(), .readonly() or .managed(false)) (docs: errors/E_BAD_CHAIN.md)
21
+ ```
@@ -0,0 +1,17 @@
1
+ # E_BINDING_CHANGED
2
+
3
+ A plan binds an address to a portal resource that kalup.config.ts or the portal does not give it now. Exit 1. Nothing was written.
4
+
5
+ ## When
6
+
7
+ A plan's `bindings` say which portal resource each address stands for: a target's name override, or a custom object's type ID. `kalup apply` never trusts the file. Before approval, every step but a release must be on an object `kalup.config.ts` declares under `objects`, each name binding must be the one the target's overrides give, and no two steps but releases may resolve to one portal resource. Under the lock, each custom object's type ID must be the one the schemas list gives now: a custom object made again has a new one.
8
+
9
+ ## Fix
10
+
11
+ Run `kalup plan --target <name> --out <file>` again, review it, and apply that file. Never edit a plan file by hand.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ E_BINDING_CHANGED: plan pl_7f3a1c07b2e4 does not name what kalup.config.ts names on target sandbox: the plan binds property:companies/soil_ph to portal name plot_notes, and the name override in kalup.config.ts gives none. Nothing was written. (fix: run kalup plan --target sandbox --out <file> again and review it; a plan file is never edited by hand) (docs: errors/E_BINDING_CHANGED.md)
17
+ ```
@@ -0,0 +1,17 @@
1
+ # E_BIOME_CONFIG
2
+
3
+ `init` found a `biome.json` that is not valid JSON. Exit 1. Nothing was written.
4
+
5
+ ## When
6
+
7
+ `init` adds `!kalup`, `!kalup.config.ts` and `!.kalup` to `files.includes` in `biome.json`, so it reads that file before it writes anything. Biome reads `biome.json` as plain JSON, so a comment in it is also this error. A `biome.jsonc` that does not parse is not an error: `init` leaves it alone and prints a note.
8
+
9
+ ## Fix
10
+
11
+ Fix the JSON in `biome.json` (a trailing comma or a comment is the usual cause), then run `npx --no-install kalup init --portal <id>` again.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ biome.json: E_BIOME_CONFIG: biome.json is not valid JSON: <parser message> (fix: fix the file, then run npx kalup init again) (docs: errors/E_BIOME_CONFIG.md)
17
+ ```