@407dev/cli 0.1.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/dist/index.js ADDED
@@ -0,0 +1,4289 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs10 from "fs";
5
+ import { fileURLToPath as fileURLToPath2 } from "url";
6
+ import { parseArgs } from "util";
7
+
8
+ // src/extract/check.ts
9
+ import fs4 from "fs";
10
+ import path4 from "path";
11
+
12
+ // src/extract/collect.ts
13
+ import ts5 from "typescript";
14
+
15
+ // src/extract/collection-schema.ts
16
+ import ts4 from "typescript";
17
+
18
+ // src/extract/resolve.ts
19
+ import fs2 from "fs";
20
+ import path2 from "path";
21
+ import ts2 from "typescript";
22
+
23
+ // src/extract/field-registry.ts
24
+ import { listFieldTypes, resolveTypeAlias } from "@407dev/field-types";
25
+ function knownFieldMethodNames() {
26
+ const types = listFieldTypes();
27
+ const names = /* @__PURE__ */ new Set();
28
+ for (const def of types) {
29
+ names.add(def.id);
30
+ if (def.aliases) {
31
+ for (const alias of def.aliases) {
32
+ names.add(alias);
33
+ }
34
+ }
35
+ }
36
+ return names;
37
+ }
38
+ function normalizeFieldType(methodName) {
39
+ return resolveTypeAlias(methodName);
40
+ }
41
+
42
+ // src/extract/parse.ts
43
+ import fs from "fs";
44
+ import path from "path";
45
+ import { convertToTSX } from "@astrojs/compiler";
46
+ import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
47
+ import ts from "typescript";
48
+ async function parseFile(filePath, rootPath = process.cwd()) {
49
+ const content = fs.readFileSync(filePath, "utf-8");
50
+ const relativePath = path.relative(rootPath, filePath).replace(/\\/g, "/");
51
+ const isAstro = filePath.endsWith(".astro");
52
+ if (isAstro) {
53
+ const { code, map } = await convertToTSX(content, {
54
+ sourcemap: "both"
55
+ });
56
+ const traceMap = map ? new TraceMap(map) : void 0;
57
+ const sourceFile2 = ts.createSourceFile(
58
+ filePath,
59
+ code,
60
+ ts.ScriptTarget.Latest,
61
+ true,
62
+ ts.ScriptKind.TSX
63
+ );
64
+ const getLocation2 = (node) => {
65
+ const pos = node.getStart(sourceFile2);
66
+ const { line, character } = sourceFile2.getLineAndCharacterOfPosition(pos);
67
+ if (traceMap) {
68
+ const orig = originalPositionFor(traceMap, {
69
+ line: line + 1,
70
+ column: character
71
+ });
72
+ if (orig && orig.line !== null && orig.column !== null) {
73
+ return {
74
+ file: relativePath,
75
+ line: orig.line,
76
+ column: orig.column + 1
77
+ };
78
+ }
79
+ }
80
+ return {
81
+ file: relativePath,
82
+ line: line + 1,
83
+ column: character + 1
84
+ };
85
+ };
86
+ return {
87
+ filePath,
88
+ relativePath,
89
+ isAstro: true,
90
+ sourceFile: sourceFile2,
91
+ traceMap,
92
+ getLocation: getLocation2
93
+ };
94
+ }
95
+ const isTsx = filePath.endsWith(".tsx") || filePath.endsWith(".jsx");
96
+ const scriptKind = isTsx ? ts.ScriptKind.TSX : filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS;
97
+ const sourceFile = ts.createSourceFile(
98
+ filePath,
99
+ content,
100
+ ts.ScriptTarget.Latest,
101
+ true,
102
+ scriptKind
103
+ );
104
+ const getLocation = (node) => {
105
+ const pos = node.getStart(sourceFile);
106
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
107
+ return {
108
+ file: relativePath,
109
+ line: line + 1,
110
+ column: character + 1
111
+ };
112
+ };
113
+ return {
114
+ filePath,
115
+ relativePath,
116
+ isAstro: false,
117
+ sourceFile,
118
+ getLocation
119
+ };
120
+ }
121
+ function globSourceFiles(dir) {
122
+ const files = [];
123
+ function walk(currentDir) {
124
+ if (!fs.existsSync(currentDir)) return;
125
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
126
+ for (const entry of entries) {
127
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") {
128
+ continue;
129
+ }
130
+ const full = path.join(currentDir, entry.name);
131
+ if (entry.isDirectory()) {
132
+ walk(full);
133
+ } else if (/\.(astro|ts|tsx|js|jsx)$/.test(entry.name) && !entry.name.endsWith(".d.ts") && !entry.name.includes(".test.") && !entry.name.includes(".spec.")) {
134
+ files.push(full);
135
+ }
136
+ }
137
+ }
138
+ walk(dir);
139
+ return files.sort();
140
+ }
141
+
142
+ // src/extract/resolve.ts
143
+ function parseBareSpecifier(specifier) {
144
+ if (specifier.startsWith("@")) {
145
+ const parts2 = specifier.split("/");
146
+ const packageName2 = parts2.slice(0, 2).join("/");
147
+ const subpathParts2 = parts2.slice(2);
148
+ const subpath2 = subpathParts2.length > 0 ? `./${subpathParts2.join("/")}` : ".";
149
+ return { packageName: packageName2, subpath: subpath2 };
150
+ }
151
+ const parts = specifier.split("/");
152
+ const packageName = parts[0];
153
+ const subpathParts = parts.slice(1);
154
+ const subpath = subpathParts.length > 0 ? `./${subpathParts.join("/")}` : ".";
155
+ return { packageName, subpath };
156
+ }
157
+ function findPackageRoot(fromFile, packageName) {
158
+ let currentDir = path2.dirname(path2.resolve(fromFile));
159
+ const root = path2.parse(currentDir).root;
160
+ while (true) {
161
+ const candidate = path2.join(currentDir, "node_modules", ...packageName.split("/"));
162
+ if (fs2.existsSync(candidate) && fs2.existsSync(path2.join(candidate, "package.json"))) {
163
+ return candidate;
164
+ }
165
+ if (currentDir === root) {
166
+ break;
167
+ }
168
+ currentDir = path2.dirname(currentDir);
169
+ }
170
+ return null;
171
+ }
172
+ function resolvePackageExport(pkgRoot, subpath) {
173
+ try {
174
+ const pkgJsonPath = path2.join(pkgRoot, "package.json");
175
+ if (!fs2.existsSync(pkgJsonPath)) {
176
+ return { resolvedPath: null, hasSource: false, exportFound: false };
177
+ }
178
+ const pkgJson = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
179
+ if (pkgJson.exports) {
180
+ const exportsObj = pkgJson.exports;
181
+ let target = exportsObj[subpath];
182
+ let matchedSub = null;
183
+ if (!target && typeof exportsObj === "object" && exportsObj !== null) {
184
+ let bestPrefix = "";
185
+ let bestKey = null;
186
+ for (const key of Object.keys(exportsObj)) {
187
+ if (key.includes("*")) {
188
+ const starIdx = key.indexOf("*");
189
+ const prefix = key.slice(0, starIdx);
190
+ const suffix = key.slice(starIdx + 1);
191
+ if (subpath.startsWith(prefix) && subpath.endsWith(suffix) && subpath.length >= prefix.length + suffix.length) {
192
+ if (prefix.length >= bestPrefix.length) {
193
+ bestPrefix = prefix;
194
+ bestKey = key;
195
+ }
196
+ }
197
+ }
198
+ }
199
+ if (bestKey !== null) {
200
+ const starIdx = bestKey.indexOf("*");
201
+ const prefix = bestKey.slice(0, starIdx);
202
+ const suffix = bestKey.slice(starIdx + 1);
203
+ matchedSub = subpath.slice(prefix.length, subpath.length - suffix.length);
204
+ target = exportsObj[bestKey];
205
+ }
206
+ }
207
+ if (!target && subpath === ".") {
208
+ if (typeof exportsObj === "string" || exportsObj.source || exportsObj.import || exportsObj.default) {
209
+ target = exportsObj;
210
+ }
211
+ }
212
+ if (!target) {
213
+ return { resolvedPath: null, hasSource: false, exportFound: false };
214
+ }
215
+ const substitute = (val) => matchedSub !== null ? val.replaceAll("*", matchedSub) : val;
216
+ if (typeof target === "string") {
217
+ return {
218
+ resolvedPath: path2.resolve(pkgRoot, substitute(target)),
219
+ hasSource: true,
220
+ exportFound: true
221
+ };
222
+ }
223
+ if (typeof target === "object" && target !== null) {
224
+ if (typeof target.source === "string") {
225
+ return {
226
+ resolvedPath: path2.resolve(pkgRoot, substitute(target.source)),
227
+ hasSource: true,
228
+ exportFound: true
229
+ };
230
+ }
231
+ return { resolvedPath: null, hasSource: false, exportFound: true };
232
+ }
233
+ }
234
+ if (subpath === "." && typeof pkgJson.source === "string") {
235
+ return {
236
+ resolvedPath: path2.resolve(pkgRoot, pkgJson.source),
237
+ hasSource: true,
238
+ exportFound: true
239
+ };
240
+ }
241
+ } catch {
242
+ }
243
+ return { resolvedPath: null, hasSource: false, exportFound: false };
244
+ }
245
+ function resolveModulePath(fromFile, importSpecifier) {
246
+ if (importSpecifier.startsWith(".")) {
247
+ const dir = path2.dirname(fromFile);
248
+ const resolvedBase = path2.resolve(dir, importSpecifier);
249
+ const extensions = [".ts", ".tsx", ".js", ".jsx", ".astro", "/index.ts", "/index.js"];
250
+ for (const ext of extensions) {
251
+ const candidate = resolvedBase + ext;
252
+ if (fs2.existsSync(candidate) && fs2.statSync(candidate).isFile()) {
253
+ return candidate;
254
+ }
255
+ }
256
+ if (fs2.existsSync(resolvedBase) && fs2.statSync(resolvedBase).isFile()) {
257
+ return resolvedBase;
258
+ }
259
+ return null;
260
+ }
261
+ const { packageName, subpath } = parseBareSpecifier(importSpecifier);
262
+ const pkgRoot = findPackageRoot(fromFile, packageName);
263
+ if (!pkgRoot) {
264
+ return null;
265
+ }
266
+ const result = resolvePackageExport(pkgRoot, subpath);
267
+ if (result.hasSource && result.resolvedPath && fs2.existsSync(result.resolvedPath)) {
268
+ return result.resolvedPath;
269
+ }
270
+ return null;
271
+ }
272
+ function getStringLiteralValue(node) {
273
+ if (ts2.isStringLiteral(node) || ts2.isNoSubstitutionTemplateLiteral(node)) {
274
+ return node.text;
275
+ }
276
+ return null;
277
+ }
278
+ function shapeValueMethodName(node) {
279
+ const target = ts2.isCallExpression(node) ? node.expression : node;
280
+ if (ts2.isPropertyAccessExpression(target)) {
281
+ return target.name.text;
282
+ }
283
+ return null;
284
+ }
285
+ function extractGroupFromCall(callExpr, fallbackName, sf, diagnostics = [], getLocation) {
286
+ const args = callExpr.arguments;
287
+ let groupName = fallbackName;
288
+ let shapeNode = null;
289
+ if (args.length === 1) {
290
+ shapeNode = args[0];
291
+ } else if (args.length >= 2) {
292
+ const literalName = getStringLiteralValue(args[0]);
293
+ if (literalName) {
294
+ groupName = literalName;
295
+ }
296
+ shapeNode = args[1];
297
+ }
298
+ if (shapeNode && ts2.isObjectLiteralExpression(shapeNode)) {
299
+ const known = knownFieldMethodNames();
300
+ const shape = {};
301
+ for (const prop of shapeNode.properties) {
302
+ if (ts2.isPropertyAssignment(prop)) {
303
+ const propName = prop.name.getText(sf);
304
+ const methodName = shapeValueMethodName(prop.initializer);
305
+ if (methodName && known.has(methodName)) {
306
+ shape[propName] = normalizeFieldType(methodName);
307
+ } else {
308
+ shape[propName] = "text";
309
+ if (getLocation) {
310
+ diagnostics.push({
311
+ type: "warning",
312
+ message: `Group '${fallbackName}' field '${propName}' does not reference a known field helper (got '${prop.initializer.getText(sf)}'); defaulting to 'text'`,
313
+ file: getLocation(prop).file,
314
+ line: getLocation(prop).line,
315
+ column: getLocation(prop).column
316
+ });
317
+ }
318
+ }
319
+ } else if (ts2.isShorthandPropertyAssignment(prop)) {
320
+ const propName = prop.name.getText(sf);
321
+ shape[propName] = "text";
322
+ }
323
+ }
324
+ return { groupName, shape };
325
+ }
326
+ return null;
327
+ }
328
+ async function checkIsGroupOrCollection(filePath, symbolName, rootPath, cachedParsedFiles, visited = /* @__PURE__ */ new Set()) {
329
+ const visitKey = `${filePath}::${symbolName}`;
330
+ if (visited.has(visitKey)) return false;
331
+ visited.add(visitKey);
332
+ let parsed = cachedParsedFiles.get(filePath);
333
+ if (!parsed) {
334
+ parsed = await parseFile(filePath, rootPath);
335
+ cachedParsedFiles.set(filePath, parsed);
336
+ }
337
+ for (const stmt of parsed.sourceFile.statements) {
338
+ if (ts2.isVariableStatement(stmt)) {
339
+ for (const decl of stmt.declarationList.declarations) {
340
+ if (decl.name.getText(parsed.sourceFile) === symbolName && decl.initializer) {
341
+ if (ts2.isCallExpression(decl.initializer)) {
342
+ const callText = decl.initializer.expression.getText(parsed.sourceFile);
343
+ if (callText === "f.group" || callText === "defineGroup" || callText.endsWith(".group") || callText === "defineCollection" || callText === "f.defineCollection") {
344
+ return true;
345
+ }
346
+ } else if (ts2.isArrowFunction(decl.initializer) || ts2.isFunctionExpression(decl.initializer)) {
347
+ let found = false;
348
+ ts2.forEachChild(decl.initializer.body, (child) => {
349
+ if (ts2.isCallExpression(child)) {
350
+ const t = child.expression.getText(parsed.sourceFile);
351
+ if (t === "defineCollection" || t === "f.defineCollection") found = true;
352
+ }
353
+ });
354
+ if (found) return true;
355
+ }
356
+ }
357
+ }
358
+ }
359
+ if (ts2.isFunctionDeclaration(stmt) && stmt.name?.text === symbolName) {
360
+ if (stmt.body) {
361
+ let found = false;
362
+ ts2.forEachChild(stmt.body, (child) => {
363
+ if (ts2.isCallExpression(child)) {
364
+ const t = child.expression.getText(parsed.sourceFile);
365
+ if (t === "defineCollection" || t === "f.defineCollection") found = true;
366
+ }
367
+ });
368
+ if (found) return true;
369
+ }
370
+ }
371
+ if (ts2.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
372
+ const specifier = getStringLiteralValue(stmt.moduleSpecifier);
373
+ if (specifier && stmt.exportClause && ts2.isNamedExports(stmt.exportClause)) {
374
+ for (const el of stmt.exportClause.elements) {
375
+ if (el.name.text === symbolName) {
376
+ const targetSymbol = el.propertyName ? el.propertyName.text : el.name.text;
377
+ const targetModulePath = resolveModulePath(parsed.filePath, specifier);
378
+ if (targetModulePath) {
379
+ const res = await checkIsGroupOrCollection(
380
+ targetModulePath,
381
+ targetSymbol,
382
+ rootPath,
383
+ cachedParsedFiles,
384
+ visited
385
+ );
386
+ if (res) return true;
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
392
+ if (ts2.isImportDeclaration(stmt) && stmt.moduleSpecifier) {
393
+ const specifier = getStringLiteralValue(stmt.moduleSpecifier);
394
+ if (specifier && stmt.importClause?.namedBindings) {
395
+ if (ts2.isNamedImports(stmt.importClause.namedBindings)) {
396
+ for (const el of stmt.importClause.namedBindings.elements) {
397
+ if (el.name.text === symbolName) {
398
+ const targetSymbol = el.propertyName ? el.propertyName.text : el.name.text;
399
+ const targetModulePath = resolveModulePath(parsed.filePath, specifier);
400
+ if (targetModulePath) {
401
+ const res = await checkIsGroupOrCollection(
402
+ targetModulePath,
403
+ targetSymbol,
404
+ rootPath,
405
+ cachedParsedFiles,
406
+ visited
407
+ );
408
+ if (res) return true;
409
+ }
410
+ }
411
+ }
412
+ }
413
+ }
414
+ }
415
+ }
416
+ return false;
417
+ }
418
+ async function resolveFileContext(parsed, rootPath = process.cwd(), cachedParsedFiles = /* @__PURE__ */ new Map()) {
419
+ const scopes = /* @__PURE__ */ new Map();
420
+ const groups = /* @__PURE__ */ new Map();
421
+ const groupInstances = /* @__PURE__ */ new Map();
422
+ const collections = /* @__PURE__ */ new Map();
423
+ const diagnostics = [];
424
+ const { sourceFile, getLocation } = parsed;
425
+ const importedSymbols = /* @__PURE__ */ new Map();
426
+ for (const statement of sourceFile.statements) {
427
+ if (ts2.isImportDeclaration(statement)) {
428
+ const moduleSpecifier = getStringLiteralValue(statement.moduleSpecifier);
429
+ if (moduleSpecifier && statement.importClause?.namedBindings) {
430
+ if (ts2.isNamedImports(statement.importClause.namedBindings)) {
431
+ for (const element of statement.importClause.namedBindings.elements) {
432
+ const importName = element.propertyName ? element.propertyName.text : element.name.text;
433
+ const localName = element.name.text;
434
+ importedSymbols.set(localName, {
435
+ importName,
436
+ moduleSpecifier,
437
+ node: statement
438
+ });
439
+ }
440
+ }
441
+ }
442
+ }
443
+ }
444
+ for (const statement of sourceFile.statements) {
445
+ if (ts2.isVariableStatement(statement)) {
446
+ const isExported = statement.modifiers?.some((m) => m.kind === ts2.SyntaxKind.ExportKeyword);
447
+ if (isExported) {
448
+ for (const decl of statement.declarationList.declarations) {
449
+ if (decl.initializer && ts2.isCallExpression(decl.initializer)) {
450
+ const calleeText = decl.initializer.expression.getText(sourceFile);
451
+ const importInfo = importedSymbols.get(calleeText);
452
+ if (importInfo && !importInfo.moduleSpecifier.startsWith(".")) {
453
+ const { packageName, subpath } = parseBareSpecifier(importInfo.moduleSpecifier);
454
+ const pkgRoot = findPackageRoot(parsed.filePath, packageName);
455
+ if (pkgRoot) {
456
+ const res = resolvePackageExport(pkgRoot, subpath);
457
+ if (res.exportFound && !res.hasSource) {
458
+ diagnostics.push({
459
+ type: "error",
460
+ message: `Package '${packageName}' export '${subpath}' lacks a 'source' condition required for static extraction`,
461
+ file: parsed.relativePath,
462
+ line: getLocation(decl).line,
463
+ column: getLocation(decl).column
464
+ });
465
+ }
466
+ }
467
+ }
468
+ }
469
+ }
470
+ }
471
+ }
472
+ }
473
+ function walk(node) {
474
+ if (ts2.isVariableDeclaration(node) && node.initializer) {
475
+ const varName = node.name.getText(sourceFile);
476
+ if (ts2.isCallExpression(node.initializer)) {
477
+ const callText = node.initializer.expression.getText(sourceFile);
478
+ if (callText === "f.scope" || callText.endsWith(".scope")) {
479
+ const firstArg = node.initializer.arguments[0];
480
+ if (!firstArg) {
481
+ diagnostics.push({
482
+ type: "error",
483
+ message: `Scope '${varName}' declared without a prefix`,
484
+ file: parsed.relativePath,
485
+ line: getLocation(node).line,
486
+ column: getLocation(node).column
487
+ });
488
+ } else {
489
+ const prefix = getStringLiteralValue(firstArg);
490
+ if (prefix === null) {
491
+ diagnostics.push({
492
+ type: "error",
493
+ message: `Scope prefix for '${varName}' must be a static string literal, got non-literal expression`,
494
+ file: parsed.relativePath,
495
+ line: getLocation(firstArg).line,
496
+ column: getLocation(firstArg).column
497
+ });
498
+ } else {
499
+ if (callText.endsWith(".scope") && callText !== "f.scope") {
500
+ const parentVar = callText.replace(/\.scope$/, "");
501
+ const parentScope = scopes.get(parentVar);
502
+ const fullPrefix = parentScope ? `${parentScope.prefix}.${prefix}` : prefix;
503
+ scopes.set(varName, {
504
+ prefix: fullPrefix,
505
+ location: getLocation(node)
506
+ });
507
+ } else {
508
+ scopes.set(varName, {
509
+ prefix,
510
+ location: getLocation(node)
511
+ });
512
+ }
513
+ }
514
+ }
515
+ }
516
+ if (callText === "f.group" || callText === "defineGroup" || callText.endsWith(".group")) {
517
+ const groupInfo = extractGroupFromCall(
518
+ node.initializer,
519
+ varName,
520
+ sourceFile,
521
+ diagnostics,
522
+ getLocation
523
+ );
524
+ if (groupInfo) {
525
+ groups.set(varName, {
526
+ groupName: groupInfo.groupName,
527
+ shape: groupInfo.shape,
528
+ definedLocation: getLocation(node),
529
+ hopCount: 0
530
+ });
531
+ }
532
+ }
533
+ }
534
+ }
535
+ ts2.forEachChild(node, walk);
536
+ }
537
+ walk(sourceFile);
538
+ for (const [localName, importInfo] of importedSymbols) {
539
+ const resolvedPath = resolveModulePath(parsed.filePath, importInfo.moduleSpecifier);
540
+ if (!resolvedPath) {
541
+ continue;
542
+ }
543
+ let targetParsed = cachedParsedFiles.get(resolvedPath);
544
+ if (!targetParsed) {
545
+ targetParsed = await parseFile(resolvedPath, rootPath);
546
+ cachedParsedFiles.set(resolvedPath, targetParsed);
547
+ }
548
+ for (const stmt of targetParsed.sourceFile.statements) {
549
+ if (ts2.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
550
+ const specifier = getStringLiteralValue(stmt.moduleSpecifier);
551
+ if (specifier && stmt.exportClause && ts2.isNamedExports(stmt.exportClause)) {
552
+ for (const el of stmt.exportClause.elements) {
553
+ if (el.name.text === importInfo.importName) {
554
+ const targetSymbol = el.propertyName ? el.propertyName.text : el.name.text;
555
+ const nextPath = resolveModulePath(targetParsed.filePath, specifier);
556
+ const isGroupOrColl = nextPath ? await checkIsGroupOrCollection(
557
+ nextPath,
558
+ targetSymbol,
559
+ rootPath,
560
+ cachedParsedFiles
561
+ ) : false;
562
+ if (isGroupOrColl) {
563
+ diagnostics.push({
564
+ type: "error",
565
+ message: `Group or collection '${importInfo.importName}' is imported across multiple hops (> 1 hop) via '${importInfo.moduleSpecifier}'. It must be defined in the directly imported module.`,
566
+ file: parsed.relativePath,
567
+ line: getLocation(importInfo.node).line,
568
+ column: getLocation(importInfo.node).column
569
+ });
570
+ }
571
+ }
572
+ }
573
+ }
574
+ }
575
+ if (ts2.isImportDeclaration(stmt) && stmt.moduleSpecifier) {
576
+ const specifier = getStringLiteralValue(stmt.moduleSpecifier);
577
+ if (specifier && stmt.importClause?.namedBindings) {
578
+ if (ts2.isNamedImports(stmt.importClause.namedBindings)) {
579
+ for (const el of stmt.importClause.namedBindings.elements) {
580
+ if (el.name.text === importInfo.importName) {
581
+ const targetSymbol = el.propertyName ? el.propertyName.text : el.name.text;
582
+ const nextPath = resolveModulePath(targetParsed.filePath, specifier);
583
+ const isGroupOrColl = nextPath ? await checkIsGroupOrCollection(
584
+ nextPath,
585
+ targetSymbol,
586
+ rootPath,
587
+ cachedParsedFiles
588
+ ) : false;
589
+ if (isGroupOrColl) {
590
+ diagnostics.push({
591
+ type: "error",
592
+ message: `Group or collection '${importInfo.importName}' is imported across multiple hops (> 1 hop) via '${importInfo.moduleSpecifier}'. It must be defined in the directly imported module.`,
593
+ file: parsed.relativePath,
594
+ line: getLocation(importInfo.node).line,
595
+ column: getLocation(importInfo.node).column
596
+ });
597
+ }
598
+ }
599
+ }
600
+ }
601
+ }
602
+ }
603
+ if (ts2.isVariableStatement(stmt)) {
604
+ for (const decl of stmt.declarationList.declarations) {
605
+ if (decl.name.getText(targetParsed.sourceFile) === importInfo.importName && decl.initializer) {
606
+ if (ts2.isCallExpression(decl.initializer)) {
607
+ const callText = decl.initializer.expression.getText(targetParsed.sourceFile);
608
+ if (callText === "f.group" || callText === "defineGroup" || callText.endsWith(".group")) {
609
+ const groupInfo = extractGroupFromCall(
610
+ decl.initializer,
611
+ importInfo.importName,
612
+ targetParsed.sourceFile,
613
+ diagnostics,
614
+ targetParsed.getLocation
615
+ );
616
+ if (groupInfo) {
617
+ groups.set(localName, {
618
+ groupName: groupInfo.groupName,
619
+ shape: groupInfo.shape,
620
+ definedLocation: targetParsed.getLocation(decl),
621
+ hopCount: 1
622
+ });
623
+ }
624
+ } else if (callText === "defineCollection" || callText === "f.defineCollection") {
625
+ const coll = extractCollectionFromCall(
626
+ decl.initializer,
627
+ targetParsed.sourceFile,
628
+ targetParsed.getLocation,
629
+ diagnostics,
630
+ targetParsed.relativePath
631
+ );
632
+ if (coll) {
633
+ collections.set(localName, coll);
634
+ }
635
+ }
636
+ } else if (ts2.isArrowFunction(decl.initializer) || ts2.isFunctionExpression(decl.initializer)) {
637
+ const findCall = (n) => {
638
+ if (ts2.isCallExpression(n)) {
639
+ const t = n.expression.getText(targetParsed.sourceFile);
640
+ if (t === "defineCollection" || t === "f.defineCollection") return n;
641
+ }
642
+ let res = null;
643
+ ts2.forEachChild(n, (child) => {
644
+ if (!res) res = findCall(child);
645
+ });
646
+ return res;
647
+ };
648
+ const call = findCall(decl.initializer.body);
649
+ if (call) {
650
+ const coll = extractCollectionFromCall(
651
+ call,
652
+ targetParsed.sourceFile,
653
+ targetParsed.getLocation,
654
+ diagnostics,
655
+ targetParsed.relativePath
656
+ );
657
+ if (coll) {
658
+ collections.set(localName, coll);
659
+ }
660
+ }
661
+ }
662
+ }
663
+ }
664
+ }
665
+ if (ts2.isFunctionDeclaration(stmt) && stmt.name?.text === importInfo.importName) {
666
+ const findCall = (n) => {
667
+ if (ts2.isCallExpression(n)) {
668
+ const t = n.expression.getText(targetParsed.sourceFile);
669
+ if (t === "defineCollection" || t === "f.defineCollection") return n;
670
+ }
671
+ let res = null;
672
+ ts2.forEachChild(n, (child) => {
673
+ if (!res) res = findCall(child);
674
+ });
675
+ return res;
676
+ };
677
+ if (stmt.body) {
678
+ const call = findCall(stmt.body);
679
+ if (call) {
680
+ const coll = extractCollectionFromCall(
681
+ call,
682
+ targetParsed.sourceFile,
683
+ targetParsed.getLocation,
684
+ diagnostics,
685
+ targetParsed.relativePath
686
+ );
687
+ if (coll) {
688
+ collections.set(localName, coll);
689
+ }
690
+ }
691
+ }
692
+ }
693
+ }
694
+ }
695
+ function walkInstances(node) {
696
+ if (ts2.isVariableDeclaration(node) && node.initializer) {
697
+ const varName = node.name.getText(sourceFile);
698
+ if (ts2.isCallExpression(node.initializer)) {
699
+ const calleeName = node.initializer.expression.getText(sourceFile);
700
+ const resolvedGroup = groups.get(calleeName);
701
+ if (resolvedGroup) {
702
+ const firstArg = node.initializer.arguments[0];
703
+ if (!firstArg) {
704
+ diagnostics.push({
705
+ type: "error",
706
+ message: `Group '${calleeName}' instantiated without a scope prefix`,
707
+ file: parsed.relativePath,
708
+ line: getLocation(node).line,
709
+ column: getLocation(node).column
710
+ });
711
+ } else {
712
+ const prefix = getStringLiteralValue(firstArg);
713
+ if (prefix === null) {
714
+ diagnostics.push({
715
+ type: "error",
716
+ message: `Group '${calleeName}' instantiated with dynamic non-literal scope`,
717
+ file: parsed.relativePath,
718
+ line: getLocation(firstArg).line,
719
+ column: getLocation(firstArg).column
720
+ });
721
+ } else {
722
+ groupInstances.set(varName, {
723
+ groupName: resolvedGroup.groupName,
724
+ prefix,
725
+ shape: resolvedGroup.shape,
726
+ location: getLocation(node)
727
+ });
728
+ }
729
+ }
730
+ }
731
+ }
732
+ }
733
+ ts2.forEachChild(node, walkInstances);
734
+ }
735
+ walkInstances(sourceFile);
736
+ return {
737
+ scopes,
738
+ groups,
739
+ groupInstances,
740
+ collections,
741
+ diagnostics
742
+ };
743
+ }
744
+
745
+ // src/extract/seed-eval.ts
746
+ import ts3 from "typescript";
747
+ function unwrapTypeAssertions(node) {
748
+ let current = node;
749
+ while (ts3.isAsExpression(current) || ts3.isTypeAssertionExpression(current) || (ts3.isSatisfiesExpression?.(current) ?? false) || ts3.isParenthesizedExpression(current)) {
750
+ current = current.expression;
751
+ }
752
+ return current;
753
+ }
754
+ function evalStaticJson(rawNode) {
755
+ const node = unwrapTypeAssertions(rawNode);
756
+ if (ts3.isStringLiteral(node) || ts3.isNoSubstitutionTemplateLiteral(node)) {
757
+ return { ok: true, value: node.text };
758
+ }
759
+ if (ts3.isNumericLiteral(node)) {
760
+ return { ok: true, value: Number(node.text) };
761
+ }
762
+ if (node.kind === ts3.SyntaxKind.TrueKeyword) {
763
+ return { ok: true, value: true };
764
+ }
765
+ if (node.kind === ts3.SyntaxKind.FalseKeyword) {
766
+ return { ok: true, value: false };
767
+ }
768
+ if (node.kind === ts3.SyntaxKind.NullKeyword) {
769
+ return { ok: true, value: null };
770
+ }
771
+ if (ts3.isPrefixUnaryExpression(node)) {
772
+ const operand = unwrapTypeAssertions(node.operand);
773
+ if (node.operator === ts3.SyntaxKind.MinusToken && ts3.isNumericLiteral(operand)) {
774
+ return { ok: true, value: -Number(operand.text) };
775
+ }
776
+ if (node.operator === ts3.SyntaxKind.PlusToken && ts3.isNumericLiteral(operand)) {
777
+ return { ok: true, value: Number(operand.text) };
778
+ }
779
+ return { ok: false, errorNode: node };
780
+ }
781
+ if (ts3.isArrayLiteralExpression(node)) {
782
+ const arr = [];
783
+ for (const el of node.elements) {
784
+ if (ts3.isSpreadElement(el)) {
785
+ return { ok: false, errorNode: el };
786
+ }
787
+ const res = evalStaticJson(el);
788
+ if (!res.ok) return res;
789
+ arr.push(res.value);
790
+ }
791
+ return { ok: true, value: arr };
792
+ }
793
+ if (ts3.isObjectLiteralExpression(node)) {
794
+ const obj = {};
795
+ for (const prop of node.properties) {
796
+ if (!ts3.isPropertyAssignment(prop)) {
797
+ return { ok: false, errorNode: prop };
798
+ }
799
+ let propName;
800
+ if (ts3.isIdentifier(prop.name) || ts3.isStringLiteral(prop.name) || ts3.isNoSubstitutionTemplateLiteral(prop.name)) {
801
+ propName = prop.name.text;
802
+ } else if (ts3.isNumericLiteral(prop.name)) {
803
+ propName = prop.name.text;
804
+ } else {
805
+ return { ok: false, errorNode: prop.name };
806
+ }
807
+ const valRes = evalStaticJson(prop.initializer);
808
+ if (!valRes.ok) return valRes;
809
+ obj[propName] = valRes.value;
810
+ }
811
+ return { ok: true, value: obj };
812
+ }
813
+ return { ok: false, errorNode: node };
814
+ }
815
+ function extractCollectionSeed(rawSeedNode, sourceFile, getLocation, relativePath, diagnostics) {
816
+ const seedNode = unwrapTypeAssertions(rawSeedNode);
817
+ let targetArrayNode;
818
+ if (ts3.isIdentifier(seedNode)) {
819
+ let walk2 = function(node) {
820
+ if (found) return;
821
+ if (ts3.isVariableStatement(node)) {
822
+ const hasConst = Boolean(node.declarationList.flags & ts3.NodeFlags.Const);
823
+ for (const decl of node.declarationList.declarations) {
824
+ if (ts3.isIdentifier(decl.name) && decl.name.text === idName) {
825
+ found = true;
826
+ isConst = hasConst;
827
+ declInit = decl.initializer;
828
+ return;
829
+ }
830
+ }
831
+ }
832
+ ts3.forEachChild(node, walk2);
833
+ };
834
+ var walk = walk2;
835
+ const idName = seedNode.text;
836
+ let found = false;
837
+ let isConst = false;
838
+ let declInit;
839
+ walk2(sourceFile);
840
+ const unwrappedDeclInit = declInit ? unwrapTypeAssertions(declInit) : void 0;
841
+ if (!found || !isConst || !unwrappedDeclInit || !ts3.isArrayLiteralExpression(unwrappedDeclInit)) {
842
+ diagnostics.push({
843
+ type: "error",
844
+ message: "seed must be a literal",
845
+ file: relativePath,
846
+ line: getLocation(
847
+ declInit && !ts3.isArrayLiteralExpression(unwrappedDeclInit ?? declInit) ? declInit : seedNode
848
+ ).line,
849
+ column: getLocation(
850
+ declInit && !ts3.isArrayLiteralExpression(unwrappedDeclInit ?? declInit) ? declInit : seedNode
851
+ ).column
852
+ });
853
+ return void 0;
854
+ }
855
+ targetArrayNode = unwrappedDeclInit;
856
+ } else if (ts3.isArrayLiteralExpression(seedNode)) {
857
+ targetArrayNode = seedNode;
858
+ } else {
859
+ diagnostics.push({
860
+ type: "error",
861
+ message: "seed must be a literal",
862
+ file: relativePath,
863
+ line: getLocation(seedNode).line,
864
+ column: getLocation(seedNode).column
865
+ });
866
+ return void 0;
867
+ }
868
+ const evalResult = evalStaticJson(targetArrayNode);
869
+ if (!evalResult.ok || !Array.isArray(evalResult.value)) {
870
+ const errNode = evalResult.errorNode || targetArrayNode;
871
+ diagnostics.push({
872
+ type: "error",
873
+ message: "seed must be a literal",
874
+ file: relativePath,
875
+ line: getLocation(errNode).line,
876
+ column: getLocation(errNode).column
877
+ });
878
+ return void 0;
879
+ }
880
+ return evalResult.value;
881
+ }
882
+
883
+ // src/extract/collection-schema.ts
884
+ function parseZodObjectSchema(schemaNode, sourceFile, bodyField) {
885
+ const properties = {};
886
+ const required = [];
887
+ function processPropAssignment(name, initNode) {
888
+ const initText = initNode.getText(sourceFile);
889
+ let propSchema = { type: "string" };
890
+ if (name === bodyField) {
891
+ propSchema = { type: "object", "x-cms-type": "richtext" };
892
+ } else if (initText.includes("number()")) {
893
+ propSchema = { type: "number" };
894
+ } else if (initText.includes("boolean()")) {
895
+ propSchema = { type: "boolean" };
896
+ } else if (initText.includes("array(")) {
897
+ propSchema = { type: "array", items: { type: "string" } };
898
+ } else if (initText.includes("richtext") || initText.includes("richText")) {
899
+ propSchema = { type: "object", "x-cms-type": "richtext" };
900
+ } else if (initText.includes("streamVideo(")) {
901
+ propSchema = { type: "object", "x-cms-type": "media.streamVideo" };
902
+ } else if (initText.includes("image(") || initText.includes("video(") || initText.includes("asset(")) {
903
+ propSchema = { type: "string", format: "cms-asset-ref" };
904
+ }
905
+ properties[name] = propSchema;
906
+ if (!initText.includes(".optional()")) {
907
+ required.push(name);
908
+ }
909
+ }
910
+ if (ts4.isCallExpression(schemaNode)) {
911
+ const callText = schemaNode.expression.getText(sourceFile);
912
+ if (callText === "z.object" && schemaNode.arguments[0]) {
913
+ const objArg = schemaNode.arguments[0];
914
+ if (ts4.isObjectLiteralExpression(objArg)) {
915
+ for (const prop of objArg.properties) {
916
+ if (ts4.isPropertyAssignment(prop)) {
917
+ processPropAssignment(prop.name.getText(sourceFile), prop.initializer);
918
+ }
919
+ }
920
+ }
921
+ }
922
+ }
923
+ return {
924
+ jsonSchema: {
925
+ $schema: "https://json-schema.org/draft/2020-12/schema",
926
+ type: "object",
927
+ properties,
928
+ required,
929
+ additionalProperties: false
930
+ },
931
+ properties
932
+ };
933
+ }
934
+ function extractCollectionFromCall(callExpr, sf, getLocation, diagnostics = [], file = "") {
935
+ const arg0 = callExpr.arguments[0];
936
+ if (!arg0 || !ts4.isObjectLiteralExpression(arg0)) return null;
937
+ let collKey = "";
938
+ let route;
939
+ let presentation;
940
+ let titleField;
941
+ let bodyField;
942
+ let listColumns;
943
+ let orderable;
944
+ let group;
945
+ let schemaNode;
946
+ let seed;
947
+ for (const prop of arg0.properties) {
948
+ if (ts4.isPropertyAssignment(prop)) {
949
+ const pName = prop.name.getText(sf);
950
+ if (pName === "key") {
951
+ collKey = getStringLiteralValue(prop.initializer) || "";
952
+ } else if (pName === "route") {
953
+ route = getStringLiteralValue(prop.initializer) || void 0;
954
+ } else if (pName === "presentation") {
955
+ const val = getStringLiteralValue(prop.initializer);
956
+ if (val === "visual" || val === "form") presentation = val;
957
+ } else if (pName === "titleField") {
958
+ titleField = getStringLiteralValue(prop.initializer) || void 0;
959
+ } else if (pName === "bodyField") {
960
+ bodyField = getStringLiteralValue(prop.initializer) || void 0;
961
+ } else if (pName === "group") {
962
+ group = getStringLiteralValue(prop.initializer) || void 0;
963
+ } else if (pName === "orderable") {
964
+ orderable = prop.initializer.kind === ts4.SyntaxKind.TrueKeyword;
965
+ } else if (pName === "schema") {
966
+ schemaNode = prop.initializer;
967
+ } else if (pName === "seed") {
968
+ seed = extractCollectionSeed(prop.initializer, sf, getLocation, file, diagnostics);
969
+ } else if (pName === "listColumns") {
970
+ if (ts4.isArrayLiteralExpression(prop.initializer)) {
971
+ listColumns = prop.initializer.elements.map((el) => getStringLiteralValue(el)).filter((s) => s !== null);
972
+ }
973
+ }
974
+ }
975
+ }
976
+ if (!collKey) {
977
+ diagnostics.push({
978
+ type: "error",
979
+ message: "Collection defined without a static string key",
980
+ file,
981
+ line: getLocation(callExpr).line,
982
+ column: getLocation(callExpr).column
983
+ });
984
+ return null;
985
+ }
986
+ const { jsonSchema } = schemaNode ? parseZodObjectSchema(schemaNode, sf, bodyField) : { jsonSchema: { type: "object", properties: {} } };
987
+ return {
988
+ key: collKey,
989
+ jsonSchema,
990
+ schemaVersion: 1,
991
+ orderable,
992
+ routePattern: route,
993
+ presentation,
994
+ titleField,
995
+ listColumns,
996
+ bodyField,
997
+ group,
998
+ seed,
999
+ location: getLocation(callExpr)
1000
+ };
1001
+ }
1002
+
1003
+ // src/extract/collect.ts
1004
+ function collectFromFile(parsed, context) {
1005
+ const knownFieldTypes = knownFieldMethodNames();
1006
+ const { sourceFile, getLocation, relativePath } = parsed;
1007
+ const fields = [];
1008
+ const collectionsMap = /* @__PURE__ */ new Map();
1009
+ for (const coll of context.collections.values()) {
1010
+ collectionsMap.set(coll.key, coll);
1011
+ }
1012
+ const diagnostics = [...context.diagnostics];
1013
+ function extractOptions(node) {
1014
+ if (!node || !ts5.isObjectLiteralExpression(node)) return {};
1015
+ const result = {};
1016
+ for (const prop of node.properties) {
1017
+ if (ts5.isPropertyAssignment(prop)) {
1018
+ const propName = prop.name.getText(sourceFile);
1019
+ if (propName === "defaultValue") {
1020
+ const strVal = getStringLiteralValue(prop.initializer);
1021
+ if (strVal !== null) {
1022
+ result.defaultValue = strVal;
1023
+ } else if (ts5.isNumericLiteral(prop.initializer)) {
1024
+ result.defaultValue = Number(prop.initializer.text);
1025
+ } else if (prop.initializer.kind === ts5.SyntaxKind.TrueKeyword || prop.initializer.kind === ts5.SyntaxKind.FalseKeyword) {
1026
+ result.defaultValue = prop.initializer.kind === ts5.SyntaxKind.TrueKeyword;
1027
+ }
1028
+ }
1029
+ }
1030
+ }
1031
+ return result;
1032
+ }
1033
+ for (const [varName, instance] of context.groupInstances) {
1034
+ for (const [shapeKey, shapeType] of Object.entries(instance.shape)) {
1035
+ const fullKey = `${instance.prefix}.${shapeKey}`;
1036
+ fields.push({
1037
+ key: fullKey,
1038
+ type: normalizeFieldType(shapeType),
1039
+ group: instance.groupName,
1040
+ location: instance.location
1041
+ });
1042
+ }
1043
+ }
1044
+ function checkKeyArgument(keyNode, calleeText) {
1045
+ if (!keyNode) {
1046
+ diagnostics.push({
1047
+ type: "error",
1048
+ message: `Field helper '${calleeText}' called without a key`,
1049
+ file: relativePath,
1050
+ line: getLocation(keyNode || sourceFile).line,
1051
+ column: getLocation(keyNode || sourceFile).column
1052
+ });
1053
+ return null;
1054
+ }
1055
+ const literalVal = getStringLiteralValue(keyNode);
1056
+ if (literalVal === null) {
1057
+ diagnostics.push({
1058
+ type: "error",
1059
+ message: `Field key in '${calleeText}' must be a static string literal, got non-literal expression`,
1060
+ file: relativePath,
1061
+ line: getLocation(keyNode).line,
1062
+ column: getLocation(keyNode).column
1063
+ });
1064
+ return null;
1065
+ }
1066
+ return literalVal;
1067
+ }
1068
+ function walk(node) {
1069
+ if (ts5.isCallExpression(node)) {
1070
+ const expr = node.expression;
1071
+ const exprText = expr.getText(sourceFile);
1072
+ if (exprText === "defineCollection" || exprText === "f.defineCollection") {
1073
+ const arg0 = node.arguments[0];
1074
+ if (arg0 && ts5.isObjectLiteralExpression(arg0)) {
1075
+ let collKey = "";
1076
+ let route;
1077
+ let presentation;
1078
+ let titleField;
1079
+ let bodyField;
1080
+ let listColumns;
1081
+ let orderable;
1082
+ let group;
1083
+ let schemaNode;
1084
+ let seed;
1085
+ for (const prop of arg0.properties) {
1086
+ if (ts5.isPropertyAssignment(prop)) {
1087
+ const pName = prop.name.getText(sourceFile);
1088
+ if (pName === "key") {
1089
+ collKey = getStringLiteralValue(prop.initializer) || "";
1090
+ } else if (pName === "route") {
1091
+ route = getStringLiteralValue(prop.initializer) || void 0;
1092
+ } else if (pName === "presentation") {
1093
+ const val = getStringLiteralValue(prop.initializer);
1094
+ if (val === "visual" || val === "form") presentation = val;
1095
+ } else if (pName === "titleField") {
1096
+ titleField = getStringLiteralValue(prop.initializer) || void 0;
1097
+ } else if (pName === "bodyField") {
1098
+ bodyField = getStringLiteralValue(prop.initializer) || void 0;
1099
+ } else if (pName === "group") {
1100
+ group = getStringLiteralValue(prop.initializer) || void 0;
1101
+ } else if (pName === "orderable") {
1102
+ orderable = prop.initializer.kind === ts5.SyntaxKind.TrueKeyword;
1103
+ } else if (pName === "schema") {
1104
+ schemaNode = prop.initializer;
1105
+ } else if (pName === "seed") {
1106
+ seed = extractCollectionSeed(
1107
+ prop.initializer,
1108
+ sourceFile,
1109
+ getLocation,
1110
+ relativePath,
1111
+ diagnostics
1112
+ );
1113
+ } else if (pName === "listColumns") {
1114
+ if (ts5.isArrayLiteralExpression(prop.initializer)) {
1115
+ listColumns = prop.initializer.elements.map((el) => getStringLiteralValue(el)).filter((s) => s !== null);
1116
+ }
1117
+ }
1118
+ }
1119
+ }
1120
+ if (!collKey) {
1121
+ diagnostics.push({
1122
+ type: "error",
1123
+ message: "Collection defined without a static string key",
1124
+ file: relativePath,
1125
+ line: getLocation(node).line,
1126
+ column: getLocation(node).column
1127
+ });
1128
+ } else {
1129
+ const { jsonSchema } = schemaNode ? parseZodObjectSchema(schemaNode, sourceFile, bodyField) : { jsonSchema: { type: "object", properties: {} } };
1130
+ collectionsMap.set(collKey, {
1131
+ key: collKey,
1132
+ jsonSchema,
1133
+ schemaVersion: 1,
1134
+ orderable,
1135
+ routePattern: route,
1136
+ presentation,
1137
+ titleField,
1138
+ listColumns,
1139
+ bodyField,
1140
+ group,
1141
+ seed,
1142
+ location: getLocation(node)
1143
+ });
1144
+ }
1145
+ }
1146
+ }
1147
+ if (ts5.isPropertyAccessExpression(expr)) {
1148
+ const objText = expr.expression.getText(sourceFile);
1149
+ const methodName = expr.name.getText(sourceFile);
1150
+ let fieldType;
1151
+ let prefix = "";
1152
+ let groupName;
1153
+ if (objText === "f") {
1154
+ if (knownFieldTypes.has(methodName)) {
1155
+ fieldType = methodName;
1156
+ }
1157
+ } else if (context.scopes.has(objText)) {
1158
+ if (knownFieldTypes.has(methodName)) {
1159
+ fieldType = methodName;
1160
+ prefix = context.scopes.get(objText)?.prefix || "";
1161
+ }
1162
+ } else if (context.groupInstances.has(objText)) {
1163
+ const inst = context.groupInstances.get(objText);
1164
+ if (inst?.shape[methodName]) {
1165
+ fieldType = inst.shape[methodName];
1166
+ prefix = inst.prefix;
1167
+ groupName = inst.groupName;
1168
+ }
1169
+ }
1170
+ if (fieldType) {
1171
+ const keyArg = node.arguments[0];
1172
+ let key = null;
1173
+ if (groupName && !keyArg) {
1174
+ key = methodName;
1175
+ } else {
1176
+ key = checkKeyArgument(keyArg, exprText);
1177
+ }
1178
+ if (key !== null) {
1179
+ const fullKey = prefix ? `${prefix}.${key}` : key;
1180
+ const normType = normalizeFieldType(fieldType);
1181
+ const opts = extractOptions(node.arguments[1]);
1182
+ fields.push({
1183
+ key: fullKey,
1184
+ type: normType,
1185
+ defaultValue: opts.defaultValue,
1186
+ constraints: opts.constraints,
1187
+ group: groupName,
1188
+ location: getLocation(node)
1189
+ });
1190
+ }
1191
+ }
1192
+ }
1193
+ }
1194
+ if (ts5.isJsxSelfClosingElement(node) || ts5.isJsxOpeningElement(node)) {
1195
+ const tagText = node.tagName.getText(sourceFile);
1196
+ const parts = tagText.split(".");
1197
+ if (parts.length === 2) {
1198
+ const [objName, memberName] = parts;
1199
+ let fieldType;
1200
+ let prefix = "";
1201
+ let groupName;
1202
+ if (objName === "f") {
1203
+ if (knownFieldTypes.has(memberName)) {
1204
+ fieldType = memberName;
1205
+ }
1206
+ } else if (context.scopes.has(objName)) {
1207
+ if (knownFieldTypes.has(memberName)) {
1208
+ fieldType = memberName;
1209
+ prefix = context.scopes.get(objName)?.prefix || "";
1210
+ }
1211
+ } else if (context.groupInstances.has(objName)) {
1212
+ const inst = context.groupInstances.get(objName);
1213
+ if (inst?.shape[memberName]) {
1214
+ fieldType = inst.shape[memberName];
1215
+ prefix = inst.prefix;
1216
+ groupName = inst.groupName;
1217
+ }
1218
+ }
1219
+ if (fieldType) {
1220
+ let keyAttrVal = null;
1221
+ let defaultValue;
1222
+ for (const attr of node.attributes.properties) {
1223
+ if (ts5.isJsxAttribute(attr)) {
1224
+ const aName = attr.name.getText(sourceFile);
1225
+ if (aName === "k" || aName === "key") {
1226
+ if (attr.initializer) {
1227
+ if (ts5.isStringLiteral(attr.initializer)) {
1228
+ keyAttrVal = attr.initializer.text;
1229
+ } else if (ts5.isJsxExpression(attr.initializer) && attr.initializer.expression) {
1230
+ keyAttrVal = getStringLiteralValue(attr.initializer.expression);
1231
+ if (keyAttrVal === null) {
1232
+ diagnostics.push({
1233
+ type: "error",
1234
+ message: `JSX attribute '${aName}' on '<${tagText}>' must be a static string literal`,
1235
+ file: relativePath,
1236
+ line: getLocation(attr.initializer).line,
1237
+ column: getLocation(attr.initializer).column
1238
+ });
1239
+ }
1240
+ }
1241
+ }
1242
+ } else if (aName === "defaultValue" && attr.initializer) {
1243
+ if (ts5.isStringLiteral(attr.initializer)) {
1244
+ defaultValue = attr.initializer.text;
1245
+ }
1246
+ }
1247
+ }
1248
+ }
1249
+ if (groupName && !keyAttrVal) {
1250
+ keyAttrVal = memberName;
1251
+ }
1252
+ if (keyAttrVal !== null) {
1253
+ const fullKey = prefix ? `${prefix}.${keyAttrVal}` : keyAttrVal;
1254
+ const normType = normalizeFieldType(fieldType);
1255
+ fields.push({
1256
+ key: fullKey,
1257
+ type: normType,
1258
+ defaultValue,
1259
+ group: groupName,
1260
+ location: getLocation(node)
1261
+ });
1262
+ }
1263
+ }
1264
+ }
1265
+ }
1266
+ ts5.forEachChild(node, walk);
1267
+ }
1268
+ walk(sourceFile);
1269
+ return {
1270
+ file: relativePath,
1271
+ fields,
1272
+ collections: Array.from(collectionsMap.values()),
1273
+ groups: Array.from(context.groups.values()).map((g) => ({
1274
+ groupName: g.groupName,
1275
+ shape: g.shape,
1276
+ location: g.definedLocation
1277
+ })),
1278
+ scopes: Array.from(context.scopes.entries()).map(([varName, s]) => ({
1279
+ varName,
1280
+ prefix: s.prefix,
1281
+ location: s.location
1282
+ })),
1283
+ diagnostics
1284
+ };
1285
+ }
1286
+
1287
+ // src/extract/manifest.ts
1288
+ import { validateContent } from "@407dev/blocks";
1289
+ function buildSiteManifest(merged, extraRoutes = []) {
1290
+ const diagnostics = [...merged.diagnostics];
1291
+ const fields = {};
1292
+ const collections = {};
1293
+ for (const [key, field] of Object.entries(merged.fields)) {
1294
+ const uniqueFiles = Array.from(new Set(field.sources.map((s) => s.file))).sort();
1295
+ const sourcePath = uniqueFiles.join(",");
1296
+ const manifestField = {
1297
+ type: field.type,
1298
+ defaultValue: field.defaultValue,
1299
+ constraints: field.constraints,
1300
+ group: field.group,
1301
+ sourcePath: sourcePath || void 0
1302
+ };
1303
+ fields[key] = manifestField;
1304
+ }
1305
+ for (const [key, coll] of Object.entries(merged.collections)) {
1306
+ const schemaProps = coll.jsonSchema.properties || {};
1307
+ if (coll.titleField && !(coll.titleField in schemaProps)) {
1308
+ diagnostics.push({
1309
+ type: "error",
1310
+ message: `Collection '${key}' specifies titleField '${coll.titleField}' which does not exist in its schema properties`,
1311
+ file: coll.location.file,
1312
+ line: coll.location.line,
1313
+ column: coll.location.column
1314
+ });
1315
+ }
1316
+ if (coll.bodyField && !(coll.bodyField in schemaProps)) {
1317
+ diagnostics.push({
1318
+ type: "error",
1319
+ message: `Collection '${key}' specifies bodyField '${coll.bodyField}' which does not exist in its schema properties`,
1320
+ file: coll.location.file,
1321
+ line: coll.location.line,
1322
+ column: coll.location.column
1323
+ });
1324
+ }
1325
+ if (coll.listColumns) {
1326
+ for (const col of coll.listColumns) {
1327
+ if (!(col in schemaProps)) {
1328
+ diagnostics.push({
1329
+ type: "error",
1330
+ message: `Collection '${key}' specifies listColumn '${col}' which does not exist in its schema properties`,
1331
+ file: coll.location.file,
1332
+ line: coll.location.line,
1333
+ column: coll.location.column
1334
+ });
1335
+ }
1336
+ }
1337
+ }
1338
+ if (coll.seed) {
1339
+ const seenSlugs = /* @__PURE__ */ new Set();
1340
+ for (const item of coll.seed) {
1341
+ if (!item || typeof item !== "object" || !item.slug || typeof item.slug !== "string") {
1342
+ diagnostics.push({
1343
+ type: "error",
1344
+ message: `Collection '${key}' seed item must be an object with a 'slug' string`,
1345
+ file: coll.location.file,
1346
+ line: coll.location.line,
1347
+ column: coll.location.column
1348
+ });
1349
+ continue;
1350
+ }
1351
+ if (seenSlugs.has(item.slug)) {
1352
+ diagnostics.push({
1353
+ type: "error",
1354
+ message: `Collection '${key}' seed has duplicate slug '${item.slug}'`,
1355
+ file: coll.location.file,
1356
+ line: coll.location.line,
1357
+ column: coll.location.column
1358
+ });
1359
+ }
1360
+ seenSlugs.add(item.slug);
1361
+ const valResult = validateContent(coll.jsonSchema, item.content || {});
1362
+ if (!valResult.valid) {
1363
+ for (const err of valResult.errors ?? []) {
1364
+ diagnostics.push({
1365
+ type: "error",
1366
+ message: `Collection '${key}' seed item '${item.slug}' validation failed: ${err.message}`,
1367
+ file: coll.location.file,
1368
+ line: coll.location.line,
1369
+ column: coll.location.column
1370
+ });
1371
+ }
1372
+ }
1373
+ }
1374
+ }
1375
+ collections[key] = {
1376
+ jsonSchema: coll.jsonSchema,
1377
+ schemaVersion: coll.schemaVersion || 1,
1378
+ orderable: coll.orderable,
1379
+ routePattern: coll.routePattern,
1380
+ presentation: coll.presentation,
1381
+ titleField: coll.titleField,
1382
+ listColumns: coll.listColumns,
1383
+ bodyField: coll.bodyField,
1384
+ group: coll.group,
1385
+ seed: coll.seed
1386
+ };
1387
+ }
1388
+ const routes = [...extraRoutes];
1389
+ for (const [key, coll] of Object.entries(merged.collections)) {
1390
+ if (coll.routePattern) {
1391
+ routes.push({
1392
+ pathOrPattern: coll.routePattern,
1393
+ kind: "collection",
1394
+ collectionKey: key,
1395
+ sourcePath: coll.location.file
1396
+ });
1397
+ }
1398
+ }
1399
+ const manifest = {
1400
+ manifestVersion: 1,
1401
+ fields,
1402
+ collections: Object.keys(collections).length > 0 ? collections : void 0,
1403
+ routes: routes.length > 0 ? routes : void 0
1404
+ };
1405
+ return { manifest, diagnostics };
1406
+ }
1407
+
1408
+ // src/extract/merge.ts
1409
+ function areObjectsEqual(a, b) {
1410
+ if (!a && !b) return true;
1411
+ if (!a || !b) return false;
1412
+ const keysA = Object.keys(a).sort();
1413
+ const keysB = Object.keys(b).sort();
1414
+ if (keysA.length !== keysB.length) return false;
1415
+ return JSON.stringify(a) === JSON.stringify(b);
1416
+ }
1417
+ function mergeExtractionResults(fileResults) {
1418
+ const mergedFields = {};
1419
+ const collections = {};
1420
+ const diagnostics = [];
1421
+ for (const res of fileResults) {
1422
+ diagnostics.push(...res.diagnostics);
1423
+ for (const coll of res.collections) {
1424
+ if (!collections[coll.key]) {
1425
+ collections[coll.key] = coll;
1426
+ }
1427
+ }
1428
+ for (const field of res.fields) {
1429
+ const existing = mergedFields[field.key];
1430
+ if (!existing) {
1431
+ mergedFields[field.key] = {
1432
+ type: field.type,
1433
+ constraints: field.constraints,
1434
+ defaultValue: field.defaultValue,
1435
+ group: field.group,
1436
+ sources: [field.location]
1437
+ };
1438
+ } else {
1439
+ const typeConflict = existing.type !== field.type;
1440
+ const defaultConflict = existing.defaultValue !== void 0 && field.defaultValue !== void 0 && existing.defaultValue !== field.defaultValue;
1441
+ const constraintsConflict = !areObjectsEqual(existing.constraints, field.constraints);
1442
+ if (typeConflict || defaultConflict || constraintsConflict) {
1443
+ const allSources = [...existing.sources, field.location];
1444
+ const locStr = allSources.map((s) => `${s.file}:${s.line}:${s.column}`).join(" vs ");
1445
+ diagnostics.push({
1446
+ type: "error",
1447
+ message: `Conflicting field definition for key '${field.key}' (${locStr})`,
1448
+ file: field.location.file,
1449
+ line: field.location.line,
1450
+ column: field.location.column
1451
+ });
1452
+ }
1453
+ existing.sources.push(field.location);
1454
+ if (!existing.group && field.group) {
1455
+ existing.group = field.group;
1456
+ }
1457
+ }
1458
+ }
1459
+ }
1460
+ return {
1461
+ fields: mergedFields,
1462
+ collections,
1463
+ routes: [],
1464
+ diagnostics
1465
+ };
1466
+ }
1467
+
1468
+ // src/extract/routes.ts
1469
+ import { execSync } from "child_process";
1470
+ import fs3 from "fs";
1471
+ import path3 from "path";
1472
+ function shouldSyncRoutes(siteDir, cmsRoutesFile) {
1473
+ if (!fs3.existsSync(cmsRoutesFile)) {
1474
+ return true;
1475
+ }
1476
+ const routesStat = fs3.statSync(cmsRoutesFile);
1477
+ const srcDir = path3.join(siteDir, "src");
1478
+ if (!fs3.existsSync(srcDir)) {
1479
+ return false;
1480
+ }
1481
+ let newestMtime = 0;
1482
+ function checkMtime(dir) {
1483
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
1484
+ for (const entry of entries) {
1485
+ const full = path3.join(dir, entry.name);
1486
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
1487
+ checkMtime(full);
1488
+ } else if (entry.isFile()) {
1489
+ const stat = fs3.statSync(full);
1490
+ if (stat.mtimeMs > newestMtime) {
1491
+ newestMtime = stat.mtimeMs;
1492
+ }
1493
+ }
1494
+ }
1495
+ }
1496
+ checkMtime(srcDir);
1497
+ return newestMtime > routesStat.mtimeMs;
1498
+ }
1499
+ function syncAstroRoutes(siteDir) {
1500
+ try {
1501
+ execSync("npx astro sync", {
1502
+ cwd: siteDir,
1503
+ stdio: "ignore"
1504
+ });
1505
+ return true;
1506
+ } catch {
1507
+ return false;
1508
+ }
1509
+ }
1510
+ function deriveRouteFromComponent(componentPath) {
1511
+ let route = componentPath.replace(/\\/g, "/").replace(/^.*src\/pages\/?/, "/").replace(/\.(astro|mdx|md|ts|js|jsx|tsx)$/, "").replace(/\/index$/, "");
1512
+ if (!route.startsWith("/")) {
1513
+ route = `/${route}`;
1514
+ }
1515
+ return route === "" ? "/" : route;
1516
+ }
1517
+ function scanPagesDirectory(siteDir) {
1518
+ const pagesDir = path3.join(siteDir, "src", "pages");
1519
+ if (!fs3.existsSync(pagesDir)) return [];
1520
+ const found = [];
1521
+ function walk(dir) {
1522
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
1523
+ for (const entry of entries) {
1524
+ const fullPath = path3.join(dir, entry.name);
1525
+ if (entry.isDirectory()) {
1526
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
1527
+ walk(fullPath);
1528
+ }
1529
+ } else if (entry.isFile()) {
1530
+ const ext = path3.extname(entry.name).toLowerCase();
1531
+ if ([".astro", ".md", ".mdx", ".html", ".ts", ".js", ".jsx", ".tsx"].includes(ext)) {
1532
+ if (entry.name.startsWith("_")) continue;
1533
+ const route = deriveRouteFromComponent(fullPath);
1534
+ const params = [];
1535
+ const matches = route.matchAll(/\[([^\]]+)\]/g);
1536
+ for (const m of matches) {
1537
+ params.push(m[1]);
1538
+ }
1539
+ found.push({
1540
+ route,
1541
+ component: fullPath,
1542
+ type: "page",
1543
+ params
1544
+ });
1545
+ }
1546
+ }
1547
+ }
1548
+ }
1549
+ walk(pagesDir);
1550
+ return found;
1551
+ }
1552
+ function loadDerivedRoutes(siteDir, collections = {}, options = {}) {
1553
+ const diagnostics = [];
1554
+ const cmsRoutesFile = path3.join(siteDir, ".cms", "routes.json");
1555
+ if (!options.noSync && shouldSyncRoutes(siteDir, cmsRoutesFile)) {
1556
+ syncAstroRoutes(siteDir);
1557
+ }
1558
+ const staticRoutes = [];
1559
+ const seenPatterns = /* @__PURE__ */ new Set();
1560
+ let astroRoutes = [];
1561
+ if (fs3.existsSync(cmsRoutesFile)) {
1562
+ try {
1563
+ const raw = fs3.readFileSync(cmsRoutesFile, "utf-8");
1564
+ astroRoutes = JSON.parse(raw);
1565
+ } catch {
1566
+ }
1567
+ }
1568
+ const scannedRoutes = scanPagesDirectory(siteDir);
1569
+ const combinedRoutes = [...astroRoutes, ...scannedRoutes];
1570
+ const knownAstroPatterns = /* @__PURE__ */ new Set();
1571
+ for (const r of combinedRoutes) {
1572
+ if (r.type === "endpoint") continue;
1573
+ let pattern = r.route || "";
1574
+ if (!pattern && r.component) {
1575
+ pattern = deriveRouteFromComponent(r.component);
1576
+ }
1577
+ if (!pattern || pattern === "/404" || pattern.startsWith("/_")) continue;
1578
+ knownAstroPatterns.add(pattern);
1579
+ if (!r.params || r.params.length === 0) {
1580
+ if (!seenPatterns.has(pattern)) {
1581
+ seenPatterns.add(pattern);
1582
+ let relComponent;
1583
+ if (r.component) {
1584
+ const absComp = path3.isAbsolute(r.component) ? r.component : path3.resolve(siteDir, r.component);
1585
+ relComponent = path3.relative(siteDir, absComp).replace(/\\/g, "/");
1586
+ }
1587
+ staticRoutes.push({
1588
+ pathOrPattern: pattern,
1589
+ kind: "static",
1590
+ sourcePath: relComponent || void 0
1591
+ });
1592
+ }
1593
+ }
1594
+ }
1595
+ for (const [key, coll] of Object.entries(collections)) {
1596
+ if (coll.routePattern) {
1597
+ const normalizedRoute = coll.routePattern.startsWith("/") ? coll.routePattern : `/${coll.routePattern}`;
1598
+ if (knownAstroPatterns.size > 0 && !knownAstroPatterns.has(normalizedRoute)) {
1599
+ diagnostics.push({
1600
+ type: "error",
1601
+ message: `Collection '${key}' specifies route '${coll.routePattern}' with no matching dynamic route found in Astro pages`,
1602
+ file: coll.location.file,
1603
+ line: coll.location.line,
1604
+ column: coll.location.column
1605
+ });
1606
+ }
1607
+ }
1608
+ }
1609
+ return { routes: staticRoutes, diagnostics };
1610
+ }
1611
+
1612
+ // src/extract/check.ts
1613
+ async function extractProject(siteDir = process.cwd(), options = {}) {
1614
+ const srcDir = path4.join(siteDir, "src");
1615
+ const targetDir = fs4.existsSync(srcDir) ? srcDir : siteDir;
1616
+ const files = globSourceFiles(targetDir);
1617
+ const fileResults = [];
1618
+ const cachedParsedFiles = /* @__PURE__ */ new Map();
1619
+ for (const file of files) {
1620
+ const parsed = await parseFile(file, siteDir);
1621
+ cachedParsedFiles.set(file, parsed);
1622
+ const context = await resolveFileContext(parsed, siteDir, cachedParsedFiles);
1623
+ const collected = collectFromFile(parsed, context);
1624
+ fileResults.push(collected);
1625
+ }
1626
+ const merged = mergeExtractionResults(fileResults);
1627
+ const { routes, diagnostics: routeDiagnostics } = loadDerivedRoutes(siteDir, merged.collections, {
1628
+ noSync: options.noSync
1629
+ });
1630
+ const { manifest, diagnostics } = buildSiteManifest(merged, routes);
1631
+ diagnostics.push(...routeDiagnostics);
1632
+ const errors = diagnostics.filter((d) => d.type === "error");
1633
+ const warnings = diagnostics.filter((d) => d.type === "warning");
1634
+ return {
1635
+ manifest,
1636
+ diagnostics,
1637
+ errors,
1638
+ warnings,
1639
+ merged
1640
+ };
1641
+ }
1642
+ function formatDiagnostics(diagnostics) {
1643
+ if (diagnostics.length === 0) {
1644
+ return "\u2728 All checks passed cleanly with 0 errors.";
1645
+ }
1646
+ const lines = [];
1647
+ for (const d of diagnostics) {
1648
+ const prefix = d.type === "error" ? "\u274C" : "\u26A0\uFE0F";
1649
+ const loc = `${d.file}:${d.line}:${d.column}`;
1650
+ lines.push(`${prefix} ${loc}: ${d.message}`);
1651
+ }
1652
+ const errors = diagnostics.filter((d) => d.type === "error").length;
1653
+ const warnings = diagnostics.filter((d) => d.type === "warning").length;
1654
+ lines.push(`
1655
+ Found ${errors} error(s) and ${warnings} warning(s).`);
1656
+ return lines.join("\n");
1657
+ }
1658
+
1659
+ // src/commands/check.ts
1660
+ async function executeCheck(options = {}) {
1661
+ const result = await extractProject(options.siteDir, { noSync: options.noSync });
1662
+ const report = formatDiagnostics(result.diagnostics);
1663
+ return {
1664
+ success: result.errors.length === 0,
1665
+ errorsCount: result.errors.length,
1666
+ warningsCount: result.warnings.length,
1667
+ report
1668
+ };
1669
+ }
1670
+
1671
+ // src/commands/extract.ts
1672
+ async function executeExtract(options = {}) {
1673
+ const result = await extractProject(options.siteDir, { noSync: options.noSync });
1674
+ if (options.json) {
1675
+ return JSON.stringify(result.manifest, null, 2);
1676
+ }
1677
+ const lines = [];
1678
+ lines.push("\u{1F4E6} Extracted Site Manifest:");
1679
+ lines.push(` Fields: ${Object.keys(result.manifest.fields).length}`);
1680
+ lines.push(` Collections: ${Object.keys(result.manifest.collections || {}).length}`);
1681
+ lines.push(` Routes: ${(result.manifest.routes || []).length}`);
1682
+ if (result.errors.length > 0) {
1683
+ lines.push(`
1684
+ \u274C Found ${result.errors.length} error(s):`);
1685
+ for (const e of result.errors) {
1686
+ lines.push(` - ${e.file}:${e.line}:${e.column}: ${e.message}`);
1687
+ }
1688
+ }
1689
+ if (result.warnings.length > 0) {
1690
+ lines.push(`
1691
+ \u26A0\uFE0F Found ${result.warnings.length} warning(s):`);
1692
+ for (const w of result.warnings) {
1693
+ lines.push(` - ${w.file}:${w.line}:${w.column}: ${w.message}`);
1694
+ }
1695
+ }
1696
+ return lines.join("\n");
1697
+ }
1698
+
1699
+ // src/commands/link.ts
1700
+ import fs7 from "fs";
1701
+ import path7 from "path";
1702
+ import readline from "readline/promises";
1703
+
1704
+ // ../config/src/index.ts
1705
+ var DEFAULT_DEV_API_URL = "http://127.0.0.1:8787";
1706
+ var DEFAULT_DEV_CONTENT_URL = "http://127.0.0.1:8788";
1707
+ var DEFAULT_DEV_WEB_URL = "https://web.localhost";
1708
+
1709
+ // ../events/dist/index.js
1710
+ var LIVE_EVENTS_WINDOW_MS = 5 * 6e4;
1711
+ var ROLLUP_QUERIES = [
1712
+ "allTimeSummary",
1713
+ "allTimeTimeseries",
1714
+ "allTimeTopPages",
1715
+ "allTimeReferrers",
1716
+ "allTimeCountries",
1717
+ "allTimeCustomEvents",
1718
+ "allTimeVitalsPercentiles"
1719
+ ];
1720
+ var DEEP_QUERIES = [
1721
+ "funnel",
1722
+ "funnelBreakdown",
1723
+ "entryExitPaths",
1724
+ "pathTransitions",
1725
+ "retentionCohorts",
1726
+ "longRangeTimeseries",
1727
+ "longRangeTopPages",
1728
+ "longRangeReferrers",
1729
+ "longRangeCountries",
1730
+ "longRangeCustomEvents",
1731
+ "longRangeSummary",
1732
+ "vitalsPercentiles",
1733
+ "visitorList",
1734
+ "visitorEvents",
1735
+ ...ROLLUP_QUERIES
1736
+ ];
1737
+ function generateLogEdges(min, max, count, decimals = 0) {
1738
+ const edges = [];
1739
+ const factor = (max / min) ** (1 / count);
1740
+ let cur = min;
1741
+ for (let i = 1; i <= count; i++) {
1742
+ cur *= factor;
1743
+ const factorDec = 10 ** decimals;
1744
+ edges.push(Math.round(cur * factorDec) / factorDec);
1745
+ }
1746
+ return edges;
1747
+ }
1748
+ var VITALS_HISTOGRAM_EDGES = {
1749
+ lcp: generateLogEdges(100, 1e4, 48, 1),
1750
+ inp: generateLogEdges(10, 2e3, 48, 1),
1751
+ cls: generateLogEdges(5e-3, 1, 48, 4),
1752
+ fcp: generateLogEdges(100, 1e4, 48, 1),
1753
+ ttfb: generateLogEdges(10, 5e3, 48, 1)
1754
+ };
1755
+
1756
+ // ../api-client/src/index.ts
1757
+ function createApiClient(options) {
1758
+ const getHeaders = () => {
1759
+ const headers = {
1760
+ "Content-Type": "application/json"
1761
+ };
1762
+ if (options.authToken) {
1763
+ headers.Authorization = `Bearer ${options.authToken}`;
1764
+ }
1765
+ if (options.workspaceId) {
1766
+ headers["x-workspace-id"] = options.workspaceId;
1767
+ }
1768
+ return headers;
1769
+ };
1770
+ const request = async (path10, init) => {
1771
+ try {
1772
+ const url = path10.startsWith("http") ? path10 : `${options.baseUrl}${path10.startsWith("/") ? "" : "/"}${path10}`;
1773
+ const res = await fetch(url, {
1774
+ ...init,
1775
+ headers: {
1776
+ ...getHeaders(),
1777
+ ...init?.headers || {}
1778
+ }
1779
+ });
1780
+ const json = await res.json().catch(() => ({}));
1781
+ if (!res.ok) {
1782
+ return {
1783
+ data: null,
1784
+ ok: false,
1785
+ error: json.error || res.statusText,
1786
+ errors: json.errors
1787
+ };
1788
+ }
1789
+ return {
1790
+ data: json.data !== void 0 ? json.data : json,
1791
+ ok: true
1792
+ };
1793
+ } catch (err) {
1794
+ const message = err instanceof Error ? err.message : String(err);
1795
+ return { data: null, ok: false, error: message };
1796
+ }
1797
+ };
1798
+ return {
1799
+ baseUrl: options.baseUrl,
1800
+ async get(path10) {
1801
+ return request(path10, { method: "GET" });
1802
+ },
1803
+ async post(path10, body) {
1804
+ return request(path10, {
1805
+ method: "POST",
1806
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1807
+ });
1808
+ },
1809
+ async patch(path10, body) {
1810
+ return request(path10, {
1811
+ method: "PATCH",
1812
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1813
+ });
1814
+ },
1815
+ async put(path10, body) {
1816
+ return request(path10, {
1817
+ method: "PUT",
1818
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1819
+ });
1820
+ },
1821
+ async delete(path10, body) {
1822
+ return request(path10, {
1823
+ method: "DELETE",
1824
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1825
+ });
1826
+ },
1827
+ // Sites
1828
+ async getSites(workspaceId) {
1829
+ const ws = workspaceId || options.workspaceId;
1830
+ const query = ws ? `?workspace_id=${ws}` : "";
1831
+ return this.get(`/sites${query}`);
1832
+ },
1833
+ async getSite(id) {
1834
+ return this.get(`/sites/${id}`);
1835
+ },
1836
+ async createSite(input) {
1837
+ return this.post("/sites", input);
1838
+ },
1839
+ async updateSite(siteId, updates) {
1840
+ return this.patch(`/sites/${siteId}`, updates);
1841
+ },
1842
+ async deleteSite(siteId) {
1843
+ return this.delete(`/sites/${siteId}`);
1844
+ },
1845
+ async reprovisionPreviewHost(siteId) {
1846
+ return this.post(`/sites/${siteId}/preview-host`);
1847
+ },
1848
+ async checkPagesProject(name, workspaceId) {
1849
+ const ws = workspaceId || options.workspaceId || "";
1850
+ const query = `?name=${encodeURIComponent(name)}&workspace_id=${encodeURIComponent(ws)}`;
1851
+ return this.get(`/sites/pages-project-status${query}`);
1852
+ },
1853
+ // Change Requests & Comments
1854
+ async getChangeRequests(siteId, status) {
1855
+ const query = status ? `?status=${encodeURIComponent(status)}` : "";
1856
+ return this.get(`/sites/${siteId}/change-requests${query}`);
1857
+ },
1858
+ async getOrCreateDraftChangeRequest(siteId, input) {
1859
+ return this.post(`/sites/${siteId}/change-requests`, input || {});
1860
+ },
1861
+ async updateChangeRequest(id, input) {
1862
+ return this.patch(`/change-requests/${id}`, input);
1863
+ },
1864
+ async sendChangeRequest(id) {
1865
+ return this.post(`/change-requests/${id}/send`, {});
1866
+ },
1867
+ async getChangeRequestComments(changeRequestId) {
1868
+ return this.get(`/change-requests/${changeRequestId}/comments`);
1869
+ },
1870
+ async createChangeRequestComment(changeRequestId, input) {
1871
+ return this.post(`/change-requests/${changeRequestId}/comments`, input);
1872
+ },
1873
+ async updateChangeRequestComment(commentId, input) {
1874
+ return this.patch(`/change-request-comments/${commentId}`, input);
1875
+ },
1876
+ async deleteChangeRequestComment(commentId) {
1877
+ return this.delete(`/change-request-comments/${commentId}`);
1878
+ },
1879
+ async createCommentReply(commentId, body) {
1880
+ return this.post(`/change-request-comments/${commentId}/replies`, {
1881
+ body
1882
+ });
1883
+ },
1884
+ async listChangeRequests(options2) {
1885
+ const params = new URLSearchParams();
1886
+ if (options2?.siteId) params.set("siteId", options2.siteId);
1887
+ if (options2?.status) params.set("status", options2.status);
1888
+ const query = params.toString() ? `?${params.toString()}` : "";
1889
+ return this.get(`/change-requests${query}`);
1890
+ },
1891
+ // Notifications & Channels
1892
+ async getNotifications(options2) {
1893
+ const params = new URLSearchParams();
1894
+ if (options2?.unread) params.set("unread", "1");
1895
+ if (options2?.limit) params.set("limit", String(options2.limit));
1896
+ const query = params.toString() ? `?${params.toString()}` : "";
1897
+ return this.get(`/notifications${query}`);
1898
+ },
1899
+ async markNotificationsRead(input) {
1900
+ return this.post("/notifications/read", input);
1901
+ },
1902
+ async getEmailPrefs() {
1903
+ return this.get("/notifications/email-prefs");
1904
+ },
1905
+ async updateEmailPrefs(input) {
1906
+ return this.patch("/notifications/email-prefs", input);
1907
+ },
1908
+ async listNotificationChannels(workspaceId) {
1909
+ return this.get(`/workspaces/${workspaceId}/notification-channels`);
1910
+ },
1911
+ async createNotificationChannel(workspaceId, input) {
1912
+ return this.post(
1913
+ `/workspaces/${workspaceId}/notification-channels`,
1914
+ input
1915
+ );
1916
+ },
1917
+ async updateNotificationChannel(id, input) {
1918
+ return this.patch(`/notification-channels/${id}`, input);
1919
+ },
1920
+ async deleteNotificationChannel(id) {
1921
+ return this.delete(`/notification-channels/${id}`);
1922
+ },
1923
+ async testNotificationChannel(id) {
1924
+ return this.post(
1925
+ `/notification-channels/${id}/test`,
1926
+ {}
1927
+ );
1928
+ },
1929
+ // Manifest Push
1930
+ async pushManifest(siteId, manifest, renames) {
1931
+ return this.put(`/sites/${siteId}/manifest`, {
1932
+ manifest,
1933
+ renames
1934
+ });
1935
+ },
1936
+ // Fields
1937
+ async getFields(siteId, opts) {
1938
+ const query = opts?.include_orphaned ? "?include_orphaned=1" : "";
1939
+ return this.get(`/sites/${siteId}/fields${query}`);
1940
+ },
1941
+ async setFieldValue(siteId, key, value, type) {
1942
+ return this.patch(
1943
+ `/sites/${siteId}/fields/${encodeURIComponent(key)}`,
1944
+ { value, type }
1945
+ );
1946
+ },
1947
+ async setFieldValues(siteId, values, types) {
1948
+ return this.patch(`/sites/${siteId}/fields`, { values, types });
1949
+ },
1950
+ async deleteOrphanedFields(siteId, keys) {
1951
+ return this.delete(`/sites/${siteId}/fields/orphaned`, { keys });
1952
+ },
1953
+ // Collections
1954
+ async getCollections(siteId, opts) {
1955
+ const query = opts?.include_orphaned ? "?include_orphaned=1" : "";
1956
+ return this.get(`/sites/${siteId}/collections${query}`);
1957
+ },
1958
+ async getCollectionStats(siteId) {
1959
+ return this.get(`/sites/${siteId}/collections/stats`);
1960
+ },
1961
+ // Stream videos (Cloudflare Stream) — distinct from `assets`, which is the R2 pipeline.
1962
+ async createStreamDirectUpload(siteId, body) {
1963
+ return this.post(
1964
+ `/sites/${siteId}/stream/direct-upload`,
1965
+ body ?? {}
1966
+ );
1967
+ },
1968
+ async getStreamVideos(siteId) {
1969
+ return this.get(`/sites/${siteId}/stream/videos`);
1970
+ },
1971
+ async getStreamVideo(siteId, uid) {
1972
+ return this.get(`/sites/${siteId}/stream/videos/${uid}`);
1973
+ },
1974
+ async updateStreamVideo(siteId, uid, updates) {
1975
+ return this.patch(`/sites/${siteId}/stream/videos/${uid}`, updates);
1976
+ },
1977
+ async getStreamVideoUsage(siteId, uid) {
1978
+ return this.get(`/sites/${siteId}/stream/videos/${uid}/usage`);
1979
+ },
1980
+ async deleteStreamVideo(siteId, uid) {
1981
+ return this.delete(`/sites/${siteId}/stream/videos/${uid}`);
1982
+ },
1983
+ async listDeployTokens(siteId) {
1984
+ return this.get(`/sites/${siteId}/deploy-tokens`);
1985
+ },
1986
+ async createDeployToken(siteId, name) {
1987
+ return this.post(`/sites/${siteId}/deploy-tokens`, { name });
1988
+ },
1989
+ async revokeDeployToken(siteId, tokenId) {
1990
+ return this.delete(`/sites/${siteId}/deploy-tokens/${tokenId}`);
1991
+ },
1992
+ async createCliSession() {
1993
+ return this.post("/cli/sessions");
1994
+ },
1995
+ async listEntries(siteId, collectionKey, opts) {
1996
+ const query = opts?.status ? `?status=${opts.status}` : "";
1997
+ return this.get(
1998
+ `/sites/${siteId}/collections/${encodeURIComponent(collectionKey)}/entries${query}`
1999
+ );
2000
+ },
2001
+ async createEntry(siteId, collectionKey, input) {
2002
+ return this.post(
2003
+ `/sites/${siteId}/collections/${encodeURIComponent(collectionKey)}/entries`,
2004
+ input
2005
+ );
2006
+ },
2007
+ async getEntry(id) {
2008
+ return this.get(`/entries/${id}`);
2009
+ },
2010
+ async updateEntry(id, input) {
2011
+ return this.patch(`/entries/${id}`, input);
2012
+ },
2013
+ async setEntryStatus(id, status, scheduled_at) {
2014
+ return this.patch(`/entries/${id}`, { status, scheduled_at });
2015
+ },
2016
+ async reorderEntry(id, input) {
2017
+ return this.post(`/entries/${id}/reorder`, input);
2018
+ },
2019
+ async deleteEntry(id) {
2020
+ return this.delete(`/entries/${id}`);
2021
+ },
2022
+ // Routes (read-only list derived from manifest)
2023
+ async getRoutes(siteId) {
2024
+ return this.get(`/sites/${siteId}/routes`);
2025
+ },
2026
+ // Site Status & Publishing
2027
+ async getSiteStatus(siteId) {
2028
+ return this.get(`/sites/${siteId}/status`);
2029
+ },
2030
+ async publishSite(siteId) {
2031
+ return this.post(`/sites/${siteId}/publish`);
2032
+ },
2033
+ async getSiteVersions(siteId, limit = 50) {
2034
+ return this.get(`/sites/${siteId}/versions?limit=${limit}`);
2035
+ },
2036
+ async diffSiteVersion(siteId, versionId, scope) {
2037
+ return this.post(
2038
+ `/sites/${siteId}/versions/${versionId}/diff`,
2039
+ scope || {}
2040
+ );
2041
+ },
2042
+ async restoreSiteVersion(siteId, versionId, scope) {
2043
+ return this.post(
2044
+ `/sites/${siteId}/versions/${versionId}/restore`,
2045
+ scope || {}
2046
+ );
2047
+ },
2048
+ // Assets
2049
+ async getAssets(siteId) {
2050
+ return this.get(`/assets?site_id=${siteId}`);
2051
+ },
2052
+ async getAsset(id) {
2053
+ return this.get(`/assets/${id}`);
2054
+ },
2055
+ async createAsset(input) {
2056
+ const workspace_id = input.workspace_id || options.workspaceId;
2057
+ return this.post("/assets", { ...input, workspace_id });
2058
+ },
2059
+ async uploadAsset(siteId, file, filename, opts) {
2060
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
2061
+ const query = new URLSearchParams({
2062
+ site_id: siteId,
2063
+ filename
2064
+ });
2065
+ if (opts?.width) query.set("width", String(opts.width));
2066
+ if (opts?.height) query.set("height", String(opts.height));
2067
+ const headers = {
2068
+ "Content-Type": opts?.contentType || file.type || "application/octet-stream"
2069
+ };
2070
+ if (options.authToken) {
2071
+ headers.Authorization = `Bearer ${options.authToken}`;
2072
+ }
2073
+ if (options.workspaceId) {
2074
+ headers["x-workspace-id"] = options.workspaceId;
2075
+ }
2076
+ try {
2077
+ const res = await fetch(`${baseUrl}/assets/upload?${query.toString()}`, {
2078
+ method: "POST",
2079
+ headers,
2080
+ body: file
2081
+ });
2082
+ const json = await res.json().catch(() => ({}));
2083
+ if (!res.ok) {
2084
+ return {
2085
+ data: null,
2086
+ ok: false,
2087
+ error: json.error || `HTTP ${res.status}: ${res.statusText}`
2088
+ };
2089
+ }
2090
+ return json;
2091
+ } catch (err) {
2092
+ return {
2093
+ data: null,
2094
+ ok: false,
2095
+ error: err instanceof Error ? err.message : "Network request failed"
2096
+ };
2097
+ }
2098
+ },
2099
+ async updateAsset(id, updates) {
2100
+ return this.patch(`/assets/${id}`, updates);
2101
+ },
2102
+ async deleteAsset(id) {
2103
+ return this.delete(`/assets/${id}`);
2104
+ },
2105
+ async getAssetUsage(id) {
2106
+ return this.get(`/assets/${id}/usage`);
2107
+ },
2108
+ async startMultipartUpload(input) {
2109
+ return this.post("/assets/multipart/start", input);
2110
+ },
2111
+ async uploadMultipartPart(params) {
2112
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
2113
+ const query = new URLSearchParams({
2114
+ assetId: params.assetId,
2115
+ uploadId: params.uploadId,
2116
+ n: String(params.partNumber)
2117
+ });
2118
+ const headers = {
2119
+ "Content-Type": "application/octet-stream"
2120
+ };
2121
+ if (options.authToken) {
2122
+ headers.Authorization = `Bearer ${options.authToken}`;
2123
+ }
2124
+ if (options.workspaceId) {
2125
+ headers["x-workspace-id"] = options.workspaceId;
2126
+ }
2127
+ try {
2128
+ const res = await fetch(`${baseUrl}/assets/multipart/part?${query.toString()}`, {
2129
+ method: "PUT",
2130
+ headers,
2131
+ body: params.data
2132
+ });
2133
+ const json = await res.json().catch(() => ({}));
2134
+ if (!res.ok) {
2135
+ return {
2136
+ data: null,
2137
+ ok: false,
2138
+ error: json.error || `HTTP ${res.status}: ${res.statusText}`
2139
+ };
2140
+ }
2141
+ return json;
2142
+ } catch (err) {
2143
+ return {
2144
+ data: null,
2145
+ ok: false,
2146
+ error: err instanceof Error ? err.message : "Network request failed"
2147
+ };
2148
+ }
2149
+ },
2150
+ async completeMultipartUpload(input) {
2151
+ return this.post("/assets/multipart/complete", input);
2152
+ },
2153
+ async abortMultipartUpload(input) {
2154
+ return this.post("/assets/multipart/abort", input);
2155
+ },
2156
+ async uploadAssetMultipart(siteId, file, opts) {
2157
+ let assetId = opts?.resume?.assetId;
2158
+ let uploadId = opts?.resume?.uploadId;
2159
+ const partSize = 8 * 1024 * 1024;
2160
+ const uploadedParts = [
2161
+ ...opts?.resume?.parts || []
2162
+ ];
2163
+ if (!assetId || !uploadId) {
2164
+ const startRes = await this.startMultipartUpload({
2165
+ site_id: siteId,
2166
+ filename: file.name,
2167
+ mime: file.type || "application/octet-stream",
2168
+ size: file.size,
2169
+ preset_set: opts?.presetSet
2170
+ });
2171
+ if (!startRes.ok || !startRes.data) {
2172
+ return {
2173
+ data: null,
2174
+ ok: false,
2175
+ error: startRes.error || "Failed to start multipart upload"
2176
+ };
2177
+ }
2178
+ assetId = startRes.data.assetId;
2179
+ uploadId = startRes.data.uploadId;
2180
+ }
2181
+ const totalParts = Math.ceil(file.size / partSize);
2182
+ try {
2183
+ for (let i = 0; i < totalParts; i++) {
2184
+ const partNumber = i + 1;
2185
+ if (uploadedParts.some((p) => p.partNumber === partNumber)) {
2186
+ if (opts?.onProgress) {
2187
+ opts.onProgress(Math.round((i + 1) / totalParts * 100));
2188
+ }
2189
+ continue;
2190
+ }
2191
+ const start = i * partSize;
2192
+ const end = Math.min(start + partSize, file.size);
2193
+ const chunk = file.slice(start, end);
2194
+ let partSuccess = false;
2195
+ let lastErr = "";
2196
+ for (let attempt = 1; attempt <= 3; attempt++) {
2197
+ const partRes = await this.uploadMultipartPart({
2198
+ assetId,
2199
+ uploadId,
2200
+ partNumber,
2201
+ data: chunk
2202
+ });
2203
+ if (partRes.ok && partRes.data?.etag) {
2204
+ uploadedParts.push({ partNumber, etag: partRes.data.etag });
2205
+ partSuccess = true;
2206
+ break;
2207
+ }
2208
+ lastErr = partRes.error || "Part upload failed";
2209
+ await new Promise((r) => setTimeout(r, attempt * 500));
2210
+ }
2211
+ if (!partSuccess) {
2212
+ if (opts?.abortOnFailure) {
2213
+ await this.abortMultipartUpload({ assetId, uploadId }).catch(() => {
2214
+ });
2215
+ }
2216
+ return {
2217
+ data: null,
2218
+ ok: false,
2219
+ error: `Failed to upload part ${partNumber}/${totalParts}: ${lastErr}`,
2220
+ resumeState: { assetId, uploadId, parts: uploadedParts }
2221
+ };
2222
+ }
2223
+ if (opts?.onProgress) {
2224
+ opts.onProgress(Math.round((i + 1) / totalParts * 100));
2225
+ }
2226
+ }
2227
+ return await this.completeMultipartUpload({
2228
+ assetId,
2229
+ uploadId,
2230
+ parts: uploadedParts.sort((a, b) => a.partNumber - b.partNumber)
2231
+ });
2232
+ } catch (err) {
2233
+ if (opts?.abortOnFailure) {
2234
+ await this.abortMultipartUpload({ assetId, uploadId }).catch(() => {
2235
+ });
2236
+ }
2237
+ return {
2238
+ data: null,
2239
+ ok: false,
2240
+ error: err instanceof Error ? err.message : "Multipart upload failed",
2241
+ resumeState: { assetId, uploadId, parts: uploadedParts }
2242
+ };
2243
+ }
2244
+ },
2245
+ // Redirects
2246
+ async getRedirects(siteId) {
2247
+ return this.get(`/redirects?site_id=${siteId}`);
2248
+ },
2249
+ async getRedirect(id) {
2250
+ return this.get(`/redirects/${id}`);
2251
+ },
2252
+ async createRedirect(input) {
2253
+ const workspace_id = input.workspace_id || options.workspaceId;
2254
+ return this.post("/redirects", { ...input, workspace_id });
2255
+ },
2256
+ async updateRedirect(id, updates) {
2257
+ return this.patch(`/redirects/${id}`, updates);
2258
+ },
2259
+ async deleteRedirect(id) {
2260
+ return this.delete(`/redirects/${id}`);
2261
+ },
2262
+ async reorderRedirect(id, prevOrderKey, nextOrderKey) {
2263
+ return this.post(`/redirects/${id}/reorder`, {
2264
+ prev_order_key: prevOrderKey,
2265
+ next_order_key: nextOrderKey
2266
+ });
2267
+ },
2268
+ // Site Settings
2269
+ async getSiteSettings(siteId) {
2270
+ return this.get(`/sites/${siteId}/settings`);
2271
+ },
2272
+ async updateSiteSettings(siteId, settings) {
2273
+ return this.put(`/sites/${siteId}/settings`, settings);
2274
+ },
2275
+ // Preview Tokens
2276
+ async createPreviewToken(siteId) {
2277
+ return this.post(`/sites/${siteId}/preview-token`);
2278
+ },
2279
+ // Builds
2280
+ async retryBuild(buildId) {
2281
+ return this.post(`/builds/${buildId}/retry`);
2282
+ },
2283
+ // Auth
2284
+ async getAccountAuthMethod(email) {
2285
+ return this.post("/auth/account-type", { email });
2286
+ },
2287
+ // Portal: Orgs
2288
+ async getOrgs(workspaceId) {
2289
+ const ws = workspaceId || options.workspaceId;
2290
+ const query = ws ? `?workspace_id=${ws}` : "";
2291
+ return this.get(`/orgs${query}`);
2292
+ },
2293
+ async getOrg(id) {
2294
+ return this.get(`/orgs/${id}`);
2295
+ },
2296
+ async createOrg(input) {
2297
+ const workspace_id = input.workspace_id || options.workspaceId;
2298
+ return this.post("/orgs", { ...input, workspace_id });
2299
+ },
2300
+ async updateOrg(id, updates) {
2301
+ return this.patch(`/orgs/${id}`, updates);
2302
+ },
2303
+ async deleteOrg(id) {
2304
+ return this.delete(`/orgs/${id}`);
2305
+ },
2306
+ async inviteOrgMember(orgId, input) {
2307
+ return this.post(`/orgs/${orgId}/members`, input);
2308
+ },
2309
+ // Workspaces
2310
+ async listWorkspaces() {
2311
+ return this.get("/workspaces");
2312
+ },
2313
+ async listWorkspaceMembers(workspaceId) {
2314
+ const ws = workspaceId || options.workspaceId;
2315
+ return this.get(`/workspaces/${ws}/members`);
2316
+ },
2317
+ async inviteWorkspaceMember(workspaceId, input) {
2318
+ return this.post(`/workspaces/${workspaceId}/members`, input);
2319
+ },
2320
+ async updateWorkspaceMemberRole(workspaceId, userId, role) {
2321
+ return this.patch(`/workspaces/${workspaceId}/members/${userId}`, {
2322
+ role
2323
+ });
2324
+ },
2325
+ async removeWorkspaceMember(workspaceId, userId) {
2326
+ return this.delete(`/workspaces/${workspaceId}/members/${userId}`);
2327
+ },
2328
+ // Portal: Projects
2329
+ async getProjects(workspaceId) {
2330
+ const ws = workspaceId || options.workspaceId;
2331
+ const query = ws ? `?workspace_id=${ws}` : "";
2332
+ return this.get(`/projects${query}`);
2333
+ },
2334
+ async getProject(id) {
2335
+ return this.get(`/projects/${id}`);
2336
+ },
2337
+ async getOrgProjects(orgId) {
2338
+ return this.get(`/orgs/${orgId}/projects`);
2339
+ },
2340
+ async createProject(orgId, input) {
2341
+ return this.post(`/orgs/${orgId}/projects`, input);
2342
+ },
2343
+ async updateProject(id, updates) {
2344
+ return this.patch(`/projects/${id}`, updates);
2345
+ },
2346
+ async deleteProject(id) {
2347
+ return this.delete(`/projects/${id}`);
2348
+ },
2349
+ async advanceProjectStage(projectId, to) {
2350
+ return this.post(
2351
+ `/projects/${projectId}/advance-stage`,
2352
+ { to }
2353
+ );
2354
+ },
2355
+ // Portal: Deliverables
2356
+ async getProjectDeliverables(projectId) {
2357
+ return this.get(`/projects/${projectId}/deliverables`);
2358
+ },
2359
+ async getProjectDeliverableFolders(projectId) {
2360
+ return this.get(`/projects/${projectId}/deliverable-folders`);
2361
+ },
2362
+ async createDeliverableFolder(projectId, payload) {
2363
+ return this.post(
2364
+ `/projects/${projectId}/deliverable-folders`,
2365
+ payload
2366
+ );
2367
+ },
2368
+ async updateDeliverableFolder(id, updates) {
2369
+ return this.patch(`/deliverable-folders/${id}`, updates);
2370
+ },
2371
+ async deleteDeliverableFolder(id) {
2372
+ return this.delete(`/deliverable-folders/${id}`);
2373
+ },
2374
+ async getOrgDeliverables(orgId) {
2375
+ return this.get(`/orgs/${orgId}/deliverables`);
2376
+ },
2377
+ async uploadDeliverable(projectId, file, opts) {
2378
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
2379
+ const query = new URLSearchParams({
2380
+ title: opts.title,
2381
+ filename: opts.filename
2382
+ });
2383
+ if (opts.kind) query.set("kind", opts.kind);
2384
+ if (opts.folderId) query.set("folder_id", opts.folderId);
2385
+ const headers = {
2386
+ "Content-Type": opts.contentType || file.type || "application/octet-stream"
2387
+ };
2388
+ if (options.authToken) {
2389
+ headers.Authorization = `Bearer ${options.authToken}`;
2390
+ }
2391
+ if (options.workspaceId) {
2392
+ headers["x-workspace-id"] = options.workspaceId;
2393
+ }
2394
+ try {
2395
+ const res = await fetch(
2396
+ `${baseUrl}/projects/${projectId}/deliverables/upload?${query.toString()}`,
2397
+ {
2398
+ method: "POST",
2399
+ headers,
2400
+ body: file
2401
+ }
2402
+ );
2403
+ const json = await res.json().catch(() => ({}));
2404
+ if (!res.ok) {
2405
+ return {
2406
+ data: null,
2407
+ ok: false,
2408
+ error: json.error || `HTTP ${res.status}: ${res.statusText}`
2409
+ };
2410
+ }
2411
+ return json;
2412
+ } catch (err) {
2413
+ return {
2414
+ data: null,
2415
+ ok: false,
2416
+ error: err instanceof Error ? err.message : "Network request failed"
2417
+ };
2418
+ }
2419
+ },
2420
+ async updateDeliverable(id, updates) {
2421
+ return this.patch(`/deliverables/${id}`, updates);
2422
+ },
2423
+ async deleteDeliverable(id) {
2424
+ return this.delete(`/deliverables/${id}`);
2425
+ },
2426
+ async getDeliverableDownloadUrl(id, opts) {
2427
+ return this.post(`/deliverables/${id}/download-url`, opts);
2428
+ },
2429
+ async getDeliverableShare(deliverableId) {
2430
+ return this.get(`/deliverables/${deliverableId}/share`);
2431
+ },
2432
+ async createDeliverableShare(deliverableId, payload) {
2433
+ return this.post(`/deliverables/${deliverableId}/share`, payload || {});
2434
+ },
2435
+ async revokeDeliverableShare(deliverableId) {
2436
+ return this.delete(`/deliverables/${deliverableId}/share`);
2437
+ },
2438
+ async getPublicShare(token) {
2439
+ return this.get(`/share/${token}`);
2440
+ },
2441
+ // Portal: Documents (Proposals & Contracts) & E-Signature
2442
+ async getProjectDocuments(projectId) {
2443
+ return this.get(`/projects/${projectId}/documents`);
2444
+ },
2445
+ async getDocument(documentId) {
2446
+ return this.get(`/documents/${documentId}`);
2447
+ },
2448
+ async getDocumentEvents(documentId) {
2449
+ return this.get(`/documents/${documentId}/events`);
2450
+ },
2451
+ async createDocument(projectId, file, opts) {
2452
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
2453
+ const query = new URLSearchParams({
2454
+ title: opts.title,
2455
+ type: opts.type || "proposal",
2456
+ filename: opts.filename || "document.pdf"
2457
+ });
2458
+ const headers = {
2459
+ "Content-Type": file.type || "application/pdf"
2460
+ };
2461
+ if (options.authToken) {
2462
+ headers.Authorization = `Bearer ${options.authToken}`;
2463
+ }
2464
+ if (options.workspaceId) {
2465
+ headers["x-workspace-id"] = options.workspaceId;
2466
+ }
2467
+ try {
2468
+ const res = await fetch(`${baseUrl}/projects/${projectId}/documents?${query.toString()}`, {
2469
+ method: "POST",
2470
+ headers,
2471
+ body: file
2472
+ });
2473
+ const json = await res.json().catch(() => ({}));
2474
+ if (!res.ok) {
2475
+ return {
2476
+ data: null,
2477
+ ok: false,
2478
+ error: json.error || `HTTP ${res.status}: ${res.statusText}`
2479
+ };
2480
+ }
2481
+ return json;
2482
+ } catch (err) {
2483
+ return {
2484
+ data: null,
2485
+ ok: false,
2486
+ error: err instanceof Error ? err.message : "Network request failed"
2487
+ };
2488
+ }
2489
+ },
2490
+ async approveDocument(documentId) {
2491
+ return this.post(`/documents/${documentId}/approve`);
2492
+ },
2493
+ async uploadDocumentVersion(documentId, file, opts) {
2494
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
2495
+ const query = new URLSearchParams({
2496
+ filename: opts?.filename || "document.pdf"
2497
+ });
2498
+ const headers = {
2499
+ "Content-Type": file.type || "application/pdf"
2500
+ };
2501
+ if (options.authToken) {
2502
+ headers.Authorization = `Bearer ${options.authToken}`;
2503
+ }
2504
+ if (options.workspaceId) {
2505
+ headers["x-workspace-id"] = options.workspaceId;
2506
+ }
2507
+ try {
2508
+ const res = await fetch(`${baseUrl}/documents/${documentId}/versions?${query.toString()}`, {
2509
+ method: "POST",
2510
+ headers,
2511
+ body: file
2512
+ });
2513
+ const json = await res.json().catch(() => ({}));
2514
+ if (!res.ok) {
2515
+ return {
2516
+ data: null,
2517
+ ok: false,
2518
+ error: json.error || `HTTP ${res.status}: ${res.statusText}`
2519
+ };
2520
+ }
2521
+ return json;
2522
+ } catch (err) {
2523
+ return {
2524
+ data: null,
2525
+ ok: false,
2526
+ error: err instanceof Error ? err.message : "Network request failed"
2527
+ };
2528
+ }
2529
+ },
2530
+ async uploadDocumentThumbnail(versionId, blob) {
2531
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
2532
+ const headers = { "Content-Type": "image/png" };
2533
+ if (options.authToken) {
2534
+ headers.Authorization = `Bearer ${options.authToken}`;
2535
+ }
2536
+ if (options.workspaceId) {
2537
+ headers["x-workspace-id"] = options.workspaceId;
2538
+ }
2539
+ try {
2540
+ const res = await fetch(`${baseUrl}/document-versions/${versionId}/thumbnail`, {
2541
+ method: "POST",
2542
+ headers,
2543
+ body: blob
2544
+ });
2545
+ const json = await res.json().catch(() => ({}));
2546
+ if (!res.ok) {
2547
+ return {
2548
+ data: null,
2549
+ ok: false,
2550
+ error: json.error || `HTTP ${res.status}: ${res.statusText}`
2551
+ };
2552
+ }
2553
+ return json;
2554
+ } catch (err) {
2555
+ return {
2556
+ data: null,
2557
+ ok: false,
2558
+ error: err instanceof Error ? err.message : "Network request failed"
2559
+ };
2560
+ }
2561
+ },
2562
+ async sendDocument(documentId, input) {
2563
+ return this.post(`/documents/${documentId}/send`, input);
2564
+ },
2565
+ async voidDocument(documentId, reason) {
2566
+ return this.post(`/documents/${documentId}/void`, { reason });
2567
+ },
2568
+ async getDocumentSigningLinks(documentId) {
2569
+ return this.get(`/documents/${documentId}/signing-links`);
2570
+ },
2571
+ async syncDocumentStatus(documentId) {
2572
+ return this.post(`/documents/${documentId}/sync-status`);
2573
+ },
2574
+ async getDocumentVersionViewUrl(versionId, kind = "original") {
2575
+ return this.post(`/document-versions/${versionId}/view-url?kind=${kind}`);
2576
+ },
2577
+ async getDocumentSignedUrl(envelopeId, kind = "signed") {
2578
+ return this.post(
2579
+ `/document-envelopes/${envelopeId}/signed-url?kind=${kind}`
2580
+ );
2581
+ },
2582
+ async getProjectDocumentRecipients(projectId) {
2583
+ return this.get(`/projects/${projectId}/document-recipients`);
2584
+ },
2585
+ // Backwards-compatible aliases for proposals
2586
+ async getProjectProposals(projectId) {
2587
+ return this.getProjectDocuments(projectId);
2588
+ },
2589
+ async getProposal(proposalId) {
2590
+ return this.getDocument(proposalId);
2591
+ },
2592
+ async getProposalEvents(proposalId) {
2593
+ return this.getDocumentEvents(proposalId);
2594
+ },
2595
+ async createProposal(projectId, file, opts) {
2596
+ return this.createDocument(projectId, file, { ...opts, type: "proposal" });
2597
+ },
2598
+ async uploadProposalVersion(proposalId, file, opts) {
2599
+ return this.uploadDocumentVersion(proposalId, file, opts);
2600
+ },
2601
+ async sendProposal(proposalId, input) {
2602
+ return this.sendDocument(proposalId, input);
2603
+ },
2604
+ async voidProposal(proposalId, reason) {
2605
+ return this.voidDocument(proposalId, reason);
2606
+ },
2607
+ async getProposalSigningLinks(proposalId) {
2608
+ return this.getDocumentSigningLinks(proposalId);
2609
+ },
2610
+ async getProposalVersionViewUrl(versionId, kind = "original") {
2611
+ return this.getDocumentVersionViewUrl(versionId, kind);
2612
+ },
2613
+ async getProposalSignedUrl(envelopeId, kind = "signed") {
2614
+ return this.getDocumentSignedUrl(envelopeId, kind);
2615
+ },
2616
+ async getProjectProposalRecipients(projectId) {
2617
+ return this.getProjectDocumentRecipients(projectId);
2618
+ },
2619
+ // Site Services (form submissions inbox)
2620
+ async getSiteSubmissions(siteId, opts) {
2621
+ const params = new URLSearchParams();
2622
+ if (opts?.status) params.set("status", opts.status);
2623
+ if (opts?.cursor) params.set("cursor", opts.cursor);
2624
+ if (opts?.limit) params.set("limit", String(opts.limit));
2625
+ const query = params.toString();
2626
+ return this.get(`/sites/${siteId}/submissions${query ? `?${query}` : ""}`);
2627
+ },
2628
+ async getSiteSubmission(siteId, submissionId) {
2629
+ return this.get(`/sites/${siteId}/submissions/${submissionId}`);
2630
+ },
2631
+ async updateSiteSubmission(siteId, submissionId, updates) {
2632
+ return this.patch(`/sites/${siteId}/submissions/${submissionId}`, updates);
2633
+ },
2634
+ async getSiteServicesHealth(siteId) {
2635
+ return this.get(`/sites/${siteId}/services/health`);
2636
+ },
2637
+ // Site Insights / Analytics
2638
+ async getSiteInsights(siteId, query, opts) {
2639
+ const params = new URLSearchParams();
2640
+ if (opts?.from !== void 0) params.set("from", String(opts.from));
2641
+ if (opts?.to !== void 0) params.set("to", String(opts.to));
2642
+ if (opts?.interval) params.set("interval", opts.interval);
2643
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
2644
+ if (opts?.tier) params.set("tier", opts.tier);
2645
+ if (opts?.sessionization) params.set("sessionization", opts.sessionization);
2646
+ if (opts?.dimension) params.set("dimension", opts.dimension);
2647
+ if (opts?.filters && opts.filters.length > 0) {
2648
+ params.set("filters", JSON.stringify(opts.filters));
2649
+ }
2650
+ const q = params.toString();
2651
+ return this.get(`/sites/${siteId}/insights/${query}${q ? `?${q}` : ""}`);
2652
+ },
2653
+ async getSiteInsightsDeep(siteId, query, params) {
2654
+ const sp = new URLSearchParams();
2655
+ if (params) {
2656
+ for (const [k, v] of Object.entries(params)) {
2657
+ if (v !== void 0 && v !== null) {
2658
+ if (typeof v === "object") {
2659
+ sp.set(k, JSON.stringify(v));
2660
+ } else {
2661
+ sp.set(k, String(v));
2662
+ }
2663
+ }
2664
+ }
2665
+ }
2666
+ const q = sp.toString();
2667
+ return this.get(
2668
+ `/sites/${siteId}/insights/deep/${query}${q ? `?${q}` : ""}`
2669
+ );
2670
+ },
2671
+ async getSiteInsightsCoverage(siteId) {
2672
+ return this.get(`/sites/${siteId}/insights/coverage`);
2673
+ },
2674
+ async getSiteContentPerformance(siteId, opts) {
2675
+ const params = new URLSearchParams();
2676
+ if (opts?.from !== void 0) params.set("from", String(opts.from));
2677
+ if (opts?.to !== void 0) params.set("to", String(opts.to));
2678
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
2679
+ if (opts?.collection) params.set("collection", opts.collection);
2680
+ const q = params.toString();
2681
+ return this.get(
2682
+ `/sites/${siteId}/insights/content${q ? `?${q}` : ""}`
2683
+ );
2684
+ },
2685
+ async getSiteInsightsDrift(siteId) {
2686
+ return this.get(`/sites/${siteId}/insights/drift`);
2687
+ },
2688
+ async getSiteVisitors(siteId, opts) {
2689
+ const params = new URLSearchParams();
2690
+ if (opts?.day) params.set("day", opts.day);
2691
+ if (opts?.from !== void 0) params.set("from", String(opts.from));
2692
+ if (opts?.to !== void 0) params.set("to", String(opts.to));
2693
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
2694
+ if (opts?.before !== void 0) params.set("before", String(opts.before));
2695
+ if (opts?.filters && opts.filters.length > 0) {
2696
+ params.set("filters", JSON.stringify(opts.filters));
2697
+ }
2698
+ const q = params.toString();
2699
+ return this.get(
2700
+ `/sites/${siteId}/insights/visitors${q ? `?${q}` : ""}`
2701
+ );
2702
+ },
2703
+ async getSiteVisitorJourney(siteId, visitor, opts) {
2704
+ const params = new URLSearchParams();
2705
+ if (opts?.day) params.set("day", opts.day);
2706
+ if (opts?.from !== void 0) params.set("from", String(opts.from));
2707
+ if (opts?.to !== void 0) params.set("to", String(opts.to));
2708
+ const q = params.toString();
2709
+ return this.get(
2710
+ `/sites/${siteId}/insights/visitors/${encodeURIComponent(visitor)}${q ? `?${q}` : ""}`
2711
+ );
2712
+ },
2713
+ // Google Search Console
2714
+ async getSearchConsoleConnection(siteId) {
2715
+ return this.get(`/sites/${siteId}/search-console`);
2716
+ },
2717
+ async startSearchConsoleOAuth(siteId) {
2718
+ return this.post(`/sites/${siteId}/search-console/oauth/start`, {});
2719
+ },
2720
+ async listSearchConsoleProperties(siteId) {
2721
+ return this.get(
2722
+ `/sites/${siteId}/search-console/properties`
2723
+ );
2724
+ },
2725
+ async bindSearchConsoleProperty(siteId, payload) {
2726
+ return this.post(
2727
+ `/sites/${siteId}/search-console/property`,
2728
+ payload
2729
+ );
2730
+ },
2731
+ async syncSearchConsole(siteId) {
2732
+ return this.post(
2733
+ `/sites/${siteId}/search-console/sync`,
2734
+ {}
2735
+ );
2736
+ },
2737
+ async disconnectSearchConsole(siteId) {
2738
+ return this.delete(`/sites/${siteId}/search-console`);
2739
+ },
2740
+ async getSearchConsoleQueries(siteId, opts) {
2741
+ const params = new URLSearchParams();
2742
+ if (opts?.from !== void 0) params.set("from", String(opts.from));
2743
+ if (opts?.to !== void 0) params.set("to", String(opts.to));
2744
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
2745
+ const q = params.toString();
2746
+ return this.get(
2747
+ `/sites/${siteId}/search-console/queries${q ? `?${q}` : ""}`
2748
+ );
2749
+ },
2750
+ async getSearchConsolePages(siteId, opts) {
2751
+ const params = new URLSearchParams();
2752
+ if (opts?.from !== void 0) params.set("from", String(opts.from));
2753
+ if (opts?.to !== void 0) params.set("to", String(opts.to));
2754
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
2755
+ const q = params.toString();
2756
+ return this.get(
2757
+ `/sites/${siteId}/search-console/pages${q ? `?${q}` : ""}`
2758
+ );
2759
+ },
2760
+ // Marketing & Campaigns
2761
+ async listCampaignEntries(siteId) {
2762
+ return this.get(`/sites/${siteId}/campaigns/entries`);
2763
+ },
2764
+ async getCampaignPreview(siteId, entryId) {
2765
+ return this.get(
2766
+ `/sites/${siteId}/campaigns/preview?entryId=${encodeURIComponent(entryId)}`
2767
+ );
2768
+ },
2769
+ async createCampaignSend(siteId, entryId) {
2770
+ return this.post(`/sites/${siteId}/campaigns/sends`, { entryId });
2771
+ },
2772
+ async dispatchCampaignSend(siteId, sendId) {
2773
+ return this.post(`/sites/${siteId}/campaigns/sends/${sendId}/dispatch`);
2774
+ },
2775
+ async testSendCampaign(siteId, entryId, to) {
2776
+ return this.post(`/sites/${siteId}/campaigns/test-send`, {
2777
+ entryId,
2778
+ to
2779
+ });
2780
+ },
2781
+ async listCampaignSends(siteId, opts) {
2782
+ const params = new URLSearchParams();
2783
+ if (opts?.limit) params.set("limit", String(opts.limit));
2784
+ if (opts?.cursor) params.set("cursor", opts.cursor);
2785
+ const q = params.toString();
2786
+ return this.get(
2787
+ `/sites/${siteId}/campaigns/sends${q ? `?${q}` : ""}`
2788
+ );
2789
+ },
2790
+ async getCampaignSend(siteId, sendId) {
2791
+ return this.get(`/sites/${siteId}/campaigns/sends/${sendId}`);
2792
+ },
2793
+ async listSubscribers(siteId, opts) {
2794
+ const params = new URLSearchParams();
2795
+ if (opts?.status) params.set("status", opts.status);
2796
+ if (opts?.limit) params.set("limit", String(opts.limit));
2797
+ if (opts?.cursor) params.set("cursor", opts.cursor);
2798
+ const q = params.toString();
2799
+ return this.get(
2800
+ `/sites/${siteId}/subscribers${q ? `?${q}` : ""}`
2801
+ );
2802
+ },
2803
+ async getSubscriberStats(siteId) {
2804
+ return this.get(`/sites/${siteId}/subscribers/stats`);
2805
+ },
2806
+ async getMarketingConfig(siteId) {
2807
+ return this.get(`/sites/${siteId}/marketing/config`);
2808
+ },
2809
+ // SMS Marketing
2810
+ async listSmsCampaignEntries(siteId) {
2811
+ return this.get(`/sites/${siteId}/sms/entries`);
2812
+ },
2813
+ async getSmsPreview(siteId, entryId) {
2814
+ return this.get(
2815
+ `/sites/${siteId}/sms/preview?entryId=${encodeURIComponent(entryId)}`
2816
+ );
2817
+ },
2818
+ async createSmsSend(siteId, entryId) {
2819
+ return this.post(`/sites/${siteId}/sms/sends`, { entryId });
2820
+ },
2821
+ async dispatchSmsSend(siteId, sendId) {
2822
+ return this.post(`/sites/${siteId}/sms/sends/${sendId}/dispatch`);
2823
+ },
2824
+ async testSendSms(siteId, entryId, to) {
2825
+ return this.post(`/sites/${siteId}/sms/test-send`, {
2826
+ entryId,
2827
+ to
2828
+ });
2829
+ },
2830
+ async listSmsSends(siteId, opts) {
2831
+ const params = new URLSearchParams();
2832
+ if (opts?.limit) params.set("limit", String(opts.limit));
2833
+ if (opts?.cursor) params.set("cursor", opts.cursor);
2834
+ const q = params.toString();
2835
+ return this.get(
2836
+ `/sites/${siteId}/sms/sends${q ? `?${q}` : ""}`
2837
+ );
2838
+ },
2839
+ async getSmsSend(siteId, sendId) {
2840
+ return this.get(`/sites/${siteId}/sms/sends/${sendId}`);
2841
+ },
2842
+ async listSmsSubscribers(siteId, opts) {
2843
+ const params = new URLSearchParams();
2844
+ if (opts?.status) params.set("status", opts.status);
2845
+ if (opts?.limit) params.set("limit", String(opts.limit));
2846
+ if (opts?.cursor) params.set("cursor", opts.cursor);
2847
+ const q = params.toString();
2848
+ return this.get(
2849
+ `/sites/${siteId}/sms/subscribers${q ? `?${q}` : ""}`
2850
+ );
2851
+ },
2852
+ async getSmsSubscriberStats(siteId) {
2853
+ return this.get(`/sites/${siteId}/sms/subscribers/stats`);
2854
+ },
2855
+ async getSmsConfig(siteId) {
2856
+ return this.get(`/sites/${siteId}/sms/config`);
2857
+ },
2858
+ async provisionSms(siteId) {
2859
+ return this.post(
2860
+ `/sites/${siteId}/sms/provision`
2861
+ );
2862
+ },
2863
+ async setSmsMessagingServiceSid(siteId, sid) {
2864
+ return this.put(
2865
+ `/sites/${siteId}/sms/messaging-service-sid`,
2866
+ { sid }
2867
+ );
2868
+ },
2869
+ async listSiteMembers(siteId, opts) {
2870
+ const params = new URLSearchParams();
2871
+ if (opts?.limit) params.set("limit", String(opts.limit));
2872
+ if (opts?.cursor) params.set("cursor", opts.cursor);
2873
+ const q = params.toString();
2874
+ return this.get(
2875
+ `/sites/${siteId}/members${q ? `?${q}` : ""}`
2876
+ );
2877
+ },
2878
+ async getSiteMemberStats(siteId) {
2879
+ return this.get(`/sites/${siteId}/members/stats`);
2880
+ },
2881
+ async deleteSiteMember(siteId, memberId) {
2882
+ return this.delete(`/sites/${siteId}/members/${memberId}`);
2883
+ },
2884
+ // Linear Mirror & Status Board
2885
+ async getProjectBoard(projectId) {
2886
+ return this.get(`/projects/${projectId}/board`);
2887
+ },
2888
+ async getProjectActivity(projectId, days = 30) {
2889
+ return this.get(`/projects/${projectId}/activity?days=${days}`);
2890
+ },
2891
+ async getProjectLinearLink(projectId) {
2892
+ return this.get(`/projects/${projectId}/linear-link`);
2893
+ },
2894
+ async setProjectLinearLink(projectId, linearProjectId) {
2895
+ return this.put(`/projects/${projectId}/linear-link`, {
2896
+ linear_project_id: linearProjectId
2897
+ });
2898
+ },
2899
+ async deleteProjectLinearLink(projectId) {
2900
+ return this.delete(`/projects/${projectId}/linear-link`);
2901
+ },
2902
+ async listLinearProjects() {
2903
+ return this.get("/linear/projects");
2904
+ },
2905
+ async refreshLinearProjects() {
2906
+ return this.post("/linear/projects/refresh");
2907
+ },
2908
+ async resyncProjectLinear(projectId) {
2909
+ return this.post(`/projects/${projectId}/linear-resync`);
2910
+ },
2911
+ // Billing & Stripe Mirror
2912
+ async getProjectBilling(projectId) {
2913
+ return this.get(`/projects/${projectId}/billing`);
2914
+ },
2915
+ async getOrgBilling(orgId, projectId) {
2916
+ const q = projectId ? `?project_id=${encodeURIComponent(projectId)}` : "";
2917
+ return this.get(`/orgs/${orgId}/billing${q}`);
2918
+ },
2919
+ async getBillingOverview() {
2920
+ return this.get("/billing/overview");
2921
+ },
2922
+ async getAgencyHome() {
2923
+ return this.get("/home/agency");
2924
+ },
2925
+ async createBillingPortalSession(orgId) {
2926
+ return this.post(`/orgs/${orgId}/billing/portal-session`);
2927
+ },
2928
+ async linkBillingAccount(orgId, stripeCustomerId) {
2929
+ return this.put(`/orgs/${orgId}/billing/account`, {
2930
+ stripe_customer_id: stripeCustomerId
2931
+ });
2932
+ },
2933
+ async unlinkBillingAccount(orgId) {
2934
+ return this.delete(`/orgs/${orgId}/billing/account`);
2935
+ },
2936
+ async getBillingCustomerDefaults(orgId) {
2937
+ return this.get(`/orgs/${orgId}/billing/customer-defaults`);
2938
+ },
2939
+ async createBillingCustomer(orgId, input) {
2940
+ return this.post(`/orgs/${orgId}/billing/customer`, {
2941
+ name: input.name,
2942
+ email: input.email,
2943
+ description: input.description,
2944
+ allow_duplicate: input.allowDuplicate
2945
+ });
2946
+ },
2947
+ // Price Book, Monthly Close, and Usage
2948
+ async getBillingUsage(orgId, period) {
2949
+ const q = period ? `&period=${encodeURIComponent(period)}` : "";
2950
+ return this.get(`/billing/usage?orgId=${encodeURIComponent(orgId)}${q}`);
2951
+ },
2952
+ async getPriceBook(orgId) {
2953
+ const q = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
2954
+ return this.get(
2955
+ `/billing/price-book${q}`
2956
+ );
2957
+ },
2958
+ async updatePriceBook(entry) {
2959
+ return this.put("/billing/price-book", entry);
2960
+ },
2961
+ async previewClose(orgId, period) {
2962
+ return this.post("/billing/closes/preview", { orgId, period });
2963
+ },
2964
+ async commitClose(closeRunId) {
2965
+ return this.post(`/billing/closes/${closeRunId}/commit`);
2966
+ },
2967
+ async getCloseRuns(params) {
2968
+ const q = new URLSearchParams();
2969
+ if (params?.org) q.set("org", params.org);
2970
+ if (params?.period) q.set("period", params.period);
2971
+ const queryStr = q.toString() ? `?${q.toString()}` : "";
2972
+ return this.get(`/billing/closes${queryStr}`);
2973
+ },
2974
+ async getBillingWorklist() {
2975
+ return this.get("/billing/worklist");
2976
+ },
2977
+ async getBillingMargin(period) {
2978
+ const q = period ? `?period=${encodeURIComponent(period)}` : "";
2979
+ return this.get(`/billing/margin${q}`);
2980
+ },
2981
+ // Global search across sites, projects, deliverables, documents, invoices, routes, collections, assets
2982
+ async search(q, opts) {
2983
+ const params = new URLSearchParams();
2984
+ params.set("q", q);
2985
+ if (opts?.siteId) params.set("site_id", opts.siteId);
2986
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
2987
+ return this.get(`/search?${params.toString()}`);
2988
+ }
2989
+ };
2990
+ }
2991
+
2992
+ // src/credentials.ts
2993
+ import fs5 from "fs";
2994
+ import os from "os";
2995
+ import path5 from "path";
2996
+ import { createClient } from "@supabase/supabase-js";
2997
+ function normalizeApiUrl(url) {
2998
+ return url.trim().replace(/\/+$/, "");
2999
+ }
3000
+ function getDefaultCredentialsPath() {
3001
+ if (process.env.CMS_CREDENTIALS_PATH) {
3002
+ return path5.resolve(process.env.CMS_CREDENTIALS_PATH);
3003
+ }
3004
+ return path5.join(os.homedir(), ".config", "407dev", "credentials.json");
3005
+ }
3006
+ function loadAllCredentials(customPath) {
3007
+ const filePath = customPath || getDefaultCredentialsPath();
3008
+ if (!fs5.existsSync(filePath)) {
3009
+ return {};
3010
+ }
3011
+ try {
3012
+ const raw = fs5.readFileSync(filePath, "utf-8");
3013
+ return JSON.parse(raw);
3014
+ } catch {
3015
+ return {};
3016
+ }
3017
+ }
3018
+ function getStoredCredentials(apiUrl, customPath) {
3019
+ const all = loadAllCredentials(customPath);
3020
+ const normalized = normalizeApiUrl(apiUrl);
3021
+ return all[normalized] || null;
3022
+ }
3023
+ function saveStoredCredentials(apiUrl, creds, customPath) {
3024
+ const filePath = customPath || getDefaultCredentialsPath();
3025
+ const dir = path5.dirname(filePath);
3026
+ if (!fs5.existsSync(dir)) {
3027
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
3028
+ } else {
3029
+ try {
3030
+ fs5.chmodSync(dir, 448);
3031
+ } catch {
3032
+ }
3033
+ }
3034
+ const all = loadAllCredentials(customPath);
3035
+ const normalized = normalizeApiUrl(apiUrl);
3036
+ all[normalized] = creds;
3037
+ fs5.writeFileSync(filePath, JSON.stringify(all, null, 2), {
3038
+ encoding: "utf-8",
3039
+ mode: 384
3040
+ });
3041
+ try {
3042
+ fs5.chmodSync(filePath, 384);
3043
+ } catch {
3044
+ }
3045
+ }
3046
+ function removeStoredCredentials(apiUrl, customPath) {
3047
+ const filePath = customPath || getDefaultCredentialsPath();
3048
+ if (!fs5.existsSync(filePath)) {
3049
+ return false;
3050
+ }
3051
+ const all = loadAllCredentials(customPath);
3052
+ const normalized = normalizeApiUrl(apiUrl);
3053
+ if (!(normalized in all)) {
3054
+ return false;
3055
+ }
3056
+ delete all[normalized];
3057
+ fs5.writeFileSync(filePath, JSON.stringify(all, null, 2), {
3058
+ encoding: "utf-8",
3059
+ mode: 384
3060
+ });
3061
+ try {
3062
+ fs5.chmodSync(filePath, 384);
3063
+ } catch {
3064
+ }
3065
+ return true;
3066
+ }
3067
+ async function refreshCredentialsIfNeeded(apiUrl, creds, customPath, fetchFn = fetch) {
3068
+ const now = Math.floor(Date.now() / 1e3);
3069
+ if (creds.expires_at - now >= 60) {
3070
+ return creds;
3071
+ }
3072
+ const normalized = normalizeApiUrl(apiUrl);
3073
+ const configRes = await fetchFn(`${normalized}/cli/config`);
3074
+ if (!configRes.ok) {
3075
+ throw new Error(
3076
+ `Failed to fetch CLI config from ${normalized} (status ${configRes.status}): ${configRes.statusText}`
3077
+ );
3078
+ }
3079
+ const configData = await configRes.json();
3080
+ const supabaseUrl = configData.supabaseUrl || configData.data?.supabaseUrl || configData.supabase_url;
3081
+ const supabaseAnonKey = configData.supabaseAnonKey || configData.data?.supabaseAnonKey || configData.supabase_anon_key;
3082
+ if (!supabaseUrl || !supabaseAnonKey) {
3083
+ throw new Error(`Invalid CLI config returned from ${normalized}: missing Supabase credentials`);
3084
+ }
3085
+ const supabase = createClient(supabaseUrl, supabaseAnonKey, {
3086
+ auth: {
3087
+ persistSession: false,
3088
+ autoRefreshToken: false
3089
+ }
3090
+ });
3091
+ const { data, error } = await supabase.auth.refreshSession({
3092
+ refresh_token: creds.refresh_token
3093
+ });
3094
+ if (error || !data.session) {
3095
+ throw new Error(`Failed to refresh session: ${error?.message || "unknown error"}`);
3096
+ }
3097
+ const updated = {
3098
+ access_token: data.session.access_token,
3099
+ refresh_token: data.session.refresh_token,
3100
+ expires_at: data.session.expires_at || Math.floor(Date.now() / 1e3) + 3600,
3101
+ email: data.session.user?.email || creds.email
3102
+ };
3103
+ saveStoredCredentials(apiUrl, updated, customPath);
3104
+ return updated;
3105
+ }
3106
+
3107
+ // src/jwt.ts
3108
+ import crypto2 from "crypto";
3109
+ function signDevJwt(secret, workspaceId = "00000000-0000-0000-0000-000000000001") {
3110
+ const header = { alg: "HS256", typ: "JWT" };
3111
+ const payload = {
3112
+ aud: "authenticated",
3113
+ role: "authenticated",
3114
+ sub: "00000000-0000-0000-0000-000000000003",
3115
+ email: "dev@example.com",
3116
+ app_metadata: {
3117
+ workspace_id: workspaceId,
3118
+ role: "owner",
3119
+ workspaces: {
3120
+ [workspaceId]: { role: "owner" }
3121
+ }
3122
+ },
3123
+ user_metadata: { role: "owner" },
3124
+ iat: Math.floor(Date.now() / 1e3),
3125
+ exp: Math.floor(Date.now() / 1e3) + 3600 * 24 * 365
3126
+ };
3127
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString("base64url");
3128
+ const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url");
3129
+ const data = `${encodedHeader}.${encodedPayload}`;
3130
+ const signature = crypto2.createHmac("sha256", secret).update(data).digest("base64url");
3131
+ return `${data}.${signature}`;
3132
+ }
3133
+
3134
+ // src/auth.ts
3135
+ async function resolveAuthToken(apiUrl = process.env.CMS_API_URL || DEFAULT_DEV_API_URL, customCredentialsPath) {
3136
+ const envToken = process.env.CMS_AUTH_TOKEN || process.env.SUPABASE_ACCESS_TOKEN;
3137
+ if (envToken?.trim()) {
3138
+ return envToken.trim();
3139
+ }
3140
+ const normalized = normalizeApiUrl(apiUrl);
3141
+ const creds = getStoredCredentials(normalized, customCredentialsPath);
3142
+ if (creds) {
3143
+ try {
3144
+ const refreshed = await refreshCredentialsIfNeeded(normalized, creds, customCredentialsPath);
3145
+ return refreshed.access_token;
3146
+ } catch {
3147
+ }
3148
+ }
3149
+ if (normalized.includes("127.0.0.1") || normalized.includes("localhost")) {
3150
+ const secret = process.env.SUPABASE_JWT_SECRET || "super-secret-jwt-token-with-at-least-32-characters-long";
3151
+ try {
3152
+ return await signDevJwt(secret);
3153
+ } catch {
3154
+ }
3155
+ }
3156
+ throw new Error(`Not logged in to ${normalized}. Run: cms login`);
3157
+ }
3158
+
3159
+ // src/env.ts
3160
+ import fs6 from "fs";
3161
+ import path6 from "path";
3162
+ function loadEnvFiles(siteDir = process.cwd()) {
3163
+ const envFiles = [
3164
+ path6.join(siteDir, ".env"),
3165
+ path6.join(siteDir, ".env.local"),
3166
+ path6.join(siteDir, ".env.development"),
3167
+ path6.join(process.cwd(), ".env"),
3168
+ path6.join(process.cwd(), ".env.local")
3169
+ ];
3170
+ const seen = /* @__PURE__ */ new Set();
3171
+ for (const file of envFiles) {
3172
+ const resolved = path6.resolve(file);
3173
+ if (seen.has(resolved)) continue;
3174
+ seen.add(resolved);
3175
+ if (fs6.existsSync(resolved)) {
3176
+ try {
3177
+ const content = fs6.readFileSync(resolved, "utf-8");
3178
+ for (const line of content.split("\n")) {
3179
+ const trimmed = line.trim();
3180
+ if (!trimmed || trimmed.startsWith("#")) continue;
3181
+ const eqIdx = trimmed.indexOf("=");
3182
+ if (eqIdx > 0) {
3183
+ const key = trimmed.slice(0, eqIdx).trim();
3184
+ let val = trimmed.slice(eqIdx + 1).trim();
3185
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
3186
+ val = val.slice(1, -1);
3187
+ }
3188
+ if (process.env[key] === void 0) {
3189
+ process.env[key] = val;
3190
+ }
3191
+ }
3192
+ }
3193
+ } catch {
3194
+ }
3195
+ }
3196
+ }
3197
+ }
3198
+
3199
+ // src/commands/link.ts
3200
+ function mergeEnvContent(content, vars) {
3201
+ const lines = content ? content.split("\n") : [];
3202
+ const remainingKeys = new Set(Object.keys(vars));
3203
+ const updatedLines = lines.map((line) => {
3204
+ const trimmed = line.trim();
3205
+ if (!trimmed || trimmed.startsWith("#")) return line;
3206
+ const eqIdx = line.indexOf("=");
3207
+ if (eqIdx > 0) {
3208
+ const key = line.slice(0, eqIdx).trim();
3209
+ if (remainingKeys.has(key)) {
3210
+ remainingKeys.delete(key);
3211
+ return `${key}=${vars[key]}`;
3212
+ }
3213
+ }
3214
+ return line;
3215
+ });
3216
+ if (updatedLines.length > 0 && updatedLines[updatedLines.length - 1].trim() !== "" && remainingKeys.size > 0) {
3217
+ updatedLines.push("");
3218
+ }
3219
+ for (const key of remainingKeys) {
3220
+ updatedLines.push(`${key}=${vars[key]}`);
3221
+ }
3222
+ let result = updatedLines.join("\n");
3223
+ if (!result.endsWith("\n")) {
3224
+ result += "\n";
3225
+ }
3226
+ return result;
3227
+ }
3228
+ function mergeEnvFile(filePath, vars) {
3229
+ const existing = fs7.existsSync(filePath) ? fs7.readFileSync(filePath, "utf-8") : "";
3230
+ const merged = mergeEnvContent(existing, vars);
3231
+ fs7.writeFileSync(filePath, merged, "utf-8");
3232
+ }
3233
+ async function defaultSelectPrompt(sites) {
3234
+ if (!process.stdin.isTTY) {
3235
+ if (sites.length === 1) {
3236
+ return sites[0];
3237
+ }
3238
+ throw new Error("Multiple sites available. Non-interactive shell must specify --site <id>.");
3239
+ }
3240
+ console.log("\nAvailable sites:");
3241
+ sites.forEach((s, idx) => {
3242
+ console.log(` [${idx + 1}] ${s.name} (${s.id})`);
3243
+ });
3244
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3245
+ try {
3246
+ while (true) {
3247
+ const ans = await rl.question(`
3248
+ Select site [1-${sites.length}]: `);
3249
+ const num = Number.parseInt(ans.trim(), 10);
3250
+ if (!Number.isNaN(num) && num >= 1 && num <= sites.length) {
3251
+ return sites[num - 1];
3252
+ }
3253
+ console.log(`Invalid selection. Please enter a number between 1 and ${sites.length}.`);
3254
+ }
3255
+ } finally {
3256
+ rl.close();
3257
+ }
3258
+ }
3259
+ async function executeLink(options = {}) {
3260
+ const siteDir = options.siteDir || process.cwd();
3261
+ loadEnvFiles(siteDir);
3262
+ const apiUrl = normalizeApiUrl(options.apiUrl || process.env.CMS_API_URL || DEFAULT_DEV_API_URL);
3263
+ let contentUrl = DEFAULT_DEV_CONTENT_URL;
3264
+ try {
3265
+ const configRes = await fetch(`${apiUrl}/cli/config`);
3266
+ if (configRes.ok) {
3267
+ const configJson = await configRes.json();
3268
+ contentUrl = configJson.contentUrl || configJson.data?.contentUrl || configJson.content_url || DEFAULT_DEV_CONTENT_URL;
3269
+ }
3270
+ } catch {
3271
+ }
3272
+ let apiClient = options.apiClient;
3273
+ if (!apiClient) {
3274
+ const authToken = await resolveAuthToken(apiUrl);
3275
+ apiClient = createApiClient({
3276
+ baseUrl: apiUrl,
3277
+ authToken
3278
+ });
3279
+ }
3280
+ let selectedSiteId = options.siteId;
3281
+ let selectedSiteName = "";
3282
+ if (selectedSiteId) {
3283
+ const siteRes = await apiClient.getSite(selectedSiteId);
3284
+ if (siteRes.ok && siteRes.data) {
3285
+ selectedSiteName = siteRes.data.name;
3286
+ }
3287
+ } else {
3288
+ const sitesRes = await apiClient.getSites();
3289
+ if (!sitesRes.ok || !sitesRes.data || sitesRes.data.length === 0) {
3290
+ throw new Error("No sites found in workspace. Create a site in the web dashboard first.");
3291
+ }
3292
+ const sites = sitesRes.data;
3293
+ const promptFn = options.selectPrompt || defaultSelectPrompt;
3294
+ const chosen = await promptFn(sites);
3295
+ selectedSiteId = chosen.id;
3296
+ selectedSiteName = chosen.name;
3297
+ }
3298
+ if (!selectedSiteId) {
3299
+ throw new Error("Failed to resolve site ID to link");
3300
+ }
3301
+ const envPath = path7.join(siteDir, ".env");
3302
+ mergeEnvFile(envPath, {
3303
+ CMS_SITE_ID: selectedSiteId,
3304
+ CMS_API_URL: apiUrl,
3305
+ CMS_CONTENT_URL: contentUrl
3306
+ });
3307
+ let astroConfigCheck = "";
3308
+ const possibleConfigs = [
3309
+ "astro.config.mjs",
3310
+ "astro.config.ts",
3311
+ "astro.config.js",
3312
+ "astro.config.cjs"
3313
+ ];
3314
+ let configFound = false;
3315
+ let configConfigured = false;
3316
+ for (const cfg of possibleConfigs) {
3317
+ const fullPath = path7.join(siteDir, cfg);
3318
+ if (fs7.existsSync(fullPath)) {
3319
+ configFound = true;
3320
+ try {
3321
+ const content = fs7.readFileSync(fullPath, "utf-8");
3322
+ if (content.includes("cmsAstro") || content.includes("@407dev/cms-astro")) {
3323
+ configConfigured = true;
3324
+ }
3325
+ } catch {
3326
+ }
3327
+ break;
3328
+ }
3329
+ }
3330
+ if (!configConfigured) {
3331
+ astroConfigCheck = `
3332
+ Next steps:
3333
+ Add @407dev/cms-astro to your astro.config.mjs:
3334
+ import { cmsAstro } from '@407dev/cms-astro';
3335
+
3336
+ export default defineConfig({
3337
+ integrations: [cmsAstro()],
3338
+ });
3339
+ `;
3340
+ }
3341
+ const siteDisplay = selectedSiteName ? `"${selectedSiteName}" (${selectedSiteId})` : selectedSiteId;
3342
+ return `\u2728 Linked site ${siteDisplay}
3343
+ Updated .env:
3344
+ CMS_SITE_ID=${selectedSiteId}
3345
+ CMS_API_URL=${apiUrl}
3346
+ CMS_CONTENT_URL=${contentUrl}
3347
+ ${astroConfigCheck ? `${astroConfigCheck}
3348
+ ` : ""}Ready to push schema: run "cms push"`;
3349
+ }
3350
+
3351
+ // src/commands/login.ts
3352
+ import { spawn } from "child_process";
3353
+ import crypto3 from "crypto";
3354
+ import http from "http";
3355
+ async function executeLogin(options = {}) {
3356
+ const apiUrl = normalizeApiUrl(options.apiUrl || process.env.CMS_API_URL || DEFAULT_DEV_API_URL);
3357
+ const openBrowser = options.openBrowser ?? true;
3358
+ const timeoutMs = options.timeoutMs ?? 3e5;
3359
+ let webUrl = DEFAULT_DEV_WEB_URL;
3360
+ try {
3361
+ const configRes = await fetch(`${apiUrl}/cli/config`);
3362
+ if (configRes.ok) {
3363
+ const configJson = await configRes.json();
3364
+ webUrl = configJson.webUrl || configJson.data?.webUrl || configJson.web_url || DEFAULT_DEV_WEB_URL;
3365
+ }
3366
+ } catch {
3367
+ }
3368
+ const state = crypto3.randomBytes(16).toString("hex");
3369
+ return new Promise((resolve3, reject) => {
3370
+ let timeoutId = null;
3371
+ const server = http.createServer((req, res) => {
3372
+ res.setHeader("Access-Control-Allow-Origin", "*");
3373
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
3374
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
3375
+ if (req.method === "OPTIONS") {
3376
+ res.writeHead(204);
3377
+ res.end();
3378
+ return;
3379
+ }
3380
+ if (req.method === "POST" && req.url === "/callback") {
3381
+ let body = "";
3382
+ req.on("data", (chunk) => {
3383
+ body += chunk;
3384
+ });
3385
+ req.on("end", () => {
3386
+ let payload = {};
3387
+ const contentType = req.headers["content-type"] || "";
3388
+ if (contentType.includes("application/json")) {
3389
+ try {
3390
+ payload = JSON.parse(body);
3391
+ } catch {
3392
+ payload = {};
3393
+ }
3394
+ } else {
3395
+ const params = new URLSearchParams(body);
3396
+ payload = Object.fromEntries(params.entries());
3397
+ }
3398
+ if (!payload.state || payload.state !== state) {
3399
+ res.writeHead(400, { "Content-Type": "text/plain" });
3400
+ res.end("Invalid state parameter");
3401
+ return;
3402
+ }
3403
+ const accessToken = typeof payload.access_token === "string" ? payload.access_token : void 0;
3404
+ const refreshToken = typeof payload.refresh_token === "string" ? payload.refresh_token : void 0;
3405
+ const expiresAt = Number(payload.expires_at) || Math.floor(Date.now() / 1e3) + 3600;
3406
+ const email = typeof payload.email === "string" ? payload.email : void 0;
3407
+ if (!accessToken || !refreshToken) {
3408
+ res.writeHead(400, { "Content-Type": "text/plain" });
3409
+ res.end("Missing access_token or refresh_token in callback payload");
3410
+ return;
3411
+ }
3412
+ saveStoredCredentials(apiUrl, {
3413
+ access_token: accessToken,
3414
+ refresh_token: refreshToken,
3415
+ expires_at: expiresAt,
3416
+ email
3417
+ });
3418
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3419
+ res.end(`<!DOCTYPE html>
3420
+ <html>
3421
+ <head><title>Authenticated</title></head>
3422
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #fafafa;">
3423
+ <div style="background: #ffffff; padding: 2rem 3rem; border-radius: 8px; border: 1px solid #eaeaea; text-align: center; max-width: 420px; box-shadow: 0 4px 12px rgba(0,0,0,0.05);">
3424
+ <h2 style="margin-top: 0; color: #111;">Authenticated Successfully</h2>
3425
+ <p style="color: #666; font-size: 14px;">You can now close this tab and return to your terminal.</p>
3426
+ </div>
3427
+ </body>
3428
+ </html>`);
3429
+ if (timeoutId) clearTimeout(timeoutId);
3430
+ server.close();
3431
+ const emailMsg = email ? ` as ${email}` : "";
3432
+ resolve3(`\u2728 Logged in successfully${emailMsg} (${apiUrl})`);
3433
+ });
3434
+ return;
3435
+ }
3436
+ res.writeHead(404, { "Content-Type": "text/plain" });
3437
+ res.end("Not Found");
3438
+ });
3439
+ server.listen(0, "127.0.0.1", () => {
3440
+ const address = server.address();
3441
+ const port = address.port;
3442
+ const authorizeUrl = `${webUrl.replace(/\/+$/, "")}/cli/authorize?port=${port}&state=${state}`;
3443
+ console.log(`
3444
+ Opening browser to authenticate:
3445
+ ${authorizeUrl}
3446
+ `);
3447
+ if (openBrowser) {
3448
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3449
+ try {
3450
+ spawn(cmd, [authorizeUrl], { detached: true, stdio: "ignore" }).unref();
3451
+ } catch {
3452
+ }
3453
+ }
3454
+ timeoutId = setTimeout(() => {
3455
+ server.close();
3456
+ reject(new Error("Login timed out after 5 minutes. Please try again."));
3457
+ }, timeoutMs);
3458
+ });
3459
+ server.on("error", (err) => {
3460
+ if (timeoutId) clearTimeout(timeoutId);
3461
+ reject(err);
3462
+ });
3463
+ });
3464
+ }
3465
+
3466
+ // src/commands/logout.ts
3467
+ function executeLogout(options = {}) {
3468
+ const apiUrl = normalizeApiUrl(options.apiUrl || process.env.CMS_API_URL || DEFAULT_DEV_API_URL);
3469
+ const removed = removeStoredCredentials(apiUrl, options.customCredentialsPath);
3470
+ if (removed) {
3471
+ return `Logged out from ${apiUrl}.`;
3472
+ }
3473
+ return `Not currently logged in to ${apiUrl}.`;
3474
+ }
3475
+
3476
+ // src/commands/push.ts
3477
+ import fs8 from "fs";
3478
+ import path8 from "path";
3479
+ import { hashSchema } from "@407dev/blocks";
3480
+
3481
+ // src/manifest-loader.ts
3482
+ import { existsSync } from "fs";
3483
+ import { resolve } from "path";
3484
+ import { pathToFileURL } from "url";
3485
+ async function loadManifest(pathOrObject) {
3486
+ if (typeof pathOrObject === "object" && pathOrObject !== null) {
3487
+ if (pathOrObject.manifestVersion !== 1) {
3488
+ throw new Error(`Unsupported manifestVersion: ${pathOrObject.manifestVersion}`);
3489
+ }
3490
+ return pathOrObject;
3491
+ }
3492
+ const manifestPath = pathOrObject ? resolve(pathOrObject) : [
3493
+ resolve(process.cwd(), "cms.manifest.ts"),
3494
+ resolve(process.cwd(), "cms.manifest.js"),
3495
+ resolve(process.cwd(), "cms.config.ts"),
3496
+ resolve(process.cwd(), "cms.config.js")
3497
+ ].find((p) => existsSync(p));
3498
+ if (!manifestPath || !existsSync(manifestPath)) {
3499
+ throw new Error(`Manifest file not found. Expected cms.manifest.ts in ${process.cwd()}`);
3500
+ }
3501
+ const fileUrl = pathToFileURL(manifestPath).href;
3502
+ const mod = await import(fileUrl);
3503
+ const manifest = mod.manifest || (mod.default && "manifestVersion" in mod.default ? mod.default : mod.default?.manifest);
3504
+ if (!manifest) {
3505
+ throw new Error(`No manifest export found in ${manifestPath}`);
3506
+ }
3507
+ if (manifest.manifestVersion !== 1) {
3508
+ throw new Error(`Unsupported manifestVersion: ${manifest.manifestVersion}`);
3509
+ }
3510
+ return manifest;
3511
+ }
3512
+
3513
+ // src/migrations.ts
3514
+ async function runEntryMigrations(apiClient, siteId, migrations) {
3515
+ let migratedCount = 0;
3516
+ const errors = [];
3517
+ for (const m of migrations) {
3518
+ const listRes = await apiClient.listEntries(siteId, m.collection);
3519
+ if (!listRes.ok || !listRes.data) {
3520
+ errors.push(`Failed to list entries for collection '${m.collection}': ${listRes.error}`);
3521
+ continue;
3522
+ }
3523
+ const entriesToMigrate = listRes.data.filter((e) => e.schema_version === m.fromVersion);
3524
+ for (const entry of entriesToMigrate) {
3525
+ try {
3526
+ const migratedContent = m.migrate(entry.content || {});
3527
+ const updateRes = await apiClient.updateEntry(entry.id, {
3528
+ content: migratedContent,
3529
+ expected_updated_at: entry.updated_at
3530
+ });
3531
+ if (updateRes.ok) {
3532
+ migratedCount++;
3533
+ } else {
3534
+ errors.push(
3535
+ `Failed to migrate entry '${entry.slug}' in '${m.collection}': ${updateRes.error}`
3536
+ );
3537
+ }
3538
+ } catch (err) {
3539
+ errors.push(
3540
+ `Exception in migration for entry '${entry.slug}' in '${m.collection}': ${err instanceof Error ? err.message : String(err)}`
3541
+ );
3542
+ }
3543
+ }
3544
+ }
3545
+ return { migratedCount, errors };
3546
+ }
3547
+
3548
+ // src/state-store.ts
3549
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
3550
+ import { dirname, resolve as resolve2 } from "path";
3551
+ var StateStore = class {
3552
+ filePath;
3553
+ constructor(customPath) {
3554
+ this.filePath = customPath ? resolve2(customPath) : resolve2(process.cwd(), ".cms", "state.json");
3555
+ }
3556
+ load() {
3557
+ if (!existsSync2(this.filePath)) {
3558
+ return { sites: {} };
3559
+ }
3560
+ try {
3561
+ const content = readFileSync(this.filePath, "utf-8");
3562
+ return JSON.parse(content);
3563
+ } catch {
3564
+ return { sites: {} };
3565
+ }
3566
+ }
3567
+ getSiteManifestHash(siteId) {
3568
+ const data = this.load();
3569
+ return data.sites[siteId]?.manifestHash || null;
3570
+ }
3571
+ saveSiteManifestHash(siteId, hash) {
3572
+ const data = this.load();
3573
+ if (!data.sites) {
3574
+ data.sites = {};
3575
+ }
3576
+ data.sites[siteId] = {
3577
+ manifestHash: hash,
3578
+ lastPushedAt: (/* @__PURE__ */ new Date()).toISOString()
3579
+ };
3580
+ try {
3581
+ const dir = dirname(this.filePath);
3582
+ if (!existsSync2(dir)) {
3583
+ mkdirSync(dir, { recursive: true });
3584
+ }
3585
+ writeFileSync(this.filePath, JSON.stringify(data, null, 2), "utf-8");
3586
+ } catch (err) {
3587
+ const message = err instanceof Error ? err.message : String(err);
3588
+ console.warn(`[CMS Warning] Could not save CLI state: ${message}`);
3589
+ }
3590
+ }
3591
+ };
3592
+
3593
+ // src/commands/push.ts
3594
+ async function defaultConfirm(message) {
3595
+ if (!process.stdin.isTTY) {
3596
+ return false;
3597
+ }
3598
+ const readline2 = await import("readline/promises");
3599
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3600
+ try {
3601
+ const answer = await rl.question(`${message} [y/N] `);
3602
+ return /^y(es)?$/i.test(answer.trim());
3603
+ } finally {
3604
+ rl.close();
3605
+ }
3606
+ }
3607
+ async function previewOrphans(apiClient, siteId, manifest) {
3608
+ if (typeof apiClient.getFields !== "function" || !manifest) {
3609
+ return { fieldKeys: [], collectionKeys: [] };
3610
+ }
3611
+ const [fieldsRes, collectionsRes] = await Promise.all([
3612
+ apiClient.getFields(siteId),
3613
+ typeof apiClient.getCollections === "function" ? apiClient.getCollections(siteId) : Promise.resolve({ ok: true, data: [] })
3614
+ ]);
3615
+ const existingFields = fieldsRes.ok ? fieldsRes.data || [] : [];
3616
+ const existingCollections = collectionsRes.ok ? collectionsRes.data || [] : [];
3617
+ return {
3618
+ fieldKeys: existingFields.filter((f) => !f.orphaned_at && !(f.key in manifest.fields)).map((f) => f.key),
3619
+ collectionKeys: existingCollections.filter((c) => !manifest.collections?.[c.key]).map((c) => c.key)
3620
+ };
3621
+ }
3622
+ async function executePush(options) {
3623
+ const siteDir = options.siteDir || process.cwd();
3624
+ loadEnvFiles(siteDir);
3625
+ const siteId = options.siteId || process.env.CMS_SITE_ID;
3626
+ if (!siteId) {
3627
+ return {
3628
+ success: false,
3629
+ hash: "",
3630
+ warnings: [],
3631
+ errors: ["site ID is required. Pass --site <id> or set CMS_SITE_ID in .env."]
3632
+ };
3633
+ }
3634
+ const { configPath, force, dryRun, check, yes, noSync, statePath, renames, migrations } = options;
3635
+ const warnings = [];
3636
+ const errors = [];
3637
+ let manifest = options.manifest;
3638
+ if (!manifest) {
3639
+ const srcDir = path8.join(siteDir, "src");
3640
+ const hasSourceDir = fs8.existsSync(srcDir) && fs8.statSync(srcDir).isDirectory();
3641
+ if (hasSourceDir && !configPath) {
3642
+ try {
3643
+ const extractResult = await extractProject(siteDir, { noSync });
3644
+ for (const w of extractResult.warnings) {
3645
+ warnings.push(`${w.file}:${w.line}:${w.column}: ${w.message}`);
3646
+ }
3647
+ if (extractResult.errors.length > 0) {
3648
+ const formattedErrors = extractResult.errors.map(
3649
+ (e) => `${e.file}:${e.line}:${e.column}: ${e.message}`
3650
+ );
3651
+ if (check) {
3652
+ return {
3653
+ success: false,
3654
+ hash: "",
3655
+ warnings,
3656
+ errors: formattedErrors
3657
+ };
3658
+ }
3659
+ errors.push(...formattedErrors);
3660
+ }
3661
+ manifest = extractResult.manifest;
3662
+ } catch (err) {
3663
+ const msg = err instanceof Error ? err.message : String(err);
3664
+ return {
3665
+ success: false,
3666
+ hash: "",
3667
+ warnings,
3668
+ errors: [`Extraction failed: ${msg}`]
3669
+ };
3670
+ }
3671
+ } else {
3672
+ try {
3673
+ manifest = await loadManifest(configPath);
3674
+ } catch (err) {
3675
+ const msg = err instanceof Error ? err.message : String(err);
3676
+ return {
3677
+ success: false,
3678
+ hash: "",
3679
+ warnings,
3680
+ errors: [msg]
3681
+ };
3682
+ }
3683
+ }
3684
+ }
3685
+ const hash = hashSchema(manifest);
3686
+ const stateStore = new StateStore(statePath);
3687
+ const lastHash = stateStore.getSiteManifestHash(siteId);
3688
+ if (lastHash === hash && !force) {
3689
+ return {
3690
+ success: true,
3691
+ skipped: true,
3692
+ hash,
3693
+ warnings: ["Manifest is unchanged. Use --force to push anyway."],
3694
+ errors: []
3695
+ };
3696
+ }
3697
+ const baseUrl = process.env.CMS_API_URL || DEFAULT_DEV_API_URL;
3698
+ let apiClient = options.apiClient;
3699
+ if (!apiClient) {
3700
+ try {
3701
+ const authToken = await resolveAuthToken(baseUrl);
3702
+ apiClient = createApiClient({
3703
+ baseUrl,
3704
+ authToken
3705
+ });
3706
+ } catch (err) {
3707
+ if (!dryRun) {
3708
+ const message = err instanceof Error ? err.message : "Authentication required. Run: cms login";
3709
+ return {
3710
+ success: false,
3711
+ hash,
3712
+ warnings,
3713
+ errors: [message]
3714
+ };
3715
+ }
3716
+ }
3717
+ }
3718
+ if (dryRun) {
3719
+ const dryRunWarnings = ["Dry run complete. No changes were applied."];
3720
+ if (apiClient && manifest?.collections) {
3721
+ try {
3722
+ const [collectionsRes, statsRes] = await Promise.all([
3723
+ typeof apiClient.getCollections === "function" ? apiClient.getCollections(siteId) : Promise.resolve({ ok: true, data: [] }),
3724
+ typeof apiClient.getCollectionStats === "function" ? apiClient.getCollectionStats(siteId) : Promise.resolve({ ok: true, data: {} })
3725
+ ]);
3726
+ const existingCollections = collectionsRes.ok ? collectionsRes.data || [] : [];
3727
+ const existingStats = statsRes.ok && statsRes.data ? statsRes.data : {};
3728
+ const wouldSeed = [];
3729
+ for (const [key, coll] of Object.entries(manifest.collections)) {
3730
+ if (coll.seed && coll.seed.length > 0) {
3731
+ const existing = existingCollections.find((c) => c.key === key);
3732
+ const stats = existingStats[key];
3733
+ const entryCount = stats ? stats.draft + stats.published + stats.scheduled : 0;
3734
+ if (!existing || !existing.seeded_at && entryCount === 0) {
3735
+ wouldSeed.push({ key, count: coll.seed.length });
3736
+ }
3737
+ }
3738
+ }
3739
+ if (wouldSeed.length > 0) {
3740
+ const list = wouldSeed.map((s) => `${s.key} (${s.count} ${s.count === 1 ? "entry" : "entries"})`).join(", ");
3741
+ dryRunWarnings.push(`Would seed ${list}.`);
3742
+ }
3743
+ } catch {
3744
+ }
3745
+ }
3746
+ return {
3747
+ success: true,
3748
+ hash,
3749
+ warnings: dryRunWarnings,
3750
+ errors: []
3751
+ };
3752
+ }
3753
+ if (!apiClient) {
3754
+ return {
3755
+ success: false,
3756
+ hash,
3757
+ warnings,
3758
+ errors: ["Authentication required. Run: cms login"]
3759
+ };
3760
+ }
3761
+ if (!yes) {
3762
+ const { fieldKeys, collectionKeys } = await previewOrphans(apiClient, siteId, manifest);
3763
+ if (fieldKeys.length > 0 || collectionKeys.length > 0) {
3764
+ const parts = [];
3765
+ if (fieldKeys.length > 0) parts.push(`${fieldKeys.length} field(s): ${fieldKeys.join(", ")}`);
3766
+ if (collectionKeys.length > 0) {
3767
+ parts.push(`${collectionKeys.length} collection(s): ${collectionKeys.join(", ")}`);
3768
+ }
3769
+ const confirmFn = options.confirm || defaultConfirm;
3770
+ const proceed = await confirmFn(
3771
+ `This push will orphan ${parts.join("; ")}. Stored values are preserved but will stop syncing from code. Continue?`
3772
+ );
3773
+ if (!proceed) {
3774
+ return {
3775
+ success: false,
3776
+ aborted: true,
3777
+ hash,
3778
+ warnings,
3779
+ errors: [
3780
+ "Push aborted: orphaned fields/collections were not confirmed. Re-run with --yes to skip this prompt."
3781
+ ]
3782
+ };
3783
+ }
3784
+ }
3785
+ }
3786
+ const pushRes = await apiClient.pushManifest(siteId, manifest, renames);
3787
+ if (!pushRes.ok || !pushRes.data) {
3788
+ return {
3789
+ success: false,
3790
+ hash,
3791
+ warnings,
3792
+ errors: [pushRes.error || "Failed to push manifest to CMS API"]
3793
+ };
3794
+ }
3795
+ const report = pushRes.data;
3796
+ if (report.warnings && report.warnings.length > 0) {
3797
+ warnings.push(...report.warnings);
3798
+ }
3799
+ let migrationsRun = 0;
3800
+ if (migrations && migrations.length > 0) {
3801
+ const migrationResult = await runEntryMigrations(apiClient, siteId, migrations);
3802
+ migrationsRun = migrationResult.migratedCount;
3803
+ if (migrationResult.errors.length > 0) {
3804
+ errors.push(...migrationResult.errors);
3805
+ }
3806
+ }
3807
+ stateStore.saveSiteManifestHash(siteId, hash);
3808
+ return {
3809
+ success: errors.length === 0,
3810
+ hash,
3811
+ report,
3812
+ migrationsRun,
3813
+ warnings,
3814
+ errors
3815
+ };
3816
+ }
3817
+
3818
+ // src/commands/skills.ts
3819
+ import fs9 from "fs";
3820
+ import path9 from "path";
3821
+ import { fileURLToPath } from "url";
3822
+ function resolveBundledSkillsDir() {
3823
+ let dir = path9.dirname(fileURLToPath(import.meta.url));
3824
+ while (true) {
3825
+ const pkgPath = path9.join(dir, "package.json");
3826
+ if (fs9.existsSync(pkgPath)) {
3827
+ const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
3828
+ if (pkg.name === "@407dev/cli") return path9.join(dir, "skills");
3829
+ }
3830
+ const parent = path9.dirname(dir);
3831
+ if (parent === dir) throw new Error("Could not locate bundled skills for @407dev/cli");
3832
+ dir = parent;
3833
+ }
3834
+ }
3835
+ function listBundledSkills(sourceDir = resolveBundledSkillsDir()) {
3836
+ return fs9.readdirSync(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory() && fs9.existsSync(path9.join(sourceDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
3837
+ }
3838
+ function listFilesRecursive(root, rel = "") {
3839
+ const files = [];
3840
+ for (const entry of fs9.readdirSync(path9.join(root, rel), { withFileTypes: true })) {
3841
+ const entryRel = path9.join(rel, entry.name);
3842
+ if (entry.isDirectory()) files.push(...listFilesRecursive(root, entryRel));
3843
+ else if (entry.isFile()) files.push(entryRel);
3844
+ }
3845
+ return files.sort();
3846
+ }
3847
+ function installSkills(options = {}) {
3848
+ const sourceDir = options.sourceDir || resolveBundledSkillsDir();
3849
+ const destRoot = path9.join(options.targetDir || process.cwd(), ".claude", "skills");
3850
+ const results = [];
3851
+ for (const name of listBundledSkills(sourceDir)) {
3852
+ const srcSkillDir = path9.join(sourceDir, name);
3853
+ const destSkillDir = path9.join(destRoot, name);
3854
+ const existedBefore = fs9.existsSync(destSkillDir);
3855
+ const conflicts = [];
3856
+ let wroteAny = false;
3857
+ for (const rel of listFilesRecursive(srcSkillDir)) {
3858
+ const srcContent = fs9.readFileSync(path9.join(srcSkillDir, rel));
3859
+ const destPath = path9.join(destSkillDir, rel);
3860
+ if (fs9.existsSync(destPath)) {
3861
+ if (fs9.readFileSync(destPath).equals(srcContent)) continue;
3862
+ if (!options.force) {
3863
+ conflicts.push(rel);
3864
+ continue;
3865
+ }
3866
+ }
3867
+ fs9.mkdirSync(path9.dirname(destPath), { recursive: true });
3868
+ fs9.writeFileSync(destPath, srcContent);
3869
+ wroteAny = true;
3870
+ }
3871
+ let status = "unchanged";
3872
+ if (conflicts.length > 0) status = "conflict";
3873
+ else if (!existedBefore) status = "installed";
3874
+ else if (wroteAny) status = "updated";
3875
+ results.push({ name, status, conflicts });
3876
+ }
3877
+ return results;
3878
+ }
3879
+ function executeSkillsInstall(options = {}) {
3880
+ const targetDir = path9.resolve(options.targetDir || process.cwd());
3881
+ const results = installSkills({ ...options, targetDir });
3882
+ const lines = [`Agent skills in ${path9.join(targetDir, ".claude", "skills")}:`];
3883
+ for (const r of results) {
3884
+ if (r.status === "conflict") {
3885
+ lines.push(
3886
+ ` ! ${r.name}: kept local edits to ${r.conflicts.join(", ")} (re-run with --force to overwrite)`
3887
+ );
3888
+ } else {
3889
+ lines.push(` \u2713 ${r.name} (${r.status})`);
3890
+ }
3891
+ }
3892
+ return lines.join("\n");
3893
+ }
3894
+ function executeSkillsList() {
3895
+ return ["Bundled agent skills:", ...listBundledSkills().map((n) => ` - ${n}`)].join("\n");
3896
+ }
3897
+
3898
+ // src/commands/whoami.ts
3899
+ async function executeWhoami(options = {}) {
3900
+ const apiUrl = normalizeApiUrl(options.apiUrl || process.env.CMS_API_URL || DEFAULT_DEV_API_URL);
3901
+ const creds = getStoredCredentials(apiUrl, options.customCredentialsPath);
3902
+ if (creds) {
3903
+ try {
3904
+ const refreshed = await refreshCredentialsIfNeeded(
3905
+ apiUrl,
3906
+ creds,
3907
+ options.customCredentialsPath
3908
+ );
3909
+ const email = refreshed.email || "authenticated user";
3910
+ return `Logged in to ${apiUrl} as ${email}`;
3911
+ } catch {
3912
+ return `Logged in to ${apiUrl} (stored credentials expired or refresh failed)`;
3913
+ }
3914
+ }
3915
+ const envToken = process.env.CMS_AUTH_TOKEN || process.env.SUPABASE_ACCESS_TOKEN;
3916
+ if (envToken?.trim()) {
3917
+ return `Logged in to ${apiUrl} via environment token (CMS_AUTH_TOKEN)`;
3918
+ }
3919
+ if (apiUrl.includes("127.0.0.1") || apiUrl.includes("localhost")) {
3920
+ return `Using local development credentials for ${apiUrl}`;
3921
+ }
3922
+ return `Not logged in to ${apiUrl}. Run "cms login" to authenticate.`;
3923
+ }
3924
+
3925
+ // src/index.ts
3926
+ var VERSION = "0.0.1";
3927
+ function printHelp() {
3928
+ return `
3929
+ 407dev CMS CLI - v${VERSION}
3930
+
3931
+ Usage:
3932
+ cms login [options] Authenticate CLI via browser session
3933
+ cms logout [options] Log out and remove local credentials
3934
+ cms whoami [options] Show current authentication status
3935
+ cms link [options] Link local repository to a CMS site
3936
+ cms push [options] Push code-first site manifest to the CMS API
3937
+ cms check [options] Validate templates, scopes, groups, collections, and routes
3938
+ cms extract [options] Extract code-first site manifest from AST
3939
+ cms skills install [dir] Install CMS agent skills into <dir>/.claude/skills
3940
+ cms skills list List bundled CMS agent skills
3941
+
3942
+ Commands & Options:
3943
+ login:
3944
+ --api-url <url> API URL (default: CMS_API_URL or local dev)
3945
+
3946
+ logout:
3947
+ --api-url <url> API URL (default: CMS_API_URL or local dev)
3948
+
3949
+ whoami:
3950
+ --api-url <url> API URL (default: CMS_API_URL or local dev)
3951
+
3952
+ link:
3953
+ -s, --site <id> Site ID to link (prompted interactively if omitted)
3954
+ --api-url <url> API URL (default: CMS_API_URL or local dev)
3955
+
3956
+ push:
3957
+ -s, --site <id> Site ID (default: env CMS_SITE_ID)
3958
+ -c, --config <path> Path to cms.manifest.ts (default: static AST extraction)
3959
+ --check Run static AST validation checks before pushing
3960
+ -y, --yes Automatically accept orphaned fields and changes
3961
+ --no-sync Skip running 'astro sync' when deriving routes
3962
+ --force Force push even if local manifest hash matches state
3963
+ --dry-run Validate manifest and compute diff without modifying database
3964
+
3965
+ check & extract:
3966
+ --json Output raw manifest as JSON (extract only)
3967
+ --no-sync Skip running 'astro sync' when deriving routes
3968
+
3969
+ skills install:
3970
+ --force Overwrite skill files that were edited locally
3971
+
3972
+ Global Options:
3973
+ -v, --version Show CLI version
3974
+ -h, --help Show help information
3975
+ `;
3976
+ }
3977
+ function formatPushReport(result) {
3978
+ if (result.skipped) {
3979
+ return "\u2728 Manifest is unchanged. Nothing to push.";
3980
+ }
3981
+ if (result.aborted) {
3982
+ return `\u{1F6D1} ${result.errors.join("\n")}`;
3983
+ }
3984
+ const lines = [];
3985
+ lines.push("\u{1F4E6} Manifest Push Summary:");
3986
+ lines.push(` Hash: ${result.hash}`);
3987
+ if (result.report) {
3988
+ const r = result.report;
3989
+ lines.push(` Created items: ${r.created}`);
3990
+ lines.push(` Updated items: ${r.updated}`);
3991
+ lines.push(` Unchanged items: ${r.unchanged}`);
3992
+ lines.push(` Renamed items: ${r.renamed}`);
3993
+ if (r.orphaned_fields.length > 0) {
3994
+ lines.push(` Orphaned fields: ${r.orphaned_fields.map((f) => f.key).join(", ")}`);
3995
+ }
3996
+ if (r.orphaned_collections.length > 0) {
3997
+ lines.push(
3998
+ ` Orphaned collections: ${r.orphaned_collections.map((c) => `${c.key} (${c.entry_count} entries)`).join(", ")}`
3999
+ );
4000
+ }
4001
+ if (r.seeded && r.seeded.length > 0) {
4002
+ const seededList = r.seeded.map((s) => `${s.key} (${s.count} ${s.count === 1 ? "entry" : "entries"})`).join(", ");
4003
+ lines.push(` Seeded ${seededList} \u2014 publish to make them live.`);
4004
+ }
4005
+ if (r.unpublished_collections && r.unpublished_collections.length > 0) {
4006
+ const unpublishedList = r.unpublished_collections.map((c) => `${c.key} (${c.entry_count} entries)`).join(", ");
4007
+ lines.push(
4008
+ ` \u26A0\uFE0F Unpublished: ${unpublishedList} \u2014 publish in the dashboard to make them live (rebuilding or redeploying won't).`
4009
+ );
4010
+ }
4011
+ lines.push(` Routes: +${r.routes.added} / -${r.routes.removed}`);
4012
+ }
4013
+ if (result.migrationsRun) {
4014
+ lines.push(` Entry migrations applied: ${result.migrationsRun}`);
4015
+ }
4016
+ if (result.warnings.length > 0) {
4017
+ lines.push("\n\u26A0\uFE0F Warnings:");
4018
+ for (const w of result.warnings) {
4019
+ lines.push(` - ${w}`);
4020
+ }
4021
+ }
4022
+ if (result.errors.length > 0) {
4023
+ lines.push("\n\u274C Errors:");
4024
+ for (const e of result.errors) {
4025
+ lines.push(` - ${e}`);
4026
+ }
4027
+ }
4028
+ return lines.join("\n");
4029
+ }
4030
+ async function runCli(args = []) {
4031
+ const targetDir = args[1] && !args[1].startsWith("-") ? args[1] : process.cwd();
4032
+ loadEnvFiles(targetDir);
4033
+ if (args.includes("--version") || args.includes("-v")) {
4034
+ return VERSION;
4035
+ }
4036
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
4037
+ return printHelp();
4038
+ }
4039
+ const command = args[0];
4040
+ if (command === "login") {
4041
+ const rawOptions = parseArgs({
4042
+ args: args.slice(1),
4043
+ options: {
4044
+ "api-url": { type: "string" },
4045
+ help: { type: "boolean", short: "h" }
4046
+ },
4047
+ allowPositionals: true,
4048
+ strict: false
4049
+ });
4050
+ if (rawOptions.values.help) {
4051
+ return printHelp();
4052
+ }
4053
+ return await executeLogin({
4054
+ apiUrl: rawOptions.values["api-url"] || void 0
4055
+ });
4056
+ }
4057
+ if (command === "logout") {
4058
+ const rawOptions = parseArgs({
4059
+ args: args.slice(1),
4060
+ options: {
4061
+ "api-url": { type: "string" },
4062
+ help: { type: "boolean", short: "h" }
4063
+ },
4064
+ allowPositionals: true,
4065
+ strict: false
4066
+ });
4067
+ if (rawOptions.values.help) {
4068
+ return printHelp();
4069
+ }
4070
+ return executeLogout({
4071
+ apiUrl: rawOptions.values["api-url"] || void 0
4072
+ });
4073
+ }
4074
+ if (command === "whoami") {
4075
+ const rawOptions = parseArgs({
4076
+ args: args.slice(1),
4077
+ options: {
4078
+ "api-url": { type: "string" },
4079
+ help: { type: "boolean", short: "h" }
4080
+ },
4081
+ allowPositionals: true,
4082
+ strict: false
4083
+ });
4084
+ if (rawOptions.values.help) {
4085
+ return printHelp();
4086
+ }
4087
+ return await executeWhoami({
4088
+ apiUrl: rawOptions.values["api-url"] || void 0
4089
+ });
4090
+ }
4091
+ if (command === "link") {
4092
+ const rawOptions = parseArgs({
4093
+ args: args.slice(1),
4094
+ options: {
4095
+ site: { type: "string", short: "s" },
4096
+ "site-id": { type: "string" },
4097
+ "api-url": { type: "string" },
4098
+ help: { type: "boolean", short: "h" }
4099
+ },
4100
+ allowPositionals: true,
4101
+ strict: false
4102
+ });
4103
+ if (rawOptions.values.help) {
4104
+ return printHelp();
4105
+ }
4106
+ const siteId = rawOptions.values.site || rawOptions.values["site-id"] || void 0;
4107
+ return await executeLink({
4108
+ siteId,
4109
+ apiUrl: rawOptions.values["api-url"] || void 0,
4110
+ siteDir: rawOptions.positionals[0] || void 0
4111
+ });
4112
+ }
4113
+ if (command === "extract") {
4114
+ const rawOptions = parseArgs({
4115
+ args: args.slice(1),
4116
+ options: {
4117
+ json: { type: "boolean" },
4118
+ "no-sync": { type: "boolean" },
4119
+ help: { type: "boolean", short: "h" }
4120
+ },
4121
+ allowPositionals: true,
4122
+ strict: false
4123
+ });
4124
+ if (rawOptions.values.help) {
4125
+ return printHelp();
4126
+ }
4127
+ return await executeExtract({
4128
+ siteDir: rawOptions.positionals[0] || void 0,
4129
+ json: Boolean(rawOptions.values.json),
4130
+ noSync: Boolean(rawOptions.values["no-sync"])
4131
+ });
4132
+ }
4133
+ if (command === "check") {
4134
+ const rawOptions = parseArgs({
4135
+ args: args.slice(1),
4136
+ options: {
4137
+ "no-sync": { type: "boolean" },
4138
+ help: { type: "boolean", short: "h" }
4139
+ },
4140
+ allowPositionals: true,
4141
+ strict: false
4142
+ });
4143
+ if (rawOptions.values.help) {
4144
+ return printHelp();
4145
+ }
4146
+ const checkRes = await executeCheck({
4147
+ siteDir: rawOptions.positionals[0] || void 0,
4148
+ noSync: Boolean(rawOptions.values["no-sync"])
4149
+ });
4150
+ if (!checkRes.success) {
4151
+ throw new Error(checkRes.report);
4152
+ }
4153
+ return checkRes.report;
4154
+ }
4155
+ if (command === "push") {
4156
+ const rawOptions = parseArgs({
4157
+ args: args.slice(1),
4158
+ options: {
4159
+ site: { type: "string", short: "s" },
4160
+ "site-id": { type: "string" },
4161
+ config: { type: "string", short: "c" },
4162
+ check: { type: "boolean" },
4163
+ yes: { type: "boolean", short: "y" },
4164
+ "no-sync": { type: "boolean" },
4165
+ force: { type: "boolean" },
4166
+ "dry-run": { type: "boolean" },
4167
+ help: { type: "boolean", short: "h" }
4168
+ },
4169
+ allowPositionals: true,
4170
+ strict: false
4171
+ });
4172
+ if (rawOptions.values.help) {
4173
+ return printHelp();
4174
+ }
4175
+ const siteId = rawOptions.values.site || rawOptions.values["site-id"] || process.env.CMS_SITE_ID;
4176
+ if (!siteId) {
4177
+ throw new Error("site ID is required. Pass --site <id> or set CMS_SITE_ID.");
4178
+ }
4179
+ const result = await executePush({
4180
+ siteId,
4181
+ siteDir: rawOptions.positionals[0] || void 0,
4182
+ configPath: rawOptions.values.config,
4183
+ check: Boolean(rawOptions.values.check),
4184
+ yes: Boolean(rawOptions.values.yes),
4185
+ noSync: Boolean(rawOptions.values["no-sync"]),
4186
+ force: Boolean(rawOptions.values.force),
4187
+ dryRun: Boolean(rawOptions.values["dry-run"])
4188
+ });
4189
+ const report = formatPushReport(result);
4190
+ if (!result.success) {
4191
+ throw new Error(report);
4192
+ }
4193
+ return report;
4194
+ }
4195
+ if (command === "skills") {
4196
+ const rawOptions = parseArgs({
4197
+ args: args.slice(1),
4198
+ options: {
4199
+ force: { type: "boolean" },
4200
+ help: { type: "boolean", short: "h" }
4201
+ },
4202
+ allowPositionals: true,
4203
+ strict: false
4204
+ });
4205
+ if (rawOptions.values.help) {
4206
+ return printHelp();
4207
+ }
4208
+ const [subcommand, targetDir2] = rawOptions.positionals;
4209
+ if (subcommand === "install") {
4210
+ return executeSkillsInstall({ targetDir: targetDir2, force: Boolean(rawOptions.values.force) });
4211
+ }
4212
+ if (subcommand === "list") {
4213
+ return executeSkillsList();
4214
+ }
4215
+ throw new Error(`Unknown skills subcommand: "${subcommand ?? ""}". Use "install" or "list".`);
4216
+ }
4217
+ throw new Error(`Unknown command: "${command}". Run "cms --help" for available commands.`);
4218
+ }
4219
+ function isDirectExecution() {
4220
+ if (!process.argv[1]) return false;
4221
+ try {
4222
+ const scriptPath = fileURLToPath2(import.meta.url);
4223
+ const invokedPath = fs10.realpathSync(process.argv[1]);
4224
+ return scriptPath === invokedPath;
4225
+ } catch {
4226
+ return false;
4227
+ }
4228
+ }
4229
+ if (isDirectExecution()) {
4230
+ runCli(process.argv.slice(2)).then((output) => {
4231
+ console.log(output);
4232
+ }).catch((err) => {
4233
+ console.error(`
4234
+ \u274C Error: ${err.message}
4235
+ `);
4236
+ process.exit(1);
4237
+ });
4238
+ }
4239
+ export {
4240
+ StateStore,
4241
+ VERSION,
4242
+ buildSiteManifest,
4243
+ collectFromFile,
4244
+ deriveRouteFromComponent,
4245
+ executeCheck,
4246
+ executeExtract,
4247
+ executeLink,
4248
+ executeLogin,
4249
+ executeLogout,
4250
+ executePush,
4251
+ executeSkillsInstall,
4252
+ executeSkillsList,
4253
+ executeWhoami,
4254
+ extractGroupFromCall,
4255
+ extractProject,
4256
+ findPackageRoot,
4257
+ formatDiagnostics,
4258
+ formatPushReport,
4259
+ getDefaultCredentialsPath,
4260
+ getStoredCredentials,
4261
+ getStringLiteralValue,
4262
+ globSourceFiles,
4263
+ installSkills,
4264
+ listBundledSkills,
4265
+ loadAllCredentials,
4266
+ loadDerivedRoutes,
4267
+ loadManifest,
4268
+ mergeEnvContent,
4269
+ mergeEnvFile,
4270
+ mergeExtractionResults,
4271
+ normalizeApiUrl,
4272
+ parseBareSpecifier,
4273
+ parseFile,
4274
+ printHelp,
4275
+ refreshCredentialsIfNeeded,
4276
+ removeStoredCredentials,
4277
+ resolveAuthToken,
4278
+ resolveBundledSkillsDir,
4279
+ resolveFileContext,
4280
+ resolveModulePath,
4281
+ resolvePackageExport,
4282
+ runCli,
4283
+ runEntryMigrations,
4284
+ saveStoredCredentials,
4285
+ scanPagesDirectory,
4286
+ shouldSyncRoutes,
4287
+ syncAstroRoutes
4288
+ };
4289
+ //# sourceMappingURL=index.js.map