Skip to content

Commit

Permalink
Basic features
Browse files Browse the repository at this point in the history
  • Loading branch information
schpet committed Nov 23, 2024
1 parent 3b41f6f commit 03ba7c9
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 13 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on: push

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Check formatting
run: deno fmt --check

- name: Lint code
run: deno lint

- name: Type check
run: deno check main.ts
18 changes: 18 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# .github/workflows/publish.yml

name: Publish

on:
push:
branches:
- main

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # The OIDC ID token is used for authentication with JSR.
steps:
- uses: actions/checkout@v4
- run: npx jsr publish
15 changes: 13 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
{
"version": "0.0.1",
"license": "MIT",
"tasks": {
"dev": "deno run --watch main.ts"
"dev": "deno run --allow-env --allow-sys --allow-run --allow-read --allow-net main.ts",
"install": "deno install --allow-env --allow-sys --allow-run --allow-read --allow-net -c deno.json -g -n linear main.ts"
},
"imports": {
"@std/assert": "jsr:@std/assert@1"
"@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.7",
"@littletof/charmd": "jsr:@littletof/charmd@^0.1.2",
"@opensrc/deno-open": "jsr:@opensrc/deno-open@^1.0.0",
"@std/assert": "jsr:@std/assert@1",
"@std/encoding": "jsr:@std/encoding@^1.0.5",
"@std/path": "jsr:@std/path@^1.0.8"
},
"publish": {
"include": ["main.ts", "README.md"]
}
}
103 changes: 103 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

175 changes: 170 additions & 5 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,173 @@
export function add(a: number, b: number): number {
return a + b;
import { Command } from "@cliffy/command";
import { open } from "@opensrc/deno-open";
import { CompletionsCommand } from "@cliffy/command/completions";
import denoConfig from "./deno.json" with { type: "json" };
import { encodeBase64 } from "@std/encoding/base64";
import { renderMarkdown } from "@littletof/charmd";
import { basename } from "@std/path";

async function getCurrentBranch(): Promise<string> {
const process = new Deno.Command("git", {
args: ["symbolic-ref", "--short", "HEAD"],
});
const { stdout } = await process.output();
return new TextDecoder().decode(stdout).trim();
}

async function getRepoDir(): Promise<string> {
const process = new Deno.Command("git", {
args: ["rev-parse", "--show-toplevel"],
});
const { stdout } = await process.output();
const fullPath = new TextDecoder().decode(stdout).trim();
return basename(fullPath);
}

async function getIssueId(): Promise<string | null> {
const branch = await getCurrentBranch();
const match = branch.match(/[a-zA-Z]{2,5}-[1-9][0-9]*/i);
return match ? match[0].toUpperCase() : null;
}

async function getTeamId(): Promise<string | null> {
const dir = await getRepoDir();
const match = dir.match(/^[a-zA-Z]{2,5}/);
return match ? match[0].toUpperCase() : null;
}

async function fetchGraphQL(query: string, variables: Record<string, unknown>) {
const apiKey = Deno.env.get("LINEAR_API_KEY");
if (!apiKey) {
throw new Error("LINEAR_API_KEY environment variable is not set.");
}

const response = await fetch("https://api.linear.app/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": apiKey,
},
body: JSON.stringify({ query, variables }),
});

const data = await response.json();
if (data.errors) {
throw new Error(JSON.stringify(data.errors, null, 2));
}
return data;
}

// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts
if (import.meta.main) {
console.log("Add 2 + 3 =", add(2, 3));
async function fetchIssueDetails(
issueId: string,
): Promise<{ title: string; description: string | null }> {
const query = `query($id: String!) { issue(id: $id) { title, description } }`;
const data = await fetchGraphQL(query, { id: issueId });
return data.data.issue;
}

async function openTeamPage() {
const teamId = await getTeamId();
if (!teamId) {
console.error("Could not determine team id from directory name.");
Deno.exit(1);
}

const workspace = Deno.env.get("LINEAR_WORKSPACE");
if (!workspace) {
console.error("LINEAR_WORKSPACE environment variable is not set.");
Deno.exit(1);
}

const filterObj = {
"and": [{ "assignee": { "or": [{ "isMe": { "eq": true } }] } }],
};
const filter = encodeBase64(JSON.stringify(filterObj)).replace(/=/g, "");
const url =
`https://linear.app/${workspace}/team/${teamId}/active?filter=${filter}`;
await open(url, { app: { name: "Linear" } });
}

async function openIssuePage() {
const issueId = await getIssueId();
if (!issueId) {
console.error(
"The current branch does not contain a valid linear issue id.",
);
Deno.exit(1);
}

const workspace = Deno.env.get("LINEAR_WORKSPACE");
if (!workspace) {
console.error("LINEAR_WORKSPACE environment variable is not set.");
Deno.exit(1);
}

const url = `https://linear.app/${workspace}/issue/${issueId}`;
console.log(`Opening ${url} in Linear.app`);
await open(url, { app: { name: "Linear" } });
}

const teamCommand = new Command()
.description("Manage Linear teams")
.action(openTeamPage)
.command("open", "Open the team page in Linear.app")
.action(openTeamPage)
.command("id", "Print the team id derived from the repository name")
.action(async () => {
const teamId = await getTeamId();
if (teamId) {
console.log(teamId);
} else {
console.error("Could not determine team id from directory name.");
Deno.exit(1);
}
});

const issueCommand = new Command()
.description("Manage Linear issues")
.action(openIssuePage)
.command("open", "Open the issue in Linear.app")
.action(openIssuePage)
.command("print", "Print the issue details")
.option("--no-color", "Disable colored output")
.action(async ({ color }) => {
const issueId = await getIssueId();
if (!issueId) {
console.error(
"The current branch does not contain a valid linear issue id.",
);
Deno.exit(1);
}

const { title, description } = await fetchIssueDetails(issueId);
const markdown = `# ${title}${description ? "\n\n" + description : ""}`;
if (color && Deno.stderr.isTerminal()) {
console.log(renderMarkdown(markdown));
} else {
console.log(markdown);
}
})
.command("id", "Print the issue id in the current git branch")
.action(async () => {
const issueId = await getIssueId();
if (issueId) {
console.log(issueId);
} else {
console.error(
"The current branch does not contain a valid linear issue id.",
);
Deno.exit(1);
}
});

await new Command()
.name("linear")
.version(denoConfig.version)
.description("Handy linear commands from the command line")
.action(() => {
console.log("Use --help to see available commands");
})
.command("issue", issueCommand)
.command("team", teamCommand)
.command("completions", new CompletionsCommand())
.parse(Deno.args);
6 changes: 0 additions & 6 deletions main_test.ts

This file was deleted.

0 comments on commit 03ba7c9

Please sign in to comment.