@hiveku-apps/sync 0.3.6 → 0.5.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.
- package/README.md +183 -9
- package/bin/hiveku-sync.mjs +178 -28
- package/package.json +1 -1
- package/src/claude-md.mjs +193 -0
- package/src/codex-prompt-tools.json +2 -1
- package/src/codex.mjs +234 -45
- package/src/init.mjs +60 -53
- package/src/knowledge-files.mjs +306 -0
- package/src/memory-owner.mjs +464 -0
- package/src/projects.mjs +126 -69
- package/src/pull.mjs +182 -78
- package/src/pullRecord.mjs +98 -0
- package/src/pullTree.mjs +211 -0
- package/src/push.mjs +230 -94
- package/src/pushGuards.mjs +100 -0
- package/src/saveFiles.mjs +111 -0
- package/src/util.mjs +0 -10
- package/src/versionName.mjs +569 -0
- package/src/versions.mjs +811 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save code files to Hiveku in batches with project_files_bulk_save, and say
|
|
3
|
+
* exactly which ones did not land.
|
|
4
|
+
*
|
|
5
|
+
* On Your site the route writes files one by one and stops at an 85 s wall
|
|
6
|
+
* budget (the edge would otherwise close the request): it answers 207 with
|
|
7
|
+
* `partial: true` and `remaining_paths`, the files it did NOT attempt, which
|
|
8
|
+
* are not in `results[]`. Its own hint: call again with ONLY the files in
|
|
9
|
+
* remaining_paths, never the whole batch. So each batch is sent once, then
|
|
10
|
+
* once more with only what did not land (ok:false results plus
|
|
11
|
+
* remaining_paths); whatever still has not landed is reported as not saved.
|
|
12
|
+
* A file is counted as saved once, however many times it was sent.
|
|
13
|
+
*
|
|
14
|
+
* A branch save answers the branch's new `working_tree_etag`; the last one
|
|
15
|
+
* is returned so the caller can record which tree its push left behind.
|
|
16
|
+
*
|
|
17
|
+
* Builder contract: hiveku_builder feature/versions-core
|
|
18
|
+
* src/app/api/olympus/builder/projects/[projectId]/files-bulk/route.ts (POST).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Batches of about 25 text files finish inside the route's time budget (the tool's own advice). */
|
|
22
|
+
export const BATCH_FILES = 25;
|
|
23
|
+
/** Well under the route's 20 MB cap; sized for MCP transport reliability. */
|
|
24
|
+
export const BATCH_BYTES = 4_000_000;
|
|
25
|
+
const ATTEMPTS = 2;
|
|
26
|
+
|
|
27
|
+
/** Split files into batches of at most BATCH_FILES files and BATCH_BYTES of content. */
|
|
28
|
+
export function batchFiles(files, { maxFiles = BATCH_FILES, maxBytes = BATCH_BYTES } = {}) {
|
|
29
|
+
const batches = [];
|
|
30
|
+
let batch = [];
|
|
31
|
+
let bytes = 0;
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const size = String(file.content ?? '').length;
|
|
34
|
+
if (batch.length >= maxFiles || (bytes > 0 && bytes + size > maxBytes)) {
|
|
35
|
+
batches.push(batch);
|
|
36
|
+
batch = [];
|
|
37
|
+
bytes = 0;
|
|
38
|
+
}
|
|
39
|
+
batch.push(file);
|
|
40
|
+
bytes += size;
|
|
41
|
+
}
|
|
42
|
+
if (batch.length) batches.push(batch);
|
|
43
|
+
return batches;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read one bulk_save answer for the files that were sent: which landed, which
|
|
48
|
+
* did not, and the branch etag. `results[]` names each file the route tried
|
|
49
|
+
* and `remaining_paths` the ones it did not try. A file the answer does not
|
|
50
|
+
* name counts as landed only when `summary.succeeded` accounts for it (an
|
|
51
|
+
* older answer carries the summary alone), else as not landed.
|
|
52
|
+
*/
|
|
53
|
+
export function readSaveAnswer(data, sentPaths) {
|
|
54
|
+
if (!data || typeof data !== 'object') return { landed: [], notLanded: [...sentPaths], etag: null };
|
|
55
|
+
const sent = new Set(sentPaths);
|
|
56
|
+
const results = Array.isArray(data.results) ? data.results.filter((r) => r && sent.has(r.path)) : [];
|
|
57
|
+
const notLanded = new Set([
|
|
58
|
+
...results.filter((r) => r.ok === false).map((r) => r.path),
|
|
59
|
+
...(Array.isArray(data.remaining_paths) ? data.remaining_paths.filter((p) => sent.has(p)) : []),
|
|
60
|
+
]);
|
|
61
|
+
const landed = new Set(results.filter((r) => r.ok !== false && !notLanded.has(r.path)).map((r) => r.path));
|
|
62
|
+
const unnamed = sentPaths.filter((p) => !landed.has(p) && !notLanded.has(p));
|
|
63
|
+
if (unnamed.length) {
|
|
64
|
+
const succeeded = typeof data.summary?.succeeded === 'number' ? data.summary.succeeded : null;
|
|
65
|
+
const restLanded = succeeded === null ? notLanded.size === 0 : succeeded - landed.size >= unnamed.length;
|
|
66
|
+
for (const p of unnamed) (restLanded ? landed : notLanded).add(p);
|
|
67
|
+
}
|
|
68
|
+
const etag = typeof data.working_tree_etag === 'string' && data.working_tree_etag ? data.working_tree_etag : null;
|
|
69
|
+
return { landed: [...landed], notLanded: [...notLanded], etag };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Save `files` ({ path, content, encoding }) to Your site (branch null) or a
|
|
74
|
+
* branch. Returns { saved, failed: [paths], workingTreeEtag }. Never throws for
|
|
75
|
+
* a refused or failed call: those files are reported in `failed`.
|
|
76
|
+
*/
|
|
77
|
+
export async function saveCodeFiles({ client, projectId, files, branch = null, note, log = () => {} }) {
|
|
78
|
+
const savedPaths = new Set();
|
|
79
|
+
const failed = [];
|
|
80
|
+
let workingTreeEtag = null;
|
|
81
|
+
const total = files.length;
|
|
82
|
+
for (const batch of batchFiles(files)) {
|
|
83
|
+
let pending = batch;
|
|
84
|
+
for (let attempt = 1; attempt <= ATTEMPTS && pending.length; attempt++) {
|
|
85
|
+
const sentPaths = pending.map((f) => f.path);
|
|
86
|
+
let data;
|
|
87
|
+
try {
|
|
88
|
+
data = (
|
|
89
|
+
await client.callToolJson('project_files_bulk_save', {
|
|
90
|
+
project_id: projectId,
|
|
91
|
+
files: pending.map((f) => ({ path: f.path, content: f.content, encoding: f.encoding })),
|
|
92
|
+
// A note on each file's row, never a version (the version is saved once, after every file).
|
|
93
|
+
commit_message: note,
|
|
94
|
+
...(branch ? { branch } : {}),
|
|
95
|
+
})
|
|
96
|
+
)?.data;
|
|
97
|
+
} catch {
|
|
98
|
+
// No answer: nothing is known about this batch, so all of it is sent again.
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const answer = readSaveAnswer(data, sentPaths);
|
|
102
|
+
for (const p of answer.landed) savedPaths.add(p);
|
|
103
|
+
if (answer.etag) workingTreeEtag = answer.etag;
|
|
104
|
+
const again = new Set(answer.notLanded);
|
|
105
|
+
pending = pending.filter((f) => again.has(f.path) && !savedPaths.has(f.path));
|
|
106
|
+
}
|
|
107
|
+
failed.push(...pending.map((f) => f.path));
|
|
108
|
+
log(` code ${savedPaths.size}/${total}`);
|
|
109
|
+
}
|
|
110
|
+
return { saved: savedPaths.size, failed, workingTreeEtag };
|
|
111
|
+
}
|
package/src/util.mjs
CHANGED
|
@@ -87,16 +87,6 @@ export const TYPE_TO_FOLDER = {
|
|
|
87
87
|
|
|
88
88
|
export const SUPPORTED_TYPES = Object.keys(TYPE_TO_FOLDER);
|
|
89
89
|
|
|
90
|
-
/** Extract a department tag from content. Looks for HTML or YAML markers. */
|
|
91
|
-
export function extractDepartmentTag(content) {
|
|
92
|
-
if (!content) return null;
|
|
93
|
-
const htmlMatch = content.match(/<!--\s*department:\s*([a-z0-9_-]+)\s*-->/i);
|
|
94
|
-
if (htmlMatch) return htmlMatch[1];
|
|
95
|
-
const yamlMatch = content.match(/^department:\s*["']?([a-z0-9_-]+)["']?\s*$/im);
|
|
96
|
-
if (yamlMatch) return yamlMatch[1];
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
90
|
/** Resolve account base directory (default: ./). */
|
|
101
91
|
export function defaultBaseDir(cwd = process.cwd()) {
|
|
102
92
|
return cwd;
|