@overaihq/cli 0.5.0 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +232 -4
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -24,7 +24,7 @@ overai clone <workspaceId> # materialize resources and WORKSPACE.md
24
24
  # ...edit files...
25
25
  overai status # local edits + remote drift
26
26
  overai diff # what changed
27
- overai review # validation + best-practice review packet
27
+ overai review # local checks + authenticated TypeScript lint + guides
28
28
  overai report bug "<details>" # report broken or unexplained behavior
29
29
  overai report feature "<details>" # request a missing platform capability
30
30
  overai push -m "why" # create new versions (not live yet)
package/dist/index.js CHANGED
@@ -34764,7 +34764,8 @@ var workflow = `# CLI editing workflow
34764
34764
  each nested workspace you need separately.
34765
34765
  2. Edit files. The id in each filename is authoritative; slugs are cosmetic.
34766
34766
  3. \`overai status\` / \`overai diff\` \u2014 review what changed.
34767
- 4. \`overai review\` \u2014 validation + these guides; fix findings first.
34767
+ 4. \`overai review\` \u2014 local checks, authenticated TypeScript lint against the
34768
+ live workspace, and these guides; fix findings first.
34768
34769
  5. \`overai push -m "why"\` \u2014 creates NEW versions (not live). Fast-forward
34769
34770
  only: if the resource moved remotely, pull, re-apply, retry. Write commit
34770
34771
  messages like PR titles \u2014 they're the version history.
@@ -34774,11 +34775,34 @@ var workflow = `# CLI editing workflow
34774
34775
 
34775
34776
  Never edit .overai/ by hand \u2014 it holds the manifest and pristine base copies
34776
34777
  that make status/diff/fast-forward work.`;
34778
+ var tablespaces = `# TableSpace best practices
34779
+
34780
+ TableSpaces are isolated PostgreSQL databases. Use PostgreSQL syntax and the
34781
+ generated env.TBSxxx client from pages, functions, and automation code.
34782
+
34783
+ - \`overai sql <tableSpaceId>\` runs SQL through OverAI's authenticated,
34784
+ audited HTTP surface. This is the normal interactive CLI path.
34785
+ - \`overai sql credential create <tableSpaceId> --name <name>\` creates a
34786
+ durable \`postgresql://\` connection string for standard database clients.
34787
+ The CLI prints a PostgreSQL 16+ libpq URL with
34788
+ \`sslmode=verify-full&sslrootcert=system\`. Older libpq clients must point
34789
+ \`sslrootcert\` at a trusted CA file. Other drivers should use the host,
34790
+ port, database, username, and secret with hostname and certificate
34791
+ verification enabled through that driver's TLS settings.
34792
+ It expires after one year unless \`--expires-in <days>\` or \`--never\` is
34793
+ supplied and permitted by organization policy.
34794
+ - Give each service its own credential. List, rotate, or revoke it with
34795
+ \`overai sql credential list|rotate|revoke\`.
34796
+ - The generated secret is the PostgreSQL password. Account passwords, PATs,
34797
+ and JWTs are not accepted. Create and rotate show the secret once.
34798
+ - Rotation, expiration, and revocation reject new connections. Existing
34799
+ sessions continue until disconnect.`;
34777
34800
  var BEST_PRACTICES = {
34778
34801
  platform: platform2,
34779
34802
  automations,
34780
34803
  pages,
34781
34804
  functions,
34805
+ tablespaces,
34782
34806
  workflow
34783
34807
  };
34784
34808
 
@@ -34802,11 +34826,11 @@ var runBestPractices = (args) => {
34802
34826
  );
34803
34827
  };
34804
34828
  var runReview = async () => {
34805
- const root = findWorkspaceRoot();
34806
- const manifest = readManifest(root);
34829
+ const { root, manifest } = openClone();
34807
34830
  const findings2 = [];
34808
34831
  const touchedTypes = /* @__PURE__ */ new Set();
34809
34832
  const changed = [];
34833
+ const lintTargets = [];
34810
34834
  for (const [id, resource] of Object.entries(manifest.resources)) {
34811
34835
  const { state, changedFiles } = localState(root, resource);
34812
34836
  if (state !== "modified") continue;
@@ -34853,6 +34877,37 @@ var runReview = async () => {
34853
34877
  }
34854
34878
  }
34855
34879
  }
