@tenonhq/dovetail-core 0.0.85 → 0.0.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,55 @@
1
1
  # @tenonhq/dovetail-core
2
2
 
3
- This module contains the core of Dovetail. It is required to use Dovetail at all.
4
- It can interact with other plugins after you configure them.
3
+ The core of Dovetail — ships the `dove` CLI binary and the sync engine that connects local source files to a ServiceNow instance via REST.
5
4
 
5
+ ## What's in this package
6
6
 
7
+ - **`dove` CLI** — command router built on yargs. Watch, push, pull, build, deploy, scope/update-set management, record CRUD, ClickUp integration, schema pull, dashboard launch, and the project migration command (`dove migrate`).
8
+ - **Sync engine** — file watcher, manifest-driven file ↔ ServiceNow record mapping, plugin pipeline runner, Scripted REST API client.
9
+ - **Skills** — `skills/` ships ~8 Claude Code skill markdowns. `dove init-claude` copies them into `<cwd>/.claude/commands/` for in-project Claude usage.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ nvm use 20
15
+ npm i -D @tenonhq/dovetail-core
16
+ npx dove init # scaffolds dove.config.js
17
+ npx dove configure # creates .env (do not commit)
18
+ ```
19
+
20
+ ## Commands
21
+
22
+ The full command surface is documented in the [root CLAUDE.md](../../CLAUDE.md#essential-commands). Source of truth lives in [`src/commander.ts`](src/commander.ts).
23
+
24
+ Quick reference:
25
+
26
+ ```bash
27
+ npx dove watch # multi-scope watch + dashboard
28
+ npx dove push # build + push current files
29
+ npx dove refresh # pull manifest + new files
30
+ npx dove build # local build only
31
+ npx dove deploy # deploy built artifacts
32
+ npx dove dashboard # update-set dashboard web UI
33
+ npx dove migrate # migrate a Sincronia project to Dovetail (dry-run by default; --apply to write)
34
+ ```
35
+
36
+ See [`UPDATE_SET_COMMANDS.md`](UPDATE_SET_COMMANDS.md) for the full update-set CLI surface.
37
+
38
+ ## Plugins
39
+
40
+ Dovetail's build pipeline is plugin-driven. Each plugin is an npm package implementing `run(context, content, options) => Promise<PluginResults>`. Shipped plugins live in sibling packages (`@tenonhq/dovetail-typescript-plugin`, `-babel-plugin`, `-webpack-plugin`, `-sass-plugin`, `-eslint-plugin`, `-prettier-plugin`, `-babel-preset-servicenow`, `-babel-plugin-remove-modules`).
41
+
42
+ ## Related packages
43
+
44
+ | Package | Purpose |
45
+ |---|---|
46
+ | `@tenonhq/dovetail-types` | Shared `Sinc.*` + `SN.*` type namespaces |
47
+ | `@tenonhq/dovetail-schema` | ServiceNow table schema fetcher |
48
+ | `@tenonhq/dovetail-dashboard` | Update-set dashboard web UI |
49
+ | `@tenonhq/dovetail-servicenow` | Platform helpers + `dove-sn` build-flow CLI |
50
+ | `@tenonhq/dovetail-sawmill` | Cross-instance update-set retrieve/preview/commit |
51
+ | `@tenonhq/dovetail-mcp` | Read-only MCP server (ClickUp, Gmail, Calendar, ServiceNow) |
52
+
53
+ ## Full spec
54
+
55
+ See [`docs/dovetail-platform-spec.md`](../../docs/dovetail-platform-spec.md) for the complete architecture, type system, REST API contract, build pipeline, and sync mechanism.
@@ -45,11 +45,28 @@ function decodeV2Values(blob) {
45
45
  /**
46
46
  * Encode a V2 values payload back to ServiceNow's storage format.
47
47
  *
48
- * Inverse of decodeV2Values. Provided for symmetry, diagnostic round-trip
49
- * tests, and any future write path. Current Craftsman sync treats sys_hub_*
50
- * as _readOnlyTables, so this is not invoked on the push path today.
48
+ * Inverse of decodeV2Values. The output's gzip envelope may differ byte-for-byte
49
+ * from a Java-produced original (Deflater and zlib emit different but compatible
50
+ * streams), but the decompressed JSON is identical content. Round-trip
51
+ * fidelity is verified against real Tenon-shipped fixtures in
52
+ * tests/v2Values.test.ts; SN-side acceptance is verified by the
53
+ * `@tenonhq/sincronia-servicenow` flowDesigner write path.
54
+ *
55
+ * Throws when `value` is undefined or otherwise not JSON-serializable
56
+ * (e.g. functions, BigInt). Anything that survives JSON.stringify will be
57
+ * accepted; structural validity is the caller's responsibility.
51
58
  */
52
- function encodeV2Values(value) {
59
+ function encodeV2Values(value, opts = {}) {
60
+ if (value === undefined) {
61
+ throw new Error("encodeV2Values: value must not be undefined");
62
+ }
53
63
  const json = JSON.stringify(value);
54
- return (0, zlib_1.gzipSync)(Buffer.from(json, "utf8")).toString("base64");
64
+ if (json === undefined) {
65
+ throw new Error("encodeV2Values: value is not JSON-serializable (e.g. function or BigInt at the root)");
66
+ }
67
+ const zlibOpts = {};
68
+ if (opts.level !== undefined && opts.level !== null) {
69
+ zlibOpts.level = opts.level;
70
+ }
71
+ return (0, zlib_1.gzipSync)(Buffer.from(json, "utf8"), zlibOpts).toString("base64");
55
72
  }
@@ -1,7 +1,43 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fs = __importStar(require("fs"));
37
+ const path = __importStar(require("path"));
3
38
  const zlib_1 = require("zlib");
4
39
  const values_1 = require("../flowDesigner/values");
40
+ const FIXTURES_DIR = path.join(__dirname, "fixtures", "v2-blobs");
5
41
  describe("decodeV2Values / encodeV2Values", () => {
6
42
  describe("round-trip", () => {
7
43
  it("decodes what encodeV2Values encoded (primitive array)", () => {
@@ -76,4 +112,81 @@ describe("decodeV2Values / encodeV2Values", () => {
76
112
  expect(decoded[0].id).toBe("1");
77
113
  });
78
114
  });
115
+ describe("encode — input validation", () => {
116
+ it("throws on undefined", () => {
117
+ expect(() => (0, values_1.encodeV2Values)(undefined)).toThrow(/must not be undefined/);
118
+ });
119
+ it("throws on non-JSON-serializable values (function at root)", () => {
120
+ expect(() => (0, values_1.encodeV2Values)(() => 1)).toThrow(/not JSON-serializable/);
121
+ });
122
+ it("throws on non-JSON-serializable values (BigInt at root)", () => {
123
+ expect(() => (0, values_1.encodeV2Values)(BigInt(1))).toThrow();
124
+ });
125
+ it("accepts null", () => {
126
+ expect((0, values_1.decodeV2Values)((0, values_1.encodeV2Values)(null))).toBeNull();
127
+ });
128
+ it("accepts plain objects (logic_instance_v2 shape)", () => {
129
+ const original = { outputsToAssign: [], inputs: [{ name: "x", value: "" }] };
130
+ expect((0, values_1.decodeV2Values)((0, values_1.encodeV2Values)(original))).toEqual(original);
131
+ });
132
+ });
133
+ describe("encode — options", () => {
134
+ it("round-trips correctly at every supported compression level", () => {
135
+ const v = Array.from({ length: 200 }, (_, i) => ({ id: String(i), value: "x".repeat(50) }));
136
+ for (let level = 1; level <= 9; level++) {
137
+ const blob = (0, values_1.encodeV2Values)(v, { level });
138
+ expect((0, values_1.decodeV2Values)(blob)).toEqual(v);
139
+ }
140
+ });
141
+ it("emits different byte streams at different levels (level is honored)", () => {
142
+ // Highly compressible payload — level=1 vs level=9 must differ in bytes,
143
+ // even if which is shorter is not strictly monotonic for tiny inputs.
144
+ const v = Array.from({ length: 500 }, (_, i) => ({ id: String(i), value: "abcde".repeat(20) }));
145
+ const lvl1 = (0, values_1.encodeV2Values)(v, { level: 1 });
146
+ const lvl9 = (0, values_1.encodeV2Values)(v, { level: 9 });
147
+ expect(lvl1).not.toEqual(lvl9);
148
+ });
149
+ it("treats undefined level as default (no throw, valid output)", () => {
150
+ const v = [{ id: "1", value: "a" }];
151
+ expect((0, values_1.decodeV2Values)((0, values_1.encodeV2Values)(v, {}))).toEqual(v);
152
+ expect((0, values_1.decodeV2Values)((0, values_1.encodeV2Values)(v, { level: undefined }))).toEqual(v);
153
+ });
154
+ });
155
+ describe("round-trip — real Tenon fixtures", () => {
156
+ // Real V2 blobs pulled from tenonworkstudio sit under
157
+ // tests/fixtures/v2-blobs/<table>.<sys_id>.txt. Each one must survive a
158
+ // decode→encode→decode pass with structural deep-equality preserved. This
159
+ // is the gate that lets us trust encodeV2Values for write-path use.
160
+ const fixtures = fs.existsSync(FIXTURES_DIR)
161
+ ? fs.readdirSync(FIXTURES_DIR).filter((f) => f.endsWith(".txt"))
162
+ : [];
163
+ it("fixtures directory has at least one sample per V2 table", () => {
164
+ expect(fixtures.length).toBeGreaterThan(0);
165
+ const tables = new Set(fixtures.map((f) => f.split(".")[0]));
166
+ // We expect samples for at least the action_instance and flow_logic_instance
167
+ // shapes. trigger_instance is nice-to-have but not gating.
168
+ expect(tables.has("sys_hub_action_instance_v2")).toBe(true);
169
+ expect(tables.has("sys_hub_flow_logic_instance_v2")).toBe(true);
170
+ });
171
+ fixtures.forEach((filename) => {
172
+ it(`round-trips: ${filename}`, () => {
173
+ const blob = fs.readFileSync(path.join(FIXTURES_DIR, filename), "utf8").trim();
174
+ const decoded = (0, values_1.decodeV2Values)(blob);
175
+ const reencoded = (0, values_1.encodeV2Values)(decoded);
176
+ const reDecoded = (0, values_1.decodeV2Values)(reencoded);
177
+ expect(reDecoded).toEqual(decoded);
178
+ });
179
+ });
180
+ it("re-encoded blobs decompress to byte-identical JSON via Node's zlib", () => {
181
+ // Belt-and-suspenders: our own zlib can read what we just wrote.
182
+ // (RFC 1952 conformance check; SN-side acceptance is a separate integration test.)
183
+ if (!fixtures.length)
184
+ return;
185
+ const blob = fs.readFileSync(path.join(FIXTURES_DIR, fixtures[0]), "utf8").trim();
186
+ const decoded = (0, values_1.decodeV2Values)(blob);
187
+ const reencoded = (0, values_1.encodeV2Values)(decoded);
188
+ const ourJson = (0, zlib_1.gunzipSync)(Buffer.from(reencoded, "base64")).toString("utf8");
189
+ expect(JSON.parse(ourJson)).toEqual(decoded);
190
+ });
191
+ });
79
192
  });
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.85",
3
+ "version": "0.0.86",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",
7
7
  "author": "Multiple",
8
8
  "private": false,
9
9
  "scripts": {
10
- "copy:skills": "mkdir -p skills && cp ../../skills/*.md ./skills/",
11
- "prepack": "npm run copy:skills && tsc && npm run validate-dist",
10
+ "prepack": "tsc && npm run validate-dist",
12
11
  "validate-dist": "node -e \"var fs=require('fs');if(!fs.existsSync('./dist/index.js')){console.error('FATAL: dist/index.js missing after build');process.exit(1)}\"",
13
12
  "test": "jest --ci --reporters=jest-junit --reporters=default --coverage --coverageReporters=cobertura --coverageReporters=html",
14
13
  "version:bump": "node ./../../Scripts/bump-version.js ./package.json",