clefbase 1.4.1 → 1.5.1

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/cli.js CHANGED
@@ -959,8 +959,8 @@ var require_command = __commonJS({
959
959
  "node_modules/commander/lib/command.js"(exports2) {
960
960
  var EventEmitter = require("node:events").EventEmitter;
961
961
  var childProcess = require("node:child_process");
962
- var path5 = require("node:path");
963
- var fs4 = require("node:fs");
962
+ var path6 = require("node:path");
963
+ var fs5 = require("node:fs");
964
964
  var process11 = require("node:process");
965
965
  var { Argument: Argument2, humanReadableArgName } = require_argument();
966
966
  var { CommanderError: CommanderError2 } = require_error();
@@ -1893,11 +1893,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1893
1893
  let launchWithNode = false;
1894
1894
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1895
1895
  function findFile(baseDir, baseName) {
1896
- const localBin = path5.resolve(baseDir, baseName);
1897
- if (fs4.existsSync(localBin)) return localBin;
1898
- if (sourceExt.includes(path5.extname(baseName))) return void 0;
1896
+ const localBin = path6.resolve(baseDir, baseName);
1897
+ if (fs5.existsSync(localBin)) return localBin;
1898
+ if (sourceExt.includes(path6.extname(baseName))) return void 0;
1899
1899
  const foundExt = sourceExt.find(
1900
- (ext) => fs4.existsSync(`${localBin}${ext}`)
1900
+ (ext) => fs5.existsSync(`${localBin}${ext}`)
1901
1901
  );
1902
1902
  if (foundExt) return `${localBin}${foundExt}`;
1903
1903
  return void 0;
@@ -1909,21 +1909,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1909
1909
  if (this._scriptPath) {
1910
1910
  let resolvedScriptPath;
1911
1911
  try {
1912
- resolvedScriptPath = fs4.realpathSync(this._scriptPath);
1912
+ resolvedScriptPath = fs5.realpathSync(this._scriptPath);
1913
1913
  } catch (err) {
1914
1914
  resolvedScriptPath = this._scriptPath;
1915
1915
  }
1916
- executableDir = path5.resolve(
1917
- path5.dirname(resolvedScriptPath),
1916
+ executableDir = path6.resolve(
1917
+ path6.dirname(resolvedScriptPath),
1918
1918
  executableDir
1919
1919
  );
1920
1920
  }
1921
1921
  if (executableDir) {
1922
1922
  let localFile = findFile(executableDir, executableFile);
1923
1923
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1924
- const legacyName = path5.basename(
1924
+ const legacyName = path6.basename(
1925
1925
  this._scriptPath,
1926
- path5.extname(this._scriptPath)
1926
+ path6.extname(this._scriptPath)
1927
1927
  );
1928
1928
  if (legacyName !== this._name) {
1929
1929
  localFile = findFile(
@@ -1934,7 +1934,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1934
1934
  }
1935
1935
  executableFile = localFile || executableFile;
1936
1936
  }
1937
- launchWithNode = sourceExt.includes(path5.extname(executableFile));
1937
+ launchWithNode = sourceExt.includes(path6.extname(executableFile));
1938
1938
  let proc;
1939
1939
  if (process11.platform !== "win32") {
1940
1940
  if (launchWithNode) {
@@ -2776,7 +2776,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2776
2776
  * @return {Command}
2777
2777
  */
2778
2778
  nameFromFilename(filename) {
2779
- this._name = path5.basename(filename, path5.extname(filename));
2779
+ this._name = path6.basename(filename, path6.extname(filename));
2780
2780
  return this;
2781
2781
  }
2782
2782
  /**
@@ -2790,9 +2790,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2790
2790
  * @param {string} [path]
2791
2791
  * @return {(string|null|Command)}
2792
2792
  */
2793
- executableDir(path6) {
2794
- if (path6 === void 0) return this._executableDir;
2795
- this._executableDir = path6;
2793
+ executableDir(path7) {
2794
+ if (path7 === void 0) return this._executableDir;
2795
+ this._executableDir = path7;
2796
2796
  return this;
2797
2797
  }
2798
2798
  /**
@@ -13742,15 +13742,15 @@ var require_route = __commonJS({
13742
13742
  };
13743
13743
  }
13744
13744
  function wrapConversion(toModel, graph) {
13745
- const path5 = [graph[toModel].parent, toModel];
13745
+ const path6 = [graph[toModel].parent, toModel];
13746
13746
  let fn = conversions[graph[toModel].parent][toModel];
13747
13747
  let cur = graph[toModel].parent;
13748
13748
  while (graph[cur].parent) {
13749
- path5.unshift(graph[cur].parent);
13749
+ path6.unshift(graph[cur].parent);
13750
13750
  fn = link(conversions[graph[cur].parent][cur], fn);
13751
13751
  cur = graph[cur].parent;
13752
13752
  }
13753
- fn.conversion = path5;
13753
+ fn.conversion = path6;
13754
13754
  return fn;
13755
13755
  }
13756
13756
  module2.exports = function(fromModel) {
@@ -25890,10 +25890,10 @@ var require_lib = __commonJS({
25890
25890
  exports2.analyse = analyse;
25891
25891
  var detectFile = (filepath, opts = {}) => new Promise((resolve, reject) => {
25892
25892
  let fd;
25893
- const fs4 = (0, node_1.default)();
25893
+ const fs5 = (0, node_1.default)();
25894
25894
  const handler = (err, buffer) => {
25895
25895
  if (fd) {
25896
- fs4.closeSync(fd);
25896
+ fs5.closeSync(fd);
25897
25897
  }
25898
25898
  if (err) {
25899
25899
  reject(err);
@@ -25905,9 +25905,9 @@ var require_lib = __commonJS({
25905
25905
  };
25906
25906
  const sampleSize = (opts === null || opts === void 0 ? void 0 : opts.sampleSize) || 0;
25907
25907
  if (sampleSize > 0) {
25908
- fd = fs4.openSync(filepath, "r");
25908
+ fd = fs5.openSync(filepath, "r");
25909
25909
  let sample = Buffer.allocUnsafe(sampleSize);
25910
- fs4.read(fd, sample, 0, sampleSize, opts.offset, (err, bytesRead) => {
25910
+ fs5.read(fd, sample, 0, sampleSize, opts.offset, (err, bytesRead) => {
25911
25911
  if (err) {
25912
25912
  handler(err, null);
25913
25913
  } else {
@@ -25919,22 +25919,22 @@ var require_lib = __commonJS({
25919
25919
  });
25920
25920
  return;
25921
25921
  }
25922
- fs4.readFile(filepath, handler);
25922
+ fs5.readFile(filepath, handler);
25923
25923
  });
25924
25924
  exports2.detectFile = detectFile;
25925
25925
  var detectFileSync = (filepath, opts = {}) => {
25926
- const fs4 = (0, node_1.default)();
25926
+ const fs5 = (0, node_1.default)();
25927
25927
  if (opts && opts.sampleSize) {
25928
- const fd = fs4.openSync(filepath, "r");
25928
+ const fd = fs5.openSync(filepath, "r");
25929
25929
  let sample = Buffer.allocUnsafe(opts.sampleSize);
25930
- const bytesRead = fs4.readSync(fd, sample, 0, opts.sampleSize, opts.offset);
25930
+ const bytesRead = fs5.readSync(fd, sample, 0, opts.sampleSize, opts.offset);
25931
25931
  if (bytesRead < opts.sampleSize) {
25932
25932
  sample = sample.subarray(0, bytesRead);
25933
25933
  }
25934
- fs4.closeSync(fd);
25934
+ fs5.closeSync(fd);
25935
25935
  return (0, exports2.detect)(sample);
25936
25936
  }
25937
- return (0, exports2.detect)(fs4.readFileSync(filepath));
25937
+ return (0, exports2.detect)(fs5.readFileSync(filepath));
25938
25938
  };
25939
25939
  exports2.detectFileSync = detectFileSync;
25940
25940
  exports2.default = {
@@ -32520,9 +32520,9 @@ var fetchAsyncQuestionProperty = function(question, prop, answers) {
32520
32520
 
32521
32521
  // node_modules/inquirer/lib/ui/prompt.js
32522
32522
  var _ = {
32523
- set: (obj, path5 = "", value) => {
32523
+ set: (obj, path6 = "", value) => {
32524
32524
  let pointer = obj;
32525
- path5.split(".").forEach((key, index, arr) => {
32525
+ path6.split(".").forEach((key, index, arr) => {
32526
32526
  if (key === "__proto__" || key === "constructor") return;
32527
32527
  if (index === arr.length - 1) {
32528
32528
  pointer[key] = value;
@@ -32532,8 +32532,8 @@ var _ = {
32532
32532
  pointer = pointer[key];
32533
32533
  });
32534
32534
  },
32535
- get: (obj, path5 = "", defaultValue) => {
32536
- const travel = (regexp) => String.prototype.split.call(path5, regexp).filter(Boolean).reduce(
32535
+ get: (obj, path6 = "", defaultValue) => {
32536
+ const travel = (regexp) => String.prototype.split.call(path6, regexp).filter(Boolean).reduce(
32537
32537
  // @ts-expect-error implicit any on res[key]
32538
32538
  (res, key) => res !== null && res !== void 0 ? res[key] : res,
32539
32539
  obj
@@ -33637,7 +33637,11 @@ function loadConfig(cwd = process.cwd()) {
33637
33637
  const p = findConfigPath(cwd);
33638
33638
  if (!p) return null;
33639
33639
  try {
33640
- return JSON.parse(import_fs2.default.readFileSync(p, "utf-8"));
33640
+ const raw = JSON.parse(import_fs2.default.readFileSync(p, "utf-8"));
33641
+ if (raw.services && raw.services.functions === void 0) {
33642
+ raw.services.functions = false;
33643
+ }
33644
+ return raw;
33641
33645
  } catch {
33642
33646
  return null;
33643
33647
  }
@@ -33720,6 +33724,9 @@ function base(cfg) {
33720
33724
  function adminHeaders(cfg) {
33721
33725
  return { "Content-Type": "application/json", "x-admin-secret": cfg.adminSecret };
33722
33726
  }
33727
+ function cfxHeaders(cfg) {
33728
+ return { "Content-Type": "application/json", "x-cfx-key": cfg.apiKey };
33729
+ }
33723
33730
  async function listSites(cfg) {
33724
33731
  return apiFetch(
33725
33732
  `${base(cfg)}/api/hosting/databases/${cfg.projectId}/sites`,
@@ -33729,11 +33736,7 @@ async function listSites(cfg) {
33729
33736
  async function createSite(cfg, name, description) {
33730
33737
  return apiFetch(
33731
33738
  `${base(cfg)}/api/hosting/databases/${cfg.projectId}/sites`,
33732
- {
33733
- method: "POST",
33734
- headers: adminHeaders(cfg),
33735
- body: JSON.stringify({ name, description })
33736
- }
33739
+ { method: "POST", headers: adminHeaders(cfg), body: JSON.stringify({ name, description }) }
33737
33740
  );
33738
33741
  }
33739
33742
  async function getDnsStatus(cfg, siteId) {
@@ -33825,6 +33828,36 @@ async function getActiveDeploy(cfg, siteId) {
33825
33828
  throw err;
33826
33829
  }
33827
33830
  }
33831
+ async function apiFnList(cfg) {
33832
+ return apiFetch(
33833
+ `${base(cfg)}/functions/`,
33834
+ { headers: cfxHeaders(cfg) }
33835
+ );
33836
+ }
33837
+ async function apiFnDeploy(cfg, options) {
33838
+ return apiFetch(
33839
+ `${base(cfg)}/functions/deploy`,
33840
+ { method: "POST", headers: cfxHeaders(cfg), body: JSON.stringify(options) }
33841
+ );
33842
+ }
33843
+ async function apiFnDelete(cfg, name) {
33844
+ return apiFetch(
33845
+ `${base(cfg)}/functions/${encodeURIComponent(name)}`,
33846
+ { method: "DELETE", headers: cfxHeaders(cfg) }
33847
+ );
33848
+ }
33849
+ async function apiFnCall(cfg, name, data) {
33850
+ return apiFetch(
33851
+ `${base(cfg)}/functions/call/${encodeURIComponent(name)}`,
33852
+ { method: "POST", headers: cfxHeaders(cfg), body: JSON.stringify({ data: data ?? null }) }
33853
+ );
33854
+ }
33855
+ async function apiFnExecutions(cfg, name, limit = 30) {
33856
+ return apiFetch(
33857
+ `${base(cfg)}/functions/${encodeURIComponent(name)}/executions?limit=${Math.min(limit, 100)}`,
33858
+ { headers: cfxHeaders(cfg) }
33859
+ );
33860
+ }
33828
33861
  async function testConnection(cfg) {
33829
33862
  try {
33830
33863
  const fetchFn = await getFetch();
@@ -33882,7 +33915,7 @@ async function runInit(cwd = process.cwd()) {
33882
33915
  projectId: projectId.trim(),
33883
33916
  apiKey: apiKey.trim(),
33884
33917
  adminSecret: adminSecret.trim(),
33885
- services: { database: false, auth: false, storage: false, hosting: false }
33918
+ services: { database: false, auth: false, storage: false, hosting: false, functions: false }
33886
33919
  };
33887
33920
  const connSpinner = ora2("Testing connection\u2026").start();
33888
33921
  const ok = await testConnection(cfg);
@@ -33897,17 +33930,19 @@ async function runInit(cwd = process.cwd()) {
33897
33930
  name: "services",
33898
33931
  message: "Which services will you use?",
33899
33932
  choices: [
33900
- { name: "Database \u2014 store and query documents", value: "database", checked: true },
33901
- { name: "Auth \u2014 user sign-up / sign-in", value: "auth", checked: true },
33902
- { name: "Storage \u2014 file uploads", value: "storage", checked: false },
33903
- { name: "Hosting \u2014 deploy static sites", value: "hosting", checked: false }
33933
+ { name: "Database \u2014 store and query documents", value: "database", checked: true },
33934
+ { name: "Auth \u2014 user sign-up / sign-in", value: "auth", checked: true },
33935
+ { name: "Storage \u2014 file uploads", value: "storage", checked: false },
33936
+ { name: "Hosting \u2014 deploy static sites", value: "hosting", checked: false },
33937
+ { name: "Functions \u2014 serverless JS/TS & Python functions", value: "functions", checked: false }
33904
33938
  ]
33905
33939
  }]);
33906
33940
  cfg.services = {
33907
33941
  database: services.includes("database"),
33908
33942
  auth: services.includes("auth"),
33909
33943
  storage: services.includes("storage"),
33910
- hosting: services.includes("hosting")
33944
+ hosting: services.includes("hosting"),
33945
+ functions: services.includes("functions")
33911
33946
  };
33912
33947
  if (cfg.services.hosting) {
33913
33948
  await setupHosting(cfg, cwd);
@@ -33936,7 +33971,6 @@ function scaffoldLib(cfg, cwd = process.cwd()) {
33936
33971
  serverUrl: cfg.serverUrl,
33937
33972
  projectId: cfg.projectId,
33938
33973
  apiKey: cfg.apiKey,
33939
- // adminSecret intentionally omitted — not needed in client code
33940
33974
  services: cfg.services,
33941
33975
  ...cfg.hosting ? { hosting: cfg.hosting } : {}
33942
33976
  };
@@ -33950,22 +33984,31 @@ function scaffoldLib(cfg, cwd = process.cwd()) {
33950
33984
  };
33951
33985
  }
33952
33986
  function buildLibTs(cfg) {
33953
- const { database, auth, storage } = cfg.services;
33987
+ const { database, auth, storage, functions: fns } = cfg.services;
33954
33988
  const sdkImports = ["initClefbase"];
33955
33989
  if (database) sdkImports.push("getDatabase");
33956
33990
  if (auth) sdkImports.push("getAuth");
33957
33991
  if (storage) sdkImports.push("getStorage");
33992
+ if (fns) sdkImports.push("getFunctions", "httpsCallable");
33958
33993
  const typeImports = [];
33959
33994
  if (database) typeImports.push("Database");
33960
33995
  if (auth) typeImports.push("Auth");
33961
33996
  if (storage) typeImports.push("ClefbaseStorage");
33997
+ if (fns) typeImports.push("ClefbaseFunctions");
33962
33998
  const lines = [
33963
33999
  `/**`,
33964
34000
  ` * Clefbase \u2014 pre-initialised service exports`,
33965
34001
  ` *`,
33966
34002
  ` * Usage:`,
33967
- ` * import { db, auth, storage } from "@lib/clefBase";`,
33968
- ` * import db from "@lib/clefBase";`,
34003
+ ...database ? [` * import { db } from "@lib/clefBase";`] : [],
34004
+ ...auth ? [` * import { auth } from "@lib/clefBase";`] : [],
34005
+ ...storage ? [` * import { storage } from "@lib/clefBase";`] : [],
34006
+ ...fns ? [
34007
+ ` * import { fns, httpsCallable } from "@lib/clefBase";`,
34008
+ ` *`,
34009
+ ` * const greet = httpsCallable<{ name: string }, { message: string }>(fns, "greetUser");`,
34010
+ ` * const { data } = await greet({ name: "Alice" });`
34011
+ ] : [],
33969
34012
  ` */`,
33970
34013
  ``,
33971
34014
  `import { ${sdkImports.join(", ")} } from "clefbase";`,
@@ -33982,7 +34025,8 @@ function buildLibTs(cfg) {
33982
34025
  `});`,
33983
34026
  ``
33984
34027
  ];
33985
- if (database || auth || storage) {
34028
+ const hasServices = database || auth || storage || fns;
34029
+ if (hasServices) {
33986
34030
  lines.push("// \u2500\u2500\u2500 Services \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
33987
34031
  lines.push("");
33988
34032
  }
@@ -34001,6 +34045,20 @@ function buildLibTs(cfg) {
34001
34045
  lines.push(`export const storage: ClefbaseStorage = getStorage(app);`);
34002
34046
  lines.push("");
34003
34047
  }
34048
+ if (fns) {
34049
+ lines.push(`/** Clefbase Functions \u2014 deploy and call serverless functions. */`);
34050
+ lines.push(`export const fns: ClefbaseFunctions = getFunctions(app);`);
34051
+ lines.push("");
34052
+ lines.push(`/**`);
34053
+ lines.push(` * Create a typed callable reference to an HTTP-triggered function.`);
34054
+ lines.push(` *`);
34055
+ lines.push(` * @example`);
34056
+ lines.push(` * const greet = httpsCallable<{ name: string }, { message: string }>(fns, "greetUser");`);
34057
+ lines.push(` * const { data } = await greet({ name: "Alice" });`);
34058
+ lines.push(` */`);
34059
+ lines.push(`export { httpsCallable };`);
34060
+ lines.push("");
34061
+ }
34004
34062
  lines.push(
34005
34063
  `// \u2500\u2500\u2500 Advanced \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
34006
34064
  ``,
@@ -34008,7 +34066,6 @@ function buildLibTs(cfg) {
34008
34066
  `export { app };`,
34009
34067
  ``
34010
34068
  );
34011
- lines.push("");
34012
34069
  return lines.join("\n");
34013
34070
  }
34014
34071
  async function setupHosting(cfg, cwd) {
@@ -34095,12 +34152,11 @@ function printUsageHint(cfg) {
34095
34152
  if (cfg.services.database) namedImports.push("db");
34096
34153
  if (cfg.services.auth) namedImports.push("auth");
34097
34154
  if (cfg.services.storage) namedImports.push("storage");
34155
+ if (cfg.services.functions) namedImports.push("fns", "httpsCallable");
34098
34156
  console.log(source_default.bold(" Quick start:"));
34099
34157
  console.log();
34100
34158
  if (namedImports.length > 0) {
34101
- console.log(
34102
- source_default.cyan(` import { ${namedImports.join(", ")} } from "@lib/clefBase";`)
34103
- );
34159
+ console.log(source_default.cyan(` import { ${namedImports.join(", ")} } from "@lib/clefBase";`));
34104
34160
  } else {
34105
34161
  console.log(source_default.cyan(` import app from "@lib/clefBase";`));
34106
34162
  }
@@ -34116,6 +34172,16 @@ function printUsageHint(cfg) {
34116
34172
  console.log();
34117
34173
  console.log(source_default.cyan(` await storage.ref("uploads/photo.jpg").upload(file);`));
34118
34174
  }
34175
+ if (cfg.services.functions) {
34176
+ console.log();
34177
+ console.log(source_default.bold(" Functions:"));
34178
+ console.log(source_default.cyan(` // Deploy from a file`));
34179
+ console.log(source_default.cyan(` $ clefbase functions:deploy -f ./src/functions/hello.ts`));
34180
+ console.log();
34181
+ console.log(source_default.cyan(` // Call from your app`));
34182
+ console.log(source_default.cyan(` const greet = httpsCallable(fns, "greetUser");`));
34183
+ console.log(source_default.cyan(` const { data } = await greet({ name: "Alice" });`));
34184
+ }
34119
34185
  if (cfg.services.hosting && cfg.hosting) {
34120
34186
  console.log();
34121
34187
  console.log(source_default.bold(" Deploy:"));
@@ -34522,12 +34588,301 @@ async function runSitesList(cwd = process.cwd()) {
34522
34588
  }
34523
34589
  }
34524
34590
 
34591
+ // src/cli/commands/functions.ts
34592
+ var import_fs5 = __toESM(require("fs"));
34593
+ var import_path4 = __toESM(require("path"));
34594
+ function fmtDuration(ms) {
34595
+ return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(2)}s`;
34596
+ }
34597
+ function fmtDate(iso) {
34598
+ return new Date(iso).toLocaleString(void 0, {
34599
+ month: "short",
34600
+ day: "numeric",
34601
+ year: "numeric",
34602
+ hour: "2-digit",
34603
+ minute: "2-digit"
34604
+ });
34605
+ }
34606
+ function triggerLabel(fn) {
34607
+ const t = fn.trigger;
34608
+ if (t.type === "schedule") return `schedule ${source_default.dim(t.cron ?? "")}`;
34609
+ if (t.collection) return `${t.type} ${source_default.dim(t.collection)}`;
34610
+ if (t.bucket) return `${t.type} ${source_default.dim(t.bucket)}`;
34611
+ return t.type;
34612
+ }
34613
+ function statusColor(status) {
34614
+ if (status === "active" || status === "success") return source_default.green(status);
34615
+ if (status === "error" || status === "timeout") return source_default.red(status);
34616
+ if (status === "disabled") return source_default.dim(status);
34617
+ return status;
34618
+ }
34619
+ async function runFunctionsList(cwd = process.cwd()) {
34620
+ const cfg = requireConfig(cwd);
34621
+ const sp = ora2("Fetching functions\u2026").start();
34622
+ let fns;
34623
+ try {
34624
+ fns = await apiFnList(cfg);
34625
+ sp.succeed(`${fns.length} function${fns.length !== 1 ? "s" : ""}`);
34626
+ } catch (err) {
34627
+ sp.fail(err.message);
34628
+ return;
34629
+ }
34630
+ if (fns.length === 0) {
34631
+ console.log(source_default.dim("\n No functions deployed yet. Run `clefbase functions:deploy` to get started.\n"));
34632
+ return;
34633
+ }
34634
+ console.log();
34635
+ for (const fn of fns) {
34636
+ const err = fn.errorCount > 0 ? source_default.red(` ${fn.errorCount} err`) : "";
34637
+ console.log(
34638
+ ` ${source_default.bold(fn.name.padEnd(28))}${RUNTIME_BADGE[fn.runtime] ?? fn.runtime} ${statusColor(fn.status).padEnd(14)} ${triggerLabel(fn)}`
34639
+ );
34640
+ console.log(
34641
+ ` ${source_default.dim("".padEnd(28))}invocations: ${fn.invocationCount}${err} last: ${fn.lastInvokedAt ? fmtDate(fn.lastInvokedAt) : "\u2014"}`
34642
+ );
34643
+ console.log();
34644
+ }
34645
+ }
34646
+ var RUNTIME_BADGE = {
34647
+ node: source_default.green("[JS/TS]"),
34648
+ python: source_default.blue("[PY] ")
34649
+ };
34650
+ async function runFunctionsDeploy(opts) {
34651
+ const cwd = opts.cwd ?? process.cwd();
34652
+ const cfg = requireConfig(cwd);
34653
+ console.log();
34654
+ console.log(source_default.bold.cyan(" Deploy Function"));
34655
+ console.log();
34656
+ const name = opts.name ?? await promptRequired("Function name");
34657
+ let source;
34658
+ if (opts.file) {
34659
+ const absFile = import_path4.default.isAbsolute(opts.file) ? opts.file : import_path4.default.join(cwd, opts.file);
34660
+ if (!import_fs5.default.existsSync(absFile)) {
34661
+ console.error(source_default.red(`
34662
+ File not found: ${absFile}
34663
+ `));
34664
+ process.exit(1);
34665
+ }
34666
+ source = import_fs5.default.readFileSync(absFile, "utf-8");
34667
+ } else {
34668
+ const { inputMode } = await lib_default.prompt([{
34669
+ type: "list",
34670
+ name: "inputMode",
34671
+ message: "Source code",
34672
+ choices: [
34673
+ { name: "From a file (recommended)", value: "file" },
34674
+ { name: "Inline snippet (paste in terminal)", value: "inline" }
34675
+ ]
34676
+ }]);
34677
+ if (inputMode === "file") {
34678
+ const { filePath } = await lib_default.prompt([{
34679
+ type: "input",
34680
+ name: "filePath",
34681
+ message: "Path to source file (.js, .ts, .py)",
34682
+ validate: (v) => {
34683
+ if (!v.trim()) return "Required";
34684
+ const abs2 = import_path4.default.isAbsolute(v) ? v : import_path4.default.join(cwd, v);
34685
+ return import_fs5.default.existsSync(abs2) || `File not found: ${abs2}`;
34686
+ }
34687
+ }]);
34688
+ const abs = import_path4.default.isAbsolute(filePath) ? filePath : import_path4.default.join(cwd, filePath.trim());
34689
+ source = import_fs5.default.readFileSync(abs, "utf-8");
34690
+ } else {
34691
+ const { snippet } = await lib_default.prompt([{
34692
+ type: "editor",
34693
+ name: "snippet",
34694
+ message: "Paste your function source (opens $EDITOR)"
34695
+ }]);
34696
+ source = snippet;
34697
+ }
34698
+ }
34699
+ let runtime = opts.runtime;
34700
+ if (!runtime) {
34701
+ if (opts.file) {
34702
+ const ext = import_path4.default.extname(opts.file).toLowerCase();
34703
+ if (ext === ".py") runtime = "python";
34704
+ else runtime = "node";
34705
+ } else {
34706
+ const { rt } = await lib_default.prompt([{
34707
+ type: "list",
34708
+ name: "rt",
34709
+ message: "Runtime",
34710
+ choices: [
34711
+ { name: "Node.js / TypeScript (JS/TS)", value: "node" },
34712
+ { name: "Python 3 (PY)", value: "python" }
34713
+ ]
34714
+ }]);
34715
+ runtime = rt;
34716
+ }
34717
+ }
34718
+ let triggerType = opts.trigger;
34719
+ if (!triggerType) {
34720
+ const { tt } = await lib_default.prompt([{
34721
+ type: "list",
34722
+ name: "tt",
34723
+ message: "Trigger type",
34724
+ choices: [
34725
+ { name: "HTTP (POST /functions/call/:name)", value: "http" },
34726
+ { name: "Schedule (cron timer)", value: "schedule" },
34727
+ { name: "onDocumentCreate", value: "onDocumentCreate" },
34728
+ { name: "onDocumentUpdate", value: "onDocumentUpdate" },
34729
+ { name: "onDocumentDelete", value: "onDocumentDelete" },
34730
+ { name: "onDocumentWrite (create + update + delete)", value: "onDocumentWrite" },
34731
+ { name: "onUserCreate", value: "onUserCreate" },
34732
+ { name: "onUserDelete", value: "onUserDelete" },
34733
+ { name: "onFileUpload", value: "onFileUpload" },
34734
+ { name: "onFileDelete", value: "onFileDelete" }
34735
+ ]
34736
+ }]);
34737
+ triggerType = tt;
34738
+ }
34739
+ const trigger = { type: triggerType };
34740
+ if (triggerType === "schedule") {
34741
+ trigger.cron = opts.cron ?? await promptRequired("Cron expression (e.g. 0 * * * *)");
34742
+ }
34743
+ if (["onDocumentWrite", "onDocumentCreate", "onDocumentUpdate", "onDocumentDelete"].includes(triggerType)) {
34744
+ trigger.collection = opts.collection ?? await promptRequired("Collection path (e.g. users)");
34745
+ }
34746
+ if (["onFileUpload", "onFileDelete"].includes(triggerType)) {
34747
+ const { bucket } = await lib_default.prompt([{
34748
+ type: "input",
34749
+ name: "bucket",
34750
+ message: "Bucket name filter (leave blank = all buckets)"
34751
+ }]);
34752
+ if (bucket.trim()) trigger.bucket = bucket.trim();
34753
+ }
34754
+ const entryPoint = opts.entry ?? "handler";
34755
+ const timeoutMs = opts.timeout ? parseInt(opts.timeout, 10) : 3e4;
34756
+ const env2 = {};
34757
+ for (const pair of opts.env ?? []) {
34758
+ const idx = pair.indexOf("=");
34759
+ if (idx > 0) env2[pair.slice(0, idx)] = pair.slice(idx + 1);
34760
+ }
34761
+ const payload = { name, runtime, trigger, source, entryPoint, timeoutMs, env: env2 };
34762
+ const sp = ora2(`Deploying "${name}"\u2026`).start();
34763
+ try {
34764
+ const result = await apiFnDeploy(cfg, payload);
34765
+ sp.succeed(source_default.green(`Deployed "${result.name}" \u2192 ${statusColor(result.status)}`));
34766
+ console.log();
34767
+ console.log(` ${source_default.bold("Name:")} ${result.name}`);
34768
+ console.log(` ${source_default.bold("Runtime:")} ${runtime}`);
34769
+ console.log(` ${source_default.bold("Trigger:")} ${triggerLabel({ trigger })}`);
34770
+ if (trigger.type === "http") {
34771
+ console.log(` ${source_default.bold("Call:")} POST ${cfg.serverUrl.replace(/\/+$/, "")}/functions/call/${name}`);
34772
+ }
34773
+ console.log();
34774
+ } catch (err) {
34775
+ sp.fail(err.message);
34776
+ process.exit(1);
34777
+ }
34778
+ }
34779
+ async function runFunctionsCall(name, opts) {
34780
+ const cwd = opts.cwd ?? process.cwd();
34781
+ const cfg = requireConfig(cwd);
34782
+ let data = null;
34783
+ if (opts.data) {
34784
+ try {
34785
+ data = JSON.parse(opts.data);
34786
+ } catch {
34787
+ console.error(source_default.red(`
34788
+ --data must be valid JSON. Got: ${opts.data}
34789
+ `));
34790
+ process.exit(1);
34791
+ }
34792
+ }
34793
+ const sp = ora2(`Calling "${name}"\u2026`).start();
34794
+ try {
34795
+ const result = await apiFnCall(cfg, name, data);
34796
+ sp.succeed(`${source_default.green("\u2713")} ${name} ${source_default.dim(fmtDuration(result.durationMs))}`);
34797
+ console.log();
34798
+ console.log(JSON.stringify(result.data, null, 2));
34799
+ console.log();
34800
+ } catch (err) {
34801
+ sp.fail(err.message);
34802
+ process.exit(1);
34803
+ }
34804
+ }
34805
+ async function runFunctionsDelete(name, opts) {
34806
+ const cwd = opts.cwd ?? process.cwd();
34807
+ const cfg = requireConfig(cwd);
34808
+ if (!opts.force) {
34809
+ const { confirmed } = await lib_default.prompt([{
34810
+ type: "confirm",
34811
+ name: "confirmed",
34812
+ message: `Delete function "${name}"? This cannot be undone.`,
34813
+ default: false
34814
+ }]);
34815
+ if (!confirmed) {
34816
+ console.log(source_default.dim("\n Cancelled.\n"));
34817
+ return;
34818
+ }
34819
+ }
34820
+ const sp = ora2(`Deleting "${name}"\u2026`).start();
34821
+ try {
34822
+ await apiFnDelete(cfg, name);
34823
+ sp.succeed(source_default.green(`"${name}" deleted`));
34824
+ console.log();
34825
+ } catch (err) {
34826
+ sp.fail(err.message);
34827
+ process.exit(1);
34828
+ }
34829
+ }
34830
+ async function runFunctionsLogs(name, opts) {
34831
+ const cwd = opts.cwd ?? process.cwd();
34832
+ const cfg = requireConfig(cwd);
34833
+ const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
34834
+ const sp = ora2(`Fetching logs for "${name}"\u2026`).start();
34835
+ let execs;
34836
+ try {
34837
+ execs = await apiFnExecutions(cfg, name, limit);
34838
+ sp.succeed(`${execs.length} execution${execs.length !== 1 ? "s" : ""}`);
34839
+ } catch (err) {
34840
+ sp.fail(err.message);
34841
+ return;
34842
+ }
34843
+ if (execs.length === 0) {
34844
+ console.log(source_default.dim(`
34845
+ No executions recorded yet for "${name}".
34846
+ `));
34847
+ return;
34848
+ }
34849
+ console.log();
34850
+ for (const ex of execs) {
34851
+ const statusStr = statusColor(ex.status);
34852
+ console.log(
34853
+ ` ${statusStr.padEnd(16)}${source_default.dim(fmtDate(ex.startedAt))} ${source_default.dim(fmtDuration(ex.durationMs))} ${source_default.dim("via")} ${ex.triggeredBy}`
34854
+ );
34855
+ if (ex.error) {
34856
+ console.log(source_default.red(` \u2717 ${ex.error.split("\n")[0]}`));
34857
+ }
34858
+ if (ex.logs.length > 0) {
34859
+ for (const line of ex.logs) {
34860
+ console.log(source_default.dim(` \u25B6 ${line}`));
34861
+ }
34862
+ }
34863
+ if (ex.result !== void 0 && ex.result !== null) {
34864
+ const preview = JSON.stringify(ex.result);
34865
+ console.log(source_default.dim(` \u2190 ${preview.length > 120 ? preview.slice(0, 120) + "\u2026" : preview}`));
34866
+ }
34867
+ console.log();
34868
+ }
34869
+ }
34870
+ async function promptRequired(message) {
34871
+ const { value } = await lib_default.prompt([{
34872
+ type: "input",
34873
+ name: "value",
34874
+ message,
34875
+ validate: (v) => v.trim().length > 0 || "Required"
34876
+ }]);
34877
+ return value.trim();
34878
+ }
34879
+
34525
34880
  // package.json
34526
- var version = "1.4.1";
34881
+ var version = "1.5.1";
34527
34882
 
34528
34883
  // src/cli/index.ts
34529
34884
  var program2 = new Command();
34530
- program2.name("clefbase").description("Clefbase CLI \u2014 initialise projects, deploy sites, manage hosting").version(version);
34885
+ program2.name("clefbase").description("Clefbase CLI \u2014 initialise projects, deploy sites, manage functions").version(version);
34531
34886
  program2.command("init").description("Initialise a Clefbase project in the current directory").action(async () => {
34532
34887
  try {
34533
34888
  await runInit();
@@ -34577,6 +34932,41 @@ program2.command("hosting:dns:reprovision").alias("dns:reprovision").description
34577
34932
  fatal(err);
34578
34933
  }
34579
34934
  });
34935
+ program2.command("functions:list").alias("fn:list").description("List all deployed functions for this project").action(async () => {
34936
+ try {
34937
+ await runFunctionsList();
34938
+ } catch (err) {
34939
+ fatal(err);
34940
+ }
34941
+ });
34942
+ program2.command("functions:deploy").alias("fn:deploy").description("Deploy (or redeploy) a function from a source file or interactively").option("-n, --name <name>", "Function name").option("-f, --file <path>", "Path to source file (.js, .ts, .py)").option("-r, --runtime <runtime>", "Runtime: node | python (auto-detected from file ext)").option("-t, --trigger <type>", "Trigger type (http, schedule, onDocumentCreate, \u2026)").option("-c, --cron <expr>", "Cron expression for schedule triggers").option("-C, --collection <path>", "Collection path for document triggers").option("-T, --timeout <ms>", "Execution timeout in milliseconds (default: 30000)").option("-e, --entry <name>", "Exported function name to call (default: handler)").option("--env <KEY=VALUE...>", "Environment variable(s) \u2014 repeatable").action(async (opts) => {
34943
+ try {
34944
+ await runFunctionsDeploy(opts);
34945
+ } catch (err) {
34946
+ fatal(err);
34947
+ }
34948
+ });
34949
+ program2.command("functions:call <name>").alias("fn:call").description("Call an HTTP-triggered function and print its return value").option("-d, --data <json>", "JSON payload to pass as ctx.data").action(async (name, opts) => {
34950
+ try {
34951
+ await runFunctionsCall(name, opts);
34952
+ } catch (err) {
34953
+ fatal(err);
34954
+ }
34955
+ });
34956
+ program2.command("functions:delete <name>").alias("fn:delete").description("Delete a deployed function").option("-y, --force", "Skip confirmation prompt").action(async (name, opts) => {
34957
+ try {
34958
+ await runFunctionsDelete(name, opts);
34959
+ } catch (err) {
34960
+ fatal(err);
34961
+ }
34962
+ });
34963
+ program2.command("functions:logs <name>").alias("fn:logs").description("Show recent execution history for a function").option("-l, --limit <n>", "Number of executions to show (default: 20, max: 100)").action(async (name, opts) => {
34964
+ try {
34965
+ await runFunctionsLogs(name, opts);
34966
+ } catch (err) {
34967
+ fatal(err);
34968
+ }
34969
+ });
34580
34970
  program2.command("info").description("Show project config and server connectivity").action(async () => {
34581
34971
  try {
34582
34972
  await runInfo();
@@ -34586,16 +34976,29 @@ program2.command("info").description("Show project config and server connectivit
34586
34976
  });
34587
34977
  program2.addHelpText("after", `
34588
34978
  ${source_default.bold("Examples:")}
34589
- ${source_default.cyan("clefbase init")} Set up a new project
34590
- ${source_default.cyan("clefbase deploy")} Deploy your built site
34591
- ${source_default.cyan("clefbase deploy -d ./dist")} Deploy from a specific directory
34592
- ${source_default.cyan('clefbase deploy -m "v2 release"')} Deploy with a release note
34593
- ${source_default.cyan("clefbase hosting:init")} Link or create a hosted site
34594
- ${source_default.cyan("clefbase hosting:status")} Show current live deploy
34595
- ${source_default.cyan("clefbase hosting:sites")} List all sites
34596
- ${source_default.cyan("clefbase hosting:dns")} Show DNS status
34597
- ${source_default.cyan("clefbase hosting:dns:reprovision")} Fix / create the preview CNAME
34598
- ${source_default.cyan("clefbase info")} Show config & connection status
34979
+ ${source_default.bold("Project:")}
34980
+ ${source_default.cyan("clefbase init")} Set up a new project
34981
+ ${source_default.cyan("clefbase info")} Show config & connection status
34982
+
34983
+ ${source_default.bold("Hosting:")}
34984
+ ${source_default.cyan("clefbase deploy")} Deploy your built site
34985
+ ${source_default.cyan('clefbase deploy -d ./dist -m "v2"')} Deploy from a dir with a note
34986
+ ${source_default.cyan("clefbase hosting:init")} Link or create a hosted site
34987
+ ${source_default.cyan("clefbase hosting:status")} Show current live deploy
34988
+ ${source_default.cyan("clefbase hosting:sites")} List all sites
34989
+ ${source_default.cyan("clefbase hosting:dns")} Show DNS status
34990
+ ${source_default.cyan("clefbase hosting:dns:reprovision")} Fix / create the preview CNAME
34991
+
34992
+ ${source_default.bold("Functions:")}
34993
+ ${source_default.cyan("clefbase functions:list")} List all deployed functions
34994
+ ${source_default.cyan("clefbase functions:deploy")} Interactive deploy wizard
34995
+ ${source_default.cyan("clefbase functions:deploy -f ./fn.ts")} Deploy from a file (auto-detects runtime)
34996
+ ${source_default.cyan("clefbase functions:deploy -n greet -f fn.ts --trigger http")}
34997
+ ${source_default.cyan("clefbase functions:call greetUser")} Call an HTTP function
34998
+ ${source_default.cyan(`clefbase functions:call greetUser -d '{"name":"Alice"}'}`)}
34999
+ ${source_default.cyan("clefbase functions:logs greetUser")} View execution history
35000
+ ${source_default.cyan("clefbase functions:logs greetUser -l 50")} View last 50 executions
35001
+ ${source_default.cyan("clefbase functions:delete oldFunction")} Delete a function
34599
35002
  `);
34600
35003
  program2.parse(process.argv);
34601
35004
  function fatal(err) {