34880
+ if (resource.type === "webPage") {
34881
+ changedFiles.filter((relPath) => relPath.endsWith(".tsx")).forEach((relPath) => {
34882
+ const code = readWorkingFile(root, relPath);
34883
+ if (code != null) {
34884
+ lintTargets.push({ label: relPath, code, surface: "webPage" });
34885
+ }
34886
+ });
34887
+ }
34888
+ if (resource.type === "function") {
34889
+ const codeFile = resource.files.find((path) => path.endsWith(".ts"));
34890
+ const metaFile = resource.files.find(
34891
+ (path) => path.endsWith(".meta.json")
34892
+ );
34893
+ const code = codeFile == null ? null : readWorkingFile(root, codeFile);
34894
+ const meta = metaFile == null ? null : readWorkingFile(root, metaFile);
34895
+ if (codeFile != null && code != null) {
34896
+ const parsedMeta = (() => {
34897
+ try {
34898
+ return JSON.parse(meta ?? "null");
34899
+ } catch {
34900
+ return null;
34901
+ }
34902
+ })();
34903
+ lintTargets.push({
34904
+ label: codeFile,
34905
+ code,
34906
+ surface: "function",
34907
+ functionOutputSchema: parsedMeta != null && typeof parsedMeta === "object" && "outputSchema" in parsedMeta && typeof parsedMeta.outputSchema === "string" ? parsedMeta.outputSchema : void 0
34908
+ });
34909
+ }
34910
+ }
34856
34911
  if (resource.type === "automation") {
34857
34912
  const docFile = resource.files.find((f) => f.endsWith(".json"));
34858
34913
  const raw2 = docFile == null ? null : readWorkingFile(root, docFile);
@@ -34864,6 +34919,25 @@ var runReview = async () => {
34864
34919
  (f) => `${docFile} elements.${f}`
34865
34920
  )
34866
34921
  );
34922
+ const nodes = Array.isArray(doc.elements) ? [...doc.elements] : [];
34923
+ for (const node of nodes) {
34924
+ if (node == null || typeof node !== "object") continue;
34925
+ const nodeRecord = node;
34926
+ if (Array.isArray(nodeRecord.elements)) {
34927
+ nodes.push(...nodeRecord.elements);
34928
+ }
34929
+ const inputs = nodeRecord.inputs;
34930
+ if (nodeRecord.nodeTypeId !== "TYPESCRIPT_CODE" || inputs == null || typeof inputs !== "object") {
34931
+ continue;
34932
+ }
34933
+ const code = inputs.code;
34934
+ if (typeof code !== "string") continue;
34935
+ lintTargets.push({
34936
+ label: `${docFile} ${typeof nodeRecord.variableId === "string" ? nodeRecord.variableId : "unknown node"}`,
34937
+ code,
34938
+ surface: "automationCode"
34939
+ });
34940
+ }
34867
34941
  } catch {
34868
34942
  }
34869
34943
  }
@@ -34873,6 +34947,47 @@ var runReview = async () => {
34873
34947
  process.stdout.write("Nothing modified locally \u2014 nothing to review.\n");
34874
34948
  return;
34875
34949
  }
34950
+ for (const target of lintTargets) {
34951
+ try {
34952
+ const response = await v2Fetch("/v2/architect/lint", {
34953
+ method: "POST",
34954
+ headers: { "Content-Type": "application/json" },
34955
+ body: JSON.stringify({
34956
+ code: target.code,
34957
+ surface: target.surface,
34958
+ workspaceId: manifest.workspaceId,
34959
+ functionOutputSchema: target.functionOutputSchema
34960
+ })
34961
+ });
34962
+ const responseText = await response.text();
34963
+ if (!response.ok) {
34964
+ throw new Error(
34965
+ `${target.label}: HTTP ${String(response.status)}${responseText ? ` \u2014 ${responseText}` : ""}`
34966
+ );
34967
+ }
34968
+ const parsed = lintResponseSchema.safeParse(
34969
+ (() => {
34970
+ try {
34971
+ return JSON.parse(responseText);
34972
+ } catch {
34973
+ return null;
34974
+ }
34975
+ })()
34976
+ );
34977
+ if (!parsed.success) {
34978
+ throw new Error(`${target.label}: invalid response from lint service`);
34979
+ }
34980
+ findings2.push(
34981
+ ...parsed.data.diagnostics.map(
34982
+ (diagnostic) => `${target.label}:${String(diagnostic.line)}:${String(diagnostic.column)}: ${diagnostic.severity}: ${diagnostic.message}${diagnostic.ruleId == null ? "" : ` (${diagnostic.ruleId})`}`
34983
+ )
34984
+ );
34985
+ } catch (error) {
34986
+ findings2.push(
34987
+ `${target.label}: remote lint could not run \u2014 ${error instanceof Error ? error.message : String(error)}`
34988
+ );
34989
+ }
34990
+ }
34876
34991
  const guides = ["workflow", ...touchedTypes].map((t) => t === "webPage" || t === "printablePage" ? "pages" : t).map((t) => t === "automation" ? "automations" : t).map((t) => t === "function" ? "functions" : t);
