Vivek Kodira

Vivek Kodira

// Name: github project tasks
// Description: Fetch all tasks in a GitHub ProjectV2 using the GitHub GraphQL API 
// and flattens the response into a flat object so that it can be easily inspected.
// Author: Vivek Kodira

import "@johnlindquist/kit";

const GITHUB_API_URL = "https://api.github.com/graphql";
const GITHUB_ACCESS_TOKEN = await env("GITHUB_ACCESS_TOKEN");
const GITHUB_PROJECT_ID = await env("GITHUB_PROJECT_ID");

// TypeScript types
interface FieldValue {
  name?: string; // Status field (e.g., "Done", "In Progress")
}

interface ProjectItem {
  id: string;
  title: string;
  fieldValues: {
    nodes: FieldValue[];
  };
}

interface PageInfo {
  hasNextPage: boolean;
  endCursor: string | null;
}

interface ProjectItemsResponse {
  data?: {
    node: {
      items: {
        nodes: ProjectItem[];
        pageInfo: PageInfo;
      };
    };
  };
  errors?: { message: string }[];
}

// Fetch tasks in a GitHub ProjectV2 with pagination
async function fetchAllProjectTasks(projectId: string): Promise<ProjectItem[]> {
  let tasks: ProjectItem[] = [];
  let hasNextPage = true;
  let endCursor: string | null = null;

  while (hasNextPage) {
    const query = `

        query ($projectId: ID!, $after: String) {
    node(id: $projectId) {
        ... on ProjectV2 {
            items(first: 50, after: $after) {
                nodes {
                    id
                    content {
                        ... on Issue {
                            title
                            number
                            repository {
                                name
                                owner { login }
                            }
                            assignees(first: 5) {
                                nodes { login }
                            }
                            closed
                        }
                    }
                    fieldValues(first: 100) {
                        nodes {
                            ... on ProjectV2ItemFieldSingleSelectValue {
                                name
                                field { ... on ProjectV2FieldCommon { name } }
                            }
                            ... on ProjectV2ItemFieldTextValue {
                                text
                                field { ... on ProjectV2FieldCommon { name } }
                            }
                            ... on ProjectV2ItemFieldNumberValue {
                                number
                                field { ... on ProjectV2FieldCommon { name } }
                            }
                            ... on ProjectV2ItemFieldIterationValue {  
                                title  
                                field { 
                                    ... on ProjectV2FieldCommon { name } 
                                }
                            }
                        }
                    }
                }
                pageInfo {
                    hasNextPage
                    endCursor
                }
            }
        }
    }
}

`;

    const variables = { projectId, after: endCursor };

    const response = await fetch(GITHUB_API_URL, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${GITHUB_ACCESS_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ query, variables }),
    });

    const data: ProjectItemsResponse = await response.json();

    if (data.errors) {
      console.error("GraphQL Errors:", data.errors);
      return tasks;
    }

    const items = data.data?.node.items;
    if (!items) break;

    tasks = tasks.concat(items.nodes);

    // Update pagination info
    hasNextPage = items.pageInfo.hasNextPage;
    endCursor = items.pageInfo.endCursor;
  }

  return tasks;
}

// Flatten a GraphQL response into a flat object
const flattenGraphQLResponse = function (response) {
  let flat = {};

  // Extract top-level fields
  flat["id"] = response.id;
  if (response.content) {
    flat["title"] = response.content.title;
    flat["issue_number"] = response.content.number;
    flat["repository"] = response.content.repository?.name;
    flat["repo_owner"] = response.content.repository?.owner?.login;
    flat["closed"] = response.content.closed;

    // Extract assignees (comma-separated list)
    flat["assignees"] = response.content?.assignees?.nodes
      .map((a) => a.login)
      .join(", ");
  }

  // Extract fieldValues
  if (response.fieldValues) {
    response.fieldValues.nodes.forEach((field) => {
      if (field.field && field.field.name) {
        if (field.text !== undefined) {
          flat[field.field.name] = field.text;
        } else if (field.name !== undefined) {
          flat[field.field.name] = field.name;
        } else if (field.number !== undefined) {
          flat[field.field.name] = field.number;
        } else if (field.title !== undefined) {
          flat[field.field.name] = field.title;
        }
      }
    });
  }

  return flat;
};

try {
  let tasks = await fetchAllProjectTasks(GITHUB_PROJECT_ID);
  let flattenedTasks = tasks.map((task) => flattenGraphQLResponse(task));
  inspect(flattenedTasks, "falttened tasks");
} catch (error) {
  console.log("Fetch Error:", error);
}

/*
# Bookmarks Manager.
*/

// Name: bookmarksManager
// Description: Manage bookmarks. Open a bookmark from a list of bookmarks.
// Author: Vivek Kodira

import "@johnlindquist/kit";

// NOTE: This array should ideally be part of another file and imported here. You'd have one for each group of bookmarks
// This is just a sample array to demonstrate the concept. 
// TIP: Make the name as descriptive as possible. Since ScriptKit supports fuzzy search, you will find the bookmark easier

export const tools = [
  {
    name: "ScriptKit: API", 
    value: "https://johnlindquist.github.io/kit-docs/",
  },
  {
    name: "ScriptKit: gitrepo",
    value: "https://github.com/johnlindquist/kit",
  },
  {
    name: "ScriptKit: website",
    value: "https://www.scriptkit.com/",
  },
  {
    name: "Jest: CLI config",
    value: "https://jestjs.io/docs/cli",
  },
  {
    name: "ESlint docs",
    value: "https://eslint.org/docs/latest/",
  },
  {
    name: "Github Search documentation",
    value:
      "https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests",
  },
  {
    name: "Material UI | MUI Component list",
    value: "https://mui.com/material-ui/all-components/",
  },
];

/* Actual start of script. */
export const bookmarks = [
  ...tools, // Spread each set of bookmarks into the main array
];

// Not used by this script but can be exported so you can open several bookmarks at one go
// Ex: open all the bookmarks associated with a particular project
export const openBookMarks = async (bookmarksToOpen) => {
  for (let bookmark of bookmarksToOpen) {
    const chosenBookmark = bookmarks.find((b) => b.name.includes(bookmark));
    if (chosenBookmark) {
      await exec(`open "${chosenBookmark.value}"`);
    }
  }
};

let url = await arg(
  { strict: false, ignoreBlur: true, placeholder: `Select bookmark` },
  bookmarks
);

exec(`open "${url}"`);