@hiveku-apps/sync 0.1.0 → 0.3.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/src/init.mjs CHANGED
@@ -7,9 +7,10 @@
7
7
 
8
8
  import { existsSync } from 'node:fs';
9
9
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
10
- import { dirname, join, resolve } from 'node:path';
10
+ import { basename, dirname, join, resolve } from 'node:path';
11
11
 
12
- import { writeFileAtomic, safeSlug, TYPE_TO_FOLDER } from './util.mjs';
12
+ import { writeFileAtomic, safeSlug, TYPE_TO_FOLDER, loadDotenv } from './util.mjs';
13
+ import { writeCodexScaffold, preTrustFolder } from './codex.mjs';
13
14
 
14
15
  const VALID_PROFILES = ['full', 'sales', 'marketing', 'helpdesk', 'pm', 'dev'];
15
16
  const DEFAULT_BASE_URL = 'https://core.hiveku.com';
@@ -148,7 +149,7 @@ async function appendToWorkspaceFile(workspaceFilePath, folderRelPath) {
148
149
  return true;
149
150
  }
150
151
 
151
- export async function runInit({ slug, parentDir, profile = 'full' }) {
152
+ export async function runInit({ slug, parentDir, profile = 'full', codex = false, trust = false }) {
152
153
  if (!slug) throw new Error('slug is required, e.g. hiveku-sync init acme-corp');
153
154
  if (!VALID_PROFILES.includes(profile)) {
154
155
  throw new Error(
@@ -190,5 +191,36 @@ export async function runInit({ slug, parentDir, profile = 'full' }) {
190
191
  `./${safe}`,
191
192
  );
192
193
 
193
- return { folder, slug: safe, profile, workspaceAdded };
194
+ // Optional Codex lane: AGENTS.md + .codex/config.toml + knowledge→skills, so
195
+ // the same folder works from the Codex CLI / IDE / ChatGPT desktop too.
196
+ let codexResult = null;
197
+ if (codex) {
198
+ codexResult = await writeCodexScaffold({
199
+ folder,
200
+ slug: safe,
201
+ baseUrl: DEFAULT_BASE_URL,
202
+ profile,
203
+ });
204
+ if (trust) codexResult.trust = await preTrustFolder(folder);
205
+ }
206
+
207
+ return { folder, slug: safe, profile, workspaceAdded, codex: codexResult };
208
+ }
209
+
210
+ /**
211
+ * Retrofit an EXISTING account folder with the Codex lane (AGENTS.md +
212
+ * .codex/config.toml + knowledge→skills). Run from inside the folder, or pass
213
+ * one. Derives the account slug from the folder name and the profile from
214
+ * .env (HIVEKU_PROFILE), and inlines the real OLYMPUS_API_KEY when .env has it.
215
+ */
216
+ export async function runCodex({ folder, trust = false } = {}) {
217
+ const target = folder ? resolve(folder) : process.cwd();
218
+ if (!existsSync(target)) throw new Error(`Folder not found: ${target}`);
219
+ const dotenv = await loadDotenv(join(target, '.env'));
220
+ const profile = dotenv.HIVEKU_PROFILE || 'full';
221
+ const baseUrl = dotenv.OLYMPUS_BASE_URL || DEFAULT_BASE_URL;
222
+ const slug = safeSlug(basename(target));
223
+ const result = await writeCodexScaffold({ folder: target, slug, baseUrl, profile });
224
+ if (trust) result.trust = await preTrustFolder(target);
225
+ return { folder: target, slug, profile, ...result };
194
226
  }
package/src/projects.mjs CHANGED
@@ -19,8 +19,11 @@ import {
19
19
  writeFileAtomic,
20
20
  readJson,
21
21
  } from './util.mjs';
22
+ // NOTE: push.mjs imports helpers from this module; the cycle is safe because
23
+ // both sides reference the imports only inside functions (never at module eval).
24
+ import { computePushPlan } from './push.mjs';
22
25
 
23
- const PROJECTS_DIR = 'projects';
26
+ export const PROJECTS_DIR = 'projects';
24
27
  const PROJECTS_MANIFEST = '.hiveku/projects.json';
25
28
 
26
29
  const IGNORE_DIRS = new Set([
@@ -46,11 +49,11 @@ const RUN_HINTS = {
46
49
  static: 'npx serve .',
47
50
  };
48
51
 
49
- function sha256(content) {
52
+ export function sha256(content) {
50
53
  return createHash('sha256').update(content).digest('hex');
51
54
  }
52
55
 
53
- async function clientFromCwd({ profileOverride } = {}) {
56
+ export async function clientFromCwd({ profileOverride } = {}) {
54
57
  const baseDir = process.cwd();
55
58
  const dotenv = await loadDotenv(join(baseDir, '.env'));
56
59
  const apiKey = readEnv(process.env, dotenv, 'OLYMPUS_API_KEY');
@@ -86,7 +89,7 @@ async function listAllRemoteFiles(client, projectId) {
86
89
  return all;
87
90
  }
88
91
 
89
- async function walkLocalFiles(rootDir) {
92
+ export async function walkLocalFiles(rootDir) {
90
93
  const out = [];
91
94
  async function recurse(dir) {
92
95
  let entries;
@@ -125,16 +128,16 @@ function runHintForProjectType(type) {
125
128
  return RUN_HINTS[type] || `# Unknown project_type "${type}". Inspect package.json/requirements.txt to find the dev command.`;
126
129
  }
127
130
 
128
- async function readProjectsManifest(baseDir) {
131
+ export async function readProjectsManifest(baseDir) {
129
132
  const data = await readJson(join(baseDir, PROJECTS_MANIFEST));
130
133
  return data && typeof data === 'object' ? data : { projects: {} };
131
134
  }
132
135
 
133
- async function writeProjectsManifest(baseDir, manifest) {
136
+ export async function writeProjectsManifest(baseDir, manifest) {
134
137
  await writeFileAtomic(join(baseDir, PROJECTS_MANIFEST), JSON.stringify(manifest, null, 2));
135
138
  }
136
139
 
137
- async function resolveProject(client, slugOrId) {
140
+ export async function resolveProject(client, slugOrId) {
138
141
  // Accept either UUID (use directly) or slug (look up via list_projects).
139
142
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
140
143
  if (UUID_RE.test(slugOrId)) {
@@ -227,19 +230,22 @@ export async function runPullProject({ slugOrId, profile, branch }) {
227
230
  runHintForProjectType(project.project_type),
228
231
  '```',
229
232
  '',
230
- '## Commit changes back',
233
+ '## Ship changes back',
231
234
  '',
232
- 'This folder is NOT a git clone — it\'s a snapshot. Use the Hiveku MCP `project_commit` tool',
233
- 'to write edits back to the project\'s GitHub branch (same path the v3 sidebar uses).',
235
+ "This folder is NOT a git clone — it's a snapshot. `push` writes your edits to the",
236
+ 'DEPLOY SOURCE (what `deploy` builds from), lane-routing images/fonts/video to the',
237
+ 'S3/CDN asset store and everything else to versioned code files.',
234
238
  '',
235
239
  'From this folder:',
236
240
  '',
237
241
  '```bash',
238
- `hiveku-sync status # diff local vs remote`,
239
- `hiveku-sync push -m "your message" # save + commit changed files via MCP`,
242
+ 'hiveku-sync pull-env # project secrets → .env.local',
243
+ 'hiveku-sync status # diff local vs the deploy source',
244
+ 'hiveku-sync push # save changes back',
245
+ 'hiveku-sync push --deploy development --wait # ...then deploy and wait',
240
246
  '```',
241
247
  '',
242
- 'Or ask Claude in this workspace to commit the changed files for you.',
248
+ 'Or ask Claude in this workspace to push/deploy for you.',
243
249
  '',
244
250
  ]
245
251
  .filter((line) => line !== undefined)
@@ -263,7 +269,7 @@ export async function runPullProject({ slugOrId, profile, branch }) {
263
269
  return { dest, written, skipped, projectId, slug: localSlug };
264
270
  }
265
271
 
266
- async function findProjectFolder({ slug, baseDir }) {
272
+ export async function findProjectFolder({ slug, baseDir }) {
267
273
  // Allow status/push to be called from inside ./projects/<slug>/, ./<slug>/,
268
274
  // or the account root with an explicit slug arg.
269
275
  const manifest = await readProjectsManifest(baseDir);
@@ -310,39 +316,68 @@ export async function runProjectStatus({ slug, profile, json = false }) {
310
316
  throw new Error(`Project folder ${folder} doesn't exist — run pull-project first.`);
311
317
  }
312
318
 
313
- const localManifest = await buildLocalManifest(folder);
314
- const diff = (await client.callToolJson('project_files_status', {
315
- project_id: info.project_id,
316
- local: localManifest,
317
- })).data;
319
+ // Deploy-source-accurate + asset-lane-aware — the SAME diff `push` runs, so
320
+ // status never flags unchanged images as only_local (code goes through
321
+ // project_files_status; assets through assets_list).
322
+ const { diff, assetAll, assetToUpload, assetSkipped, plan } = await computePushPlan({
323
+ client,
324
+ projectId: info.project_id,
325
+ folder,
326
+ includeDeletes: true,
327
+ });
318
328
 
319
329
  if (json) {
320
- console.log(JSON.stringify({ slug: resolvedSlug, project_id: info.project_id, ...diff }, null, 2));
321
- return diff;
330
+ console.log(
331
+ JSON.stringify(
332
+ {
333
+ slug: resolvedSlug,
334
+ project_id: info.project_id,
335
+ code: diff,
336
+ assets: {
337
+ to_upload: assetToUpload.map((f) => f.path),
338
+ unchanged: assetSkipped,
339
+ total_local: assetAll.length,
340
+ },
341
+ plan,
342
+ },
343
+ null,
344
+ 2,
345
+ ),
346
+ );
347
+ return { diff, plan };
322
348
  }
323
349
 
324
- const sum = summarizeDiff(diff);
350
+ const changed = diff.changed?.length || 0;
351
+ const onlyLocal = diff.only_local?.length || 0;
352
+ const onlyRemote = diff.only_remote?.length || 0;
353
+ const unchanged = diff.same_count || 0;
325
354
  console.log(`Project: ${resolvedSlug} (${info.project_id})`);
326
- console.log(` changed: ${sum.changed}`);
327
- console.log(` only_local: ${sum.only_local}`);
328
- console.log(` only_remote: ${sum.only_remote}`);
329
- console.log(` unchanged: ${sum.same}`);
330
- if (sum.changed > 0) {
331
- console.log('\nChanged files:');
332
- for (const f of diff.changed.slice(0, 20)) console.log(` M ${f.path}`);
333
- if (diff.changed.length > 20) console.log(` ... and ${diff.changed.length - 20} more`);
334
- }
335
- if (sum.only_local > 0) {
336
- console.log('\nLocal-only (will be added on push):');
337
- for (const f of diff.only_local.slice(0, 20)) console.log(` + ${f.path}`);
338
- if (diff.only_local.length > 20) console.log(` ... and ${diff.only_local.length - 20} more`);
339
- }
340
- if (sum.only_remote > 0) {
341
- console.log('\nRemote-only (deleted locally or never pulled — pass --include-deletes to push to commit deletions):');
342
- for (const f of diff.only_remote.slice(0, 20)) console.log(` - ${f.path}`);
343
- if (diff.only_remote.length > 20) console.log(` ... and ${diff.only_remote.length - 20} more`);
344
- }
345
- return diff;
355
+ console.log('Compared against the DEPLOY SOURCE (builder_code_versions + asset store).\n');
356
+ console.log('Code:');
357
+ console.log(` changed: ${changed}`);
358
+ console.log(` added: ${onlyLocal}`);
359
+ console.log(` remote-only: ${onlyRemote}`);
360
+ console.log(` unchanged: ${unchanged}`);
361
+ console.log('Assets (images/fonts/video under public/<subdir>/):');
362
+ console.log(` to upload: ${assetToUpload.length} (new or changed)`);
363
+ console.log(` unchanged: ${assetSkipped}`);
364
+ console.log(` total local: ${assetAll.length}`);
365
+
366
+ const printList = (items, marker, header, getPath = (f) => f.path) => {
367
+ if (!items || items.length === 0) return;
368
+ console.log(`\n${header}`);
369
+ for (const f of items.slice(0, 20)) console.log(` ${marker} ${getPath(f)}`);
370
+ if (items.length > 20) console.log(` ... and ${items.length - 20} more`);
371
+ };
372
+ printList(diff.changed, 'M', 'Changed code:');
373
+ printList(diff.only_local, '+', 'Added code (will be created on push):');
374
+ printList(
375
+ diff.only_remote,
376
+ '-',
377
+ 'Remote-only code (deleted locally or never pulled — push --include-deletes to delete):',
378
+ );
379
+ printList(assetToUpload, 'A', 'Assets to upload (new or changed):');
380
+ return { diff, plan };
346
381
  }
347
382
 
348
383
  export async function runProjectPush({