34877
34992
  const guideDocs = [...new Set(guides)].map((t) => BEST_PRACTICES[t]).filter((d) => d != null);
34878
34993
  process.stdout.write(
@@ -34894,6 +35009,112 @@ var runReview = async () => {
34894
35009
 
34895
35010
  // src/commands/sql.ts
34896
35011
  import { createInterface } from "readline";
35012
+
35013
+ // src/commands/sqlCredential.ts
35014
+ var connectionString = async (tableSpaceId, secret) => {
35015
+ const response = await v2Fetch("/v2/cluster-info");
35016
+ const body = await response.json();
35017
+ return `postgresql://overai:${encodeURIComponent(secret)}@${body.postgres.host}:${String(body.postgres.port)}/${tableSpaceId}?sslmode=${body.postgres.sslMode}${body.postgres.sslMode === "verify-full" ? "&sslrootcert=system" : ""}`;
35018
+ };
35019
+ var credentialTableSpaceId = async (credentialId) => {
35020
+ const data = await gqlRequest(
35021
+ `query TableSpaceCredential($id: String!) {
35022
+ tableSpaceCredential(id: $id) { tableSpaceId }
35023
+ }`,
35024
+ { id: credentialId }
35025
+ );
35026
+ if (!data.tableSpaceCredential) {
35027
+ throw new Error("Credential not found or you lack access to it");
35028
+ }
35029
+ return data.tableSpaceCredential.tableSpaceId;
35030
+ };
35031
+ var runSqlCredential = async (args) => {
35032
+ const [action, id] = args;
35033
+ if (!action || !id) {
35034
+ throw new Error(
35035
+ "Usage: overai sql credential create|list <tableSpaceId> | rotate|revoke <credentialId>"
35036
+ );
35037
+ }
35038
+ if (action === "create") {
35039
+ const tableSpaceId = id;
35040
+ const name2 = flagValue(args, "--name");
35041
+ if (!name2) throw new Error("create requires --name <name>");
35042
+ const expiresIn = flagValue(args, "--expires-in");
35043
+ if (expiresIn && args.includes("--never")) {
35044
+ throw new Error("Use either --expires-in <days> or --never, not both");
35045
+ }
35046
+ const days = expiresIn ? Number(expiresIn) : 365;
35047
+ if (!args.includes("--never") && (!Number.isInteger(days) || days < 1)) {
35048
+ throw new Error("--expires-in must be a positive whole number of days");
35049
+ }
35050
+ const data = await gqlRequest(
35051
+ `mutation CreateTableSpaceCredential($tableSpaceId: String!, $name: String!, $validUntil: Datetime) {
35052
+ createTableSpaceCredential(input: { tableSpaceId: $tableSpaceId, name: $name, validUntil: $validUntil }) { result }
35053
+ }`,
35054
+ {
35055
+ tableSpaceId,
35056
+ name: name2,
35057
+ validUntil: args.includes("--never") ? null : new Date(Date.now() + days * 864e5).toISOString()
35058
+ }
35059
+ );
35060
+ const result = data.createTableSpaceCredential?.result;
35061
+ if (!result) throw new Error("The server did not return a credential");
35062
+ process.stdout.write(
35063
+ `${await connectionString(tableSpaceId, result.secret)}
35064
+ `
35065
+ );
35066
+ return;
35067
+ }
35068
+ if (action === "list") {
35069
+ const tableSpaceId = id;
35070
+ const data = await gqlRequest(
35071
+ `query TableSpaceCredentials($tableSpaceId: String!) {
35072
+ tableSpaceCredentials(condition: { tableSpaceId: $tableSpaceId }) {
35073
+ nodes { id name validUntil lastUsedAt createdAt }
35074
+ }
35075
+ }`,
35076
+ { tableSpaceId }
35077
+ );
35078
+ printResult(
35079
+ data.tableSpaceCredentials?.nodes ?? [],
35080
+ resolveFormat(flagValue(args, "-o", "--output"))
35081
+ );
35082
+ return;
35083
+ }
35084
+ if (action === "rotate") {
35085
+ const credentialId = id;
35086
+ const tableSpaceId = await credentialTableSpaceId(credentialId);
35087
+ const data = await gqlRequest(
35088
+ `mutation RotateTableSpaceCredential($credentialId: String!) {
35089
+ rotateTableSpaceCredential(input: { credentialId: $credentialId }) { result }
35090
+ }`,
35091
+ { credentialId }
35092
+ );
35093
+ const secret = data.rotateTableSpaceCredential?.result;
35094
+ if (!secret) throw new Error("The server did not return a credential");
35095
+ process.stdout.write(`${await connectionString(tableSpaceId, secret)}
35096
+ `);
35097
+ return;
35098
+ }
35099
+ if (action === "revoke") {
35100
+ const credentialId = id;
35101
+ await credentialTableSpaceId(credentialId);
35102
+ await gqlRequest(
35103
+ `mutation RevokeTableSpaceCredential($id: String!, $validUntil: Datetime!) {
35104
+ updateTableSpaceCredential(input: { id: $id, patch: { validUntil: $validUntil } }) {
35105
+ tableSpaceCredential { id }
35106
+ }
35107
+ }`,
35108
+ { id: credentialId, validUntil: (/* @__PURE__ */ new Date()).toISOString() }
35109
+ );
35110
+ process.stdout.write(`${credentialId} revoked
35111
+ `);
35112
+ return;
35113
+ }
35114
+ throw new Error(`Unknown credential action: ${action}`);
35115
+ };
35116
+
35117
+ // src/commands/sql.ts
34897
35118
  var runQuery = async (organizationId, tableSpaceId, query, params) => {
34898
35119
  const res = await v2Fetch(
34899
35120
  `/v2/table-spaces/${organizationId}/${tableSpaceId}/run`,
@@ -35043,6 +35264,9 @@ var runRepl = async (organizationId, tableSpaceId, format2) => {
35043
35264
  });
35044
35265
  };
35045
35266
  var runSql = async (args) => {
35267
+ if (args[0] === "credential") {
35268
+ return runSqlCredential(args.slice(1));
35269
+ }
35046
35270
  const tableSpaceId = args[0];
35047
35271
  if (tableSpaceId == null || tableSpaceId.startsWith("-")) {
35048
35272
  throw new Error(
@@ -35516,6 +35740,10 @@ Usage:
35516
35740
  Execution history / logs
35517
35741
  overai sql <tableSpaceId> ["<SQL>"] Run SQL against a tablespace; no query
35518
35742
  opens a psql-style session (stdin scripts work)
35743
+ overai sql credential create <tableSpaceId> --name <name> [--expires-in <days>|--never]
35744
+ Create a durable PostgreSQL connection string
35745
+ overai sql credential list <tableSpaceId>
35746
+ overai sql credential rotate|revoke <credentialId>
35519
35747
 
35520
35748
  Workspace sync (edit resources as local files):
35521
35749
  overai clone <workspaceId> [dir] Materialize resources and WORKSPACE.md
@@ -35540,7 +35768,7 @@ Workspace sync (edit resources as local files):
35540
35768
  overai open [filter | id] Open a resource (or the workspace) in the webapp
35541
35769
  overai dev <filter> [--port N] [--open]
35542
35770
  Live-preview a web page from local files
35543
- overai review Validation + best-practice review packet
35771
+ overai review Local checks + authenticated TypeScript lint + guides
35544
35772
  overai best-practices [topic] Print bundled guides
35545
35773
  overai report bug|feature "<details>"
35546
35774
  Send product feedback (use - for stdin)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overaihq/cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Create, edit, deploy, and run OverAI workspace resources from the command line",
5
5
  "type": "module",
6
6
  "homepage": "https://overai.com",
@@ -30,7 +30,7 @@
30
30
  "lint": "echo 'Run lint from the root of the repo'",
31
31
  "ts-check": "tsc --noEmit",
32
32
  "build": "tsup",
33
- "build:watch": "tsc -w",
33
+ "build:watch": "tsup --watch",
34
34
  "start": "node dist/index.js",
35
35
  "test": "vitest",
36
36
  "test:localhost": "vitest --run --config vitest.localhost.config.js"