datagrok-tools 6.5.7 → 6.5.8

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.
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.bytesPath = bytesPath;
7
+ exports.hashOf = hashOf;
8
+ exports.hashView = hashView;
9
+ exports.list = list;
10
+ exports.listShares = listShares;
11
+ exports.normalize = normalize;
12
+ exports.read = read;
13
+ exports.sharePath = sharePath;
14
+ exports.stripPrivate = stripPrivate;
15
+ exports.write = write;
16
+ exports.writeIdmap = writeIdmap;
17
+ exports.writeShares = writeShares;
18
+ var fs = _interopRequireWildcard(require("fs"));
19
+ var path = _interopRequireWildcard(require("path"));
20
+ var _crypto = require("crypto");
21
+ var _registry = require("./registry");
22
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
23
+ /// Docs: [Entity export / import](/docs/features/grok-tool/export-import/DESIGN.md)
24
+
25
+ // `entityTags` rows carry their own primary keys and come back in an unstable order:
26
+ // pushing them makes the hash flap AND re-points the source's tag rows at the copy.
27
+ const VOLATILE = ['createdOn', 'updatedOn', 'author', 'pictureId', 'encryptedParametersId', 'keyKid', 'isAvailable', 'entityTags'];
28
+ function normalize(type, json) {
29
+ const copy = JSON.parse(JSON.stringify(json));
30
+ // Tag rows are volatile, but the tag names are worth migrating: keep them as a
31
+ // bundle-only list the pusher replays through `POST /entities/tag`.
32
+ const tags = [...new Set((copy.entityTags ?? []).map(t => t?.tag).filter(Boolean))].sort();
33
+ if (tags.length) copy._tags = tags;
34
+ for (const k of VOLATILE) delete copy[k];
35
+ if (copy.package) copy.package = {
36
+ id: copy.package.id
37
+ };
38
+ // The stamp is written by the pusher, so it must not make an unchanged entity look different.
39
+ if (copy.metaParams) delete copy.metaParams.sync_id;
40
+ _registry.TYPES[type]?.strip?.(copy);
41
+ return sortKeys(copy);
42
+ }
43
+ function sortKeys(v) {
44
+ if (Array.isArray(v)) return v.map(sortKeys);
45
+ if (v === null || typeof v !== 'object') return v;
46
+ const out = {};
47
+ for (const k of Object.keys(v).sort()) out[k] = sortKeys(v[k]);
48
+ return out;
49
+ }
50
+
51
+ /** Bundle-only keys (`_credentials`, `_grants`, `_members`) never travel to the server. */
52
+ function stripPrivate(json) {
53
+ for (const k of Object.keys(json)) if (k.startsWith('_')) delete json[k];
54
+ return json;
55
+ }
56
+
57
+ /**
58
+ * What a comparison sees: the payload without the bundle-only keys and without the
59
+ * namespace, which the server computes from ownership (`Askalkin:` on the source,
60
+ * `Admin:` on the target) and would otherwise make every cross-instance push a rewrite.
61
+ * The bundle file keeps it — `findByNqName` resolves the twin by it.
62
+ */
63
+ function hashView(type, json) {
64
+ const view = stripPrivate(normalize(type, json));
65
+ delete view.namespace;
66
+ // Relations have a pass of their own, which only ever adds: their row ids belong to the
67
+ // target, and a target that links more than the bundle is not stale. The pusher compares
68
+ // them by coverage instead.
69
+ delete view.relations;
70
+ return view;
71
+ }
72
+ function hashOf(type, json) {
73
+ return (0, _crypto.createHash)('sha256').update(JSON.stringify(hashView(type, json))).digest('hex');
74
+ }
75
+ function bytesPath(dir, kind, id) {
76
+ return path.join(dir, kind, kind === 'tables' ? `${id}.d42` : id);
77
+ }
78
+
79
+ /**
80
+ * A file a datasync table reads is stored under its full share path, flattened into one
81
+ * file name so the bundle stays a flat directory per kind and the path survives a round trip.
82
+ */
83
+ function sharePath(dir, remotePath) {
84
+ return path.join(dir, 'shares', encodeURIComponent(remotePath));
85
+ }
86
+ function writeShares(dir, files) {
87
+ if (!files.size) return;
88
+ fs.mkdirSync(path.join(dir, 'shares'), {
89
+ recursive: true
90
+ });
91
+ for (const [remote, buf] of files) fs.writeFileSync(sharePath(dir, remote), buf);
92
+ }
93
+ function listShares(dir) {
94
+ const at = path.join(dir, 'shares');
95
+ return fs.existsSync(at) ? fs.readdirSync(at).map(decodeURIComponent) : [];
96
+ }
97
+ const EMPTY = {
98
+ formatVersion: 1,
99
+ source: {
100
+ url: '',
101
+ version: '',
102
+ userNamespace: ''
103
+ },
104
+ pulls: [],
105
+ order: [],
106
+ packages: []
107
+ };
108
+ function readManifest(dir) {
109
+ const file = path.join(dir, 'manifest.json');
110
+ if (!fs.existsSync(file)) return JSON.parse(JSON.stringify(EMPTY));
111
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
112
+ }
113
+ function write(dir, entities, meta, opts, bytes = new Map()) {
114
+ if (opts.replace) fs.rmSync(dir, {
115
+ recursive: true,
116
+ force: true
117
+ });
118
+ const manifest = readManifest(dir);
119
+ if (!manifest.source.url) manifest.source = meta.source;else if (manifest.source.url !== meta.source.url) throw new Error(`${dir} was pulled from ${manifest.source.url}; ` + `use --replace to start a new bundle from ${meta.source.url}`);
120
+ fs.mkdirSync(dir, {
121
+ recursive: true
122
+ });
123
+ const byId = new Map(manifest.order.map(e => [e.id, e]));
124
+ const takenBy = new Map([...byId.values()].map(e => [e.file, e.id]));
125
+ const ids = [];
126
+ for (const [id, {
127
+ type,
128
+ json
129
+ }] of entities) {
130
+ const base = `${type}/${(0, _registry.fileNameFor)(json)}`;
131
+ // Names are not unique across a bundle (two files of the same name in different shares);
132
+ // whoever claimed the plain name keeps it, the rest are suffixed by their id.
133
+ const owner = takenBy.get(`${base}.json`);
134
+ const file = !owner || owner === id ? `${base}.json` : `${base}-${id.slice(0, 8)}.json`;
135
+ const previous = byId.get(id);
136
+ if (previous && previous.file !== file) {
137
+ fs.rmSync(path.join(dir, previous.file), {
138
+ force: true
139
+ });
140
+ takenBy.delete(previous.file);
141
+ }
142
+ takenBy.set(file, id);
143
+ fs.mkdirSync(path.join(dir, type), {
144
+ recursive: true
145
+ });
146
+ fs.writeFileSync(path.join(dir, file), JSON.stringify(normalize(type, json), null, 2));
147
+ byId.set(id, {
148
+ type,
149
+ id,
150
+ file
151
+ });
152
+ ids.push(id);
153
+ }
154
+ for (const [id, buf] of bytes) {
155
+ const kind = _registry.TYPES[entities.get(id).type].bytes.kind;
156
+ fs.mkdirSync(path.join(dir, kind), {
157
+ recursive: true
158
+ });
159
+ fs.writeFileSync(bytesPath(dir, kind, id), buf);
160
+ }
161
+ manifest.formatVersion = 1;
162
+ manifest.pulls.push({
163
+ at: new Date().toISOString(),
164
+ args: meta.args,
165
+ ids
166
+ });
167
+ manifest.packages = [...new Set([...manifest.packages, ...meta.packages])].sort();
168
+ if (meta.externals?.length) {
169
+ const byId = new Map((manifest.externals ?? []).map(e => [e.id, e]));
170
+ for (const e of meta.externals) byId.set(e.id, e);
171
+ manifest.externals = [...byId.values()].sort((a, b) => a.nqName.localeCompare(b.nqName));
172
+ }
173
+ if (meta.dangling?.length) manifest.dangling = [...new Set([...(manifest.dangling ?? []), ...meta.dangling])].sort();
174
+ const pullOf = new Map();
175
+ manifest.pulls.forEach((p, i) => p.ids.forEach(id => pullOf.has(id) || pullOf.set(id, i)));
176
+ manifest.order = [...byId.values()].sort((a, b) => (0, _registry.rankOf)(a.type) - (0, _registry.rankOf)(b.type) || pullOf.get(a.id) - pullOf.get(b.id) || a.file.localeCompare(b.file));
177
+ fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2));
178
+ return manifest;
179
+ }
180
+ function read(dir) {
181
+ if (!fs.existsSync(path.join(dir, 'manifest.json'))) throw new Error(`Not a bundle directory (no manifest.json): ${dir}`);
182
+ const manifest = readManifest(dir);
183
+ const entities = new Map();
184
+ for (const e of manifest.order) {
185
+ const file = path.join(dir, e.file);
186
+ if (!fs.existsSync(file)) throw new Error(`Bundle is missing ${e.file} listed in manifest.json`);
187
+ entities.set(e.id, {
188
+ type: e.type,
189
+ json: JSON.parse(fs.readFileSync(file, 'utf8')),
190
+ file: e.file
191
+ });
192
+ }
193
+ const idmapFile = path.join(dir, 'idmap.json');
194
+ const idmap = fs.existsSync(idmapFile) ? JSON.parse(fs.readFileSync(idmapFile, 'utf8')) : {};
195
+ return {
196
+ dir,
197
+ manifest,
198
+ entities,
199
+ idmap
200
+ };
201
+ }
202
+
203
+ /** Adopted `sourceId → targetId` pairs, so the next push of this bundle is stable. */
204
+ function writeIdmap(dir, idmap) {
205
+ fs.writeFileSync(path.join(dir, 'idmap.json'), JSON.stringify(idmap, null, 2));
206
+ }
207
+ function list(dir) {
208
+ const {
209
+ manifest,
210
+ entities
211
+ } = read(dir);
212
+ return manifest.order.map(e => {
213
+ const pull = manifest.pulls.findIndex(p => p.ids.includes(e.id));
214
+ const json = entities.get(e.id).json;
215
+ return {
216
+ type: e.type,
217
+ nqName: (0, _registry.nqNameOf)(json),
218
+ file: e.file,
219
+ pulledOn: manifest.pulls[pull]?.at ?? '',
220
+ pull: pull + 1
221
+ };
222
+ });
223
+ }
@@ -0,0 +1,222 @@
1
+ /// Docs: [Entity export / import](/docs/features/grok-tool/export-import/DESIGN.md)
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import {createHash} from 'crypto';
5
+ import {BytesKind, TYPES, fileNameFor, nqNameOf, rankOf} from './registry';
6
+
7
+ export interface ManifestEntry {type: string; id: string; file: string}
8
+
9
+ export interface Manifest {
10
+ formatVersion: 1;
11
+ source: {url: string; version: string; commit?: string; userNamespace: string};
12
+ pulls: {at: string; args: string[]; ids: string[]}[];
13
+ order: ManifestEntry[];
14
+ packages: string[];
15
+ /** Referenced entities the source keeps; the push re-finds them by name. */
16
+ externals?: {id: string; type: string; nqName: string}[];
17
+ /** Ids the bundle references that nothing on the source answers to — dead there, unfixable here. */
18
+ dangling?: string[];
19
+ }
20
+
21
+ export interface BundleEntity {type: string; json: any; file?: string}
22
+
23
+ export interface Bundle {
24
+ dir: string;
25
+ manifest: Manifest;
26
+ entities: Map<string, BundleEntity>;
27
+ idmap: Record<string, string>;
28
+ }
29
+
30
+ // `entityTags` rows carry their own primary keys and come back in an unstable order:
31
+ // pushing them makes the hash flap AND re-points the source's tag rows at the copy.
32
+ const VOLATILE = ['createdOn', 'updatedOn', 'author', 'pictureId', 'encryptedParametersId',
33
+ 'keyKid', 'isAvailable', 'entityTags'];
34
+
35
+ export function normalize(type: string, json: any): any {
36
+ const copy = JSON.parse(JSON.stringify(json));
37
+ // Tag rows are volatile, but the tag names are worth migrating: keep them as a
38
+ // bundle-only list the pusher replays through `POST /entities/tag`.
39
+ const tags = [...new Set((copy.entityTags ?? []).map((t: any) => t?.tag).filter(Boolean))].sort();
40
+ if (tags.length)
41
+ copy._tags = tags;
42
+ for (const k of VOLATILE)
43
+ delete copy[k];
44
+ if (copy.package)
45
+ copy.package = {id: copy.package.id};
46
+ // The stamp is written by the pusher, so it must not make an unchanged entity look different.
47
+ if (copy.metaParams)
48
+ delete copy.metaParams.sync_id;
49
+ TYPES[type]?.strip?.(copy);
50
+ return sortKeys(copy);
51
+ }
52
+
53
+ function sortKeys(v: any): any {
54
+ if (Array.isArray(v)) return v.map(sortKeys);
55
+ if (v === null || typeof v !== 'object') return v;
56
+ const out: any = {};
57
+ for (const k of Object.keys(v).sort())
58
+ out[k] = sortKeys(v[k]);
59
+ return out;
60
+ }
61
+
62
+ /** Bundle-only keys (`_credentials`, `_grants`, `_members`) never travel to the server. */
63
+ export function stripPrivate(json: any): any {
64
+ for (const k of Object.keys(json))
65
+ if (k.startsWith('_'))
66
+ delete json[k];
67
+ return json;
68
+ }
69
+
70
+ /**
71
+ * What a comparison sees: the payload without the bundle-only keys and without the
72
+ * namespace, which the server computes from ownership (`Askalkin:` on the source,
73
+ * `Admin:` on the target) and would otherwise make every cross-instance push a rewrite.
74
+ * The bundle file keeps it — `findByNqName` resolves the twin by it.
75
+ */
76
+ export function hashView(type: string, json: any): any {
77
+ const view = stripPrivate(normalize(type, json));
78
+ delete view.namespace;
79
+ // Relations have a pass of their own, which only ever adds: their row ids belong to the
80
+ // target, and a target that links more than the bundle is not stale. The pusher compares
81
+ // them by coverage instead.
82
+ delete view.relations;
83
+ return view;
84
+ }
85
+
86
+ export function hashOf(type: string, json: any): string {
87
+ return createHash('sha256').update(JSON.stringify(hashView(type, json))).digest('hex');
88
+ }
89
+
90
+ export function bytesPath(dir: string, kind: BytesKind, id: string): string {
91
+ return path.join(dir, kind, kind === 'tables' ? `${id}.d42` : id);
92
+ }
93
+
94
+ /**
95
+ * A file a datasync table reads is stored under its full share path, flattened into one
96
+ * file name so the bundle stays a flat directory per kind and the path survives a round trip.
97
+ */
98
+ export function sharePath(dir: string, remotePath: string): string {
99
+ return path.join(dir, 'shares', encodeURIComponent(remotePath));
100
+ }
101
+
102
+ export function writeShares(dir: string, files: Map<string, Buffer>): void {
103
+ if (!files.size) return;
104
+ fs.mkdirSync(path.join(dir, 'shares'), {recursive: true});
105
+ for (const [remote, buf] of files)
106
+ fs.writeFileSync(sharePath(dir, remote), buf);
107
+ }
108
+
109
+ export function listShares(dir: string): string[] {
110
+ const at = path.join(dir, 'shares');
111
+ return fs.existsSync(at) ? fs.readdirSync(at).map(decodeURIComponent) : [];
112
+ }
113
+
114
+ const EMPTY: Manifest = {
115
+ formatVersion: 1,
116
+ source: {url: '', version: '', userNamespace: ''},
117
+ pulls: [],
118
+ order: [],
119
+ packages: [],
120
+ };
121
+
122
+ function readManifest(dir: string): Manifest {
123
+ const file = path.join(dir, 'manifest.json');
124
+ if (!fs.existsSync(file)) return JSON.parse(JSON.stringify(EMPTY));
125
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
126
+ }
127
+
128
+ export function write(dir: string, entities: Map<string, BundleEntity>,
129
+ meta: {source: Manifest['source']; args: string[]; packages: string[];
130
+ externals?: {id: string; type: string; nqName: string}[]; dangling?: string[]},
131
+ opts: {replace?: boolean},
132
+ bytes: Map<string, Buffer> = new Map()): Manifest {
133
+ if (opts.replace)
134
+ fs.rmSync(dir, {recursive: true, force: true});
135
+ const manifest = readManifest(dir);
136
+ if (!manifest.source.url)
137
+ manifest.source = meta.source;
138
+ else if (manifest.source.url !== meta.source.url)
139
+ throw new Error(`${dir} was pulled from ${manifest.source.url}; ` +
140
+ `use --replace to start a new bundle from ${meta.source.url}`);
141
+ fs.mkdirSync(dir, {recursive: true});
142
+
143
+ const byId = new Map<string, ManifestEntry>(manifest.order.map((e) => [e.id, e]));
144
+ const takenBy = new Map<string, string>([...byId.values()].map((e) => [e.file, e.id]));
145
+ const ids: string[] = [];
146
+ for (const [id, {type, json}] of entities) {
147
+ const base = `${type}/${fileNameFor(json)}`;
148
+ // Names are not unique across a bundle (two files of the same name in different shares);
149
+ // whoever claimed the plain name keeps it, the rest are suffixed by their id.
150
+ const owner = takenBy.get(`${base}.json`);
151
+ const file = !owner || owner === id ? `${base}.json` : `${base}-${id.slice(0, 8)}.json`;
152
+ const previous = byId.get(id);
153
+ if (previous && previous.file !== file) {
154
+ fs.rmSync(path.join(dir, previous.file), {force: true});
155
+ takenBy.delete(previous.file);
156
+ }
157
+ takenBy.set(file, id);
158
+ fs.mkdirSync(path.join(dir, type), {recursive: true});
159
+ fs.writeFileSync(path.join(dir, file), JSON.stringify(normalize(type, json), null, 2));
160
+ byId.set(id, {type, id, file});
161
+ ids.push(id);
162
+ }
163
+
164
+ for (const [id, buf] of bytes) {
165
+ const kind = TYPES[entities.get(id)!.type].bytes!.kind;
166
+ fs.mkdirSync(path.join(dir, kind), {recursive: true});
167
+ fs.writeFileSync(bytesPath(dir, kind, id), buf);
168
+ }
169
+
170
+ manifest.formatVersion = 1;
171
+ manifest.pulls.push({at: new Date().toISOString(), args: meta.args, ids});
172
+ manifest.packages = [...new Set([...manifest.packages, ...meta.packages])].sort();
173
+ if (meta.externals?.length) {
174
+ const byId = new Map((manifest.externals ?? []).map((e) => [e.id, e]));
175
+ for (const e of meta.externals) byId.set(e.id, e);
176
+ manifest.externals = [...byId.values()].sort((a, b) => a.nqName.localeCompare(b.nqName));
177
+ }
178
+ if (meta.dangling?.length)
179
+ manifest.dangling = [...new Set([...(manifest.dangling ?? []), ...meta.dangling])].sort();
180
+ const pullOf = new Map<string, number>();
181
+ manifest.pulls.forEach((p, i) => p.ids.forEach((id) => pullOf.has(id) || pullOf.set(id, i)));
182
+ manifest.order = [...byId.values()].sort((a, b) =>
183
+ rankOf(a.type) - rankOf(b.type) || pullOf.get(a.id)! - pullOf.get(b.id)! || a.file.localeCompare(b.file));
184
+ fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2));
185
+ return manifest;
186
+ }
187
+
188
+ export function read(dir: string): Bundle {
189
+ if (!fs.existsSync(path.join(dir, 'manifest.json')))
190
+ throw new Error(`Not a bundle directory (no manifest.json): ${dir}`);
191
+ const manifest = readManifest(dir);
192
+ const entities = new Map<string, BundleEntity>();
193
+ for (const e of manifest.order) {
194
+ const file = path.join(dir, e.file);
195
+ if (!fs.existsSync(file))
196
+ throw new Error(`Bundle is missing ${e.file} listed in manifest.json`);
197
+ entities.set(e.id, {type: e.type, json: JSON.parse(fs.readFileSync(file, 'utf8')), file: e.file});
198
+ }
199
+ const idmapFile = path.join(dir, 'idmap.json');
200
+ const idmap = fs.existsSync(idmapFile) ? JSON.parse(fs.readFileSync(idmapFile, 'utf8')) : {};
201
+ return {dir, manifest, entities, idmap};
202
+ }
203
+
204
+ /** Adopted `sourceId → targetId` pairs, so the next push of this bundle is stable. */
205
+ export function writeIdmap(dir: string, idmap: Record<string, string>): void {
206
+ fs.writeFileSync(path.join(dir, 'idmap.json'), JSON.stringify(idmap, null, 2));
207
+ }
208
+
209
+ export function list(dir: string): any[] {
210
+ const {manifest, entities} = read(dir);
211
+ return manifest.order.map((e) => {
212
+ const pull = manifest.pulls.findIndex((p) => p.ids.includes(e.id));
213
+ const json = entities.get(e.id)!.json;
214
+ return {
215
+ type: e.type,
216
+ nqName: nqNameOf(json),
217
+ file: e.file,
218
+ pulledOn: manifest.pulls[pull]?.at ?? '',
219
+ pull: pull + 1,
220
+ };
221
+ });
222
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.SWEEP = void 0;
7
+ exports.missingPackages = missingPackages;
8
+ exports.missingUsers = missingUsers;
9
+ exports.namespacesOf = namespacesOf;
10
+ exports.plannedParts = plannedParts;
11
+ exports.readState = readState;
12
+ exports.writeState = writeState;
13
+ var fs = _interopRequireWildcard(require("fs"));
14
+ var path = _interopRequireWildcard(require("path"));
15
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
16
+ /// Docs: [Entity export / import](/docs/features/grok-tool/export-import/DESIGN.md)
17
+
18
+ /**
19
+ * Every space worth migrating on its own: the personal space of each user, and each team space.
20
+ * A whole instance moved in one bundle has every project competing for the same entities, and
21
+ * placement is exclusive on the server — scoping a run to one space keeps that contention inside
22
+ * it, which is the difference between a run that finishes and one that does not.
23
+ */
24
+ async function namespacesOf(dapi) {
25
+ const names = new Set();
26
+ for (const p of await dapi.internal('/projects').listAll({
27
+ includeRoot: 'true'
28
+ })) {
29
+ if (p?.isEntity || p?.isPackage) continue;
30
+ // The platform's own space: its connections are already refused as `platform_connection`, the
31
+ // target builds its own, and listing what is under it does not answer.
32
+ if (String(p.namespace ?? '').startsWith('System:') || p.name === 'System') continue;
33
+ const own = String(p.namespace ?? '').split(':')[0];
34
+ if (own) names.add(own);else if (p.isRoot && p.name) names.add(String(p.name));
35
+ }
36
+ return [...names].sort();
37
+ }
38
+
39
+ /** Users the source has and the target does not: their content lands under the pusher instead. */
40
+ async function missingUsers(from, to) {
41
+ const here = new Set((await to.internal('/users').listAll({})).map(u => u.login));
42
+ return (await from.internal('/users').listAll({})).map(u => u.login).filter(login => login && !here.has(login)).sort();
43
+ }
44
+
45
+ /** Packages the source has and the target does not: their functions and connections cannot resolve. */
46
+ async function missingPackages(from, to) {
47
+ const here = new Set((await to.internal('/packages').listAll({})).map(p => p.name));
48
+ return (await from.internal('/packages').listAll({})).map(p => p.name).filter(name => name && !here.has(name)).sort();
49
+ }
50
+
51
+ /** A run is resumable: what finished is remembered, so a repeat does not redo it. */
52
+ function readState(file) {
53
+ try {
54
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
55
+ } catch {
56
+ return {};
57
+ }
58
+ }
59
+ function writeState(file, state) {
60
+ fs.mkdirSync(path.dirname(file), {
61
+ recursive: true
62
+ });
63
+ fs.writeFileSync(file, JSON.stringify(state, null, 2));
64
+ }
65
+
66
+ /** The last part: whatever no space owns, which would otherwise never travel. */
67
+ const SWEEP = exports.SWEEP = '(unowned)';
68
+
69
+ /**
70
+ * Which parts a run still has to do. A name that matches no space is a typo, not an empty
71
+ * selection — silently migrating nothing is the one outcome worse than refusing.
72
+ */
73
+ function plannedParts(all, opts) {
74
+ const only = opts.only ?? [];
75
+ const skip = new Set(opts.skip ?? []);
76
+ const unknown = [...only, ...(opts.skip ?? [])].filter(n => !all.includes(n));
77
+ if (unknown.length) throw new Error(`No such space: ${unknown.join(', ')}`);
78
+ const chosen = all.filter(n => (!only.length || only.includes(n)) && !skip.has(n));
79
+ if (opts.sweep) chosen.push(SWEEP);
80
+ // A part that failed, or that reported failures, is not finished.
81
+ const done = opts.state ?? {};
82
+ return chosen.filter(n => !done[n] || done[n].error || done[n].failed);
83
+ }
@@ -0,0 +1,72 @@
1
+ /// Docs: [Entity export / import](/docs/features/grok-tool/export-import/DESIGN.md)
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import {NodeDapi} from '../node-dapi';
5
+
6
+ export interface Part {name: string; entities?: number; failed?: number; seconds?: number; error?: string}
7
+
8
+ /**
9
+ * Every space worth migrating on its own: the personal space of each user, and each team space.
10
+ * A whole instance moved in one bundle has every project competing for the same entities, and
11
+ * placement is exclusive on the server — scoping a run to one space keeps that contention inside
12
+ * it, which is the difference between a run that finishes and one that does not.
13
+ */
14
+ export async function namespacesOf(dapi: NodeDapi): Promise<string[]> {
15
+ const names = new Set<string>();
16
+ for (const p of await dapi.internal('/projects').listAll({includeRoot: 'true'})) {
17
+ if (p?.isEntity || p?.isPackage) continue;
18
+ // The platform's own space: its connections are already refused as `platform_connection`, the
19
+ // target builds its own, and listing what is under it does not answer.
20
+ if (String(p.namespace ?? '').startsWith('System:') || p.name === 'System') continue;
21
+ const own = String(p.namespace ?? '').split(':')[0];
22
+ if (own) names.add(own);
23
+ else if (p.isRoot && p.name) names.add(String(p.name));
24
+ }
25
+ return [...names].sort();
26
+ }
27
+
28
+ /** Users the source has and the target does not: their content lands under the pusher instead. */
29
+ export async function missingUsers(from: NodeDapi, to: NodeDapi): Promise<string[]> {
30
+ const here = new Set((await to.internal('/users').listAll({})).map((u: any) => u.login));
31
+ return (await from.internal('/users').listAll({}))
32
+ .map((u: any) => u.login).filter((login: string) => login && !here.has(login)).sort();
33
+ }
34
+
35
+ /** Packages the source has and the target does not: their functions and connections cannot resolve. */
36
+ export async function missingPackages(from: NodeDapi, to: NodeDapi): Promise<string[]> {
37
+ const here = new Set((await to.internal('/packages').listAll({})).map((p: any) => p.name));
38
+ return (await from.internal('/packages').listAll({}))
39
+ .map((p: any) => p.name).filter((name: string) => name && !here.has(name)).sort();
40
+ }
41
+
42
+ /** A run is resumable: what finished is remembered, so a repeat does not redo it. */
43
+ export function readState(file: string): Record<string, Part> {
44
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
45
+ catch { return {}; }
46
+ }
47
+
48
+ export function writeState(file: string, state: Record<string, Part>): void {
49
+ fs.mkdirSync(path.dirname(file), {recursive: true});
50
+ fs.writeFileSync(file, JSON.stringify(state, null, 2));
51
+ }
52
+
53
+ /** The last part: whatever no space owns, which would otherwise never travel. */
54
+ export const SWEEP = '(unowned)';
55
+
56
+ /**
57
+ * Which parts a run still has to do. A name that matches no space is a typo, not an empty
58
+ * selection — silently migrating nothing is the one outcome worse than refusing.
59
+ */
60
+ export function plannedParts(all: string[], opts: {only?: string[]; skip?: string[];
61
+ state?: Record<string, Part>; sweep?: boolean}): string[] {
62
+ const only = opts.only ?? [];
63
+ const skip = new Set(opts.skip ?? []);
64
+ const unknown = [...only, ...(opts.skip ?? [])].filter((n) => !all.includes(n));
65
+ if (unknown.length)
66
+ throw new Error(`No such space: ${unknown.join(', ')}`);
67
+ const chosen = all.filter((n) => (!only.length || only.includes(n)) && !skip.has(n));
68
+ if (opts.sweep) chosen.push(SWEEP);
69
+ // A part that failed, or that reported failures, is not finished.
70
+ const done = opts.state ?? {};
71
+ return chosen.filter((n) => !done[n] || done[n].error || done[n].failed);
72
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.pool = pool;
7
+ /// Docs: [Entity export / import](/docs/features/grok-tool/export-import/DESIGN.md)
8
+
9
+ /** Runs `work` over `items` with at most `concurrency` in flight, in the order given. */
10
+ async function pool(items, concurrency, work) {
11
+ const queue = items.slice();
12
+ const workers = [];
13
+ for (let i = 0; i < Math.min(concurrency, queue.length); i++) workers.push((async () => {
14
+ while (queue.length) await work(queue.shift());
15
+ })());
16
+ await Promise.all(workers);
17
+ }
@@ -0,0 +1,13 @@
1
+ /// Docs: [Entity export / import](/docs/features/grok-tool/export-import/DESIGN.md)
2
+
3
+ /** Runs `work` over `items` with at most `concurrency` in flight, in the order given. */
4
+ export async function pool<T>(items: T[], concurrency: number, work: (item: T) => Promise<void>): Promise<void> {
5
+ const queue = items.slice();
6
+ const workers: Promise<void>[] = [];
7
+ for (let i = 0; i < Math.min(concurrency, queue.length); i++)
8
+ workers.push((async () => {
9
+ while (queue.length)
10
+ await work(queue.shift()!);
11
+ })());
12
+ await Promise.all(workers);
13
+ }