@hiveku-apps/sync 0.3.2 → 0.3.3
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/package.json +1 -1
- package/src/projects.mjs +236 -34
- package/src/push.mjs +37 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hiveku-apps/sync",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Sync a Hiveku account's memory/rules/skills + coder projects into a local folder, and ship projects (push/deploy/pull-env) from the terminal. Pairs with the Hiveku MCP at core.hiveku.com/mcp so Claude Code can drive Hiveku tools per-folder.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/projects.mjs
CHANGED
|
@@ -47,8 +47,37 @@ const RUN_HINTS = {
|
|
|
47
47
|
'python-lambda': 'python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt',
|
|
48
48
|
expo: 'npm install && npx expo start',
|
|
49
49
|
static: 'npx serve .',
|
|
50
|
+
// The server's name for a static site is `static-html` (site_create's enum);
|
|
51
|
+
// `static` above is kept for rows created before that name settled.
|
|
52
|
+
'static-html': 'npx serve .',
|
|
53
|
+
external:
|
|
54
|
+
'# External site: there is no code to run. Manage it from the Hiveku dashboard (Webflow sites: /dashboard/<project id>/webflow) or the webflow_* MCP tools.',
|
|
50
55
|
};
|
|
51
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Project types with no code tree to mirror. Mirrors the builder's
|
|
59
|
+
* NO_LOCAL_CODE_TYPES minus python-lambda, which hiveku-sync still pulls and
|
|
60
|
+
* ships with the run hint above; folding it in is a separate decision.
|
|
61
|
+
*/
|
|
62
|
+
const EXTERNAL_TYPES = new Set(['external']);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Refuse an external project (a site hosted on Webflow, WordPress or any other
|
|
66
|
+
* host that Hiveku only tracks) before any file, VCS or deploy call. Accepts a
|
|
67
|
+
* project_get/sites_list row (`id`) or a projects.json manifest row
|
|
68
|
+
* (`project_id`); both carry `project_type`, the former `external_platform`.
|
|
69
|
+
*/
|
|
70
|
+
export function refuseExternal(project) {
|
|
71
|
+
if (!project || !EXTERNAL_TYPES.has(String(project.project_type || '').toLowerCase())) return;
|
|
72
|
+
const id = project.id || project.project_id;
|
|
73
|
+
const platform = project.external_platform ? `, platform=${project.external_platform}` : '';
|
|
74
|
+
throw new Error(
|
|
75
|
+
`"${project.name || id}" is an external site (project_type=external${platform}). ` +
|
|
76
|
+
`hiveku-sync mirrors code projects only; edit this site in the Hiveku dashboard ` +
|
|
77
|
+
`(/dashboard/${id}/webflow for Webflow) or through the webflow_* MCP tools.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
export function sha256(content) {
|
|
53
82
|
return createHash('sha256').update(content).digest('hex');
|
|
54
83
|
}
|
|
@@ -124,10 +153,21 @@ async function buildLocalManifest(rootDir) {
|
|
|
124
153
|
return manifest;
|
|
125
154
|
}
|
|
126
155
|
|
|
127
|
-
function runHintForProjectType(type) {
|
|
156
|
+
export function runHintForProjectType(type) {
|
|
128
157
|
return RUN_HINTS[type] || `# Unknown project_type "${type}". Inspect package.json/requirements.txt to find the dev command.`;
|
|
129
158
|
}
|
|
130
159
|
|
|
160
|
+
/**
|
|
161
|
+
* The MCP client and account folder an entry point works with. Tests inject
|
|
162
|
+
* `client` (a fake with callToolJson) and `baseDir`; the CLI passes neither and
|
|
163
|
+
* gets the .env-configured client with cwd as the account folder.
|
|
164
|
+
*/
|
|
165
|
+
async function resolveClient({ client, profile, baseDir } = {}) {
|
|
166
|
+
if (client) return { client, baseDir: baseDir || process.cwd() };
|
|
167
|
+
const resolved = await clientFromCwd({ profileOverride: profile });
|
|
168
|
+
return { client: resolved.client, baseDir: baseDir || resolved.baseDir };
|
|
169
|
+
}
|
|
170
|
+
|
|
131
171
|
export async function readProjectsManifest(baseDir) {
|
|
132
172
|
const data = await readJson(join(baseDir, PROJECTS_MANIFEST));
|
|
133
173
|
return data && typeof data === 'object' ? data : { projects: {} };
|
|
@@ -137,45 +177,202 @@ export async function writeProjectsManifest(baseDir, manifest) {
|
|
|
137
177
|
await writeFileAtomic(join(baseDir, PROJECTS_MANIFEST), JSON.stringify(manifest, null, 2));
|
|
138
178
|
}
|
|
139
179
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
180
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
181
|
+
|
|
182
|
+
// sites_list and list_projects both cap `limit` at 100 server-side.
|
|
183
|
+
const LIST_PAGE_LIMIT = 100;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Two id spaces, two tool families (S2, confirmed live 2026-09-10):
|
|
187
|
+
*
|
|
188
|
+
* website_projects — the code this CLI pulls, pushes and deploys.
|
|
189
|
+
* Read with `project_get` (one) and `sites_list` (all).
|
|
190
|
+
* pm_projects — project-MANAGEMENT rows (tasks, owners, status), with
|
|
191
|
+
* project_type seo|ppc|marketing|website|... and their
|
|
192
|
+
* own ids. Read with `get_project` and `list_projects`.
|
|
193
|
+
* A PM row may point at a website project through
|
|
194
|
+
* `website_project_id` / `linked_site`.
|
|
195
|
+
*
|
|
196
|
+
* Until 0.3.3 resolveProject used the PM tools for both shapes: a website
|
|
197
|
+
* UUID got get_project's 404 back as the "project", and a slug got a PM row
|
|
198
|
+
* whose id then 404'd on every file call and whose project_type "website"
|
|
199
|
+
* stamped an "Unknown project_type" RUN.md. Every lookup below lands on a
|
|
200
|
+
* website_projects row, which carries the project_type and external_platform
|
|
201
|
+
* that refuseExternal reads.
|
|
202
|
+
*/
|
|
203
|
+
|
|
204
|
+
/** Does this error (thrown or returned) mean the row does not exist, as opposed to any other failure? */
|
|
205
|
+
function isNotFoundText(text) {
|
|
206
|
+
return /"status"\s*:\s*404\b|\bproject not found\b|Olympus API returned 404\b/i.test(String(text || ''));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* One lookup call that answers null for "no such row" instead of throwing.
|
|
211
|
+
* The MCP proxy flags an upstream 404 with isError, which callTool turns into
|
|
212
|
+
* a thrown Error carrying the status; older servers handed the error body
|
|
213
|
+
* back as a completed call with no `data`. Both read as null here. Anything
|
|
214
|
+
* else (network, auth, 500) still throws.
|
|
215
|
+
*/
|
|
216
|
+
async function lookupRow(client, toolName, args) {
|
|
217
|
+
let body;
|
|
218
|
+
try {
|
|
219
|
+
body = await client.callToolJson(toolName, args);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (isNotFoundText(err?.message)) return null;
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
const row = body && typeof body === 'object' && body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
225
|
+
? body.data
|
|
226
|
+
: null;
|
|
227
|
+
if (row && row.id) return row;
|
|
228
|
+
if (body && typeof body === 'object' && body.error !== undefined) {
|
|
229
|
+
if (body.status === 404 || isNotFoundText(body.error)) return null;
|
|
230
|
+
throw new Error(`Tool ${toolName} failed: ${body.error}`);
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Every row of a paginated list tool (sites_list, list_projects), walking `pagination.total_pages`. */
|
|
236
|
+
async function listAllRows(client, toolName) {
|
|
237
|
+
const rows = [];
|
|
238
|
+
let page = 1;
|
|
239
|
+
while (true) {
|
|
240
|
+
const result = await client.callToolJson(toolName, { page, limit: LIST_PAGE_LIMIT });
|
|
241
|
+
const batch = Array.isArray(result?.data) ? result.data : [];
|
|
242
|
+
rows.push(...batch);
|
|
243
|
+
const totalPages = result?.pagination?.total_pages ?? 1;
|
|
244
|
+
if (page >= totalPages || batch.length === 0) break;
|
|
245
|
+
page += 1;
|
|
246
|
+
}
|
|
247
|
+
return rows;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The website project a PM row points at, if any. */
|
|
251
|
+
function linkedWebsiteId(pmRow) {
|
|
252
|
+
if (!pmRow) return null;
|
|
253
|
+
return pmRow.website_project_id || pmRow.linked_site?.id || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const PROJECTS_HINT = 'Run `hiveku-sync projects` to list the website projects on this account.';
|
|
257
|
+
|
|
258
|
+
async function resolveProjectByUuid(client, projectId) {
|
|
259
|
+
const website = await lookupRow(client, 'project_get', { project_id: projectId });
|
|
260
|
+
if (website) return website;
|
|
261
|
+
|
|
262
|
+
// Not a website id. It may be the PM project that tracks one.
|
|
263
|
+
const pmRow = await lookupRow(client, 'get_project', { project_id: projectId });
|
|
264
|
+
const linkedId = linkedWebsiteId(pmRow);
|
|
265
|
+
if (linkedId) {
|
|
266
|
+
const linked = await lookupRow(client, 'project_get', { project_id: linkedId });
|
|
267
|
+
if (linked) return linked;
|
|
268
|
+
throw new Error(
|
|
269
|
+
`${projectId} is the PM project "${pmRow.name || projectId}" (pm_projects), which links website project ${linkedId}, ` +
|
|
270
|
+
`but project_get finds no website project with that id. ${PROJECTS_HINT}`,
|
|
271
|
+
);
|
|
146
272
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (!match) {
|
|
154
|
-
throw new Error(`No project found matching "${slugOrId}"`);
|
|
273
|
+
if (pmRow) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`${projectId} is the PM project "${pmRow.name || projectId}" (pm_projects, the id space of get_project / list_projects) ` +
|
|
276
|
+
`and it links no website project. hiveku-sync pulls code from website projects ` +
|
|
277
|
+
`(website_projects, the id space of project_get / sites_list). ${PROJECTS_HINT}`,
|
|
278
|
+
);
|
|
155
279
|
}
|
|
156
|
-
|
|
280
|
+
throw new Error(
|
|
281
|
+
`No project with id ${projectId}: it is neither a website project (website_projects, the id space of ` +
|
|
282
|
+
`project_get / sites_list) nor a PM project (pm_projects, the id space of get_project / list_projects). ${PROJECTS_HINT}`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Match a slug against website rows: exact subdomain, then exact name
|
|
288
|
+
* (case-insensitive), then the name slugified the way pull-project names the
|
|
289
|
+
* local folder, so the folder name under ./projects/ resolves too.
|
|
290
|
+
*/
|
|
291
|
+
function matchSiteRow(rows, slug) {
|
|
292
|
+
const lower = slug.toLowerCase();
|
|
293
|
+
const slugified = safeSlug(slug);
|
|
294
|
+
return (
|
|
295
|
+
rows.find((row) => String(row.subdomain || '').toLowerCase() === lower) ||
|
|
296
|
+
rows.find((row) => String(row.name || '').toLowerCase() === lower) ||
|
|
297
|
+
rows.find((row) => safeSlug(row.name) === slugified) ||
|
|
298
|
+
null
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function matchPmRow(rows, slug) {
|
|
303
|
+
const lower = slug.toLowerCase();
|
|
304
|
+
return (
|
|
305
|
+
rows.find((row) => String(row.slug || '').toLowerCase() === lower) ||
|
|
306
|
+
rows.find((row) => String(row.name || '').toLowerCase() === lower) ||
|
|
307
|
+
null
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function resolveProjectBySlug(client, slug) {
|
|
312
|
+
const sites = await listAllRows(client, 'sites_list');
|
|
313
|
+
const site = matchSiteRow(sites, slug);
|
|
314
|
+
if (site) return site;
|
|
315
|
+
|
|
316
|
+
// Fall back to the PM project of that slug/name and follow its link.
|
|
317
|
+
const pmRows = await listAllRows(client, 'list_projects');
|
|
318
|
+
const pmRow = matchPmRow(pmRows, slug);
|
|
319
|
+
const linkedId = linkedWebsiteId(pmRow);
|
|
320
|
+
if (linkedId) {
|
|
321
|
+
const linked = await lookupRow(client, 'project_get', { project_id: linkedId });
|
|
322
|
+
if (linked) return linked;
|
|
323
|
+
throw new Error(
|
|
324
|
+
`"${slug}" is the PM project "${pmRow.name || slug}" (pm_projects), which links website project ${linkedId}, ` +
|
|
325
|
+
`but project_get finds no website project with that id. ${PROJECTS_HINT}`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (pmRow) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`"${slug}" is the PM project "${pmRow.name || slug}" (pm_projects) and it links no website project. ` +
|
|
331
|
+
`hiveku-sync pulls code from website projects (website_projects, sites_list). ${PROJECTS_HINT}`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
throw new Error(
|
|
335
|
+
`No website project matching "${slug}": nothing in sites_list has that subdomain or name, and no PM project ` +
|
|
336
|
+
`(list_projects) with that slug or name links a website project. ${PROJECTS_HINT}`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Resolve a website project (website_projects row) from a UUID or a slug.
|
|
342
|
+
* A UUID is tried as a website id (project_get) and then as a PM id
|
|
343
|
+
* (get_project → website_project_id → project_get). A slug is matched against
|
|
344
|
+
* sites_list (subdomain, then name) and then against list_projects
|
|
345
|
+
* (slug, then name) → its linked website project. The row returned always
|
|
346
|
+
* carries project_type and external_platform for refuseExternal.
|
|
347
|
+
*/
|
|
348
|
+
export async function resolveProject(client, slugOrId) {
|
|
349
|
+
if (UUID_RE.test(slugOrId)) return resolveProjectByUuid(client, slugOrId);
|
|
350
|
+
return resolveProjectBySlug(client, slugOrId);
|
|
157
351
|
}
|
|
158
352
|
|
|
159
|
-
export async function runProjectsList({ profile } = {}) {
|
|
160
|
-
const { client } = await
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
console.log('No projects on this account.');
|
|
353
|
+
export async function runProjectsList({ profile, client: injectedClient } = {}) {
|
|
354
|
+
const { client } = await resolveClient({ client: injectedClient, profile });
|
|
355
|
+
const sites = await listAllRows(client, 'sites_list');
|
|
356
|
+
if (sites.length === 0) {
|
|
357
|
+
console.log('No website projects on this account.');
|
|
165
358
|
return;
|
|
166
359
|
}
|
|
167
|
-
console.log(`${
|
|
168
|
-
for (const
|
|
169
|
-
const type =
|
|
170
|
-
const
|
|
171
|
-
|
|
360
|
+
console.log(`${sites.length} website project${sites.length !== 1 ? 's' : ''}:\n`);
|
|
361
|
+
for (const site of sites) {
|
|
362
|
+
const type = site.project_type || '?';
|
|
363
|
+
const handle = site.subdomain || site.name || site.id;
|
|
364
|
+
const nameColumn = site.name && site.name !== handle ? ` ${site.name}` : '';
|
|
365
|
+
console.log(` ${handle.padEnd(32)} ${type.padEnd(14)} ${site.id}${nameColumn}`);
|
|
172
366
|
}
|
|
173
367
|
}
|
|
174
368
|
|
|
175
|
-
export async function runPullProject({ slugOrId, profile, branch }) {
|
|
369
|
+
export async function runPullProject({ slugOrId, profile, branch, client: injectedClient, baseDir: baseDirOverride }) {
|
|
176
370
|
if (!slugOrId) throw new Error('Provide a project slug or UUID');
|
|
177
|
-
const { client, baseDir } = await
|
|
371
|
+
const { client, baseDir } = await resolveClient({ client: injectedClient, profile, baseDir: baseDirOverride });
|
|
178
372
|
const project = await resolveProject(client, slugOrId);
|
|
373
|
+
// An external site has no file tree: the pull would write nothing and stamp
|
|
374
|
+
// a RUN.md that says "unknown project type". Refuse before the first file call.
|
|
375
|
+
refuseExternal(project);
|
|
179
376
|
const projectId = project.id;
|
|
180
377
|
const localSlug = safeSlug(project.slug || project.name || projectId);
|
|
181
378
|
const dest = join(baseDir, PROJECTS_DIR, localSlug);
|
|
@@ -259,6 +456,7 @@ export async function runPullProject({ slugOrId, profile, branch }) {
|
|
|
259
456
|
project_id: projectId,
|
|
260
457
|
name: project.name || project.slug || localSlug,
|
|
261
458
|
project_type: project.project_type || null,
|
|
459
|
+
external_platform: project.external_platform || null,
|
|
262
460
|
branch: branch || null,
|
|
263
461
|
last_pull_at: new Date().toISOString(),
|
|
264
462
|
};
|
|
@@ -301,16 +499,17 @@ function summarizeDiff(diff) {
|
|
|
301
499
|
};
|
|
302
500
|
}
|
|
303
501
|
|
|
304
|
-
export async function runProjectStatus({ slug, profile, json = false }) {
|
|
502
|
+
export async function runProjectStatus({ slug, profile, json = false, client: injectedClient, baseDir: baseDirOverride }) {
|
|
305
503
|
// Resolve baseDir as the account folder. If we're inside ./projects/<x>/,
|
|
306
504
|
// walk up two levels. Otherwise treat cwd as the account folder.
|
|
307
505
|
const cwd = process.cwd();
|
|
308
506
|
const projectsIdx = cwd.indexOf(`${sep}${PROJECTS_DIR}${sep}`);
|
|
309
|
-
const
|
|
507
|
+
const detectedBaseDir =
|
|
310
508
|
projectsIdx > -1 ? cwd.slice(0, projectsIdx) : cwd;
|
|
311
509
|
|
|
312
|
-
const { client } = await
|
|
510
|
+
const { client, baseDir } = await resolveClient({ client: injectedClient, profile, baseDir: baseDirOverride || detectedBaseDir });
|
|
313
511
|
const { slug: resolvedSlug, info, folder } = await findProjectFolder({ slug, baseDir });
|
|
512
|
+
refuseExternal(info);
|
|
314
513
|
|
|
315
514
|
if (!existsSync(folder)) {
|
|
316
515
|
throw new Error(`Project folder ${folder} doesn't exist — run pull-project first.`);
|
|
@@ -387,6 +586,8 @@ export async function runProjectPush({
|
|
|
387
586
|
profile,
|
|
388
587
|
includeDeletes = false,
|
|
389
588
|
dryRun = false,
|
|
589
|
+
client: injectedClient,
|
|
590
|
+
baseDir: baseDirOverride,
|
|
390
591
|
}) {
|
|
391
592
|
if (!message || !message.trim()) {
|
|
392
593
|
throw new Error('Provide a commit message via -m "..."');
|
|
@@ -394,10 +595,11 @@ export async function runProjectPush({
|
|
|
394
595
|
|
|
395
596
|
const cwd = process.cwd();
|
|
396
597
|
const projectsIdx = cwd.indexOf(`${sep}${PROJECTS_DIR}${sep}`);
|
|
397
|
-
const
|
|
598
|
+
const detectedBaseDir = projectsIdx > -1 ? cwd.slice(0, projectsIdx) : cwd;
|
|
398
599
|
|
|
399
|
-
const { client } = await
|
|
600
|
+
const { client, baseDir } = await resolveClient({ client: injectedClient, profile, baseDir: baseDirOverride || detectedBaseDir });
|
|
400
601
|
const { slug: resolvedSlug, info, folder } = await findProjectFolder({ slug, baseDir });
|
|
602
|
+
refuseExternal(info);
|
|
401
603
|
|
|
402
604
|
if (!existsSync(folder)) {
|
|
403
605
|
throw new Error(`Project folder ${folder} doesn't exist — run pull-project first.`);
|
package/src/push.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { join, relative, sep } from 'node:path';
|
|
|
28
28
|
import {
|
|
29
29
|
clientFromCwd,
|
|
30
30
|
findProjectFolder,
|
|
31
|
+
refuseExternal,
|
|
31
32
|
resolveProject,
|
|
32
33
|
walkLocalFiles,
|
|
33
34
|
readProjectsManifest,
|
|
@@ -225,10 +226,13 @@ export async function computePushPlan({ client, projectId, folder, includeDelete
|
|
|
225
226
|
|
|
226
227
|
// Thin CLI wrapper: resolve the MCP client + project folder from cwd, then run
|
|
227
228
|
// the (fully testable) orchestration in pushProject.
|
|
228
|
-
export async function runPush({ slug, profile, ...opts } = {}) {
|
|
229
|
-
const baseDir = resolveBaseDir();
|
|
230
|
-
const
|
|
229
|
+
export async function runPush({ slug, profile, client: injectedClient, baseDir: baseDirOverride, ...opts } = {}) {
|
|
230
|
+
const baseDir = baseDirOverride || resolveBaseDir();
|
|
231
|
+
const client = injectedClient || (await clientFromCwd({ profileOverride: profile })).client;
|
|
231
232
|
const { slug: resolvedSlug, info, folder } = await findProjectFolder({ slug, baseDir });
|
|
233
|
+
// An external site (Webflow, WordPress, any tracked host) has no deploy
|
|
234
|
+
// source to push to; refuse before the diff.
|
|
235
|
+
refuseExternal(info);
|
|
232
236
|
if (!existsSync(folder)) {
|
|
233
237
|
throw new Error(`Project folder ${folder} doesn't exist — run pull-project first.`);
|
|
234
238
|
}
|
|
@@ -500,22 +504,32 @@ async function runDeployFor({ client, projectId, slug, environment, wait }) {
|
|
|
500
504
|
return { deployment_id: deploymentId, environment: env, status: last, waited: true, ok: false };
|
|
501
505
|
}
|
|
502
506
|
|
|
503
|
-
export async function runDeploy({ slugOrId, environment, wait = false, profile }) {
|
|
507
|
+
export async function runDeploy({ slugOrId, environment, wait = false, profile, client: injectedClient, baseDir: baseDirOverride }) {
|
|
504
508
|
if (!environment) throw new Error('Usage: hiveku-sync deploy <development|staging|production> [<slug>] [--wait]');
|
|
505
|
-
const baseDir = resolveBaseDir();
|
|
506
|
-
const
|
|
509
|
+
const baseDir = baseDirOverride || resolveBaseDir();
|
|
510
|
+
const client = injectedClient || (await clientFromCwd({ profileOverride: profile })).client;
|
|
507
511
|
|
|
508
512
|
// Resolve project: prefer the pulled-folder manifest (slug), else treat the
|
|
509
|
-
// arg as a slug/UUID via
|
|
513
|
+
// arg as a slug/UUID via resolveProject (project_get / sites_list, the
|
|
514
|
+
// website_projects id space - not the PM tools). Either way an external site has
|
|
515
|
+
// nothing to deploy (deploy_site 400s on project_type external) - refuse
|
|
516
|
+
// with the pointer to where it IS managed.
|
|
510
517
|
let projectId;
|
|
511
518
|
let slug = slugOrId;
|
|
519
|
+
let found;
|
|
512
520
|
try {
|
|
513
|
-
|
|
514
|
-
projectId = info.project_id;
|
|
515
|
-
slug = s;
|
|
521
|
+
found = await findProjectFolder({ slug: slugOrId, baseDir });
|
|
516
522
|
} catch {
|
|
523
|
+
found = null;
|
|
524
|
+
}
|
|
525
|
+
if (found) {
|
|
526
|
+
refuseExternal(found.info);
|
|
527
|
+
projectId = found.info.project_id;
|
|
528
|
+
slug = found.slug;
|
|
529
|
+
} else {
|
|
517
530
|
if (!slugOrId) throw new Error('No project folder detected — pass a project slug or UUID: hiveku-sync deploy <env> <slug>');
|
|
518
531
|
const project = await resolveProject(client, slugOrId);
|
|
532
|
+
refuseExternal(project);
|
|
519
533
|
projectId = project.id;
|
|
520
534
|
slug = project.slug || project.name || project.id;
|
|
521
535
|
}
|
|
@@ -587,8 +601,12 @@ export async function runPullEnv({ slug, profile, target = 'development' } = {})
|
|
|
587
601
|
|
|
588
602
|
const res = (await client.callToolJson('project_secrets_list', { project_id: projectId })).data;
|
|
589
603
|
const secrets = res?.secrets || {};
|
|
604
|
+
// Variables marked sensitive are write-only on the platform: the server OMITS them
|
|
605
|
+
// from `secrets` and lists their names here. Without saying so, the .env file below
|
|
606
|
+
// would be silently incomplete and the user would go debugging a phantom.
|
|
607
|
+
const sensitiveKeys = Array.isArray(res?.sensitive_keys) ? res.sensitive_keys : [];
|
|
590
608
|
const count = Object.keys(secrets).length;
|
|
591
|
-
if (count === 0) {
|
|
609
|
+
if (count === 0 && sensitiveKeys.length === 0) {
|
|
592
610
|
console.log(`No secrets set on ${resolvedSlug}. Nothing to write.`);
|
|
593
611
|
return { written: 0 };
|
|
594
612
|
}
|
|
@@ -614,5 +632,12 @@ export async function runPullEnv({ slug, profile, target = 'development' } = {})
|
|
|
614
632
|
}
|
|
615
633
|
|
|
616
634
|
console.log(`Wrote ${Object.keys(resolved).length} var${Object.keys(resolved).length !== 1 ? 's' : ''} → ${outName} (tier: ${tier}, from ${count} secret${count !== 1 ? 's' : ''}).`);
|
|
617
|
-
|
|
635
|
+
if (sensitiveKeys.length > 0) {
|
|
636
|
+
const names = sensitiveKeys.slice().sort();
|
|
637
|
+
console.log(
|
|
638
|
+
`Skipped ${names.length} sensitive var${names.length !== 1 ? 's' : ''}: ${names.join(', ')}.`,
|
|
639
|
+
);
|
|
640
|
+
console.log('These are write-only on Hiveku and cannot be read back. Set them by hand if your local build needs them.');
|
|
641
|
+
}
|
|
642
|
+
return { written: Object.keys(resolved).length, skippedSensitive: sensitiveKeys, path: outPath, tier };
|
|
618
643
|
}
|