Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions apps/cli/src/legacy/cli/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { legacyFunctionsCommand } from "../commands/functions/functions.command.
import { legacyGenCommand } from "../commands/gen/gen.command.ts";
import { legacyInitCommand } from "../commands/init/init.command.ts";
import { legacyInspectCommand } from "../commands/inspect/inspect.command.ts";
import { legacyIssueCommand } from "../commands/issue/issue.command.ts";
import { legacyLinkCommand } from "../commands/link/link.command.ts";
import { legacyLoginCommand } from "../commands/login/login.command.ts";
import { legacyLogoutCommand } from "../commands/logout/logout.command.ts";
Expand Down Expand Up @@ -69,6 +70,7 @@ export const legacyRoot = Command.make("supabase").pipe(
legacyGenCommand,
legacyInitCommand,
legacyInspectCommand,
legacyIssueCommand,
legacyLinkCommand,
legacyLoginCommand,
legacyLogoutCommand,
Expand Down
11 changes: 11 additions & 0 deletions apps/cli/src/legacy/commands/issue/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# `supabase issue`

## Side effects

- Opens a GitHub issue form URL in the user's default browser, unless `--no-browser` is passed.
- Writes the generated issue form URL to stdout.

## No local project changes

This command does not read or write Supabase project files, stack state, credentials, or linked
project metadata.
129 changes: 129 additions & 0 deletions apps/cli/src/legacy/commands/issue/issue.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";
import { browserLayer } from "../../../shared/runtime/browser.layer.ts";
import { commandRuntimeLayer } from "../../../shared/runtime/command-runtime.layer.ts";
import { withJsonErrorHandling } from "../../../shared/output/json-error-handling.ts";
import { withLegacyCommandInstrumentation } from "../../telemetry/legacy-command-instrumentation.ts";
import { legacyIssueBug, legacyIssueDocs, legacyIssueFeature } from "./issue.handler.ts";

const legacyIssueNoBrowserFlag = Flag.boolean("no-browser").pipe(
Flag.withDescription("Print the issue form URL without opening a browser."),
);

const legacyIssueOptionalTextFlag = (name: string, description: string) =>
Flag.string(name).pipe(Flag.withDescription(description), Flag.optional);

const legacyIssueCommonContextFlag = legacyIssueOptionalTextFlag(
"additional-context",
"Extra context to prefill on the issue form.",
);

const legacyIssueBugConfig = {
area: legacyIssueOptionalTextFlag("area", "Affected CLI area."),
command: legacyIssueOptionalTextFlag("command", "Command that failed."),
actualOutput: legacyIssueOptionalTextFlag("actual-output", "Actual output or error text."),
expectedBehavior: legacyIssueOptionalTextFlag("expected-behavior", "Expected behavior."),
reproduce: legacyIssueOptionalTextFlag("reproduce", "Steps to reproduce."),
ticketId: legacyIssueOptionalTextFlag("ticket-id", "Crash report or support ticket ID."),
dockerServices: legacyIssueOptionalTextFlag(
"docker-services",
"Relevant Docker service status or logs.",
),
additionalContext: legacyIssueCommonContextFlag,
noBrowser: legacyIssueNoBrowserFlag,
} as const;

const legacyIssueFeatureConfig = {
existingIssues: Flag.boolean("existing-issues").pipe(
Flag.withDescription("Prefill the existing issues checklist."),
),
area: legacyIssueOptionalTextFlag("area", "Affected CLI area."),
problem: legacyIssueOptionalTextFlag("problem", "Problem the feature should solve."),
proposedSolution: legacyIssueOptionalTextFlag("proposed-solution", "Proposed solution."),
alternatives: legacyIssueOptionalTextFlag("alternatives", "Alternatives considered."),
additionalContext: legacyIssueCommonContextFlag,
noBrowser: legacyIssueNoBrowserFlag,
} as const;

const legacyIssueDocsConfig = {
link: legacyIssueOptionalTextFlag("link", "Relevant documentation link."),
issueType: legacyIssueOptionalTextFlag("issue-type", "Documentation issue type."),
problem: legacyIssueOptionalTextFlag("problem", "What is confusing, missing, or incorrect."),
improvement: legacyIssueOptionalTextFlag("improvement", "Suggested documentation improvement."),
additionalContext: legacyIssueCommonContextFlag,
noBrowser: legacyIssueNoBrowserFlag,
} as const;

export type LegacyIssueBugFlags = CliCommand.Command.Config.Infer<typeof legacyIssueBugConfig>;
export type LegacyIssueFeatureFlags = CliCommand.Command.Config.Infer<
typeof legacyIssueFeatureConfig
>;
export type LegacyIssueDocsFlags = CliCommand.Command.Config.Infer<typeof legacyIssueDocsConfig>;

const legacyIssueBugCommand = Command.make("bug", legacyIssueBugConfig).pipe(
Command.withDescription("Open a GitHub bug report with local CLI details prefilled."),
Command.withShortDescription("Open a bug report"),
Command.withExamples([
{
command:
'supabase issue bug --command "supabase start" --actual-output "database failed to start"',
description: "Open a prefilled bug report for a failing command",
},
{
command: 'supabase issue bug --ticket-id "abc123" --no-browser',
description: "Print a prefilled issue URL for a crash report",
},
]),
Command.withHandler((flags) =>
legacyIssueBug(flags).pipe(withLegacyCommandInstrumentation({ flags }), withJsonErrorHandling),
),
Command.provide(commandRuntimeLayer(["issue", "bug"])),
Command.provide(browserLayer),
);

const legacyIssueFeatureCommand = Command.make("feature", legacyIssueFeatureConfig).pipe(
Command.withDescription("Open a GitHub feature request with useful context prefilled."),
Command.withShortDescription("Open a feature request"),
Command.withExamples([
{
command:
'supabase issue feature --existing-issues --problem "I need to rotate local secrets" --proposed-solution "Add a secrets rotate command"',
description: "Open a prefilled feature request",
},
]),
Command.withHandler((flags) =>
legacyIssueFeature(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
),
Command.provide(commandRuntimeLayer(["issue", "feature"])),
Command.provide(browserLayer),
);

const legacyIssueDocsCommand = Command.make("docs", legacyIssueDocsConfig).pipe(
Command.withDescription("Open a GitHub documentation issue with useful context prefilled."),
Command.withShortDescription("Open a documentation issue"),
Command.withExamples([
{
command:
'supabase issue docs --link "https://supabase.com/docs/guides/cli" --problem "The flag description is outdated"',
description: "Open a prefilled documentation issue",
},
]),
Command.withHandler((flags) =>
legacyIssueDocs(flags).pipe(withLegacyCommandInstrumentation({ flags }), withJsonErrorHandling),
),
Command.provide(commandRuntimeLayer(["issue", "docs"])),
Command.provide(browserLayer),
);

export const legacyIssueCommand = Command.make("issue").pipe(
Command.withDescription("Open Supabase CLI GitHub issue forms."),
Command.withShortDescription("Open GitHub issue forms"),
Command.withSubcommands([
legacyIssueBugCommand,
legacyIssueFeatureCommand,
legacyIssueDocsCommand,
]),
);
88 changes: 88 additions & 0 deletions apps/cli/src/legacy/commands/issue/issue.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Effect } from "effect";
import {
buildIssueUrl,
inferIssueInstallMethod,
issueTemplateContract,
readIssueFlagValue,
searchedExistingIssuesValue,
} from "../../../shared/issue/issue-url.ts";
import { Output } from "../../../shared/output/output.service.ts";
import { Browser } from "../../../shared/runtime/browser.service.ts";
import { RuntimeInfo } from "../../../shared/runtime/runtime-info.service.ts";
import { TelemetryRuntime } from "../../../shared/telemetry/runtime.service.ts";
import type {
LegacyIssueBugFlags,
LegacyIssueDocsFlags,
LegacyIssueFeatureFlags,
} from "./issue.command.ts";

const legacyOpenIssueUrl = Effect.fnUntraced(function* (url: string, noBrowser: boolean) {
const output = yield* Output;
if (!noBrowser) {
const browser = yield* Browser;
yield* browser.open(url);
yield* output.success("Opened GitHub issue form.", { url });
} else {
yield* output.info("GitHub issue form URL:");
}
yield* output.raw(`${url}\n`);
});

export const legacyIssueBug = Effect.fn("legacy.issue.bug")(function* (flags: LegacyIssueBugFlags) {
const runtimeInfo = yield* RuntimeInfo;
const telemetryRuntime = yield* TelemetryRuntime;

const url = buildIssueUrl({
template: issueTemplateContract.bug.template,
fields: {
"affected-area": readIssueFlagValue(flags.area),
"cli-version": telemetryRuntime.cliVersion,
os: `${runtimeInfo.platform} ${runtimeInfo.arch}`,
"install-method": inferIssueInstallMethod(runtimeInfo),
command: readIssueFlagValue(flags.command),
"actual-output": readIssueFlagValue(flags.actualOutput),
"expected-behavior": readIssueFlagValue(flags.expectedBehavior),
reproduce: readIssueFlagValue(flags.reproduce),
"ticket-id": readIssueFlagValue(flags.ticketId),
"docker-services": readIssueFlagValue(flags.dockerServices),
"additional-context": readIssueFlagValue(flags.additionalContext),
},
});

yield* legacyOpenIssueUrl(url, flags.noBrowser);
});

export const legacyIssueFeature = Effect.fn("legacy.issue.feature")(function* (
flags: LegacyIssueFeatureFlags,
) {
const url = buildIssueUrl({
template: issueTemplateContract.feature.template,
fields: {
"existing-issues": flags.existingIssues ? searchedExistingIssuesValue : undefined,
"affected-area": readIssueFlagValue(flags.area),
problem: readIssueFlagValue(flags.problem),
"proposed-solution": readIssueFlagValue(flags.proposedSolution),
alternatives: readIssueFlagValue(flags.alternatives),
"additional-context": readIssueFlagValue(flags.additionalContext),
},
});

yield* legacyOpenIssueUrl(url, flags.noBrowser);
});

export const legacyIssueDocs = Effect.fn("legacy.issue.docs")(function* (
flags: LegacyIssueDocsFlags,
) {
const url = buildIssueUrl({
template: issueTemplateContract.docs.template,
fields: {
link: readIssueFlagValue(flags.link),
"issue-type": readIssueFlagValue(flags.issueType),
problem: readIssueFlagValue(flags.problem),
improvement: readIssueFlagValue(flags.improvement),
"additional-context": readIssueFlagValue(flags.additionalContext),
},
});

yield* legacyOpenIssueUrl(url, flags.noBrowser);
});
Loading