Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e2fbd73
fix(cli): edge and cosmetic parity sweep from the 2026-07-24 audit (C…
Coly010 Jul 28, 2026
9ae621e
fix(cli): inject Go aqua/bold styling from legacy handlers so next sh…
Coly010 Jul 29, 2026
da936f1
fix(cli): reject negative storage cp --jobs at flag-parse time like G…
Coly010 Jul 29, 2026
c5f2e4d
fix(cli): validate telemetry session_last_active with Go's calendar r…
Coly010 Jul 29, 2026
1372885
fix(cli): narrow telemetry.json parse with an isRecord type guard ins…
Coly010 Jul 29, 2026
8bf2f29
fix(cli): keep Go bold styling out of json/stream-json no-functions d…
Coly010 Jul 29, 2026
32e0c04
fix(cli): accept Go's comma fractional-second separator in telemetry …
Coly010 Jul 29, 2026
40b3adb
fix(cli): validate storage cp --jobs raw token like Go's strconv.Pars…
Coly010 Jul 29, 2026
41a9e6c
test(cli): apply oxfmt to telemetry state comma tests
Coly010 Jul 29, 2026
0d1aace
fix(cli): measure snippet UUID forms in UTF-8 bytes like Go's uuid.Pa…
Coly010 Jul 29, 2026
2743a67
fix(cli): quote --jobs error token with Go %q semantics like pflag/st…
Coly010 Jul 29, 2026
c2e22b9
fix(cli): compute telemetry session expiry from the Go-shape timestam…
Coly010 Jul 29, 2026
c2afec1
Merge remote-tracking branch 'origin/develop' into columferry/cli-199…
Coly010 Jul 30, 2026
9b8e14c
Merge remote-tracking branch 'origin/develop' into columferry/cli-199…
Coly010 Jul 30, 2026
c5890a9
fix(cli): reject mistyped enabled on the telemetry consent path like …
Coly010 Jul 30, 2026
08efdb1
Merge remote-tracking branch 'origin/develop' into columferry/cli-199…
Coly010 Jul 30, 2026
b09081e
fix(cli): reject telemetry-state integers outside Go's int64 range (r…
Coly010 Jul 30, 2026
f3b72a3
fix(cli): start pull retry banners on a fresh line like Go's Fprintln…
Coly010 Jul 30, 2026
067e7e4
test(cli): add live coverage for stop --no-backup --debug prune repor…
Coly010 Jul 30, 2026
33ddb2e
chore(cli): apply oxfmt formatting to telemetry state layer
Coly010 Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/cli/src/legacy/auth/legacy-credentials.layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,10 @@ const makeLegacyCredentials = Effect.gen(function* () {
);
if (ok) return;
}
yield* fs.makeDirectory(fallbackDir, { recursive: true, mode: 0o700 }).pipe(Effect.orDie);
// Go's `fallbackSaveToken` creates the dir via `MkdirIfNotExistFS` →
// `MkdirAll(path, 0755)` (`access_token.go:91`, `misc.go:273`); only
// the token FILE itself is private (0600, `access_token.go:94`).
yield* fs.makeDirectory(fallbackDir, { recursive: true, mode: 0o755 }).pipe(Effect.orDie);
yield* fs.writeFileString(fallbackPath, token, { mode: 0o600 }).pipe(Effect.orDie);
}),

Expand Down
26 changes: 23 additions & 3 deletions apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

Expand Down Expand Up @@ -357,12 +365,24 @@ describe("legacyCredentialsLayer.saveAccessToken", () => {

it.effect("falls back to the filesystem when the keyring write throws", () => {
throwOnSetPassword = true;
// Deterministic mode assertions require a permissive umask: Go pins the
// fallback dir to 0755 (`access_token.go:91` → `MkdirIfNotExistFS`,
// `misc.go:273`, changed from the prior 0700) and the token file itself to
// 0600 (`access_token.go:94`).
const prevUmask = process.umask(0);
return Effect.gen(function* () {
const { saveAccessToken } = yield* LegacyCredentials;
yield* saveAccessToken(VALID_TOKEN);
const content = readFileSync(join(tempHome, ".supabase", "access-token"), "utf-8");
const fallbackDir = join(tempHome, ".supabase");
const fallbackPath = join(fallbackDir, "access-token");
const content = readFileSync(fallbackPath, "utf-8");
expect(content).toBe(VALID_TOKEN);
}).pipe(Effect.provide(makeLayer()));
expect(statSync(fallbackDir).mode & 0o777).toBe(0o755);
expect(statSync(fallbackPath).mode & 0o777).toBe(0o600);
}).pipe(
Effect.provide(makeLayer()),
Effect.ensuring(Effect.sync(() => process.umask(prevUmask))),
);
});

it.effect("filesystem fallback honors SUPABASE_HOME when configured", () => {
Expand Down
12 changes: 9 additions & 3 deletions apps/cli/src/legacy/commands/db/dump/dump.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ export const legacyDbDump = Effect.fn("legacy.db.dump")(function* (flags: Legacy
} as const);
const modeEnv = mode.buildEnv(conn, opt);

// Go keys every `--file` branch off `len(path) > 0`, not flag presence
// (`internal/db/dump/dump.go:20-32`; `cmd/db.go:152-159` for the PostRun
// print): an explicit `--file ""` means stdout, with no file open and no
// `Dumped schema to …` line.
const fileFlag = Option.filter(flags.file, (file) => file.length > 0);

// 5. Dry-run: print the env-expanded script to stdout (no container).
if (flags.dryRun) {
yield* output.raw("DRY RUN: *only* printing the pg_dump script to console.\n", "stderr");
Expand All @@ -235,8 +241,8 @@ export const legacyDbDump = Effect.fn("legacy.db.dump")(function* (flags: Legacy
// stderr line here WITHOUT creating/truncating the file — Go never touches it on
// a dry-run (`internal/db/dump/dump.go:23-32`). Resolve the path like the real
// path (Go's `filepath.Abs` after the PreRun chdir into the workdir).
if (Option.isSome(flags.file)) {
const dryRunFile = path.resolve(cliConfig.workdir, flags.file.value);
if (Option.isSome(fileFlag)) {
const dryRunFile = path.resolve(cliConfig.workdir, fileFlag.value);
yield* output.raw(`Dumped schema to ${legacyBold(dryRunFile)}.\n`, "stderr");
}
return;
Expand All @@ -258,7 +264,7 @@ export const legacyDbDump = Effect.fn("legacy.db.dump")(function* (flags: Legacy
// in PersistentPreRunE before opening the file (`cmd/root.go:104` →
// `internal/utils/misc.go`), so `--workdir /repo db dump -f out.sql` writes
// `/repo/out.sql`. `path.resolve` leaves absolute paths unchanged.
const resolvedFile = Option.map(flags.file, (file) => path.resolve(cliConfig.workdir, file));
const resolvedFile = Option.map(fileFlag, (file) => path.resolve(cliConfig.workdir, file));

// Open (create + truncate) the output file up front so an unwritable `--file`
// path fails before the dump runs, matching Go's `OpenFile(O_WRONLY|O_CREATE|
Expand Down
13 changes: 13 additions & 0 deletions apps/cli/src/legacy/commands/db/dump/dump.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,19 @@ describe("legacy db dump integration", () => {
}).pipe(Effect.provide(layer));
});

it.live("treats an explicit --file '' as stdout on --dry-run (Go: len(path) > 0)", () => {
// Go keys every --file branch off len(path) > 0, not flag presence
// (internal/db/dump/dump.go:20-32); an explicit empty --file means stdout, with
// no "Dumped schema to …" line and no file ever touched.
const { layer, out, docker } = setup({ isLocal: true });
return Effect.gen(function* () {
yield* legacyDbDump(flags({ dryRun: true, local: Option.some(true), file: Option.some("") }));
expect(out.stderrText).toContain("DRY RUN: *only* printing the pg_dump script to console.");
expect(out.stderrText).not.toContain("Dumped schema to");
expect(docker.lastOpts).toBeUndefined();
}).pipe(Effect.provide(layer));
});

it.live("validates the merged config before the --dry-run print (Go root PreRun order)", () => {
// Go runs ParseDatabaseConfig (→ config.Load → Validate) in the root PreRunE
// before dump.Run, even for --dry-run, so an invalid config fails without printing.
Expand Down
34 changes: 3 additions & 31 deletions apps/cli/src/legacy/commands/db/query/query.format.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Option } from "effect";

import { legacyGoFormatFloat } from "../../../shared/legacy-go-float.ts";
import { legacyStringWidth } from "../../../shared/legacy-rune-width.ts";

// `JSON.rawJSON` (ES2025, present in Bun) wraps a string so `JSON.stringify` emits it
Expand All @@ -19,35 +20,6 @@ declare global {
* Go-parity rules (NULL rendering, key sort order, HTML escaping) are explicit.
*/

/**
* Render a number the way Go's `fmt.Sprintf("%v", float64)` does — JSON numbers
* decode to `float64`, so Go uses shortest `%g`: exponent form when the decimal
* exponent is `< -4` or `>= 6` (e.g. `1000000` → `1e+06`, `1.5e8` → `1.5e+08`,
* `1e-5` → `1e-05`), fixed notation otherwise. The exponent is signed and at least
* two digits. JS fixed notation matches Go for the `[-4, 6)` range, so only the
* exponent cases need reformatting.
*/
function goFormatFloat(n: number): string {
if (Number.isNaN(n)) return "NaN";
if (!Number.isFinite(n)) return n > 0 ? "+Inf" : "-Inf";
// Go's `%v` preserves the sign of negative zero (`-0`); `n === 0` is true for
// both `+0` and `-0`, so distinguish them with `Object.is` before the shortcut.
if (Object.is(n, -0)) return "-0";
if (n === 0) return "0";
const neg = n < 0;
const abs = Math.abs(n);
const [mantissa, eRaw] = abs.toExponential().split("e");
const exp = Number.parseInt(eRaw!, 10);
let out: string;
if (exp < -4 || exp >= 6) {
const mag = Math.abs(exp).toString().padStart(2, "0");
out = `${mantissa}e${exp < 0 ? "-" : "+"}${mag}`;
} else {
out = abs.toString();
}
return neg ? `-${out}` : out;
}

/**
* Reproduce Go's `fmt.Sprintf("%v", v)` for JSON-decoded (`interface{}`) values:
* objects → `map[k:v ...]` with byte-sorted keys, arrays → `[a b ...]`
Expand All @@ -58,7 +30,7 @@ function goFormatValue(value: unknown): string {
if (value === null || value === undefined) return "<nil>";
if (typeof value === "string") return value;
if (typeof value === "boolean") return value ? "true" : "false";
if (typeof value === "number") return goFormatFloat(value);
if (typeof value === "number") return legacyGoFormatFloat(value);
// `bytea` columns: pgx scans them into a Go `[]byte`, so `fmt.Sprintf("%v")`
// prints the decimal byte values space-separated in brackets (`[222 173]`).
// node-postgres returns a `Buffer` (a `Uint8Array`), which would otherwise hit
Expand Down Expand Up @@ -231,7 +203,7 @@ export function legacyMakeLocalCellFormatter(
// Defensive: native rows may still carry a `Date`; render it like Go's `%v`.
if (value instanceof Date) return formatGoTime(value);
if (typeof value === "number" && (oid === PG_FLOAT4_OID || oid === PG_FLOAT8_OID)) {
return goFormatFloat(value);
return legacyGoFormatFloat(value);
}
return legacyFormatValue(value);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Effect, Option } from "effect";

import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts";
import { legacyAqua } from "../../../shared/legacy-colors.ts";
import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts";
import { Output } from "../../../../shared/output/output.service.ts";
import { Stdin } from "../../../../shared/runtime/stdin.service.ts";
Expand Down Expand Up @@ -59,9 +60,10 @@ export const legacyEncryptionUpdateRootKey = Effect.fn("legacy.encryption.update
return;
}

// text — Go prints a plain finished notice to stderr (`fmt.Fprintln`,
// `utils.Aqua` rendered as plain text per the legacy-port convention).
yield* output.raw("Finished supabase root-key update.\n", "stderr");
// text — Go: `fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase
// root-key update")+".")` (`internal/encryption/update/update.go:26`).
// `legacyAqua` renders cyan on a TTY and plain when piped, like lipgloss.
yield* output.raw(`Finished ${legacyAqua("supabase root-key update")}.\n`, "stderr");
}).pipe(Effect.ensuring(linkedProjectCache.cache(ref)), Effect.ensuring(telemetryState.flush));
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ describe("legacy encryption update-root-key integration", () => {
// Go parity: prompt to stderr, trailing newline to stdout (defer Println),
// finished notice to stderr.
expect(out.stderrText).toContain("Enter a new root key: ");
expect(out.stderrText).toContain("Finished supabase root-key update.");
// The command path is wrapped in ANSI (legacyAqua) in colour-capable
// environments, so assert on the tokens around it — same convention as
// `db/reset/reset.integration.test.ts`'s aqua'd branch name.
expect(out.stderrText).toContain("Finished ");
expect(out.stderrText).toContain("supabase root-key update");
expect(out.stdoutText).toBe("\n");
}).pipe(Effect.provide(layer));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Effect, Option } from "effect";
import { deleteFunction } from "../../../../shared/functions/delete.ts";
import { legacyAqua } from "../../../shared/legacy-colors.ts";
import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts";
import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts";
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
Expand Down Expand Up @@ -27,6 +28,10 @@ export const legacyFunctionsDelete = Effect.fn("legacy.functions.delete")(functi
}),
),
),
// Go: `fmt.Printf("Deleted Function %s from project %s.\n", utils.Aqua(slug),
// utils.Aqua(projectRef))` (`internal/functions/delete/delete.go:20`) —
// stdout-bound, so the TTY gate must check stdout.
styleIdentifier: (text) => legacyAqua(text, process.stdout),
},
).pipe(
Effect.ensuring(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ function mockContextualAnalytics() {
return { layer, captured };
}

// Strip ANSI SGR (aqua slug/ref via `legacyAqua`) so byte-assertions are
// stable whether or not the test stdout supports color.
// eslint-disable-next-line no-control-regex
const stripSgr = (text: string) => text.replace(/\x1b\[[0-9;]*m/gu, "");

describe("legacy functions delete", () => {
it.live("deletes a function natively through the Management API", () => {
const out = mockOutput({ format: "text" });
Expand All @@ -66,7 +71,9 @@ describe("legacy functions delete", () => {
expect(api.requests[0]?.url).toBe(
"https://api.supabase.com/v1/projects/abcdefghijklmnopqrst/functions/hello-world",
);
expect(out.stdoutText).toBe(
// The slug and ref are wrapped in ANSI (legacyAqua) in colour-capable
// environments — strip SGR so the byte assertion stays stable.
expect(stripSgr(out.stdoutText)).toBe(
"Deleted Function hello-world from project abcdefghijklmnopqrst.\n",
);
expect(linkedProjectCache.cached).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { Effect, Option, Stdio } from "effect";
import { deployFunctions } from "../../../../shared/functions/deploy.ts";
import { legacyAqua, legacyBold } from "../../../shared/legacy-colors.ts";
import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts";
import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts";
import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts";
Expand Down Expand Up @@ -56,6 +57,14 @@ export const legacyFunctionsDeploy = Effect.fn("legacy.functions.deploy")(functi
}),
),
),
// Go: `fmt.Printf("Deployed Functions on project %s: %s\n",
// utils.Aqua(flags.ProjectRef), …)` (`internal/functions/deploy/deploy.go:70`)
// — stdout-bound, so the TTY gate must check stdout.
styleIdentifier: (text) => legacyAqua(text, process.stdout),
// Go: `utils.Bold` on the `Bundling Function:` slug (`bundle.go:30`, stderr)
// and the no-functions error dir (`deploy.go:35`, rendered on stderr) —
// both stderr-bound, matching `legacyBold`'s default TTY gate.
styleEmphasis: (text) => legacyBold(text),
}).pipe(
Effect.ensuring(
Effect.suspend(() =>
Expand Down
Loading