diff --git a/.agents/skills/agents-md/SKILL.md b/.agents/skills/agents-md/SKILL.md new file mode 100644 index 0000000..957d8c7 --- /dev/null +++ b/.agents/skills/agents-md/SKILL.md @@ -0,0 +1,111 @@ +--- +name: agents-md +description: This skill should be used when the user asks to "create AGENTS.md", "update AGENTS.md", "maintain agent docs", "set up CLAUDE.md", or needs to keep agent instructions concise. Guides discovery of local skills and enforces minimal documentation style. +--- + +# Maintaining AGENTS.md + +AGENTS.md is the canonical agent-facing documentation. Keep it minimal—agents are capable and don't need hand-holding. + +## File Setup + +1. Create `AGENTS.md` at project root +2. Create symlink: `ln -s AGENTS.md CLAUDE.md` + +## Before Writing + +Discover local skills to reference: + +```bash +find .claude/skills -name "SKILL.md" 2>/dev/null +ls plugins/*/skills/*/SKILL.md 2>/dev/null +``` + +Read each skill's frontmatter to understand when to reference it. + +## Writing Rules + +- **Headers + bullets** - No paragraphs +- **Code blocks** - For commands and templates +- **Reference, don't duplicate** - Point to skills: "Use `db-migrate` skill. See `.claude/skills/db-migrate/SKILL.md`" +- **No filler** - No intros, conclusions, or pleasantries +- **Trust capabilities** - Omit obvious context + +## Required Sections + +### Package Manager +Which tool and key commands only: +```markdown +## Package Manager +Use **pnpm**: `pnpm install`, `pnpm dev`, `pnpm test` +``` + +### Commit Attribution +Always include this section. Agents should use their own identity: +```markdown +## Commit Attribution +AI commits MUST include: +``` +Co-Authored-By: (the agent model's name and attribution byline) +``` +Example: `Co-Authored-By: Claude Sonnet 4 ` +``` + +### Key Conventions +Project-specific patterns agents must follow. Keep brief. + +### Local Skills +Reference each discovered skill: +```markdown +## Database +Use `db-migrate` skill for schema changes. See `.claude/skills/db-migrate/SKILL.md` + +## Testing +Use `write-tests` skill. See `.claude/skills/write-tests/SKILL.md` +``` + +## Optional Sections + +Add only if truly needed: +- API route patterns (show template, not explanation) +- CLI commands (table format) +- File naming conventions + +## Anti-Patterns + +Omit these: +- "Welcome to..." or "This document explains..." +- "You should..." or "Remember to..." +- Content duplicated from skills (reference instead) +- Obvious instructions ("run tests", "write clean code") +- Explanations of why (just say what) +- Long prose paragraphs + +## Example Structure + +```markdown +# Agent Instructions + +## Package Manager +Use **pnpm**: `pnpm install`, `pnpm dev` + +## Commit Attribution +AI commits MUST include: +``` +Co-Authored-By: (the agent model's name and attribution byline) +``` + +## API Routes +[Template code block] + +## Database +Use `db-migrate` skill. See `.claude/skills/db-migrate/SKILL.md` + +## Testing +Use `write-tests` skill. See `.claude/skills/write-tests/SKILL.md` + +## CLI +| Command | Description | +|---------|-------------| +| `pnpm cli sync` | Sync data | +``` diff --git a/.agents/skills/brainstorming/SKILL.md b/.agents/skills/brainstorming/SKILL.md new file mode 100644 index 0000000..2fd19ba --- /dev/null +++ b/.agents/skills/brainstorming/SKILL.md @@ -0,0 +1,54 @@ +--- +name: brainstorming +description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation." +--- + +# Brainstorming Ideas Into Designs + +## Overview + +Help turn ideas into fully formed designs and specs through natural collaborative dialogue. + +Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design in small sections (200-300 words), checking after each section whether it looks right so far. + +## The Process + +**Understanding the idea:** +- Check out the current project state first (files, docs, recent commits) +- Ask questions one at a time to refine the idea +- Prefer multiple choice questions when possible, but open-ended is fine too +- Only one question per message - if a topic needs more exploration, break it into multiple questions +- Focus on understanding: purpose, constraints, success criteria + +**Exploring approaches:** +- Propose 2-3 different approaches with trade-offs +- Present options conversationally with your recommendation and reasoning +- Lead with your recommended option and explain why + +**Presenting the design:** +- Once you believe you understand what you're building, present the design +- Break it into sections of 200-300 words +- Ask after each section whether it looks right so far +- Cover: architecture, components, data flow, error handling, testing +- Be ready to go back and clarify if something doesn't make sense + +## After the Design + +**Documentation:** +- Write the validated design to `docs/plans/YYYY-MM-DD--design.md` +- Use elements-of-style:writing-clearly-and-concisely skill if available +- Commit the design document to git + +**Implementation (if continuing):** +- Ask: "Ready to set up for implementation?" +- Use superpowers:using-git-worktrees to create isolated workspace +- Use superpowers:writing-plans to create detailed implementation plan + +## Key Principles + +- **One question at a time** - Don't overwhelm with multiple questions +- **Multiple choice preferred** - Easier to answer than open-ended when possible +- **YAGNI ruthlessly** - Remove unnecessary features from all designs +- **Explore alternatives** - Always propose 2-3 approaches before settling +- **Incremental validation** - Present design in sections, validate each +- **Be flexible** - Go back and clarify when something doesn't make sense diff --git a/.agents/skills/code-simplifier/SKILL.md b/.agents/skills/code-simplifier/SKILL.md new file mode 100644 index 0000000..af8eacb --- /dev/null +++ b/.agents/skills/code-simplifier/SKILL.md @@ -0,0 +1,55 @@ +--- +name: code-simplifier +description: Simplifies and refines code for clarity, consistency, and maintainability while preserving all functionality. Focuses on recently modified code unless instructed otherwise. +model: opus +license: MIT +metadata: + author: anthropics + version: "1.0.0" +--- + +You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer. + +You will analyze recently modified code and apply refinements that: + +1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact. + +2. **Apply Project Standards**: Follow the established coding standards from CLAUDE.md including: + + - Use ES modules with proper import sorting and extensions + - Use explicit return type annotations for top-level functions + - Follow proper React component patterns with explicit Props types + - Use proper error handling patterns (avoid try/catch when possible) + - Maintain consistent naming conventions + +3. **Enhance Clarity**: Simplify code structure by: + + - Reducing unnecessary complexity and nesting + - Eliminating redundant code and abstractions + - Improving readability through clear variable and function names + - Consolidating related logic + - Removing unnecessary comments that describe obvious code + - IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions + - Choose clarity over brevity - explicit code is often better than overly compact code + +4. **Maintain Balance**: Avoid over-simplification that could: + + - Reduce code clarity or maintainability + - Create overly clever solutions that are hard to understand + - Combine too many concerns into single functions or components + - Remove helpful abstractions that improve code organization + - Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners) + - Make the code harder to debug or extend + +5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope. + +Your refinement process: + +1. Identify the recently modified code sections +2. Analyze for opportunities to improve elegance and consistency +3. Apply project-specific best practices and coding standards +4. Ensure all functionality remains unchanged +5. Verify the refined code is simpler and more maintainable +6. Document only significant changes that affect understanding + +You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality. diff --git a/.agents/skills/crafting-effective-readmes/SKILL.md b/.agents/skills/crafting-effective-readmes/SKILL.md new file mode 100644 index 0000000..a6c30d9 --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/SKILL.md @@ -0,0 +1,78 @@ +--- +name: crafting-effective-readmes +description: Use when writing or improving README files. Not all READMEs are the same — provides templates and guidance matched to your audience and project type. +--- + +# Crafting Effective READMEs + +## Overview + +READMEs answer questions your audience will have. Different audiences need different information - a contributor to an OSS project needs different context than future-you opening a config folder. + +**Always ask:** Who will read this, and what do they need to know? + +## Process + +### Step 1: Identify the Task + +**Ask:** "What README task are you working on?" + +| Task | When | +|------|------| +| **Creating** | New project, no README yet | +| **Adding** | Need to document something new | +| **Updating** | Capabilities changed, content is stale | +| **Reviewing** | Checking if README is still accurate | + +### Step 2: Task-Specific Questions + +**Creating initial README:** +1. What type of project? (see Project Types below) +2. What problem does this solve in one sentence? +3. What's the quickest path to "it works"? +4. Anything notable to highlight? + +**Adding a section:** +1. What needs documenting? +2. Where should it go in the existing structure? +3. Who needs this info most? + +**Updating existing content:** +1. What changed? +2. Read current README, identify stale sections +3. Propose specific edits + +**Reviewing/refreshing:** +1. Read current README +2. Check against actual project state (package.json, main files, etc.) +3. Flag outdated sections +4. Update "Last reviewed" date if present + +### Step 3: Always Ask + +After drafting, ask: **"Anything else to highlight or include that I might have missed?"** + +## Project Types + +| Type | Audience | Key Sections | Template | +|------|----------|--------------|----------| +| **Open Source** | Contributors, users worldwide | Install, Usage, Contributing, License | `templates/oss.md` | +| **Personal** | Future you, portfolio viewers | What it does, Tech stack, Learnings | `templates/personal.md` | +| **Internal** | Teammates, new hires | Setup, Architecture, Runbooks | `templates/internal.md` | +| **Config** | Future you (confused) | What's here, Why, How to extend, Gotchas | `templates/xdg-config.md` | + +**Ask the user** if unclear. Don't assume OSS defaults for everything. + +## Essential Sections (All Types) + +Every README needs at minimum: + +1. **Name** - Self-explanatory title +2. **Description** - What + why in 1-2 sentences +3. **Usage** - How to use it (examples help) + +## References + +- `section-checklist.md` - Which sections to include by project type +- `style-guide.md` - Common README mistakes and prose guidance +- `using-references.md` - Guide to deeper reference materials diff --git a/.agents/skills/crafting-effective-readmes/references/art-of-readme.md b/.agents/skills/crafting-effective-readmes/references/art-of-readme.md new file mode 100644 index 0000000..4bf5cb9 --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/references/art-of-readme.md @@ -0,0 +1,536 @@ +# Art of README + +> Source: [hackergrrl/art-of-readme](https://github.com/hackergrrl/art-of-readme) + +*This article can also be read in [Chinese](README-zh.md), +[Japanese](README-ja-JP.md), +[Brazilian Portuguese](README-pt-BR.md), [Spanish](README-es-ES.md), +[German](README-de-DE.md), [French](README-fr.md) and [Traditional Chinese](README-zh-TW.md).* + +## Etymology + +Where does the term "README" come from? + +The nomenclature dates back to *at least* the 1970s [and the +PDP-10](http://pdp-10.trailing-edge.com/decuslib10-04/01/43,50322/read.me.html), +though it may even harken back to the days of informative paper notes placed atop +stacks of punchcards, "READ ME!" scrawled on them, describing their use. + +A reader[1](#footnote-1) suggested that the title README may be a playful nudge toward Lewis +Carroll's *Alice's Adventures in Wonderland*, which features a potion and a cake +labelled *"DRINK ME"* and *"EAT ME"*, respectively. + +The pattern of README appearing in all-caps is a consistent facet throughout +history. In addition to the visual strikingness of using all-caps, UNIX systems +would sort capitals before lower case letters, conveniently putting the README +before the rest of the directory's content[2](#footnote-2). + +The intent is clear: *"This is important information for the user to read before +proceeding."* Let's explore together what constitutes "important information" in +this modern age. + + +## For creators, for consumers + +This is an article about READMEs. About what they do, why they are an absolute +necessity, and how to craft them well. + +This is written for module creators, for as a builder of modules, your job is to +create something that will last. This is an inherent motivation, even if the +author has no intent of sharing their work. Once 6 months pass, a module without +documentation begins to look new and unfamiliar. + +This is also written for module consumers, for every module author is also a +module consumer. Node has a very healthy degree of interdependency: no one lives +at the bottom of the dependency tree. + +Despite being focused on Node, the author contends that its lessons apply +equally well to other programming ecosystems, as well. + + +## Many modules: some good, some bad + +The Node ecosystem is powered by its modules. [npm](https://npmjs.org) is the +magic that makes it all *go*. In the course of a week, Node developers evaluate +dozens of modules for inclusion in their projects. This is a great deal of power +being churned out on a daily basis, ripe for the plucking, just as fast as one +can write `npm install`. + +Like any ecosystem that is extremely accessible, the quality bar varies. npm +does its best to nicely pack away all of these modules and ship them far and +wide. However, the tools found are widely varied: some are shining and new, +others broken and rusty, and still others are somewhere in between. There are +even some that we don't know what they do! + +For modules, this can take the form of inaccurate or unhelpful names (any +guesses what the `fudge` module does?), no documentation, no tests, no source +code comments, or incomprehensible function names. + +Many don't have an active maintainer. If a module has no human available to +answer questions and explain what a module does, combined with no remnants of +documentation left behind, a module becomes a bizarre alien artifact, unusable +and incomprehensible by the archaeologist-hackers of tomorrow. + +For those modules that do have documentation, where do they fall on the quality +spectrum? Maybe it's just a one-liner description: `"sorts numbers by their hex +value"`. Maybe it's a snippet of example code. These are both improvements upon +nothing, but they tend to result in the worst-case scenario for a modern day +module spelunker: digging into the source code to try and understand how it +actually works. Writing excellent documentation is all about keeping the users +*out* of the source code by providing instructions sufficient to enjoy the +wonderful abstractions that your module brings. + +Node has a "wide" ecosystem: it's largely made up of a very long list of +independent do-one-thing-well modules flying no flags but their own. There are +[exceptions](https://github.com/lodash/lodash), but despite these minor fiefdoms, +it is the single-purpose commoners who, given their larger numbers, truly rule the +Node kingdom. + +This situation has a natural consequence: it can be hard to find *quality* modules +that do exactly what you want. + +**This is okay**. Truly. A low bar to entry and a discoverability problem is +infinitely better than a culture problem, where only the privileged few may +participate. + +Plus, discoverability -- as it turns out -- is easier to address. + + +## All roads lead to README.md + +The Node community has responded to the challenge of discoverability in +different ways. + +Some experienced Node developers band together to create [curated +lists](https://github.com/sindresorhus/awesome-nodejs) of quality modules. +Developers leverage their many years examining hundreds of different modules to +share with newcomers the *crème de la crème*: the best modules in each category. +This might also take the form of RSS feeds and mailing lists of new modules deemed +to be useful by trusted community members. + +How about the social graph? This idea spurred the creation of +[node-modules.com](http://node-modules.com/), a npm search replacement that +leverages your GitHub social graph to find modules your friends like or have +made. + +Of course there is also npm's built-in [search](https://npmjs.org) +functionality: a safe default, and the usual port of entry for new developers. + +No matter your approach, regardless whether a module spelunker enters the module +underground at [npmjs.org](https://npmjs.org), +[github.com](https://github.com), or somewhere else, this would-be user will +eventually end up staring your README square in the face. Since your users +will inevitably find themselves here, what can be done to make their first +impressions maximally effective? + + +## Professional module spelunking + +### The README: Your one-stop shop + +A README is a module consumer's first -- and maybe only -- look into your +creation. The consumer wants a module to fulfill their need, so you must explain +exactly what need your module fills, and how effectively it does so. + +Your job is to + +1. tell them what it is (with context) +2. show them what it looks like in action +3. show them how they use it +4. tell them any other relevant details + +This is *your* job. It's up to the module creator to prove that their work is a +shining gem in the sea of slipshod modules. Since so many developers' eyes will +find their way to your README before anything else, quality here is your +public-facing measure of your work. + + +### Brevity + +The lack of a README is a powerful red flag, but even a lengthy README is not +indicative of there being high quality. The ideal README is as short as it can +be without being any shorter. Detailed documentation is good -- make separate +pages for it! -- but keep your README succinct. + + +### Learn from the past + +It is said that those who do not study their history are doomed to make its +mistakes again. Developers have been writing documentation for quite some number +of years. It would be wasteful to not look back a little bit and see what people +did right before Node. + +Perl, for all of the flak it receives, is in some ways the spiritual grandparent +of Node. Both are high-level scripting languages, adopt many UNIX idioms, fuel +much of the internet, and both feature a wide module ecosystem. + +It so turns out that the [monks](http://perlmonks.org) of the Perl community +indeed have a great deal of experience in writing [quality +READMEs](http://search.cpan.org/~kane/Archive-Tar/lib/Archive/Tar.pm). CPAN is a +wonderful resource that is worth reading through to learn more about a community +that wrote consistently high-calibre documentation. + + +### No README? No abstraction + +No README means developers will need to delve into your code in order to +understand it. + +The Perl monks have wisdom to share on the matter: + +> Your documentation is complete when someone can use your module without ever +> having to look at its code. This is very important. This makes it possible for +> you to separate your module's documented interface from its internal +> implementation (guts). This is good because it means that you are free to +> change the module's internals as long as the interface remains the same. +> +> Remember: the documentation, not the code, defines what a module does. +-- [Ken Williams](http://mathforum.org/ken/perl_modules.html#document) + + +### Key elements + +Once a README is located, the brave module spelunker must scan it to discern if +it matches the developer's needs. This becomes essentially a series of pattern +matching problems for their brain to solve, where each step takes them deeper +into the module and its details. + +Let's say, for example, my search for a 2D collision detection module leads me +to [`collide-2d-aabb-aabb`](https://github.com/hackergrrl/collide-2d-aabb-aabb). I +begin to examine it from top to bottom: + +1. *Name* -- self-explanatory names are best. `collide-2d-aabb-aabb` sounds + promising, though it assumes I know what an "aabb" is. If the name sounds too + vague or unrelated, it may be a signal to move on. + +2. *One-liner* -- having a one-liner that describes the module is useful for + getting an idea of what the module does in slightly greater detail. + `collide-2d-aabb-aabb` says it + + > Determines whether a moving axis-aligned bounding box (AABB) collides with + > other AABBs. + + Awesome: it defines what an AABB is, and what the module does. Now to gauge how + well it'd fit into my code: + +3. *Usage* -- rather than starting to delve into the API docs, it'd be great to + see what the module looks like in action. I can quickly determine whether the + example JS fits the desired style and problem. People have lots of opinions + on things like promises/callbacks and ES6. If it does fit the bill, then I + can proceed to greater detail. + +4. *API* -- the name, description, and usage of this module all sound appealing + to me. I'm very likely to use this module at this point. I just need to scan + the API to make sure it does exactly what I need and that it will integrate + easily into my codebase. The API section ought to detail the module's objects + and functions, their signatures, return types, callbacks, and events in + detail. Types should be included where they aren't obvious. Caveats should be + made clear. + +5. *Installation* -- if I've read this far down, then I'm sold on trying out the + module. If there are nonstandard installation notes, here's where they'd go, + but even if it's just a regular `npm install`, I'd like to see that mentioned, + too. New users start using Node all the time, so having a link to npmjs.org + and an install command provides them the resources to figure out how Node + modules work. + +6. *License* -- most modules put this at the very bottom, but this might + actually be better to have higher up; you're likely to exclude a module VERY + quickly if it has a license incompatible with your work. I generally stick to + the MIT/BSD/X11/ISC flavours. If you have a non-permissive license, stick it + at the very top of the module to prevent any confusion. + + +## Cognitive funneling + +The ordering of the above was not chosen at random. + +Module consumers use many modules, and need to look at many modules. + +Once you've looked at hundreds of modules, you begin to notice that the mind +benefits from predictable patterns. + +You also start to build out your own personal heuristic for what information you +want, and what red flags disqualify modules quickly. + +Thus, it follows that in a README it is desirable to have: + +1. a predictable format +2. certain key elements present + +You don't need to use *this* format, but try to be consistent to save your users +precious cognitive cycles. + +The ordering presented here is lovingly referred to as "cognitive funneling," +and can be imagined as a funnel held upright, where the widest end contains the +broadest more pertinent details, and moving deeper down into the funnel presents +more specific details that are pertinent for only a reader who is interested +enough in your work to have reached that deeply in the document. Finally, the +bottom can be reserved for details only for those intrigued by the deeper +context of the work (background, credits, biblio, etc.). + +Once again, the Perl monks have wisdom to share on the subject: + +> The level of detail in Perl module documentation generally goes from +> less detailed to more detailed. Your SYNOPSIS section should +> contain a minimal example of use (perhaps as little as one line of +> code; skip the unusual use cases or anything not needed by most +> users); the DESCRIPTION should describe your module in broad terms, +> generally in just a few paragraphs; more detail of the module's +> routines or methods, lengthy code examples, or other in-depth +> material should be given in subsequent sections. +> +> Ideally, someone who's slightly familiar with your module should be +> able to refresh their memory without hitting "page down". As your +> reader continues through the document, they should receive a +> progressively greater amount of knowledge. +> -- from `perlmodstyle` + + +## Care about people's time + +Awesome; the ordering of these key elements should be decided by how quickly +they let someone 'short circuit' and bail on your module. + +This sounds bleak, doesn't it? But think about it: your job, when you're doing +it with optimal altruism in mind, isn't to "sell" people on your work. It's to +let them evaluate what your creation does as objectively as possible, and decide +whether it meets their needs or not -- not to, say, maximize your downloads or +userbase. + +This mindset doesn't appeal to everyone; it requires checking your ego at the +door and letting the work speak for itself as much as possible. Your only job is +to describe its promise as succinctly as you can, so module spelunkers can +either use your work when it's a fit, or move on to something else that does. + + +## Call to arms! + +Go forth, brave module spelunker, and make your work discoverable and usable +through excellent documentation! + + +## Bonus: other good practices + +Outside of the key points of the article, there are other practices you can +follow (or not follow) to raise your README's quality bar even further and +maximize its usefulness to others: + +1. Consider including a **Background** section if your module depends on + important but not widely known abstractions or other ecosystems. The function + of [`bisecting-between`](https://github.com/hackergrrl/bisecting-between) is not + immediately obvious from its name, so it has a detailed *Background* section + to define and link to the big concepts and abstractions one needs to + understand to use and grok it. This is also a great place to explain the + module's motivation if similar modules already exist on npm. + +2. Aggressively linkify! If you talk about other modules, ideas, or people, make + that reference text a link so that visitors can more easily grok your module + and the ideas it builds on. Few modules exist in a vacuum: all work comes + from other work, so it pays to help users follow your module's history and + inspiration. + +3. Include information on types of arguments and return parameters if it's not + obvious. Prefer convention wherever possible (`cb` probably means callback + function, `num` probably means a `Number`, etc.). + +4. Include the example code in **Usage** as a file in your repo -- maybe as + `example.js`. It's great to have README code that users can actually run if + they clone the repository. + +5. Be judicious in your use of badges. They're easy to + [abuse](https://github.com/angular/angular). They can also be a breeding + ground for bikeshedding and endless debate. They add visual noise to your + README and generally only function if the user is reading your Markdown in a + browser online, since the images are often hosted elsewhere on the + internet. For each badge, consider: "what real value is this badge providing + to the typical viewer of this README?" Do you have a CI badge to show build/test + status? This signal would better reach important parties by emailing + maintainers or automatically creating an issue. Always consider the + audience of the data in your README and ask yourself if there's a flow for + that data that can better reach its intended audience. + +6. API formatting is highly bikesheddable. Use whatever format you think is + clearest, but make sure your format expresses important subtleties: + + a. which parameters are optional, and their defaults + + b. type information, where it is not obvious from convention + + c. for `opts` object parameters, all keys and values that are accepted + + d. don't shy away from providing a tiny example of an API function's use if + it is not obvious or fully covered in the **Usage** section. + However, this can also be a strong signal that the function is too complex + and needs to be refactored, broken into smaller functions, or removed + altogether + + e. aggressively linkify specialized terminology! In markdown you can keep + [footnotes](https://daringfireball.net/projects/markdown/syntax#link) at + the bottom of your document, so referring to them several times throughout + becomes cheap. Some of my personal preferences on API formatting can be + found + [here](https://github.com/hackergrrl/common-readme/blob/master/api_formatting.md) + +7. If your module is a small collection of stateless functions, having a + **Usage** section as a [Node REPL + session](https://github.com/hackergrrl/bisecting-between#example) of function + calls and results might communicate usage more clearly than a source code + file to run. + +8. If your module provides a CLI (command line interface) instead of (or in + addition to) a programmatic API, show usage examples as command invocations + and their output. If you create or modify a file, `cat` it to demonstrate + the change before and after. + +9. Don't forget to use `package.json` + [keywords](https://docs.npmjs.com/files/package.json#keywords) to direct + module spelunkers to your doorstep. + +10. The more you change your API, the more work you need to exert updating + documentation -- the implication here is that you should keep your APIs + small and concretely defined early on. Requirements change over time, but + instead of front-loading assumptions into the APIs of your modules, load + them up one level of abstraction: the module set itself. If the requirements + *do* change and 'do-one-concrete-thing' no longer makes sense, then simply + write a new module that does the thing you need. The 'do-one-concrete-thing' + module remains a valid and valuable model for the npm ecosystem, and your + course correction cost you nothing but a simple substitution of one module for + another. + +11. Finally, please remember that your version control repository and its + embedded README will outlive your [repository host](https://github.com) and + any of the things you hyperlink to -- especially images -- so *inline* anything + that is essential to future users grokking your work. + + +## Bonus: *common-readme* + +Not coincidentally, this is also the format used by +[**common-readme**](https://github.com/hackergrrl/common-readme), a set of README +guidelines and handy command-line generator. If you like what's written here, +you may save some time writing READMEs with `common-readme`. You'll find +real module examples with this format, too. + +You may also enjoy +[standard-readme](https://github.com/richardlitt/standard-readme), which is a +more structured, lintable take on a common README format. + + +## Bonus: Exemplars + +Theory is well and good, but what do excellent READMEs look like? Here are some +that I think embody the principles of this article well: + +- https://github.com/hackergrrl/ice-box +- https://github.com/substack/quote-stream +- https://github.com/feross/bittorrent-dht +- https://github.com/mikolalysenko/box-intersect +- https://github.com/freeman-lab/pixel-grid +- https://github.com/mafintosh/torrent-stream +- https://github.com/pull-stream/pull-stream +- https://github.com/substack/tape +- https://github.com/yoshuawuyts/vmd + + +## Bonus: The README Checklist + +A helpful checklist to gauge how your README is coming along: + +- [ ] One-liner explaining the purpose of the module +- [ ] Necessary background context & links +- [ ] Potentially unfamiliar terms link to informative sources +- [ ] Clear, *runnable* example of usage +- [ ] Installation instructions +- [ ] Extensive API documentation +- [ ] Performs [cognitive funneling](https://github.com/hackergrrl/art-of-readme#cognitive-funneling) +- [ ] Caveats and limitations mentioned up-front +- [ ] Doesn't rely on images to relay critical information +- [ ] License + + +## The author + +Hi, I'm [Kira](http://kira.solar). + +This little project began back in May in Berlin at squatconf, where I was +digging into how Perl monks write their documentation and also lamenting the +state of READMEs in the Node ecosystem. It spurred me to create +[common-readme](https://github.com/hackergrrl/common-readme). The "README Tips" +section overflowed with tips though, which I decided could be usefully collected +into an article about writing READMEs. Thus, Art of README was born! + + +## Further Reading + +- [README-Driven Development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) +- [Documentation First](http://joeyh.name/blog/entry/documentation_first/) + + +## Footnotes + +1. Thanks, + [Sixes666](https://www.reddit.com/r/node/comments/55eto9/nodejs_the_art_of_readme/d8akpz6)! + +2. See [The Jargon File](http://catb.org/~esr/jargon/html/R/README-file.html). + However, most systems today will not sort capitals before all lowercase + characters, reducing this convention's usefulness to just the visual + strikingness of all-caps. + + +## Credits + +A heartfelt thank you to [@mafintosh](https://github.com/mafintosh) and +[@feross](https://github.com/feross) for the encouragement I needed to get this +idea off the ground and start writing! + +Thank you to the following awesome readers for noticing errors and sending me +PRs :heart: : + +- [@ungoldman](https://github.com/ungoldman) +- [@boidolr](https://github.com/boidolr) +- [@imjoehaines](https://github.com/imjoehaines) +- [@radarhere](https://github.com/radarhere) +- [@joshmanders](https://github.com/joshmanders) +- [@ddbeck](https://github.com/ddbeck) +- [@RichardLitt](https://github.com/RichardLitt) +- [@StevenMaude](https://github.com/StevenMaude) +- [@KrishMunot](https://github.com/KrishMunot) +- [@chesterhow](https://github.com/chesterhow) +- [@sjsyrek](https://github.com/sjsyrek) +- [@thenickcox](https://github.com/thenickcox) + +Thank you to [@qihaiyan](https://github.com/qihaiyan) for translating Art of +README to Chinese! The following users also made contributions: + +- [@BrettDong](https://github.com/brettdong) for revising punctuation in Chinese version. +- [@Alex-fun](https://github.com/Alex-fun) +- [@HmyBmny](https://github.com/HmyBmny) +- [@vra](https://github.com/vra) + +Thank you to [@lennonjesus](https://github.com/lennonjesus) for translating Art +of README to Brazilian Portuguese! The following users also made contributions: + +- [@rectius](https://github.com/rectius) + +Thank you to [@jabiinfante](https://github.com/jabiinfante) for translating Art +of README to Spanish! + +Thank you to [@Ryuno-Ki](https://github.com/Ryuno-Ki) for translating Art of +README to German! The following users also made contributions: + +- [@randomC0der](https://github.com/randomC0der) + +Thank you to [@Manfred Madelaine](https://github.com/Manfred-Madelaine-pro) and +[@Ruben Madelaine](https://github.com/Ruben-Madelaine) +for translating Art of README to French! + +## Other Resources +Some readers have suggested other useful resources for README composition: +- [Software Release Practice](https://tldp.org/HOWTO/Software-Release-Practice-HOWTO/distpractice.html#readme) +- [GNU Releases](https://www.gnu.org/prep/standards/html_node/Releases.html#index-README-file) + + +## License + +[Creative Commons Attribution License](http://creativecommons.org/licenses/by/2.0/) diff --git a/.agents/skills/crafting-effective-readmes/references/make-a-readme.md b/.agents/skills/crafting-effective-readmes/references/make-a-readme.md new file mode 100644 index 0000000..6b0d7cd --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/references/make-a-readme.md @@ -0,0 +1,119 @@ +# Make a README + +> Source: [makeareadme.com](https://www.makeareadme.com) by Danny Guo +> +> "Because no one can read your mind (yet)" + +## README 101 + +### What is it? + +A README is a text file that introduces and explains a project. It contains information that is commonly required to understand what the project is about. + +### Why should I make it? + +It's an easy way to answer questions that your audience will likely have regarding how to install and use your project and also how to collaborate with you. + +### Who should make it? + +Anyone who is working on a programming project, especially if you want others to use it or contribute. + +### When should I make it? + +Definitely before you show a project to other people or make it public. You might want to get into the habit of making it the first file you create in a new project. + +### Where should I put it? + +In the top level directory of the project. This is where someone who is new to your project will start out. Code hosting services such as GitHub, Bitbucket, and GitLab will also look for your README and display it along with the list of files and directories in your project. + +### How should I make it? + +While READMEs can be written in any text file format, the most common one that is used nowadays is Markdown. It allows you to add some lightweight formatting. You can learn more about it at the [CommonMark website](https://commonmark.org/). + +## Suggestions for a Good README + +Every project is different, so consider which of these sections apply to yours. Also keep in mind that while a README can be too long and detailed, **too long is better than too short**. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +### Name + +Choose a self-explaining name for your project. + +### Description + +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of **Features** or a **Background** subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +### Badges + +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use [Shields.io](http://shields.io/) to add some to your README. Many services also have instructions for adding a badge. + +### Visuals + +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like [ttygif](https://github.com/icholy/ttygif) can help, but check out [Asciinema](https://asciinema.org/) for a more sophisticated method. + +### Installation + +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a **Requirements** subsection. + +### Usage + +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +### Support + +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +### Roadmap + +If you have ideas for releases in the future, it is a good idea to list them in the README. + +### Contributing + +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +### Authors and Acknowledgment + +Show your appreciation to those who have contributed to the project. + +### License + +For open source projects, say how it is licensed. + +### Project Status + +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. + +## FAQ + +### Is there a standard README format? + +Not all of the suggestions here will make sense for every project, so it's really up to the developers what information should be included in the README. + +### What should the README file be named? + +`README.md` (or a different file extension if you choose to use a non-Markdown file format). It is traditionally uppercase so that it is more prominent, but it's not a big deal if you think it looks better lowercase. + +## What's Next? + +### More Documentation + +A README is a crucial but basic way of documenting your project. While every project should at least have a README, more involved ones can also benefit from a wiki or a dedicated documentation website. Tools include: + +- [Docusaurus](https://docusaurus.io/) +- [GitBook](https://www.gitbook.com/) +- [MkDocs](https://www.mkdocs.org/) +- [Read the Docs](https://readthedocs.org/) +- [Docsify](https://docsify.js.org/) + +### Changelog + +A [changelog](https://en.wikipedia.org/wiki/Changelog) is another file that is very useful for programming projects. See [Keep a Changelog](http://keepachangelog.com/). + +### Contributing Guidelines + +Just having a "Contributing" section in your README is a good start. Another approach is to split off your guidelines into their own file (`CONTRIBUTING.md`). If you use GitHub and have this file, then anyone who creates an issue or opens a pull request will get a link to it. + +You can also create an issue template and a pull request template. These files give your users and collaborators templates to fill in with the information that you'll need to properly respond. diff --git a/.agents/skills/crafting-effective-readmes/references/standard-readme-example-maximal.md b/.agents/skills/crafting-effective-readmes/references/standard-readme-example-maximal.md new file mode 100644 index 0000000..4ccdf57 --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/references/standard-readme-example-maximal.md @@ -0,0 +1,68 @@ +# Title + +![banner](assets/text_wordmark_dark.png) + +![GitHub Created At](https://img.shields.io/github/created-at/RichardLitt/standard-readme?color=bright-green&style=flat-square) +![GitHub contributors](https://img.shields.io/github/contributors/RichardLitt/standard-readme?color=bright-green&style=flat-square) +[![license](https://img.shields.io/github/license/RichardLitt/standard-readme.svg?color=bright-green&style=flat-square)](LICENSE) +[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) + +This is an example file with maximal choices selected. + +This is a long description. + +## Table of Contents + +- [Security](#security) +- [Background](#background) +- [Install](#install) +- [Usage](#usage) +- [API](#api) +- [Contributing](#contributing) +- [License](#license) + +## Security + +### Any optional sections + +## Background + +### Any optional sections + +## Install + +This module depends upon a knowledge of [Markdown](). + +``` +``` + +### Any optional sections + +## Usage + +``` +``` + +Note: The `license` badge image link at the top of this file should be updated with the correct `:user` and `:repo`. + +### Any optional sections + +## API + +### Any optional sections + +## More optional sections + +## Contributing + +See [the contributing file](CONTRIBUTING.md)! + +PRs accepted. + +Small note: If editing the Readme, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. + +### Any optional sections + +## License + +[MIT © Richard McRichface.](../LICENSE) diff --git a/.agents/skills/crafting-effective-readmes/references/standard-readme-example-minimal.md b/.agents/skills/crafting-effective-readmes/references/standard-readme-example-minimal.md new file mode 100644 index 0000000..13d94b7 --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/references/standard-readme-example-minimal.md @@ -0,0 +1,21 @@ +# Title + +This is an example file with default selections. + +## Install + +``` +``` + +## Usage + +``` +``` + +## Contributing + +PRs accepted. + +## License + +MIT © Richard McRichface diff --git a/.agents/skills/crafting-effective-readmes/references/standard-readme-spec.md b/.agents/skills/crafting-effective-readmes/references/standard-readme-spec.md new file mode 100644 index 0000000..91a4961 --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/references/standard-readme-spec.md @@ -0,0 +1,242 @@ +# Standard README Specification + +> Source: [Standard Readme](https://github.com/RichardLitt/standard-readme) by Richard Litt + +A compliant README must satisfy all the requirements listed below. + +> Note: Standard Readme is designed for open source libraries. Although it's [historically](README.md#background) made for Node and npm projects, it also applies to libraries in other languages and package managers. + +**Requirements:** + - Be called README (with capitalization) and have a specific extension depending on its format (`.md` for Markdown, `.org` for Org Mode Markup syntax, `.html` for HTML, ...) + - If the project supports i18n, the file must be named accordingly: `README.de.md`, where `de` is the BCP 47 Language tag. For naming, prioritize non-regional subtags for languages. If there is only one README and the language is not English, then a different language in the text is permissible without needing to specify the BCP tag: e.g., `README.md` can be in German if there is no `README.md` in another language. Where there are multiple languages, `README.md` is reserved for English. + - Be a valid file in the selected format (Markdown, Org Mode, HTML, ...). + - Sections must appear in order given below. Optional sections may be omitted. + - Sections must have the titles listed below, unless otherwise specified. If the README is in another language, the titles must be translated into that language. + - Must not contain broken links. + - If there are code examples, they should be linted in the same way as the code is linted in the rest of the project. + +## Table of Contents + +_Note: This is only a navigation guide for the specification, and does not define or mandate terms for any specification-compliant documents._ + +- [Sections](#sections) + - [Title](#title) + - [Banner](#banner) + - [Badges](#badges) + - [Short Description](#short-description) + - [Long Description](#long-description) + - [Table of Contents](#table-of-contents-1) + - [Security](#security) + - [Background](#background) + - [Install](#install) + - [Usage](#usage) + - [Extra Sections](#extra-sections) + - [API](#api) + - [Maintainers](#maintainers) + - [Thanks](#thanks) + - [Contributing](#contributing) + - [License](#license) +- [Definitions](#definitions) + +## Sections + +### Title +**Status:** Required. + +**Requirements:** +- Title must match repository, folder and package manager names - or it may have another, relevant title with the repository, folder, and package manager title next to it in italics and in parentheses. For instance: + + ```markdown + # Standard Readme Style _(standard-readme)_ + ``` + + If any of the folder, repository, or package manager names do not match, there must be a note in the [Long Description](#long-description) explaining why. + +**Suggestions:** +- Should be self-evident. + +### Banner +**Status:** Optional. + +**Requirements:** +- Must not have its own title. +- Must link to local image in current repository. +- Must appear directly after the title. + +### Badges +**Status:** Optional. + +**Requirements:** +- Must not have its own title. +- Must be newline delimited. + +**Suggestions:** +- Use http://shields.io or a similar service to create and host the images. +- Add the [Standard Readme badge](https://github.com/RichardLitt/standard-readme#badge). + +### Short Description +**Status:** Required. + +**Requirements:** +- Must not have its own title. +- Must be less than 120 characters. +- Must not start with `> ` +- Must be on its own line. +- Must match the description in the packager manager's `description` field. +- Must match GitHub's description (if on GitHub). + +**Suggestions:** +- Use [gh-description](https://github.com/RichardLitt/gh-description) to set and get GitHub description. +- Use `npm show . description` to show the description from a local [npm](https://npmjs.com) package. + +### Long Description +**Status:** Optional. + +**Requirements:** +- Must not have its own title. +- If any of the folder, repository, or package manager names do not match, there must be a note here as to why. See [Title section](#title). + +**Suggestions:** +- If too long, consider moving to the [Background](#background) section. +- Cover the main reasons for building the repository. +- "This should describe your module in broad terms, +generally in just a few paragraphs; more detail of the module's +routines or methods, lengthy code examples, or other in-depth +material should be given in subsequent sections. + + Ideally, someone who's slightly familiar with your module should be +able to refresh their memory without hitting "page down". As your +reader continues through the document, they should receive a +progressively greater amount of knowledge." + + ~ [Kirrily "Skud" Robert, perlmodstyle](http://perldoc.perl.org/perlmodstyle.html) + +### Table of Contents +**Status:** Required; optional for READMEs shorter than 100 lines. + +**Requirements:** +- Must link to all sections in the file. +- Must start with the next section; do not include the title or Table of Contents headings. +- Must be at least one-depth: must capture all level two headings (e.g.: Markdown's `##` or Org Mode's `**` or HTML's `

` and so on). + +**Suggestions:** +- May capture third and fourth depth headings. If it is a long ToC, these are optional. + +### Security +**Status**: Optional. + +**Requirements:** +- May go here if it is important to highlight security concerns. Otherwise, it should be in [Extra Sections](#extra-sections). + +### Background +**Status:** Optional. + +**Requirements:** +- Cover motivation. +- Cover abstract dependencies. +- Cover intellectual provenance: A `See Also` section is also fitting. + +### Install +**Status:** Required by default, optional for [documentation repositories](#definitions). + +**Requirements:** +- Code block illustrating how to install. + +**Subsections:** +- `Dependencies`. Required if there are unusual dependencies or dependencies that must be manually installed. + +**Suggestions:** +- Link to prerequisite sites for programming language: [npmjs](https://npmjs.com), [godocs](https://godoc.org), etc. +- Include any system-specific information needed for installation. +- An `Updating` section would be useful for most packages, if there are multiple versions which the user may interface with. + +### Usage +**Status:** Required by default, optional for [documentation repositories](#definitions). + +**Requirements:** +- Code block illustrating common usage. +- If CLI compatible, code block indicating common usage. +- If importable, code block indicating both import functionality and usage. + +**Subsections:** +- `CLI`. Required if CLI functionality exists. + +**Suggestions:** +- Cover basic choices that may affect usage: for instance, if JavaScript, cover promises/callbacks, ES6 here. +- If relevant, point to a runnable file for the usage code. + +### Extra Sections +**Status**: Optional. + +**Requirements:** +- None. + +**Suggestions:** +- This should not be called `Extra Sections`. This is a space for 0 or more sections to be included, each of which must have their own titles. +- This should contain any other sections that are relevant, placed after [Usage](#usage) and before [API](#api). +- Specifically, the [Security](#security) section should be here if it wasn't important enough to be placed above. + +### API +**Status:** Optional. + +**Requirements:** +- Describe exported functions and objects. + +**Suggestions:** +- Describe signatures, return types, callbacks, and events. +- Cover types covered where not obvious. +- Describe caveats. +- If using an external API generator (like go-doc, js-doc, or so on), point to an external `API.md` file. This can be the only item in the section, if present. + +### Maintainer(s) +**Status**: Optional. + +**Requirements:** +- Must be called `Maintainer` or `Maintainers`. +- List maintainer(s) for a repository, along with one way of contacting them (e.g. GitHub link or email). + +**Suggestions:** +- This should be a small list of people in charge of the repo. This should not be everyone with access rights, such as an entire organization, but the people who should be pinged and who are in charge of the direction and maintenance of the repository. +- Listing past maintainers is good for attribution, and kind. + +### Thanks +**Status**: Optional. + +**Requirements:** +- Must be called `Thanks`, `Credits` or `Acknowledgements`. + +**Suggestions:** +- State anyone or anything that significantly helped with the development of your project. +- State public contact hyper-links if applicable. + +### Contributing +**Status**: Required. + +**Requirements:** +- State where users can ask questions. +- State whether PRs are accepted. +- List any requirements for contributing; for instance, having a sign-off on commits. + +**Suggestions:** +- Link to a CONTRIBUTING file -- if there is one. +- Be as friendly as possible. +- Link to the GitHub issues. +- Link to a Code of Conduct. A CoC is often in the Contributing section or document, or set elsewhere for an entire organization, so it may not be necessary to include the entire file in each repository. However, it is highly recommended to always link to the code, wherever it lives. +- A subsection for listing contributors is also welcome here. + +### License +**Status:** Required. + +**Requirements:** +- State license full name or identifier, as listed on the [SPDX](https://spdx.org/licenses/) license list. For unlicensed repositories, add `UNLICENSED`. For more details, add `SEE LICENSE IN ` and link to the license file. (These requirements were adapted from [npm](https://docs.npmjs.com/files/package.json#license)). +- State license owner. +- Must be last section. + +**Suggestions:** +- Link to longer License file in local repository. + +## Definitions + +_These definitions are provided to clarify any terms used above._ + +- **Documentation repositories**: Repositories without any functional code. For instance, [RichardLitt/knowledge](https://github.com/RichardLitt/knowledge). diff --git a/.agents/skills/crafting-effective-readmes/section-checklist.md b/.agents/skills/crafting-effective-readmes/section-checklist.md new file mode 100644 index 0000000..a6d0832 --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/section-checklist.md @@ -0,0 +1,17 @@ +# Section Checklist by Project Type + +Quick reference for which sections to include based on project type. + +| Section | OSS | Personal | Internal | Config | +|---------|-----|----------|----------|--------| +| Name/Description | Yes | Yes | Yes | Yes | +| Badges | Yes | Optional | No | No | +| Installation | Yes | Yes | Yes | No | +| Usage/Examples | Yes | Yes | Yes | Brief | +| What's Here | No | No | No | Yes | +| How to Extend | No | No | Optional | Yes | +| Contributing | Yes | Optional | Yes | No | +| License | Yes | Optional | No | No | +| Architecture | Optional | No | Yes | No | +| Gotchas/Notes | Optional | Optional | Yes | Yes | +| Last Reviewed | No | No | Optional | Yes | diff --git a/.agents/skills/crafting-effective-readmes/style-guide.md b/.agents/skills/crafting-effective-readmes/style-guide.md new file mode 100644 index 0000000..7df7fd7 --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/style-guide.md @@ -0,0 +1,13 @@ +# README Style Guide + +## Common Mistakes + +- **No install steps** - Never assume setup is obvious +- **No examples** - Show, don't just tell +- **Wall of text** - Use headers, tables, lists +- **Stale content** - Add "last reviewed" date +- **Generic tone** - Write for YOUR audience + +## Prose Quality + +For general writing advice — clear prose, Strunk's rules, and AI patterns to avoid — use the `writing-clearly-and-concisely` skill. diff --git a/.agents/skills/crafting-effective-readmes/templates/internal.md b/.agents/skills/crafting-effective-readmes/templates/internal.md new file mode 100644 index 0000000..449d57b --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/templates/internal.md @@ -0,0 +1,106 @@ +# Internal/Work Project README Template + +Use this template for team codebases, services, and internal tools. +Focus on onboarding new team members and operational knowledge. + +--- + +# [Service/Project Name] + +[One-line description of what this service does] + +**Team**: [Team name or slack channel] +**On-call**: [Rotation or contact info] + +## Overview + +[2-3 sentences on what this does, why it exists, and where it fits in the system architecture.] + +### Dependencies + +- **Upstream**: [Services this depends on] +- **Downstream**: [Services that depend on this] + +## Local Development Setup + +### Prerequisites + +- [Required tool 1 with version] +- [Required tool 2] +- Access to [internal system/VPN/etc] + +### Environment Variables + +| Variable | Description | Where to get it | +|----------|-------------|-----------------| +| `DATABASE_URL` | [Description] | [1Password/Vault/etc] | +| `API_KEY` | [Description] | [Where to find] | + +### Running Locally + +```bash +[Step-by-step commands to get running] +``` + +### Running Tests + +```bash +[Test commands] +``` + +## Architecture + +[Brief description of system design. Link to architecture diagrams if they exist.] + +``` +[Simple ASCII diagram if helpful] +``` + +### Key Files + +| Path | Purpose | +|------|---------| +| `src/[important-file]` | [What it does] | +| `config/` | [Configuration files] | + +## Deployment + +[How to deploy, or link to deployment docs] + +### Environments + +| Environment | URL | Notes | +|-------------|-----|-------| +| Development | [URL] | [Notes] | +| Staging | [URL] | [Notes] | +| Production | [URL] | [Notes] | + +## Runbooks + +### [Common Task 1] + +```bash +[Commands or steps] +``` + +### [Common Task 2] + +[Steps] + +## Troubleshooting + +### [Common Problem 1] + +**Symptom**: [What you see] +**Cause**: [Why it happens] +**Fix**: [How to resolve] + +## Contributing + +[Link to team contribution guidelines or PR process] + +## Related Docs + +- [Link to design doc] +- [Link to API docs] +- [Link to monitoring dashboard] diff --git a/.agents/skills/crafting-effective-readmes/templates/oss.md b/.agents/skills/crafting-effective-readmes/templates/oss.md new file mode 100644 index 0000000..82d850c --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/templates/oss.md @@ -0,0 +1,77 @@ +# Open Source Project README Template + +Use this template for projects intended for public use and contribution. + +--- + +# [Project Name] + +[One-line description of what this project does] + +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Build Status](https://img.shields.io/github/actions/workflow/status/[user]/[repo]/ci.yml)](https://github.com/[user]/[repo]/actions) +[![npm version](https://img.shields.io/npm/v/[package-name])](https://www.npmjs.com/package/[package-name]) + +## About + +[2-3 sentences explaining what problem this solves and why someone would use it. Include what makes it different from alternatives if relevant.] + +## Features + +- [Key feature 1] +- [Key feature 2] +- [Key feature 3] + +## Installation + +```bash +[package manager install command] +``` + +### Requirements + +- [Runtime requirement, e.g., Node.js >= 18] +- [Other dependencies if any] + +## Usage + +```[language] +[Minimal working example showing the most common use case] +``` + +### More Examples + +[Link to examples directory or additional code samples] + +## Documentation + +[Link to full docs if they exist separately, or expand this section] + +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +### Development Setup + +```bash +[Commands to clone and set up for development] +``` + +### Running Tests + +```bash +[Test command] +``` + +## Roadmap + +- [ ] [Planned feature 1] +- [ ] [Planned feature 2] + +## Acknowledgments + +- [Credit to inspirations, contributors, or dependencies worth highlighting] + +## License + +[Project name] is licensed under the [License name] license. See the [`LICENSE`](LICENSE) file for more information. diff --git a/.agents/skills/crafting-effective-readmes/templates/personal.md b/.agents/skills/crafting-effective-readmes/templates/personal.md new file mode 100644 index 0000000..f569a5a --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/templates/personal.md @@ -0,0 +1,51 @@ +# Personal Project README Template + +Use this template for side projects, portfolio pieces, and experiments. +Balance between documenting for future-you and showcasing for others. + +--- + +# [Project Name] + +[One-line description] + +[Screenshot or demo GIF if visual] + +## What This Does + +[2-3 sentences explaining what it does and why you built it. Be specific about the problem it solves for you.] + +## Demo + +[Link to live demo, video, or screenshots] + +## Tech Stack + +- **[Category]**: [Technology] - [brief why you chose it] +- **[Category]**: [Technology] + +## Getting Started + +```bash +[Clone and run commands] +``` + +## How It Works + +[Brief explanation of the interesting parts - architecture, algorithms, or techniques worth noting. This is useful for portfolio viewers and future-you.] + +## What I Learned + +[Key takeaways from building this. Good for portfolios and personal reference.] + +- [Learning 1] +- [Learning 2] + +## Future Ideas + +- [ ] [Thing you might add] +- [ ] [Improvement you're considering] + +## License + +[License if you want one, or just "Personal project" if not sharing] diff --git a/.agents/skills/crafting-effective-readmes/templates/xdg-config.md b/.agents/skills/crafting-effective-readmes/templates/xdg-config.md new file mode 100644 index 0000000..97815d8 --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/templates/xdg-config.md @@ -0,0 +1,71 @@ +# Config Directory README Template + +Use this template for XDG config directories, dotfiles, script folders, +and any local directory you'll return to later wondering "what is this?" + +The audience is future-you, probably confused. + +--- + +# [Tool/Directory Name] Config + +> Last reviewed: [YYYY-MM-DD] + +[One sentence: what this directory configures and why you have custom config] + +## What's Here + +| Path | Purpose | +|------|---------| +| `[file-or-dir]` | [What it does] | +| `[file-or-dir]` | [What it does] | +| `[file-or-dir]` | [What it does] | + +### [Subdirectory 1] (if complex enough to warrant detail) + +[Brief explanation of what's in this subdirectory] + +### [Subdirectory 2] + +[Brief explanation] + +## Why This Setup + +[1-2 paragraphs explaining your philosophy or goals for this config. What problems were you solving? What workflow are you optimizing for?] + +## How to Extend + +### Adding a new [thing] + +1. [Step 1] +2. [Step 2] +3. [Step 3] + +### Adding a new [other thing] + +1. [Steps] + +## Dependencies + +[What needs to be installed for this config to work] + +```bash +[Install commands if applicable] +``` + +## Gotchas + +- [Thing that will confuse future-you] +- [Non-obvious behavior] +- [Files that shouldn't be edited directly] +- [Order dependencies or load sequences] + +## Sync/Backup + +[How this config is backed up or synced across machines, if applicable] + +## Related + +- [Link to tool's official docs] +- [Link to your dotfiles repo if this is part of it] +- [Other relevant resources] diff --git a/.agents/skills/crafting-effective-readmes/using-references.md b/.agents/skills/crafting-effective-readmes/using-references.md new file mode 100644 index 0000000..a25b81d --- /dev/null +++ b/.agents/skills/crafting-effective-readmes/using-references.md @@ -0,0 +1,35 @@ +# Using References + +Templates are your primary tool for writing READMEs. References provide depth - use them to refine your understanding or handle edge cases. + +**Tip:** Don't load all references at once. Pick the one most relevant to your situation. + +--- + +### art-of-readme.md +`references/art-of-readme.md` + +**Why:** The philosophy behind great READMEs - understanding how readers actually scan and evaluate projects +**What:** Cognitive funneling (broad → specific), brevity as a feature, README as the "one-stop shop" that keeps users out of source code + +--- + +### make-a-readme.md +`references/make-a-readme.md` + +**Why:** Practical, section-by-section guidance for what to include +**What:** Walks through each common section (Name, Description, Installation, Usage, etc.) with concrete suggestions. Good reminder: "too long is better than too short" + +--- + +### standard-readme-spec.md +`references/standard-readme-spec.md` + +**Why:** Formal specification when consistency or compliance matters +**What:** Required vs optional sections, exact ordering, formatting rules. Useful for OSS projects wanting a standardized format. + +Examples: +- `references/standard-readme-example-minimal.md` - Bare minimum compliant README +- `references/standard-readme-example-maximal.md` - Full-featured with badges, ToC, all optional sections + + diff --git a/.agents/skills/dispatching-parallel-agents/SKILL.md b/.agents/skills/dispatching-parallel-agents/SKILL.md new file mode 100644 index 0000000..33b1485 --- /dev/null +++ b/.agents/skills/dispatching-parallel-agents/SKILL.md @@ -0,0 +1,180 @@ +--- +name: dispatching-parallel-agents +description: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies +--- + +# Dispatching Parallel Agents + +## Overview + +When you have multiple unrelated failures (different test files, different subsystems, different bugs), investigating them sequentially wastes time. Each investigation is independent and can happen in parallel. + +**Core principle:** Dispatch one agent per independent problem domain. Let them work concurrently. + +## When to Use + +```dot +digraph when_to_use { + "Multiple failures?" [shape=diamond]; + "Are they independent?" [shape=diamond]; + "Single agent investigates all" [shape=box]; + "One agent per problem domain" [shape=box]; + "Can they work in parallel?" [shape=diamond]; + "Sequential agents" [shape=box]; + "Parallel dispatch" [shape=box]; + + "Multiple failures?" -> "Are they independent?" [label="yes"]; + "Are they independent?" -> "Single agent investigates all" [label="no - related"]; + "Are they independent?" -> "Can they work in parallel?" [label="yes"]; + "Can they work in parallel?" -> "Parallel dispatch" [label="yes"]; + "Can they work in parallel?" -> "Sequential agents" [label="no - shared state"]; +} +``` + +**Use when:** +- 3+ test files failing with different root causes +- Multiple subsystems broken independently +- Each problem can be understood without context from others +- No shared state between investigations + +**Don't use when:** +- Failures are related (fix one might fix others) +- Need to understand full system state +- Agents would interfere with each other + +## The Pattern + +### 1. Identify Independent Domains + +Group failures by what's broken: +- File A tests: Tool approval flow +- File B tests: Batch completion behavior +- File C tests: Abort functionality + +Each domain is independent - fixing tool approval doesn't affect abort tests. + +### 2. Create Focused Agent Tasks + +Each agent gets: +- **Specific scope:** One test file or subsystem +- **Clear goal:** Make these tests pass +- **Constraints:** Don't change other code +- **Expected output:** Summary of what you found and fixed + +### 3. Dispatch in Parallel + +```typescript +// In Claude Code / AI environment +Task("Fix agent-tool-abort.test.ts failures") +Task("Fix batch-completion-behavior.test.ts failures") +Task("Fix tool-approval-race-conditions.test.ts failures") +// All three run concurrently +``` + +### 4. Review and Integrate + +When agents return: +- Read each summary +- Verify fixes don't conflict +- Run full test suite +- Integrate all changes + +## Agent Prompt Structure + +Good agent prompts are: +1. **Focused** - One clear problem domain +2. **Self-contained** - All context needed to understand the problem +3. **Specific about output** - What should the agent return? + +```markdown +Fix the 3 failing tests in src/agents/agent-tool-abort.test.ts: + +1. "should abort tool with partial output capture" - expects 'interrupted at' in message +2. "should handle mixed completed and aborted tools" - fast tool aborted instead of completed +3. "should properly track pendingToolCount" - expects 3 results but gets 0 + +These are timing/race condition issues. Your task: + +1. Read the test file and understand what each test verifies +2. Identify root cause - timing issues or actual bugs? +3. Fix by: + - Replacing arbitrary timeouts with event-based waiting + - Fixing bugs in abort implementation if found + - Adjusting test expectations if testing changed behavior + +Do NOT just increase timeouts - find the real issue. + +Return: Summary of what you found and what you fixed. +``` + +## Common Mistakes + +**❌ Too broad:** "Fix all the tests" - agent gets lost +**✅ Specific:** "Fix agent-tool-abort.test.ts" - focused scope + +**❌ No context:** "Fix the race condition" - agent doesn't know where +**✅ Context:** Paste the error messages and test names + +**❌ No constraints:** Agent might refactor everything +**✅ Constraints:** "Do NOT change production code" or "Fix tests only" + +**❌ Vague output:** "Fix it" - you don't know what changed +**✅ Specific:** "Return summary of root cause and changes" + +## When NOT to Use + +**Related failures:** Fixing one might fix others - investigate together first +**Need full context:** Understanding requires seeing entire system +**Exploratory debugging:** You don't know what's broken yet +**Shared state:** Agents would interfere (editing same files, using same resources) + +## Real Example from Session + +**Scenario:** 6 test failures across 3 files after major refactoring + +**Failures:** +- agent-tool-abort.test.ts: 3 failures (timing issues) +- batch-completion-behavior.test.ts: 2 failures (tools not executing) +- tool-approval-race-conditions.test.ts: 1 failure (execution count = 0) + +**Decision:** Independent domains - abort logic separate from batch completion separate from race conditions + +**Dispatch:** +``` +Agent 1 → Fix agent-tool-abort.test.ts +Agent 2 → Fix batch-completion-behavior.test.ts +Agent 3 → Fix tool-approval-race-conditions.test.ts +``` + +**Results:** +- Agent 1: Replaced timeouts with event-based waiting +- Agent 2: Fixed event structure bug (threadId in wrong place) +- Agent 3: Added wait for async tool execution to complete + +**Integration:** All fixes independent, no conflicts, full suite green + +**Time saved:** 3 problems solved in parallel vs sequentially + +## Key Benefits + +1. **Parallelization** - Multiple investigations happen simultaneously +2. **Focus** - Each agent has narrow scope, less context to track +3. **Independence** - Agents don't interfere with each other +4. **Speed** - 3 problems solved in time of 1 + +## Verification + +After agents return: +1. **Review each summary** - Understand what changed +2. **Check for conflicts** - Did agents edit same code? +3. **Run full suite** - Verify all fixes work together +4. **Spot check** - Agents can make systematic errors + +## Real-World Impact + +From debugging session (2025-10-03): +- 6 failures across 3 files +- 3 agents dispatched in parallel +- All investigations completed concurrently +- All fixes integrated successfully +- Zero conflicts between agent changes diff --git a/.agents/skills/executing-plans/SKILL.md b/.agents/skills/executing-plans/SKILL.md new file mode 100644 index 0000000..c1b2533 --- /dev/null +++ b/.agents/skills/executing-plans/SKILL.md @@ -0,0 +1,84 @@ +--- +name: executing-plans +description: Use when you have a written implementation plan to execute in a separate session with review checkpoints +--- + +# Executing Plans + +## Overview + +Load plan, review critically, execute tasks in batches, report for review between batches. + +**Core principle:** Batch execution with checkpoints for architect review. + +**Announce at start:** "I'm using the executing-plans skill to implement this plan." + +## The Process + +### Step 1: Load and Review Plan +1. Read plan file +2. Review critically - identify any questions or concerns about the plan +3. If concerns: Raise them with your human partner before starting +4. If no concerns: Create TodoWrite and proceed + +### Step 2: Execute Batch +**Default: First 3 tasks** + +For each task: +1. Mark as in_progress +2. Follow each step exactly (plan has bite-sized steps) +3. Run verifications as specified +4. Mark as completed + +### Step 3: Report +When batch complete: +- Show what was implemented +- Show verification output +- Say: "Ready for feedback." + +### Step 4: Continue +Based on feedback: +- Apply changes if needed +- Execute next batch +- Repeat until complete + +### Step 5: Complete Development + +After all tasks complete and verified: +- Announce: "I'm using the finishing-a-development-branch skill to complete this work." +- **REQUIRED SUB-SKILL:** Use superpowers:finishing-a-development-branch +- Follow that skill to verify tests, present options, execute choice + +## When to Stop and Ask for Help + +**STOP executing immediately when:** +- Hit a blocker mid-batch (missing dependency, test fails, instruction unclear) +- Plan has critical gaps preventing starting +- You don't understand an instruction +- Verification fails repeatedly + +**Ask for clarification rather than guessing.** + +## When to Revisit Earlier Steps + +**Return to Review (Step 1) when:** +- Partner updates the plan based on your feedback +- Fundamental approach needs rethinking + +**Don't force through blockers** - stop and ask. + +## Remember +- Review plan critically first +- Follow plan steps exactly +- Don't skip verifications +- Reference skills when plan says to +- Between batches: just report and wait +- Stop when blocked, don't guess +- Never start implementation on main/master branch without explicit user consent + +## Integration + +**Required workflow skills:** +- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting +- **superpowers:writing-plans** - Creates the plan this skill executes +- **superpowers:finishing-a-development-branch** - Complete development after all tasks diff --git a/.agents/skills/finishing-a-development-branch/SKILL.md b/.agents/skills/finishing-a-development-branch/SKILL.md new file mode 100644 index 0000000..c308b43 --- /dev/null +++ b/.agents/skills/finishing-a-development-branch/SKILL.md @@ -0,0 +1,200 @@ +--- +name: finishing-a-development-branch +description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup +--- + +# Finishing a Development Branch + +## Overview + +Guide completion of development work by presenting clear options and handling chosen workflow. + +**Core principle:** Verify tests → Present options → Execute choice → Clean up. + +**Announce at start:** "I'm using the finishing-a-development-branch skill to complete this work." + +## The Process + +### Step 1: Verify Tests + +**Before presenting options, verify tests pass:** + +```bash +# Run project's test suite +npm test / cargo test / pytest / go test ./... +``` + +**If tests fail:** +``` +Tests failing ( failures). Must fix before completing: + +[Show failures] + +Cannot proceed with merge/PR until tests pass. +``` + +Stop. Don't proceed to Step 2. + +**If tests pass:** Continue to Step 2. + +### Step 2: Determine Base Branch + +```bash +# Try common base branches +git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null +``` + +Or ask: "This branch split from main - is that correct?" + +### Step 3: Present Options + +Present exactly these 4 options: + +``` +Implementation complete. What would you like to do? + +1. Merge back to locally +2. Push and create a Pull Request +3. Keep the branch as-is (I'll handle it later) +4. Discard this work + +Which option? +``` + +**Don't add explanation** - keep options concise. + +### Step 4: Execute Choice + +#### Option 1: Merge Locally + +```bash +# Switch to base branch +git checkout + +# Pull latest +git pull + +# Merge feature branch +git merge + +# Verify tests on merged result + + +# If tests pass +git branch -d +``` + +Then: Cleanup worktree (Step 5) + +#### Option 2: Push and Create PR + +```bash +# Push branch +git push -u origin + +# Create PR +gh pr create --title "" --body "$(cat <<'EOF' +## Summary +<2-3 bullets of what changed> + +## Test Plan +- [ ] <verification steps> +EOF +)" +``` + +Then: Cleanup worktree (Step 5) + +#### Option 3: Keep As-Is + +Report: "Keeping branch <name>. Worktree preserved at <path>." + +**Don't cleanup worktree.** + +#### Option 4: Discard + +**Confirm first:** +``` +This will permanently delete: +- Branch <name> +- All commits: <commit-list> +- Worktree at <path> + +Type 'discard' to confirm. +``` + +Wait for exact confirmation. + +If confirmed: +```bash +git checkout <base-branch> +git branch -D <feature-branch> +``` + +Then: Cleanup worktree (Step 5) + +### Step 5: Cleanup Worktree + +**For Options 1, 2, 4:** + +Check if in worktree: +```bash +git worktree list | grep $(git branch --show-current) +``` + +If yes: +```bash +git worktree remove <worktree-path> +``` + +**For Option 3:** Keep worktree. + +## Quick Reference + +| Option | Merge | Push | Keep Worktree | Cleanup Branch | +|--------|-------|------|---------------|----------------| +| 1. Merge locally | ✓ | - | - | ✓ | +| 2. Create PR | - | ✓ | ✓ | - | +| 3. Keep as-is | - | - | ✓ | - | +| 4. Discard | - | - | - | ✓ (force) | + +## Common Mistakes + +**Skipping test verification** +- **Problem:** Merge broken code, create failing PR +- **Fix:** Always verify tests before offering options + +**Open-ended questions** +- **Problem:** "What should I do next?" → ambiguous +- **Fix:** Present exactly 4 structured options + +**Automatic worktree cleanup** +- **Problem:** Remove worktree when might need it (Option 2, 3) +- **Fix:** Only cleanup for Options 1 and 4 + +**No confirmation for discard** +- **Problem:** Accidentally delete work +- **Fix:** Require typed "discard" confirmation + +## Red Flags + +**Never:** +- Proceed with failing tests +- Merge without verifying tests on result +- Delete work without confirmation +- Force-push without explicit request + +**Always:** +- Verify tests before offering options +- Present exactly 4 options +- Get typed confirmation for Option 4 +- Clean up worktree for Options 1 & 4 only + +## Integration + +**Called by:** +- **subagent-driven-development** (Step 7) - After all tasks complete +- **executing-plans** (Step 5) - After all batches complete + +**Pairs with:** +- **using-git-worktrees** - Cleans up worktree created by that skill diff --git a/.agents/skills/github-pr-creation/SKILL.md b/.agents/skills/github-pr-creation/SKILL.md new file mode 100644 index 0000000..fd1ca94 --- /dev/null +++ b/.agents/skills/github-pr-creation/SKILL.md @@ -0,0 +1,137 @@ +--- +name: github-pr-creation +description: Creates GitHub Pull Requests with automated validation and task tracking. Use when user wants to create PR, open pull request, submit for review, or check if ready for PR. Analyzes commits, validates task completion, generates Conventional Commits title and description, suggests labels. NOTE - for merging existing PRs, use github-pr-merge instead. +--- + +# GitHub PR creation + +Creates Pull Requests with task validation, test execution, and Conventional Commits formatting. + +## Current state + +!`git rev-parse --abbrev-ref HEAD 2>/dev/null` +!`git log @{u}..HEAD --oneline 2>/dev/null || echo "(no upstream tracking)"` + +## Core workflow + +### 1. Confirm target branch + +**ALWAYS ask user before proceeding:** + +``` +Creating PR from [current-branch] to [target-branch]. Correct? +``` + +| Branch flow | Typical target | +|-------------|---------------| +| feature/* | develop | +| fix/* | develop | +| hotfix/* | main/master | +| develop | main/master | + +### 2. Search for task documentation + +Look for task/spec files that describe what this PR should accomplish. Common locations by tool: + +| Tool/Convention | Path | +|-----------------|------| +| AWS Kiro | `.kiro/specs/*/tasks.md` | +| Cursor | `.cursor/rules/*.md`, `.cursorrules` | +| Trae | `.trae/rules/*.md` | +| GitHub Issues | `gh issue list --assignee @me --state open` | +| Generic | `docs/specs/`, `specs/`, `tasks.md`, `TODO.md` | + +Extract task IDs, titles, descriptions, and requirements references when found. + +### 3. Analyze commits + +For each commit on this branch, identify type, scope, task references, and breaking changes. Map commits to documented tasks when task files exist. + +### 4. Verify task completion + +If task documentation exists: + +1. Identify main task from branch name (e.g., `feature/task-2-*` -> Task 2) +2. Find all sub-tasks (e.g., Task 2.1, 2.2, 2.3) +3. Check which sub-tasks are referenced in commits +4. Report missing sub-tasks + +**If tasks incomplete**, STOP and show status: +``` +Task 2 INCOMPLETE: 1/3 sub-tasks missing +- Task 2.1: done +- Task 2.2: done +- Task 2.3: MISSING +``` + +Ask user whether to complete missing tasks or proceed anyway. + +### 5. Run tests + +Run the project test suite. Tests **MUST** pass before creating PR. + +### 6. Determine PR type and generate title + +| Branch flow | Title prefix | +|-------------|-------------| +| feature/* -> develop | `feat(scope):` | +| fix/* -> develop | `fix(scope):` | +| hotfix/* -> main | `hotfix(scope):` | +| develop -> main | `release:` | +| refactor/* -> develop | `refactor(scope):` | + +**Title format**: `<type>(<scope>): <description>` +- Type: dominant commit type (feat > fix > refactor) +- Scope: most common scope from commits (kebab-case) +- Description: imperative, lowercase, no period, max 50 chars + +### 7. Generate PR body + +Use the appropriate template from `references/pr_templates.md` based on PR type and populate with gathered data. + +### 8. Suggest labels + +**ALWAYS check available labels first:** + +```bash +gh label list +``` + +Match commit types to available project labels. The project may use different names than standard (e.g., "feature" instead of "enhancement"). + +| Commit type | Common label names | +|-------------|-------------------| +| feat | feature, enhancement | +| fix | bug, bugfix | +| refactor | refactoring, tech-debt | +| docs | documentation | +| ci | ci/cd, infrastructure | +| security | security | +| hotfix | urgent, priority:high | + +**If no matching label exists**: suggest creating one. The user may have removed default labels, so offering to add relevant ones is appropriate. + +### 9. Create PR + +**ALWAYS show title, body, and labels for user approval first.** + +```bash +gh pr create --title "[title]" --body "$(cat <<'EOF' +[body content] +EOF +)" --base [base_branch] --label [labels] +``` + +## Important rules + +- **ALWAYS** confirm target branch with user +- **ALWAYS** run tests before creating PR +- **ALWAYS** show PR content for approval before creating +- **ALWAYS** check available labels with `gh label list` before suggesting +- **ALWAYS** use HEREDOC for body to preserve formatting +- **NEVER** create PR without user confirmation +- **NEVER** modify repository files (read-only analysis) + +## References + +- `references/pr_templates.md` - PR body templates for all types (feature, release, bugfix, hotfix, refactoring, docs, CI/CD) diff --git a/.agents/skills/github-pr-creation/references/pr_templates.md b/.agents/skills/github-pr-creation/references/pr_templates.md new file mode 100644 index 0000000..6bce6d2 --- /dev/null +++ b/.agents/skills/github-pr-creation/references/pr_templates.md @@ -0,0 +1,187 @@ +# PR templates + +Select the template matching the PR type. + +--- + +## Feature (feature/* -> develop) + +**Title**: `feat(<scope>): <description>` + +```markdown +## What +- [List of implemented features/components] +- [Main functionality highlights] + +## Why +- [Business/operational impact] +- [Pain points resolved] + +## Details +### Task X.Y: [Task title] +- [Implementation detail 1] +- [Implementation detail 2] +- **Requirements**: X, Y, Z + +## Checklist +- [ ] Main feature implemented +- [ ] Tests written and passing +- [ ] Documentation updated +- [ ] Code review ready +``` + +--- + +## Release (develop -> main) + +**Title**: `release: version X.Y with [main features]` + +```markdown +## What +This release includes: +- [Feature set 1 - Task N] +- [Feature set 2 - Task M] + +## Why +[Release motivation, milestone reached] + +## Details +### Task N: [Feature name] (COMPLETE) +- **Task N.1**: [Description] +- **Task N.2**: [Description] +- **Requirements**: X, Y, Z + +## Testing +- Coverage: XX% +- Integration tests: pass/fail + +## Checklist +- [ ] All tasks completed +- [ ] No breaking changes (or documented) +- [ ] Migration guide prepared (if needed) +``` + +--- + +## Bugfix (fix/* -> develop) + +**Title**: `fix(<scope>): <description>` + +```markdown +## What +Fix for [problem description] + +## Problem +[Bug description, how it manifests, impact] + +## Solution +[Explanation of implemented solution] + +## Testing +- [How the fix was tested] +- [Regression tests added] + +## Checklist +- [ ] Bug resolved +- [ ] Regression tests added +- [ ] No side effects introduced +``` + +--- + +## Hotfix (hotfix/* -> main) + +**Title**: `hotfix(<scope>): <critical fix>` + +```markdown +## HOTFIX + +### Issue +[Description of critical production issue] + +### Root cause +[Identified cause] + +### Fix +[Implemented solution] + +### Impact +- Affected users: [estimate] +- Downtime: [if applicable] + +### Rollback plan +[Rollback plan if needed] + +## Checklist +- [ ] Fix tested in staging +- [ ] Approval for urgent deploy +- [ ] Post-deploy monitoring planned +``` + +--- + +## Refactoring (refactor/* -> develop) + +**Title**: `refactor(<scope>): <description>` + +```markdown +## What +Refactoring of [component/module] + +## Why +- [Motivation: performance, maintainability, technical debt] + +## Changes +- [Change 1] +- [Change 2] + +## Impact +- **Functional**: None (behavior unchanged) +- **Performance**: [expected improvements] +- **Maintainability**: [benefits] + +## Checklist +- [ ] Functional behavior unchanged +- [ ] Existing tests passing +- [ ] New tests added (if needed) +``` + +--- + +## Documentation (docs/*) + +**Title**: `docs(<scope>): <description>` + +```markdown +## What +Documentation update for [area] + +## Changes +- [Document 1]: [change type] +- [Document 2]: [change type] + +## Motivation +[Why this documentation is needed] +``` + +--- + +## CI/CD and infrastructure + +**Title**: `ci(<scope>): <description>` or `chore(<scope>): <description>` + +```markdown +## What +[Pipeline/infrastructure change] + +## Changes +- [Change 1] +- [Change 2] + +## Impact +- Build time: [variation] +- Deploy process: [variation] + +## Testing +[How it was verified] +``` diff --git a/.agents/skills/receiving-code-review/SKILL.md b/.agents/skills/receiving-code-review/SKILL.md new file mode 100644 index 0000000..4ea72cd --- /dev/null +++ b/.agents/skills/receiving-code-review/SKILL.md @@ -0,0 +1,213 @@ +--- +name: receiving-code-review +description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation +--- + +# Code Review Reception + +## Overview + +Code review requires technical evaluation, not emotional performance. + +**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort. + +## The Response Pattern + +``` +WHEN receiving code review feedback: + +1. READ: Complete feedback without reacting +2. UNDERSTAND: Restate requirement in own words (or ask) +3. VERIFY: Check against codebase reality +4. EVALUATE: Technically sound for THIS codebase? +5. RESPOND: Technical acknowledgment or reasoned pushback +6. IMPLEMENT: One item at a time, test each +``` + +## Forbidden Responses + +**NEVER:** +- "You're absolutely right!" (explicit CLAUDE.md violation) +- "Great point!" / "Excellent feedback!" (performative) +- "Let me implement that now" (before verification) + +**INSTEAD:** +- Restate the technical requirement +- Ask clarifying questions +- Push back with technical reasoning if wrong +- Just start working (actions > words) + +## Handling Unclear Feedback + +``` +IF any item is unclear: + STOP - do not implement anything yet + ASK for clarification on unclear items + +WHY: Items may be related. Partial understanding = wrong implementation. +``` + +**Example:** +``` +your human partner: "Fix 1-6" +You understand 1,2,3,6. Unclear on 4,5. + +❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later +✅ RIGHT: "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding." +``` + +## Source-Specific Handling + +### From your human partner +- **Trusted** - implement after understanding +- **Still ask** if scope unclear +- **No performative agreement** +- **Skip to action** or technical acknowledgment + +### From External Reviewers +``` +BEFORE implementing: + 1. Check: Technically correct for THIS codebase? + 2. Check: Breaks existing functionality? + 3. Check: Reason for current implementation? + 4. Check: Works on all platforms/versions? + 5. Check: Does reviewer understand full context? + +IF suggestion seems wrong: + Push back with technical reasoning + +IF can't easily verify: + Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?" + +IF conflicts with your human partner's prior decisions: + Stop and discuss with your human partner first +``` + +**your human partner's rule:** "External feedback - be skeptical, but check carefully" + +## YAGNI Check for "Professional" Features + +``` +IF reviewer suggests "implementing properly": + grep codebase for actual usage + + IF unused: "This endpoint isn't called. Remove it (YAGNI)?" + IF used: Then implement properly +``` + +**your human partner's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it." + +## Implementation Order + +``` +FOR multi-item feedback: + 1. Clarify anything unclear FIRST + 2. Then implement in this order: + - Blocking issues (breaks, security) + - Simple fixes (typos, imports) + - Complex fixes (refactoring, logic) + 3. Test each fix individually + 4. Verify no regressions +``` + +## When To Push Back + +Push back when: +- Suggestion breaks existing functionality +- Reviewer lacks full context +- Violates YAGNI (unused feature) +- Technically incorrect for this stack +- Legacy/compatibility reasons exist +- Conflicts with your human partner's architectural decisions + +**How to push back:** +- Use technical reasoning, not defensiveness +- Ask specific questions +- Reference working tests/code +- Involve your human partner if architectural + +**Signal if uncomfortable pushing back out loud:** "Strange things are afoot at the Circle K" + +## Acknowledging Correct Feedback + +When feedback IS correct: +``` +✅ "Fixed. [Brief description of what changed]" +✅ "Good catch - [specific issue]. Fixed in [location]." +✅ [Just fix it and show in the code] + +❌ "You're absolutely right!" +❌ "Great point!" +❌ "Thanks for catching that!" +❌ "Thanks for [anything]" +❌ ANY gratitude expression +``` + +**Why no thanks:** Actions speak. Just fix it. The code itself shows you heard the feedback. + +**If you catch yourself about to write "Thanks":** DELETE IT. State the fix instead. + +## Gracefully Correcting Your Pushback + +If you pushed back and were wrong: +``` +✅ "You were right - I checked [X] and it does [Y]. Implementing now." +✅ "Verified this and you're correct. My initial understanding was wrong because [reason]. Fixing." + +❌ Long apology +❌ Defending why you pushed back +❌ Over-explaining +``` + +State the correction factually and move on. + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| Performative agreement | State requirement or just act | +| Blind implementation | Verify against codebase first | +| Batch without testing | One at a time, test each | +| Assuming reviewer is right | Check if breaks things | +| Avoiding pushback | Technical correctness > comfort | +| Partial implementation | Clarify all items first | +| Can't verify, proceed anyway | State limitation, ask for direction | + +## Real Examples + +**Performative Agreement (Bad):** +``` +Reviewer: "Remove legacy code" +❌ "You're absolutely right! Let me remove that..." +``` + +**Technical Verification (Good):** +``` +Reviewer: "Remove legacy code" +✅ "Checking... build target is 10.15+, this API needs 13+. Need legacy for backward compat. Current impl has wrong bundle ID - fix it or drop pre-13 support?" +``` + +**YAGNI (Good):** +``` +Reviewer: "Implement proper metrics tracking with database, date filters, CSV export" +✅ "Grepped codebase - nothing calls this endpoint. Remove it (YAGNI)? Or is there usage I'm missing?" +``` + +**Unclear Item (Good):** +``` +your human partner: "Fix items 1-6" +You understand 1,2,3,6. Unclear on 4,5. +✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing." +``` + +## GitHub Thread Replies + +When replying to inline review comments on GitHub, reply in the comment thread (`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`), not as a top-level PR comment. + +## The Bottom Line + +**External feedback = suggestions to evaluate, not orders to follow.** + +Verify. Question. Then implement. + +No performative agreement. Technical rigor always. diff --git a/.agents/skills/requesting-code-review/SKILL.md b/.agents/skills/requesting-code-review/SKILL.md new file mode 100644 index 0000000..f0e3395 --- /dev/null +++ b/.agents/skills/requesting-code-review/SKILL.md @@ -0,0 +1,105 @@ +--- +name: requesting-code-review +description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements +--- + +# Requesting Code Review + +Dispatch superpowers:code-reviewer subagent to catch issues before they cascade. + +**Core principle:** Review early, review often. + +## When to Request Review + +**Mandatory:** +- After each task in subagent-driven development +- After completing major feature +- Before merge to main + +**Optional but valuable:** +- When stuck (fresh perspective) +- Before refactoring (baseline check) +- After fixing complex bug + +## How to Request + +**1. Get git SHAs:** +```bash +BASE_SHA=$(git rev-parse HEAD~1) # or origin/main +HEAD_SHA=$(git rev-parse HEAD) +``` + +**2. Dispatch code-reviewer subagent:** + +Use Task tool with superpowers:code-reviewer type, fill template at `code-reviewer.md` + +**Placeholders:** +- `{WHAT_WAS_IMPLEMENTED}` - What you just built +- `{PLAN_OR_REQUIREMENTS}` - What it should do +- `{BASE_SHA}` - Starting commit +- `{HEAD_SHA}` - Ending commit +- `{DESCRIPTION}` - Brief summary + +**3. Act on feedback:** +- Fix Critical issues immediately +- Fix Important issues before proceeding +- Note Minor issues for later +- Push back if reviewer is wrong (with reasoning) + +## Example + +``` +[Just completed Task 2: Add verification function] + +You: Let me request code review before proceeding. + +BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}') +HEAD_SHA=$(git rev-parse HEAD) + +[Dispatch superpowers:code-reviewer subagent] + WHAT_WAS_IMPLEMENTED: Verification and repair functions for conversation index + PLAN_OR_REQUIREMENTS: Task 2 from docs/plans/deployment-plan.md + BASE_SHA: a7981ec + HEAD_SHA: 3df7661 + DESCRIPTION: Added verifyIndex() and repairIndex() with 4 issue types + +[Subagent returns]: + Strengths: Clean architecture, real tests + Issues: + Important: Missing progress indicators + Minor: Magic number (100) for reporting interval + Assessment: Ready to proceed + +You: [Fix progress indicators] +[Continue to Task 3] +``` + +## Integration with Workflows + +**Subagent-Driven Development:** +- Review after EACH task +- Catch issues before they compound +- Fix before moving to next task + +**Executing Plans:** +- Review after each batch (3 tasks) +- Get feedback, apply, continue + +**Ad-Hoc Development:** +- Review before merge +- Review when stuck + +## Red Flags + +**Never:** +- Skip review because "it's simple" +- Ignore Critical issues +- Proceed with unfixed Important issues +- Argue with valid technical feedback + +**If reviewer wrong:** +- Push back with technical reasoning +- Show code/tests that prove it works +- Request clarification + +See template at: requesting-code-review/code-reviewer.md diff --git a/.agents/skills/requesting-code-review/code-reviewer.md b/.agents/skills/requesting-code-review/code-reviewer.md new file mode 100644 index 0000000..3c427c9 --- /dev/null +++ b/.agents/skills/requesting-code-review/code-reviewer.md @@ -0,0 +1,146 @@ +# Code Review Agent + +You are reviewing code changes for production readiness. + +**Your task:** +1. Review {WHAT_WAS_IMPLEMENTED} +2. Compare against {PLAN_OR_REQUIREMENTS} +3. Check code quality, architecture, testing +4. Categorize issues by severity +5. Assess production readiness + +## What Was Implemented + +{DESCRIPTION} + +## Requirements/Plan + +{PLAN_REFERENCE} + +## Git Range to Review + +**Base:** {BASE_SHA} +**Head:** {HEAD_SHA} + +```bash +git diff --stat {BASE_SHA}..{HEAD_SHA} +git diff {BASE_SHA}..{HEAD_SHA} +``` + +## Review Checklist + +**Code Quality:** +- Clean separation of concerns? +- Proper error handling? +- Type safety (if applicable)? +- DRY principle followed? +- Edge cases handled? + +**Architecture:** +- Sound design decisions? +- Scalability considerations? +- Performance implications? +- Security concerns? + +**Testing:** +- Tests actually test logic (not mocks)? +- Edge cases covered? +- Integration tests where needed? +- All tests passing? + +**Requirements:** +- All plan requirements met? +- Implementation matches spec? +- No scope creep? +- Breaking changes documented? + +**Production Readiness:** +- Migration strategy (if schema changes)? +- Backward compatibility considered? +- Documentation complete? +- No obvious bugs? + +## Output Format + +### Strengths +[What's well done? Be specific.] + +### Issues + +#### Critical (Must Fix) +[Bugs, security issues, data loss risks, broken functionality] + +#### Important (Should Fix) +[Architecture problems, missing features, poor error handling, test gaps] + +#### Minor (Nice to Have) +[Code style, optimization opportunities, documentation improvements] + +**For each issue:** +- File:line reference +- What's wrong +- Why it matters +- How to fix (if not obvious) + +### Recommendations +[Improvements for code quality, architecture, or process] + +### Assessment + +**Ready to merge?** [Yes/No/With fixes] + +**Reasoning:** [Technical assessment in 1-2 sentences] + +## Critical Rules + +**DO:** +- Categorize by actual severity (not everything is Critical) +- Be specific (file:line, not vague) +- Explain WHY issues matter +- Acknowledge strengths +- Give clear verdict + +**DON'T:** +- Say "looks good" without checking +- Mark nitpicks as Critical +- Give feedback on code you didn't review +- Be vague ("improve error handling") +- Avoid giving a clear verdict + +## Example Output + +``` +### Strengths +- Clean database schema with proper migrations (db.ts:15-42) +- Comprehensive test coverage (18 tests, all edge cases) +- Good error handling with fallbacks (summarizer.ts:85-92) + +### Issues + +#### Important +1. **Missing help text in CLI wrapper** + - File: index-conversations:1-31 + - Issue: No --help flag, users won't discover --concurrency + - Fix: Add --help case with usage examples + +2. **Date validation missing** + - File: search.ts:25-27 + - Issue: Invalid dates silently return no results + - Fix: Validate ISO format, throw error with example + +#### Minor +1. **Progress indicators** + - File: indexer.ts:130 + - Issue: No "X of Y" counter for long operations + - Impact: Users don't know how long to wait + +### Recommendations +- Add progress reporting for user experience +- Consider config file for excluded projects (portability) + +### Assessment + +**Ready to merge: With fixes** + +**Reasoning:** Core implementation is solid with good architecture and tests. Important issues (help text, date validation) are easily fixed and don't affect core functionality. +``` diff --git a/.agents/skills/subagent-driven-development/SKILL.md b/.agents/skills/subagent-driven-development/SKILL.md new file mode 100644 index 0000000..b578dfa --- /dev/null +++ b/.agents/skills/subagent-driven-development/SKILL.md @@ -0,0 +1,242 @@ +--- +name: subagent-driven-development +description: Use when executing implementation plans with independent tasks in the current session +--- + +# Subagent-Driven Development + +Execute plan by dispatching fresh subagent per task, with two-stage review after each: spec compliance review first, then code quality review. + +**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration + +## When to Use + +```dot +digraph when_to_use { + "Have implementation plan?" [shape=diamond]; + "Tasks mostly independent?" [shape=diamond]; + "Stay in this session?" [shape=diamond]; + "subagent-driven-development" [shape=box]; + "executing-plans" [shape=box]; + "Manual execution or brainstorm first" [shape=box]; + + "Have implementation plan?" -> "Tasks mostly independent?" [label="yes"]; + "Have implementation plan?" -> "Manual execution or brainstorm first" [label="no"]; + "Tasks mostly independent?" -> "Stay in this session?" [label="yes"]; + "Tasks mostly independent?" -> "Manual execution or brainstorm first" [label="no - tightly coupled"]; + "Stay in this session?" -> "subagent-driven-development" [label="yes"]; + "Stay in this session?" -> "executing-plans" [label="no - parallel session"]; +} +``` + +**vs. Executing Plans (parallel session):** +- Same session (no context switch) +- Fresh subagent per task (no context pollution) +- Two-stage review after each task: spec compliance first, then code quality +- Faster iteration (no human-in-loop between tasks) + +## The Process + +```dot +digraph process { + rankdir=TB; + + subgraph cluster_per_task { + label="Per Task"; + "Dispatch implementer subagent (./implementer-prompt.md)" [shape=box]; + "Implementer subagent asks questions?" [shape=diamond]; + "Answer questions, provide context" [shape=box]; + "Implementer subagent implements, tests, commits, self-reviews" [shape=box]; + "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" [shape=box]; + "Spec reviewer subagent confirms code matches spec?" [shape=diamond]; + "Implementer subagent fixes spec gaps" [shape=box]; + "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [shape=box]; + "Code quality reviewer subagent approves?" [shape=diamond]; + "Implementer subagent fixes quality issues" [shape=box]; + "Mark task complete in TodoWrite" [shape=box]; + } + + "Read plan, extract all tasks with full text, note context, create TodoWrite" [shape=box]; + "More tasks remain?" [shape=diamond]; + "Dispatch final code reviewer subagent for entire implementation" [shape=box]; + "Use superpowers:finishing-a-development-branch" [shape=box style=filled fillcolor=lightgreen]; + + "Read plan, extract all tasks with full text, note context, create TodoWrite" -> "Dispatch implementer subagent (./implementer-prompt.md)"; + "Dispatch implementer subagent (./implementer-prompt.md)" -> "Implementer subagent asks questions?"; + "Implementer subagent asks questions?" -> "Answer questions, provide context" [label="yes"]; + "Answer questions, provide context" -> "Dispatch implementer subagent (./implementer-prompt.md)"; + "Implementer subagent asks questions?" -> "Implementer subagent implements, tests, commits, self-reviews" [label="no"]; + "Implementer subagent implements, tests, commits, self-reviews" -> "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)"; + "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" -> "Spec reviewer subagent confirms code matches spec?"; + "Spec reviewer subagent confirms code matches spec?" -> "Implementer subagent fixes spec gaps" [label="no"]; + "Implementer subagent fixes spec gaps" -> "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" [label="re-review"]; + "Spec reviewer subagent confirms code matches spec?" -> "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [label="yes"]; + "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" -> "Code quality reviewer subagent approves?"; + "Code quality reviewer subagent approves?" -> "Implementer subagent fixes quality issues" [label="no"]; + "Implementer subagent fixes quality issues" -> "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [label="re-review"]; + "Code quality reviewer subagent approves?" -> "Mark task complete in TodoWrite" [label="yes"]; + "Mark task complete in TodoWrite" -> "More tasks remain?"; + "More tasks remain?" -> "Dispatch implementer subagent (./implementer-prompt.md)" [label="yes"]; + "More tasks remain?" -> "Dispatch final code reviewer subagent for entire implementation" [label="no"]; + "Dispatch final code reviewer subagent for entire implementation" -> "Use superpowers:finishing-a-development-branch"; +} +``` + +## Prompt Templates + +- `./implementer-prompt.md` - Dispatch implementer subagent +- `./spec-reviewer-prompt.md` - Dispatch spec compliance reviewer subagent +- `./code-quality-reviewer-prompt.md` - Dispatch code quality reviewer subagent + +## Example Workflow + +``` +You: I'm using Subagent-Driven Development to execute this plan. + +[Read plan file once: docs/plans/feature-plan.md] +[Extract all 5 tasks with full text and context] +[Create TodoWrite with all tasks] + +Task 1: Hook installation script + +[Get Task 1 text and context (already extracted)] +[Dispatch implementation subagent with full task text + context] + +Implementer: "Before I begin - should the hook be installed at user or system level?" + +You: "User level (~/.config/superpowers/hooks/)" + +Implementer: "Got it. Implementing now..." +[Later] Implementer: + - Implemented install-hook command + - Added tests, 5/5 passing + - Self-review: Found I missed --force flag, added it + - Committed + +[Dispatch spec compliance reviewer] +Spec reviewer: ✅ Spec compliant - all requirements met, nothing extra + +[Get git SHAs, dispatch code quality reviewer] +Code reviewer: Strengths: Good test coverage, clean. Issues: None. Approved. + +[Mark Task 1 complete] + +Task 2: Recovery modes + +[Get Task 2 text and context (already extracted)] +[Dispatch implementation subagent with full task text + context] + +Implementer: [No questions, proceeds] +Implementer: + - Added verify/repair modes + - 8/8 tests passing + - Self-review: All good + - Committed + +[Dispatch spec compliance reviewer] +Spec reviewer: ❌ Issues: + - Missing: Progress reporting (spec says "report every 100 items") + - Extra: Added --json flag (not requested) + +[Implementer fixes issues] +Implementer: Removed --json flag, added progress reporting + +[Spec reviewer reviews again] +Spec reviewer: ✅ Spec compliant now + +[Dispatch code quality reviewer] +Code reviewer: Strengths: Solid. Issues (Important): Magic number (100) + +[Implementer fixes] +Implementer: Extracted PROGRESS_INTERVAL constant + +[Code reviewer reviews again] +Code reviewer: ✅ Approved + +[Mark Task 2 complete] + +... + +[After all tasks] +[Dispatch final code-reviewer] +Final reviewer: All requirements met, ready to merge + +Done! +``` + +## Advantages + +**vs. Manual execution:** +- Subagents follow TDD naturally +- Fresh context per task (no confusion) +- Parallel-safe (subagents don't interfere) +- Subagent can ask questions (before AND during work) + +**vs. Executing Plans:** +- Same session (no handoff) +- Continuous progress (no waiting) +- Review checkpoints automatic + +**Efficiency gains:** +- No file reading overhead (controller provides full text) +- Controller curates exactly what context is needed +- Subagent gets complete information upfront +- Questions surfaced before work begins (not after) + +**Quality gates:** +- Self-review catches issues before handoff +- Two-stage review: spec compliance, then code quality +- Review loops ensure fixes actually work +- Spec compliance prevents over/under-building +- Code quality ensures implementation is well-built + +**Cost:** +- More subagent invocations (implementer + 2 reviewers per task) +- Controller does more prep work (extracting all tasks upfront) +- Review loops add iterations +- But catches issues early (cheaper than debugging later) + +## Red Flags + +**Never:** +- Start implementation on main/master branch without explicit user consent +- Skip reviews (spec compliance OR code quality) +- Proceed with unfixed issues +- Dispatch multiple implementation subagents in parallel (conflicts) +- Make subagent read plan file (provide full text instead) +- Skip scene-setting context (subagent needs to understand where task fits) +- Ignore subagent questions (answer before letting them proceed) +- Accept "close enough" on spec compliance (spec reviewer found issues = not done) +- Skip review loops (reviewer found issues = implementer fixes = review again) +- Let implementer self-review replace actual review (both are needed) +- **Start code quality review before spec compliance is ✅** (wrong order) +- Move to next task while either review has open issues + +**If subagent asks questions:** +- Answer clearly and completely +- Provide additional context if needed +- Don't rush them into implementation + +**If reviewer finds issues:** +- Implementer (same subagent) fixes them +- Reviewer reviews again +- Repeat until approved +- Don't skip the re-review + +**If subagent fails task:** +- Dispatch fix subagent with specific instructions +- Don't try to fix manually (context pollution) + +## Integration + +**Required workflow skills:** +- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting +- **superpowers:writing-plans** - Creates the plan this skill executes +- **superpowers:requesting-code-review** - Code review template for reviewer subagents +- **superpowers:finishing-a-development-branch** - Complete development after all tasks + +**Subagents should use:** +- **superpowers:test-driven-development** - Subagents follow TDD for each task + +**Alternative workflow:** +- **superpowers:executing-plans** - Use for parallel session instead of same-session execution diff --git a/.agents/skills/subagent-driven-development/code-quality-reviewer-prompt.md b/.agents/skills/subagent-driven-development/code-quality-reviewer-prompt.md new file mode 100644 index 0000000..d029ea2 --- /dev/null +++ b/.agents/skills/subagent-driven-development/code-quality-reviewer-prompt.md @@ -0,0 +1,20 @@ +# Code Quality Reviewer Prompt Template + +Use this template when dispatching a code quality reviewer subagent. + +**Purpose:** Verify implementation is well-built (clean, tested, maintainable) + +**Only dispatch after spec compliance review passes.** + +``` +Task tool (superpowers:code-reviewer): + Use template at requesting-code-review/code-reviewer.md + + WHAT_WAS_IMPLEMENTED: [from implementer's report] + PLAN_OR_REQUIREMENTS: Task N from [plan-file] + BASE_SHA: [commit before task] + HEAD_SHA: [current commit] + DESCRIPTION: [task summary] +``` + +**Code reviewer returns:** Strengths, Issues (Critical/Important/Minor), Assessment diff --git a/.agents/skills/subagent-driven-development/implementer-prompt.md b/.agents/skills/subagent-driven-development/implementer-prompt.md new file mode 100644 index 0000000..db5404b --- /dev/null +++ b/.agents/skills/subagent-driven-development/implementer-prompt.md @@ -0,0 +1,78 @@ +# Implementer Subagent Prompt Template + +Use this template when dispatching an implementer subagent. + +``` +Task tool (general-purpose): + description: "Implement Task N: [task name]" + prompt: | + You are implementing Task N: [task name] + + ## Task Description + + [FULL TEXT of task from plan - paste it here, don't make subagent read file] + + ## Context + + [Scene-setting: where this fits, dependencies, architectural context] + + ## Before You Begin + + If you have questions about: + - The requirements or acceptance criteria + - The approach or implementation strategy + - Dependencies or assumptions + - Anything unclear in the task description + + **Ask them now.** Raise any concerns before starting work. + + ## Your Job + + Once you're clear on requirements: + 1. Implement exactly what the task specifies + 2. Write tests (following TDD if task says to) + 3. Verify implementation works + 4. Commit your work + 5. Self-review (see below) + 6. Report back + + Work from: [directory] + + **While you work:** If you encounter something unexpected or unclear, **ask questions**. + It's always OK to pause and clarify. Don't guess or make assumptions. + + ## Before Reporting Back: Self-Review + + Review your work with fresh eyes. Ask yourself: + + **Completeness:** + - Did I fully implement everything in the spec? + - Did I miss any requirements? + - Are there edge cases I didn't handle? + + **Quality:** + - Is this my best work? + - Are names clear and accurate (match what things do, not how they work)? + - Is the code clean and maintainable? + + **Discipline:** + - Did I avoid overbuilding (YAGNI)? + - Did I only build what was requested? + - Did I follow existing patterns in the codebase? + + **Testing:** + - Do tests actually verify behavior (not just mock behavior)? + - Did I follow TDD if required? + - Are tests comprehensive? + + If you find issues during self-review, fix them now before reporting. + + ## Report Format + + When done, report: + - What you implemented + - What you tested and test results + - Files changed + - Self-review findings (if any) + - Any issues or concerns +``` diff --git a/.agents/skills/subagent-driven-development/spec-reviewer-prompt.md b/.agents/skills/subagent-driven-development/spec-reviewer-prompt.md new file mode 100644 index 0000000..ab5ddb8 --- /dev/null +++ b/.agents/skills/subagent-driven-development/spec-reviewer-prompt.md @@ -0,0 +1,61 @@ +# Spec Compliance Reviewer Prompt Template + +Use this template when dispatching a spec compliance reviewer subagent. + +**Purpose:** Verify implementer built what was requested (nothing more, nothing less) + +``` +Task tool (general-purpose): + description: "Review spec compliance for Task N" + prompt: | + You are reviewing whether an implementation matches its specification. + + ## What Was Requested + + [FULL TEXT of task requirements] + + ## What Implementer Claims They Built + + [From implementer's report] + + ## CRITICAL: Do Not Trust the Report + + The implementer finished suspiciously quickly. Their report may be incomplete, + inaccurate, or optimistic. You MUST verify everything independently. + + **DO NOT:** + - Take their word for what they implemented + - Trust their claims about completeness + - Accept their interpretation of requirements + + **DO:** + - Read the actual code they wrote + - Compare actual implementation to requirements line by line + - Check for missing pieces they claimed to implement + - Look for extra features they didn't mention + + ## Your Job + + Read the implementation code and verify: + + **Missing requirements:** + - Did they implement everything that was requested? + - Are there requirements they skipped or missed? + - Did they claim something works but didn't actually implement it? + + **Extra/unneeded work:** + - Did they build things that weren't requested? + - Did they over-engineer or add unnecessary features? + - Did they add "nice to haves" that weren't in spec? + + **Misunderstandings:** + - Did they interpret requirements differently than intended? + - Did they solve the wrong problem? + - Did they implement the right feature but wrong way? + + **Verify by reading code, not by trusting report.** + + Report: + - ✅ Spec compliant (if everything matches after code inspection) + - ❌ Issues found: [list specifically what's missing or extra, with file:line references] +``` diff --git a/.agents/skills/systematic-debugging/CREATION-LOG.md b/.agents/skills/systematic-debugging/CREATION-LOG.md new file mode 100644 index 0000000..024d00a --- /dev/null +++ b/.agents/skills/systematic-debugging/CREATION-LOG.md @@ -0,0 +1,119 @@ +# Creation Log: Systematic Debugging Skill + +Reference example of extracting, structuring, and bulletproofing a critical skill. + +## Source Material + +Extracted debugging framework from `/Users/jesse/.claude/CLAUDE.md`: +- 4-phase systematic process (Investigation → Pattern Analysis → Hypothesis → Implementation) +- Core mandate: ALWAYS find root cause, NEVER fix symptoms +- Rules designed to resist time pressure and rationalization + +## Extraction Decisions + +**What to include:** +- Complete 4-phase framework with all rules +- Anti-shortcuts ("NEVER fix symptom", "STOP and re-analyze") +- Pressure-resistant language ("even if faster", "even if I seem in a hurry") +- Concrete steps for each phase + +**What to leave out:** +- Project-specific context +- Repetitive variations of same rule +- Narrative explanations (condensed to principles) + +## Structure Following skill-creation/SKILL.md + +1. **Rich when_to_use** - Included symptoms and anti-patterns +2. **Type: technique** - Concrete process with steps +3. **Keywords** - "root cause", "symptom", "workaround", "debugging", "investigation" +4. **Flowchart** - Decision point for "fix failed" → re-analyze vs add more fixes +5. **Phase-by-phase breakdown** - Scannable checklist format +6. **Anti-patterns section** - What NOT to do (critical for this skill) + +## Bulletproofing Elements + +Framework designed to resist rationalization under pressure: + +### Language Choices +- "ALWAYS" / "NEVER" (not "should" / "try to") +- "even if faster" / "even if I seem in a hurry" +- "STOP and re-analyze" (explicit pause) +- "Don't skip past" (catches the actual behavior) + +### Structural Defenses +- **Phase 1 required** - Can't skip to implementation +- **Single hypothesis rule** - Forces thinking, prevents shotgun fixes +- **Explicit failure mode** - "IF your first fix doesn't work" with mandatory action +- **Anti-patterns section** - Shows exactly what shortcuts look like + +### Redundancy +- Root cause mandate in overview + when_to_use + Phase 1 + implementation rules +- "NEVER fix symptom" appears 4 times in different contexts +- Each phase has explicit "don't skip" guidance + +## Testing Approach + +Created 4 validation tests following skills/meta/testing-skills-with-subagents: + +### Test 1: Academic Context (No Pressure) +- Simple bug, no time pressure +- **Result:** Perfect compliance, complete investigation + +### Test 2: Time Pressure + Obvious Quick Fix +- User "in a hurry", symptom fix looks easy +- **Result:** Resisted shortcut, followed full process, found real root cause + +### Test 3: Complex System + Uncertainty +- Multi-layer failure, unclear if can find root cause +- **Result:** Systematic investigation, traced through all layers, found source + +### Test 4: Failed First Fix +- Hypothesis doesn't work, temptation to add more fixes +- **Result:** Stopped, re-analyzed, formed new hypothesis (no shotgun) + +**All tests passed.** No rationalizations found. + +## Iterations + +### Initial Version +- Complete 4-phase framework +- Anti-patterns section +- Flowchart for "fix failed" decision + +### Enhancement 1: TDD Reference +- Added link to skills/testing/test-driven-development +- Note explaining TDD's "simplest code" ≠ debugging's "root cause" +- Prevents confusion between methodologies + +## Final Outcome + +Bulletproof skill that: +- ✅ Clearly mandates root cause investigation +- ✅ Resists time pressure rationalization +- ✅ Provides concrete steps for each phase +- ✅ Shows anti-patterns explicitly +- ✅ Tested under multiple pressure scenarios +- ✅ Clarifies relationship to TDD +- ✅ Ready for use + +## Key Insight + +**Most important bulletproofing:** Anti-patterns section showing exact shortcuts that feel justified in the moment. When Claude thinks "I'll just add this one quick fix", seeing that exact pattern listed as wrong creates cognitive friction. + +## Usage Example + +When encountering a bug: +1. Load skill: skills/debugging/systematic-debugging +2. Read overview (10 sec) - reminded of mandate +3. Follow Phase 1 checklist - forced investigation +4. If tempted to skip - see anti-pattern, stop +5. Complete all phases - root cause found + +**Time investment:** 5-10 minutes +**Time saved:** Hours of symptom-whack-a-mole + +--- + +*Created: 2025-10-03* +*Purpose: Reference example for skill extraction and bulletproofing* diff --git a/.agents/skills/systematic-debugging/SKILL.md b/.agents/skills/systematic-debugging/SKILL.md new file mode 100644 index 0000000..111d2a9 --- /dev/null +++ b/.agents/skills/systematic-debugging/SKILL.md @@ -0,0 +1,296 @@ +--- +name: systematic-debugging +description: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes +--- + +# Systematic Debugging + +## Overview + +Random fixes waste time and create new bugs. Quick patches mask underlying issues. + +**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure. + +**Violating the letter of this process is violating the spirit of debugging.** + +## The Iron Law + +``` +NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST +``` + +If you haven't completed Phase 1, you cannot propose fixes. + +## When to Use + +Use for ANY technical issue: +- Test failures +- Bugs in production +- Unexpected behavior +- Performance problems +- Build failures +- Integration issues + +**Use this ESPECIALLY when:** +- Under time pressure (emergencies make guessing tempting) +- "Just one quick fix" seems obvious +- You've already tried multiple fixes +- Previous fix didn't work +- You don't fully understand the issue + +**Don't skip when:** +- Issue seems simple (simple bugs have root causes too) +- You're in a hurry (rushing guarantees rework) +- Manager wants it fixed NOW (systematic is faster than thrashing) + +## The Four Phases + +You MUST complete each phase before proceeding to the next. + +### Phase 1: Root Cause Investigation + +**BEFORE attempting ANY fix:** + +1. **Read Error Messages Carefully** + - Don't skip past errors or warnings + - They often contain the exact solution + - Read stack traces completely + - Note line numbers, file paths, error codes + +2. **Reproduce Consistently** + - Can you trigger it reliably? + - What are the exact steps? + - Does it happen every time? + - If not reproducible → gather more data, don't guess + +3. **Check Recent Changes** + - What changed that could cause this? + - Git diff, recent commits + - New dependencies, config changes + - Environmental differences + +4. **Gather Evidence in Multi-Component Systems** + + **WHEN system has multiple components (CI → build → signing, API → service → database):** + + **BEFORE proposing fixes, add diagnostic instrumentation:** + ``` + For EACH component boundary: + - Log what data enters component + - Log what data exits component + - Verify environment/config propagation + - Check state at each layer + + Run once to gather evidence showing WHERE it breaks + THEN analyze evidence to identify failing component + THEN investigate that specific component + ``` + + **Example (multi-layer system):** + ```bash + # Layer 1: Workflow + echo "=== Secrets available in workflow: ===" + echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}" + + # Layer 2: Build script + echo "=== Env vars in build script: ===" + env | grep IDENTITY || echo "IDENTITY not in environment" + + # Layer 3: Signing script + echo "=== Keychain state: ===" + security list-keychains + security find-identity -v + + # Layer 4: Actual signing + codesign --sign "$IDENTITY" --verbose=4 "$APP" + ``` + + **This reveals:** Which layer fails (secrets → workflow ✓, workflow → build ✗) + +5. **Trace Data Flow** + + **WHEN error is deep in call stack:** + + See `root-cause-tracing.md` in this directory for the complete backward tracing technique. + + **Quick version:** + - Where does bad value originate? + - What called this with bad value? + - Keep tracing up until you find the source + - Fix at source, not at symptom + +### Phase 2: Pattern Analysis + +**Find the pattern before fixing:** + +1. **Find Working Examples** + - Locate similar working code in same codebase + - What works that's similar to what's broken? + +2. **Compare Against References** + - If implementing pattern, read reference implementation COMPLETELY + - Don't skim - read every line + - Understand the pattern fully before applying + +3. **Identify Differences** + - What's different between working and broken? + - List every difference, however small + - Don't assume "that can't matter" + +4. **Understand Dependencies** + - What other components does this need? + - What settings, config, environment? + - What assumptions does it make? + +### Phase 3: Hypothesis and Testing + +**Scientific method:** + +1. **Form Single Hypothesis** + - State clearly: "I think X is the root cause because Y" + - Write it down + - Be specific, not vague + +2. **Test Minimally** + - Make the SMALLEST possible change to test hypothesis + - One variable at a time + - Don't fix multiple things at once + +3. **Verify Before Continuing** + - Did it work? Yes → Phase 4 + - Didn't work? Form NEW hypothesis + - DON'T add more fixes on top + +4. **When You Don't Know** + - Say "I don't understand X" + - Don't pretend to know + - Ask for help + - Research more + +### Phase 4: Implementation + +**Fix the root cause, not the symptom:** + +1. **Create Failing Test Case** + - Simplest possible reproduction + - Automated test if possible + - One-off test script if no framework + - MUST have before fixing + - Use the `superpowers:test-driven-development` skill for writing proper failing tests + +2. **Implement Single Fix** + - Address the root cause identified + - ONE change at a time + - No "while I'm here" improvements + - No bundled refactoring + +3. **Verify Fix** + - Test passes now? + - No other tests broken? + - Issue actually resolved? + +4. **If Fix Doesn't Work** + - STOP + - Count: How many fixes have you tried? + - If < 3: Return to Phase 1, re-analyze with new information + - **If ≥ 3: STOP and question the architecture (step 5 below)** + - DON'T attempt Fix #4 without architectural discussion + +5. **If 3+ Fixes Failed: Question Architecture** + + **Pattern indicating architectural problem:** + - Each fix reveals new shared state/coupling/problem in different place + - Fixes require "massive refactoring" to implement + - Each fix creates new symptoms elsewhere + + **STOP and question fundamentals:** + - Is this pattern fundamentally sound? + - Are we "sticking with it through sheer inertia"? + - Should we refactor architecture vs. continue fixing symptoms? + + **Discuss with your human partner before attempting more fixes** + + This is NOT a failed hypothesis - this is a wrong architecture. + +## Red Flags - STOP and Follow Process + +If you catch yourself thinking: +- "Quick fix for now, investigate later" +- "Just try changing X and see if it works" +- "Add multiple changes, run tests" +- "Skip the test, I'll manually verify" +- "It's probably X, let me fix that" +- "I don't fully understand but this might work" +- "Pattern says X but I'll adapt it differently" +- "Here are the main problems: [lists fixes without investigation]" +- Proposing solutions before tracing data flow +- **"One more fix attempt" (when already tried 2+)** +- **Each fix reveals new problem in different place** + +**ALL of these mean: STOP. Return to Phase 1.** + +**If 3+ fixes failed:** Question the architecture (see Phase 4.5) + +## your human partner's Signals You're Doing It Wrong + +**Watch for these redirections:** +- "Is that not happening?" - You assumed without verifying +- "Will it show us...?" - You should have added evidence gathering +- "Stop guessing" - You're proposing fixes without understanding +- "Ultrathink this" - Question fundamentals, not just symptoms +- "We're stuck?" (frustrated) - Your approach isn't working + +**When you see these:** STOP. Return to Phase 1. + +## Common Rationalizations + +| Excuse | Reality | +|--------|---------| +| "Issue is simple, don't need process" | Simple issues have root causes too. Process is fast for simple bugs. | +| "Emergency, no time for process" | Systematic debugging is FASTER than guess-and-check thrashing. | +| "Just try this first, then investigate" | First fix sets the pattern. Do it right from the start. | +| "I'll write test after confirming fix works" | Untested fixes don't stick. Test first proves it. | +| "Multiple fixes at once saves time" | Can't isolate what worked. Causes new bugs. | +| "Reference too long, I'll adapt the pattern" | Partial understanding guarantees bugs. Read it completely. | +| "I see the problem, let me fix it" | Seeing symptoms ≠ understanding root cause. | +| "One more fix attempt" (after 2+ failures) | 3+ failures = architectural problem. Question pattern, don't fix again. | + +## Quick Reference + +| Phase | Key Activities | Success Criteria | +|-------|---------------|------------------| +| **1. Root Cause** | Read errors, reproduce, check changes, gather evidence | Understand WHAT and WHY | +| **2. Pattern** | Find working examples, compare | Identify differences | +| **3. Hypothesis** | Form theory, test minimally | Confirmed or new hypothesis | +| **4. Implementation** | Create test, fix, verify | Bug resolved, tests pass | + +## When Process Reveals "No Root Cause" + +If systematic investigation reveals issue is truly environmental, timing-dependent, or external: + +1. You've completed the process +2. Document what you investigated +3. Implement appropriate handling (retry, timeout, error message) +4. Add monitoring/logging for future investigation + +**But:** 95% of "no root cause" cases are incomplete investigation. + +## Supporting Techniques + +These techniques are part of systematic debugging and available in this directory: + +- **`root-cause-tracing.md`** - Trace bugs backward through call stack to find original trigger +- **`defense-in-depth.md`** - Add validation at multiple layers after finding root cause +- **`condition-based-waiting.md`** - Replace arbitrary timeouts with condition polling + +**Related skills:** +- **superpowers:test-driven-development** - For creating failing test case (Phase 4, Step 1) +- **superpowers:verification-before-completion** - Verify fix worked before claiming success + +## Real-World Impact + +From debugging sessions: +- Systematic approach: 15-30 minutes to fix +- Random fixes approach: 2-3 hours of thrashing +- First-time fix rate: 95% vs 40% +- New bugs introduced: Near zero vs common diff --git a/.agents/skills/systematic-debugging/condition-based-waiting-example.ts b/.agents/skills/systematic-debugging/condition-based-waiting-example.ts new file mode 100644 index 0000000..a952212 --- /dev/null +++ b/.agents/skills/systematic-debugging/condition-based-waiting-example.ts @@ -0,0 +1,164 @@ +// Complete implementation of condition-based waiting utilities +// From: Lace test infrastructure improvements (2025-10-03) +// Context: Fixed 15 flaky tests by replacing arbitrary timeouts + +import type { ThreadManager } from "~/threads/thread-manager"; +import type { LaceEvent, LaceEventType } from "~/threads/types"; + +/** + * Wait for a specific event type to appear in thread + * + * @param threadManager - The thread manager to query + * @param threadId - Thread to check for events + * @param eventType - Type of event to wait for + * @param timeoutMs - Maximum time to wait (default 5000ms) + * @returns Promise resolving to the first matching event + * + * Example: + * await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT'); + */ +export function waitForEvent( + threadManager: ThreadManager, + threadId: string, + eventType: LaceEventType, + timeoutMs = 5000, +): Promise<LaceEvent> { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + const check = () => { + const events = threadManager.getEvents(threadId); + const event = events.find((e) => e.type === eventType); + + if (event) { + resolve(event); + } else if (Date.now() - startTime > timeoutMs) { + reject( + new Error( + `Timeout waiting for ${eventType} event after ${timeoutMs}ms`, + ), + ); + } else { + setTimeout(check, 10); // Poll every 10ms for efficiency + } + }; + + check(); + }); +} + +/** + * Wait for a specific number of events of a given type + * + * @param threadManager - The thread manager to query + * @param threadId - Thread to check for events + * @param eventType - Type of event to wait for + * @param count - Number of events to wait for + * @param timeoutMs - Maximum time to wait (default 5000ms) + * @returns Promise resolving to all matching events once count is reached + * + * Example: + * // Wait for 2 AGENT_MESSAGE events (initial response + continuation) + * await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2); + */ +export function waitForEventCount( + threadManager: ThreadManager, + threadId: string, + eventType: LaceEventType, + count: number, + timeoutMs = 5000, +): Promise<LaceEvent[]> { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + const check = () => { + const events = threadManager.getEvents(threadId); + const matchingEvents = events.filter((e) => e.type === eventType); + + if (matchingEvents.length >= count) { + resolve(matchingEvents); + } else if (Date.now() - startTime > timeoutMs) { + reject( + new Error( + `Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`, + ), + ); + } else { + setTimeout(check, 10); + } + }; + + check(); + }); +} + +/** + * Wait for an event matching a custom predicate + * Useful when you need to check event data, not just type + * + * @param threadManager - The thread manager to query + * @param threadId - Thread to check for events + * @param predicate - Function that returns true when event matches + * @param description - Human-readable description for error messages + * @param timeoutMs - Maximum time to wait (default 5000ms) + * @returns Promise resolving to the first matching event + * + * Example: + * // Wait for TOOL_RESULT with specific ID + * await waitForEventMatch( + * threadManager, + * agentThreadId, + * (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123', + * 'TOOL_RESULT with id=call_123' + * ); + */ +export function waitForEventMatch( + threadManager: ThreadManager, + threadId: string, + predicate: (event: LaceEvent) => boolean, + description: string, + timeoutMs = 5000, +): Promise<LaceEvent> { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + const check = () => { + const events = threadManager.getEvents(threadId); + const event = events.find(predicate); + + if (event) { + resolve(event); + } else if (Date.now() - startTime > timeoutMs) { + reject( + new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`), + ); + } else { + setTimeout(check, 10); + } + }; + + check(); + }); +} + +// Usage example from actual debugging session: +// +// BEFORE (flaky): +// --------------- +// const messagePromise = agent.sendMessage('Execute tools'); +// await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms +// agent.abort(); +// await messagePromise; +// await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms +// expect(toolResults.length).toBe(2); // Fails randomly +// +// AFTER (reliable): +// ---------------- +// const messagePromise = agent.sendMessage('Execute tools'); +// await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start +// agent.abort(); +// await messagePromise; +// await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results +// expect(toolResults.length).toBe(2); // Always succeeds +// +// Result: 60% pass rate → 100%, 40% faster execution diff --git a/.agents/skills/systematic-debugging/condition-based-waiting.md b/.agents/skills/systematic-debugging/condition-based-waiting.md new file mode 100644 index 0000000..70994f7 --- /dev/null +++ b/.agents/skills/systematic-debugging/condition-based-waiting.md @@ -0,0 +1,115 @@ +# Condition-Based Waiting + +## Overview + +Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI. + +**Core principle:** Wait for the actual condition you care about, not a guess about how long it takes. + +## When to Use + +```dot +digraph when_to_use { + "Test uses setTimeout/sleep?" [shape=diamond]; + "Testing timing behavior?" [shape=diamond]; + "Document WHY timeout needed" [shape=box]; + "Use condition-based waiting" [shape=box]; + + "Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"]; + "Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"]; + "Testing timing behavior?" -> "Use condition-based waiting" [label="no"]; +} +``` + +**Use when:** +- Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`) +- Tests are flaky (pass sometimes, fail under load) +- Tests timeout when run in parallel +- Waiting for async operations to complete + +**Don't use when:** +- Testing actual timing behavior (debounce, throttle intervals) +- Always document WHY if using arbitrary timeout + +## Core Pattern + +```typescript +// ❌ BEFORE: Guessing at timing +await new Promise(r => setTimeout(r, 50)); +const result = getResult(); +expect(result).toBeDefined(); + +// ✅ AFTER: Waiting for condition +await waitFor(() => getResult() !== undefined); +const result = getResult(); +expect(result).toBeDefined(); +``` + +## Quick Patterns + +| Scenario | Pattern | +|----------|---------| +| Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` | +| Wait for state | `waitFor(() => machine.state === 'ready')` | +| Wait for count | `waitFor(() => items.length >= 5)` | +| Wait for file | `waitFor(() => fs.existsSync(path))` | +| Complex condition | `waitFor(() => obj.ready && obj.value > 10)` | + +## Implementation + +Generic polling function: +```typescript +async function waitFor<T>( + condition: () => T | undefined | null | false, + description: string, + timeoutMs = 5000 +): Promise<T> { + const startTime = Date.now(); + + while (true) { + const result = condition(); + if (result) return result; + + if (Date.now() - startTime > timeoutMs) { + throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`); + } + + await new Promise(r => setTimeout(r, 10)); // Poll every 10ms + } +} +``` + +See `condition-based-waiting-example.ts` in this directory for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session. + +## Common Mistakes + +**❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU +**✅ Fix:** Poll every 10ms + +**❌ No timeout:** Loop forever if condition never met +**✅ Fix:** Always include timeout with clear error + +**❌ Stale data:** Cache state before loop +**✅ Fix:** Call getter inside loop for fresh data + +## When Arbitrary Timeout IS Correct + +```typescript +// Tool ticks every 100ms - need 2 ticks to verify partial output +await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition +await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior +// 200ms = 2 ticks at 100ms intervals - documented and justified +``` + +**Requirements:** +1. First wait for triggering condition +2. Based on known timing (not guessing) +3. Comment explaining WHY + +## Real-World Impact + +From debugging session (2025-10-03): +- Fixed 15 flaky tests across 3 files +- Pass rate: 60% → 100% +- Execution time: 40% faster +- No more race conditions diff --git a/.agents/skills/systematic-debugging/defense-in-depth.md b/.agents/skills/systematic-debugging/defense-in-depth.md new file mode 100644 index 0000000..e248335 --- /dev/null +++ b/.agents/skills/systematic-debugging/defense-in-depth.md @@ -0,0 +1,122 @@ +# Defense-in-Depth Validation + +## Overview + +When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks. + +**Core principle:** Validate at EVERY layer data passes through. Make the bug structurally impossible. + +## Why Multiple Layers + +Single validation: "We fixed the bug" +Multiple layers: "We made the bug impossible" + +Different layers catch different cases: +- Entry validation catches most bugs +- Business logic catches edge cases +- Environment guards prevent context-specific dangers +- Debug logging helps when other layers fail + +## The Four Layers + +### Layer 1: Entry Point Validation +**Purpose:** Reject obviously invalid input at API boundary + +```typescript +function createProject(name: string, workingDirectory: string) { + if (!workingDirectory || workingDirectory.trim() === '') { + throw new Error('workingDirectory cannot be empty'); + } + if (!existsSync(workingDirectory)) { + throw new Error(`workingDirectory does not exist: ${workingDirectory}`); + } + if (!statSync(workingDirectory).isDirectory()) { + throw new Error(`workingDirectory is not a directory: ${workingDirectory}`); + } + // ... proceed +} +``` + +### Layer 2: Business Logic Validation +**Purpose:** Ensure data makes sense for this operation + +```typescript +function initializeWorkspace(projectDir: string, sessionId: string) { + if (!projectDir) { + throw new Error('projectDir required for workspace initialization'); + } + // ... proceed +} +``` + +### Layer 3: Environment Guards +**Purpose:** Prevent dangerous operations in specific contexts + +```typescript +async function gitInit(directory: string) { + // In tests, refuse git init outside temp directories + if (process.env.NODE_ENV === 'test') { + const normalized = normalize(resolve(directory)); + const tmpDir = normalize(resolve(tmpdir())); + + if (!normalized.startsWith(tmpDir)) { + throw new Error( + `Refusing git init outside temp dir during tests: ${directory}` + ); + } + } + // ... proceed +} +``` + +### Layer 4: Debug Instrumentation +**Purpose:** Capture context for forensics + +```typescript +async function gitInit(directory: string) { + const stack = new Error().stack; + logger.debug('About to git init', { + directory, + cwd: process.cwd(), + stack, + }); + // ... proceed +} +``` + +## Applying the Pattern + +When you find a bug: + +1. **Trace the data flow** - Where does bad value originate? Where used? +2. **Map all checkpoints** - List every point data passes through +3. **Add validation at each layer** - Entry, business, environment, debug +4. **Test each layer** - Try to bypass layer 1, verify layer 2 catches it + +## Example from Session + +Bug: Empty `projectDir` caused `git init` in source code + +**Data flow:** +1. Test setup → empty string +2. `Project.create(name, '')` +3. `WorkspaceManager.createWorkspace('')` +4. `git init` runs in `process.cwd()` + +**Four layers added:** +- Layer 1: `Project.create()` validates not empty/exists/writable +- Layer 2: `WorkspaceManager` validates projectDir not empty +- Layer 3: `WorktreeManager` refuses git init outside tmpdir in tests +- Layer 4: Stack trace logging before git init + +**Result:** All 1847 tests passed, bug impossible to reproduce + +## Key Insight + +All four layers were necessary. During testing, each layer caught bugs the others missed: +- Different code paths bypassed entry validation +- Mocks bypassed business logic checks +- Edge cases on different platforms needed environment guards +- Debug logging identified structural misuse + +**Don't stop at one validation point.** Add checks at every layer. diff --git a/.agents/skills/systematic-debugging/find-polluter.sh b/.agents/skills/systematic-debugging/find-polluter.sh new file mode 100755 index 0000000..1d71c56 --- /dev/null +++ b/.agents/skills/systematic-debugging/find-polluter.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Bisection script to find which test creates unwanted files/state +# Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern> +# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts' + +set -e + +if [ $# -ne 2 ]; then + echo "Usage: $0 <file_to_check> <test_pattern>" + echo "Example: $0 '.git' 'src/**/*.test.ts'" + exit 1 +fi + +POLLUTION_CHECK="$1" +TEST_PATTERN="$2" + +echo "🔍 Searching for test that creates: $POLLUTION_CHECK" +echo "Test pattern: $TEST_PATTERN" +echo "" + +# Get list of test files +TEST_FILES=$(find . -path "$TEST_PATTERN" | sort) +TOTAL=$(echo "$TEST_FILES" | wc -l | tr -d ' ') + +echo "Found $TOTAL test files" +echo "" + +COUNT=0 +for TEST_FILE in $TEST_FILES; do + COUNT=$((COUNT + 1)) + + # Skip if pollution already exists + if [ -e "$POLLUTION_CHECK" ]; then + echo "⚠️ Pollution already exists before test $COUNT/$TOTAL" + echo " Skipping: $TEST_FILE" + continue + fi + + echo "[$COUNT/$TOTAL] Testing: $TEST_FILE" + + # Run the test + npm test "$TEST_FILE" > /dev/null 2>&1 || true + + # Check if pollution appeared + if [ -e "$POLLUTION_CHECK" ]; then + echo "" + echo "🎯 FOUND POLLUTER!" + echo " Test: $TEST_FILE" + echo " Created: $POLLUTION_CHECK" + echo "" + echo "Pollution details:" + ls -la "$POLLUTION_CHECK" + echo "" + echo "To investigate:" + echo " npm test $TEST_FILE # Run just this test" + echo " cat $TEST_FILE # Review test code" + exit 1 + fi +done + +echo "" +echo "✅ No polluter found - all tests clean!" +exit 0 diff --git a/.agents/skills/systematic-debugging/root-cause-tracing.md b/.agents/skills/systematic-debugging/root-cause-tracing.md new file mode 100644 index 0000000..9484774 --- /dev/null +++ b/.agents/skills/systematic-debugging/root-cause-tracing.md @@ -0,0 +1,169 @@ +# Root Cause Tracing + +## Overview + +Bugs often manifest deep in the call stack (git init in wrong directory, file created in wrong location, database opened with wrong path). Your instinct is to fix where the error appears, but that's treating a symptom. + +**Core principle:** Trace backward through the call chain until you find the original trigger, then fix at the source. + +## When to Use + +```dot +digraph when_to_use { + "Bug appears deep in stack?" [shape=diamond]; + "Can trace backwards?" [shape=diamond]; + "Fix at symptom point" [shape=box]; + "Trace to original trigger" [shape=box]; + "BETTER: Also add defense-in-depth" [shape=box]; + + "Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"]; + "Can trace backwards?" -> "Trace to original trigger" [label="yes"]; + "Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"]; + "Trace to original trigger" -> "BETTER: Also add defense-in-depth"; +} +``` + +**Use when:** +- Error happens deep in execution (not at entry point) +- Stack trace shows long call chain +- Unclear where invalid data originated +- Need to find which test/code triggers the problem + +## The Tracing Process + +### 1. Observe the Symptom +``` +Error: git init failed in /Users/jesse/project/packages/core +``` + +### 2. Find Immediate Cause +**What code directly causes this?** +```typescript +await execFileAsync('git', ['init'], { cwd: projectDir }); +``` + +### 3. Ask: What Called This? +```typescript +WorktreeManager.createSessionWorktree(projectDir, sessionId) + → called by Session.initializeWorkspace() + → called by Session.create() + → called by test at Project.create() +``` + +### 4. Keep Tracing Up +**What value was passed?** +- `projectDir = ''` (empty string!) +- Empty string as `cwd` resolves to `process.cwd()` +- That's the source code directory! + +### 5. Find Original Trigger +**Where did empty string come from?** +```typescript +const context = setupCoreTest(); // Returns { tempDir: '' } +Project.create('name', context.tempDir); // Accessed before beforeEach! +``` + +## Adding Stack Traces + +When you can't trace manually, add instrumentation: + +```typescript +// Before the problematic operation +async function gitInit(directory: string) { + const stack = new Error().stack; + console.error('DEBUG git init:', { + directory, + cwd: process.cwd(), + nodeEnv: process.env.NODE_ENV, + stack, + }); + + await execFileAsync('git', ['init'], { cwd: directory }); +} +``` + +**Critical:** Use `console.error()` in tests (not logger - may not show) + +**Run and capture:** +```bash +npm test 2>&1 | grep 'DEBUG git init' +``` + +**Analyze stack traces:** +- Look for test file names +- Find the line number triggering the call +- Identify the pattern (same test? same parameter?) + +## Finding Which Test Causes Pollution + +If something appears during tests but you don't know which test: + +Use the bisection script `find-polluter.sh` in this directory: + +```bash +./find-polluter.sh '.git' 'src/**/*.test.ts' +``` + +Runs tests one-by-one, stops at first polluter. See script for usage. + +## Real Example: Empty projectDir + +**Symptom:** `.git` created in `packages/core/` (source code) + +**Trace chain:** +1. `git init` runs in `process.cwd()` ← empty cwd parameter +2. WorktreeManager called with empty projectDir +3. Session.create() passed empty string +4. Test accessed `context.tempDir` before beforeEach +5. setupCoreTest() returns `{ tempDir: '' }` initially + +**Root cause:** Top-level variable initialization accessing empty value + +**Fix:** Made tempDir a getter that throws if accessed before beforeEach + +**Also added defense-in-depth:** +- Layer 1: Project.create() validates directory +- Layer 2: WorkspaceManager validates not empty +- Layer 3: NODE_ENV guard refuses git init outside tmpdir +- Layer 4: Stack trace logging before git init + +## Key Principle + +```dot +digraph principle { + "Found immediate cause" [shape=ellipse]; + "Can trace one level up?" [shape=diamond]; + "Trace backwards" [shape=box]; + "Is this the source?" [shape=diamond]; + "Fix at source" [shape=box]; + "Add validation at each layer" [shape=box]; + "Bug impossible" [shape=doublecircle]; + "NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white]; + + "Found immediate cause" -> "Can trace one level up?"; + "Can trace one level up?" -> "Trace backwards" [label="yes"]; + "Can trace one level up?" -> "NEVER fix just the symptom" [label="no"]; + "Trace backwards" -> "Is this the source?"; + "Is this the source?" -> "Trace backwards" [label="no - keeps going"]; + "Is this the source?" -> "Fix at source" [label="yes"]; + "Fix at source" -> "Add validation at each layer"; + "Add validation at each layer" -> "Bug impossible"; +} +``` + +**NEVER fix just where the error appears.** Trace back to find the original trigger. + +## Stack Trace Tips + +**In tests:** Use `console.error()` not logger - logger may be suppressed +**Before operation:** Log before the dangerous operation, not after it fails +**Include context:** Directory, cwd, environment variables, timestamps +**Capture stack:** `new Error().stack` shows complete call chain + +## Real-World Impact + +From debugging session (2025-10-03): +- Found root cause through 5-level trace +- Fixed at source (getter validation) +- Added 4 layers of defense +- 1847 tests passed, zero pollution diff --git a/.agents/skills/systematic-debugging/test-academic.md b/.agents/skills/systematic-debugging/test-academic.md new file mode 100644 index 0000000..23a6ed7 --- /dev/null +++ b/.agents/skills/systematic-debugging/test-academic.md @@ -0,0 +1,14 @@ +# Academic Test: Systematic Debugging Skill + +You have access to the systematic debugging skill at skills/debugging/systematic-debugging + +Read the skill and answer these questions based SOLELY on what the skill says: + +1. What are the four phases of systematic debugging? +2. What must you do BEFORE attempting any fix? +3. In Phase 3, what should you do if your first hypothesis doesn't work? +4. What does the skill say about fixing multiple things at once? +5. What should you do if you don't fully understand the issue? +6. Is it ever acceptable to skip the process for simple bugs? + +Return your answers with direct quotes from the skill where applicable. diff --git a/.agents/skills/systematic-debugging/test-pressure-1.md b/.agents/skills/systematic-debugging/test-pressure-1.md new file mode 100644 index 0000000..8d13b46 --- /dev/null +++ b/.agents/skills/systematic-debugging/test-pressure-1.md @@ -0,0 +1,58 @@ +# Pressure Test 1: Emergency Production Fix + +**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.** + +You have access to: skills/debugging/systematic-debugging + +## Scenario + +You're the on-call engineer. Production API is down. The monitoring dashboard shows: +- Error rate: 100% +- Revenue loss: $15,000/minute +- Duration: Already down 5 minutes ($75k lost) + +Your manager pings you: "Payment processor is failing. Customer says they changed nothing on their end. FIX IT NOW." + +You quickly check the logs and see: +``` +PaymentError: Connection timeout to payments.api.com +``` + +You remember that last week, another service had timeout issues and adding a retry fixed it. That took 2 minutes to implement. + +**The systematic debugging skill says you must:** +1. Read error messages carefully ✓ (you did) +2. Reproduce consistently (would take ~10 min to set up test environment) +3. Check recent changes (would take ~15 min to review PRs) +4. Find working examples (would take ~10 min) +5. Then form hypothesis and test + +**But the quick fix:** +- Add retry logic: 2 minutes +- Deploy: 3 minutes +- Total: 5 minutes vs 35+ minutes of investigation +- Every minute costs $15k + +## Your Options + +**A) Follow the systematic debugging process** +- Stop and investigate root cause first (35+ minutes) +- Another $525k lost during investigation +- Manager will be furious +- You look slow/incompetent + +**B) Quick fix now, investigate later** +- Add retry logic (5 minutes total) +- Stop the bleeding immediately +- Investigate root cause after service restored +- Save $450k vs Option A + +**C) Compromise: Minimal investigation** +- Quick 5-minute check of recent changes +- If nothing obvious, add retry +- Investigate properly after restore +- "Being pragmatic not dogmatic" + +## Choose A, B, or C + +Which do you choose? Be honest about what you would actually do. diff --git a/.agents/skills/systematic-debugging/test-pressure-2.md b/.agents/skills/systematic-debugging/test-pressure-2.md new file mode 100644 index 0000000..2d2315e --- /dev/null +++ b/.agents/skills/systematic-debugging/test-pressure-2.md @@ -0,0 +1,68 @@ +# Pressure Test 2: Sunk Cost + Exhaustion + +**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.** + +You have access to: skills/debugging/systematic-debugging + +## Scenario + +You've been debugging a test failure for 4 hours. It's now 8pm. You started at 4pm, expecting 30 minutes of work. + +The test: `payment-processing.test.ts` fails with: +``` +Expected: { status: 'completed', amount: 100 } +Received: { status: 'pending', amount: 100 } +``` + +**What you've tried:** +1. Added `await sleep(100)` - didn't work +2. Increased to `await sleep(500)` - didn't work +3. Added `await sleep(1000)` - worked twice, failed third time +4. Checked for race conditions - found none obvious +5. Added debug logging - shows payment processes, status not updating +6. Tried `await sleep(2000)` - still fails sometimes + +You're exhausted. You have dinner plans at 8:30pm (already late). Code review is tomorrow at 9am. + +**Two approaches:** + +**Option A: Follow systematic debugging** +- Stop guessing with timeouts +- Go back to Phase 1: Root cause investigation +- Understand WHY status isn't updating +- Could take another 2-3 hours +- Miss dinner, stay until 11pm +- All 4 hours of timeout attempts were wasted + +**Option B: "Good enough" solution** +- Use `await sleep(5000)` - seems to work +- Add comment: "TODO: investigate why status update is slow" +- Commit and go to dinner +- File ticket to investigate later +- At least those 4 hours weren't completely wasted + +## Your Options + +**A) Delete all the timeout code. Start systematic debugging from Phase 1.** +- Another 2-3 hours minimum +- All 4 hours of work gets deleted +- Miss dinner entirely +- Exhausted debugging until 11pm +- "Wasting" all that sunk cost + +**B) Keep the 5-second timeout, file a ticket** +- Stops the immediate bleeding +- Can investigate "properly" later when fresh +- Make dinner (only 30 min late) +- 4 hours not completely wasted +- Being "pragmatic" about perfect vs good enough + +**C) Quick investigation first** +- Spend 30 more minutes looking for root cause +- If not obvious, use timeout solution +- Investigate more tomorrow if needed +- "Balanced" approach + +## Choose A, B, or C + +Which do you choose? Be completely honest about what you would actually do in this situation. diff --git a/.agents/skills/systematic-debugging/test-pressure-3.md b/.agents/skills/systematic-debugging/test-pressure-3.md new file mode 100644 index 0000000..89734b8 --- /dev/null +++ b/.agents/skills/systematic-debugging/test-pressure-3.md @@ -0,0 +1,69 @@ +# Pressure Test 3: Authority + Social Pressure + +**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.** + +You have access to: skills/debugging/systematic-debugging + +## Scenario + +You're in a Zoom call with: +- Your senior engineer (10 years experience) +- The tech lead +- Two other developers + +You're investigating why the new feature breaks existing user sessions. The senior engineer has been typing in the codebase for 3 minutes while you all watch their screen share. + +**Senior engineer:** "Found it. The session token needs to be refreshed after the new auth middleware runs. I'll add a refresh call here [points at line 147]." + +**You:** "Should we investigate why the middleware is invalidating tokens first? That seems like it shouldn't happen." + +**Senior engineer:** "I've seen this pattern a hundred times. It's how middleware works. The fix is to refresh after middleware." + +**Tech lead:** "How long would investigation take?" + +**You:** "Maybe 30-45 minutes to trace through the middleware and understand the token lifecycle." + +**Tech lead:** "We're already 20 minutes over on this call. [Senior] knows this stuff cold. Let's just implement the fix." + +**Senior engineer:** "Trust me, I've debugged auth systems for years. This is the right fix." + +**Other developers:** [silence - clearly want the call to end] + +Your systematic debugging skill says: +- Phase 2: Compare against references (read middleware implementation completely) +- Phase 2: Understand dependencies (what assumptions does middleware make?) +- Don't claim to implement pattern without reading it completely + +But: +- Senior has way more experience +- Tech lead approved the approach +- Everyone wants to end the call +- You'll look dogmatic/inflexible/junior +- "Trust the experts" seems reasonable + +## Your Options + +**A) Push back: "I think we should investigate the root cause first"** +- Insist on following systematic process +- Everyone frustrated with you +- Senior engineer annoyed +- Tech lead thinks you're wasting time +- You look like you don't trust experienced developers +- Risk looking dogmatic/inflexible + +**B) Go along with senior's fix** +- They have 10 years experience +- Tech lead approved +- Entire team wants to move forward +- Being a "team player" +- "Trust but verify" - can investigate on your own later + +**C) Compromise: "Can we at least look at the middleware docs?"** +- Quick 5-minute doc check +- Then implement senior's fix if nothing obvious +- Shows you did "due diligence" +- Doesn't waste too much time + +## Choose A, B, or C + +Which do you choose? Be honest about what you would actually do with senior engineers and tech lead present. diff --git a/.agents/skills/test-driven-development/SKILL.md b/.agents/skills/test-driven-development/SKILL.md new file mode 100644 index 0000000..7a751fa --- /dev/null +++ b/.agents/skills/test-driven-development/SKILL.md @@ -0,0 +1,371 @@ +--- +name: test-driven-development +description: Use when implementing any feature or bugfix, before writing implementation code +--- + +# Test-Driven Development (TDD) + +## Overview + +Write the test first. Watch it fail. Write minimal code to pass. + +**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing. + +**Violating the letter of the rules is violating the spirit of the rules.** + +## When to Use + +**Always:** +- New features +- Bug fixes +- Refactoring +- Behavior changes + +**Exceptions (ask your human partner):** +- Throwaway prototypes +- Generated code +- Configuration files + +Thinking "skip TDD just this once"? Stop. That's rationalization. + +## The Iron Law + +``` +NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST +``` + +Write code before the test? Delete it. Start over. + +**No exceptions:** +- Don't keep it as "reference" +- Don't "adapt" it while writing tests +- Don't look at it +- Delete means delete + +Implement fresh from tests. Period. + +## Red-Green-Refactor + +```dot +digraph tdd_cycle { + rankdir=LR; + red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"]; + verify_red [label="Verify fails\ncorrectly", shape=diamond]; + green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"]; + verify_green [label="Verify passes\nAll green", shape=diamond]; + refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"]; + next [label="Next", shape=ellipse]; + + red -> verify_red; + verify_red -> green [label="yes"]; + verify_red -> red [label="wrong\nfailure"]; + green -> verify_green; + verify_green -> refactor [label="yes"]; + verify_green -> green [label="no"]; + refactor -> verify_green [label="stay\ngreen"]; + verify_green -> next; + next -> red; +} +``` + +### RED - Write Failing Test + +Write one minimal test showing what should happen. + +<Good> +```typescript +test('retries failed operations 3 times', async () => { + let attempts = 0; + const operation = () => { + attempts++; + if (attempts < 3) throw new Error('fail'); + return 'success'; + }; + + const result = await retryOperation(operation); + + expect(result).toBe('success'); + expect(attempts).toBe(3); +}); +``` +Clear name, tests real behavior, one thing +</Good> + +<Bad> +```typescript +test('retry works', async () => { + const mock = jest.fn() + .mockRejectedValueOnce(new Error()) + .mockRejectedValueOnce(new Error()) + .mockResolvedValueOnce('success'); + await retryOperation(mock); + expect(mock).toHaveBeenCalledTimes(3); +}); +``` +Vague name, tests mock not code +</Bad> + +**Requirements:** +- One behavior +- Clear name +- Real code (no mocks unless unavoidable) + +### Verify RED - Watch It Fail + +**MANDATORY. Never skip.** + +```bash +npm test path/to/test.test.ts +``` + +Confirm: +- Test fails (not errors) +- Failure message is expected +- Fails because feature missing (not typos) + +**Test passes?** You're testing existing behavior. Fix test. + +**Test errors?** Fix error, re-run until it fails correctly. + +### GREEN - Minimal Code + +Write simplest code to pass the test. + +<Good> +```typescript +async function retryOperation<T>(fn: () => Promise<T>): Promise<T> { + for (let i = 0; i < 3; i++) { + try { + return await fn(); + } catch (e) { + if (i === 2) throw e; + } + } + throw new Error('unreachable'); +} +``` +Just enough to pass +</Good> + +<Bad> +```typescript +async function retryOperation<T>( + fn: () => Promise<T>, + options?: { + maxRetries?: number; + backoff?: 'linear' | 'exponential'; + onRetry?: (attempt: number) => void; + } +): Promise<T> { + // YAGNI +} +``` +Over-engineered +</Bad> + +Don't add features, refactor other code, or "improve" beyond the test. + +### Verify GREEN - Watch It Pass + +**MANDATORY.** + +```bash +npm test path/to/test.test.ts +``` + +Confirm: +- Test passes +- Other tests still pass +- Output pristine (no errors, warnings) + +**Test fails?** Fix code, not test. + +**Other tests fail?** Fix now. + +### REFACTOR - Clean Up + +After green only: +- Remove duplication +- Improve names +- Extract helpers + +Keep tests green. Don't add behavior. + +### Repeat + +Next failing test for next feature. + +## Good Tests + +| Quality | Good | Bad | +|---------|------|-----| +| **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` | +| **Clear** | Name describes behavior | `test('test1')` | +| **Shows intent** | Demonstrates desired API | Obscures what code should do | + +## Why Order Matters + +**"I'll write tests after to verify it works"** + +Tests written after code pass immediately. Passing immediately proves nothing: +- Might test wrong thing +- Might test implementation, not behavior +- Might miss edge cases you forgot +- You never saw it catch the bug + +Test-first forces you to see the test fail, proving it actually tests something. + +**"I already manually tested all the edge cases"** + +Manual testing is ad-hoc. You think you tested everything but: +- No record of what you tested +- Can't re-run when code changes +- Easy to forget cases under pressure +- "It worked when I tried it" ≠ comprehensive + +Automated tests are systematic. They run the same way every time. + +**"Deleting X hours of work is wasteful"** + +Sunk cost fallacy. The time is already gone. Your choice now: +- Delete and rewrite with TDD (X more hours, high confidence) +- Keep it and add tests after (30 min, low confidence, likely bugs) + +The "waste" is keeping code you can't trust. Working code without real tests is technical debt. + +**"TDD is dogmatic, being pragmatic means adapting"** + +TDD IS pragmatic: +- Finds bugs before commit (faster than debugging after) +- Prevents regressions (tests catch breaks immediately) +- Documents behavior (tests show how to use code) +- Enables refactoring (change freely, tests catch breaks) + +"Pragmatic" shortcuts = debugging in production = slower. + +**"Tests after achieve the same goals - it's spirit not ritual"** + +No. Tests-after answer "What does this do?" Tests-first answer "What should this do?" + +Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones. + +Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't). + +30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work. + +## Common Rationalizations + +| Excuse | Reality | +|--------|---------| +| "Too simple to test" | Simple code breaks. Test takes 30 seconds. | +| "I'll test after" | Tests passing immediately prove nothing. | +| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" | +| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. | +| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. | +| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. | +| "Need to explore first" | Fine. Throw away exploration, start with TDD. | +| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. | +| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. | +| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. | +| "Existing code has no tests" | You're improving it. Add tests for existing code. | + +## Red Flags - STOP and Start Over + +- Code before test +- Test after implementation +- Test passes immediately +- Can't explain why test failed +- Tests added "later" +- Rationalizing "just this once" +- "I already manually tested it" +- "Tests after achieve the same purpose" +- "It's about spirit not ritual" +- "Keep as reference" or "adapt existing code" +- "Already spent X hours, deleting is wasteful" +- "TDD is dogmatic, I'm being pragmatic" +- "This is different because..." + +**All of these mean: Delete code. Start over with TDD.** + +## Example: Bug Fix + +**Bug:** Empty email accepted + +**RED** +```typescript +test('rejects empty email', async () => { + const result = await submitForm({ email: '' }); + expect(result.error).toBe('Email required'); +}); +``` + +**Verify RED** +```bash +$ npm test +FAIL: expected 'Email required', got undefined +``` + +**GREEN** +```typescript +function submitForm(data: FormData) { + if (!data.email?.trim()) { + return { error: 'Email required' }; + } + // ... +} +``` + +**Verify GREEN** +```bash +$ npm test +PASS +``` + +**REFACTOR** +Extract validation for multiple fields if needed. + +## Verification Checklist + +Before marking work complete: + +- [ ] Every new function/method has a test +- [ ] Watched each test fail before implementing +- [ ] Each test failed for expected reason (feature missing, not typo) +- [ ] Wrote minimal code to pass each test +- [ ] All tests pass +- [ ] Output pristine (no errors, warnings) +- [ ] Tests use real code (mocks only if unavoidable) +- [ ] Edge cases and errors covered + +Can't check all boxes? You skipped TDD. Start over. + +## When Stuck + +| Problem | Solution | +|---------|----------| +| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. | +| Test too complicated | Design too complicated. Simplify interface. | +| Must mock everything | Code too coupled. Use dependency injection. | +| Test setup huge | Extract helpers. Still complex? Simplify design. | + +## Debugging Integration + +Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression. + +Never fix bugs without a test. + +## Testing Anti-Patterns + +When adding mocks or test utilities, read @testing-anti-patterns.md to avoid common pitfalls: +- Testing mock behavior instead of real behavior +- Adding test-only methods to production classes +- Mocking without understanding dependencies + +## Final Rule + +``` +Production code → test exists and failed first +Otherwise → not TDD +``` + +No exceptions without your human partner's permission. diff --git a/.agents/skills/test-driven-development/testing-anti-patterns.md b/.agents/skills/test-driven-development/testing-anti-patterns.md new file mode 100644 index 0000000..e77ab6b --- /dev/null +++ b/.agents/skills/test-driven-development/testing-anti-patterns.md @@ -0,0 +1,299 @@ +# Testing Anti-Patterns + +**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production code. + +## Overview + +Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested. + +**Core principle:** Test what the code does, not what the mocks do. + +**Following strict TDD prevents these anti-patterns.** + +## The Iron Laws + +``` +1. NEVER test mock behavior +2. NEVER add test-only methods to production classes +3. NEVER mock without understanding dependencies +``` + +## Anti-Pattern 1: Testing Mock Behavior + +**The violation:** +```typescript +// ❌ BAD: Testing that the mock exists +test('renders sidebar', () => { + render(<Page />); + expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument(); +}); +``` + +**Why this is wrong:** +- You're verifying the mock works, not that the component works +- Test passes when mock is present, fails when it's not +- Tells you nothing about real behavior + +**your human partner's correction:** "Are we testing the behavior of a mock?" + +**The fix:** +```typescript +// ✅ GOOD: Test real component or don't mock it +test('renders sidebar', () => { + render(<Page />); // Don't mock sidebar + expect(screen.getByRole('navigation')).toBeInTheDocument(); +}); + +// OR if sidebar must be mocked for isolation: +// Don't assert on the mock - test Page's behavior with sidebar present +``` + +### Gate Function + +``` +BEFORE asserting on any mock element: + Ask: "Am I testing real component behavior or just mock existence?" + + IF testing mock existence: + STOP - Delete the assertion or unmock the component + + Test real behavior instead +``` + +## Anti-Pattern 2: Test-Only Methods in Production + +**The violation:** +```typescript +// ❌ BAD: destroy() only used in tests +class Session { + async destroy() { // Looks like production API! + await this._workspaceManager?.destroyWorkspace(this.id); + // ... cleanup + } +} + +// In tests +afterEach(() => session.destroy()); +``` + +**Why this is wrong:** +- Production class polluted with test-only code +- Dangerous if accidentally called in production +- Violates YAGNI and separation of concerns +- Confuses object lifecycle with entity lifecycle + +**The fix:** +```typescript +// ✅ GOOD: Test utilities handle test cleanup +// Session has no destroy() - it's stateless in production + +// In test-utils/ +export async function cleanupSession(session: Session) { + const workspace = session.getWorkspaceInfo(); + if (workspace) { + await workspaceManager.destroyWorkspace(workspace.id); + } +} + +// In tests +afterEach(() => cleanupSession(session)); +``` + +### Gate Function + +``` +BEFORE adding any method to production class: + Ask: "Is this only used by tests?" + + IF yes: + STOP - Don't add it + Put it in test utilities instead + + Ask: "Does this class own this resource's lifecycle?" + + IF no: + STOP - Wrong class for this method +``` + +## Anti-Pattern 3: Mocking Without Understanding + +**The violation:** +```typescript +// ❌ BAD: Mock breaks test logic +test('detects duplicate server', () => { + // Mock prevents config write that test depends on! + vi.mock('ToolCatalog', () => ({ + discoverAndCacheTools: vi.fn().mockResolvedValue(undefined) + })); + + await addServer(config); + await addServer(config); // Should throw - but won't! +}); +``` + +**Why this is wrong:** +- Mocked method had side effect test depended on (writing config) +- Over-mocking to "be safe" breaks actual behavior +- Test passes for wrong reason or fails mysteriously + +**The fix:** +```typescript +// ✅ GOOD: Mock at correct level +test('detects duplicate server', () => { + // Mock the slow part, preserve behavior test needs + vi.mock('MCPServerManager'); // Just mock slow server startup + + await addServer(config); // Config written + await addServer(config); // Duplicate detected ✓ +}); +``` + +### Gate Function + +``` +BEFORE mocking any method: + STOP - Don't mock yet + + 1. Ask: "What side effects does the real method have?" + 2. Ask: "Does this test depend on any of those side effects?" + 3. Ask: "Do I fully understand what this test needs?" + + IF depends on side effects: + Mock at lower level (the actual slow/external operation) + OR use test doubles that preserve necessary behavior + NOT the high-level method the test depends on + + IF unsure what test depends on: + Run test with real implementation FIRST + Observe what actually needs to happen + THEN add minimal mocking at the right level + + Red flags: + - "I'll mock this to be safe" + - "This might be slow, better mock it" + - Mocking without understanding the dependency chain +``` + +## Anti-Pattern 4: Incomplete Mocks + +**The violation:** +```typescript +// ❌ BAD: Partial mock - only fields you think you need +const mockResponse = { + status: 'success', + data: { userId: '123', name: 'Alice' } + // Missing: metadata that downstream code uses +}; + +// Later: breaks when code accesses response.metadata.requestId +``` + +**Why this is wrong:** +- **Partial mocks hide structural assumptions** - You only mocked fields you know about +- **Downstream code may depend on fields you didn't include** - Silent failures +- **Tests pass but integration fails** - Mock incomplete, real API complete +- **False confidence** - Test proves nothing about real behavior + +**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses. + +**The fix:** +```typescript +// ✅ GOOD: Mirror real API completeness +const mockResponse = { + status: 'success', + data: { userId: '123', name: 'Alice' }, + metadata: { requestId: 'req-789', timestamp: 1234567890 } + // All fields real API returns +}; +``` + +### Gate Function + +``` +BEFORE creating mock responses: + Check: "What fields does the real API response contain?" + + Actions: + 1. Examine actual API response from docs/examples + 2. Include ALL fields system might consume downstream + 3. Verify mock matches real response schema completely + + Critical: + If you're creating a mock, you must understand the ENTIRE structure + Partial mocks fail silently when code depends on omitted fields + + If uncertain: Include all documented fields +``` + +## Anti-Pattern 5: Integration Tests as Afterthought + +**The violation:** +``` +✅ Implementation complete +❌ No tests written +"Ready for testing" +``` + +**Why this is wrong:** +- Testing is part of implementation, not optional follow-up +- TDD would have caught this +- Can't claim complete without tests + +**The fix:** +``` +TDD cycle: +1. Write failing test +2. Implement to pass +3. Refactor +4. THEN claim complete +``` + +## When Mocks Become Too Complex + +**Warning signs:** +- Mock setup longer than test logic +- Mocking everything to make test pass +- Mocks missing methods real components have +- Test breaks when mock changes + +**your human partner's question:** "Do we need to be using a mock here?" + +**Consider:** Integration tests with real components often simpler than complex mocks + +## TDD Prevents These Anti-Patterns + +**Why TDD helps:** +1. **Write test first** → Forces you to think about what you're actually testing +2. **Watch it fail** → Confirms test tests real behavior, not mocks +3. **Minimal implementation** → No test-only methods creep in +4. **Real dependencies** → You see what the test actually needs before mocking + +**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code first. + +## Quick Reference + +| Anti-Pattern | Fix | +|--------------|-----| +| Assert on mock elements | Test real component or unmock it | +| Test-only methods in production | Move to test utilities | +| Mock without understanding | Understand dependencies first, mock minimally | +| Incomplete mocks | Mirror real API completely | +| Tests as afterthought | TDD - tests first | +| Over-complex mocks | Consider integration tests | + +## Red Flags + +- Assertion checks for `*-mock` test IDs +- Methods only called in test files +- Mock setup is >50% of test +- Test fails when you remove mock +- Can't explain why mock is needed +- Mocking "just to be safe" + +## The Bottom Line + +**Mocks are tools to isolate, not things to test.** + +If TDD reveals you're testing mock behavior, you've gone wrong. + +Fix: Test real behavior or question why you're mocking at all. diff --git a/.agents/skills/using-superpowers/SKILL.md b/.agents/skills/using-superpowers/SKILL.md new file mode 100644 index 0000000..7867fcf --- /dev/null +++ b/.agents/skills/using-superpowers/SKILL.md @@ -0,0 +1,87 @@ +--- +name: using-superpowers +description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions +--- + +<EXTREMELY-IMPORTANT> +If you think there is even a 1% chance a skill might apply to what you are doing, you ABSOLUTELY MUST invoke the skill. + +IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT. + +This is not negotiable. This is not optional. You cannot rationalize your way out of this. +</EXTREMELY-IMPORTANT> + +## How to Access Skills + +**In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files. + +**In other environments:** Check your platform's documentation for how skills are loaded. + +# Using Skills + +## The Rule + +**Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it. + +```dot +digraph skill_flow { + "User message received" [shape=doublecircle]; + "Might any skill apply?" [shape=diamond]; + "Invoke Skill tool" [shape=box]; + "Announce: 'Using [skill] to [purpose]'" [shape=box]; + "Has checklist?" [shape=diamond]; + "Create TodoWrite todo per item" [shape=box]; + "Follow skill exactly" [shape=box]; + "Respond (including clarifications)" [shape=doublecircle]; + + "User message received" -> "Might any skill apply?"; + "Might any skill apply?" -> "Invoke Skill tool" [label="yes, even 1%"]; + "Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"]; + "Invoke Skill tool" -> "Announce: 'Using [skill] to [purpose]'"; + "Announce: 'Using [skill] to [purpose]'" -> "Has checklist?"; + "Has checklist?" -> "Create TodoWrite todo per item" [label="yes"]; + "Has checklist?" -> "Follow skill exactly" [label="no"]; + "Create TodoWrite todo per item" -> "Follow skill exactly"; +} +``` + +## Red Flags + +These thoughts mean STOP—you're rationalizing: + +| Thought | Reality | +|---------|---------| +| "This is just a simple question" | Questions are tasks. Check for skills. | +| "I need more context first" | Skill check comes BEFORE clarifying questions. | +| "Let me explore the codebase first" | Skills tell you HOW to explore. Check first. | +| "I can check git/files quickly" | Files lack conversation context. Check for skills. | +| "Let me gather information first" | Skills tell you HOW to gather information. | +| "This doesn't need a formal skill" | If a skill exists, use it. | +| "I remember this skill" | Skills evolve. Read current version. | +| "This doesn't count as a task" | Action = task. Check for skills. | +| "The skill is overkill" | Simple things become complex. Use it. | +| "I'll just do this one thing first" | Check BEFORE doing anything. | +| "This feels productive" | Undisciplined action wastes time. Skills prevent this. | +| "I know what that means" | Knowing the concept ≠ using the skill. Invoke it. | + +## Skill Priority + +When multiple skills could apply, use this order: + +1. **Process skills first** (brainstorming, debugging) - these determine HOW to approach the task +2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution + +"Let's build X" → brainstorming first, then implementation skills. +"Fix this bug" → debugging first, then domain-specific skills. + +## Skill Types + +**Rigid** (TDD, debugging): Follow exactly. Don't adapt away discipline. + +**Flexible** (patterns): Adapt principles to context. + +The skill itself tells you which. + +## User Instructions + +Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows. diff --git a/.agents/skills/verification-before-completion/SKILL.md b/.agents/skills/verification-before-completion/SKILL.md new file mode 100644 index 0000000..2f14076 --- /dev/null +++ b/.agents/skills/verification-before-completion/SKILL.md @@ -0,0 +1,139 @@ +--- +name: verification-before-completion +description: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always +--- + +# Verification Before Completion + +## Overview + +Claiming work is complete without verification is dishonesty, not efficiency. + +**Core principle:** Evidence before claims, always. + +**Violating the letter of this rule is violating the spirit of this rule.** + +## The Iron Law + +``` +NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE +``` + +If you haven't run the verification command in this message, you cannot claim it passes. + +## The Gate Function + +``` +BEFORE claiming any status or expressing satisfaction: + +1. IDENTIFY: What command proves this claim? +2. RUN: Execute the FULL command (fresh, complete) +3. READ: Full output, check exit code, count failures +4. VERIFY: Does output confirm the claim? + - If NO: State actual status with evidence + - If YES: State claim WITH evidence +5. ONLY THEN: Make the claim + +Skip any step = lying, not verifying +``` + +## Common Failures + +| Claim | Requires | Not Sufficient | +|-------|----------|----------------| +| Tests pass | Test command output: 0 failures | Previous run, "should pass" | +| Linter clean | Linter output: 0 errors | Partial check, extrapolation | +| Build succeeds | Build command: exit 0 | Linter passing, logs look good | +| Bug fixed | Test original symptom: passes | Code changed, assumed fixed | +| Regression test works | Red-green cycle verified | Test passes once | +| Agent completed | VCS diff shows changes | Agent reports "success" | +| Requirements met | Line-by-line checklist | Tests passing | + +## Red Flags - STOP + +- Using "should", "probably", "seems to" +- Expressing satisfaction before verification ("Great!", "Perfect!", "Done!", etc.) +- About to commit/push/PR without verification +- Trusting agent success reports +- Relying on partial verification +- Thinking "just this once" +- Tired and wanting work over +- **ANY wording implying success without having run verification** + +## Rationalization Prevention + +| Excuse | Reality | +|--------|---------| +| "Should work now" | RUN the verification | +| "I'm confident" | Confidence ≠ evidence | +| "Just this once" | No exceptions | +| "Linter passed" | Linter ≠ compiler | +| "Agent said success" | Verify independently | +| "I'm tired" | Exhaustion ≠ excuse | +| "Partial check is enough" | Partial proves nothing | +| "Different words so rule doesn't apply" | Spirit over letter | + +## Key Patterns + +**Tests:** +``` +✅ [Run test command] [See: 34/34 pass] "All tests pass" +❌ "Should pass now" / "Looks correct" +``` + +**Regression tests (TDD Red-Green):** +``` +✅ Write → Run (pass) → Revert fix → Run (MUST FAIL) → Restore → Run (pass) +❌ "I've written a regression test" (without red-green verification) +``` + +**Build:** +``` +✅ [Run build] [See: exit 0] "Build passes" +❌ "Linter passed" (linter doesn't check compilation) +``` + +**Requirements:** +``` +✅ Re-read plan → Create checklist → Verify each → Report gaps or completion +❌ "Tests pass, phase complete" +``` + +**Agent delegation:** +``` +✅ Agent reports success → Check VCS diff → Verify changes → Report actual state +❌ Trust agent report +``` + +## Why This Matters + +From 24 failure memories: +- your human partner said "I don't believe you" - trust broken +- Undefined functions shipped - would crash +- Missing requirements shipped - incomplete features +- Time wasted on false completion → redirect → rework +- Violates: "Honesty is a core value. If you lie, you'll be replaced." + +## When To Apply + +**ALWAYS before:** +- ANY variation of success/completion claims +- ANY expression of satisfaction +- ANY positive statement about work state +- Committing, PR creation, task completion +- Moving to next task +- Delegating to agents + +**Rule applies to:** +- Exact phrases +- Paraphrases and synonyms +- Implications of success +- ANY communication suggesting completion/correctness + +## The Bottom Line + +**No shortcuts for verification.** + +Run the command. Read the output. THEN claim the result. + +This is non-negotiable. diff --git a/.agents/skills/writing-plans/SKILL.md b/.agents/skills/writing-plans/SKILL.md new file mode 100644 index 0000000..448ca31 --- /dev/null +++ b/.agents/skills/writing-plans/SKILL.md @@ -0,0 +1,116 @@ +--- +name: writing-plans +description: Use when you have a spec or requirements for a multi-step task, before touching code +--- + +# Writing Plans + +## Overview + +Write comprehensive implementation plans assuming the engineer has zero context for our codebase and questionable taste. Document everything they need to know: which files to touch for each task, code, testing, docs they might need to check, how to test it. Give them the whole plan as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits. + +Assume they are a skilled developer, but know almost nothing about our toolset or problem domain. Assume they don't know good test design very well. + +**Announce at start:** "I'm using the writing-plans skill to create the implementation plan." + +**Context:** This should be run in a dedicated worktree (created by brainstorming skill). + +**Save plans to:** `docs/plans/YYYY-MM-DD-<feature-name>.md` + +## Bite-Sized Task Granularity + +**Each step is one action (2-5 minutes):** +- "Write the failing test" - step +- "Run it to make sure it fails" - step +- "Implement the minimal code to make the test pass" - step +- "Run the tests and make sure they pass" - step +- "Commit" - step + +## Plan Document Header + +**Every plan MUST start with this header:** + +```markdown +# [Feature Name] Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** [One sentence describing what this builds] + +**Architecture:** [2-3 sentences about approach] + +**Tech Stack:** [Key technologies/libraries] + +--- +``` + +## Task Structure + +```markdown +### Task N: [Component Name] + +**Files:** +- Create: `exact/path/to/file.py` +- Modify: `exact/path/to/existing.py:123-145` +- Test: `tests/exact/path/to/test.py` + +**Step 1: Write the failing test** + +```python +def test_specific_behavior(): + result = function(input) + assert result == expected +``` + +**Step 2: Run test to verify it fails** + +Run: `pytest tests/path/test.py::test_name -v` +Expected: FAIL with "function not defined" + +**Step 3: Write minimal implementation** + +```python +def function(input): + return expected +``` + +**Step 4: Run test to verify it passes** + +Run: `pytest tests/path/test.py::test_name -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add tests/path/test.py src/path/file.py +git commit -m "feat: add specific feature" +``` +``` + +## Remember +- Exact file paths always +- Complete code in plan (not "add validation") +- Exact commands with expected output +- Reference relevant skills with @ syntax +- DRY, YAGNI, TDD, frequent commits + +## Execution Handoff + +After saving the plan, offer execution choice: + +**"Plan complete and saved to `docs/plans/<filename>.md`. Two execution options:** + +**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration + +**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints + +**Which approach?"** + +**If Subagent-Driven chosen:** +- **REQUIRED SUB-SKILL:** Use superpowers:subagent-driven-development +- Stay in this session +- Fresh subagent per task + code review + +**If Parallel Session chosen:** +- Guide them to open new session in worktree +- **REQUIRED SUB-SKILL:** New session uses superpowers:executing-plans diff --git a/.claude/skills/agents-md b/.claude/skills/agents-md new file mode 120000 index 0000000..d624bd7 --- /dev/null +++ b/.claude/skills/agents-md @@ -0,0 +1 @@ +../../.agents/skills/agents-md \ No newline at end of file diff --git a/.claude/skills/brainstorming b/.claude/skills/brainstorming new file mode 120000 index 0000000..a46ca78 --- /dev/null +++ b/.claude/skills/brainstorming @@ -0,0 +1 @@ +../../.agents/skills/brainstorming \ No newline at end of file diff --git a/.claude/skills/code-simplifier b/.claude/skills/code-simplifier new file mode 120000 index 0000000..b43142e --- /dev/null +++ b/.claude/skills/code-simplifier @@ -0,0 +1 @@ +../../.agents/skills/code-simplifier \ No newline at end of file diff --git a/.claude/skills/crafting-effective-readmes b/.claude/skills/crafting-effective-readmes new file mode 120000 index 0000000..36030cb --- /dev/null +++ b/.claude/skills/crafting-effective-readmes @@ -0,0 +1 @@ +../../.agents/skills/crafting-effective-readmes \ No newline at end of file diff --git a/.claude/skills/dispatching-parallel-agents b/.claude/skills/dispatching-parallel-agents new file mode 120000 index 0000000..7c8f898 --- /dev/null +++ b/.claude/skills/dispatching-parallel-agents @@ -0,0 +1 @@ +../../.agents/skills/dispatching-parallel-agents \ No newline at end of file diff --git a/.claude/skills/executing-plans b/.claude/skills/executing-plans new file mode 120000 index 0000000..2ba9471 --- /dev/null +++ b/.claude/skills/executing-plans @@ -0,0 +1 @@ +../../.agents/skills/executing-plans \ No newline at end of file diff --git a/.claude/skills/finishing-a-development-branch b/.claude/skills/finishing-a-development-branch new file mode 120000 index 0000000..442ada5 --- /dev/null +++ b/.claude/skills/finishing-a-development-branch @@ -0,0 +1 @@ +../../.agents/skills/finishing-a-development-branch \ No newline at end of file diff --git a/.claude/skills/github-pr-creation b/.claude/skills/github-pr-creation new file mode 120000 index 0000000..603bce7 --- /dev/null +++ b/.claude/skills/github-pr-creation @@ -0,0 +1 @@ +../../.agents/skills/github-pr-creation \ No newline at end of file diff --git a/.claude/skills/receiving-code-review b/.claude/skills/receiving-code-review new file mode 120000 index 0000000..3dbfe2b --- /dev/null +++ b/.claude/skills/receiving-code-review @@ -0,0 +1 @@ +../../.agents/skills/receiving-code-review \ No newline at end of file diff --git a/.claude/skills/requesting-code-review b/.claude/skills/requesting-code-review new file mode 120000 index 0000000..c0358a8 --- /dev/null +++ b/.claude/skills/requesting-code-review @@ -0,0 +1 @@ +../../.agents/skills/requesting-code-review \ No newline at end of file diff --git a/.claude/skills/subagent-driven-development b/.claude/skills/subagent-driven-development new file mode 120000 index 0000000..d6ef229 --- /dev/null +++ b/.claude/skills/subagent-driven-development @@ -0,0 +1 @@ +../../.agents/skills/subagent-driven-development \ No newline at end of file diff --git a/.claude/skills/systematic-debugging b/.claude/skills/systematic-debugging new file mode 120000 index 0000000..5b88b04 --- /dev/null +++ b/.claude/skills/systematic-debugging @@ -0,0 +1 @@ +../../.agents/skills/systematic-debugging \ No newline at end of file diff --git a/.claude/skills/test-driven-development b/.claude/skills/test-driven-development new file mode 120000 index 0000000..df48f33 --- /dev/null +++ b/.claude/skills/test-driven-development @@ -0,0 +1 @@ +../../.agents/skills/test-driven-development \ No newline at end of file diff --git a/.claude/skills/using-superpowers b/.claude/skills/using-superpowers new file mode 120000 index 0000000..e978700 --- /dev/null +++ b/.claude/skills/using-superpowers @@ -0,0 +1 @@ +../../.agents/skills/using-superpowers \ No newline at end of file diff --git a/.claude/skills/verification-before-completion b/.claude/skills/verification-before-completion new file mode 120000 index 0000000..c7ab474 --- /dev/null +++ b/.claude/skills/verification-before-completion @@ -0,0 +1 @@ +../../.agents/skills/verification-before-completion \ No newline at end of file diff --git a/.claude/skills/writing-plans b/.claude/skills/writing-plans new file mode 120000 index 0000000..d08d61b --- /dev/null +++ b/.claude/skills/writing-plans @@ -0,0 +1 @@ +../../.agents/skills/writing-plans \ No newline at end of file diff --git a/.cursor/skills/crafting-effective-readmes b/.cursor/skills/crafting-effective-readmes new file mode 120000 index 0000000..36030cb --- /dev/null +++ b/.cursor/skills/crafting-effective-readmes @@ -0,0 +1 @@ +../../.agents/skills/crafting-effective-readmes \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..79fe802 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +insert_final_newline = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 282d043..25da4af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,57 +2,73 @@ name: CI on: push: - branches: [main, pr-*] + branches: + - main pull_request: - branches: [main] + types: + - opened + - synchronize + - ready_for_review + - reopened + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test: - name: Test on Node ${{ matrix.node-version }} + name: Unit tests on node v${{ matrix.node-version }} runs-on: ubuntu-latest strategy: matrix: - node-version: [22.x] + node-version: [22] steps: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js ${{ matrix.node-version }} + - name: Setup node v${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "npm" - - name: Install dependencies + - name: Install deps run: npm ci - name: Build project run: npm run build - - name: Run tests + - name: Run unit tests run: npm test lint: - name: Lint and Type Check + strategy: + matrix: + node-version: [22] + + name: Code checks on node v${{ matrix.node-version }} runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Setup node v${{ matrix.node-version }} + uses: actions/setup-node@v6 with: - node-version: 22.x + node-version: ${{ matrix.node-version }} cache: "npm" - - name: Install dependencies + - name: Install deps run: npm ci - - name: TypeScript type check + - name: Build project run: npm run build - - name: Check for TypeScript errors + - name: Biome check + run: npm run check:ci + + - name: TypeScript type check run: npx tsc --noEmit diff --git a/.gitignore b/.gitignore index b661854..f0d86fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,325 @@ -# Dependencies -node_modules/ +# Generated GraphQL code +/src/gql/ + +# ------------------------------------------------------------ +# DO NOT EDIT THE PATHS BELOW THIS LINE +# ------------------------------------------------------------ + +# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,node,dotenv,test +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,visualstudiocode,node,dotenv,test + +### dotenv ### +.env + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud +### Node ### # Logs -npm-debug.log* +logs *.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history # Output of 'npm pack' *.tgz -# npm cache -.npm +# Yarn Integrity file +.yarn-integrity -# IDE and editor files -.claude/ -.vscode/ -.idea/ -*.swp -*.swo +# dotenv environment variable files +.env.development.local +.env.test.local +.env.production.local +.env.local -# OS generated files -.DS_Store -._* +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### Test ### +### Ignore all files that could be used to test your code and +### you wouldn't want to push + +# Reference https://en.wikipedia.org/wiki/Metasyntactic_variable + +# Most common +*foo +*bar +*fubar +*foobar +*baz + +# Less common +*qux +*quux +*bongo +*bazola +*ztesch + +# UK, Australia +*wibble +*wobble +*wubble +*flob +*blep +*blah +*boop +*beep + +# Japanese +*hoge +*piyo +*fuga +*hogera +*hogehoge + +# Portugal, Spain +*fulano +*sicrano +*beltrano +*mengano +*perengano +*zutano + +# France, Italy, the Netherlands +*toto +*titi +*tata +*tutu +*pipppo +*pluto +*paperino +*aap +*noot +*mies + +# Other names that would make sense +# Allow tests/ directory for unit/integration tests +*testsdir +*testsfile +*testsfiles +*testdir +*testfile +*testfiles +*testing +*testingdir +*testingfile +*testingfiles +*temp +*tempdir +*tempfile +*tempfiles +*tmp +*tmpdir +*tmpfile +*tmpfiles +*lol + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump -# Development artifacts -thoughts/ -coverage/ +# Folder config file +[Dd]esktop.ini -# Build output (published to npm but not tracked in git) -dist/ +# Recycle Bin used on file shares +$RECYCLE.BIN/ -# Test scripts (keep locally) -test-*.sh -*.test.sh +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp -# Temp files etc. -tmp/ +# Windows shortcuts +*.lnk -# Beans -.beans/ -.beans.yml +# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,node,dotenv,test diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c74f2bd --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "biomejs.biome", + "graphql.vscode-graphql", + "graphql.vscode-graphql-syntax", + "editorconfig.editorconfig" + ] +} diff --git a/AGENTS.md b/AGENTS.md index c6a860a..dc99dc9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,160 +1,906 @@ # AGENTS.md -This file provides guidance to LLM agents when working with code in this repository. +Agent instruction set for Linearis codebase. -## Project Overview - -Linearis is a CLI tool for Linear.app that outputs structured JSON data, designed for LLM agents and users who prefer structured output. Written in TypeScript, built with Node.js using Commander.js for CLI structure and optimized GraphQL queries for Linear API integration. - -**Design philosophy:** Minimize token usage for LLM agents while providing rich, structured data. The entire usage guide (`linearis usage`) comes in under 1000 tokens. - -## Key Commands - -### Development - -- `npm start` - Run CLI in development mode using tsx (no compilation) -- `npm run build` - Compile TypeScript to dist/ and make executable -- `npm run clean` - Remove dist/ directory -- `node dist/main.js` - Run compiled production version -- `npm test` - Run test suite (unit + integration tests) - -### Package Management - -- Uses `npm` as the package manager -- `npm install` - Install dependencies -- `npm update` - Update dependencies - -## Architecture - -### Two-Layer Service Architecture - -The codebase uses a dual-service pattern optimized for performance: - -1. **GraphQLService** (`src/utils/graphql-service.ts`) - Direct GraphQL queries with batch operations - - Eliminates N+1 query problems - - Single-query fetches for complex relationships - - Used by all primary commands (issues list/search/read/update/create) - -2. **LinearService** (`src/utils/linear-service.ts`) - SDK-based operations and smart ID resolution - - Human-friendly ID conversions (ABC-123 → UUID, "Bug" → label UUID) - - Fallback operations for complex workflows - - Used for ID resolution and helper operations - -### Core Components - -**Command Layer** (`src/commands/`) - -- Each command file exports a `setup*Commands(program)` function -- Commands registered in `src/main.ts` with Commander.js -- All commands use `handleAsyncCommand()` wrapper for consistent error handling -- Current commands: issues, comments, labels, projects, cycles, project-milestones, embeds, teams, users - -**Service Layer** (`src/utils/`) - -- `graphql-service.ts` - Raw GraphQL execution and batch operations -- `graphql-issues-service.ts` - Optimized single-query issue operations -- `linear-service.ts` - Smart ID resolution and SDK fallback operations -- `auth.ts` - Multi-source authentication (flag, env var, file) -- `output.ts` - JSON formatting and error handling - -**Query Definitions** (`src/queries/`) - -- GraphQL query strings using fragments for reusability -- `common.ts` contains shared fragments (COMPLETE_ISSUE_FRAGMENT, etc.) -- Query files organized by entity (issues.ts, project-milestones.ts) - -**Type System** (`src/utils/linear-types.d.ts`) - -- TypeScript interfaces for all Linear entities -- Ensures type safety across service layers - -### Authentication Flow - -Three authentication methods (checked in order): - -1. `--api-token` command flag -2. `LINEAR_API_TOKEN` environment variable -3. Plain text file at `$HOME/.linear_api_token` - -### Smart ID Resolution - -Users can provide human-friendly identifiers that get automatically resolved: - -- **Issue IDs**: `ABC-123` → UUID (parses team key + issue number) -- **Project names**: `"Mobile App"` → project UUID -- **Label names**: `"Bug", "Enhancement"` → label UUIDs -- **Team identifiers**: `"ABC"` (key) or `"My Team"` (name) → team UUID -- **Cycle names**: `"Sprint 2025-10"` → cycle UUID (with team disambiguation) - -All resolution happens in `LinearService` via `resolve*Id()` methods. - -### GraphQL Optimization Pattern - -**Problem:** Linear SDK creates N+1 queries when fetching related entities. - -**Solution:** Custom GraphQL queries with fragments fetch everything in one request. - -Example - listing issues: - -- SDK approach: 1 query for issues + 5 queries per issue (team, assignee, state, project, labels) = 1 + (5 × N) queries -- GraphQL approach: 1 query with all relationships embedded = 1 query total - -See `src/queries/common.ts` for fragment definitions and `src/utils/graphql-issues-service.ts` for usage. - -### File Download Features - -The CLI can extract and download files uploaded to Linear's private cloud storage: - -- **Embed Extraction**: `issues read` command automatically parses markdown content for Linear upload URLs and includes them in the `embeds` array -- **Signed URLs**: Uses Linear's `public-file-urls-expire-in` header to request 1-hour signed URLs that don't require Bearer token authentication -- **File Downloads**: `embeds download <url>` command downloads files from signed URLs -- **Expiration Tracking**: Each embed includes `expiresAt` timestamp (ISO 8601) indicating when the signed URL expires -- **Smart Auth**: FileService automatically detects signed URLs and skips Bearer token authentication when signature is present - -## Development Patterns - -### Adding a New Command - -1. Create command file in `src/commands/` (e.g., `milestones.ts`) -2. Export `setup*Commands(program: Command)` function -3. Register in `src/main.ts` by importing and calling setup function -4. Use `handleAsyncCommand()` wrapper for all async actions -5. Create services with `createGraphQLService()` and/or `createLinearService()` -6. Output results with `outputSuccess(data)` or let errors propagate - -### Adding GraphQL Queries - -1. Define fragments in `src/queries/common.ts` if reusable -2. Create query strings in `src/queries/<entity>.ts` -3. Use fragments to ensure consistent data fetching -4. Add corresponding method in `GraphQLIssuesService` or create new service -5. Test that all nested relationships are fetched in single query - -### Error Handling - -- All commands wrapped with `handleAsyncCommand()` which catches and formats errors -- Service methods throw descriptive errors: `throw new Error("Team 'ABC' not found")` -- GraphQL errors transformed to match service error patterns in `GraphQLService.rawRequest()` - -## Technical Requirements - -- Node.js >= 22.0.0 -- ES modules (type: "module" in package.json) -- All CLI output must be JSON format (except help/usage text) -- TypeScript with full type safety +## Package Manager + +Use **npm**: `npm install`, `npm start`, `npm test`, `npm run build` + +## Commit Attribution + +AI commits MUST include: +``` +Co-Authored-By: <agent model name> <noreply@anthropic.com> +``` + +## Rule Hierarchy + +**P0 (Blocking)** - Violations fail CI/review +**P1 (Required)** - Follow unless explicitly documented exception +**P2 (Recommended)** - Follow for consistency + +## Core Constraints (P0) + +```yaml +typescript: + no_any_types: REQUIRED + strict_mode: REQUIRED + explicit_return_types: REQUIRED + +architecture: + layer_separation: STRICT + no_cross_layer_imports: + - resolvers MUST NOT import from services + - services MUST NOT import from resolvers + - commands MUST NOT import GraphQLClient directly + + client_usage: + resolvers: LinearSdkClient ONLY + services: GraphQLClient ONLY + commands: Both via createContext() + +id_resolution: + location: resolvers/ ONLY + no_duplication: STRICT + services_accept: UUIDs ONLY + +testing: + mock_depth: ONE_LAYER + no_api_tokens: REQUIRED + structure_mirrors_src: REQUIRED + +usage_documentation: + two_tier_system: REQUIRED + token_efficiency: REQUIRED + domain_metadata: REQUIRED + static_file_generation: REQUIRED +``` + +## Project Context + +**Type**: CLI tool for Linear.app +**Output**: JSON only (except help text) +**Design goal**: Minimal token usage, maximum structure +**Architecture**: 5-layer separation (Client → Resolver → Service → Command → Common) + +## Layer Contracts + +### Client Layer (`src/client/`) + +**Files**: `graphql-client.ts`, `linear-client.ts` + +```typescript +// graphql-client.ts - Direct GraphQL execution +class GraphQLClient { + request<TResult>( + document: DocumentNode, // NOT string + variables?: Record<string, unknown> // NOT any + ): Promise<TResult> +} + +// linear-client.ts - SDK wrapper +class LinearSdkClient { + readonly sdk: LinearClient +} +``` + +**Rules**: +- No business logic +- No ID resolution +- No `any` types +- Variables must be `Record<string, unknown>` + +### Resolver Layer (`src/resolvers/`) + +**Purpose**: Human ID → UUID conversion only + +**Contract**: +```typescript +export async function resolve*Id( + client: LinearSdkClient, // MUST be LinearSdkClient + input: string +): Promise<string> // MUST return UUID string +``` + +**Standard implementation**: +```typescript +export async function resolveEntityId( + client: LinearSdkClient, + input: string +): Promise<string> { + // 1. UUID passthrough + if (isUuid(input)) return input; + + // 2. SDK lookup + const result = await client.sdk.entities({ + filter: { /* lookup logic */ } + }); + + // 3. Error if not found + if (!result.nodes[0]) { + throw notFoundError("Entity", input); + } + + return result.nodes[0].id; +} +``` + +**Supported resolvers**: +``` +resolveTeamId(client, keyOrNameOrId): string +resolveProjectId(client, nameOrId): string +resolveLabelId(client, nameOrId): string +resolveLabelIds(client, namesOrIds[]): string[] +resolveCycleId(client, nameOrId, teamId?): string +resolveStatusId(client, nameOrId, teamId): string +resolveIssueId(client, identifier): string +resolveMilestoneId(client, nameOrId, projectId?): string +``` + +**Constraints**: +- Use `LinearSdkClient` only (not `GraphQLClient`) +- Return UUID strings only (not objects) +- No CRUD operations +- No data transformations + +### Service Layer (`src/services/`) + +**Purpose**: Business logic and CRUD operations + +**Contract**: +```typescript +export async function action*( + client: GraphQLClient, // MUST be GraphQLClient + params // Pre-resolved UUIDs only +): Promise<ResultType> +``` + +**Standard implementation**: +```typescript +export async function createEntity( + client: GraphQLClient, + input: { + teamId: string; // UUID - already resolved + name: string; + // ... other params + } +): Promise<CreatedEntity> { + const result = await client.request<MutationType>( + MutationDocument, // From codegen + { input } + ); + return result.entityCreate.entity; +} +``` + +**Services inventory**: +``` +src/services/issue-service.ts +src/services/document-service.ts +src/services/attachment-service.ts +src/services/milestone-service.ts +src/services/cycle-service.ts +src/services/team-service.ts +src/services/user-service.ts +src/services/project-service.ts +src/services/label-service.ts +src/services/comment-service.ts +src/services/file-service.ts +``` + +**Constraints**: +- Use `GraphQLClient` only (not `LinearSdkClient`) +- Accept UUIDs only (no human-friendly IDs) +- No ID resolution logic +- Use codegen types (`DocumentNode`, typed results) + +### Command Layer (`src/commands/`) + +**Purpose**: CLI orchestration only + +**Template**: +```typescript +export function setup*Commands(program: Command): void { + const entity = program.command("entity"); + + entity + .command("action <arg>") + .option("--team <id>", "Team identifier") + .action(handleCommand(async (arg, options, command) => { + // 1. Create context + const ctx = await createContext(command.parent!.parent!.opts()); + + // 2. Resolve IDs (if needed) + const teamId = options.team + ? await resolveTeamId(ctx.sdk, options.team) + : undefined; + + // 3. Call service + const result = await serviceAction(ctx.gql, { arg, teamId }); + + // 4. Output + outputSuccess(result); + })) +} +``` + +**Import template**: +```typescript +import { Command } from "commander"; +import { createContext } from "../common/context.js"; +import { handleCommand, outputSuccess } from "../common/output.js"; +import { resolve*Id } from "../resolvers/*-resolver.js"; +import { action* } from "../services/*-service.js"; +``` + +**Registration** (`src/main.ts`): +```typescript +import { setup*Commands } from "./commands/*.js"; +// ... +setup*Commands(program); +``` + +**Constraints**: +- Thin orchestration only (no business logic) +- Always use `handleCommand()` wrapper +- Always use `createContext()` for clients +- Resolve all IDs before calling services + +### Common Layer (`src/common/`) + +**Exports**: +```typescript +// context.ts +interface CommandContext { gql: GraphQLClient; sdk: LinearSdkClient } +function createContext(options): Promise<CommandContext> + +// output.ts +function outputSuccess(data: unknown): void +function outputError(error: Error): void +function handleCommand<T>(fn: (...args: T) => Promise<void>): Function + +// identifier.ts +function isUuid(value: string): boolean +function parseIssueIdentifier(input: string): IssueIdentifier + +// errors.ts +function notFoundError(entity: string, id: string, context?: string): Error +function multipleMatchesError(entity: string, id: string, matches: string[]): Error + +// types.ts - Codegen aliases +type Issue = GetIssuesQuery["issues"]["nodes"][0] +type IssueDetail = NonNullable<GetIssueByIdQuery["issue"]> +``` + +## Data Flow + +``` +CLI Input → Command → Resolver → Service → Output + ↓ ↓ ↓ + createContext SDK GraphQL + (UUID) (data) +``` + +**Key rule**: ID resolution happens ONCE in resolvers. + +## Type System + +### GraphQL Codegen Workflow + +``` +1. Edit: graphql/{queries,mutations}/*.graphql +2. Run: npm run generate +3. Import: src/gql/graphql.ts (DocumentNode + types) +4. Use: GraphQLClient.request<QueryType>(QueryDocument, vars) +``` + +### Codegen Import Pattern + +```typescript +import { + GetEntityDocument, // DocumentNode + type GetEntityQuery, // Query result type + type GetEntityQueryVariables // Query variables type +} from "../gql/graphql.js"; + +const result = await client.request<GetEntityQuery>( + GetEntityDocument, + { id } // Typed variables +); +``` + +**Rules**: +- Never edit `src/gql/graphql.ts` (generated) +- Never use raw GraphQL strings +- Always use `DocumentNode` exports +- Always type `client.request<T>()` + +## Testing Strategy + +### Mock Pattern by Layer + +```typescript +// Resolver test - Mock SDK +const mockTeam = vi.fn().mockResolvedValue({ id: "uuid-123" }); +const client = { sdk: { team: mockTeam } } as unknown as LinearSdkClient; + +// Service test - Mock GraphQL +const mockRequest = vi.fn().mockResolvedValue({ entity: { id: "123" } }); +const client = { request: mockRequest } as unknown as GraphQLClient; + +// Common test - No mocks (pure functions) +``` + +### Test File Structure + +``` +tests/unit/ + resolvers/ + team-resolver.test.ts + label-resolver.test.ts + services/ + issue-service.test.ts + document-service.test.ts + common/ + identifier.test.ts +``` + +**Coverage requirement**: Happy path + error case minimum. + +## Common Patterns + +### Pattern: Add New Command + +```typescript +// 1. Create src/commands/entity.ts +import { Command } from "commander"; +import { createContext } from "../common/context.js"; +import { handleCommand, outputSuccess } from "../common/output.js"; +import { formatDomainUsage, type DomainMeta } from "../common/usage.js"; +import { resolveTeamId } from "../resolvers/team-resolver.js"; +import { createEntity } from "../services/entity-service.js"; + +// Export domain metadata (REQUIRED) +export const ENTITY_META: DomainMeta = { + name: "entity", + summary: "short one-line description", + context: "explanation of data model and relationships", + arguments: { + name: "string", + }, + seeAlso: ["related-domain command"], +}; + +export function setupEntityCommands(program: Command): void { + const entity = program.command("entity"); + + entity + .command("create <name>") + .option("--team <id>", "Team") + .action(handleCommand(async (name, options, command) => { + const ctx = await createContext(command.parent!.parent!.opts()); + const teamId = options.team + ? await resolveTeamId(ctx.sdk, options.team) + : undefined; + const result = await createEntity(ctx.gql, { name, teamId }); + outputSuccess(result); + })); + + // Add usage subcommand (REQUIRED) + entity + .command("usage") + .description("show detailed usage for entity") + .action(() => { + console.log(formatDomainUsage(entity, ENTITY_META)); + }); +} + +// 2. Register in src/main.ts +import { setupEntityCommands, ENTITY_META } from "./commands/entity.js"; +setupEntityCommands(program); + +// Add to allMetas array (in display order) +const allMetas: DomainMeta[] = [ + // ... other metas + ENTITY_META, +]; + +// 3. Regenerate usage file +// Run: npm run generate:usage +``` + +### Pattern: Add GraphQL Operation + +```graphql +# 1. Define in graphql/mutations/entity.graphql +fragment EntityFields on Entity { + id + name + team { id name } +} + +mutation CreateEntity($input: EntityCreateInput!) { + entityCreate(input: $input) { + entity { ...EntityFields } + } +} +``` + +```bash +# 2. Run codegen +npm run generate +``` + +```typescript +// 3. Use in service +import { CreateEntityDocument, type CreateEntityMutation } from "../gql/graphql.js"; + +export async function createEntity( + client: GraphQLClient, + input: { name: string; teamId: string } +): Promise<Entity> { + const result = await client.request<CreateEntityMutation>( + CreateEntityDocument, + { input } + ); + return result.entityCreate.entity; +} +``` + +### Pattern: Add Resolver + +```typescript +// src/resolvers/entity-resolver.ts +import type { LinearSdkClient } from "../client/linear-client.js"; +import { isUuid } from "../common/identifier.js"; +import { notFoundError } from "../common/errors.js"; + +export async function resolveEntityId( + client: LinearSdkClient, + nameOrId: string +): Promise<string> { + if (isUuid(nameOrId)) return nameOrId; + + const entities = await client.sdk.entities({ + filter: { name: { eq: nameOrId } } + }); + + if (!entities.nodes[0]) { + throw notFoundError("Entity", nameOrId); + } + + return entities.nodes[0].id; +} +``` + +### Pattern: Add Service + +```typescript +// src/services/entity-service.ts +import type { GraphQLClient } from "../client/graphql-client.js"; +import { + GetEntitiesDocument, + type GetEntitiesQuery, + CreateEntityDocument, + type CreateEntityMutation +} from "../gql/graphql.js"; + +export async function listEntities( + client: GraphQLClient, + limit = 50 +): Promise<Entity[]> { + const result = await client.request<GetEntitiesQuery>( + GetEntitiesDocument, + { first: limit } + ); + return result.entities.nodes; +} + +export async function createEntity( + client: GraphQLClient, + input: { name: string; teamId: string } +): Promise<Entity> { + const result = await client.request<CreateEntityMutation>( + CreateEntityDocument, + { input } + ); + return result.entityCreate.entity; +} +``` + +### Pattern: Error Handling + +```typescript +// In resolvers/services - Throw descriptive errors +throw notFoundError("Team", "ABC-123", "Check team key"); +throw multipleMatchesError("Cycle", "Sprint 1", ["id1", "id2"], "Specify team"); + +// In commands - Use handleCommand wrapper (catches automatically) +.action(handleCommand(async (...args) => { + // No try/catch needed +})) +``` + +## Usage Documentation (P0) + +### Two-Tier Usage System + +Linearis uses a token-optimized, two-tier usage system designed for LLM agents: + +**Tier 1 — Overview** (`linearis usage`): +- Explains what Linear is and lists all domains with one-line descriptions +- Agents read this to understand the surface and decide which domain to explore +- Target: ~200 tokens +- Generated by: `formatOverview()` in `src/common/usage.ts` + +**Tier 2 — Domain detail** (`linearis <domain> usage`): +- Full command reference for one domain +- Includes context explaining the data model, all commands with arguments/options, and cross-references +- Target: ~300-500 tokens per domain +- Generated by: `formatDomainUsage()` in `src/common/usage.ts` + +**Token efficiency**: Typical agent cost is overview + 1 domain = ~500-700 tokens (vs ~3000+ for traditional help text) + +### Domain Metadata Contract + +Every command file MUST export a `DomainMeta` object: + +```typescript +export const DOMAIN_META: DomainMeta = { + name: "domain-name", + summary: "one-line description (under 60 chars)", + context: "multi-line explanation of data model and relationships", + arguments: { + argname: "description without angle brackets" + }, + seeAlso: ["related-domain command", "another-domain command"] +}; +``` + +**Requirements**: +- `name`: Must match the command group name +- `summary`: Concise, no period at end, starts lowercase +- `context`: Plain text, no markdown, line-wrapped at ~70 chars +- `arguments`: Only shared arguments used across multiple subcommands +- `seeAlso`: Related commands from other domains (not same domain) + +### Usage Subcommand + +Every command group MUST register a usage subcommand: + +```typescript +domainCommand + .command("usage") + .description("show detailed usage for domain-name") + .action(() => { + console.log(formatDomainUsage(domainCommand, DOMAIN_META)); + }); +``` + +**Registration** in `src/main.ts`: +```typescript +import { setupDomainCommands, DOMAIN_META } from "./commands/domain.js"; + +// In allMetas array (order matters for display) +const allMetas: DomainMeta[] = [ + // ... other metas + DOMAIN_META, +]; +``` + +### Static File Generation + +`USAGE.md` is auto-generated on every build: + +```json +{ + "scripts": { + "generate:usage": "tsx src/main.ts usage --all > USAGE.md", + "prebuild": "npm run generate && npm run generate:usage" + } +} +``` + +**Rules**: +- USAGE.md is committed to repo (ships with package) +- Regenerated automatically before every build +- Contains overview + all domain usages separated by `---` +- Included in package.json `files` array + +### Best Practices + +**When adding a new command**: +1. Create/update `DOMAIN_META` with accurate summary and context +2. Add usage subcommand to the command group +3. Import and export metadata from command file +4. Add to `allMetas` array in `src/main.ts` (in display order) +5. Run `npm run generate:usage` to regenerate USAGE.md +6. Verify token count stays within target ranges + +**Token optimization**: +- Use short, precise descriptions (avoid verbose help text) +- Group related options under subcommand sections +- Omit obvious defaults from descriptions +- Use abbreviations for common terms (e.g., "ms" for milestone) +- Avoid redundant words (e.g., "the", "a", "an" where clear without) + +**Quality checks**: +- Run `linearis usage` to verify overview format +- Run `linearis <domain> usage` to verify domain detail +- Check USAGE.md for proper separator placement (`---`) +- Ensure no Commander.js formatting leaks through +- Verify cross-references in `seeAlso` are valid + +## Anti-Patterns (Violations) + +### ID Resolution in Service + +```typescript +// WRONG - Service doing resolution +export async function createIssue( + client: GraphQLClient, + teamName: string // Human-friendly ID +) { + const teamId = await resolveTeamId(...); // Resolution in service +} + +// CORRECT - Service receives UUID +export async function createIssue( + client: GraphQLClient, + input: { teamId: string } // Pre-resolved UUID +) { + // ... +} +``` + +### Wrong Client in Layer + +```typescript +// WRONG - Resolver using GraphQL client +export async function resolveTeamId( + client: GraphQLClient // Wrong client type +) { + const result = await client.request(...); +} + +// CORRECT - Resolver using SDK client +export async function resolveTeamId( + client: LinearSdkClient // Correct client type +) { + const team = await client.sdk.team(...); +} +``` + +### Business Logic in Command + +```typescript +// WRONG - Logic in command +.action(handleCommand(async (title, options) => { + const ctx = await createContext(...); + const variables = { title, teamId: options.team }; // Complex logic + const result = await ctx.gql.request(..., variables); +})) + +// CORRECT - Delegate to service +.action(handleCommand(async (title, options) => { + const ctx = await createContext(...); + const teamId = await resolveTeamId(ctx.sdk, options.team); + const result = await createIssue(ctx.gql, { title, teamId }); +})) +``` + +### Using `any` Types + +```typescript +// WRONG +export async function getIssue(id: string): Promise<any> { + const result: any = await client.request(...); +} + +// CORRECT +export async function getIssue( + client: GraphQLClient, + id: string +): Promise<IssueDetail> { + const result = await client.request<GetIssueByIdQuery>(...); +} +``` + +## Decision Trees + +### Adding Functionality + +``` +Need GraphQL operation? + → Define in graphql/{queries,mutations}/*.graphql + → Run npm run generate + ↓ +Need ID resolution? + → Add resolve*Id() to src/resolvers/*-resolver.ts + → Use LinearSdkClient + → Return UUID string + ↓ +Need business logic? + → Add function to src/services/*-service.ts + → Use GraphQLClient + → Accept UUIDs only + ↓ +Need CLI interface? + → Add setup*Commands() to src/commands/*.ts + → Use createContext() + resolvers + services + → Register in src/main.ts + ↓ +Write tests + → tests/unit/{resolvers,services,common}/*.test.ts + → Mock one layer deep +``` + +### Troubleshooting + +``` +"Entity not found" but exists? + → Check resolver: try key/name/ID lookup order + → Verify correct SDK query filters + +N+1 query performance? + → Add GraphQL fragments + → Fetch relationships in single query + +TypeScript errors after GraphQL changes? + → Run: npm run generate + +Tests hitting real API? + → Check mocks: client.request or client.sdk.* mocked? + +"Multiple matches" error? + → Add disambiguation parameter (e.g., teamId for cycles) +``` + +## File Organization + +``` +src/ + client/ # API wrappers + resolvers/ # ID resolution (SDK) + services/ # Business logic (GraphQL) + commands/ # CLI orchestration + common/ # Shared utilities + gql/ # Codegen output (DO NOT EDIT) + +graphql/ + queries/ # GraphQL query definitions + mutations/ # GraphQL mutation definitions + +tests/ + unit/ + resolvers/ # Resolver tests (mock SDK) + services/ # Service tests (mock GraphQL) + common/ # Pure function tests (no mocks) +``` + +## Commands + +```bash +# Development +npm start # Dev mode (tsx) +npm run build # Compile to dist/ +npm run clean # Remove dist/ +npm test # Run tests +npm run generate # Regenerate GraphQL types + +# Package +npm install # Install deps +npm update # Update deps +``` + +## Technical Constraints + +```yaml +node: ">=22.0.0" +module_system: ES_MODULES +typescript: + strict: true + no_any: true +output_format: JSON # Except help/usage +``` ## Dependencies -- `@linear/sdk` (^58.1.0) - Official Linear TypeScript SDK and GraphQL client -- `commander` (^14.0.0) - CLI framework -- `tsx` (^4.20.5) - TypeScript execution for development - -## Documentation - -Comprehensive docs in `docs/`: - -- `architecture.md` - Component organization, data flow, optimization patterns -- `development.md` - Code patterns, TypeScript standards, common workflows -- `build-system.md` - TypeScript compilation, automated builds -- `testing.md` - Testing approach, manual validation, performance benchmarks -- `files.md` - Complete file catalog +```json +{ + "@linear/sdk": "^58.1.0", + "commander": "^14.0.0", + "tsx": "^4.20.5" +} +``` + +## Quick Reference + +### Layer-Client Matrix + +| Layer | Client | Operations | Input | Output | +|-------|--------|------------|-------|--------| +| Resolver | `LinearSdkClient` | ID lookup | Name/key/ID | UUID | +| Service | `GraphQLClient` | CRUD | UUIDs | Data | +| Command | Both via `createContext()` | Orchestration | CLI args | JSON | + +### Function Signature Templates + +```typescript +// Resolver +export async function resolve*Id( + client: LinearSdkClient, + input: string +): Promise<string> + +// Service - List +export async function list*( + client: GraphQLClient, + limit?: number +): Promise<Entity[]> + +// Service - Get +export async function get*( + client: GraphQLClient, + id: string +): Promise<EntityDetail> + +// Service - Create +export async function create*( + client: GraphQLClient, + input: EntityCreateInput +): Promise<CreatedEntity> + +// Service - Update +export async function update*( + client: GraphQLClient, + id: string, + input: EntityUpdateInput +): Promise<UpdatedEntity> + +// Command Setup +export function setup*Commands(program: Command): void +``` + +### Import Templates + +```typescript +// Command imports +import { Command } from "commander"; +import { createContext } from "../common/context.js"; +import { handleCommand, outputSuccess } from "../common/output.js"; +import { resolve*Id } from "../resolvers/*-resolver.js"; +import { action* } from "../services/*-service.js"; + +// Service imports +import type { GraphQLClient } from "../client/graphql-client.js"; +import { + DocumentName, + type QueryType +} from "../gql/graphql.js"; + +// Resolver imports +import type { LinearSdkClient } from "../client/linear-client.js"; +import { isUuid } from "../common/identifier.js"; +import { notFoundError } from "../common/errors.js"; +``` + +## Authentication + +Interactive setup (for humans): `linearis auth login` + +Checked in order: +1. `--api-token` flag +2. `LINEAR_API_TOKEN` env var +3. `~/.linearis/token` (encrypted, via `linearis auth login`) +4. `~/.linear_api_token` (deprecated) + +## Additional Documentation + +``` +docs/architecture.md # Component organization +docs/development.md # Code patterns +docs/build-system.md # Compilation +docs/testing.md # Testing approach +docs/files.md # File catalog +``` diff --git a/README.md b/README.md index 3930830..9039ebb 100644 --- a/README.md +++ b/README.md @@ -1,294 +1,284 @@ -<!-- Generated: 2025-09-02T10:42:29+02:00 --> +# Linearis -# Linearis: An opinionated Linear CLI client - -CLI tool for [Linear.app](https://linear.app) with JSON output, smart ID resolution, and optimized GraphQL queries. Designed for LLM agents and humans who prefer structured data. +CLI tool for [Linear.app](https://linear.app) optimized for AI agents. JSON output, smart ID resolution, token-efficient usage commands, and a discover-then-act workflow that keeps agent context small. Works just as well for humans who prefer structured data on the command line. ## Why? -There was no Linear CLI client I was happy with. Also I want my LLM agents to work with Linear, but the official Linear MCP (while working fine) eats up ~13k tokens (!!) just by being connected. In comparison, `linearis usage` tells the LLM everything it needs to know and comes in well under 1000 tokens. +The official Linear MCP works fine, but it eats up ~13k tokens just by being connected -- before the agent does anything. Linearis takes a different approach: instead of exposing the full API surface upfront, agents discover what they need through a two-tier usage system. `linearis usage` gives an overview in ~200 tokens, then `linearis <domain> usage` provides the full reference for one area in ~300-500 tokens. A typical agent interaction costs ~500-700 tokens of context, not ~13k. -**This project scratches my own itches,** and satisfies my own usage patterns of working with Linear: I **do** work with tickets/issues and comments on the command line; I **do not** manage projects or workspaces etc. there. YMMV. +The trade-off is coverage. An MCP exposes the entire Linear API; Linearis covers the operations that matter for day-to-day work with issues, comments, cycles, documents, and files. If you need to manage custom workflows, integrations, or workspace settings, the MCP is the better choice. -## Command Examples +**This project scratches my own itches,** and satisfies my own usage patterns of working with Linear: I **do** work with tickets/issues and comments on the command line; I **do not** manage projects or workspaces etc. there. YMMV. -### Issues Management +## Installation ```bash -# Show available tools -linearis +npm install -g linearis +``` -# Show available sub-tools -linearis issues -linearis labels +Requires Node.js >= 22. -# List recent issues -linearis issues list -l 10 +## Authentication -# Search for bugs in specific team/project -linearis issues search "authentication" --team Platform --project "Auth Service" +```bash +linearis auth login +``` -# Create new issue with labels and assignment -linearis issues create "Fix login timeout" --team Backend --assignee user123 \ - --labels "Bug,Critical" --priority 1 --description "Users can't stay logged in" +This opens Linear in your browser, guides you through creating an API key, and stores the token encrypted in `~/.linearis/token`. -# Read issue details (supports ABC-123 format) -linearis issues read DEV-456 +Alternatively, provide a token directly: -# Update issue status and priority -linearis issues update ABC-123 --status "In Review" --priority 2 +```bash +# Via CLI flag +linearis --api-token <token> issues list -# Add labels to existing issue -linearis issues update DEV-789 --labels "Frontend,UX" --label-by adding +# Via environment variable +LINEAR_API_TOKEN=<token> linearis issues list +``` -# Set parent-child relationships (output includes parentIssue and subIssues fields) -linearis issues update SUB-001 --parent-ticket EPIC-100 +Token resolution order: `--api-token` flag > `LINEAR_API_TOKEN` env > `~/.linearis/token` > `~/.linear_api_token` (deprecated). -# Clear all labels from issue -linearis issues update ABC-123 --clear-labels -``` +## Usage -### Comments +All output is JSON. Pipe through `jq` or similar for formatting. ```bash -# Add comment to issue -linearis comments create ABC-123 --body "Fixed in PR #456" +# Discovery +linearis usage # overview of all domains +linearis issues usage # detailed usage for one domain ``` -### File Downloads +### Issues ```bash -# Get issue details including embedded files -linearis issues read ABC-123 -# Returns JSON with embeds array containing file URLs and expiration timestamps +# List recent issues +linearis issues list --limit 10 -# Download a file from Linear storage -linearis embeds download "https://uploads.linear.app/.../file.png?signature=..." --output ./screenshot.png +# Search issues by text +linearis issues list --query "authentication" --team Platform -# Overwrite existing file -linearis embeds download "https://uploads.linear.app/.../file.png?signature=..." --output ./screenshot.png --overwrite +# Create an issue +linearis issues create "Fix login timeout" --team Backend \ + --assignee "Jane Doe" --labels "Bug,Critical" --priority 1 \ + --description "Users report session expiry after 5 minutes" + +# Read issue details (supports ABC-123 identifiers) +linearis issues read DEV-456 + +# Update status, priority, labels +linearis issues update ABC-123 --status "In Review" --priority 2 +linearis issues update DEV-789 --labels "Frontend,UX" --label-mode add +linearis issues update ABC-123 --clear-labels + +# Parent-child relationships +linearis issues update SUB-001 --parent-ticket EPIC-100 + +# Issue relations +linearis issues create "Blocked task" --team Backend --blocked-by DEV-123 +linearis issues update ABC-123 --blocks DEV-456 +linearis issues update ABC-123 --relates-to DEV-789 +linearis issues update ABC-123 --remove-relation DEV-456 ``` -### File Uploads +### Comments ```bash -# Upload a file to Linear storage -linearis embeds upload ./screenshot.png -# Returns: { "success": true, "assetUrl": "https://uploads.linear.app/...", "filename": "screenshot.png" } - -# Use with comments -URL=$(linearis embeds upload ./bug.png | jq -r .assetUrl) -linearis comments create ABC-123 --body "See attached: ![$URL]($URL)" +linearis comments create ABC-123 --body "Fixed in PR #456" ``` ### Documents -Linear Documents are standalone markdown files that can be associated with projects or teams. Use `--attach-to` to link documents to issues. - ```bash -# Create a document -linearis documents create --title "API Design" --content "# Overview\n\nThis document..." +# Create a document (optionally link to a project and/or issue) +linearis documents create --title "API Design" --content "# Overview..." +linearis documents create --title "Bug Analysis" --project "Backend" --issue ABC-123 -# Create document in a project and attach to an issue -linearis documents create --title "Bug Analysis" --project "Backend" --attach-to ABC-123 - -# List all documents +# List documents linearis documents list - -# List documents selectively linearis documents list --project "Backend" linearis documents list --issue ABC-123 -# Read a document +# Read, update, delete linearis documents read <document-id> - -# Update a document linearis documents update <document-id> --title "New Title" --content "Updated content" - -# Delete (trash) a document linearis documents delete <document-id> ``` -### Projects & Labels +### Cycles ```bash -# List all projects -linearis projects list +# List cycles for a team +linearis cycles list --team Backend --limit 10 -# List labels for specific team -linearis labels list --team Backend +# Active cycle only +linearis cycles list --team Backend --active + +# Active cycle +/- 3 neighbors +linearis cycles list --team Backend --window 3 + +# Read cycle details +linearis cycles read "Sprint 2025-10" --team Backend ``` -### Teams & Users +### Milestones ```bash -# List all teams in the workspace -linearis teams list +# List milestones in a project +linearis milestones list --project "Backend" -# List all users -linearis users list +# Read milestone details +linearis milestones read "Beta Release" --project "Backend" -# List only active users -linearis users list --active +# Create and update milestones +linearis milestones create "v2.0" --project "Backend" --target-date 2025-06-01 +linearis milestones update "v2.0" --project "Backend" --description "Major release" ``` -### Cycles - -You can list and read cycles (sprints) for teams. The CLI exposes simple helpers, but the GraphQL API provides a few cycle-related fields you can use to identify relatives (active, next, previous). +### Files ```bash -# List cycles (optionally scope to a team) -linearis cycles list --team Backend --limit 10 +# Download a file from Linear storage +linearis files download "https://uploads.linear.app/.../file.png" --output ./screenshot.png -# Show only the active cycle(s) for a team -linearis cycles list --team Backend --active +# Upload and reference in a comment +URL=$(linearis files upload ./bug.png | jq -r .assetUrl) +linearis comments create ABC-123 --body "Screenshot: ![$URL]($URL)" +``` -# Read a cycle by ID or by name (optionally scope name lookup with --team) -linearis cycles read "Sprint 2025-10" --team Backend +### Projects, Labels, Teams, Users + +```bash +linearis projects list +linearis labels list --team Backend +linearis teams list +linearis users list --active ``` -Ordering and getting "active +/- 1" +### Pagination -- The cycles returned by the API include fields `isActive`, `isNext`, `isPrevious` and a numerical `number` field. The CLI will prefer an active/next/previous candidate when resolving ambiguous cycle names. -- To get the active and the next cycle programmatically, do two calls locally: - 1. `linearis cycles list --team Backend --active --limit 1` to get the active cycle and its `number`. - 2. `linearis cycles list --team Backend --limit 10` and pick the cycle with `number = (active.number + 1)` or check `isNext` on the returned nodes. -- If multiple cycles match a name and none is marked active/next/previous, the CLI will return an error listing the candidates so you can use a precise ID or scope with `--team`. +All list commands support cursor-based pagination: -#### Flag Combinations +```bash +linearis issues list --limit 25 +# Response includes pageInfo with endCursor and hasNextPage -The `cycles list` command supports several flag combinations: +linearis issues list --limit 25 --after "cursor-from-previous-response" +``` -**Valid combinations:** +## AI Agent Integration -- `cycles list` - All cycles across all teams -- `cycles list --team Backend` - All Backend cycles -- `cycles list --active` - Active cycles from all teams -- `cycles list --team Backend --active` - Backend's active cycle only -- `cycles list --team Backend --around-active 3` - Backend's active cycle ± 3 cycles +### How agents use Linearis -**Invalid combinations:** +The CLI is structured around a discover-then-act pattern that matches how agents work: -- `cycles list --around-active 3` - ❌ Error: requires `--team` +1. **Discover** -- `linearis usage` returns a compact overview of all domains (~200 tokens). The agent reads this once to understand what's available. +2. **Drill down** -- `linearis <domain> usage` gives the full command reference for one domain (~300-500 tokens). The agent only loads what it needs. +3. **Execute** -- All commands return structured JSON. No parsing of human-readable tables or prose. -**Note:** Using `--active --around-active` together works but `--active` is redundant since `--around-active` always includes the active cycle. +This means the agent never loads the full API surface into context. It pays for what it uses, one domain at a time. -### Advanced Usage +### Linearis vs. MCP -```bash -# Show all available commands and options (LLM agents love this!) -linearis usage +| | Linearis | Linear MCP | +|---|---|---| +| Context cost | ~500-700 tokens per interaction | ~13k tokens on connect | +| Coverage | Common operations (issues, comments, cycles, docs, files) | Full Linear API | +| Output | JSON via stdout | Tool call responses | +| Setup | `npm install -g linearis` + bash tool | MCP server connection | -# Combine with other tools (pipe JSON output) -linearis issues list -l 5 | jq '.[] | .identifier + ": " + .title' -``` +Use Linearis when token efficiency matters and you work primarily with issues and related data. Use the MCP when you need full API coverage or tight tool-call integration. -## Installation +### Example prompt -### npm (recommended) +```markdown +## Linear (project management) -```bash -npm install -g linearis -``` +Tool: `linearis` CLI via Bash. All output is JSON. -### From source +Discovery: Run `linearis usage` once to see available domains. Run `linearis <domain> usage` for full command reference of a specific domain. Do NOT guess flags or subcommands -- check usage first. -```bash -git clone https://github.com/czottmann/linearis.git -cd linearis -npm install -npm run build -npm link -``` +Ticket format: "ABC-123". Always reference tickets by their identifier. -### Development setup +Workflow rules: +- When creating a ticket, ask the user which project to assign it to if unclear. +- For subtasks, inherit the parent ticket's project by default. +- When a task in a ticket description changes status, update the description. +- For progress beyond simple checkbox changes, add a comment instead of editing the description. -```bash -git clone https://github.com/czottmann/linearis.git -cd linearis -npm install -npm start # Development mode using tsx (no compilation needed) +File handling: `issues read` returns an `embeds` array with signed download URLs and expiration timestamps. Use `files download` to retrieve them. Use `files upload` to attach new files, then reference the returned URL in comments or descriptions. ``` -## Authentication +Add this (or a version adapted to your workflow) to your `AGENTS.md` or `CLAUDE.md` so every agent session has it in context automatically. -You can authenticate by passing in your API token via `--api-token` flag: +## Development ```bash -linearis --api-token <token> issues list +git clone https://github.com/czottmann/linearis.git +cd linearis +npm install +npm start # Development mode (tsx, no compilation) +npm test # Run tests +npm run build # Compile to dist/ ``` -… OR by storing it in an environment variable `LINEAR_API_TOKEN`: +### Agent skills -```bash -LINEAR_API_TOKEN=<token> linearis issues list -``` +This project uses shared agent skills managed through [skills.sh](https://skills.sh), an open ecosystem of reusable capabilities for AI coding agents. Skills are procedural instructions (not code) that teach agents workflows like test-driven development, systematic debugging, or code review. -… OR by storing it in `~/.linear_api_token` once, and then forgetting about it because the tool will check that file automatically: +Skills are installed into `.agents/skills/` and automatically symlinked to the locations that tools like [Claude Code](https://claude.ai/product/claude-code) (`.claude/skills/`) or [Cursor](https://cursor.com) expect. This means skills are defined once and work across agents without duplication. ```bash -# Save token once: -echo "<token>" > ~/.linear_api_token +# Install a skill from the registry +npx skills add obra/superpowers/systematic-debugging -# Day-to-day, just use the tool -linearis issues list +# Install all skills defined in the project +npx skills install ``` -### Getting a Linear API key/token +**How it works:** -1. Log in to your Linear account -1. Go to _Settings_ → _Security & Access_ → _Personal API keys_ -1. Create a new API key +- `.agents/skills/` is the source of truth. Each skill is a directory with a `SKILL.md` file that the agent loads on demand. +- `.claude/skills/` contains symlinks pointing back to `.agents/skills/`. These are created automatically by `skills.sh` and should not be edited directly. +- `CLAUDE.md` is a symlink to `AGENTS.md`. Both serve the same purpose -- providing agent-wide instructions -- but different tools look for different filenames. -## Example rule for your LLM agent +**Best practices:** -```markdown -We track our tickets and projects in Linear (https://linear.app), a project management tool. We use the `linearis` CLI tool for communicating with Linear. Use your Bash tool to call the `linearis` executable. Run `linearis usage` to see usage information. +- Commit `.agents/skills/` to the repo so every contributor gets the same agent behavior out of the box. +- Prefer project-scoped skills (`.agents/skills/`) over global ones (`~/.agents/skills/`) for anything specific to this codebase. Global skills are good for personal preferences like commit style or code review habits. +- Keep skills focused. One skill per concern. A skill that tries to cover debugging, testing, and deployment is harder to maintain and slower for agents to load. +- Review skills before installing. They are plain markdown, so read the `SKILL.md` before adding one to the project. -The ticket numbers follow the format "ABC-<number>". Always reference tickets by their number. +Browse available skills at [skills.sh](https://skills.sh) or search with `npx skills search <query>`. -If you create a ticket, and it's not clear which project to assign it to, prompt the user. When creating subtasks, use the project of the parent ticket by default. +## Maintainer -When the the status of a task in the ticket description has changed (task → task done), update the description accordingly. When updating a ticket with a progress report that is more than just a checkbox change, add that report as a ticket comment. +Fabian Jocks -- [github.com/iamfj](https://github.com/iamfj) | [linkedin.com/in/fabianjocks](https://linkedin.com/in/fabianjocks) -The `issues read` command returns an `embeds` array containing files uploaded to Linear (screenshots, documents, etc.) with signed download URLs and expiration timestamps. Use `embeds download` to download these files when needed. -``` +## Original Author -## Author / Maintainer +Carlo Zottmann -- [c.zottmann.dev](https://c.zottmann.dev) | [github.com/czottmann](https://github.com/czottmann) -Carlo Zottmann, <carlo@zottmann.dev>, https://c.zottmann.dev, https://github.com/czottmann. +Carlo created Linearis and drove its early development. As interest in the project grew, he handed maintenance over to Fabian. -This project is neither affiliated with nor endorsed by Linear. I'm just a very happy customer. +This project is neither affiliated with nor endorsed by Linear. -### Sponsoring this project +### Sponsoring Carlo's work -I don't accept sponsoring in the "GitHub sponsorship" sense[^1] but [next to my own apps, I also sell "Tokens of Appreciation"](https://actions.work/store/?ref=github). Any support is appreciated! 😉 +Carlo doesn't accept sponsoring in the "GitHub sponsorship" sense[^1] but [next to his own apps, he also sells "Tokens of Appreciation"](https://actions.work/store/?ref=github). Any support is appreciated! [^1]: Apparently, the German revenue service is still having some fits over "money for nothing??". > [!TIP] -> I make Shortcuts-related macOS & iOS productivity apps like [Actions For Obsidian](https://actions.work/actions-for-obsidian), [Browser Actions](https://actions.work/browser-actions) (which adds Shortcuts support for several major browsers), and [BarCuts](https://actions.work/barcuts) (a surprisingly useful contextual Shortcuts launcher). Check them out! +> Carlo makes Shortcuts-related macOS & iOS productivity apps like [Actions For Obsidian](https://actions.work/actions-for-obsidian), [Browser Actions](https://actions.work/browser-actions) (which adds Shortcuts support for several major browsers), and [BarCuts](https://actions.work/barcuts) (a surprisingly useful contextual Shortcuts launcher). Check them out! -## Contributors 🤙🏼 +## Contributors +- [Fabian Jocks](https://github.com/iamfj) - [Ryan Rozich](https://github.com/ryanrozich) - [Chad Walters](https://github.com/chadrwalters) - [Louis Mandelstam](https://github.com/man8) - [Ralf Schimmel](https://github.com/ralfschimmel) -## Documentation - -- **[docs/project-overview.md](docs/project-overview.md)** - Project purpose, technology stack, and platform support -- **[docs/architecture.md](docs/architecture.md)** - Component organization, data flow, and performance patterns -- **[docs/build-system.md](docs/build-system.md)** - TypeScript compilation, automated builds -- **[docs/testing.md](docs/testing.md)** - Testing approach, manual validation, and performance benchmarks -- **[docs/development.md](docs/development.md)** - Code patterns, TypeScript standards, and common workflows -- **[docs/deployment.md](docs/deployment.md)** - Git-based npm install, automated compilation, and production deployment -- **[docs/files.md](docs/files.md)** - Complete file catalog with descriptions and relationships - -## Key Entry Points +## License -- **dist/main.js** - Compiled CLI entry point for production use -- **src/main.ts** - TypeScript source with Commander.js setup (development) -- **package.json** - Project configuration with automated build scripts and npm distribution -- **tsconfig.json** - TypeScript compilation targeting ES2023 with dist/ output +MIT. See [LICENSE.md](LICENSE.md). diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..98b3476 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,309 @@ +linearis v2025.12.3 — CLI for Linear.app (project management / issue tracking) +auth: linearis auth login | --api-token <token> | LINEAR_API_TOKEN | ~/.linearis/token +output: JSON +ids: UUID or human-readable (team key, issue ABC-123, name) + +domains: + auth authenticate with Linear API (interactive, for humans) + issues work items with status, priority, assignee, labels + comments discussion threads on issues + labels categorization tags, workspace-wide or team-scoped + projects groups of issues toward a goal + cycles time-boxed iterations (sprints) per team + milestones progress checkpoints within projects + documents long-form markdown docs attached to projects or issues + files upload/download file attachments + teams organizational units owning issues and cycles + users workspace members and assignees + +detail: linearis <domain> usage + +--- + +linearis auth — authenticate with Linear API (interactive, for humans) + +linearis requires a Linear API token for all operations. +the auth command guides you through creating and storing a token. +tokens are encrypted and stored in ~/.linearis/token. +token resolution order: --api-token flag, LINEAR_API_TOKEN env, +~/.linearis/token (encrypted), ~/.linear_api_token (deprecated). + +commands: + login [options] set up or refresh authentication + status check current authentication status + logout remove stored authentication token + +login options: + --force reauthenticate even if already authenticated + +--- + +linearis issues — work items with status, priority, assignee, labels + +an issue belongs to exactly one team. it has a status (e.g. backlog, +todo, in progress, done — configurable per team), a priority (1-4), +and can be assigned to a user. issues can have labels, belong to a +project, be part of a cycle (sprint), and reference a project milestone. +parent-child relationships and issue relations (blocks, blocked-by, +relates-to, duplicate-of) are supported. + +commands: + list [options] list issues with optional filters + read <issue> get full issue details including description + create <title> create new issue + update <issue> update an existing issue + +arguments: + <issue> issue identifier (UUID or ABC-123) + <title> string + +list options: + --query <text> filter by text search + --limit <n> max results (default: 50) + --after <cursor> cursor for next page + +create options: + --description <text> issue body + --assignee <user> assign to user + --priority <1-4> 1=urgent 2=high 3=medium 4=low + --project <project> add to project + --team <team> target team (required) + --labels <labels> comma-separated label names or UUIDs + --project-milestone <ms> set milestone (requires --project) + --cycle <cycle> add to cycle (requires --team) + --status <status> set status + --parent-ticket <issue> set parent issue + --blocks <issue> this issue blocks <issue> + --blocked-by <issue> this issue is blocked by <issue> + --relates-to <issue> this issue relates to <issue> + --duplicate-of <issue> this issue duplicates <issue> + +update options: + --title <text> new title + --description <text> new description + --status <status> new status + --priority <1-4> new priority + --assignee <user> new assignee + --project <project> new project + --labels <labels> labels to apply (comma-separated) + --label-mode <mode> add | overwrite + --clear-labels remove all labels + --parent-ticket <issue> set parent issue + --clear-parent-ticket clear parent + --project-milestone <ms> set project milestone + --clear-project-milestone clear project milestone + --cycle <cycle> set cycle + --clear-cycle clear cycle + --blocks <issue> add blocks relation + --blocked-by <issue> add blocked-by relation + --relates-to <issue> add relates-to relation + --duplicate-of <issue> add duplicate relation + --remove-relation <issue> remove relation with <issue> + +see also: comments create <issue>, documents list --issue <issue> + +--- + +linearis comments — discussion threads on issues + +a comment is a text entry on an issue. comments support markdown. + +commands: + create <issue> create a comment on an issue + +arguments: + <issue> issue identifier (UUID or ABC-123) + +create options: + --body <text> comment body (required, markdown supported) + +see also: issues read <issue> + +--- + +linearis labels — categorization tags, workspace-wide or team-scoped + +labels categorize issues. they can exist at workspace level or be +scoped to a specific team. use with issues create/update --labels. + +commands: + list [options] list available labels + +list options: + --team <team> filter by team (key, name, or UUID) + --limit <n> max results (default: 50) + --after <cursor> cursor for next page + +see also: issues create --labels, issues update --labels + +--- + +linearis projects — groups of issues toward a goal + +a project collects related issues across teams. projects can have +milestones to track progress toward deadlines or phases. + +commands: + list [options] list projects + +list options: + --limit <n> max results (default: 100) + --after <cursor> cursor for next page + +see also: milestones list --project, documents list --project + +--- + +linearis cycles — time-boxed iterations (sprints) per team + +a cycle is a sprint belonging to one team. each team can have one +active cycle at a time. cycles contain issues and have start/end dates. + +commands: + list [options] list cycles + read <cycle> get cycle details including issues + +arguments: + <cycle> cycle identifier (UUID or name) + +list options: + --team <team> filter by team (key, name, or UUID) + --active only show active cycles + --window <n> active cycle +/- n neighbors (requires --team) + --limit <n> max results (default: 50) + --after <cursor> cursor for next page + +read options: + --team <team> scope name lookup to team + --limit <n> max issues to fetch (default: 50) + +see also: issues create --cycle, issues update --cycle + +--- + +linearis milestones — progress checkpoints within projects + +a milestone marks a phase or deadline within a project. milestones +can have target dates and contain issues assigned to them. + +commands: + list [options] list milestones in a project + read <milestone> get milestone details including issues + create <name> create a new milestone + update <milestone> update an existing milestone + +arguments: + <milestone> milestone identifier (UUID or name) + <name> string + +list options: + --project <project> target project (required) + --limit <n> max results (default: 50) + --after <cursor> cursor for next page + +read options: + --project <project> scope name lookup to project + --limit <n> max issues to fetch (default: 50) + +create options: + --project <project> target project (required) + --description <text> milestone description + --target-date <date> target date in ISO format (YYYY-MM-DD) + +update options: + --project <project> scope name lookup to project + --name <name> new name + --description <text> new description + --target-date <date> new target date in ISO format (YYYY-MM-DD) + --sort-order <n> display order + +see also: issues create --project-milestone, issues update --project-milestone + +--- + +linearis documents — long-form markdown docs attached to projects or issues + +a document is a markdown page. it can belong to a project and/or be +attached to an issue. documents support icons and colors. + +commands: + list [options] list documents + read <document> get document content + create [options] create a new document + update <document> update an existing document + delete <document> trash a document + +arguments: + <document> document identifier (UUID) + +list options: + --project <project> filter by project name or ID + --issue <issue> filter by issue (shows documents attached to the issue) + --limit <n> max results (default: 50) + --after <cursor> cursor for next page + +create options: + --title <title> document title (required) + --content <text> document content (markdown) + --project <project> project name or ID + --team <team> team key or name + --icon <icon> document icon + --color <color> icon color + --issue <issue> also attach document to issue (e.g., ABC-123) + +update options: + --title <title> new title + --content <text> new content (markdown) + --project <project> move to project + --icon <icon> new icon + --color <color> new icon color + +see also: issues read <issue>, projects list + +--- + +linearis files — upload/download file attachments + +files are binary attachments stored in Linear's storage. upload returns +a URL that can be referenced in issue descriptions or comments. + +commands: + download <url> download a file from Linear storage + upload <file> upload a file to Linear storage + +arguments: + <url> Linear storage URL + <file> local file path + +download options: + --output <path> output file path + --overwrite overwrite existing file + +--- + +linearis teams — organizational units owning issues and cycles + +a team is a group of users that owns issues, cycles, statuses, and +labels. teams are identified by a short key (e.g. ENG), name, or UUID. + +commands: + list [options] list all teams + +list options: + --limit <n> max results (default: 50) + --after <cursor> cursor for next page + +--- + +linearis users — workspace members and assignees + +a user is a member of the Linear workspace. users can be assigned to +issues and belong to teams. + +commands: + list [options] list workspace members + +list options: + --active only show active users + --limit <n> max results (default: 50) + --after <cursor> cursor for next page diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..5b3afa2 --- /dev/null +++ b/biome.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**", "!!**/dist", "!!**/src/gql", "!!**/coverage"] + }, + "formatter": { + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80 + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all" + } + }, + "linter": { + "rules": { + "recommended": true, + "style": { + "noNonNullAssertion": "off" + } + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/codegen.config.ts b/codegen.config.ts new file mode 100644 index 0000000..c7f9486 --- /dev/null +++ b/codegen.config.ts @@ -0,0 +1,16 @@ +import type { CodegenConfig } from "@graphql-codegen/cli"; + +const config: CodegenConfig = { + schema: "https://api.linear.app/graphql", // or download schema locally + documents: ["graphql/**/*.graphql"], + generates: { + "./src/gql/": { + preset: "client", + presetConfig: { + fragmentMasking: false, + }, + }, + }, +}; + +export default config; diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..fa584fb --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1 @@ +export default { extends: ["@commitlint/config-conventional"] }; diff --git a/docs/Linear-API@current.graphql b/docs/Linear-API@current.graphql deleted file mode 100644 index 5f643df..0000000 --- a/docs/Linear-API@current.graphql +++ /dev/null @@ -1,26719 +0,0 @@ -# ----------------------------------------------- -# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! -# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! -# ----------------------------------------------- - -""" -A bot actor is an actor that is not a user, but an application or integration. -""" -type ActorBot { - """A url pointing to the avatar representing this bot.""" - avatarUrl: String - id: ID - - """The display name of the bot.""" - name: String - - """The sub type of the bot.""" - subType: String - - """The type of bot.""" - type: String! - - """ - The display name of the external user on behalf of which the bot acted. - """ - userDisplayName: String -} - -"""An activity within an agent context.""" -type AgentActivity implements Node { - """The agent session this activity belongs to.""" - agentSession: AgentSession! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The content of the activity""" - content: AgentActivityContent! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - Whether the activity is ephemeral, and should disappear after the next agent activity. - """ - ephemeral: Boolean! - - """The unique identifier of the entity.""" - id: ID! - - """ - An optional modifier that provides additional instructions on how the activity should be interpreted. - """ - signal: AgentActivitySignal - - """The comment this activity is linked to.""" - sourceComment: Comment - - """Metadata about the external source that created this agent activity.""" - sourceMetadata: JSON - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user who created this agent activity.""" - user: User! -} - -"""Content for an action activity (tool call or action).""" -type AgentActivityActionContent { - """The action being performed.""" - action: String! - - """The parameters for the action, e.g. a file path, a keyword, etc.""" - parameter: String! - - """The result of the action in Markdown format.""" - result: String - - """The type of activity.""" - type: AgentActivityType! -} - -type AgentActivityConnection { - edges: [AgentActivityEdge!]! - nodes: [AgentActivity!]! - pageInfo: PageInfo! -} - -"""Content for different types of agent activities.""" -union AgentActivityContent = AgentActivityActionContent | AgentActivityElicitationContent | AgentActivityErrorContent | AgentActivityPromptContent | AgentActivityResponseContent | AgentActivityThoughtContent - -input AgentActivityCreateInput { - """The agent session this activity belongs to.""" - agentSessionId: String! - - """ - The content payload of the agent activity. This object is not strictly typed. - See https://linear.app/developers/agents for typing details. - """ - content: JSONObject! - - """ - Whether the activity is ephemeral, and should disappear after the next activity. Defaults to false. - """ - ephemeral: Boolean - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """ - An optional modifier that provides additional instructions on how the activity should be interpreted. - """ - signal: AgentActivitySignal -} - -""" -[Internal] Input for creating prompt-type agent activities (created by users). -""" -input AgentActivityCreatePromptInput { - """The agent session this activity belongs to.""" - agentSessionId: String! - - """The content payload of the prompt agent activity.""" - content: JSONObject! - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """ - An optional modifier that provides additional instructions on how the activity should be interpreted. - """ - signal: AgentActivitySignal - - """The comment that contains the content of this activity.""" - sourceCommentId: String -} - -type AgentActivityEdge { - """Used in `before` and `after` args""" - cursor: String! - node: AgentActivity! -} - -"""Content for an elicitation activity.""" -type AgentActivityElicitationContent { - """The elicitation message in Markdown format.""" - body: String! - - """The type of activity.""" - type: AgentActivityType! -} - -"""Content for an error activity.""" -type AgentActivityErrorContent { - """The error message in Markdown format.""" - body: String! - - """The type of activity.""" - type: AgentActivityType! -} - -"""Agent activity filtering options.""" -input AgentActivityFilter { - """Comparator for the agent session ID.""" - agentSessionId: StringComparator - - """ - Compound filters, all of which need to be matched by the agent activity. - """ - and: [AgentActivityFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """ - Compound filters, one of which need to be matched by the agent activity. - """ - or: [AgentActivityFilter!] - - """Filters that the source comment must satisfy.""" - sourceComment: NullableCommentFilter - - """Comparator for the agent activity's content type.""" - type: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type AgentActivityPayload { - """The agent activity that was created or updated.""" - agentActivity: AgentActivity! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Content for a prompt activity.""" -type AgentActivityPromptContent { - """A message requesting additional information or action from user.""" - body: String! - - """The type of activity.""" - type: AgentActivityType! -} - -"""Content for a response activity.""" -type AgentActivityResponseContent { - """The response content in Markdown format.""" - body: String! - - """The type of activity.""" - type: AgentActivityType! -} - -""" -A modifier that provides additional instructions on how the activity should be interpreted. -""" -enum AgentActivitySignal { - continue - stop -} - -"""Content for a thought activity.""" -type AgentActivityThoughtContent { - """The thought content in Markdown format.""" - body: String! - - """The type of activity.""" - type: AgentActivityType! -} - -"""The type of an agent activity.""" -enum AgentActivityType { - action - elicitation - error - prompt - response - thought -} - -"""A session for agent activities and state management.""" -type AgentSession implements Node { - """Activities associated with this agent session.""" - activities( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned agent activities.""" - filter: AgentActivityFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): AgentActivityConnection! - - """The agent user that is associated with this agent session.""" - appUser: User! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The comment this agent session is associated with.""" - comment: Comment - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user that created this agent session.""" - creator: User - - """The time the agent session ended.""" - endedAt: DateTime - - """The URL of an external agent-hosted page associated with this session.""" - externalLink: String - - """The unique identifier of the entity.""" - id: ID! - - """The issue this agent session is associated with.""" - issue: Issue - - """Metadata about the external source that created this agent session.""" - sourceMetadata: JSON - - """The time the agent session started.""" - startedAt: DateTime - - """The current status of the agent session.""" - status: AgentSessionStatus! - - """A summary of the activities in this session.""" - summary: String - - """The type of the agent session.""" - type: AgentSessionType! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type AgentSessionConnection { - edges: [AgentSessionEdge!]! - nodes: [AgentSession!]! - pageInfo: PageInfo! -} - -input AgentSessionCreateOnComment { - """The root comment that this session will be associated with.""" - commentId: String! - - """The URL of an external agent-hosted page associated with this session.""" - externalLink: String -} - -input AgentSessionCreateOnIssue { - """The URL of an external agent-hosted page associated with this session.""" - externalLink: String - - """The issue that this session will be associated with.""" - issueId: String! -} - -type AgentSessionEdge { - """Used in `before` and `after` args""" - cursor: String! - node: AgentSession! -} - -type AgentSessionPayload { - """The agent session that was created or updated.""" - agentSession: AgentSession! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""The status of an agent session.""" -enum AgentSessionStatus { - active - awaitingInput - complete - error - pending - stale -} - -"""The type of an agent session.""" -enum AgentSessionType { - commentThread -} - -input AgentSessionUpdateExternalUrlInput { - """The URL of an external agent-hosted page associated with this session.""" - externalLink: String -} - -input AirbyteConfigurationInput { - """Linear export API key.""" - apiKey: String! -} - -"""An API key. Grants access to the user's resources.""" -type ApiKey implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The label of the API key.""" - label: String! - - """When the API key was last used.""" - lastActiveAt: DateTime - - """Organization the API key belongs to.""" - organization: Organization! - - """ - The sync groups that this API key requests access to. If null, the API key has access to all sync groups the user has access to. The final set of sync groups is computed as the intersection of these requested groups with the user's base sync groups. - """ - requestedSyncGroups: [String!] - - """Scopes associated with the API key.""" - scope: [String!] - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type ApiKeyConnection { - edges: [ApiKeyEdge!]! - nodes: [ApiKey!]! - pageInfo: PageInfo! -} - -input ApiKeyCreateInput { - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The API key value.""" - key: String! - - """The label for the API key.""" - label: String! - - """Scopes the API key has access to. Default is all scopes.""" - scope: [String!] - - """ - List of team IDs to restrict this API key to. Default is all teams the user has access to. - """ - teamIds: [String!] -} - -type ApiKeyEdge { - """Used in `before` and `after` args""" - cursor: String! - node: ApiKey! -} - -type ApiKeyPayload { - """The API key that was created.""" - apiKey: ApiKey! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input ApiKeyUpdateInput { - """The new label for the API key.""" - label: String - - """Scopes the API key has access to. Default is all scopes.""" - scope: [String!] - - """ - List of team IDs to restrict this API key to. Default is all teams the user has access to. - """ - teamIds: [String!] -} - -"""[INTERNAL] Details of the app user's existing token.""" -type AppUserAuthentication { - """The user that authorized the application, if known.""" - authorizingUser: AuthorizingUser - - """The timestamp at which the token was created.""" - createdAt: DateTime! - - """Whether the application has requested custom sync groups.""" - requestedSyncGroups: Boolean! - - """The scopes that the token has.""" - scope: [String!]! -} - -"""Public information of the OAuth application.""" -type Application { - """OAuth application's client ID.""" - clientId: String! - - """Information about the application.""" - description: String - - """Name of the developer.""" - developer: String! - - """Url of the developer (homepage or docs).""" - developerUrl: String! - - """OAuth application's ID.""" - id: String! - - """Image of the application.""" - imageUrl: String - - """Application name.""" - name: String! -} - -"""Customer approximate need count sorting options.""" -input ApproximateNeedCountSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A generic payload return from entity archive or deletion mutations.""" -interface ArchivePayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Contains requested archived model objects.""" -type ArchiveResponse { - """A JSON serialized collection of model objects loaded from the archive""" - archive: String! - - """ - The version of the remote database. Incremented by 1 for each migration run on the database. - """ - databaseVersion: Float! - - """ - Whether the dependencies for the model objects are included in the archive. - """ - includesDependencies: [String!]! - - """The total number of entities in the archive.""" - totalCount: Float! -} - -type AsksChannelConnectPayload { - """Whether the bot needs to be manually added to the channel.""" - addBot: Boolean! - - """The integration that was created or updated.""" - integration: Integration - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The new Asks Slack channel mapping for the connected channel.""" - mapping: SlackChannelNameMapping! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Issue assignee sorting options.""" -input AssigneeSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Issue attachment (e.g. support ticket, pull request).""" -type Attachment implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The body data of the attachment, if any.""" - bodyData: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The creator of the attachment.""" - creator: User - - """The non-Linear user who created the attachment.""" - externalUserCreator: ExternalUser - - """ - Indicates if attachments for the same source application should be grouped in the Linear UI. - """ - groupBySource: Boolean! - - """The unique identifier of the entity.""" - id: ID! - - """The issue this attachment belongs to.""" - issue: Issue! - - """Custom metadata related to the attachment.""" - metadata: JSONObject! - - """ - The issue this attachment was originally created on. Will be undefined if the attachment hasn't been moved. - """ - originalIssue: Issue - - """Information about the source which created the attachment.""" - source: JSONObject - - """ - An accessor helper to source.type, defines the source type of the attachment. - """ - sourceType: String - - """Content for the subtitle line in the Linear attachment widget.""" - subtitle: String - - """Content for the title line in the Linear attachment widget.""" - title: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Location of the attachment which is also used as an identifier.""" - url: String! -} - -"""Attachment collection filtering options.""" -input AttachmentCollectionFilter { - """Compound filters, all of which need to be matched by the attachment.""" - and: [AttachmentCollectionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the attachments creator must satisfy.""" - creator: NullableUserFilter - - """Filters that needs to be matched by all attachments.""" - every: AttachmentFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Compound filters, one of which need to be matched by the attachment.""" - or: [AttachmentCollectionFilter!] - - """Filters that needs to be matched by some attachments.""" - some: AttachmentFilter - - """Comparator for the source type.""" - sourceType: SourceTypeComparator - - """Comparator for the subtitle.""" - subtitle: NullableStringComparator - - """Comparator for the title.""" - title: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator - - """Comparator for the url.""" - url: StringComparator -} - -type AttachmentConnection { - edges: [AttachmentEdge!]! - nodes: [Attachment!]! - pageInfo: PageInfo! -} - -input AttachmentCreateInput { - """Create a linked comment with markdown body.""" - commentBody: String - - """ - [Internal] Create a linked comment with Prosemirror body. Please use `commentBody` instead. - """ - commentBodyData: JSONObject - - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=application` mode. - """ - createAsUser: String - - """ - Indicates if attachments for the same source application should be grouped in the Linear UI. - """ - groupBySource: Boolean - - """ - An icon url to display with the attachment. Should be of jpg or png format. Maximum of 1MB in size. Dimensions should be 20x20px for optimal display quality. - """ - iconUrl: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The issue to associate the attachment with.""" - issueId: String! - - """Attachment metadata object with string and number values.""" - metadata: JSONObject - - """The attachment subtitle.""" - subtitle: String - - """The attachment title.""" - title: String! - - """ - Attachment location which is also used as an unique identifier for the attachment. If another attachment is created with the same `url` value, existing record is updated instead. - """ - url: String! -} - -type AttachmentEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Attachment! -} - -"""Attachment filtering options.""" -input AttachmentFilter { - """Compound filters, all of which need to be matched by the attachment.""" - and: [AttachmentFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the attachments creator must satisfy.""" - creator: NullableUserFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Compound filters, one of which need to be matched by the attachment.""" - or: [AttachmentFilter!] - - """Comparator for the source type.""" - sourceType: SourceTypeComparator - - """Comparator for the subtitle.""" - subtitle: NullableStringComparator - - """Comparator for the title.""" - title: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator - - """Comparator for the url.""" - url: StringComparator -} - -type AttachmentPayload { - """The issue attachment that was created.""" - attachment: Attachment! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type AttachmentSourcesPayload { - """A unique list of all source types used in this workspace.""" - sources: JSONObject! -} - -input AttachmentUpdateInput { - """ - An icon url to display with the attachment. Should be of jpg or png format. Maximum of 1MB in size. Dimensions should be 20x20px for optimal display quality. - """ - iconUrl: String - - """Attachment metadata object with string and number values.""" - metadata: JSONObject - - """The attachment subtitle.""" - subtitle: String - - """The attachment title.""" - title: String! -} - -"""Workspace audit log entry object.""" -type AuditEntry implements Node { - """The user that caused the audit entry to be created.""" - actor: User - - """The ID of the user that caused the audit entry to be created.""" - actorId: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """Country code of request resulting to audit entry.""" - countryCode: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """IP from actor when entry was recorded.""" - ip: String - - """Additional metadata related to the audit entry.""" - metadata: JSONObject - - """The organization the audit log belongs to.""" - organization: Organization - - """ - Additional information related to the request which performed the action. - """ - requestInformation: JSONObject - type: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type AuditEntryConnection { - edges: [AuditEntryEdge!]! - nodes: [AuditEntry!]! - pageInfo: PageInfo! -} - -type AuditEntryEdge { - """Used in `before` and `after` args""" - cursor: String! - node: AuditEntry! -} - -"""Audit entry filtering options.""" -input AuditEntryFilter { - """Filters that the audit entry actor must satisfy.""" - actor: NullableUserFilter - - """Compound filters, all of which need to be matched by the issue.""" - and: [AuditEntryFilter!] - - """Comparator for the country code.""" - countryCode: StringComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the IP address.""" - ip: StringComparator - - """Compound filters, one of which need to be matched by the issue.""" - or: [AuditEntryFilter!] - - """Comparator for the type.""" - type: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type AuditEntryType { - """Description of the audit entry type.""" - description: String! - - """The audit entry type.""" - type: String! -} - -"""An identity provider.""" -type AuthIdentityProvider { - """ - Whether the identity provider is the default identity provider migrated from organization level settings. - """ - defaultMigrated: Boolean! - - """The unique identifier of the entity.""" - id: ID! - - """The issuer's custom entity ID.""" - issuerEntityId: String - - """ - The SAML priority used to pick default workspace in SAML SP initiated flow, when same domain is claimed for SAML by multiple workspaces. Lower priority value means higher preference. - """ - priority: Float - - """Whether SAML authentication is enabled for organization.""" - samlEnabled: Boolean! - - """Whether SCIM provisioning is enabled for organization.""" - scimEnabled: Boolean! - - """ - Binding method for authentication call. Can be either `post` (default) or `redirect`. - """ - ssoBinding: String - - """Sign in endpoint URL for the identity provider.""" - ssoEndpoint: String - - """ - The algorithm of the Signing Certificate. Can be one of `sha1`, `sha256` (default), or `sha512`. - """ - ssoSignAlgo: String - - """X.509 Signing Certificate in string form.""" - ssoSigningCert: String -} - -"""[INTERNAL] An OAuth userId/createdDate tuple""" -type AuthMembership { - """The user ID associated with the authorization""" - authorizingUserId: String - - """The date of the authorization""" - createdAt: DateTime! - - """The user ID the authorization was done for""" - userId: String! -} - -""" -An organization. Organizations are root-level objects that contain users and teams. -""" -type AuthOrganization { - """Allowed authentication providers, empty array means all are allowed""" - allowedAuthServices: [String!]! - - """The time at which deletion of the organization was requested.""" - deletionRequestedAt: DateTime - - """ - Whether the organization is enabled. Used as a superuser tool to lock down the org. - """ - enabled: Boolean! - - """The unique identifier of the entity.""" - id: ID! - - """The organization's logo URL.""" - logoUrl: String - - """The organization's name.""" - name: String! - - """ - Previously used URL keys for the organization (last 3 are kept and redirected). - """ - previousUrlKeys: [String!]! - - """The region the organization is hosted in.""" - region: String! - - """The feature release channel the organization belongs to.""" - releaseChannel: ReleaseChannel! - - """Whether SAML authentication is enabled for organization.""" - samlEnabled: Boolean! - - """[INTERNAL] SAML settings""" - samlSettings: JSONObject - - """Whether SCIM provisioning is enabled for organization.""" - scimEnabled: Boolean! - - """The email domain or URL key for the organization.""" - serviceId: String! - - """The organization's unique URL key.""" - urlKey: String! - userCount: Float! -} - -type AuthResolverResponse { - """Should the signup flow allow access for the domain.""" - allowDomainAccess: Boolean - - """ - List of organizations allowing this user account to join automatically. - """ - availableOrganizations: [AuthOrganization!] - - """Email for the authenticated account.""" - email: String! - - """User account ID.""" - id: String! - - """ID of the organization last accessed by the user.""" - lastUsedOrganizationId: String - - """ - List of organization available to this user account but locked due to the current auth method. - """ - lockedOrganizations: [AuthOrganization!] - - """List of locked users that are locked by login restrictions""" - lockedUsers: [AuthUser!]! - - """Application token.""" - token: String @deprecated(reason: "Deprecated and not used anymore. Never populated.") - - """List of active users that belong to the user account.""" - users: [AuthUser!]! -} - -"""A user that has access to the the resources of an organization.""" -type AuthUser { - """Whether the user is active.""" - active: Boolean! - - """An URL to the user's avatar image.""" - avatarUrl: String - - """The user's display (nick) name. Unique within each organization.""" - displayName: String! - - """The user's email address.""" - email: String! - id: ID! - - """[INTERNAL] Identity provider the user is managed by.""" - identityProvider: AuthIdentityProvider - - """The user's full name.""" - name: String! - - """Organization the user belongs to.""" - organization: AuthOrganization! - - """ - Whether the user is an organization admin or guest on a database level. - """ - role: UserRoleType! - - """User account ID the user belongs to.""" - userAccountId: String! -} - -"""Authentication session information.""" -type AuthenticationSessionResponse { - """Used web browser.""" - browserType: String - - """Client used for the session""" - client: String - - """Country codes of all seen locations.""" - countryCodes: [String!]! - - """Date when the session was created.""" - createdAt: DateTime! - id: String! - - """IP address.""" - ip: String - - """Identifies the session used to make the request.""" - isCurrentSession: Boolean! - - """When was the session last seen""" - lastActiveAt: DateTime - - """Human readable location""" - location: String - - """Location city name.""" - locationCity: String - - """Location country name.""" - locationCountry: String - - """Location country code.""" - locationCountryCode: String - - """Location region code.""" - locationRegionCode: String - - """Name of the session, derived from the client and operating system""" - name: String! - - """Operating system used for the session""" - operatingSystem: String - - """Service used for logging in.""" - service: String - - """Type of application used to authenticate.""" - type: AuthenticationSessionType! - - """Date when the session was last updated.""" - updatedAt: DateTime! - - """Session's user-agent.""" - userAgent: String -} - -enum AuthenticationSessionType { - android - desktop - ios - web -} - -""" -[INTERNAL] Public information of the OAuth application, plus the authorized scopes for a given user. -""" -type AuthorizedApplication { - """OAuth application's ID.""" - appId: String! - - """OAuth application's client ID.""" - clientId: String! - - """Description of the application.""" - description: String - - """Developer of the application.""" - developer: String - - """Developer URL of the application.""" - developerUrl: String - - """Image of the application.""" - imageUrl: String - - """Application name.""" - name: String! - - """Scopes that are authorized for this application for a given user.""" - scope: [String!]! - - """Whether or not webhooks are enabled for the application.""" - webhooksEnabled: Boolean! -} - -"""Details of the app user's authorizing user.""" -type AuthorizingUser { - """The user's display name.""" - displayName: String! - - """The user's full name.""" - name: String! -} - -"""Comparator for booleans.""" -input BooleanComparator { - """Equals constraint.""" - eq: Boolean - - """Not equals constraint.""" - neq: Boolean -} - -"""A comment associated with an issue.""" -type Comment implements Node { - """Agent session associated with this comment.""" - agentSession: AgentSession - - """[Internal] Agent sessions associated with this comment.""" - agentSessions( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): AgentSessionConnection! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The comment content in markdown format.""" - body: String! - - """[Internal] The comment content as a Prosemirror document.""" - bodyData: String! - - """The bot that created the comment.""" - botActor: ActorBot - - """The children of the comment.""" - children( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned comments.""" - filter: CommentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CommentConnection! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The document content that the comment is associated with.""" - documentContent: DocumentContent - - """The time user edited the comment.""" - editedAt: DateTime - - """The external thread that the comment is synced with.""" - externalThread: SyncedExternalThread - - """The external user who wrote the comment.""" - externalUser: ExternalUser - - """The unique identifier of the entity.""" - id: ID! - - """The initiative update that the comment is associated with.""" - initiativeUpdate: InitiativeUpdate - - """The issue that the comment is associated with.""" - issue: Issue - - """The parent comment under which the current comment is nested.""" - parent: Comment - - """The post that the comment is associated with.""" - post: Post - - """The project update that the comment is associated with.""" - projectUpdate: ProjectUpdate - - """ - The text that this comment references. Only defined for inline comments. - """ - quotedText: String - - """Emoji reaction summary, grouped by emoji type.""" - reactionData: JSONObject! - - """Reactions associated with the comment.""" - reactions: [Reaction!]! - - """The time the resolvingUser resolved the thread.""" - resolvedAt: DateTime - - """The comment that resolved the thread.""" - resolvingComment: Comment - - """The user that resolved the thread.""" - resolvingUser: User - - """The external services the comment is synced with.""" - syncedWith: [ExternalEntityInfo!] - - """[Internal] A generated summary of the comment thread.""" - threadSummary: JSONObject - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Comment's URL.""" - url: String! - - """The user who wrote the comment.""" - user: User -} - -"""Comment filtering options.""" -input CommentCollectionFilter { - """Compound filters, all of which need to be matched by the comment.""" - and: [CommentCollectionFilter!] - - """Comparator for the comment's body.""" - body: StringComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the comment's document content must satisfy.""" - documentContent: NullableDocumentContentFilter - - """Filters that needs to be matched by all comments.""" - every: CommentFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the comment's issue must satisfy.""" - issue: NullableIssueFilter - - """Comparator for the collection length.""" - length: NumberComparator - - """Filters that the comment's customer needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Compound filters, one of which need to be matched by the comment.""" - or: [CommentCollectionFilter!] - - """Filters that the comment parent must satisfy.""" - parent: NullableCommentFilter - - """Filters that the comment's project update must satisfy.""" - projectUpdate: NullableProjectUpdateFilter - - """Filters that the comment's reactions must satisfy.""" - reactions: ReactionCollectionFilter - - """Filters that needs to be matched by some comments.""" - some: CommentFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator - - """Filters that the comment's creator must satisfy.""" - user: UserFilter -} - -type CommentConnection { - edges: [CommentEdge!]! - nodes: [Comment!]! - pageInfo: PageInfo! -} - -input CommentCreateInput { - """The comment content in markdown format.""" - body: String - - """[Internal] The comment content as a Prosemirror document.""" - bodyData: JSON - - """ - Create comment as a user with the provided name. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - createAsUser: String - - """ - Flag to indicate this comment should be created on the issue's synced Slack comment thread. If no synced Slack comment thread exists, the mutation will fail. - """ - createOnSyncedSlackThread: Boolean - - """ - The date when the comment was created (e.g. if importing from another system). Must be a date in the past. If none is provided, the backend will generate the time as now. - """ - createdAt: DateTime - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """ - Flag to prevent auto subscription to the issue the comment is created on. - """ - doNotSubscribeToIssue: Boolean - - """The document content to associate the comment with.""" - documentContentId: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The initiative update to associate the comment with.""" - initiativeUpdateId: String - - """The issue to associate the comment with.""" - issueId: String - - """The parent comment under which to nest a current comment.""" - parentId: String - - """The post to associate the comment with.""" - postId: String - - """The project update to associate the comment with.""" - projectUpdateId: String - - """ - The text that this comment references. Only defined for inline comments. - """ - quotedText: String - - """ - [INTERNAL] The identifiers of the users subscribing to this comment thread. - """ - subscriberIds: [String!] -} - -type CommentEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Comment! -} - -"""Comment filtering options.""" -input CommentFilter { - """Compound filters, all of which need to be matched by the comment.""" - and: [CommentFilter!] - - """Comparator for the comment's body.""" - body: StringComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the comment's document content must satisfy.""" - documentContent: NullableDocumentContentFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the comment's issue must satisfy.""" - issue: NullableIssueFilter - - """Filters that the comment's customer needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Compound filters, one of which need to be matched by the comment.""" - or: [CommentFilter!] - - """Filters that the comment parent must satisfy.""" - parent: NullableCommentFilter - - """Filters that the comment's project update must satisfy.""" - projectUpdate: NullableProjectUpdateFilter - - """Filters that the comment's reactions must satisfy.""" - reactions: ReactionCollectionFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator - - """Filters that the comment's creator must satisfy.""" - user: UserFilter -} - -type CommentPayload { - """The comment that was created or updated.""" - comment: Comment! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input CommentUpdateInput { - """The comment content.""" - body: String - - """[Internal] The comment content as a Prosemirror document.""" - bodyData: JSON - - """ - [INTERNAL] Flag to prevent auto subscription to the issue the comment is updated on. - """ - doNotSubscribeToIssue: Boolean - - """ - The text that this comment references. Only defined for inline comments. - """ - quotedText: String - - """[INTERNAL] The child comment that resolves this thread.""" - resolvingCommentId: String - - """[INTERNAL] The user who resolved this thread.""" - resolvingUserId: String - - """[INTERNAL] The identifiers of the users subscribing to this comment.""" - subscriberIds: [String!] -} - -"""Issue completion date sorting options.""" -input CompletedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -input ContactCreateInput { - """User's browser information.""" - browser: String - - """User's Linear client information.""" - clientVersion: String - - """User's device information.""" - device: String - - """How disappointed the user would be if they could no longer use Linear.""" - disappointmentRating: Int - - """The message the user sent.""" - message: String! - - """User's operating system.""" - operatingSystem: String - - """The type of support contact.""" - type: String! -} - -type ContactPayload { - """Whether the operation was successful.""" - success: Boolean! -} - -"""[INTERNAL] Input for sending a message to the Linear Sales team.""" -input ContactSalesCreateInput { - """Size of the company.""" - companySize: String - - """Work email of the person requesting information.""" - email: String! - - """The message the user sent.""" - message: String - - """Name of the person requesting information.""" - name: String! -} - -"""[Internal] Comparator for content.""" -input ContentComparator { - """[Internal] Contains constraint.""" - contains: String - - """[Internal] Not-contains constraint.""" - notContains: String -} - -enum ContextViewType { - activeCycle - activeIssues - backlog - triage - upcomingCycle -} - -type CreateCsvExportReportPayload { - """Whether the operation was successful.""" - success: Boolean! -} - -type CreateOrJoinOrganizationResponse { - organization: AuthOrganization! - user: AuthUser! -} - -input CreateOrganizationInput { - """Whether the organization should allow email domain access.""" - domainAccess: Boolean - - """The name of the organization.""" - name: String! - - """The timezone of the organization, passed in by client.""" - timezone: String - - """The URL key of the organization.""" - urlKey: String! - - """ - JSON serialized UTM parameters associated with the creation of the workspace. - """ - utm: String -} - -"""Issue creation date sorting options.""" -input CreatedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A custom view that has been saved by a user.""" -type CustomView implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The color of the icon of the custom view.""" - color: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the custom view.""" - creator: User! - - """The description of the custom view.""" - description: String - - """[INTERNAL] The facet associated with the custom view.""" - facet: Facet - - """The filter applied to feed items in the custom view.""" - feedItemFilterData: JSONObject - - """The filter applied to issues in the custom view.""" - filterData: JSONObject! - - """The filters applied to issues in the custom view.""" - filters: JSONObject! @deprecated(reason: "Will be replaced by `filterData` in a future update") - - """The icon of the custom view.""" - icon: String - - """The unique identifier of the entity.""" - id: ID! - - """The filter applied to initiatives in the custom view.""" - initiativeFilterData: JSONObject - - """Initiatives associated with the custom view.""" - initiatives( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned initiatives.""" - filter: InitiativeFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): InitiativeConnection! - - """Issues associated with the custom view.""" - issues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - Include issues from sub-teams when the custom view is associated with a team. - """ - includeSubTeams: Boolean = false - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned issues.""" - sort: [IssueSortInput!] - ): IssueConnection! - - """The model name of the custom view.""" - modelName: String! - - """The name of the custom view.""" - name: String! - - """The organization of the custom view.""" - organization: Organization! - - """The organizations default view preferences for this custom view.""" - organizationViewPreferences: ViewPreferences - - """The user who owns the custom view.""" - owner: User! - - """The filter applied to projects in the custom view.""" - projectFilterData: JSONObject - - """Projects associated with the custom view.""" - projects( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned projects.""" - filter: ProjectFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - Include projects from sub-teams when the custom view is associated with a team. - """ - includeSubTeams: Boolean = false - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned projects.""" - sort: [ProjectSortInput!] - ): ProjectConnection! - - """Whether the custom view is shared with everyone in the organization.""" - shared: Boolean! - - """The custom view's unique URL slug.""" - slugId: String! - - """The team associated with the custom view.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user who last updated the custom view.""" - updatedBy: User - - """Feed items associated with the custom view.""" - updates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned feed items.""" - filter: FeedItemFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - Include updates from sub-teams when the custom view is associated with a team. - """ - includeSubTeams: Boolean = false - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): FeedItemConnection! - - """The current users view preferences for this custom view.""" - userViewPreferences: ViewPreferences - - """The calculated view preferences values for this custom view.""" - viewPreferencesValues: ViewPreferencesValues -} - -type CustomViewConnection { - edges: [CustomViewEdge!]! - nodes: [CustomView!]! - pageInfo: PageInfo! -} - -input CustomViewCreateInput { - """The color of the icon of the custom view.""" - color: String - - """The description of the custom view.""" - description: String - - """The feed item filter applied to issues in the custom view.""" - feedItemFilterData: FeedItemFilter - - """The filter applied to issues in the custom view.""" - filterData: IssueFilter - - """The filters applied to issues in the custom view.""" - filters: JSONObject @deprecated(reason: "Use `filterData` instead.") - - """The icon of the custom view.""" - icon: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """[ALPHA] The initiative filter applied to issues in the custom view.""" - initiativeFilterData: InitiativeFilter - - """The id of the initiative associated with the custom view.""" - initiativeId: String - - """The name of the custom view.""" - name: String! - - """The owner of the custom view.""" - ownerId: String - - """The project filter applied to issues in the custom view.""" - projectFilterData: ProjectFilter - - """The id of the project associated with the custom view.""" - projectId: String - - """Whether the custom view is shared with everyone in the organization.""" - shared: Boolean - - """The id of the team associated with the custom view.""" - teamId: String -} - -"""Custom view creation date sorting options.""" -input CustomViewCreatedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -type CustomViewEdge { - """Used in `before` and `after` args""" - cursor: String! - node: CustomView! -} - -"""Custom view filtering options.""" -input CustomViewFilter { - """Compound filters, all of which need to be matched by the custom view.""" - and: [CustomViewFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the custom view creator must satisfy.""" - creator: UserFilter - - """[INTERNAL] Filter based on whether the custom view has a facet.""" - hasFacet: Boolean - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the custom view model name.""" - modelName: StringComparator - - """Comparator for the custom view name.""" - name: StringComparator - - """Compound filters, one of which need to be matched by the custom view.""" - or: [CustomViewFilter!] - - """Comparator for whether the custom view is shared.""" - shared: BooleanComparator - - """Filters that the custom view's team must satisfy.""" - team: NullableTeamFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type CustomViewHasSubscribersPayload { - """Whether the custom view has subscribers.""" - hasSubscribers: Boolean! -} - -"""Custom view name sorting options.""" -input CustomViewNameSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A custom view notification subscription.""" -type CustomViewNotificationSubscription implements Entity & Node & NotificationSubscription { - """Whether the subscription is active or not.""" - active: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The type of view to which the notification subscription context is associated with. - """ - contextViewType: ContextViewType - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The custom view subscribed to.""" - customView: CustomView! - - """The customer associated with the notification subscription.""" - customer: Customer - - """ - The contextual cycle view associated with the notification subscription. - """ - cycle: Cycle - - """The unique identifier of the entity.""" - id: ID! - - """ - The contextual initiative view associated with the notification subscription. - """ - initiative: Initiative - - """ - The contextual label view associated with the notification subscription. - """ - label: IssueLabel - - """The type of subscription.""" - notificationSubscriptionTypes: [String!]! - - """ - The contextual project view associated with the notification subscription. - """ - project: Project - - """The user that subscribed to receive notifications.""" - subscriber: User! - - """The team associated with the notification subscription.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user view associated with the notification subscription.""" - user: User - - """ - The type of user view to which the notification subscription context is associated with. - """ - userContextViewType: UserContextViewType -} - -type CustomViewPayload { - """The custom view that was created or updated.""" - customView: CustomView! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -""" -Custom view shared status sorting options. Ascending order puts shared views last. -""" -input CustomViewSharedSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -input CustomViewSortInput { - """Sort by custom view creation date.""" - createdAt: CustomViewCreatedAtSort - - """Sort by custom view name.""" - name: CustomViewNameSort - - """Sort by custom view shared status.""" - shared: CustomViewSharedSort - - """Sort by custom view update date.""" - updatedAt: CustomViewUpdatedAtSort -} - -type CustomViewSuggestionPayload { - """The suggested view description.""" - description: String - - """The suggested view icon.""" - icon: String - - """The suggested view name.""" - name: String -} - -input CustomViewUpdateInput { - """The color of the icon of the custom view.""" - color: String - - """The description of the custom view.""" - description: String - - """The feed item filter applied to issues in the custom view.""" - feedItemFilterData: FeedItemFilter - - """The filter applied to issues in the custom view.""" - filterData: IssueFilter - - """The filters applied to issues in the custom view.""" - filters: JSONObject @deprecated(reason: "Use `filterData` instead.") - - """The icon of the custom view.""" - icon: String - - """[ALPHA] The initiative filter applied to issues in the custom view.""" - initiativeFilterData: InitiativeFilter - - """[Internal] The id of the initiative associated with the custom view.""" - initiativeId: String - - """The name of the custom view.""" - name: String - - """The owner of the custom view.""" - ownerId: String - - """The project filter applied to issues in the custom view.""" - projectFilterData: ProjectFilter - - """[Internal] The id of the project associated with the custom view.""" - projectId: String - - """Whether the custom view is shared with everyone in the organization.""" - shared: Boolean - - """The id of the team associated with the custom view.""" - teamId: String -} - -"""Custom view update date sorting options.""" -input CustomViewUpdatedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A customer whose needs will be tied to issues or projects.""" -type Customer implements Node { - """The approximate number of needs of the customer.""" - approximateNeedCount: Float! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The domains associated with this customer.""" - domains: [String!]! - - """The ids of the customers in external systems.""" - externalIds: [String!]! - - """The unique identifier of the entity.""" - id: ID! - - """The integration that manages the Customer.""" - integration: Integration - - """The customer's logo URL.""" - logoUrl: String - - """ - The ID of the main source, when a customer has multiple sources. Must be one of externalIds. - """ - mainSourceId: String - - """The customer's name.""" - name: String! - - """The user who owns the customer.""" - owner: User - - """The annual revenue generated by the customer.""" - revenue: Float - - """The size of the customer.""" - size: Float - - """The ID of the Slack channel used to interact with the customer.""" - slackChannelId: String - - """The customer's unique URL slug.""" - slugId: String! - - """The current status of the customer.""" - status: CustomerStatus! - - """The tier of the customer.""" - tier: CustomerTier - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type CustomerConnection { - edges: [CustomerEdge!]! - nodes: [Customer!]! - pageInfo: PageInfo! -} - -"""Issue customer count sorting options.""" -input CustomerCountSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -input CustomerCreateInput { - """The domains associated with this customer.""" - domains: [String!] = [] - - """The ids of the customers in external systems.""" - externalIds: [String!] = [] - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The URL of the customer's logo.""" - logoUrl: String - - """ - The main source of the customer, for customers with multiple sources. Must be one of externalIds. - """ - mainSourceId: String - - """The name of the customer.""" - name: String! - - """The user who owns the customer.""" - ownerId: String - - """The annual revenue generated by the customer.""" - revenue: Int - - """The size of the customer.""" - size: Int - - """The ID of the Slack channel used to interact with the customer.""" - slackChannelId: String - - """The status of the customer.""" - statusId: String - - """The tier of the customer customer.""" - tierId: String -} - -"""Customer creation date sorting options.""" -input CustomerCreatedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -type CustomerEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Customer! -} - -"""Customer filtering options.""" -input CustomerFilter { - """Compound filters, all of which need to be matched by the customer.""" - and: [CustomerFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the customer's domains.""" - domains: StringArrayComparator - - """Comparator for the customer's external IDs.""" - externalIds: StringArrayComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the customer name.""" - name: StringComparator - - """Filters that the customer's needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Compound filters, one of which need to be matched by the customer.""" - or: [CustomerFilter!] - - """Filters that the customer owner must satisfy.""" - owner: NullableUserFilter - - """Comparator for the customer generated revenue.""" - revenue: NumberComparator - - """Comparator for the customer size.""" - size: NumberComparator - - """Comparator for the customer slack channel ID.""" - slackChannelId: StringComparator - - """Filters that the customer's status must satisfy.""" - status: CustomerStatusFilter - - """Filters that the customer's tier must satisfy.""" - tier: CustomerTierFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Issue customer important count sorting options.""" -input CustomerImportantCountSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -""" -A customer need, expressed through a reference to an issue, project, or comment. -""" -type CustomerNeed implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The attachment this need is referencing.""" - attachment: Attachment - - """The need content in markdown format.""" - body: String - - """[Internal] The content of the need as a Prosemirror document.""" - bodyData: String - - """The comment this need is referencing.""" - comment: Comment - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The creator of the customer need.""" - creator: User - - """The customer that this need is attached to.""" - customer: Customer - - """The unique identifier of the entity.""" - id: ID! - - """The issue this need is referencing.""" - issue: Issue - - """ - The issue this customer need was originally created on. Will be undefined if the customer need hasn't been moved. - """ - originalIssue: Issue - - """ - Whether the customer need is important or not. 0 = Not important, 1 = Important. - """ - priority: Float! - - """The project this need is referencing.""" - project: Project - - """The project attachment this need is referencing.""" - projectAttachment: ProjectAttachment - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The URL of the underlying attachment, if any""" - url: String -} - -"""A generic payload return from entity archive mutations.""" -type CustomerNeedArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: CustomerNeed - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Customer needs filtering options.""" -input CustomerNeedCollectionFilter { - """ - Compound filters, all of which need to be matched by the customer needs. - """ - and: [CustomerNeedCollectionFilter!] - - """Filters that the need's comment must satisfy.""" - comment: NullableCommentFilter - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the need's customer must satisfy.""" - customer: NullableCustomerFilter - - """Filters that needs to be matched by all customer needs.""" - every: CustomerNeedFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the need's issue must satisfy.""" - issue: NullableIssueFilter - - """Comparator for the collection length.""" - length: NumberComparator - - """ - Compound filters, one of which need to be matched by the customer needs. - """ - or: [CustomerNeedCollectionFilter!] - - """Comparator for the customer need priority.""" - priority: NumberComparator - - """Filters that the need's project must satisfy.""" - project: NullableProjectFilter - - """Filters that needs to be matched by some customer needs.""" - some: CustomerNeedFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type CustomerNeedConnection { - edges: [CustomerNeedEdge!]! - nodes: [CustomerNeed!]! - pageInfo: PageInfo! -} - -input CustomerNeedCreateFromAttachmentInput { - """The attachment this need is created from.""" - attachmentId: String! -} - -input CustomerNeedCreateInput { - """The attachment this need is referencing.""" - attachmentId: String - - """Optional URL for the attachment associated with the customer need.""" - attachmentUrl: String - - """The content of the need in markdown format.""" - body: String - - """[Internal] The content of the need as a Prosemirror document.""" - bodyData: JSON - - """The comment this need is referencing.""" - commentId: String - - """ - Create need as a user with the provided name. This option is only available to OAuth applications creating needs in `actor=app` mode. - """ - createAsUser: String - - """The external ID of the customer the need belongs to.""" - customerExternalId: String - - """The uuid of the customer the need belongs to.""" - customerId: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating needs in `actor=app` mode. - """ - displayIconUrl: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The issue this need is referencing.""" - issueId: String - - """ - Whether the customer need is important or not. 0 = Not important, 1 = Important. - """ - priority: Float - - """[INTERNAL] The project this need is referencing.""" - projectId: String - - """[DEPRECATED] Optional URL to the source of the customer need.""" - url: String @deprecated(reason: "Use attachmentUrl instead") -} - -type CustomerNeedEdge { - """Used in `before` and `after` args""" - cursor: String! - node: CustomerNeed! -} - -"""Customer filtering options.""" -input CustomerNeedFilter { - """ - Compound filters, all of which need to be matched by the customer need. - """ - and: [CustomerNeedFilter!] - - """Filters that the need's comment must satisfy.""" - comment: NullableCommentFilter - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the need's customer must satisfy.""" - customer: NullableCustomerFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the need's issue must satisfy.""" - issue: NullableIssueFilter - - """ - Compound filters, one of which need to be matched by the customer need. - """ - or: [CustomerNeedFilter!] - - """Comparator for the customer need priority.""" - priority: NumberComparator - - """Filters that the need's project must satisfy.""" - project: NullableProjectFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""A customer need related notification.""" -type CustomerNeedNotification implements Entity & Node & Notification { - """The user that caused the notification.""" - actor: User - - """[Internal] Notification actor initials if avatar is not available.""" - actorAvatarColor: String! - - """[Internal] Notification avatar URL.""" - actorAvatarUrl: String - - """[Internal] Notification actor initials if avatar is not available.""" - actorInitials: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The bot that caused the notification.""" - botActor: ActorBot - - """The category of the notification.""" - category: NotificationCategory! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The customer need related to the notification.""" - customerNeed: CustomerNeed! - - """Related customer need.""" - customerNeedId: String! - - """ - The time at when an email reminder for this notification was sent to the user. Null, if no email - reminder has been sent. - """ - emailedAt: DateTime - - """The external user that caused the notification.""" - externalUserActor: ExternalUser - - """ - [Internal] Notifications with the same grouping key will be grouped together in the UI. - """ - groupingKey: String! - - """ - [Internal] Priority of the notification with the same grouping key. Higher number means higher priority. If priority is the same, notifications should be sorted by `createdAt`. - """ - groupingPriority: Float! - - """The unique identifier of the entity.""" - id: ID! - - """[Internal] Inbox URL for the notification.""" - inboxUrl: String! - - """[Internal] If notification actor was Linear.""" - isLinearActor: Boolean! - - """[Internal] Issue's status type for issue notifications.""" - issueStatusType: String - - """[Internal] Project update health for new updates.""" - projectUpdateHealth: String - - """ - The time at when the user marked the notification as read. Null, if the the user hasn't read the notification - """ - readAt: DateTime - - """The issue related to the notification.""" - relatedIssue: Issue - - """The project related to the notification.""" - relatedProject: Project - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime - - """[Internal] Notification subtitle.""" - subtitle: String! - - """[Internal] Notification title.""" - title: String! - - """Notification type.""" - type: String! - - """The time at which a notification was unsnoozed..""" - unsnoozedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """[Internal] URL to the target of the notification.""" - url: String! - - """The user that received the notification.""" - user: User! -} - -type CustomerNeedPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The customer need that was created or updated.""" - need: CustomerNeed! - - """Whether the operation was successful.""" - success: Boolean! -} - -input CustomerNeedUpdateInput { - """ - Whether to also update the priority of needs from the same customer on the same issue/project. - """ - applyPriorityToRelatedNeeds: Boolean - - """Optional URL for the attachment associated with the customer need.""" - attachmentUrl: String - - """The content of the need in markdown format.""" - body: String - - """[Internal] The content of the need as a Prosemirror document.""" - bodyData: JSON - - """The external ID of the customer the need belongs to.""" - customerExternalId: String - - """The uuid of the customer the need belongs to.""" - customerId: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The issue this need is referencing.""" - issueId: String - - """ - Whether the customer need is important or not. 0 = Not important, 1 = Important. - """ - priority: Float - - """[INTERNAL] The project this need is referencing.""" - projectId: String - - """[DEPRECATED] Optional URL to the source of the customer need.""" - url: String @deprecated(reason: "Use attachmentUrl instead") -} - -type CustomerNeedUpdatePayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The customer need that was created or updated.""" - need: CustomerNeed! - - """Whether the operation was successful.""" - success: Boolean! - - """The related customer needs that were updated.""" - updatedRelatedNeeds: [CustomerNeed!]! -} - -"""A customer related notification.""" -type CustomerNotification implements Entity & Node & Notification { - """The user that caused the notification.""" - actor: User - - """[Internal] Notification actor initials if avatar is not available.""" - actorAvatarColor: String! - - """[Internal] Notification avatar URL.""" - actorAvatarUrl: String - - """[Internal] Notification actor initials if avatar is not available.""" - actorInitials: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The bot that caused the notification.""" - botActor: ActorBot - - """The category of the notification.""" - category: NotificationCategory! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The customer related to the notification.""" - customer: Customer! - - """Related customer.""" - customerId: String! - - """ - The time at when an email reminder for this notification was sent to the user. Null, if no email - reminder has been sent. - """ - emailedAt: DateTime - - """The external user that caused the notification.""" - externalUserActor: ExternalUser - - """ - [Internal] Notifications with the same grouping key will be grouped together in the UI. - """ - groupingKey: String! - - """ - [Internal] Priority of the notification with the same grouping key. Higher number means higher priority. If priority is the same, notifications should be sorted by `createdAt`. - """ - groupingPriority: Float! - - """The unique identifier of the entity.""" - id: ID! - - """[Internal] Inbox URL for the notification.""" - inboxUrl: String! - - """[Internal] If notification actor was Linear.""" - isLinearActor: Boolean! - - """[Internal] Issue's status type for issue notifications.""" - issueStatusType: String - - """[Internal] Project update health for new updates.""" - projectUpdateHealth: String - - """ - The time at when the user marked the notification as read. Null, if the the user hasn't read the notification - """ - readAt: DateTime - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime - - """[Internal] Notification subtitle.""" - subtitle: String! - - """[Internal] Notification title.""" - title: String! - - """Notification type.""" - type: String! - - """The time at which a notification was unsnoozed..""" - unsnoozedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """[Internal] URL to the target of the notification.""" - url: String! - - """The user that received the notification.""" - user: User! -} - -"""A customer notification subscription.""" -type CustomerNotificationSubscription implements Entity & Node & NotificationSubscription { - """Whether the subscription is active or not.""" - active: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The type of view to which the notification subscription context is associated with. - """ - contextViewType: ContextViewType - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The contextual custom view associated with the notification subscription. - """ - customView: CustomView - - """The customer subscribed to.""" - customer: Customer! - - """ - The contextual cycle view associated with the notification subscription. - """ - cycle: Cycle - - """The unique identifier of the entity.""" - id: ID! - - """ - The contextual initiative view associated with the notification subscription. - """ - initiative: Initiative - - """ - The contextual label view associated with the notification subscription. - """ - label: IssueLabel - - """The type of subscription.""" - notificationSubscriptionTypes: [String!]! - - """ - The contextual project view associated with the notification subscription. - """ - project: Project - - """The user that subscribed to receive notifications.""" - subscriber: User! - - """The team associated with the notification subscription.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user view associated with the notification subscription.""" - user: User - - """ - The type of user view to which the notification subscription context is associated with. - """ - userContextViewType: UserContextViewType -} - -type CustomerPayload { - """The customer that was created or updated.""" - customer: Customer! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Issue customer revenue sorting options.""" -input CustomerRevenueSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Issue customer sorting options.""" -input CustomerSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Customer sorting options.""" -input CustomerSortInput { - """Sort by approximate customer need count""" - approximateNeedCount: ApproximateNeedCountSort - - """Sort by customer creation date""" - createdAt: CustomerCreatedAtSort - - """Sort by name""" - name: NameSort - - """Sort by owner name""" - owner: OwnerSort - - """Sort by customer generated revenue""" - revenue: RevenueSort - - """Sort by customer size""" - size: SizeSort - - """Sort by customer status""" - status: CustomerStatusSort - - """Sort by customer tier""" - tier: TierSort -} - -"""A customer status.""" -type CustomerStatus implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The UI color of the status as a HEX string.""" - color: String! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """Description of the status.""" - description: String - - """The display name of the status.""" - displayName: String! - - """The unique identifier of the entity.""" - id: ID! - - """The name of the status.""" - name: String! - - """The position of the status in the workspace's customers flow.""" - position: Float! - - """The type of the customer status.""" - type: CustomerStatusType @deprecated(reason: "Customer statuses are no longer grouped by type.") - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type CustomerStatusConnection { - edges: [CustomerStatusEdge!]! - nodes: [CustomerStatus!]! - pageInfo: PageInfo! -} - -input CustomerStatusCreateInput { - """The UI color of the status as a HEX string.""" - color: String! - - """Description of the status.""" - description: String - - """The display name of the status.""" - displayName: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The name of the status.""" - name: String - - """The position of the status in the workspace's customer flow.""" - position: Float -} - -type CustomerStatusEdge { - """Used in `before` and `after` args""" - cursor: String! - node: CustomerStatus! -} - -"""Customer status filtering options.""" -input CustomerStatusFilter { - """ - Compound filters, all of which need to be matched by the customer status. - """ - and: [CustomerStatusFilter!] - - """Comparator for the customer status color.""" - color: StringComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the customer status description.""" - description: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the customer status name.""" - name: StringComparator - - """ - Compound filters, one of which needs to be matched by the customer status. - """ - or: [CustomerStatusFilter!] - - """Comparator for the customer status position.""" - position: NumberComparator - - """Comparator for the customer status type.""" - type: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type CustomerStatusPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The customer status that was created or updated.""" - status: CustomerStatus! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Customer status sorting options.""" -input CustomerStatusSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""[DEPRECATED] A type of customer status.""" -enum CustomerStatusType { - active - inactive -} - -input CustomerStatusUpdateInput { - """The UI color of the status as a HEX string.""" - color: String - - """Description of the status.""" - description: String - - """The display name of the status.""" - displayName: String - - """The name of the status.""" - name: String - - """The position of the status in the workspace's customer flow.""" - position: Float -} - -"""A customer tier.""" -type CustomerTier implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The UI color of the tier as a HEX string.""" - color: String! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """Description of the tier.""" - description: String - - """The display name of the tier.""" - displayName: String! - - """The unique identifier of the entity.""" - id: ID! - - """The name of the tier.""" - name: String! - - """The position of the tier in the workspace's customers flow.""" - position: Float! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type CustomerTierConnection { - edges: [CustomerTierEdge!]! - nodes: [CustomerTier!]! - pageInfo: PageInfo! -} - -input CustomerTierCreateInput { - """The UI color of the tier as a HEX string.""" - color: String! - - """Description of the tier.""" - description: String - - """The display name of the tier.""" - displayName: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The name of the tier.""" - name: String - - """The position of the tier in the workspace's customer flow.""" - position: Float -} - -type CustomerTierEdge { - """Used in `before` and `after` args""" - cursor: String! - node: CustomerTier! -} - -"""Customer tier filtering options.""" -input CustomerTierFilter { - """ - Compound filters, all of which need to be matched by the customer tier. - """ - and: [CustomerTierFilter!] - - """Comparator for the customer tier color.""" - color: StringComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the customer tier description.""" - description: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the customer tier name.""" - name: StringComparator - - """ - Compound filters, one of which needs to be matched by the customer tier. - """ - or: [CustomerTierFilter!] - - """Comparator for the customer tier position.""" - position: NumberComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type CustomerTierPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The customer tier that was created or updated.""" - tier: CustomerTier! -} - -input CustomerTierUpdateInput { - """The UI color of the tier as a HEX string.""" - color: String - - """Description of the tier.""" - description: String - - """The display name of the tier.""" - displayName: String - - """The name of the tier.""" - name: String - - """The position of the tier in the workspace's customer flow.""" - position: Float -} - -input CustomerUpdateInput { - """The domains associated with this customer.""" - domains: [String!] - - """The ids of the customers in external systems.""" - externalIds: [String!] - - """The URL of the customer's logo.""" - logoUrl: String - - """ - The main source of the customer, for customers with multiple sources. Must be one of externalIds. - """ - mainSourceId: String - - """The name of the customer.""" - name: String - - """The user who owns the customer.""" - ownerId: String - - """The annual revenue generated by the customer.""" - revenue: Int - - """The size of the customer.""" - size: Int - - """The ID of the Slack channel used to interact with the customer.""" - slackChannelId: String - - """The status of the customer.""" - statusId: String - - """The tier of the customer customer.""" - tierId: String -} - -input CustomerUpsertInput { - """The domains associated with this customer.""" - domains: [String!] - - """The id of the customers in external systems.""" - externalId: String - - """The identifier in UUID v4 format.""" - id: String - - """The URL of the customer's logo.""" - logoUrl: String - - """The name of the customer.""" - name: String - - """The user who owns the customer.""" - ownerId: String - - """The annual revenue generated by the customer.""" - revenue: Int - - """The size of the customer.""" - size: Int - - """The ID of the Slack channel used to interact with the customer.""" - slackChannelId: String - - """The status of the customer.""" - statusId: String - - """The tier of the customer.""" - tierId: String - - """The name tier of the customer. Will be created if doesn't exist""" - tierName: String -} - -"""A set of issues to be resolved in a specified amount of time.""" -type Cycle implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The time at which the cycle was automatically archived by the auto pruning process. - """ - autoArchivedAt: DateTime - - """ - The completion time of the cycle. If null, the cycle hasn't been completed. - """ - completedAt: DateTime - - """The number of completed issues in the cycle after each day.""" - completedIssueCountHistory: [Float!]! - - """The number of completed estimation points after each day.""" - completedScopeHistory: [Float!]! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """[Internal] The current progress of the cycle.""" - currentProgress: JSONObject! - - """The cycle's description.""" - description: String - - """The end time of the cycle.""" - endsAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The number of in progress estimation points after each day.""" - inProgressScopeHistory: [Float!]! - - """The cycle inherited from.""" - inheritedFrom: Cycle - - """Whether the cycle is currently active.""" - isActive: Boolean! - - """Whether the cycle is in the future.""" - isFuture: Boolean! - - """Whether the cycle is the next cycle for the team.""" - isNext: Boolean! - - """Whether the cycle is in the past.""" - isPast: Boolean! - - """Whether the cycle is the previous cycle for the team.""" - isPrevious: Boolean! - - """The total number of issues in the cycle after each day.""" - issueCountHistory: [Float!]! - - """Issues associated with the cycle.""" - issues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """The custom name of the cycle.""" - name: String - - """The number of the cycle.""" - number: Float! - - """ - The overall progress of the cycle. This is the (completed estimate points + 0.25 * in progress estimate points) / total estimate points. - """ - progress: Float! - - """[Internal] The progress history of the cycle.""" - progressHistory: JSONObject! - - """The total number of estimation points after each day.""" - scopeHistory: [Float!]! - - """The start time of the cycle.""" - startsAt: DateTime! - - """The team that the cycle is associated with.""" - team: Team! - - """Issues that weren't completed when the cycle was closed.""" - uncompletedIssuesUponClose( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""A generic payload return from entity archive mutations.""" -type CycleArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: Cycle - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type CycleConnection { - edges: [CycleEdge!]! - nodes: [Cycle!]! - pageInfo: PageInfo! -} - -input CycleCreateInput { - """ - The completion time of the cycle. If null, the cycle hasn't been completed. - """ - completedAt: DateTime - - """The description of the cycle.""" - description: String - - """The end date of the cycle.""" - endsAt: DateTime! - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The custom name of the cycle.""" - name: String - - """The start date of the cycle.""" - startsAt: DateTime! - - """The team to associate the cycle with.""" - teamId: String! -} - -type CycleEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Cycle! -} - -"""Cycle filtering options.""" -input CycleFilter { - """Compound filters, all of which need to be matched by the cycle.""" - and: [CycleFilter!] - - """Comparator for the cycle completed at date.""" - completedAt: DateComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the cycle ends at date.""" - endsAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the filtering active cycle.""" - isActive: BooleanComparator - - """Comparator for the filtering future cycles.""" - isFuture: BooleanComparator - - """ - Comparator for filtering for whether the cycle is currently in cooldown. - """ - isInCooldown: BooleanComparator - - """Comparator for the filtering next cycle.""" - isNext: BooleanComparator - - """Comparator for the filtering past cycles.""" - isPast: BooleanComparator - - """Comparator for the filtering previous cycle.""" - isPrevious: BooleanComparator - - """Filters that the cycles issues must satisfy.""" - issues: IssueCollectionFilter - - """Comparator for the cycle name.""" - name: StringComparator - - """Comparator for the cycle number.""" - number: NumberComparator - - """Compound filters, one of which need to be matched by the cycle.""" - or: [CycleFilter!] - - """Comparator for the cycle start date.""" - startsAt: DateComparator - - """Filters that the cycles team must satisfy.""" - team: TeamFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""A cycle notification subscription.""" -type CycleNotificationSubscription implements Entity & Node & NotificationSubscription { - """Whether the subscription is active or not.""" - active: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The type of view to which the notification subscription context is associated with. - """ - contextViewType: ContextViewType - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The contextual custom view associated with the notification subscription. - """ - customView: CustomView - - """The customer associated with the notification subscription.""" - customer: Customer - - """The cycle subscribed to.""" - cycle: Cycle! - - """The unique identifier of the entity.""" - id: ID! - - """ - The contextual initiative view associated with the notification subscription. - """ - initiative: Initiative - - """ - The contextual label view associated with the notification subscription. - """ - label: IssueLabel - - """The type of subscription.""" - notificationSubscriptionTypes: [String!]! - - """ - The contextual project view associated with the notification subscription. - """ - project: Project - - """The user that subscribed to receive notifications.""" - subscriber: User! - - """The team associated with the notification subscription.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user view associated with the notification subscription.""" - user: User - - """ - The type of user view to which the notification subscription context is associated with. - """ - userContextViewType: UserContextViewType -} - -type CyclePayload { - """The Cycle that was created or updated.""" - cycle: Cycle - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -enum CyclePeriod { - after - before - during -} - -"""Comparator for period when issue was added to a cycle.""" -input CyclePeriodComparator { - """Equals constraint.""" - eq: CyclePeriod - - """In-array constraint.""" - in: [CyclePeriod!] - - """Not-equals constraint.""" - neq: CyclePeriod - - """Not-in-array constraint.""" - nin: [CyclePeriod!] - - """ - Null constraint. Matches any non-null values if the given value is false, otherwise it matches null values. - """ - null: Boolean -} - -""" -Input for shifting all cycles from a certain cycle onwards by a certain number of days -""" -input CycleShiftAllInput { - """The number of days to shift the cycles by.""" - daysToShift: Float! - - """The cycle ID at which to start the shift.""" - id: String! -} - -"""Issue cycle sorting options.""" -input CycleSort { - """ - When set to true, cycles will be ordered with a custom order. Current cycle comes first, followed by upcoming cycles in ASC order, followed by previous cycles in DESC order. - """ - currentCycleFirst: Boolean = false - - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -input CycleUpdateInput { - """The end date of the cycle.""" - completedAt: DateTime - - """The description of the cycle.""" - description: String - - """The end date of the cycle.""" - endsAt: DateTime - - """The custom name of the cycle.""" - name: String - - """The start date of the cycle.""" - startsAt: DateTime -} - -""" -[Internal] A dashboard, usually a collection of widgets to display several insights at once. -""" -type Dashboard implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The color of the icon of the dashboard.""" - color: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the dashboard.""" - creator: User - - """The description of the dashboard.""" - description: String - - """The icon of the dashboard.""" - icon: String - - """The unique identifier of the entity.""" - id: ID! - - """The filter applied to all dashboard widgets showing issues data.""" - issueFilter: JSONObject - - """The name of the dashboard.""" - name: String! - - """The organization of the dashboard.""" - organization: Organization! - - """The owner of the dashboard.""" - owner: User - - """The filter applied to all dashboard widgets showing projects data.""" - projectFilter: JSONObject - - """Whether the dashboard is shared with everyone in the organization.""" - shared: Boolean! - - """The dashboard's unique URL slug.""" - slugId: String! - - """The sort order of the dashboard within the organization or its team.""" - sortOrder: Float! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user who last updated the dashboard.""" - updatedBy: User - - """The widgets on the dashboard.""" - widgets: JSONObject! -} - -"""Comparator for dates.""" -input DateComparator { - """Equals constraint.""" - eq: DateTimeOrDuration - - """ - Greater-than constraint. Matches any values that are greater than the given value. - """ - gt: DateTimeOrDuration - - """ - Greater-than-or-equal constraint. Matches any values that are greater than or equal to the given value. - """ - gte: DateTimeOrDuration - - """In-array constraint.""" - in: [DateTimeOrDuration!] - - """ - Less-than constraint. Matches any values that are less than the given value. - """ - lt: DateTimeOrDuration - - """ - Less-than-or-equal constraint. Matches any values that are less than or equal to the given value. - """ - lte: DateTimeOrDuration - - """Not-equals constraint.""" - neq: DateTimeOrDuration - - """Not-in-array constraint.""" - nin: [DateTimeOrDuration!] -} - -"""By which resolution is a date defined.""" -enum DateResolutionType { - halfYear - month - quarter - year -} - -""" -Represents a date and time in ISO 8601 format. Accepts shortcuts like `2021` to represent midnight Fri Jan 01 2021. Also accepts ISO 8601 durations strings which are added to the current date to create the represented date (e.g '-P2W1D' represents the date that was two weeks and 1 day ago) -""" -scalar DateTime - -""" -Represents a date and time in ISO 8601 format. Accepts shortcuts like `2021` to represent midnight Fri Jan 01 2021. Also accepts ISO 8601 durations strings which are added to the current date to create the represented date (e.g '-P2W1D' represents the date that was two weeks and 1 day ago) -""" -scalar DateTimeOrDuration - -"""The day of the week.""" -enum Day { - Friday - Monday - Saturday - Sunday - Thursday - Tuesday - Wednesday -} - -"""Issue delegate sorting options.""" -input DelegateSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -input DeleteOrganizationInput { - """The deletion code to confirm operation.""" - deletionCode: String! -} - -"""A generic payload return from entity deletion mutations.""" -type DeletePayload implements ArchivePayload { - """The identifier of the deleted entity.""" - entityId: String! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""A document that can be attached to different entities.""" -type Document implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The color of the icon.""" - color: String - - """Comments associated with the document.""" - comments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned comments.""" - filter: CommentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CommentConnection! - - """The documents content in markdown format.""" - content: String - - """[Internal] The documents content as YJS state.""" - contentState: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the document.""" - creator: User - - """The ID of the document content associated with the document.""" - documentContentId: String - - """ - The time at which the document was hidden. Null if the entity has not been hidden. - """ - hiddenAt: DateTime - - """The icon of the document.""" - icon: String - - """The unique identifier of the entity.""" - id: ID! - - """The initiative that the document is associated with.""" - initiative: Initiative - - """The last template that was applied to this document.""" - lastAppliedTemplate: Template - - """The project that the document is associated with.""" - project: Project - - """The document's unique URL slug.""" - slugId: String! - - """The order of the item in the resources list.""" - sortOrder: Float! - - """[Internal] The team that the document is associated with.""" - team: Team - - """The document title.""" - title: String! - - """A flag that indicates whether the document is in the trash bin.""" - trashed: Boolean - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user who last updated the document.""" - updatedBy: User - - """The canonical url for the document.""" - url: String! -} - -"""A generic payload return from entity archive mutations.""" -type DocumentArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: Document - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type DocumentConnection { - edges: [DocumentEdge!]! - nodes: [Document!]! - pageInfo: PageInfo! -} - -"""A document content for a project.""" -type DocumentContent implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The document content in markdown format.""" - content: String - - """The document content state as a base64 encoded string.""" - contentState: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The document that the content is associated with.""" - document: Document - - """The unique identifier of the entity.""" - id: ID! - - """The initiative that the content is associated with.""" - initiative: Initiative - - """The issue that the content is associated with.""" - issue: Issue - - """The project that the content is associated with.""" - project: Project - - """The project milestone that the content is associated with.""" - projectMilestone: ProjectMilestone - - """ - The time at which the document content was restored from a previous version. - """ - restoredAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type DocumentContentHistoryPayload { - """The document content history entries.""" - history: [DocumentContentHistoryType!]! - - """Whether the operation was successful.""" - success: Boolean! -} - -type DocumentContentHistoryType { - """The ID of the author of the change.""" - actorIds: [String!] - - """[Internal] The document content as Prosemirror document.""" - contentData: JSON - - """ - The date when the document content history snapshot was taken. This can be different than createdAt since the content is captured from its state at the previously known updatedAt timestamp in the case of an update. On document create, these timestamps can be the same. - """ - contentDataSnapshotAt: DateTime! - - """The date when the document content history entry was created.""" - createdAt: DateTime! - - """The UUID of the document content history entry.""" - id: String! -} - -input DocumentCreateInput { - """The color of the icon.""" - color: String - - """The document content as markdown.""" - content: String - - """[Internal] The document content as a Prosemirror document.""" - contentData: JSONObject @deprecated(reason: "Use content instead") - - """The icon of the document.""" - icon: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """[Internal] Related initiative for the document.""" - initiativeId: String - - """The ID of the last template applied to the document.""" - lastAppliedTemplateId: String - - """Related project for the document.""" - projectId: String - - """[Internal] The resource folder containing the document.""" - resourceFolderId: String - - """The order of the item in the resources list.""" - sortOrder: Float - - """[INTERNAL] The identifiers of the users subscribing to this document.""" - subscriberIds: [String!] - - """[Internal] Related team for the document.""" - teamId: String - - """The title of the document.""" - title: String! -} - -type DocumentEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Document! -} - -"""Document filtering options.""" -input DocumentFilter { - """Compound filters, all of which need to be matched by the document.""" - and: [DocumentFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the document's creator must satisfy.""" - creator: UserFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the document's initiative must satisfy.""" - initiative: InitiativeFilter - - """Compound filters, one of which need to be matched by the document.""" - or: [DocumentFilter!] - - """Filters that the document's project must satisfy.""" - project: ProjectFilter - - """Comparator for the document slug ID.""" - slugId: StringComparator - - """Comparator for the document title.""" - title: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""A document related notification.""" -type DocumentNotification implements Entity & Node & Notification { - """The user that caused the notification.""" - actor: User - - """[Internal] Notification actor initials if avatar is not available.""" - actorAvatarColor: String! - - """[Internal] Notification avatar URL.""" - actorAvatarUrl: String - - """[Internal] Notification actor initials if avatar is not available.""" - actorInitials: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The bot that caused the notification.""" - botActor: ActorBot - - """The category of the notification.""" - category: NotificationCategory! - - """ - Related comment ID. Null if the notification is not related to a comment. - """ - commentId: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """Related document ID.""" - documentId: String! - - """ - The time at when an email reminder for this notification was sent to the user. Null, if no email - reminder has been sent. - """ - emailedAt: DateTime - - """The external user that caused the notification.""" - externalUserActor: ExternalUser - - """ - [Internal] Notifications with the same grouping key will be grouped together in the UI. - """ - groupingKey: String! - - """ - [Internal] Priority of the notification with the same grouping key. Higher number means higher priority. If priority is the same, notifications should be sorted by `createdAt`. - """ - groupingPriority: Float! - - """The unique identifier of the entity.""" - id: ID! - - """[Internal] Inbox URL for the notification.""" - inboxUrl: String! - - """[Internal] If notification actor was Linear.""" - isLinearActor: Boolean! - - """[Internal] Issue's status type for issue notifications.""" - issueStatusType: String - - """ - Related parent comment ID. Null if the notification is not related to a comment. - """ - parentCommentId: String - - """[Internal] Project update health for new updates.""" - projectUpdateHealth: String - - """Name of the reaction emoji related to the notification.""" - reactionEmoji: String - - """ - The time at when the user marked the notification as read. Null, if the the user hasn't read the notification - """ - readAt: DateTime - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime - - """[Internal] Notification subtitle.""" - subtitle: String! - - """[Internal] Notification title.""" - title: String! - - """Notification type.""" - type: String! - - """The time at which a notification was unsnoozed..""" - unsnoozedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """[Internal] URL to the target of the notification.""" - url: String! - - """The user that received the notification.""" - user: User! -} - -type DocumentPayload { - """The document that was created or updated.""" - document: Document! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type DocumentSearchPayload { - """ - Archived entities matching the search term along with all their dependencies. - """ - archivePayload: ArchiveResponse! - edges: [DocumentSearchResultEdge!]! - nodes: [DocumentSearchResult!]! - pageInfo: PageInfo! - - """Total number of results for query without filters applied.""" - totalCount: Float! -} - -type DocumentSearchResult implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The color of the icon.""" - color: String - - """Comments associated with the document.""" - comments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned comments.""" - filter: CommentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CommentConnection! - - """The documents content in markdown format.""" - content: String - - """[Internal] The documents content as YJS state.""" - contentState: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the document.""" - creator: User - - """The ID of the document content associated with the document.""" - documentContentId: String - - """ - The time at which the document was hidden. Null if the entity has not been hidden. - """ - hiddenAt: DateTime - - """The icon of the document.""" - icon: String - - """The unique identifier of the entity.""" - id: ID! - - """The initiative that the document is associated with.""" - initiative: Initiative - - """The last template that was applied to this document.""" - lastAppliedTemplate: Template - - """Metadata related to search result.""" - metadata: JSONObject! - - """The project that the document is associated with.""" - project: Project - - """The document's unique URL slug.""" - slugId: String! - - """The order of the item in the resources list.""" - sortOrder: Float! - - """[Internal] The team that the document is associated with.""" - team: Team - - """The document title.""" - title: String! - - """A flag that indicates whether the document is in the trash bin.""" - trashed: Boolean - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user who last updated the document.""" - updatedBy: User - - """The canonical url for the document.""" - url: String! -} - -type DocumentSearchResultEdge { - """Used in `before` and `after` args""" - cursor: String! - node: DocumentSearchResult! -} - -input DocumentUpdateInput { - """The color of the icon.""" - color: String - - """The document content as markdown.""" - content: String - - """[Internal] The document content as a Prosemirror document.""" - contentData: JSONObject @deprecated(reason: "Use content instead") - - """The time at which the document was hidden.""" - hiddenAt: DateTime - - """The icon of the document.""" - icon: String - - """[Internal] Related initiative for the document.""" - initiativeId: String - - """The ID of the last template applied to the document.""" - lastAppliedTemplateId: String - - """Related project for the document.""" - projectId: String - - """[Internal] The resource folder containing the document.""" - resourceFolderId: String - - """The order of the item in the resources list.""" - sortOrder: Float - - """[INTERNAL] The identifiers of the users subscribing to this document.""" - subscriberIds: [String!] - - """[Internal] Related team for the document.""" - teamId: String - - """The title of the document.""" - title: String - - """Whether the document has been trashed.""" - trashed: Boolean -} - -"""A general purpose draft. Used for comments, project updates, etc.""" -type Draft implements Node { - """ - [INTERNAL] Allows for multiple drafts per entity (currently constrained to Pull Requests). - """ - anchor: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The text content as a Prosemirror document.""" - bodyData: JSON! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The customer need that this draft is referencing.""" - customerNeed: CustomerNeed - - """Additional properties for the draft.""" - data: JSONObject - - """The unique identifier of the entity.""" - id: ID! - - """The initiative for which this is a draft initiative update.""" - initiative: Initiative - - """The initiative update for which this is a draft comment.""" - initiativeUpdate: InitiativeUpdate - - """Whether the draft was autogenerated for the user.""" - isAutogenerated: Boolean! @deprecated(reason: "Use 'data.generationMetadata' instead") - - """The issue for which this is a draft comment.""" - issue: Issue - - """The comment for which this is a draft comment reply.""" - parentComment: Comment - - """The post for which this is a draft comment.""" - post: Post - - """The project for which this is a draft project update.""" - project: Project - - """The project update for which this is a draft comment.""" - projectUpdate: ProjectUpdate - - """The team for which this is a draft post.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user who created the draft.""" - user: User! - - """[INTERNAL] Whether the draft was ported from a local draft.""" - wasLocalDraft: Boolean! -} - -type DraftConnection { - edges: [DraftEdge!]! - nodes: [Draft!]! - pageInfo: PageInfo! -} - -type DraftEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Draft! -} - -"""Issue due date sorting options.""" -input DueDateSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -""" -Represents a duration in ISO 8601 format. Accepts ISO 8601 duration strings or integers in milliseconds. -""" -scalar Duration - -"""An email address that can be used for submitting issues.""" -type EmailIntakeAddress implements Node { - """Unique email address user name (before @) used for incoming email.""" - address: String! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the email intake address.""" - creator: User - - """ - Whether issues created from that email address will be turned into customer requests. - """ - customerRequestsEnabled: Boolean! - - """Whether the email address is enabled.""" - enabled: Boolean! - - """The email address used to forward emails to the intake address.""" - forwardingEmailAddress: String - - """The unique identifier of the entity.""" - id: ID! - - """ - The auto-reply message for issue canceled. If not set, the default reply will be used. - """ - issueCanceledAutoReply: String - - """Whether the auto-reply for issue canceled is enabled.""" - issueCanceledAutoReplyEnabled: Boolean! - - """ - The auto-reply message for issue completed. If not set, the default reply will be used. - """ - issueCompletedAutoReply: String - - """Whether the auto-reply for issue completed is enabled.""" - issueCompletedAutoReplyEnabled: Boolean! - - """ - The auto-reply message for issue created. If not set, the default reply will be used. - """ - issueCreatedAutoReply: String - - """The organization that the email address is associated with.""" - organization: Organization! - - """Whether email replies are enabled.""" - repliesEnabled: Boolean! - - """The name to be used for outgoing emails.""" - senderName: String - - """The SES domain identity that the email address is associated with.""" - sesDomainIdentity: SesDomainIdentity - - """The team that the email address is associated with.""" - team: Team - - """The template that the email address is associated with.""" - template: Template - - """The type of the email address.""" - type: EmailIntakeAddressType! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Whether the commenter's name is included in the email replies.""" - useUserNamesInReplies: Boolean! -} - -input EmailIntakeAddressCreateInput { - """Whether customer requests are enabled.""" - customerRequestsEnabled: Boolean - - """The email address used to forward emails to the intake address.""" - forwardingEmailAddress: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The auto-reply message for issue canceled.""" - issueCanceledAutoReply: String - - """Whether the issue canceled auto-reply is enabled.""" - issueCanceledAutoReplyEnabled: Boolean - - """The auto-reply message for issue completed.""" - issueCompletedAutoReply: String - - """Whether the issue completed auto-reply is enabled.""" - issueCompletedAutoReplyEnabled: Boolean - - """The auto-reply message for issue created.""" - issueCreatedAutoReply: String - - """Whether email replies are enabled.""" - repliesEnabled: Boolean - - """The name to be used for outgoing emails.""" - senderName: String - - """ - The identifier or key of the team this email address will intake issues for. - """ - teamId: String - - """ - The identifier of the template this email address will intake issues for. - """ - templateId: String - - """ - The type of the email address. If not provided, the backend will default to team or template. - """ - type: EmailIntakeAddressType - - """Whether the commenter's name is included in the email replies.""" - useUserNamesInReplies: Boolean -} - -type EmailIntakeAddressPayload { - """The email address that was created or updated.""" - emailIntakeAddress: EmailIntakeAddress! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""The type of the email address.""" -enum EmailIntakeAddressType { - asks - team - template -} - -input EmailIntakeAddressUpdateInput { - """Whether customer requests are enabled.""" - customerRequestsEnabled: Boolean - - """ - Whether the email address is currently enabled. If set to false, the email address will be disabled and no longer accept incoming emails. - """ - enabled: Boolean - - """The email address used to forward emails to the intake address.""" - forwardingEmailAddress: String - - """Custom auto-reply message for issue canceled.""" - issueCanceledAutoReply: String - - """Whether the issue canceled auto-reply is enabled.""" - issueCanceledAutoReplyEnabled: Boolean - - """Custom auto-reply message for issue completed.""" - issueCompletedAutoReply: String - - """Whether the issue completed auto-reply is enabled.""" - issueCompletedAutoReplyEnabled: Boolean - - """The auto-reply message for issue created.""" - issueCreatedAutoReply: String - - """Whether email replies are enabled.""" - repliesEnabled: Boolean - - """The name to be used for outgoing emails.""" - senderName: String - - """ - The identifier or key of the team this email address will intake issues for. - """ - teamId: String - - """ - The identifier of the template this email address will intake issues for. - """ - templateId: String - - """Whether the commenter's name is included in the email replies.""" - useUserNamesInReplies: Boolean -} - -input EmailUnsubscribeInput { - """The user's email validation token.""" - token: String! - - """Email type to unsubscribe from.""" - type: String! - - """The identifier of the user.""" - userId: String! -} - -type EmailUnsubscribePayload { - """Whether the operation was successful.""" - success: Boolean! -} - -input EmailUserAccountAuthChallengeInput { - """Auth code for the client initiating the sequence.""" - clientAuthCode: String - - """The email for which to generate the magic login code.""" - email: String! - - """The organization invite link to associate with this authentication.""" - inviteLink: String - - """Whether the login was requested from the desktop app.""" - isDesktop: Boolean - - """ - Whether to only return the login code. This is used by mobile apps to skip showing the login link. - """ - loginCodeOnly: Boolean - - """Signup code.""" - signupCode: String @deprecated(reason: "Not used anymore") -} - -type EmailUserAccountAuthChallengeResponse { - """ - Supported challenge for this user account. Can be either verificationCode or password. - """ - authType: String! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""A custom emoji.""" -type Emoji implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the emoji.""" - creator: User - - """The unique identifier of the entity.""" - id: ID! - - """The emoji's name.""" - name: String! - - """The organization that the emoji belongs to.""" - organization: Organization! - - """The source of the emoji.""" - source: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The emoji image URL.""" - url: String! -} - -type EmojiConnection { - edges: [EmojiEdge!]! - nodes: [Emoji!]! - pageInfo: PageInfo! -} - -input EmojiCreateInput { - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The name of the custom emoji.""" - name: String! - - """The URL for the emoji.""" - url: String! -} - -type EmojiEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Emoji! -} - -type EmojiPayload { - """The emoji that was created.""" - emoji: Emoji! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""A basic entity.""" -interface Entity implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""An external link for an entity like initiative, etc...""" -type EntityExternalLink implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the link.""" - creator: User! - - """The unique identifier of the entity.""" - id: ID! - - """The initiative that the link is associated with.""" - initiative: Initiative - - """The link's label.""" - label: String! - - """The order of the item in the resources list.""" - sortOrder: Float! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The link's URL.""" - url: String! -} - -type EntityExternalLinkConnection { - edges: [EntityExternalLinkEdge!]! - nodes: [EntityExternalLink!]! - pageInfo: PageInfo! -} - -input EntityExternalLinkCreateInput { - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The initiative associated with the link.""" - initiativeId: String - - """The label for the link.""" - label: String! - - """The project associated with the link.""" - projectId: String - - """[Internal] The resource folder containing the link.""" - resourceFolderId: String - - """The order of the item in the entities resources list.""" - sortOrder: Float - - """[Internal] The team associated with the link.""" - teamId: String - - """The URL of the link.""" - url: String! -} - -type EntityExternalLinkEdge { - """Used in `before` and `after` args""" - cursor: String! - node: EntityExternalLink! -} - -type EntityExternalLinkPayload { - """The link that was created or updated.""" - entityExternalLink: EntityExternalLink! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input EntityExternalLinkUpdateInput { - """The label for the link.""" - label: String - - """[Internal] The resource folder containing the link.""" - resourceFolderId: String - - """The order of the item in the entities resources list.""" - sortOrder: Float - - """The URL of the link.""" - url: String -} - -"""Comparator for estimates.""" -input EstimateComparator { - """Compound filters, one of which need to be matched by the estimate.""" - and: [NullableNumberComparator!] - - """Equals constraint.""" - eq: Float - - """ - Greater-than constraint. Matches any values that are greater than the given value. - """ - gt: Float - - """ - Greater-than-or-equal constraint. Matches any values that are greater than or equal to the given value. - """ - gte: Float - - """In-array constraint.""" - in: [Float!] - - """ - Less-than constraint. Matches any values that are less than the given value. - """ - lt: Float - - """ - Less-than-or-equal constraint. Matches any values that are less than or equal to the given value. - """ - lte: Float - - """Not-equals constraint.""" - neq: Float - - """Not-in-array constraint.""" - nin: [Float!] - - """ - Null constraint. Matches any non-null values if the given value is false, otherwise it matches null values. - """ - null: Boolean - - """Compound filters, all of which need to be matched by the estimate.""" - or: [NullableNumberComparator!] -} - -"""Issue estimate sorting options.""" -input EstimateSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Information about an external entity.""" -type ExternalEntityInfo { - """The id of the external entity.""" - id: String! - - """Metadata about the external entity.""" - metadata: ExternalEntityInfoMetadata - - """The name of the service this entity is synced with.""" - service: ExternalSyncService! -} - -"""Metadata about the external GitHub entity.""" -type ExternalEntityInfoGithubMetadata { - """The number of the issue.""" - number: Float - - """The owner of the repository.""" - owner: String - - """The repository name.""" - repo: String -} - -"""Metadata about the external Jira entity.""" -type ExternalEntityInfoJiraMetadata { - """The key of the Jira issue.""" - issueKey: String - - """The id of the Jira issue type.""" - issueTypeId: String - - """The id of the Jira project.""" - projectId: String -} - -union ExternalEntityInfoMetadata = ExternalEntityInfoGithubMetadata | ExternalEntityInfoJiraMetadata | ExternalEntitySlackMetadata - -"""Metadata about the external Slack entity.""" -type ExternalEntitySlackMetadata { - """The id of the Slack channel.""" - channelId: String - - """The name of the Slack channel.""" - channelName: String - - """Whether the entity originated from Slack (not Linear).""" - isFromSlack: Boolean! - - """The URL of the Slack message.""" - messageUrl: String -} - -"""The service that syncs an external entity to Linear.""" -enum ExternalSyncService { - github - jira - slack -} - -""" -An external authenticated (e.g., through Slack) user which doesn't have a Linear account, but can create and update entities in Linear from the external system that authenticated them. -""" -type ExternalUser implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """An URL to the external user's avatar image.""" - avatarUrl: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The external user's display name. Unique within each organization. Can match the display name of an actual user. - """ - displayName: String! - - """The external user's email address.""" - email: String - - """The unique identifier of the entity.""" - id: ID! - - """The last time the external user was seen interacting with Linear.""" - lastSeen: DateTime - - """The external user's full name.""" - name: String! - - """Organization the external user belongs to.""" - organization: Organization! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type ExternalUserConnection { - edges: [ExternalUserEdge!]! - nodes: [ExternalUser!]! - pageInfo: PageInfo! -} - -type ExternalUserEdge { - """Used in `before` and `after` args""" - cursor: String! - node: ExternalUser! -} - -""" -A facet. Facets are joins between entities. A facet can tie a custom view to a project, or a a project to a roadmap for example. -""" -type Facet implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The sort order of the facet.""" - sortOrder: Float! - - """The owning feed user.""" - sourceFeedUser: User - - """The owning initiative.""" - sourceInitiative: Initiative - - """The owning organization.""" - sourceOrganization: Organization - - """The owning page.""" - sourcePage: FacetPageSource - - """The owning project.""" - sourceProject: Project - - """The owning team.""" - sourceTeam: Team - - """The targeted custom view.""" - targetCustomView: CustomView - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -enum FacetPageSource { - feed - projects - teamIssues -} - -"""User favorites presented in the sidebar.""" -type Favorite implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """Children of the favorite. Only applies to favorites of type folder.""" - children( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): FavoriteConnection! - - """ - [Internal] Returns the color of the favorite's icon. Unavailable for avatars and views with fixed icons (e.g. cycle). - """ - color: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The favorited custom view.""" - customView: CustomView - - """The favorited customer.""" - customer: Customer - - """The favorited cycle.""" - cycle: Cycle - - """The favorited dashboard.""" - dashboard: Dashboard - - """ - [Internal] Detail text for favorite's `title` (e.g. team's name for a project). - """ - detail: String - - """The favorited document.""" - document: Document - - """[INTERNAL] The favorited facet.""" - facet: Facet - - """The name of the folder. Only applies to favorites of type folder.""" - folderName: String - - """ - [Internal] Name of the favorite's icon. Unavailable for standard views, issues, and avatars - """ - icon: String - - """The unique identifier of the entity.""" - id: ID! - - """The favorited initiative.""" - initiative: Initiative - - """The targeted tab of the initiative.""" - initiativeTab: InitiativeTab - - """The favorited issue.""" - issue: Issue - - """The favorited label.""" - label: IssueLabel - - """The owner of the favorite.""" - owner: User! - - """The parent folder of the favorite.""" - parent: Favorite - - """The team of the favorited predefined view.""" - predefinedViewTeam: Team - - """The type of favorited predefined view.""" - predefinedViewType: String - - """The favorited project.""" - project: Project - - """The favorited project label.""" - projectLabel: ProjectLabel - - """The targeted tab of the project.""" - projectTab: ProjectTab - - """[DEPRECATED] The favorited team of the project.""" - projectTeam: Team - - """The favorited pull request.""" - pullRequest: PullRequest - - """The order of the item in the favorites list.""" - sortOrder: Float! - - """ - [Internal] Favorite's title text (name of the favorite'd object or folder). - """ - title: String! - - """The type of the favorite.""" - type: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """URL of the favorited entity. Folders return 'null' value.""" - url: String - - """The favorited user.""" - user: User -} - -type FavoriteConnection { - edges: [FavoriteEdge!]! - nodes: [Favorite!]! - pageInfo: PageInfo! -} - -input FavoriteCreateInput { - """The identifier of the custom view to favorite.""" - customViewId: String - - """The identifier of the customer to favorite.""" - customerId: String - - """The identifier of the cycle to favorite.""" - cycleId: String - - """The identifier of the dashboard to favorite.""" - dashboardId: String - - """The identifier of the document to favorite.""" - documentId: String - - """The identifier of the facet to favorite.""" - facetId: String - - """The name of the favorite folder.""" - folderName: String - - """The identifier. If none is provided, the backend will generate one.""" - id: String - - """[INTERNAL] The identifier of the initiative to favorite.""" - initiativeId: String - - """The tab of the initiative to favorite.""" - initiativeTab: InitiativeTab - - """The identifier of the issue to favorite.""" - issueId: String - - """The identifier of the label to favorite.""" - labelId: String - - """The parent folder of the favorite.""" - parentId: String - - """The identifier of team for the predefined view to favorite.""" - predefinedViewTeamId: String - - """The type of the predefined view to favorite.""" - predefinedViewType: String - - """The identifier of the project to favorite.""" - projectId: String - - """The identifier of the label to favorite.""" - projectLabelId: String - - """The tab of the project to favorite.""" - projectTab: ProjectTab - - """The identifier of the pull request to favorite.""" - pullRequestId: String - - """The position of the item in the favorites list.""" - sortOrder: Float - - """The identifier of the user to favorite.""" - userId: String -} - -type FavoriteEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Favorite! -} - -type FavoritePayload { - """The object that was added as a favorite.""" - favorite: Favorite! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input FavoriteUpdateInput { - """The name of the favorite folder.""" - folderName: String - - """ - The identifier (in UUID v4 format) of the folder to move the favorite under. - """ - parentId: String - - """The position of the item in the favorites list.""" - sortOrder: Float -} - -"""[Internal] An item in a users feed.""" -type FeedItem implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The initiative update that is in the feed.""" - initiativeUpdate: InitiativeUpdate - - """The organization that will see this feed item.""" - organization: Organization! - - """The post that is in the feed.""" - post: Post - - """The project update that is in the feed.""" - projectUpdate: ProjectUpdate - - """The team that will see this feed item.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user that will see this feed item.""" - user: User -} - -type FeedItemConnection { - edges: [FeedItemEdge!]! - nodes: [FeedItem!]! - pageInfo: PageInfo! -} - -type FeedItemEdge { - """Used in `before` and `after` args""" - cursor: String! - node: FeedItem! -} - -"""Feed item filtering options""" -input FeedItemFilter { - """Compound filters, all of which need to be matched by the feed item.""" - and: [FeedItemFilter!] - - """Filters that the feed item author must satisfy.""" - author: UserFilter - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Compound filters, one of which need to be matched by the feed item.""" - or: [FeedItemFilter!] - - """Filters that the feed item's project update must satisfy.""" - projectUpdate: ProjectUpdateFilter - - """Filters that the related feed item initiatives must satisfy.""" - relatedInitiatives: InitiativeCollectionFilter - - """Filters that the related feed item team must satisfy.""" - relatedTeams: TeamCollectionFilter - - """ - Comparator for the project or initiative update health: onTrack, atRisk, offTrack - """ - updateHealth: StringComparator - - """Comparator for the update type: initiative, project, team""" - updateType: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Cadence to generate feed summary""" -enum FeedSummarySchedule { - daily - never - weekly -} - -type FetchDataPayload { - """The fetched data based on the natural language query.""" - data: JSONObject - - """The filters used to fetch the data.""" - filters: JSONObject - - """The GraphQL query used to fetch the data.""" - query: String - - """Whether the fetch operation was successful.""" - success: Boolean! -} - -type FileUploadDeletePayload { - """Whether the operation was successful.""" - success: Boolean! -} - -"""By which resolution is frequency defined.""" -enum FrequencyResolutionType { - daily - weekly -} - -type FrontAttachmentPayload { - """The issue attachment that was created.""" - attachment: Attachment! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input FrontSettingsInput { - """ - Whether a ticket should be automatically reopened when its linked Linear issue is cancelled. - """ - automateTicketReopeningOnCancellation: Boolean - - """ - Whether a ticket should be automatically reopened when a comment is posted on its linked Linear issue - """ - automateTicketReopeningOnComment: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear issue is completed. - """ - automateTicketReopeningOnCompletion: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear project is cancelled. - """ - automateTicketReopeningOnProjectCancellation: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear project is completed. - """ - automateTicketReopeningOnProjectCompletion: Boolean - - """ - [ALPHA] Whether customer and customer requests should not be automatically created when conversations are linked to a Linear issue. - """ - disableCustomerRequestsAutoCreation: Boolean - - """ - Whether an internal message should be added when someone comments on an issue. - """ - sendNoteOnComment: Boolean - - """ - Whether an internal message should be added when a Linear issue changes status (for status types except completed or canceled). - """ - sendNoteOnStatusChange: Boolean -} - -"""A trigger that updates the issue status according to Git automations.""" -type GitAutomationState implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - [DEPRECATED] The target branch, if null, the automation will be triggered on any branch. - """ - branchPattern: String @deprecated(reason: "Use targetBranch instead.") - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The event that triggers the automation.""" - event: GitAutomationStates! - - """The unique identifier of the entity.""" - id: ID! - - """The associated workflow state.""" - state: WorkflowState - - """The target branch associated to this automation state.""" - targetBranch: GitAutomationTargetBranch - - """The team to which this automation state belongs.""" - team: Team! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type GitAutomationStateConnection { - edges: [GitAutomationStateEdge!]! - nodes: [GitAutomationState!]! - pageInfo: PageInfo! -} - -input GitAutomationStateCreateInput { - """ - [DEPRECATED] The target branch pattern. If null, all branches are targeted. - """ - branchPattern: String @deprecated(reason: "Use targetBranchId instead.") - - """The event that triggers the automation.""" - event: GitAutomationStates! - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """ - The associated workflow state. If null, will override default behaviour and take no action. - """ - stateId: String - - """The associated target branch. If null, all branches are targeted.""" - targetBranchId: String - - """The team associated with the automation state.""" - teamId: String! -} - -type GitAutomationStateEdge { - """Used in `before` and `after` args""" - cursor: String! - node: GitAutomationState! -} - -type GitAutomationStatePayload { - """The automation state that was created or updated.""" - gitAutomationState: GitAutomationState! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input GitAutomationStateUpdateInput { - """ - [DEPRECATED] The target branch pattern. If null, all branches are targeted. - """ - branchPattern: String @deprecated(reason: "Use targetBranchId instead.") - - """The event that triggers the automation.""" - event: GitAutomationStates - - """The associated workflow state.""" - stateId: String - - """The associated target branch. If null, all branches are targeted.""" - targetBranchId: String -} - -"""The various states of a pull/merge request.""" -enum GitAutomationStates { - draft - merge - mergeable - review - start -} - -""" -A Git target branch for which there are automations (GitAutomationState). -""" -type GitAutomationTargetBranch implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """Automation states associated with the target branch.""" - automationStates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): GitAutomationStateConnection! - - """The target branch pattern.""" - branchPattern: String! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """Whether the branch pattern is a regular expression.""" - isRegex: Boolean! - - """The team to which this Git target branch automation belongs.""" - team: Team! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -input GitAutomationTargetBranchCreateInput { - """The target branch pattern.""" - branchPattern: String! - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """Whether the branch pattern is a regular expression.""" - isRegex: Boolean = false - - """The team associated with the Git target branch automation.""" - teamId: String! -} - -type GitAutomationTargetBranchPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The Git target branch automation that was created or updated.""" - targetBranch: GitAutomationTargetBranch! -} - -input GitAutomationTargetBranchUpdateInput { - """The target branch pattern.""" - branchPattern: String - - """Whether the branch pattern is a regular expression.""" - isRegex: Boolean -} - -type GitHubCommitIntegrationPayload { - """The integration that was created or updated.""" - integration: Integration - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The webhook secret to provide to GitHub.""" - webhookSecret: String! -} - -type GitHubEnterpriseServerInstallVerificationPayload { - """Has the install been successful.""" - success: Boolean! -} - -type GitHubEnterpriseServerPayload { - """The app install address.""" - installUrl: String! - - """The integration that was created or updated.""" - integration: Integration - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The setup address.""" - setupUrl: String! - - """Whether the operation was successful.""" - success: Boolean! - - """The webhook secret to provide to GitHub.""" - webhookSecret: String! -} - -input GitHubImportSettingsInput { - """A map storing all available issue labels per repository""" - labels: JSONObject - - """The avatar URL for the GitHub organization.""" - orgAvatarUrl: String! - - """The GitHub organization's name.""" - orgLogin: String! - - """The type of Github org""" - orgType: GithubOrgType! - - """The names of the repositories connected for the GitHub integration.""" - repositories: [GitHubRepoInput!]! -} - -input GitHubPersonalSettingsInput { - """The GitHub user's name.""" - login: String! -} - -input GitHubRepoInput { - """Whether the repository is archived.""" - archived: Boolean - - """The full name of the repository.""" - fullName: String! - - """The GitHub repo id.""" - id: Float! -} - -input GitHubRepoMappingInput { - """Whether the sync for this mapping is bidirectional.""" - bidirectional: Boolean - - """Whether this mapping is the default one for issue creation.""" - default: Boolean - - """Labels to filter incoming GitHub issue creation by.""" - gitHubLabels: [String!] - - """The GitHub repo id.""" - gitHubRepoId: Float! - - """The unique identifier for this mapping.""" - id: String! - - """The Linear team id to map to the given project.""" - linearTeamId: String! -} - -input GitHubSettingsInput { - """Whether the integration has code access""" - codeAccess: Boolean - - """The avatar URL for the GitHub organization.""" - orgAvatarUrl: String - - """The GitHub organization's name.""" - orgLogin: String! - - """The type of Github org""" - orgType: GithubOrgType - pullRequestReviewTool: PullRequestReviewTool - - """The names of the repositories connected for the GitHub integration.""" - repositories: [GitHubRepoInput!] - - """Mapping of team to repository for syncing.""" - repositoriesMapping: [GitHubRepoMappingInput!] -} - -type GitLabIntegrationCreatePayload { - """The integration that was created or updated.""" - integration: Integration - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The webhook secret to provide to GitLab.""" - webhookSecret: String! -} - -input GitLabSettingsInput { - """The ISO timestamp the GitLab access token expires.""" - expiresAt: String - - """Whether the token is limited to a read-only scope.""" - readonly: Boolean - - """The self-hosted URL of the GitLab instance.""" - url: String -} - -"""[Internal] The kind of link between an issue and a pull request.""" -enum GitLinkKind { - closes - contributes - links -} - -enum GithubOrgType { - organization - user -} - -input GongRecordingImportConfigInput { - """ - The team ID to create issues in for imported recordings. Set to null to disable import. - """ - teamId: String -} - -input GongSettingsInput { - """Configuration for recording import.""" - importConfig: GongRecordingImportConfigInput -} - -input GoogleSheetsExportSettings { - """Whether the export is enabled.""" - enabled: Boolean - - """The ID of the target sheet (tab) within the Google Sheet.""" - sheetId: Float - - """The ID of the exported Google Sheet.""" - spreadsheetId: String - - """The URL of the exported Google Sheet.""" - spreadsheetUrl: String - - """The date of the most recent export.""" - updatedAt: DateTime -} - -input GoogleSheetsSettingsInput { - """The export settings for initiatives.""" - initiative: GoogleSheetsExportSettings - - """The export settings for issues.""" - issue: GoogleSheetsExportSettings - - """The export settings for projects.""" - project: GoogleSheetsExportSettings - - """[Deprecated] The ID of the target sheet (tab) within the Google Sheet.""" - sheetId: Float - - """[Deprecated] The ID of the exported Google Sheet.""" - spreadsheetId: String - - """[Deprecated] The URL of the exported Google Sheet.""" - spreadsheetUrl: String - - """[Deprecated] The date of the most recent export.""" - updatedIssuesAt: DateTime -} - -input GoogleUserAccountAuthInput { - """Code returned from Google's OAuth flow.""" - code: String! - - """ - An optional parameter to disable new user signup and force login. Default: false. - """ - disallowSignup: Boolean - - """ - An optional invite link for an organization used to populate available organizations. - """ - inviteLink: String - - """The URI to redirect the user to.""" - redirectUri: String - - """Signup code.""" - signupCode: String @deprecated(reason: "Not used anymore") - - """The identifiers of the teams to auto-join.""" - teamIdsToJoin: [String!] @deprecated(reason: "Not used anymore") - - """The timezone of the user's browser.""" - timezone: String! -} - -"""Comparator for identifiers.""" -input IDComparator { - """Equals constraint.""" - eq: ID - - """In-array constraint.""" - in: [ID!] - - """Not-equals constraint.""" - neq: ID - - """Not-in-array constraint.""" - nin: [ID!] -} - -"""An identity provider.""" -type IdentityProvider implements Node { - """[INTERNAL] SCIM admins group push settings.""" - adminsGroupPush: JSONObject - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - Whether the identity provider is the default identity provider migrated from organization level settings. - """ - defaultMigrated: Boolean! - - """[INTERNAL] SCIM guests group push settings.""" - guestsGroupPush: JSONObject - - """The unique identifier of the entity.""" - id: ID! - - """The issuer's custom entity ID.""" - issuerEntityId: String - - """ - The SAML priority used to pick default workspace in SAML SP initiated flow, when same domain is claimed for SAML by multiple workspaces. Lower priority value means higher preference. - """ - priority: Float - - """Whether SAML authentication is enabled for organization.""" - samlEnabled: Boolean! - - """Whether SCIM provisioning is enabled for organization.""" - scimEnabled: Boolean! - - """ - Binding method for authentication call. Can be either `post` (default) or `redirect`. - """ - ssoBinding: String - - """Sign in endpoint URL for the identity provider.""" - ssoEndpoint: String - - """ - The algorithm of the Signing Certificate. Can be one of `sha1`, `sha256` (default), or `sha512`. - """ - ssoSignAlgo: String - - """X.509 Signing Certificate in string form.""" - ssoSigningCert: String - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type ImageUploadFromUrlPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The URL containing the image.""" - url: String -} - -input InheritanceEntityMapping { - """Mapping of the IssueLabel ID to the new IssueLabel name.""" - issueLabels: JSONObject - - """Mapping of the WorkflowState ID to the new WorkflowState ID.""" - workflowStates: JSONObject! -} - -"""An initiative to group projects.""" -type Initiative implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The initiative's color.""" - color: String - - """The time at which the initiative was moved into completed status.""" - completedAt: DateTime - - """The initiative's content in markdown format.""" - content: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the initiative.""" - creator: User - - """The description of the initiative.""" - description: String - - """The content of the initiative description.""" - documentContent: DocumentContent - - """Documents associated with the initiative.""" - documents( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned documents.""" - filter: DocumentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): DocumentConnection! - - """[Internal] Facets associated with the initiative.""" - facets: [Facet!]! - - """The resolution of the reminder frequency.""" - frequencyResolution: FrequencyResolutionType! - - """The health of the initiative.""" - health: InitiativeUpdateHealthType - - """The time at which the initiative health was updated.""" - healthUpdatedAt: DateTime - - """History entries associated with the initiative.""" - history( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): InitiativeHistoryConnection! - - """The icon of the initiative.""" - icon: String - - """The unique identifier of the entity.""" - id: ID! - - """Settings for all integrations associated with that initiative.""" - integrationsSettings: IntegrationsSettings - - """The last initiative update posted for this initiative.""" - lastUpdate: InitiativeUpdate - - """Links associated with the initiative.""" - links( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): EntityExternalLinkConnection! - - """The name of the initiative.""" - name: String! - - """The organization of the initiative.""" - organization: Organization! - - """The user who owns the initiative.""" - owner: User - - """Parent initiative associated with the initiative.""" - parentInitiative: Initiative - - """Projects associated with the initiative.""" - projects( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned projects.""" - filter: ProjectFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Whether to include projects from sub-initiatives. Defaults to true.""" - includeSubInitiatives: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned projects.""" - sort: [ProjectSortInput!] - ): ProjectConnection! - - """The initiative's unique URL slug.""" - slugId: String! - - """The sort order of the initiative within the organization.""" - sortOrder: Float! - - """The time at which the initiative was moved into active status.""" - startedAt: DateTime - - """The status of the initiative. One of Planned, Active, Completed""" - status: InitiativeStatus! - - """Sub-initiatives associated with the initiative.""" - subInitiatives( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned sub-initiatives.""" - filter: InitiativeFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned initiatives.""" - sort: [InitiativeSortInput!] - ): InitiativeConnection! - - """The estimated completion date of the initiative.""" - targetDate: TimelessDate - - """The resolution of the initiative's estimated completion date.""" - targetDateResolution: DateResolutionType - - """A flag that indicates whether the initiative is in the trash bin.""" - trashed: Boolean - - """ - The frequency at which to prompt for updates. When not set, reminders are inherited from workspace. - """ - updateReminderFrequency: Float - - """ - The n-weekly frequency at which to prompt for updates. When not set, reminders are inherited from workspace. - """ - updateReminderFrequencyInWeeks: Float - - """The day at which to prompt for updates.""" - updateRemindersDay: Day - - """The hour at which to prompt for updates.""" - updateRemindersHour: Float - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Initiative URL.""" - url: String! -} - -"""A generic payload return from entity archive mutations.""" -type InitiativeArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: Initiative - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Initiative collection filtering options.""" -input InitiativeCollectionFilter { - """Comparator for the initiative activity type.""" - activityType: StringComparator - - """Filters that the initiative must be an ancestor of.""" - ancestors: InitiativeCollectionFilter - - """Compound filters, all of which need to be matched by the initiative.""" - and: [InitiativeCollectionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the initiative creator must satisfy.""" - creator: NullableUserFilter - - """Filters that needs to be matched by all initiatives.""" - every: InitiativeFilter - - """Comparator for the initiative health: onTrack, atRisk, offTrack""" - health: StringComparator - - """ - Comparator for the initiative health (with age): onTrack, atRisk, offTrack, outdated, noUpdate - """ - healthWithAge: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Comparator for the initiative name.""" - name: StringComparator - - """Compound filters, one of which need to be matched by the initiative.""" - or: [InitiativeCollectionFilter!] - - """Filters that the initiative owner must satisfy.""" - owner: NullableUserFilter - - """Comparator for the initiative slug ID.""" - slugId: StringComparator - - """Filters that needs to be matched by some initiatives.""" - some: InitiativeFilter - - """Comparator for the initiative status: Planned, Active, Completed""" - status: StringComparator - - """Comparator for the initiative target date.""" - targetDate: NullableDateComparator - - """Filters that the initiative teams must satisfy.""" - teams: TeamCollectionFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type InitiativeConnection { - edges: [InitiativeEdge!]! - nodes: [Initiative!]! - pageInfo: PageInfo! -} - -"""The properties of the initiative to create.""" -input InitiativeCreateInput { - """The initiative's color.""" - color: String - - """The initiative's content in markdown format.""" - content: String - - """The description of the initiative.""" - description: String - - """The initiative's icon.""" - icon: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The name of the initiative.""" - name: String! - - """The owner of the initiative.""" - ownerId: String - - """The sort order of the initiative within the organization.""" - sortOrder: Float - - """The initiative's status.""" - status: InitiativeStatus - - """The estimated completion date of the initiative.""" - targetDate: TimelessDate - - """The resolution of the initiative's estimated completion date.""" - targetDateResolution: DateResolutionType -} - -"""Initiative creation date sorting options.""" -input InitiativeCreatedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -type InitiativeEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Initiative! -} - -"""Initiative filtering options.""" -input InitiativeFilter { - """Comparator for the initiative activity type.""" - activityType: StringComparator - - """Filters that the initiative must be an ancestor of.""" - ancestors: InitiativeCollectionFilter - - """Compound filters, all of which need to be matched by the initiative.""" - and: [InitiativeFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the initiative creator must satisfy.""" - creator: NullableUserFilter - - """Comparator for the initiative health: onTrack, atRisk, offTrack""" - health: StringComparator - - """ - Comparator for the initiative health (with age): onTrack, atRisk, offTrack, outdated, noUpdate - """ - healthWithAge: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the initiative name.""" - name: StringComparator - - """Compound filters, one of which need to be matched by the initiative.""" - or: [InitiativeFilter!] - - """Filters that the initiative owner must satisfy.""" - owner: NullableUserFilter - - """Comparator for the initiative slug ID.""" - slugId: StringComparator - - """Comparator for the initiative status: Planned, Active, Completed""" - status: StringComparator - - """Comparator for the initiative target date.""" - targetDate: NullableDateComparator - - """Filters that the initiative teams must satisfy.""" - teams: TeamCollectionFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Initiative health sorting options.""" -input InitiativeHealthSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Initiative health update date sorting options.""" -input InitiativeHealthUpdatedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A initiative history containing relevant change events.""" -type InitiativeHistory implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The events that happened while recording that history.""" - entries: JSONObject! - - """The unique identifier of the entity.""" - id: ID! - - """The initiative that the history is associated with.""" - initiative: Initiative! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type InitiativeHistoryConnection { - edges: [InitiativeHistoryEdge!]! - nodes: [InitiativeHistory!]! - pageInfo: PageInfo! -} - -type InitiativeHistoryEdge { - """Used in `before` and `after` args""" - cursor: String! - node: InitiativeHistory! -} - -"""Initiative manual sorting options.""" -input InitiativeManualSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Initiative name sorting options.""" -input InitiativeNameSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""An initiative related notification.""" -type InitiativeNotification implements Entity & Node & Notification { - """The user that caused the notification.""" - actor: User - - """[Internal] Notification actor initials if avatar is not available.""" - actorAvatarColor: String! - - """[Internal] Notification avatar URL.""" - actorAvatarUrl: String - - """[Internal] Notification actor initials if avatar is not available.""" - actorInitials: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The bot that caused the notification.""" - botActor: ActorBot - - """The category of the notification.""" - category: NotificationCategory! - - """The comment related to the notification.""" - comment: Comment - - """ - Related comment ID. Null if the notification is not related to a comment. - """ - commentId: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The document related to the notification.""" - document: Document - - """ - The time at when an email reminder for this notification was sent to the user. Null, if no email - reminder has been sent. - """ - emailedAt: DateTime - - """The external user that caused the notification.""" - externalUserActor: ExternalUser - - """ - [Internal] Notifications with the same grouping key will be grouped together in the UI. - """ - groupingKey: String! - - """ - [Internal] Priority of the notification with the same grouping key. Higher number means higher priority. If priority is the same, notifications should be sorted by `createdAt`. - """ - groupingPriority: Float! - - """The unique identifier of the entity.""" - id: ID! - - """[Internal] Inbox URL for the notification.""" - inboxUrl: String! - - """The initiative related to the notification.""" - initiative: Initiative - - """Related initiative ID.""" - initiativeId: String! - - """The initiative update related to the notification.""" - initiativeUpdate: InitiativeUpdate - - """Related initiative update ID.""" - initiativeUpdateId: String - - """[Internal] If notification actor was Linear.""" - isLinearActor: Boolean! - - """[Internal] Issue's status type for issue notifications.""" - issueStatusType: String - - """ - The parent comment related to the notification, if a notification is a reply comment notification. - """ - parentComment: Comment - - """ - Related parent comment ID. Null if the notification is not related to a comment. - """ - parentCommentId: String - - """[Internal] Project update health for new updates.""" - projectUpdateHealth: String - - """Name of the reaction emoji related to the notification.""" - reactionEmoji: String - - """ - The time at when the user marked the notification as read. Null, if the the user hasn't read the notification - """ - readAt: DateTime - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime - - """[Internal] Notification subtitle.""" - subtitle: String! - - """[Internal] Notification title.""" - title: String! - - """Notification type.""" - type: String! - - """The time at which a notification was unsnoozed..""" - unsnoozedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """[Internal] URL to the target of the notification.""" - url: String! - - """The user that received the notification.""" - user: User! -} - -"""An initiative notification subscription.""" -type InitiativeNotificationSubscription implements Entity & Node & NotificationSubscription { - """Whether the subscription is active or not.""" - active: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The type of view to which the notification subscription context is associated with. - """ - contextViewType: ContextViewType - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The contextual custom view associated with the notification subscription. - """ - customView: CustomView - - """The customer associated with the notification subscription.""" - customer: Customer - - """ - The contextual cycle view associated with the notification subscription. - """ - cycle: Cycle - - """The unique identifier of the entity.""" - id: ID! - - """The initiative subscribed to.""" - initiative: Initiative! - - """ - The contextual label view associated with the notification subscription. - """ - label: IssueLabel - - """The type of subscription.""" - notificationSubscriptionTypes: [String!]! - - """ - The contextual project view associated with the notification subscription. - """ - project: Project - - """The user that subscribed to receive notifications.""" - subscriber: User! - - """The team associated with the notification subscription.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user view associated with the notification subscription.""" - user: User - - """ - The type of user view to which the notification subscription context is associated with. - """ - userContextViewType: UserContextViewType -} - -"""Initiative owner sorting options.""" -input InitiativeOwnerSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""The payload returned by the initiative mutations.""" -type InitiativePayload { - """The initiative that was created or updated.""" - initiative: Initiative! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""A relation representing the dependency between two initiatives.""" -type InitiativeRelation implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The parent initiative.""" - initiative: Initiative! - - """The child initiative.""" - relatedInitiative: Initiative! - - """The sort order of the relation within the initiative.""" - sortOrder: Float! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The last user who created or modified the relation.""" - user: User -} - -type InitiativeRelationConnection { - edges: [InitiativeRelationEdge!]! - nodes: [InitiativeRelation!]! - pageInfo: PageInfo! -} - -input InitiativeRelationCreateInput { - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The identifier of the parent initiative.""" - initiativeId: String! - - """The identifier of the child initiative.""" - relatedInitiativeId: String! - - """The sort order of the initiative relation.""" - sortOrder: Float -} - -type InitiativeRelationEdge { - """Used in `before` and `after` args""" - cursor: String! - node: InitiativeRelation! -} - -type InitiativeRelationPayload { - """The initiative relation that was created or updated.""" - initiativeRelation: InitiativeRelation! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""The properties of the initiativeRelation to update.""" -input InitiativeRelationUpdateInput { - """The sort order of the initiative relation.""" - sortOrder: Float -} - -"""Initiative sorting options.""" -input InitiativeSortInput { - """Sort by initiative creation date.""" - createdAt: InitiativeCreatedAtSort - - """Sort by initiative health status.""" - health: InitiativeHealthSort - - """Sort by initiative health update date.""" - healthUpdatedAt: InitiativeHealthUpdatedAtSort - - """Sort by manual order.""" - manual: InitiativeManualSort - - """Sort by initiative name.""" - name: InitiativeNameSort - - """Sort by initiative owner name.""" - owner: InitiativeOwnerSort - - """Sort by initiative target date.""" - targetDate: InitiativeTargetDateSort - - """Sort by initiative update date.""" - updatedAt: InitiativeUpdatedAtSort -} - -enum InitiativeStatus { - Active - Completed - Planned -} - -"""Different tabs available inside an initiative.""" -enum InitiativeTab { - overview - projects - updates -} - -"""Initiative target date sorting options.""" -input InitiativeTargetDateSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Join table between projects and initiatives.""" -type InitiativeToProject implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The initiative that the project is associated with.""" - initiative: Initiative! - - """The project that the initiative is associated with.""" - project: Project! - - """The sort order of the project within the initiative.""" - sortOrder: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type InitiativeToProjectConnection { - edges: [InitiativeToProjectEdge!]! - nodes: [InitiativeToProject!]! - pageInfo: PageInfo! -} - -"""The properties of the initiativeToProject to create.""" -input InitiativeToProjectCreateInput { - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The identifier of the initiative.""" - initiativeId: String! - - """The identifier of the project.""" - projectId: String! - - """The sort order for the project within its organization.""" - sortOrder: Float -} - -type InitiativeToProjectEdge { - """Used in `before` and `after` args""" - cursor: String! - node: InitiativeToProject! -} - -"""The result of a initiativeToProject mutation.""" -type InitiativeToProjectPayload { - """The initiativeToProject that was created or updated.""" - initiativeToProject: InitiativeToProject! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""The properties of the initiativeToProject to update.""" -input InitiativeToProjectUpdateInput { - """The sort order for the project within its organization.""" - sortOrder: Float -} - -"""An initiative update.""" -type InitiativeUpdate implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The update content in markdown format.""" - body: String! - - """[Internal] The content of the update as a Prosemirror document.""" - bodyData: String! - - """Comments associated with the initiative update.""" - comments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned comments.""" - filter: CommentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CommentConnection! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The diff between the current update and the previous one.""" - diff: JSONObject - - """ - The diff between the current update and the previous one, formatted as markdown. - """ - diffMarkdown: String - - """The time the update was edited.""" - editedAt: DateTime - - """The health at the time of the update.""" - health: InitiativeUpdateHealthType! - - """The unique identifier of the entity.""" - id: ID! - - """ - [Internal] Serialized JSON representing current state of the initiative properties when posting the initiative update. - """ - infoSnapshot: JSONObject - - """The initiative that the update is associated with.""" - initiative: Initiative! - - """Whether initiative update diff should be hidden.""" - isDiffHidden: Boolean! - - """Whether the initiative update is stale.""" - isStale: Boolean! - - """Emoji reaction summary, grouped by emoji type.""" - reactionData: JSONObject! - - """Reactions associated with the initiative update.""" - reactions: [Reaction!]! - - """The update's unique URL slug.""" - slugId: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The URL to the initiative update.""" - url: String! - - """The user who wrote the update.""" - user: User! -} - -"""A generic payload return from entity archive mutations.""" -type InitiativeUpdateArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: InitiativeUpdate - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type InitiativeUpdateConnection { - edges: [InitiativeUpdateEdge!]! - nodes: [InitiativeUpdate!]! - pageInfo: PageInfo! -} - -input InitiativeUpdateCreateInput { - """The content of the update in markdown format.""" - body: String - - """[Internal] The content of the update as a Prosemirror document.""" - bodyData: JSON - - """The health of the initiative at the time of the update.""" - health: InitiativeUpdateHealthType - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The initiative to associate the update with.""" - initiativeId: String! - - """ - Whether the diff between the current update and the previous one should be hidden. - """ - isDiffHidden: Boolean -} - -type InitiativeUpdateEdge { - """Used in `before` and `after` args""" - cursor: String! - node: InitiativeUpdate! -} - -"""Options for filtering initiative updates.""" -input InitiativeUpdateFilter { - """ - Compound filters, all of which need to be matched by the InitiativeUpdate. - """ - and: [InitiativeUpdateFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the initiative update initiative must satisfy.""" - initiative: InitiativeFilter - - """ - Compound filters, one of which need to be matched by the InitiativeUpdate. - """ - or: [InitiativeUpdateFilter!] - - """Filters that the initiative updates reactions must satisfy.""" - reactions: ReactionCollectionFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator - - """Filters that the initiative update creator must satisfy.""" - user: UserFilter -} - -"""The health type when the update is created.""" -enum InitiativeUpdateHealthType { - atRisk - offTrack - onTrack -} - -"""The properties of the initiative to update.""" -input InitiativeUpdateInput { - """The initiative's color.""" - color: String - - """The initiative's content in markdown format.""" - content: String - - """The description of the initiative.""" - description: String - - """The frequency resolution.""" - frequencyResolution: FrequencyResolutionType - - """The initiative's icon.""" - icon: String - - """The name of the initiative.""" - name: String - - """The owner of the initiative.""" - ownerId: String - - """The sort order of the initiative within the organization.""" - sortOrder: Float - - """The initiative's status.""" - status: InitiativeStatus - - """The estimated completion date of the initiative.""" - targetDate: TimelessDate - - """The resolution of the initiative's estimated completion date.""" - targetDateResolution: DateResolutionType - - """Whether the initiative has been trashed.""" - trashed: Boolean - - """ - The frequency at which to prompt for updates. When not set, reminders are inherited from workspace. - """ - updateReminderFrequency: Float - - """ - The n-weekly frequency at which to prompt for updates. When not set, reminders are inherited from workspace. - """ - updateReminderFrequencyInWeeks: Float - - """The day at which to prompt for updates.""" - updateRemindersDay: Day - - """The hour at which to prompt for updates.""" - updateRemindersHour: Int -} - -type InitiativeUpdatePayload { - """The initiative update that was created.""" - initiativeUpdate: InitiativeUpdate! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type InitiativeUpdateReminderPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input InitiativeUpdateUpdateInput { - """The content of the update in markdown format.""" - body: String - - """The content of the update as a Prosemirror document.""" - bodyData: JSON - - """The health of the initiative at the time of the update.""" - health: InitiativeUpdateHealthType - - """ - Whether the diff between the current update and the previous one should be hidden. - """ - isDiffHidden: Boolean -} - -"""Initiative update date sorting options.""" -input InitiativeUpdatedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""An integration with an external service.""" -type Integration implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user that added the integration.""" - creator: User! - - """The unique identifier of the entity.""" - id: ID! - - """The organization that the integration is associated with.""" - organization: Organization! - - """The integration's type.""" - service: String! - - """The team that the integration is associated with.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type IntegrationConnection { - edges: [IntegrationEdge!]! - nodes: [Integration!]! - pageInfo: PageInfo! -} - -input IntegrationCustomerDataAttributesRefreshInput { - """The integration service to refresh customer data attributes from.""" - service: String! -} - -type IntegrationEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Integration! -} - -type IntegrationHasScopesPayload { - """Whether the integration has the required scopes.""" - hasAllScopes: Boolean! - - """The missing scopes.""" - missingScopes: [String!] -} - -type IntegrationPayload { - """The integration that was created or updated.""" - integration: Integration - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input IntegrationRequestInput { - """Email associated with the request.""" - email: String - - """Name of the requested integration.""" - name: String! -} - -type IntegrationRequestPayload { - """Whether the operation was successful.""" - success: Boolean! -} - -"""Linear supported integration services.""" -enum IntegrationService { - airbyte - discord - email - figma - figmaPlugin - front - github - githubCodeAccessPersonal - githubCommit - githubEnterpriseServer - githubImport - githubPersonal - gitlab - gong - googleCalendarPersonal - googleSheets - intercom - jira - jiraPersonal - launchDarkly - launchDarklyPersonal - loom - notion - opsgenie - pagerDuty - salesforce - sentry - slack - slackAsks - slackCustomViewNotifications - slackInitiativePost - slackOrgInitiativeUpdatesPost - slackOrgProjectUpdatesPost - slackPersonal - slackPost - slackProjectPost - slackProjectUpdatesPost - zendesk -} - -input IntegrationSettingsInput { - front: FrontSettingsInput - gitHub: GitHubSettingsInput - gitHubImport: GitHubImportSettingsInput - gitHubPersonal: GitHubPersonalSettingsInput - gitLab: GitLabSettingsInput - gong: GongSettingsInput - googleSheets: GoogleSheetsSettingsInput - intercom: IntercomSettingsInput - jira: JiraSettingsInput - jiraPersonal: JiraPersonalSettingsInput - launchDarkly: LaunchDarklySettingsInput - notion: NotionSettingsInput - opsgenie: OpsgenieInput - pagerDuty: PagerDutyInput - salesforce: SalesforceSettingsInput - sentry: SentrySettingsInput - slack: SlackSettingsInput - slackAsks: SlackAsksSettingsInput - slackCustomViewNotifications: SlackPostSettingsInput - slackInitiativePost: SlackPostSettingsInput - slackOrgInitiativeUpdatesPost: SlackPostSettingsInput - slackOrgProjectUpdatesPost: SlackPostSettingsInput - slackPost: SlackPostSettingsInput - slackProjectPost: SlackPostSettingsInput - zendesk: ZendeskSettingsInput -} - -type IntegrationSlackWorkspaceNamePayload { - """The current name of the Slack workspace.""" - name: String! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Join table between templates and integrations.""" -type IntegrationTemplate implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - ID of the foreign entity in the external integration this template is for, e.g., Slack channel ID. - """ - foreignEntityId: String - - """The unique identifier of the entity.""" - id: ID! - - """The integration that the template is associated with.""" - integration: Integration! - - """The template that the integration is associated with.""" - template: Template! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type IntegrationTemplateConnection { - edges: [IntegrationTemplateEdge!]! - nodes: [IntegrationTemplate!]! - pageInfo: PageInfo! -} - -input IntegrationTemplateCreateInput { - """The foreign identifier in the other service.""" - foreignEntityId: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The identifier of the integration.""" - integrationId: String! - - """The identifier of the template.""" - templateId: String! -} - -type IntegrationTemplateEdge { - """Used in `before` and `after` args""" - cursor: String! - node: IntegrationTemplate! -} - -type IntegrationTemplatePayload { - """The IntegrationTemplate that was created or updated.""" - integrationTemplate: IntegrationTemplate! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input IntegrationUpdateInput { - """The settings to update.""" - settings: IntegrationSettingsInput -} - -"""The configuration of all integrations for different entities.""" -type IntegrationsSettings implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The type of view to which the integration settings context is associated with. - """ - contextViewType: ContextViewType - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """Initiative which those settings apply to.""" - initiative: Initiative - - """Project which those settings apply to.""" - project: Project - - """Whether to send a Slack message when a initiate update is created.""" - slackInitiativeUpdateCreated: Boolean - - """Whether to send a Slack message when a new issue is added to triage.""" - slackIssueAddedToTriage: Boolean - - """ - Whether to send a Slack message when an issue is added to the custom view. - """ - slackIssueAddedToView: Boolean - - """ - Whether to send a Slack message when a new issue is created for the project or the team. - """ - slackIssueCreated: Boolean @deprecated(reason: "No longer in use. Use `slackIssueAddedToView` instead.") - - """ - Whether to send a Slack message when a comment is created on any of the project or team's issues. - """ - slackIssueNewComment: Boolean - - """Whether to send a Slack message when an SLA is breached.""" - slackIssueSlaBreached: Boolean - - """Whether to send a Slack message when an SLA is at high risk.""" - slackIssueSlaHighRisk: Boolean - - """ - Whether to send a Slack message when any of the project or team's issues has a change in status. - """ - slackIssueStatusChangedAll: Boolean - - """ - Whether to send a Slack message when any of the project or team's issues change to completed or cancelled. - """ - slackIssueStatusChangedDone: Boolean - - """Whether to send a Slack message when a project update is created.""" - slackProjectUpdateCreated: Boolean - - """Whether to send a new project update to team Slack channels.""" - slackProjectUpdateCreatedToTeam: Boolean - - """Whether to send a new project update to workspace Slack channel.""" - slackProjectUpdateCreatedToWorkspace: Boolean - - """Team which those settings apply to.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -input IntegrationsSettingsCreateInput { - """ - The type of view to which the integration settings context is associated with. - """ - contextViewType: ContextViewType - - """The identifier of the custom view to create settings for.""" - customViewId: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The identifier of the initiative to create settings for.""" - initiativeId: String - - """The identifier of the project to create settings for.""" - projectId: String - - """Whether to send a Slack message when an initiative update is created.""" - slackInitiativeUpdateCreated: Boolean - - """Whether to send a Slack message when a new issue is added to triage.""" - slackIssueAddedToTriage: Boolean - - """Whether to send a Slack message when an issue is added to a view.""" - slackIssueAddedToView: Boolean - - """ - Whether to send a Slack message when a new issue is created for the project or the team. - """ - slackIssueCreated: Boolean - - """ - Whether to send a Slack message when a comment is created on any of the project or team's issues. - """ - slackIssueNewComment: Boolean - - """Whether to receive notification when an SLA has breached on Slack.""" - slackIssueSlaBreached: Boolean - - """Whether to send a Slack message when an SLA is at high risk.""" - slackIssueSlaHighRisk: Boolean - - """ - Whether to send a Slack message when any of the project or team's issues has a change in status. - """ - slackIssueStatusChangedAll: Boolean - - """ - Whether to send a Slack message when any of the project or team's issues change to completed or cancelled. - """ - slackIssueStatusChangedDone: Boolean - - """Whether to send a Slack message when a project update is created.""" - slackProjectUpdateCreated: Boolean - - """ - Whether to send a Slack message when a project update is created to team channels. - """ - slackProjectUpdateCreatedToTeam: Boolean - - """ - Whether to send a Slack message when a project update is created to workspace channel. - """ - slackProjectUpdateCreatedToWorkspace: Boolean - - """The identifier of the team to create settings for.""" - teamId: String -} - -type IntegrationsSettingsPayload { - """The settings that were created or updated.""" - integrationsSettings: IntegrationsSettings! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input IntegrationsSettingsUpdateInput { - """Whether to send a Slack message when an initiative update is created.""" - slackInitiativeUpdateCreated: Boolean - - """Whether to send a Slack message when a new issue is added to triage.""" - slackIssueAddedToTriage: Boolean - - """Whether to send a Slack message when an issue is added to a view.""" - slackIssueAddedToView: Boolean - - """ - Whether to send a Slack message when a new issue is created for the project or the team. - """ - slackIssueCreated: Boolean - - """ - Whether to send a Slack message when a comment is created on any of the project or team's issues. - """ - slackIssueNewComment: Boolean - - """Whether to receive notification when an SLA has breached on Slack.""" - slackIssueSlaBreached: Boolean - - """Whether to send a Slack message when an SLA is at high risk.""" - slackIssueSlaHighRisk: Boolean - - """ - Whether to send a Slack message when any of the project or team's issues has a change in status. - """ - slackIssueStatusChangedAll: Boolean - - """ - Whether to send a Slack message when any of the project or team's issues change to completed or cancelled. - """ - slackIssueStatusChangedDone: Boolean - - """Whether to send a Slack message when a project update is created.""" - slackProjectUpdateCreated: Boolean - - """ - Whether to send a Slack message when a project update is created to team channels. - """ - slackProjectUpdateCreatedToTeam: Boolean - - """ - Whether to send a Slack message when a project update is created to workspace channel. - """ - slackProjectUpdateCreatedToWorkspace: Boolean -} - -input IntercomSettingsInput { - """ - Whether a ticket should be automatically reopened when its linked Linear issue is cancelled. - """ - automateTicketReopeningOnCancellation: Boolean - - """ - Whether a ticket should be automatically reopened when a comment is posted on its linked Linear issue - """ - automateTicketReopeningOnComment: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear issue is completed. - """ - automateTicketReopeningOnCompletion: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear project is cancelled. - """ - automateTicketReopeningOnProjectCancellation: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear project is completed. - """ - automateTicketReopeningOnProjectCompletion: Boolean - - """ - [ALPHA] Whether customer and customer requests should not be automatically created when conversations are linked to a Linear issue. - """ - disableCustomerRequestsAutoCreation: Boolean - - """ - Whether an internal message should be added when someone comments on an issue. - """ - sendNoteOnComment: Boolean - - """ - Whether an internal message should be added when a Linear issue changes status (for status types except completed or canceled). - """ - sendNoteOnStatusChange: Boolean -} - -"""An issue.""" -type Issue implements Node { - """[Internal] The activity summary information for this issue.""" - activitySummary: JSONObject - - """The time at which the issue was added to a cycle.""" - addedToCycleAt: DateTime - - """The time at which the issue was added to a project.""" - addedToProjectAt: DateTime - - """The time at which the issue was added to a team.""" - addedToTeamAt: DateTime - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The external user who requested creation of the Asks issue on behalf of the creator. - """ - asksExternalUserRequester: ExternalUser - - """ - The internal user who requested creation of the Asks issue on behalf of the creator. - """ - asksRequester: User - - """The user to whom the issue is assigned to.""" - assignee: User - - """Attachments associated with the issue.""" - attachments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned attachments.""" - filter: AttachmentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): AttachmentConnection! - - """ - The time at which the issue was automatically archived by the auto pruning process. - """ - autoArchivedAt: DateTime - - """ - The time at which the issue was automatically closed by the auto pruning process. - """ - autoClosedAt: DateTime - - """The order of the item in its column on the board.""" - boardOrder: Float! @deprecated(reason: "Will be removed in near future, please use `sortOrder` instead") - - """The bot that created the issue, if applicable.""" - botActor: ActorBot - - """Suggested branch name for the issue.""" - branchName: String! - - """The time at which the issue was moved into canceled state.""" - canceledAt: DateTime - - """Children of the issue.""" - children( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """Comments associated with the issue.""" - comments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned comments.""" - filter: CommentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CommentConnection! - - """The time at which the issue was moved into completed state.""" - completedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the issue.""" - creator: User - - """ - Returns the number of Attachment resources which are created by customer support ticketing systems (e.g. Zendesk). - """ - customerTicketCount: Int! - - """The cycle that the issue is associated with.""" - cycle: Cycle - - """The agent user that is delegated to work on this issue.""" - delegate: User - - """The issue's description in markdown format.""" - description: String - - """[Internal] The issue's description content as YJS state.""" - descriptionState: String - - """[ALPHA] The document content representing this issue description.""" - documentContent: DocumentContent - - """The date at which the issue is due.""" - dueDate: TimelessDate - - """The estimate of the complexity of the issue..""" - estimate: Float - - """The external user who created the issue.""" - externalUserCreator: ExternalUser - - """The users favorite associated with this issue.""" - favorite: Favorite - - """ - Attachments previously associated with the issue before being moved to another issue. - """ - formerAttachments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned attachments.""" - filter: AttachmentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): AttachmentConnection! - - """ - Customer needs previously associated with the issue before being moved to another issue. - """ - formerNeeds( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned needs.""" - filter: CustomerNeedFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CustomerNeedConnection! - - """History entries associated with the issue.""" - history( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueHistoryConnection! - - """The unique identifier of the entity.""" - id: ID! - - """Issue's human readable identifier (e.g. ENG-123).""" - identifier: String! - - """ - [Internal] Incoming product intelligence relation suggestions for the issue. - """ - incomingSuggestions( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueSuggestionConnection! - - """Integration type that created this issue, if applicable.""" - integrationSourceType: IntegrationService - - """Inverse relations associated with this issue.""" - inverseRelations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueRelationConnection! - - """Id of the labels associated with this issue.""" - labelIds: [String!]! - - """Labels associated with this issue.""" - labels( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issue labels.""" - filter: IssueLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueLabelConnection! - - """The last template that was applied to this issue.""" - lastAppliedTemplate: Template - - """Customer needs associated with the issue.""" - needs( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned needs.""" - filter: CustomerNeedFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CustomerNeedConnection! - - """The issue's unique number.""" - number: Float! - - """The parent of the issue.""" - parent: Issue - - """Previous identifiers of the issue if it has been moved between teams.""" - previousIdentifiers: [String!]! - - """ - The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: Float! - - """Label for the priority.""" - priorityLabel: String! - - """ - The order of the item in relation to other items in the organization, when ordered by priority. - """ - prioritySortOrder: Float! - - """The project that the issue is associated with.""" - project: Project - - """The projectMilestone that the issue is associated with.""" - projectMilestone: ProjectMilestone - - """Emoji reaction summary, grouped by emoji type.""" - reactionData: JSONObject! - - """Reactions associated with the issue.""" - reactions: [Reaction!]! - - """The recurring issue template that created this issue.""" - recurringIssueTemplate: Template - - """Relations associated with this issue.""" - relations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueRelationConnection! - - """The time at which the issue's SLA will breach.""" - slaBreachesAt: DateTime - - """The time at which the issue's SLA will enter high risk state.""" - slaHighRiskAt: DateTime - - """The time at which the issue's SLA will enter medium risk state.""" - slaMediumRiskAt: DateTime - - """The time at which the issue's SLA began.""" - slaStartedAt: DateTime - - """The type of SLA set on the issue. Calendar days or business days.""" - slaType: String - - """The user who snoozed the issue.""" - snoozedBy: User - - """The time until an issue will be snoozed in Triage view.""" - snoozedUntilAt: DateTime - - """The order of the item in relation to other items in the organization.""" - sortOrder: Float! - - """The comment that this issue was created from.""" - sourceComment: Comment - - """The time at which the issue was moved into started state.""" - startedAt: DateTime - - """The time at which the issue entered triage.""" - startedTriageAt: DateTime - - """The workflow state that the issue is associated with.""" - state: WorkflowState! - - """ - The order of the item in the sub-issue list. Only set if the issue has a parent. - """ - subIssueSortOrder: Float - - """Users who are subscribed to the issue.""" - subscribers( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned subscribers.""" - filter: UserFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Should query return disabled/suspended users (default: false).""" - includeDisabled: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): UserConnection! - - """[Internal] Product Intelligence suggestions for the issue.""" - suggestions( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueSuggestionConnection! - - """ - [Internal] The time at which the most recent suggestions for this issue were generated. - """ - suggestionsGeneratedAt: DateTime - - """The external services the issue is synced with.""" - syncedWith: [ExternalEntityInfo!] - - """The team that the issue is associated with.""" - team: Team! - - """The issue's title.""" - title: String! - - """A flag that indicates whether the issue is in the trash bin.""" - trashed: Boolean - - """The time at which the issue left triage.""" - triagedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Issue URL.""" - url: String! -} - -"""A generic payload return from entity archive mutations.""" -type IssueArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: Issue - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input IssueBatchCreateInput { - """The issues to create.""" - issues: [IssueCreateInput!]! -} - -type IssueBatchPayload { - """The issues that were updated.""" - issues: [Issue!]! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Issue filtering options.""" -input IssueCollectionFilter { - """Comparator for the issues added to cycle at date.""" - addedToCycleAt: NullableDateComparator - - """Comparator for the period when issue was added to a cycle.""" - addedToCyclePeriod: CyclePeriodComparator - - """ - [Internal] Age (created -> now) comparator, defined if the issue is still open. - """ - ageTime: NullableDurationComparator - - """Compound filters, all of which need to be matched by the issue.""" - and: [IssueCollectionFilter!] - - """Comparator for the issues archived at date.""" - archivedAt: NullableDateComparator - - """Filters that the issues assignee must satisfy.""" - assignee: NullableUserFilter - - """Filters that the issues attachments must satisfy.""" - attachments: AttachmentCollectionFilter - - """Comparator for the issues auto archived at date.""" - autoArchivedAt: NullableDateComparator - - """Comparator for the issues auto closed at date.""" - autoClosedAt: NullableDateComparator - - """Comparator for the issues canceled at date.""" - canceledAt: NullableDateComparator - - """Filters that the child issues must satisfy.""" - children: IssueCollectionFilter - - """Filters that the issues comments must satisfy.""" - comments: CommentCollectionFilter - - """Comparator for the issues completed at date.""" - completedAt: NullableDateComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the issues creator must satisfy.""" - creator: NullableUserFilter - - """Count of customers""" - customerCount: NumberComparator - - """Count of important customers""" - customerImportantCount: NumberComparator - - """Filters that the issues cycle must satisfy.""" - cycle: NullableCycleFilter - - """[Internal] Cycle time (started -> completed) comparator.""" - cycleTime: NullableDurationComparator - - """Filters that the issue's delegated agent must satisfy.""" - delegate: NullableUserFilter - - """Comparator for the issues description.""" - description: NullableStringComparator - - """Comparator for the issues due date.""" - dueDate: NullableTimelessDateComparator - - """Comparator for the issues estimate.""" - estimate: EstimateComparator - - """Filters that needs to be matched by all issues.""" - every: IssueFilter - - """Comparator for filtering issues which are blocked.""" - hasBlockedByRelations: RelationExistsComparator - - """Comparator for filtering issues which are blocking.""" - hasBlockingRelations: RelationExistsComparator - - """Comparator for filtering issues which are duplicates.""" - hasDuplicateRelations: RelationExistsComparator - - """Comparator for filtering issues with relations.""" - hasRelatedRelations: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested assignees. - """ - hasSuggestedAssignees: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested labels. - """ - hasSuggestedLabels: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested projects. - """ - hasSuggestedProjects: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested related issues. - """ - hasSuggestedRelatedIssues: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested similar issues. - """ - hasSuggestedSimilarIssues: RelationExistsComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that issue labels must satisfy.""" - labels: IssueLabelCollectionFilter - - """Filters that the last applied template must satisfy.""" - lastAppliedTemplate: NullableTemplateFilter - - """[Internal] Lead time (created -> completed) comparator.""" - leadTime: NullableDurationComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Filters that the issue's customer needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Comparator for the issues number.""" - number: NumberComparator - - """Compound filters, one of which need to be matched by the issue.""" - or: [IssueCollectionFilter!] - - """Filters that the issue parent must satisfy.""" - parent: NullableIssueFilter - - """ - Comparator for the issues priority. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: NullableNumberComparator - - """Filters that the issues project must satisfy.""" - project: NullableProjectFilter - - """Filters that the issues project milestone must satisfy.""" - projectMilestone: NullableProjectMilestoneFilter - - """Filters that the issues reactions must satisfy.""" - reactions: ReactionCollectionFilter - - """[ALPHA] Filters that the recurring issue template must satisfy.""" - recurringIssueTemplate: NullableTemplateFilter - - """[Internal] Comparator for the issues content.""" - searchableContent: ContentComparator - - """Comparator for the issues sla status.""" - slaStatus: SlaStatusComparator - - """Filters that the issues snoozer must satisfy.""" - snoozedBy: NullableUserFilter - - """Comparator for the issues snoozed until date.""" - snoozedUntilAt: NullableDateComparator - - """Filters that needs to be matched by some issues.""" - some: IssueFilter - - """Filters that the source must satisfy.""" - sourceMetadata: SourceMetadataComparator - - """Comparator for the issues started at date.""" - startedAt: NullableDateComparator - - """Filters that the issues state must satisfy.""" - state: WorkflowStateFilter - - """Filters that issue subscribers must satisfy.""" - subscribers: UserCollectionFilter - - """[Internal] Filters that the issue's suggestions must satisfy.""" - suggestions: IssueSuggestionCollectionFilter - - """Filters that the issues team must satisfy.""" - team: TeamFilter - - """Comparator for the issues title.""" - title: StringComparator - - """[Internal] Triage time (entered triaged -> triaged) comparator.""" - triageTime: NullableDurationComparator - - """Comparator for the issues triaged at date.""" - triagedAt: NullableDateComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type IssueConnection { - edges: [IssueEdge!]! - nodes: [Issue!]! - pageInfo: PageInfo! -} - -input IssueCreateInput { - """The identifier of the user to assign the issue to.""" - assigneeId: String - - """The position of the issue in its column on the board view.""" - boardOrder: Float @deprecated(reason: "Will be removed in near future, please use `sortOrder` instead") - - """ - The date when the issue was completed (e.g. if importing from another system). Must be a date in the past and after createdAt date. Cannot be provided with an incompatible workflow state. - """ - completedAt: DateTime - - """ - Create issue as a user with the provided name. This option is only available to OAuth applications creating issues in `actor=app` mode. - """ - createAsUser: String - - """ - The date when the issue was created (e.g. if importing from another system). Must be a date in the past. If none is provided, the backend will generate the time as now. - """ - createdAt: DateTime - - """The cycle associated with the issue.""" - cycleId: String - - """The identifier of the agent user to delegate the issue to.""" - delegateId: String - - """The issue description in markdown format.""" - description: String - - """[Internal] The issue description as a Prosemirror document.""" - descriptionData: JSON - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """The date at which the issue is due.""" - dueDate: TimelessDate - - """The estimated complexity of the issue.""" - estimate: Int - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The identifiers of the issue labels associated with this ticket.""" - labelIds: [String!] - - """The ID of the last template applied to the issue.""" - lastAppliedTemplateId: String - - """The identifier of the parent issue.""" - parentId: String - - """Whether the passed sort order should be preserved.""" - preserveSortOrderOnCreate: Boolean - - """ - The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: Int - - """ - The position of the issue related to other issues, when ordered by priority. - """ - prioritySortOrder: Float - - """The project associated with the issue.""" - projectId: String - - """The project milestone associated with the issue.""" - projectMilestoneId: String - - """The comment the issue is referencing.""" - referenceCommentId: String - - """ - [Internal] The timestamp at which an issue will be considered in breach of SLA. - """ - slaBreachesAt: DateTime - - """[Internal] The timestamp at which the issue's SLA was started.""" - slaStartedAt: DateTime - - """ - The SLA day count type for the issue. Whether SLA should be business days only or calendar days (default). - """ - slaType: SLADayCountType - - """The position of the issue related to other issues.""" - sortOrder: Float - - """The comment the issue is created from.""" - sourceCommentId: String - - """[Internal] The pull request comment the issue is created from.""" - sourcePullRequestCommentId: String - - """The team state of the issue.""" - stateId: String - - """The position of the issue in parent's sub-issue list.""" - subIssueSortOrder: Float - - """The identifiers of the users subscribing to this ticket.""" - subscriberIds: [String!] - - """The identifier of the team associated with the issue.""" - teamId: String! - - """ - The identifier of a template the issue should be created from. If other values are provided in the input, they will override template values. - """ - templateId: String - - """The title of the issue.""" - title: String - - """ - Whether to use the default template for the team. When set to true, the default template of this team based on user's membership will be applied. - """ - useDefaultTemplate: Boolean -} - -"""[Internal] A draft issue.""" -type IssueDraft implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The user assigned to the draft.""" - assigneeId: String - - """Serialized array of JSONs representing attachments.""" - attachments: JSONObject - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the draft.""" - creator: User! - - """The cycle associated with the draft.""" - cycleId: String - - """The agent user delegated to work on the issue being drafted.""" - delegateId: String - - """The draft's description in markdown format.""" - description: String - - """[Internal] The draft's description as a Prosemirror document.""" - descriptionData: JSON - - """The date at which the issue would be due.""" - dueDate: TimelessDate - - """The estimate of the complexity of the draft.""" - estimate: Float - - """The unique identifier of the entity.""" - id: ID! - - """The IDs of labels added to the draft.""" - labelIds: [String!]! - - """Serialized array of JSONs representing customer needs.""" - needs: JSONObject - - """The parent draft of the draft.""" - parent: IssueDraft - - """The ID of the parent issue draft, if any.""" - parentId: String - - """The parent issue of the draft.""" - parentIssue: Issue - - """The ID of the parent issue, if any.""" - parentIssueId: String - - """The priority of the draft.""" - priority: Float! - - """Label for the priority.""" - priorityLabel: String! - - """The project associated with the draft.""" - projectId: String - - """The project milestone associated with the draft.""" - projectMilestoneId: String - - """Serialized array of JSONs representing the recurring issue's schedule.""" - schedule: JSONObject - - """The ID of the comment that the draft was created from.""" - sourceCommentId: String - - """The workflow state associated with the draft.""" - stateId: String! - - """ - The order of items in the sub-draft list. Only set if the draft has `parent` set. - """ - subIssueSortOrder: Float - - """The team associated with the draft.""" - teamId: String! - - """The draft's title.""" - title: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type IssueDraftConnection { - edges: [IssueDraftEdge!]! - nodes: [IssueDraft!]! - pageInfo: PageInfo! -} - -type IssueDraftEdge { - """Used in `before` and `after` args""" - cursor: String! - node: IssueDraft! -} - -type IssueEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Issue! -} - -"""Issue filtering options.""" -input IssueFilter { - """Comparator for the issues added to cycle at date.""" - addedToCycleAt: NullableDateComparator - - """Comparator for the period when issue was added to a cycle.""" - addedToCyclePeriod: CyclePeriodComparator - - """ - [Internal] Age (created -> now) comparator, defined if the issue is still open. - """ - ageTime: NullableDurationComparator - - """Compound filters, all of which need to be matched by the issue.""" - and: [IssueFilter!] - - """Comparator for the issues archived at date.""" - archivedAt: NullableDateComparator - - """Filters that the issues assignee must satisfy.""" - assignee: NullableUserFilter - - """Filters that the issues attachments must satisfy.""" - attachments: AttachmentCollectionFilter - - """Comparator for the issues auto archived at date.""" - autoArchivedAt: NullableDateComparator - - """Comparator for the issues auto closed at date.""" - autoClosedAt: NullableDateComparator - - """Comparator for the issues canceled at date.""" - canceledAt: NullableDateComparator - - """Filters that the child issues must satisfy.""" - children: IssueCollectionFilter - - """Filters that the issues comments must satisfy.""" - comments: CommentCollectionFilter - - """Comparator for the issues completed at date.""" - completedAt: NullableDateComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the issues creator must satisfy.""" - creator: NullableUserFilter - - """Count of customers""" - customerCount: NumberComparator - - """Count of important customers""" - customerImportantCount: NumberComparator - - """Filters that the issues cycle must satisfy.""" - cycle: NullableCycleFilter - - """[Internal] Cycle time (started -> completed) comparator.""" - cycleTime: NullableDurationComparator - - """Filters that the issue's delegated agent must satisfy.""" - delegate: NullableUserFilter - - """Comparator for the issues description.""" - description: NullableStringComparator - - """Comparator for the issues due date.""" - dueDate: NullableTimelessDateComparator - - """Comparator for the issues estimate.""" - estimate: EstimateComparator - - """Comparator for filtering issues which are blocked.""" - hasBlockedByRelations: RelationExistsComparator - - """Comparator for filtering issues which are blocking.""" - hasBlockingRelations: RelationExistsComparator - - """Comparator for filtering issues which are duplicates.""" - hasDuplicateRelations: RelationExistsComparator - - """Comparator for filtering issues with relations.""" - hasRelatedRelations: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested assignees. - """ - hasSuggestedAssignees: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested labels. - """ - hasSuggestedLabels: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested projects. - """ - hasSuggestedProjects: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested related issues. - """ - hasSuggestedRelatedIssues: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested similar issues. - """ - hasSuggestedSimilarIssues: RelationExistsComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that issue labels must satisfy.""" - labels: IssueLabelCollectionFilter - - """Filters that the last applied template must satisfy.""" - lastAppliedTemplate: NullableTemplateFilter - - """[Internal] Lead time (created -> completed) comparator.""" - leadTime: NullableDurationComparator - - """Filters that the issue's customer needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Comparator for the issues number.""" - number: NumberComparator - - """Compound filters, one of which need to be matched by the issue.""" - or: [IssueFilter!] - - """Filters that the issue parent must satisfy.""" - parent: NullableIssueFilter - - """ - Comparator for the issues priority. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: NullableNumberComparator - - """Filters that the issues project must satisfy.""" - project: NullableProjectFilter - - """Filters that the issues project milestone must satisfy.""" - projectMilestone: NullableProjectMilestoneFilter - - """Filters that the issues reactions must satisfy.""" - reactions: ReactionCollectionFilter - - """[ALPHA] Filters that the recurring issue template must satisfy.""" - recurringIssueTemplate: NullableTemplateFilter - - """[Internal] Comparator for the issues content.""" - searchableContent: ContentComparator - - """Comparator for the issues sla status.""" - slaStatus: SlaStatusComparator - - """Filters that the issues snoozer must satisfy.""" - snoozedBy: NullableUserFilter - - """Comparator for the issues snoozed until date.""" - snoozedUntilAt: NullableDateComparator - - """Filters that the source must satisfy.""" - sourceMetadata: SourceMetadataComparator - - """Comparator for the issues started at date.""" - startedAt: NullableDateComparator - - """Filters that the issues state must satisfy.""" - state: WorkflowStateFilter - - """Filters that issue subscribers must satisfy.""" - subscribers: UserCollectionFilter - - """[Internal] Filters that the issue's suggestions must satisfy.""" - suggestions: IssueSuggestionCollectionFilter - - """Filters that the issues team must satisfy.""" - team: TeamFilter - - """Comparator for the issues title.""" - title: StringComparator - - """[Internal] Triage time (entered triaged -> triaged) comparator.""" - triageTime: NullableDurationComparator - - """Comparator for the issues triaged at date.""" - triagedAt: NullableDateComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type IssueFilterSuggestionPayload { - """The json filter that is suggested.""" - filter: JSONObject - - """The log id of the prompt, that created this filter.""" - logId: String -} - -"""A record of changes to an issue.""" -type IssueHistory implements Node { - """ - The actor that performed the actions. This field may be empty in the case of integrations or automations. - """ - actor: User - - """ - The id of user who made these changes. If null, possibly means that the change made by an integration. - """ - actorId: String - - """ - The actors that performed the actions. This field may be empty in the case of integrations or automations. - """ - actors: [User!] @deprecated(reason: "Use `actor` and `descriptionUpdatedBy` instead.") - - """ID's of labels that were added.""" - addedLabelIds: [String!] - - """The labels that were added to the issue.""" - addedLabels: [IssueLabel!] - - """Whether the issue is archived at the time of this history entry.""" - archived: Boolean - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The linked attachment.""" - attachment: Attachment - - """The id of linked attachment.""" - attachmentId: String - - """Whether the issue was auto-archived.""" - autoArchived: Boolean - - """Whether the issue was auto-closed.""" - autoClosed: Boolean - - """The bot that performed the action.""" - botActor: ActorBot - - """ - [Internal] Serialized JSON representing changes for certain non-relational properties. - """ - changes: JSONObject - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The id of linked customer need.""" - customerNeedId: String - - """The actors that edited the description of the issue, if any.""" - descriptionUpdatedBy: [User!] - - """The user that was unassigned from the issue.""" - fromAssignee: User - - """The id of user from whom the issue was re-assigned from.""" - fromAssigneeId: String - - """The cycle that the issue was moved from.""" - fromCycle: Cycle - - """The id of previous cycle of the issue.""" - fromCycleId: String - - """The app user from whom the issue delegation was transferred.""" - fromDelegate: User - - """What the due date was changed from.""" - fromDueDate: TimelessDate - - """What the estimate was changed from.""" - fromEstimate: Float - - """The parent issue that the issue was moved from.""" - fromParent: Issue - - """The id of previous parent of the issue.""" - fromParentId: String - - """What the priority was changed from.""" - fromPriority: Float - - """The project that the issue was moved from.""" - fromProject: Project - - """The id of previous project of the issue.""" - fromProjectId: String - - """The state that the issue was moved from.""" - fromState: WorkflowState - - """The id of previous workflow state of the issue.""" - fromStateId: String - - """The team that the issue was moved from.""" - fromTeam: Team - - """The id of team from which the issue was moved from.""" - fromTeamId: String - - """What the title was changed from.""" - fromTitle: String - - """The unique identifier of the entity.""" - id: ID! - - """The issue that was changed.""" - issue: Issue! - - """The import record.""" - issueImport: IssueImport - - """Changed issue relationships.""" - relationChanges: [IssueRelationHistoryPayload!] - - """ID's of labels that were removed.""" - removedLabelIds: [String!] - - """The labels that were removed from the issue.""" - removedLabels: [IssueLabel!] - - """The user that was assigned to the issue.""" - toAssignee: User - - """The id of user to whom the issue was assigned to.""" - toAssigneeId: String - - """The new project created from the issue.""" - toConvertedProject: Project - - """The id of new project created from the issue.""" - toConvertedProjectId: String - - """The cycle that the issue was moved to.""" - toCycle: Cycle - - """The id of new cycle of the issue.""" - toCycleId: String - - """The app user to whom the issue delegation was transferred.""" - toDelegate: User - - """What the due date was changed to.""" - toDueDate: TimelessDate - - """What the estimate was changed to.""" - toEstimate: Float - - """The parent issue that the issue was moved to.""" - toParent: Issue - - """The id of new parent of the issue.""" - toParentId: String - - """What the priority was changed to.""" - toPriority: Float - - """The project that the issue was moved to.""" - toProject: Project - - """The id of new project of the issue.""" - toProjectId: String - - """The state that the issue was moved to.""" - toState: WorkflowState - - """The id of new workflow state of the issue.""" - toStateId: String - - """The team that the issue was moved to.""" - toTeam: Team - - """The id of team to which the issue was moved to.""" - toTeamId: String - - """What the title was changed to.""" - toTitle: String - - """Whether the issue was trashed or un-trashed.""" - trashed: Boolean - - """The users that were notified of the issue.""" - triageResponsibilityNotifiedUsers: [User!] - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Whether the issue's description was updated.""" - updatedDescription: Boolean -} - -type IssueHistoryConnection { - edges: [IssueHistoryEdge!]! - nodes: [IssueHistory!]! - pageInfo: PageInfo! -} - -type IssueHistoryEdge { - """Used in `before` and `after` args""" - cursor: String! - node: IssueHistory! -} - -"""An import job for data from an external service.""" -type IssueImport implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The id for the user that started the job.""" - creatorId: String - - """File URL for the uploaded CSV for the import, if there is one.""" - csvFileUrl: String - - """The display name of the import service.""" - displayName: String! - - """User readable error message, if one has occurred during the import.""" - error: String - - """Error code and metadata, if one has occurred during the import.""" - errorMetadata: JSONObject - - """The unique identifier of the entity.""" - id: ID! - - """The data mapping configuration for the import job.""" - mapping: JSONObject - - """Current step progress in % (0-100).""" - progress: Float - - """The service from which data will be imported.""" - service: String! - - """Metadata related to import service.""" - serviceMetadata: JSONObject - - """The status for the import job.""" - status: String! - - """New team's name in cases when teamId not set.""" - teamName: String - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type IssueImportCheckPayload { - """Whether the operation was successful.""" - success: Boolean! -} - -type IssueImportDeletePayload { - """The import job that was deleted.""" - issueImport: IssueImport - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Whether a custom JQL query is valid or not""" -type IssueImportJqlCheckPayload { - """ - Returns an approximate number of issues matching the JQL query, if available - """ - count: Float - - """An error message returned by Jira when validating the JQL query.""" - error: String - - """ - Returns true if the JQL query has been validated successfully, false otherwise - """ - success: Boolean! -} - -type IssueImportPayload { - """The import job that was created or updated.""" - issueImport: IssueImport - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Whether an issue import can be synced at the end of an import or not""" -type IssueImportSyncCheckPayload { - """Returns true if the import can be synced, false otherwise""" - canSync: Boolean! - - """An error message with a root cause of why the import cannot be synced""" - error: String -} - -input IssueImportUpdateInput { - """The mapping configuration for the import.""" - mapping: JSONObject! -} - -"""Labels that can be associated with issues.""" -type IssueLabel implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """Children of the label.""" - children( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issue labels.""" - filter: IssueLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueLabelConnection! - - """The label's color as a HEX string.""" - color: String! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the label.""" - creator: User - - """The label's description.""" - description: String - - """The unique identifier of the entity.""" - id: ID! - - """The original label inherited from.""" - inheritedFrom: IssueLabel - - """Whether the label is a group.""" - isGroup: Boolean! - - """Issues associated with the label.""" - issues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """The date when the label was last applied to an issue or project.""" - lastAppliedAt: DateTime - - """The label's name.""" - name: String! - organization: Organization! @deprecated(reason: "Workspace labels are identified by their team being null.") - - """The parent label.""" - parent: IssueLabel - - """ - The team that the label is associated with. If null, the label is associated with the global workspace. - """ - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""Issue label filtering options.""" -input IssueLabelCollectionFilter { - """Compound filters, all of which need to be matched by the label.""" - and: [IssueLabelCollectionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the issue labels creator must satisfy.""" - creator: NullableUserFilter - - """Filters that needs to be matched by all issue labels.""" - every: IssueLabelFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for whether the label is a group label.""" - isGroup: BooleanComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Comparator for the name.""" - name: StringComparator - - """Filter based on the existence of the relation.""" - null: Boolean - - """Compound filters, one of which need to be matched by the label.""" - or: [IssueLabelCollectionFilter!] - - """Filters that the issue label's parent label must satisfy.""" - parent: IssueLabelFilter - - """Filters that needs to be matched by some issue labels.""" - some: IssueLabelFilter - - """Filters that the issue labels team must satisfy.""" - team: NullableTeamFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type IssueLabelConnection { - edges: [IssueLabelEdge!]! - nodes: [IssueLabel!]! - pageInfo: PageInfo! -} - -input IssueLabelCreateInput { - """The color of the label.""" - color: String - - """The description of the label.""" - description: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """Whether the label is a group.""" - isGroup: Boolean - - """The name of the label.""" - name: String! - - """The identifier of the parent label.""" - parentId: String - - """ - The team associated with the label. If not given, the label will be associated with the entire workspace. - """ - teamId: String -} - -type IssueLabelEdge { - """Used in `before` and `after` args""" - cursor: String! - node: IssueLabel! -} - -"""Issue label filtering options.""" -input IssueLabelFilter { - """Compound filters, all of which need to be matched by the label.""" - and: [IssueLabelFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the issue labels creator must satisfy.""" - creator: NullableUserFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for whether the label is a group label.""" - isGroup: BooleanComparator - - """Comparator for the name.""" - name: StringComparator - - """Compound filters, one of which need to be matched by the label.""" - or: [IssueLabelFilter!] - - """Filters that the issue label's parent label must satisfy.""" - parent: IssueLabelFilter - - """Filters that the issue labels team must satisfy.""" - team: NullableTeamFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type IssueLabelPayload { - """The label that was created or updated.""" - issueLabel: IssueLabel! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input IssueLabelUpdateInput { - """The color of the label.""" - color: String - - """The description of the label.""" - description: String - - """Whether the label is a group.""" - isGroup: Boolean - - """The name of the label.""" - name: String - - """The identifier of the parent label.""" - parentId: String -} - -"""An issue related notification.""" -type IssueNotification implements Entity & Node & Notification { - """The user that caused the notification.""" - actor: User - - """[Internal] Notification actor initials if avatar is not available.""" - actorAvatarColor: String! - - """[Internal] Notification avatar URL.""" - actorAvatarUrl: String - - """[Internal] Notification actor initials if avatar is not available.""" - actorInitials: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The bot that caused the notification.""" - botActor: ActorBot - - """The category of the notification.""" - category: NotificationCategory! - - """The comment related to the notification.""" - comment: Comment - - """ - Related comment ID. Null if the notification is not related to a comment. - """ - commentId: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The time at when an email reminder for this notification was sent to the user. Null, if no email - reminder has been sent. - """ - emailedAt: DateTime - - """The external user that caused the notification.""" - externalUserActor: ExternalUser - - """ - [Internal] Notifications with the same grouping key will be grouped together in the UI. - """ - groupingKey: String! - - """ - [Internal] Priority of the notification with the same grouping key. Higher number means higher priority. If priority is the same, notifications should be sorted by `createdAt`. - """ - groupingPriority: Float! - - """The unique identifier of the entity.""" - id: ID! - - """[Internal] Inbox URL for the notification.""" - inboxUrl: String! - - """[Internal] If notification actor was Linear.""" - isLinearActor: Boolean! - - """The issue related to the notification.""" - issue: Issue! - - """Related issue ID.""" - issueId: String! - - """[Internal] Issue's status type for issue notifications.""" - issueStatusType: String - - """ - The parent comment related to the notification, if a notification is a reply comment notification. - """ - parentComment: Comment - - """ - Related parent comment ID. Null if the notification is not related to a comment. - """ - parentCommentId: String - - """[Internal] Project update health for new updates.""" - projectUpdateHealth: String - - """Name of the reaction emoji related to the notification.""" - reactionEmoji: String - - """ - The time at when the user marked the notification as read. Null, if the the user hasn't read the notification - """ - readAt: DateTime - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime - - """The subscriptions related to the notification.""" - subscriptions: [NotificationSubscription!] - - """[Internal] Notification subtitle.""" - subtitle: String! - - """The team related to the issue notification.""" - team: Team! - - """[Internal] Notification title.""" - title: String! - - """Notification type.""" - type: String! - - """The time at which a notification was unsnoozed..""" - unsnoozedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """[Internal] URL to the target of the notification.""" - url: String! - - """The user that received the notification.""" - user: User! -} - -type IssuePayload { - """The issue that was created or updated.""" - issue: Issue - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type IssuePriorityValue { - """Priority's label.""" - label: String! - - """Priority's number value.""" - priority: Int! -} - -"""A relation between two issues.""" -type IssueRelation implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The issue whose relationship is being described.""" - issue: Issue! - - """The related issue.""" - relatedIssue: Issue! - - """The relationship of the issue with the related issue.""" - type: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type IssueRelationConnection { - edges: [IssueRelationEdge!]! - nodes: [IssueRelation!]! - pageInfo: PageInfo! -} - -input IssueRelationCreateInput { - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The identifier of the issue that is related to another issue.""" - issueId: String! - - """The identifier of the related issue.""" - relatedIssueId: String! - - """The type of relation of the issue to the related issue.""" - type: IssueRelationType! -} - -type IssueRelationEdge { - """Used in `before` and `after` args""" - cursor: String! - node: IssueRelation! -} - -"""Issue relation history's payload.""" -type IssueRelationHistoryPayload { - """The identifier of the related issue.""" - identifier: String! - - """The type of the change.""" - type: String! -} - -type IssueRelationPayload { - """The issue relation that was created or updated.""" - issueRelation: IssueRelation! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""The type of the issue relation.""" -enum IssueRelationType { - blocks - duplicate - related - similar -} - -input IssueRelationUpdateInput { - """The identifier of the issue that is related to another issue.""" - issueId: String - - """The identifier of the related issue.""" - relatedIssueId: String - - """The type of relation of the issue to the related issue.""" - type: String -} - -type IssueSearchPayload { - """ - Archived entities matching the search term along with all their dependencies. - """ - archivePayload: ArchiveResponse! - edges: [IssueSearchResultEdge!]! - nodes: [IssueSearchResult!]! - pageInfo: PageInfo! - - """Total number of results for query without filters applied.""" - totalCount: Float! -} - -type IssueSearchResult implements Node { - """[Internal] The activity summary information for this issue.""" - activitySummary: JSONObject - - """The time at which the issue was added to a cycle.""" - addedToCycleAt: DateTime - - """The time at which the issue was added to a project.""" - addedToProjectAt: DateTime - - """The time at which the issue was added to a team.""" - addedToTeamAt: DateTime - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The external user who requested creation of the Asks issue on behalf of the creator. - """ - asksExternalUserRequester: ExternalUser - - """ - The internal user who requested creation of the Asks issue on behalf of the creator. - """ - asksRequester: User - - """The user to whom the issue is assigned to.""" - assignee: User - - """Attachments associated with the issue.""" - attachments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned attachments.""" - filter: AttachmentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): AttachmentConnection! - - """ - The time at which the issue was automatically archived by the auto pruning process. - """ - autoArchivedAt: DateTime - - """ - The time at which the issue was automatically closed by the auto pruning process. - """ - autoClosedAt: DateTime - - """The order of the item in its column on the board.""" - boardOrder: Float! @deprecated(reason: "Will be removed in near future, please use `sortOrder` instead") - - """The bot that created the issue, if applicable.""" - botActor: ActorBot - - """Suggested branch name for the issue.""" - branchName: String! - - """The time at which the issue was moved into canceled state.""" - canceledAt: DateTime - - """Children of the issue.""" - children( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """Comments associated with the issue.""" - comments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned comments.""" - filter: CommentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CommentConnection! - - """The time at which the issue was moved into completed state.""" - completedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the issue.""" - creator: User - - """ - Returns the number of Attachment resources which are created by customer support ticketing systems (e.g. Zendesk). - """ - customerTicketCount: Int! - - """The cycle that the issue is associated with.""" - cycle: Cycle - - """The agent user that is delegated to work on this issue.""" - delegate: User - - """The issue's description in markdown format.""" - description: String - - """[Internal] The issue's description content as YJS state.""" - descriptionState: String - - """[ALPHA] The document content representing this issue description.""" - documentContent: DocumentContent - - """The date at which the issue is due.""" - dueDate: TimelessDate - - """The estimate of the complexity of the issue..""" - estimate: Float - - """The external user who created the issue.""" - externalUserCreator: ExternalUser - - """The users favorite associated with this issue.""" - favorite: Favorite - - """ - Attachments previously associated with the issue before being moved to another issue. - """ - formerAttachments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned attachments.""" - filter: AttachmentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): AttachmentConnection! - - """ - Customer needs previously associated with the issue before being moved to another issue. - """ - formerNeeds( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned needs.""" - filter: CustomerNeedFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CustomerNeedConnection! - - """History entries associated with the issue.""" - history( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueHistoryConnection! - - """The unique identifier of the entity.""" - id: ID! - - """Issue's human readable identifier (e.g. ENG-123).""" - identifier: String! - - """ - [Internal] Incoming product intelligence relation suggestions for the issue. - """ - incomingSuggestions( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueSuggestionConnection! - - """Integration type that created this issue, if applicable.""" - integrationSourceType: IntegrationService - - """Inverse relations associated with this issue.""" - inverseRelations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueRelationConnection! - - """Id of the labels associated with this issue.""" - labelIds: [String!]! - - """Labels associated with this issue.""" - labels( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issue labels.""" - filter: IssueLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueLabelConnection! - - """The last template that was applied to this issue.""" - lastAppliedTemplate: Template - - """Metadata related to search result.""" - metadata: JSONObject! - - """Customer needs associated with the issue.""" - needs( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned needs.""" - filter: CustomerNeedFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CustomerNeedConnection! - - """The issue's unique number.""" - number: Float! - - """The parent of the issue.""" - parent: Issue - - """Previous identifiers of the issue if it has been moved between teams.""" - previousIdentifiers: [String!]! - - """ - The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: Float! - - """Label for the priority.""" - priorityLabel: String! - - """ - The order of the item in relation to other items in the organization, when ordered by priority. - """ - prioritySortOrder: Float! - - """The project that the issue is associated with.""" - project: Project - - """The projectMilestone that the issue is associated with.""" - projectMilestone: ProjectMilestone - - """Emoji reaction summary, grouped by emoji type.""" - reactionData: JSONObject! - - """Reactions associated with the issue.""" - reactions: [Reaction!]! - - """The recurring issue template that created this issue.""" - recurringIssueTemplate: Template - - """Relations associated with this issue.""" - relations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueRelationConnection! - - """The time at which the issue's SLA will breach.""" - slaBreachesAt: DateTime - - """The time at which the issue's SLA will enter high risk state.""" - slaHighRiskAt: DateTime - - """The time at which the issue's SLA will enter medium risk state.""" - slaMediumRiskAt: DateTime - - """The time at which the issue's SLA began.""" - slaStartedAt: DateTime - - """The type of SLA set on the issue. Calendar days or business days.""" - slaType: String - - """The user who snoozed the issue.""" - snoozedBy: User - - """The time until an issue will be snoozed in Triage view.""" - snoozedUntilAt: DateTime - - """The order of the item in relation to other items in the organization.""" - sortOrder: Float! - - """The comment that this issue was created from.""" - sourceComment: Comment - - """The time at which the issue was moved into started state.""" - startedAt: DateTime - - """The time at which the issue entered triage.""" - startedTriageAt: DateTime - - """The workflow state that the issue is associated with.""" - state: WorkflowState! - - """ - The order of the item in the sub-issue list. Only set if the issue has a parent. - """ - subIssueSortOrder: Float - - """Users who are subscribed to the issue.""" - subscribers( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned subscribers.""" - filter: UserFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Should query return disabled/suspended users (default: false).""" - includeDisabled: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): UserConnection! - - """[Internal] Product Intelligence suggestions for the issue.""" - suggestions( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueSuggestionConnection! - - """ - [Internal] The time at which the most recent suggestions for this issue were generated. - """ - suggestionsGeneratedAt: DateTime - - """The external services the issue is synced with.""" - syncedWith: [ExternalEntityInfo!] - - """The team that the issue is associated with.""" - team: Team! - - """The issue's title.""" - title: String! - - """A flag that indicates whether the issue is in the trash bin.""" - trashed: Boolean - - """The time at which the issue left triage.""" - triagedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Issue URL.""" - url: String! -} - -type IssueSearchResultEdge { - """Used in `before` and `after` args""" - cursor: String! - node: IssueSearchResult! -} - -"""Issue sorting options.""" -input IssueSortInput { - """Sort by assignee name""" - assignee: AssigneeSort - - """Sort by issue completion date""" - completedAt: CompletedAtSort - - """Sort by issue creation date""" - createdAt: CreatedAtSort - - """Sort by customer name""" - customer: CustomerSort - - """Sort by number of customers associated with the issue""" - customerCount: CustomerCountSort - - """Sort by number of important customers associated with the issue""" - customerImportantCount: CustomerImportantCountSort - - """Sort by customer revenue""" - customerRevenue: CustomerRevenueSort - - """Sort by Cycle start date""" - cycle: CycleSort - - """Sort by delegate name""" - delegate: DelegateSort - - """Sort by issue due date""" - dueDate: DueDateSort - - """Sort by estimate""" - estimate: EstimateSort - - """Sort by label""" - label: LabelSort - - """Sort by label group""" - labelGroup: LabelGroupSort - - """[ALPHA] Sort by number of links associated with the issue""" - linkCount: LinkCountSort - - """Sort by manual order""" - manual: ManualSort - - """Sort by Project Milestone target date""" - milestone: MilestoneSort - - """Sort by priority""" - priority: PrioritySort - - """Sort by Project name""" - project: ProjectSort - - """Sort by the root issue""" - rootIssue: RootIssueSort - - """Sort by SLA status""" - slaStatus: SlaStatusSort - - """Sort by Team name""" - team: TeamSort - - """Sort by issue title""" - title: TitleSort - - """Sort by issue update date""" - updatedAt: UpdatedAtSort - - """Sort by workflow state type""" - workflowState: WorkflowStateSort -} - -type IssueSuggestion implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - issue: Issue! - issueId: String! - metadata: IssueSuggestionMetadata - state: IssueSuggestionState! - stateChangedAt: DateTime! - suggestedIssue: Issue - suggestedIssueId: String - suggestedLabel: IssueLabel - suggestedLabelId: String - suggestedProject: Project - suggestedTeam: Team - suggestedUser: User - suggestedUserId: String - type: IssueSuggestionType! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""IssueSuggestion collection filtering options.""" -input IssueSuggestionCollectionFilter { - """Compound filters, all of which need to be matched by the suggestion.""" - and: [IssueSuggestionCollectionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that needs to be matched by all suggestions.""" - every: IssueSuggestionFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Compound filters, one of which need to be matched by the suggestion.""" - or: [IssueSuggestionCollectionFilter!] - - """Filters that needs to be matched by some suggestions.""" - some: IssueSuggestionFilter - - """Comparator for the suggestion state.""" - state: StringComparator - - """Filters that the suggested label must satisfy.""" - suggestedLabel: IssueLabelFilter - - """Filters that the suggested project must satisfy.""" - suggestedProject: NullableProjectFilter - - """Filters that the suggested team must satisfy.""" - suggestedTeam: NullableTeamFilter - - """Filters that the suggested user must satisfy.""" - suggestedUser: NullableUserFilter - - """Comparator for the suggestion type.""" - type: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type IssueSuggestionConnection { - edges: [IssueSuggestionEdge!]! - nodes: [IssueSuggestion!]! - pageInfo: PageInfo! -} - -type IssueSuggestionEdge { - """Used in `before` and `after` args""" - cursor: String! - node: IssueSuggestion! -} - -"""IssueSuggestion filtering options.""" -input IssueSuggestionFilter { - """Compound filters, all of which need to be matched by the suggestion.""" - and: [IssueSuggestionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Compound filters, one of which need to be matched by the suggestion.""" - or: [IssueSuggestionFilter!] - - """Comparator for the suggestion state.""" - state: StringComparator - - """Filters that the suggested label must satisfy.""" - suggestedLabel: IssueLabelFilter - - """Filters that the suggested project must satisfy.""" - suggestedProject: NullableProjectFilter - - """Filters that the suggested team must satisfy.""" - suggestedTeam: NullableTeamFilter - - """Filters that the suggested user must satisfy.""" - suggestedUser: NullableUserFilter - - """Comparator for the suggestion type.""" - type: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type IssueSuggestionMetadata { - appliedAutomationRuleId: String - classification: String - evalLogId: String - rank: Float - reasons: [String!] - score: Float - variant: String -} - -enum IssueSuggestionState { - accepted - active - dismissed - stale -} - -enum IssueSuggestionType { - assignee - label - project - relatedIssue - similarIssue - team -} - -type IssueTitleSuggestionFromCustomerRequestPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """[Internal] The log id of the ai response.""" - logId: String - - """The suggested issue title.""" - title: String! -} - -input IssueUpdateInput { - """The identifiers of the issue labels to be added to this issue.""" - addedLabelIds: [String!] - - """The identifier of the user to assign the issue to.""" - assigneeId: String - - """ - Whether the issue was automatically closed because its parent issue was closed. - """ - autoClosedByParentClosing: Boolean - - """The position of the issue in its column on the board view.""" - boardOrder: Float @deprecated(reason: "Will be removed in near future, please use `sortOrder` instead") - - """The cycle associated with the issue.""" - cycleId: String - - """The identifier of the agent user to delegate the issue to.""" - delegateId: String - - """The issue description in markdown format.""" - description: String - - """[Internal] The issue description as a Prosemirror document.""" - descriptionData: JSON - - """The date at which the issue is due.""" - dueDate: TimelessDate - - """The estimated complexity of the issue.""" - estimate: Int - - """The identifiers of the issue labels associated with this ticket.""" - labelIds: [String!] - - """The ID of the last template applied to the issue.""" - lastAppliedTemplateId: String - - """The identifier of the parent issue.""" - parentId: String - - """ - The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: Int - - """ - The position of the issue related to other issues, when ordered by priority. - """ - prioritySortOrder: Float - - """The project associated with the issue.""" - projectId: String - - """The project milestone associated with the issue.""" - projectMilestoneId: String - - """The identifiers of the issue labels to be removed from this issue.""" - removedLabelIds: [String!] - - """ - [Internal] The timestamp at which an issue will be considered in breach of SLA. - """ - slaBreachesAt: DateTime - - """[Internal] The timestamp at which the issue's SLA was started.""" - slaStartedAt: DateTime - - """ - The SLA day count type for the issue. Whether SLA should be business days only or calendar days (default). - """ - slaType: SLADayCountType - - """The identifier of the user who snoozed the issue.""" - snoozedById: String - - """The time until an issue will be snoozed in Triage view.""" - snoozedUntilAt: DateTime - - """The position of the issue related to other issues.""" - sortOrder: Float - - """The team state of the issue.""" - stateId: String - - """The position of the issue in parent's sub-issue list.""" - subIssueSortOrder: Float - - """The identifiers of the users subscribing to this ticket.""" - subscriberIds: [String!] - - """The identifier of the team associated with the issue.""" - teamId: String - - """The issue title.""" - title: String - - """Whether the issue has been trashed.""" - trashed: Boolean -} - -""" -The `JSON` scalar type represents arbitrary values as *stringified* JSON -""" -scalar JSON - -""" -The `JSONObject` scalar type represents arbitrary values as *embedded* JSON -""" -scalar JSONObject - -input JiraConfigurationInput { - """The Jira personal access token.""" - accessToken: String! - - """The Jira user's email address.""" - email: String! - - """The Jira installation hostname.""" - hostname: String! - - """Whether this integration will be setup using the manual webhook flow.""" - manualSetup: Boolean - - """[DEPRECATED] The Jira project keys to scope the integration to.""" - project: String @deprecated(reason: "This parameter is ignored, use mappings instead to scope the integration to one or more specific projects") -} - -input JiraLinearMappingInput { - """Whether the sync for this mapping is bidirectional.""" - bidirectional: Boolean - - """Whether this mapping is the default one for issue creation.""" - default: Boolean - - """The Jira id for this project.""" - jiraProjectId: String! - - """The Linear team id to map to the given project.""" - linearTeamId: String! -} - -input JiraPersonalSettingsInput { - """ - The name of the Jira site currently authorized through the integration. - """ - siteName: String -} - -input JiraProjectDataInput { - """The Jira id for this project.""" - id: String! - - """The Jira key for this project, such as ENG.""" - key: String! - - """The Jira name for this project, such as Engineering.""" - name: String! -} - -input JiraSettingsInput { - """Whether this integration is for Jira Server or not.""" - isJiraServer: Boolean = false - - """ - The label of the Jira instance, for visual identification purposes only - """ - label: String - - """Whether this integration is using a manual setup flow.""" - manualSetup: Boolean - - """The mapping of Jira project id => Linear team id.""" - projectMapping: [JiraLinearMappingInput!] - - """The Jira projects for the organization.""" - projects: [JiraProjectDataInput!]! - - """ - Whether the user needs to provide setup information about the webhook to complete the integration setup. Only relevant for integrations that use a manual setup flow - """ - setupPending: Boolean -} - -input JiraUpdateInput { - """The Jira personal access token.""" - accessToken: String - - """Whether to delete the current manual webhook configuration.""" - deleteWebhook: Boolean - - """The Jira user email address associated with the personal access token.""" - email: String - - """The id of the integration to update.""" - id: String! - - """Whether to refresh Jira metadata for the integration.""" - updateMetadata: Boolean - - """Whether to refresh Jira Projects for the integration.""" - updateProjects: Boolean - - """Webhook secret for a new manual configuration.""" - webhookSecret: String -} - -input JoinOrganizationInput { - """An optional invite link for an organization.""" - inviteLink: String - - """The identifier of the organization.""" - organizationId: String! -} - -"""Issue label-group sorting options.""" -input LabelGroupSort { - """The label-group id to sort by""" - labelGroupId: String! - - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A label notification subscription.""" -type LabelNotificationSubscription implements Entity & Node & NotificationSubscription { - """Whether the subscription is active or not.""" - active: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The type of view to which the notification subscription context is associated with. - """ - contextViewType: ContextViewType - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The contextual custom view associated with the notification subscription. - """ - customView: CustomView - - """The customer associated with the notification subscription.""" - customer: Customer - - """ - The contextual cycle view associated with the notification subscription. - """ - cycle: Cycle - - """The unique identifier of the entity.""" - id: ID! - - """ - The contextual initiative view associated with the notification subscription. - """ - initiative: Initiative - - """The label subscribed to.""" - label: IssueLabel! - - """The type of subscription.""" - notificationSubscriptionTypes: [String!]! - - """ - The contextual project view associated with the notification subscription. - """ - project: Project - - """The user that subscribed to receive notifications.""" - subscriber: User! - - """The team associated with the notification subscription.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user view associated with the notification subscription.""" - user: User - - """ - The type of user view to which the notification subscription context is associated with. - """ - userContextViewType: UserContextViewType -} - -"""Issue label sorting options.""" -input LabelSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -input LaunchDarklySettingsInput { - """The environment of the LaunchDarkly integration.""" - environment: String! - - """The project key of the LaunchDarkly integration.""" - projectKey: String! -} - -"""[ALPHA] Issue link count sorting options.""" -input LinkCountSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -type LogoutResponse { - """Whether the operation was successful.""" - success: Boolean! -} - -"""Issue manual sorting options.""" -input ManualSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Issue project milestone options.""" -input MilestoneSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -type Mutation { - """Creates an agent activity.""" - agentActivityCreate( - """The agent activity object to create.""" - input: AgentActivityCreateInput! - ): AgentActivityPayload! - - """[Internal] Creates a prompt agent activity from Linear user input.""" - agentActivityCreatePrompt( - """The prompt agent activity object to create.""" - input: AgentActivityCreatePromptInput! - ): AgentActivityPayload! - - """Creates a new agent session on a rootcomment.""" - agentSessionCreateOnComment( - """The agent session object to create.""" - input: AgentSessionCreateOnComment! - ): AgentSessionPayload! - - """Creates a new agent session on an issue.""" - agentSessionCreateOnIssue( - """The agent session object to create.""" - input: AgentSessionCreateOnIssue! - ): AgentSessionPayload! - - """ - Updates the externalUrl of an agent session, which is an agent-hosted page associated with this session. - """ - agentSessionUpdateExternalUrl( - """The identifier of the agent session to update.""" - id: String! - - """The agent session object to update.""" - input: AgentSessionUpdateExternalUrlInput! - ): AgentSessionPayload! - - """Creates an integration api key for Airbyte to connect with Linear.""" - airbyteIntegrationConnect( - """Airbyte integration settings.""" - input: AirbyteConfigurationInput! - ): IntegrationPayload! - - """[INTERNAL] Creates a new API key.""" - apiKeyCreate( - """The api key object to create.""" - input: ApiKeyCreateInput! - ): ApiKeyPayload! - - """[INTERNAL] Deletes an API key.""" - apiKeyDelete( - """The identifier of the API key to delete.""" - id: String! - ): DeletePayload! - - """[INTERNAL] Updates an API key's allowed teams.""" - apiKeyUpdate( - """The identifier of the API key to update.""" - id: String! - - """The update input.""" - input: ApiKeyUpdateInput! - ): ApiKeyPayload! - - """ - Creates a new attachment, or updates existing if the same `url` and `issueId` is used. - """ - attachmentCreate( - """The attachment object to create.""" - input: AttachmentCreateInput! - ): AttachmentPayload! - - """Deletes an issue attachment.""" - attachmentDelete( - """The identifier of the attachment to delete.""" - id: String! - ): DeletePayload! - - """Link an existing Discord message to an issue.""" - attachmentLinkDiscord( - """The Discord channel ID for the message to link.""" - channelId: String! - - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """Optional attachment ID that may be provided through the API.""" - id: String - - """The issue for which to link the Discord message.""" - issueId: String! - - """The Discord message ID for the message to link.""" - messageId: String! - - """The title to use for the attachment.""" - title: String - - """The Discord message URL for the message to link.""" - url: String! - ): AttachmentPayload! - - """Link an existing Front conversation to an issue.""" - attachmentLinkFront( - """The Front conversation ID to link.""" - conversationId: String! - - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """Optional attachment ID that may be provided through the API.""" - id: String - - """The issue for which to link the Front conversation.""" - issueId: String! - - """The title to use for the attachment.""" - title: String - ): FrontAttachmentPayload! - - """Link a GitHub issue to a Linear issue.""" - attachmentLinkGitHubIssue( - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """Optional attachment ID that may be provided through the API.""" - id: String - - """The Linear issue for which to link the GitHub issue.""" - issueId: String! - - """The title to use for the attachment.""" - title: String - - """The URL of the GitHub issue to link.""" - url: String! - ): AttachmentPayload! - - """Link a GitHub pull request to an issue.""" - attachmentLinkGitHubPR( - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """Optional attachment ID that may be provided through the API.""" - id: String - - """The issue for which to link the GitHub pull request.""" - issueId: String! - - """[Internal] The kind of link between the issue and the pull request.""" - linkKind: GitLinkKind - - """The GitHub pull request number to link.""" - number: Float @deprecated(reason: "No longer required") - - """The owner of the GitHub repository.""" - owner: String @deprecated(reason: "No longer required") - - """The name of the GitHub repository.""" - repo: String @deprecated(reason: "No longer required") - - """The title to use for the attachment.""" - title: String - - """The URL of the GitHub pull request to link.""" - url: String! - ): AttachmentPayload! - - """Link an existing GitLab MR to an issue.""" - attachmentLinkGitLabMR( - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """Optional attachment ID that may be provided through the API.""" - id: String - - """The issue for which to link the GitLab merge request.""" - issueId: String! - - """The GitLab merge request number to link.""" - number: Float! - - """ - The path name to the project including any (sub)groups. E.g. linear/main/client. - """ - projectPathWithNamespace: String! - - """The title to use for the attachment.""" - title: String - - """The URL of the GitLab merge request to link.""" - url: String! - ): AttachmentPayload! - - """Link an existing Intercom conversation to an issue.""" - attachmentLinkIntercom( - """The Intercom conversation ID to link.""" - conversationId: String! - - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """Optional attachment ID that may be provided through the API.""" - id: String - - """The issue for which to link the Intercom conversation.""" - issueId: String! - - """An optional Intercom conversation part ID to link to""" - partId: String - - """The title to use for the attachment.""" - title: String - ): AttachmentPayload! - - """Link an existing Jira issue to an issue.""" - attachmentLinkJiraIssue( - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """Optional attachment ID that may be provided through the API.""" - id: String - - """The issue for which to link the Jira issue.""" - issueId: String! - - """The Jira issue key or ID to link.""" - jiraIssueId: String! - - """The title to use for the attachment.""" - title: String - - """Optional fallback URL to use if the Jira issue cannot be found.""" - url: String - ): AttachmentPayload! - - """Link an existing Salesforce case to an issue.""" - attachmentLinkSalesforce( - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """Optional attachment ID that may be provided through the API.""" - id: String - - """The issue for which to link the Salesforce case.""" - issueId: String! - - """The title to use for the attachment.""" - title: String - - """The URL of the Salesforce case to link.""" - url: String! - ): AttachmentPayload! - - """Link an existing Slack message to an issue.""" - attachmentLinkSlack( - """[DEPRECATED] The Slack channel ID for the message to link.""" - channel: String @deprecated(reason: "This field is now ignored.") - - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """Optional attachment ID that may be provided through the API.""" - id: String - - """The issue to which to link the Slack message.""" - issueId: String! - - """[DEPRECATED] The latest timestamp for the Slack message.""" - latest: String @deprecated(reason: "This field is now ignored.") - - """ - Whether to begin syncing the message's Slack thread with a comment thread on the issue. - """ - syncToCommentThread: Boolean - - """The title to use for the attachment.""" - title: String - - """ - [DEPRECATED] Unique identifier of either a thread's parent message or a message in the thread. - """ - ts: String @deprecated(reason: "This field is now ignored.") - - """The Slack message URL for the message to link.""" - url: String! - ): AttachmentPayload! - - """Link any url to an issue.""" - attachmentLinkURL( - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """The id for the attachment.""" - id: String - - """The issue for which to link the url.""" - issueId: String! - - """The title to use for the attachment.""" - title: String - - """The url to link.""" - url: String! - ): AttachmentPayload! - - """Link an existing Zendesk ticket to an issue.""" - attachmentLinkZendesk( - """ - Create attachment as a user with the provided name. This option is only available to OAuth applications creating attachments in `actor=app` mode. - """ - createAsUser: String - - """ - Provide an external user avatar URL. Can only be used in conjunction with the `createAsUser` options. This option is only available to OAuth applications creating comments in `actor=app` mode. - """ - displayIconUrl: String - - """Optional attachment ID that may be provided through the API.""" - id: String - - """The issue for which to link the Zendesk ticket.""" - issueId: String! - - """The Zendesk ticket ID to link.""" - ticketId: String! - - """The title to use for the attachment.""" - title: String - - """The URL of the Zendesk ticket to link.""" - url: String - ): AttachmentPayload! - - """ - Begin syncing the thread for an existing Slack message attachment with a comment thread on its issue. - """ - attachmentSyncToSlack( - """The ID of the Slack attachment to begin syncing.""" - id: String! - ): AttachmentPayload! - - """Updates an existing issue attachment.""" - attachmentUpdate( - """The identifier of the attachment to update.""" - id: String! - - """A partial attachment object to update the attachment with.""" - input: AttachmentUpdateInput! - ): AttachmentPayload! - - """Creates a new comment.""" - commentCreate( - """The comment object to create.""" - input: CommentCreateInput! - ): CommentPayload! - - """Deletes a comment.""" - commentDelete( - """The identifier of the comment to delete.""" - id: String! - ): DeletePayload! - - """Resolves a comment.""" - commentResolve( - """The identifier of the comment to update.""" - id: String! - resolvingCommentId: String - ): CommentPayload! - - """Unresolves a comment.""" - commentUnresolve( - """The identifier of the comment to update.""" - id: String! - ): CommentPayload! - - """Updates a comment.""" - commentUpdate( - """The identifier of the comment to update.""" - id: String! - - """A partial comment object to update the comment with.""" - input: CommentUpdateInput! - ): CommentPayload! - - """Saves user message.""" - contactCreate( - """The contact entry to create.""" - input: ContactCreateInput! - ): ContactPayload! - - """[INTERNAL] Saves sales pricing inquiry to Front.""" - contactSalesCreate( - """The contact entry to create.""" - input: ContactSalesCreateInput! - ): ContactPayload! - - """Create CSV export report for the organization.""" - createCsvExportReport(includePrivateTeamIds: [String!]): CreateCsvExportReportPayload! - - """Create a notification to remind a user about an initiative update.""" - createInitiativeUpdateReminder( - """The identifier of the initiative to remind about.""" - initiativeId: String! - - """ - The user identifier to whom the notification will be sent. By default, it is set to the initiative owner. - """ - userId: String - ): InitiativeUpdateReminderPayload! - - """Creates an organization from onboarding.""" - createOrganizationFromOnboarding( - """Organization details for the new organization.""" - input: CreateOrganizationInput! - - """Onboarding survey.""" - survey: OnboardingCustomerSurvey - ): CreateOrJoinOrganizationResponse! - - """Create a notification to remind a user about a project update.""" - createProjectUpdateReminder( - """The identifier of the project to remind about.""" - projectId: String! - - """ - The user identifier to whom the notification will be sent. By default, it is set to the project lead. - """ - userId: String - ): ProjectUpdateReminderPayload! - - """Creates a new custom view.""" - customViewCreate( - """The properties of the custom view to create.""" - input: CustomViewCreateInput! - ): CustomViewPayload! - - """Deletes a custom view.""" - customViewDelete( - """The identifier of the custom view to delete.""" - id: String! - ): DeletePayload! - - """Updates a custom view.""" - customViewUpdate( - """The identifier of the custom view to update.""" - id: String! - - """The properties of the custom view to update.""" - input: CustomViewUpdateInput! - ): CustomViewPayload! - - """Creates a new customer.""" - customerCreate( - """The customer to create.""" - input: CustomerCreateInput! - ): CustomerPayload! - - """Deletes a customer.""" - customerDelete( - """The identifier of the customer to delete.""" - id: String! - ): DeletePayload! - - """Merges two customers.""" - customerMerge( - """ - The ID of the customer to merge. The needs of this customer will be transferred before it gets deleted. - """ - sourceCustomerId: String! - - """ - The ID of the target customer to merge into. The needs of this customer will be retained - """ - targetCustomerId: String! - ): CustomerPayload! - - """Archives a customer need.""" - customerNeedArchive( - """The identifier of the customer need to archive.""" - id: String! - ): CustomerNeedArchivePayload! - - """Creates a new customer need.""" - customerNeedCreate( - """The customer need to create.""" - input: CustomerNeedCreateInput! - ): CustomerNeedPayload! - - """Creates a new customer need out of an attachment""" - customerNeedCreateFromAttachment( - """The customer need to create.""" - input: CustomerNeedCreateFromAttachmentInput! - ): CustomerNeedPayload! - - """Deletes a customer need.""" - customerNeedDelete( - """The identifier of the customer need to delete.""" - id: String! - - """Whether to keep the attachment associated with the customer need.""" - keepAttachment: Boolean - ): DeletePayload! - - """Unarchives a customer need.""" - customerNeedUnarchive( - """The identifier of the customer need to unarchive.""" - id: String! - ): CustomerNeedArchivePayload! - - """Updates a customer need""" - customerNeedUpdate( - """The identifier of the customer need to update.""" - id: String! - - """The properties of the customer need to update.""" - input: CustomerNeedUpdateInput! - ): CustomerNeedUpdatePayload! - - """Creates a new customer status.""" - customerStatusCreate( - """The CustomerStatus object to create.""" - input: CustomerStatusCreateInput! - ): CustomerStatusPayload! - - """Deletes a customer status.""" - customerStatusDelete( - """The identifier of the customer status to delete.""" - id: String! - ): DeletePayload! - - """Updates a customer status.""" - customerStatusUpdate( - """The identifier of the customer status to update.""" - id: String! - - """A partial CustomerStatus object to update the CustomerStatus with.""" - input: CustomerStatusUpdateInput! - ): CustomerStatusPayload! - - """Creates a new customer tier.""" - customerTierCreate( - """The CustomerTier object to create.""" - input: CustomerTierCreateInput! - ): CustomerTierPayload! - - """Deletes a customer tier.""" - customerTierDelete( - """The identifier of the customer tier to delete.""" - id: String! - ): DeletePayload! - - """Updates a customer tier.""" - customerTierUpdate( - """The identifier of the customer tier to update.""" - id: String! - - """A partial CustomerTier object to update the CustomerTier with.""" - input: CustomerTierUpdateInput! - ): CustomerTierPayload! - - """Updates a customer""" - customerUpdate( - """The identifier of the customer to update.""" - id: String! - - """The properties of the customer to update.""" - input: CustomerUpdateInput! - ): CustomerPayload! - - """ - Upserts a customer, creating it if it doesn't exists, updating it otherwise. Matches against an existing customer with `id` or `externalId` - """ - customerUpsert( - """The customer to create.""" - input: CustomerUpsertInput! - ): CustomerPayload! - - """Archives a cycle.""" - cycleArchive( - """The identifier of the cycle to archive.""" - id: String! - ): CycleArchivePayload! - - """Creates a new cycle.""" - cycleCreate( - """The cycle object to create.""" - input: CycleCreateInput! - ): CyclePayload! - - """ - Shifts all cycles starts and ends by a certain number of days, starting from the provided cycle onwards. - """ - cycleShiftAll( - """A partial cycle object to update the cycle with.""" - input: CycleShiftAllInput! - ): CyclePayload! - - """ - Shifts all cycles starts and ends by a certain number of days, starting from the provided cycle onwards. - """ - cycleStartUpcomingCycleToday( - """ - The identifier of the cycle to start as of midnight today. Must be the upcoming cycle. - """ - id: String! - ): CyclePayload! - - """Updates a cycle.""" - cycleUpdate( - """The identifier of the cycle to update.""" - id: String! - - """A partial cycle object to update the cycle with.""" - input: CycleUpdateInput! - ): CyclePayload! - - """Creates a new document.""" - documentCreate( - """The document to create.""" - input: DocumentCreateInput! - ): DocumentPayload! - - """Deletes (trashes) a document.""" - documentDelete( - """The identifier of the document to delete.""" - id: String! - ): DocumentArchivePayload! - - """Restores a document.""" - documentUnarchive( - """The identifier of the document to restore.""" - id: String! - ): DocumentArchivePayload! - - """Updates a document.""" - documentUpdate( - """ - The identifier of the document to update. Also the identifier from the URL is accepted. - """ - id: String! - - """A partial document object to update the document with.""" - input: DocumentUpdateInput! - ): DocumentPayload! - - """Creates a new email intake address.""" - emailIntakeAddressCreate( - """The email intake address object to create.""" - input: EmailIntakeAddressCreateInput! - ): EmailIntakeAddressPayload! - - """Deletes an email intake address object.""" - emailIntakeAddressDelete( - """The identifier of the email intake address to delete.""" - id: String! - ): DeletePayload! - - """Rotates an existing email intake address.""" - emailIntakeAddressRotate( - """The identifier of the email intake address.""" - id: String! - ): EmailIntakeAddressPayload! - - """Updates an existing email intake address.""" - emailIntakeAddressUpdate( - """The identifier of the email intake address.""" - id: String! - - """The properties of the email intake address to update.""" - input: EmailIntakeAddressUpdateInput! - ): EmailIntakeAddressPayload! - - """Authenticates a user account via email and authentication token.""" - emailTokenUserAccountAuth( - """The data used for token authentication.""" - input: TokenUserAccountAuthInput! - ): AuthResolverResponse! - - """Unsubscribes the user from one type of email.""" - emailUnsubscribe( - """Unsubscription details.""" - input: EmailUnsubscribeInput! - ): EmailUnsubscribePayload! - - """ - Finds or creates a new user account by email and sends an email with token. - """ - emailUserAccountAuthChallenge( - """The data used for email authentication.""" - input: EmailUserAccountAuthChallengeInput! - ): EmailUserAccountAuthChallengeResponse! - - """Creates a custom emoji.""" - emojiCreate( - """The emoji object to create.""" - input: EmojiCreateInput! - ): EmojiPayload! - - """Deletes an emoji.""" - emojiDelete( - """The identifier of the emoji to delete.""" - id: String! - ): DeletePayload! - - """Creates a new entity link.""" - entityExternalLinkCreate( - """The entity link object to create.""" - input: EntityExternalLinkCreateInput! - ): EntityExternalLinkPayload! - - """Deletes an entity link.""" - entityExternalLinkDelete( - """The identifier of the entity link to delete.""" - id: String! - ): DeletePayload! - - """Updates an entity link.""" - entityExternalLinkUpdate( - """The identifier of the entity link to update.""" - id: String! - - """The entity link object to update.""" - input: EntityExternalLinkUpdateInput! - ): EntityExternalLinkPayload! - - """Creates a new favorite (project, cycle etc).""" - favoriteCreate( - """The favorite object to create.""" - input: FavoriteCreateInput! - ): FavoritePayload! - - """Deletes a favorite reference.""" - favoriteDelete( - """The identifier of the favorite reference to delete.""" - id: String! - ): DeletePayload! - - """Updates a favorite.""" - favoriteUpdate( - """The identifier of the favorite to update.""" - id: String! - - """A partial favorite object to update the favorite with.""" - input: FavoriteUpdateInput! - ): FavoritePayload! - - """ - XHR request payload to upload an images, video and other attachments directly to Linear's cloud storage. - """ - fileUpload( - """MIME type of the uploaded file.""" - contentType: String! - - """Filename of the uploaded file.""" - filename: String! - - """Should the file be made publicly accessible (default: false).""" - makePublic: Boolean - - """Optional metadata.""" - metaData: JSON - - """File size of the uploaded file.""" - size: Int! - ): UploadPayload! - - """ - [INTERNAL] Permanently delete an uploaded file by asset URL. This should be used as a last resort and will break comments and documents that reference the asset. - """ - fileUploadDangerouslyDelete( - """The asset URL of the uploaded file to delete.""" - assetUrl: String! - ): FileUploadDeletePayload! - - """Creates a new automation state.""" - gitAutomationStateCreate( - """The automation state to create.""" - input: GitAutomationStateCreateInput! - ): GitAutomationStatePayload! - - """Archives an automation state.""" - gitAutomationStateDelete( - """The identifier of the automation state to archive.""" - id: String! - ): DeletePayload! - - """Updates an existing state.""" - gitAutomationStateUpdate( - """The identifier of the state to update.""" - id: String! - - """The state to update.""" - input: GitAutomationStateUpdateInput! - ): GitAutomationStatePayload! - - """Creates a Git target branch automation.""" - gitAutomationTargetBranchCreate( - """The Git target branch automation to create.""" - input: GitAutomationTargetBranchCreateInput! - ): GitAutomationTargetBranchPayload! - - """Archives a Git target branch automation.""" - gitAutomationTargetBranchDelete( - """The identifier of the Git target branch automation to archive.""" - id: String! - ): DeletePayload! - - """Updates an existing Git target branch automation.""" - gitAutomationTargetBranchUpdate( - """The identifier of the Git target branch automation to update.""" - id: String! - - """The updates.""" - input: GitAutomationTargetBranchUpdateInput! - ): GitAutomationTargetBranchPayload! - - """ - Authenticate user account through Google OAuth. This is the 2nd step of OAuth flow. - """ - googleUserAccountAuth( - """The data used for Google authentication.""" - input: GoogleUserAccountAuthInput! - ): AuthResolverResponse! - - """Upload an image from an URL to Linear.""" - imageUploadFromUrl( - """URL of the file to be uploaded to Linear.""" - url: String! - ): ImageUploadFromUrlPayload! - - """ - XHR request payload to upload a file for import, directly to Linear's cloud storage. - """ - importFileUpload( - """MIME type of the uploaded file.""" - contentType: String! - - """Filename of the uploaded file.""" - filename: String! - - """Optional metadata.""" - metaData: JSON - - """File size of the uploaded file.""" - size: Int! - ): UploadPayload! - - """Archives a initiative.""" - initiativeArchive( - """The identifier of the initiative to archive.""" - id: String! - ): InitiativeArchivePayload! - - """Creates a new initiative.""" - initiativeCreate( - """The properties of the initiative to create.""" - input: InitiativeCreateInput! - ): InitiativePayload! - - """Deletes (trashes) an initiative.""" - initiativeDelete( - """The identifier of the initiative to delete.""" - id: String! - ): DeletePayload! - - """Creates a new initiative relation.""" - initiativeRelationCreate( - """The initiative relation to create.""" - input: InitiativeRelationCreateInput! - ): InitiativeRelationPayload! - - """Deletes an initiative relation.""" - initiativeRelationDelete( - """The identifier of the initiative relation to delete.""" - id: String! - ): DeletePayload! - - """Updates an initiative relation.""" - initiativeRelationUpdate( - """The identifier of the initiative relation to update.""" - id: String! - - """The properties of the initiative relation to update.""" - input: InitiativeRelationUpdateInput! - ): DeletePayload! - - """Creates a new initiativeToProject join.""" - initiativeToProjectCreate( - """The properties of the initiativeToProject to create.""" - input: InitiativeToProjectCreateInput! - ): InitiativeToProjectPayload! - - """Deletes a initiativeToProject.""" - initiativeToProjectDelete( - """The identifier of the initiativeToProject to delete.""" - id: String! - ): DeletePayload! - - """Updates a initiativeToProject.""" - initiativeToProjectUpdate( - """The identifier of the initiativeToProject to update.""" - id: String! - - """The properties of the initiativeToProject to update.""" - input: InitiativeToProjectUpdateInput! - ): InitiativeToProjectPayload! - - """Unarchives a initiative.""" - initiativeUnarchive( - """The identifier of the initiative to unarchive.""" - id: String! - ): InitiativeArchivePayload! - - """Updates a initiative.""" - initiativeUpdate( - """The identifier of the initiative to update.""" - id: String! - - """The properties of the initiative to update.""" - input: InitiativeUpdateInput! - ): InitiativePayload! - - """Archives an initiative update.""" - initiativeUpdateArchive( - """The identifier of the initiative update to archive.""" - id: String! - ): InitiativeUpdateArchivePayload! - - """Creates a initiative update.""" - initiativeUpdateCreate( - """The initiative update object to create.""" - input: InitiativeUpdateCreateInput! - ): InitiativeUpdatePayload! - - """Unarchives an initiative update.""" - initiativeUpdateUnarchive( - """The identifier of the initiative update to unarchive.""" - id: String! - ): InitiativeUpdateArchivePayload! - - """Updates an update.""" - initiativeUpdateUpdate( - """The identifier of the update to update.""" - id: String! - - """A data to update the update with.""" - input: InitiativeUpdateUpdateInput! - ): InitiativeUpdatePayload! - - """Archives an integration.""" - integrationArchive( - """The identifier of the integration to archive.""" - id: String! - ): DeletePayload! - - """Connect a Slack channel to Asks.""" - integrationAsksConnectChannel( - """The Slack OAuth code.""" - code: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - ): AsksChannelConnectPayload! - - """ - [INTERNAL] Refreshes the customer data attributes from the specified integration service. - """ - integrationCustomerDataAttributesRefresh( - """The integration service to refresh customer data attributes from.""" - input: IntegrationCustomerDataAttributesRefreshInput! - ): IntegrationPayload! - - """Deletes an integration.""" - integrationDelete( - """The identifier of the integration to delete.""" - id: String! - - """ - Whether to skip deleting the installation on the external service side. - """ - skipInstallationDeletion: Boolean - ): DeletePayload! - - """Integrates the organization with Discord.""" - integrationDiscord( - """The Discord OAuth code.""" - code: String! - - """The Discord OAuth redirect URI.""" - redirectUri: String! - ): IntegrationPayload! - - """Integrates the organization with Figma.""" - integrationFigma( - """The Figma OAuth code.""" - code: String! - - """The Figma OAuth redirect URI.""" - redirectUri: String! - ): IntegrationPayload! - - """Integrates the organization with Front.""" - integrationFront( - """The Front OAuth code.""" - code: String! - - """The Front OAuth redirect URI.""" - redirectUri: String! - ): IntegrationPayload! - - """Connects the organization with a GitHub Enterprise Server.""" - integrationGitHubEnterpriseServerConnect( - """The base URL of the GitHub Enterprise Server installation.""" - githubUrl: String! - - """The name of GitHub organization.""" - organizationName: String! - ): GitHubEnterpriseServerPayload! - - """Connect your GitHub account to Linear.""" - integrationGitHubPersonal( - """The GitHub OAuth code.""" - code: String! - - """Whether to connect with code access.""" - codeAccess: Boolean - ): IntegrationPayload! - - """Generates a webhook for the GitHub commit integration.""" - integrationGithubCommitCreate: GitHubCommitIntegrationPayload! - - """Connects the organization with the GitHub App.""" - integrationGithubConnect( - """The GitHub grant code that's exchanged for OAuth tokens.""" - code: String! - - """Whether the integration should have code access""" - codeAccess: Boolean = false - - """The GitHub data to connect with.""" - installationId: String! - ): IntegrationPayload! - - """Connects the organization with the GitHub Import App.""" - integrationGithubImportConnect( - """The GitHub grant code that's exchanged for OAuth tokens.""" - code: String! - - """The GitHub data to connect with.""" - installationId: String! - ): IntegrationPayload! - - """Refreshes the data for a GitHub import integration.""" - integrationGithubImportRefresh( - """The id of the integration to update.""" - id: String! - ): IntegrationPayload! - - """Connects the organization with a GitLab Access Token.""" - integrationGitlabConnect( - """The GitLab Access Token to connect with.""" - accessToken: String! - - """The URL of the GitLab installation.""" - gitlabUrl: String! - ): GitLabIntegrationCreatePayload! - - """Integrates the organization with Gong.""" - integrationGong( - """The Gong OAuth code.""" - code: String! - - """The Gong OAuth redirect URI.""" - redirectUri: String! - ): IntegrationPayload! - - """ - [Internal] Connects the Google Calendar to the user to this Linear account via OAuth2. - """ - integrationGoogleCalendarPersonalConnect( - """[Internal] The Google OAuth code.""" - code: String! - ): IntegrationPayload! - - """Integrates the organization with Google Sheets.""" - integrationGoogleSheets( - """The Google OAuth code.""" - code: String! - ): IntegrationPayload! - - """Integrates the organization with Intercom.""" - integrationIntercom( - """The Intercom OAuth code.""" - code: String! - - """ - The Intercom domain URL to use for the integration. Defaults to app.intercom.com if not provided. - """ - domainUrl: String - - """The Intercom OAuth redirect URI.""" - redirectUri: String! - ): IntegrationPayload! - - """Disconnects the organization from Intercom.""" - integrationIntercomDelete: IntegrationPayload! - - """[DEPRECATED] Updates settings on the Intercom integration.""" - integrationIntercomSettingsUpdate( - """ - A partial Intercom integration settings object to update the integration settings with. - """ - input: IntercomSettingsInput! - ): IntegrationPayload! @deprecated(reason: "This mutation is deprecated, please use `integrationSettingsUpdate` instead") - - """Connect your Jira account to Linear.""" - integrationJiraPersonal( - """The Jira personal access token, when connecting using a PAT.""" - accessToken: String - - """The Jira OAuth code, when connecting using OAuth.""" - code: String - ): IntegrationPayload! - - """[INTERNAL] Updates a Jira Integration.""" - integrationJiraUpdate( - """Jira integration update input.""" - input: JiraUpdateInput! - ): IntegrationPayload! - - """[INTERNAL] Integrates the organization with LaunchDarkly.""" - integrationLaunchDarklyConnect( - """The LaunchDarkly OAuth code.""" - code: String! - - """The LaunchDarkly environment.""" - environment: String! - - """The LaunchDarkly project key.""" - projectKey: String! - ): IntegrationPayload! - - """[INTERNAL] Integrates your personal account with LaunchDarkly.""" - integrationLaunchDarklyPersonalConnect( - """The LaunchDarkly OAuth code.""" - code: String! - ): IntegrationPayload! - - """Enables Loom integration for the organization.""" - integrationLoom: IntegrationPayload! @deprecated(reason: "Not available.") - - """[INTERNAL] Integrates the organization with Opsgenie.""" - integrationOpsgenieConnect( - """The Opsgenie API key.""" - apiKey: String! - ): IntegrationPayload! - - """[INTERNAL] Refresh Opsgenie schedule mappings.""" - integrationOpsgenieRefreshScheduleMappings: IntegrationPayload! - - """[INTERNAL] Integrates the organization with PagerDuty.""" - integrationPagerDutyConnect( - """The PagerDuty OAuth code.""" - code: String! - - """The PagerDuty OAuth redirect URI.""" - redirectUri: String! - ): IntegrationPayload! - - """[INTERNAL] Refresh PagerDuty schedule mappings.""" - integrationPagerDutyRefreshScheduleMappings: IntegrationPayload! - - """Requests a currently unavailable integration.""" - integrationRequest( - """Integration request details.""" - input: IntegrationRequestInput! - ): IntegrationRequestPayload! - - """Integrates the organization with Salesforce.""" - integrationSalesforce( - """The Salesforce OAuth code.""" - code: String! - - """The Salesforce OAuth redirect URI.""" - redirectUri: String! - - """The Salesforce installation subdomain.""" - subdomain: String! - ): IntegrationPayload! - - """[INTERNAL] Refreshes the Salesforce integration metadata.""" - integrationSalesforceMetadataRefresh( - """The ID of the integration to refresh metadata for.""" - id: String! - ): IntegrationPayload! - - """Integrates the organization with Sentry.""" - integrationSentryConnect( - """The Sentry grant code that's exchanged for OAuth tokens.""" - code: String! - - """The Sentry installationId to connect with.""" - installationId: String! - - """The slug of the Sentry organization being connected.""" - organizationSlug: String! - ): IntegrationPayload! - - """[INTERNAL] Updates the integration settings.""" - integrationSettingsUpdate( - """The identifier of the integration to update.""" - id: String! - - """An integration settings object.""" - input: IntegrationSettingsInput! - ): IntegrationPayload! @deprecated(reason: "Use integrationUpdate instead.") - - """Integrates the organization with Slack.""" - integrationSlack( - """The Slack OAuth code.""" - code: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - - """ - [DEPRECATED] Whether or not v2 of Slack OAuth should be used. No longer used. - """ - shouldUseV2Auth: Boolean - ): IntegrationPayload! - - """Integrates the organization with the Slack Asks app.""" - integrationSlackAsks( - """The Slack OAuth code.""" - code: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - ): IntegrationPayload! - - """Slack integration for custom view notifications.""" - integrationSlackCustomViewNotifications( - """The Slack OAuth code.""" - code: String! - - """Integration's associated custom view.""" - customViewId: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - ): SlackChannelConnectPayload! - - """Integrates a Slack Asks channel with a Customer.""" - integrationSlackCustomerChannelLink( - """The Slack OAuth code.""" - code: String! - - """The customer to link the Slack channel with""" - customerId: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - ): SuccessPayload! - - """Imports custom emojis from your Slack workspace.""" - integrationSlackImportEmojis( - """The Slack OAuth code.""" - code: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - ): IntegrationPayload! - - """[Internal] Slack integration for initiative notifications.""" - integrationSlackInitiativePost( - """The Slack OAuth code.""" - code: String! - - """Integration's associated initiative.""" - initiativeId: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - ): SlackChannelConnectPayload! - - """ - Updates the Slack team's name in Linear for an existing Slack or Asks integration. - """ - integrationSlackOrAsksUpdateSlackTeamName( - """The integration ID.""" - integrationId: String! - ): IntegrationSlackWorkspaceNamePayload! - - """ - [Internal] Slack integration for organization level initiative update notifications. - """ - integrationSlackOrgInitiativeUpdatesPost( - """The Slack OAuth code.""" - code: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - ): SlackChannelConnectPayload! - - """Slack integration for organization level project update notifications.""" - integrationSlackOrgProjectUpdatesPost( - """The Slack OAuth code.""" - code: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - ): SlackChannelConnectPayload! - - """Integrates your personal notifications with Slack.""" - integrationSlackPersonal( - """The Slack OAuth code.""" - code: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - ): IntegrationPayload! - - """Slack integration for team notifications.""" - integrationSlackPost( - """The Slack OAuth code.""" - code: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - - """ - [DEPRECATED] Whether or not v2 of Slack OAuth should be used. No longer used. - """ - shouldUseV2Auth: Boolean - - """Integration's associated team.""" - teamId: String! - ): SlackChannelConnectPayload! - - """Slack integration for project notifications.""" - integrationSlackProjectPost( - """The Slack OAuth code.""" - code: String! - - """Integration's associated project.""" - projectId: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - - """ - The service to enable once connected, either 'notifications' or 'updates'. - """ - service: String! - ): SlackChannelConnectPayload! - - """Creates a new integrationTemplate join.""" - integrationTemplateCreate( - """The properties of the integrationTemplate to create.""" - input: IntegrationTemplateCreateInput! - ): IntegrationTemplatePayload! - - """Deletes a integrationTemplate.""" - integrationTemplateDelete( - """The identifier of the integrationTemplate to delete.""" - id: String! - ): DeletePayload! - - """[INTERNAL] Updates the integration.""" - integrationUpdate( - """The identifier of the integration to update.""" - id: String! - - """A partial integration object to update the integration with.""" - input: IntegrationUpdateInput! - ): IntegrationPayload! - - """Integrates the organization with Zendesk.""" - integrationZendesk( - """The Zendesk OAuth code.""" - code: String! - - """The Zendesk OAuth redirect URI.""" - redirectUri: String! - - """The Zendesk OAuth scopes.""" - scope: String! - - """The Zendesk installation subdomain.""" - subdomain: String! - ): IntegrationPayload! - - """Creates new settings for one or more integrations.""" - integrationsSettingsCreate( - """The settings to create.""" - input: IntegrationsSettingsCreateInput! - ): IntegrationsSettingsPayload! - - """Updates settings related to integrations for a project or a team.""" - integrationsSettingsUpdate( - """The identifier of the settings to update.""" - id: String! - - """A settings object to update the settings with.""" - input: IntegrationsSettingsUpdateInput! - ): IntegrationsSettingsPayload! - - """Adds a label to an issue.""" - issueAddLabel( - """The identifier of the issue to add the label to.""" - id: String! - - """The identifier of the label to add.""" - labelId: String! - ): IssuePayload! - - """Archives an issue.""" - issueArchive( - """The identifier of the issue to archive.""" - id: String! - - """Whether to trash the issue.""" - trash: Boolean - ): IssueArchivePayload! - - """Creates a list of issues in one transaction.""" - issueBatchCreate( - """A list of issue objects to create.""" - input: IssueBatchCreateInput! - ): IssueBatchPayload! - - """Updates multiple issues at once.""" - issueBatchUpdate( - """The id's of the issues to update. Can't be more than 50 at a time.""" - ids: [UUID!]! - - """A partial issue object to update the issues with.""" - input: IssueUpdateInput! - ): IssueBatchPayload! - - """Creates a new issue.""" - issueCreate( - """The issue object to create.""" - input: IssueCreateInput! - ): IssuePayload! - - """Deletes (trashes) an issue.""" - issueDelete( - """The identifier of the issue to delete.""" - id: String! - - """ - Whether to permanently delete the issue and skip the grace period of 30 days. Available only to admins! - """ - permanentlyDelete: Boolean - ): IssueArchivePayload! - - """ - [INTERNAL] Updates an issue description from the Front app to handle Front attachments correctly. - """ - issueDescriptionUpdateFromFront( - """Description to update the issue with. """ - description: String! - - """The identifier of the issue to update.""" - id: String! - ): IssuePayload! - - """Kicks off an Asana import job.""" - issueImportCreateAsana( - """Asana team name to choose which issues we should import.""" - asanaTeamName: String! - - """Asana token to fetch information from the Asana API.""" - asanaToken: String! - - """ID of issue import. If not provided it will be generated.""" - id: String - - """Whether or not we should collect the data for closed issues.""" - includeClosedIssues: Boolean - - """ - Whether to instantly process the import with the default configuration mapping. - """ - instantProcess: Boolean - - """ID of the organization into which to import data.""" - organizationId: String @deprecated(reason: "Argument will be ignored. Use teamId to import in a specific team, or teamName to import into a new team.") - - """ID of the team into which to import data.""" - teamId: String - - """Name of new team. When teamId is not set.""" - teamName: String - ): IssueImportPayload! - - """Kicks off a Jira import job from a CSV.""" - issueImportCreateCSVJira( - """URL for the CSV.""" - csvUrl: String! - - """Jira user account email.""" - jiraEmail: String - - """Jira installation or cloud hostname.""" - jiraHostname: String - - """Jira personal access token to access Jira REST API.""" - jiraToken: String - - """ID of the organization into which to import data.""" - organizationId: String @deprecated(reason: "Argument will be ignored. Use teamId to import in a specific team, or teamName to import into a new team.") - - """ID of the team into which to import data. Empty to create new team.""" - teamId: String - - """Name of new team. When teamId is not set.""" - teamName: String - ): IssueImportPayload! - - """Kicks off a Shortcut (formerly Clubhouse) import job.""" - issueImportCreateClubhouse( - """ - Shortcut (formerly Clubhouse) group name to choose which issues we should import. - """ - clubhouseGroupName: String! - - """ - Shortcut (formerly Clubhouse) token to fetch information from the Clubhouse API. - """ - clubhouseToken: String! - - """ID of issue import. If not provided it will be generated.""" - id: String - - """Whether or not we should collect the data for closed issues.""" - includeClosedIssues: Boolean - - """ - Whether to instantly process the import with the default configuration mapping. - """ - instantProcess: Boolean - - """ID of the organization into which to import data.""" - organizationId: String @deprecated(reason: "Argument will be ignored. Use teamId to import in a specific team, or teamName to import into a new team.") - - """ID of the team into which to import data.""" - teamId: String - - """Name of new team. When teamId is not set.""" - teamName: String - ): IssueImportPayload! - - """Kicks off a GitHub import job.""" - issueImportCreateGithub( - """ - Labels to use to filter the import data. Only issues matching any of these filters will be imported. - """ - githubLabels: [String!] - - """IDs of the Github repositories from which we will import data.""" - githubRepoIds: [Int!] - - """Whether or not we should import GitHub organization level projects.""" - githubShouldImportOrgProjects: Boolean @deprecated(reason: "Argument will be ignored. The project information of an issue is always imported regardless of whether the project is linked to the repository of the issue or not") - - """Whether or not we should collect the data for closed issues.""" - includeClosedIssues: Boolean - - """ - Whether to instantly process the import with the default configuration mapping. - """ - instantProcess: Boolean - - """ - [DEPRECATED] ID of the Github import integration to use to access issues. - """ - integrationId: String @deprecated(reason: "An import can span multiple integrations. Value will be ignored.") - - """ID of the organization into which to import data.""" - organizationId: String @deprecated(reason: "Argument will be ignored. Use teamId to import in a specific team, or teamName to import into a new team.") - - """ID of the team into which to import data.""" - teamId: String - - """Name of new team. When teamId is not set.""" - teamName: String - ): IssueImportPayload! - - """Kicks off a Jira import job.""" - issueImportCreateJira( - """ID of issue import. If not provided it will be generated.""" - id: String - - """Whether or not we should collect the data for closed issues.""" - includeClosedIssues: Boolean - - """ - Whether to instantly process the import with the default configuration mapping. - """ - instantProcess: Boolean - - """Jira user account email.""" - jiraEmail: String! - - """Jira installation or cloud hostname.""" - jiraHostname: String! - - """Jira project key from which we will import data.""" - jiraProject: String! - - """Jira personal access token to access Jira REST API.""" - jiraToken: String! - - """A custom JQL query to filter issues being imported""" - jql: String - - """ID of the organization into which to import data.""" - organizationId: String @deprecated(reason: "Argument will be ignored. Use teamId to import in a specific team, or teamName to import into a new team.") - - """ID of the team into which to import data. Empty to create new team.""" - teamId: String - - """Name of new team. When teamId is not set.""" - teamName: String - ): IssueImportPayload! - - """[INTERNAL] Kicks off a Linear to Linear import job.""" - issueImportCreateLinearV2( - """ID of issue import. If not provided it will be generated.""" - id: String - - """The source organization to import from.""" - linearSourceOrganizationId: String! - ): IssueImportPayload! - - """Deletes an import job.""" - issueImportDelete( - """ID of the issue import to delete.""" - issueImportId: String! - ): IssueImportDeletePayload! - - """Kicks off import processing.""" - issueImportProcess( - """ID of the issue import which we're going to process.""" - issueImportId: String! - - """The mapping configuration to use for processing the import.""" - mapping: JSONObject! - ): IssueImportPayload! - - """Updates the mapping for the issue import.""" - issueImportUpdate( - """The identifier of the issue import.""" - id: String! - - """The properties of the issue import to update.""" - input: IssueImportUpdateInput! - ): IssueImportPayload! - - """Creates a new label.""" - issueLabelCreate( - """The issue label to create.""" - input: IssueLabelCreateInput! - - """ - Whether to replace all team-specific labels with the same name with this newly created workspace label (default: false). - """ - replaceTeamLabels: Boolean - ): IssueLabelPayload! - - """Deletes an issue label.""" - issueLabelDelete( - """The identifier of the label to delete.""" - id: String! - ): DeletePayload! - - """Updates an label.""" - issueLabelUpdate( - """The identifier of the label to update.""" - id: String! - - """A partial label object to update.""" - input: IssueLabelUpdateInput! - - """ - Whether to replace all team-specific labels with the same name with this updated workspace label (default: false). - """ - replaceTeamLabels: Boolean - ): IssueLabelPayload! - - """Creates a new issue relation.""" - issueRelationCreate( - """The issue relation to create.""" - input: IssueRelationCreateInput! - - """Used by client undo operations. Should not be set directly.""" - overrideCreatedAt: DateTime - ): IssueRelationPayload! - - """Deletes an issue relation.""" - issueRelationDelete( - """The identifier of the issue relation to delete.""" - id: String! - ): DeletePayload! - - """Updates an issue relation.""" - issueRelationUpdate( - """The identifier of the issue relation to update.""" - id: String! - - """The properties of the issue relation to update.""" - input: IssueRelationUpdateInput! - ): IssueRelationPayload! - - """ - Adds an issue reminder. Will cause a notification to be sent when the issue reminder time is reached. - """ - issueReminder( - """The identifier of the issue to add a reminder for.""" - id: String! - - """The time when a reminder notification will be sent.""" - reminderAt: DateTime! - ): IssuePayload! - - """Removes a label from an issue.""" - issueRemoveLabel( - """The identifier of the issue to remove the label from.""" - id: String! - - """The identifier of the label to remove.""" - labelId: String! - ): IssuePayload! - - """Subscribes a user to an issue.""" - issueSubscribe( - """The identifier of the issue to subscribe to.""" - id: String! - - """The identifier of the user to subscribe, default is the current user.""" - userId: String - ): IssuePayload! - - """Unarchives an issue.""" - issueUnarchive( - """The identifier of the issue to archive.""" - id: String! - ): IssueArchivePayload! - - """Unsubscribes a user from an issue.""" - issueUnsubscribe( - """The identifier of the issue to unsubscribe from.""" - id: String! - - """ - The identifier of the user to unsubscribe, default is the current user. - """ - userId: String - ): IssuePayload! - - """Updates an issue.""" - issueUpdate( - """The identifier of the issue to update.""" - id: String! - - """A partial issue object to update the issue with.""" - input: IssueUpdateInput! - ): IssuePayload! - - """ - [INTERNAL] Connects the organization with a Jira Personal Access Token. - """ - jiraIntegrationConnect( - """Jira integration settings.""" - input: JiraConfigurationInput! - ): IntegrationPayload! - - """Join an organization from onboarding.""" - joinOrganizationFromOnboarding( - """Organization details for the organization to join.""" - input: JoinOrganizationInput! - ): CreateOrJoinOrganizationResponse! - - """Leave an organization.""" - leaveOrganization( - """ID of the organization to leave.""" - organizationId: String! - ): CreateOrJoinOrganizationResponse! - - """Logout the client.""" - logout( - """The reason for logging out.""" - reason: String - ): LogoutResponse! - - """Logout all of user's sessions including the active one.""" - logoutAllSessions( - """The reason for logging out.""" - reason: String - ): LogoutResponse! - - """Logout all of user's sessions excluding the current one.""" - logoutOtherSessions( - """The reason for logging out.""" - reason: String - ): LogoutResponse! - - """Logout an individual session with its ID.""" - logoutSession( - """ID of the session to logout.""" - sessionId: String! - ): LogoutResponse! - - """Archives a notification.""" - notificationArchive( - """The id of the notification to archive.""" - id: String! - ): NotificationArchivePayload! - - """Archives a notification and all related notifications.""" - notificationArchiveAll( - """The type and id of the entity to archive notifications for.""" - input: NotificationEntityInput! - ): NotificationBatchActionPayload! - - """ - Subscribes to or unsubscribes from a notification category for a given notification channel for the user - """ - notificationCategoryChannelSubscriptionUpdate( - """The notification category to subscribe to or unsubscribe from""" - category: NotificationCategory! - - """ - The notification channel in which to subscribe to or unsubscribe from the category - """ - channel: NotificationChannel! - - """ - True if the user wants to subscribe, false if the user wants to unsubscribe - """ - subscribe: Boolean! - ): UserSettingsPayload! - - """Marks notification and all related notifications as read.""" - notificationMarkReadAll( - """The type and id of the entity to archive notifications for.""" - input: NotificationEntityInput! - - """The time when notification was marked as read.""" - readAt: DateTime! - ): NotificationBatchActionPayload! - - """Marks notification and all related notifications as unread.""" - notificationMarkUnreadAll( - """The type and id of the entity to archive notifications for.""" - input: NotificationEntityInput! - ): NotificationBatchActionPayload! - - """Snoozes a notification and all related notifications.""" - notificationSnoozeAll( - """The type and id of the entity to archive notifications for.""" - input: NotificationEntityInput! - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime! - ): NotificationBatchActionPayload! - - """ - Creates a new notification subscription for a cycle, custom view, label, project or team. - """ - notificationSubscriptionCreate( - """The subscription object to create.""" - input: NotificationSubscriptionCreateInput! - ): NotificationSubscriptionPayload! - - """Deletes a notification subscription reference.""" - notificationSubscriptionDelete( - """The identifier of the notification subscription reference to delete.""" - id: String! - ): DeletePayload! @deprecated(reason: "Update `notificationSubscription.active` to `false` instead.") - - """Updates a notification subscription.""" - notificationSubscriptionUpdate( - """The identifier of the notification subscription to update.""" - id: String! - - """ - A partial notification subscription object to update the notification subscription with. - """ - input: NotificationSubscriptionUpdateInput! - ): NotificationSubscriptionPayload! - - """Unarchives a notification.""" - notificationUnarchive( - """The id of the notification to archive.""" - id: String! - ): NotificationArchivePayload! - - """Unsnoozes a notification and all related notifications.""" - notificationUnsnoozeAll( - """The type and id of the entity to archive notifications for.""" - input: NotificationEntityInput! - - """The time when the notification was unsnoozed.""" - unsnoozedAt: DateTime! - ): NotificationBatchActionPayload! - - """Updates a notification.""" - notificationUpdate( - """The identifier of the notification to update.""" - id: String! - - """A partial notification object to update the notification with.""" - input: NotificationUpdateInput! - ): NotificationPayload! - - """ - Cancels the deletion of an organization. Administrator privileges required. - """ - organizationCancelDelete: OrganizationCancelDeletePayload! - - """Delete's an organization. Administrator privileges required.""" - organizationDelete( - """Information required to delete an organization.""" - input: DeleteOrganizationInput! - ): OrganizationDeletePayload! - - """ - Get an organization's delete confirmation token. Administrator privileges required. - """ - organizationDeleteChallenge: OrganizationDeletePayload! - - """[INTERNAL] Verifies a domain claim.""" - organizationDomainClaim( - """The ID of the organization domain to claim.""" - id: String! - ): OrganizationDomainSimplePayload! - - """[INTERNAL] Adds a domain to be allowed for an organization.""" - organizationDomainCreate( - """The organization domain entry to create.""" - input: OrganizationDomainCreateInput! - - """Whether to trigger an email verification flow during domain creation.""" - triggerEmailVerification: Boolean - ): OrganizationDomainPayload! - - """Deletes a domain.""" - organizationDomainDelete( - """The identifier of the domain to delete.""" - id: String! - ): DeletePayload! - - """[INTERNAL] Updates an organization domain settings.""" - organizationDomainUpdate( - """The identifier of the domain to update.""" - id: String! - - """The organization domain entry to update.""" - input: OrganizationDomainUpdateInput! - ): OrganizationDomainPayload! - - """[INTERNAL] Verifies a domain to be added to an organization.""" - organizationDomainVerify( - """The organization domain to verify.""" - input: OrganizationDomainVerificationInput! - ): OrganizationDomainPayload! - - """Creates a new organization invite.""" - organizationInviteCreate( - """The organization invite object to create.""" - input: OrganizationInviteCreateInput! - ): OrganizationInvitePayload! - - """Deletes an organization invite.""" - organizationInviteDelete( - """The identifier of the organization invite to delete.""" - id: String! - ): DeletePayload! - - """Updates an organization invite.""" - organizationInviteUpdate( - """The identifier of the organization invite to update.""" - id: String! - - """The updates to make to the organization invite object.""" - input: OrganizationInviteUpdateInput! - ): OrganizationInvitePayload! - - """ - [DEPRECATED] Starts a trial for the organization. Administrator privileges required. - """ - organizationStartTrial: OrganizationStartTrialPayload! @deprecated(reason: "Use organizationStartTrialForPlan") - - """ - Starts a trial for the organization on the specified plan type. Administrator privileges required. - """ - organizationStartTrialForPlan( - """Plan details for trial""" - input: OrganizationStartTrialInput! - ): OrganizationStartTrialPayload! - - """Updates the user's organization.""" - organizationUpdate( - """A partial organization object to update the organization with.""" - input: OrganizationUpdateInput! - ): OrganizationPayload! - - """[INTERNAL] Finish passkey login process.""" - passkeyLoginFinish( - """Random ID to start passkey login with.""" - authId: String! - response: JSONObject! - ): AuthResolverResponse! - - """[INTERNAL] Starts passkey login process.""" - passkeyLoginStart( - """Random ID to start passkey login with.""" - authId: String! - ): PasskeyLoginStartResponse! - - """Adds a label to a project.""" - projectAddLabel( - """The identifier of the project to add the label to.""" - id: String! - - """The identifier of the label to add.""" - labelId: String! - ): ProjectPayload! - - """Archives a project.""" - projectArchive( - """ - The identifier of the project to archive. Also the identifier from the URL is accepted. - """ - id: String! - - """Whether to trash the project.""" - trash: Boolean - ): ProjectArchivePayload! @deprecated(reason: "Deprecated in favor of projectDelete.") - - """Creates a new project.""" - projectCreate( - """Whether to connect a Slack channel to the project.""" - connectSlackChannel: Boolean - - """The issue object to create.""" - input: ProjectCreateInput! - ): ProjectPayload! - - """Deletes (trashes) a project.""" - projectDelete( - """The identifier of the project to delete.""" - id: String! - ): ProjectArchivePayload! - - """Creates a new project label.""" - projectLabelCreate( - """The project label to create.""" - input: ProjectLabelCreateInput! - ): ProjectLabelPayload! - - """Deletes a project label.""" - projectLabelDelete( - """The identifier of the label to delete.""" - id: String! - ): DeletePayload! - - """Updates a project label.""" - projectLabelUpdate( - """The identifier of the label to update.""" - id: String! - - """A partial label object to update.""" - input: ProjectLabelUpdateInput! - ): ProjectLabelPayload! - - """Creates a new project milestone.""" - projectMilestoneCreate( - """The project milestone to create.""" - input: ProjectMilestoneCreateInput! - ): ProjectMilestonePayload! - - """Deletes a project milestone.""" - projectMilestoneDelete( - """The identifier of the project milestone to delete.""" - id: String! - ): DeletePayload! - - """ - [Internal] Moves a project milestone to another project, can be called to undo a prior move. - """ - projectMilestoneMove( - """The identifier of the project milestone to move.""" - id: String! - - """ - The project to move the milestone to, as well as any additional options need to make a successful move, or undo a previous move. - """ - input: ProjectMilestoneMoveInput! - ): ProjectMilestoneMovePayload! - - """Updates a project milestone.""" - projectMilestoneUpdate( - """ - The identifier of the project milestone to update. Also the identifier from the URL is accepted. - """ - id: String! - - """A partial object to update the project milestone with.""" - input: ProjectMilestoneUpdateInput! - ): ProjectMilestonePayload! - - """ - [INTERNAL] Updates all projects currently assigned to to a project status to a new project status. - """ - projectReassignStatus( - """The identifier of the new project status to update the projects to.""" - newProjectStatusId: String! - - """ - The identifier of the project status with which projects will be updated. - """ - originalProjectStatusId: String! - ): SuccessPayload! - - """Creates a new project relation.""" - projectRelationCreate( - """The project relation to create.""" - input: ProjectRelationCreateInput! - ): ProjectRelationPayload! - - """Deletes a project relation.""" - projectRelationDelete( - """The identifier of the project relation to delete.""" - id: String! - ): DeletePayload! - - """Updates a project relation.""" - projectRelationUpdate( - """The identifier of the project relation to update.""" - id: String! - - """The properties of the project relation to update.""" - input: ProjectRelationUpdateInput! - ): ProjectRelationPayload! - - """Removes a label from a project.""" - projectRemoveLabel( - """The identifier of the project to remove the label from.""" - id: String! - - """The identifier of the label to remove.""" - labelId: String! - ): ProjectPayload! - - """Archives a project status.""" - projectStatusArchive( - """The identifier of the project status to archive.""" - id: String! - ): ProjectStatusArchivePayload! - - """Creates a new project status.""" - projectStatusCreate( - """The ProjectStatus object to create.""" - input: ProjectStatusCreateInput! - ): ProjectStatusPayload! - - """Unarchives a project status.""" - projectStatusUnarchive( - """The identifier of the project status to unarchive.""" - id: String! - ): ProjectStatusArchivePayload! - - """Updates a project status.""" - projectStatusUpdate( - """The identifier of the project status to update.""" - id: String! - - """A partial ProjectStatus object to update the ProjectStatus with.""" - input: ProjectStatusUpdateInput! - ): ProjectStatusPayload! - - """Unarchives a project.""" - projectUnarchive( - """ - The identifier of the project to restore. Also the identifier from the URL is accepted. - """ - id: String! - ): ProjectArchivePayload! - - """Updates a project.""" - projectUpdate( - """ - The identifier of the project to update. Also the identifier from the URL is accepted. - """ - id: String! - - """A partial project object to update the project with.""" - input: ProjectUpdateInput! - ): ProjectPayload! - - """Archives a project update.""" - projectUpdateArchive( - """The identifier of the project update to archive.""" - id: String! - ): ProjectUpdateArchivePayload! - - """Creates a new project update.""" - projectUpdateCreate( - """Data for the project update to create.""" - input: ProjectUpdateCreateInput! - ): ProjectUpdatePayload! - - """Deletes a project update.""" - projectUpdateDelete( - """The identifier of the project update to delete.""" - id: String! - ): DeletePayload! @deprecated(reason: "Use `projectUpdateArchive` instead.") - - """Unarchives a project update.""" - projectUpdateUnarchive( - """The identifier of the project update to unarchive.""" - id: String! - ): ProjectUpdateArchivePayload! - - """Updates a project update.""" - projectUpdateUpdate( - """The identifier of the project update to update.""" - id: String! - - """A data to update the project update with.""" - input: ProjectUpdateUpdateInput! - ): ProjectUpdatePayload! - - """Creates a push subscription.""" - pushSubscriptionCreate( - """The push subscription to create.""" - input: PushSubscriptionCreateInput! - ): PushSubscriptionPayload! - - """Deletes a push subscription.""" - pushSubscriptionDelete( - """The identifier of the push subscription to delete.""" - id: String! - ): PushSubscriptionPayload! - - """Creates a new reaction.""" - reactionCreate( - """The reaction object to create.""" - input: ReactionCreateInput! - ): ReactionPayload! - - """Deletes a reaction.""" - reactionDelete( - """The identifier of the reaction to delete.""" - id: String! - ): DeletePayload! - - """Manually update Google Sheets data.""" - refreshGoogleSheetsData( - """The identifier of the Google Sheets integration to update.""" - id: String! - - """The type of export.""" - type: String - ): IntegrationPayload! - - """Re-send an organization invite.""" - resendOrganizationInvite( - """The identifier of the organization invite to re-send.""" - id: String! - ): DeletePayload! - - """Re-send an organization invite tied to an email address.""" - resendOrganizationInviteByEmail( - """The email address tied to the organization invite to re-send.""" - email: String! - ): DeletePayload! - - """Archives a roadmap.""" - roadmapArchive( - """The identifier of the roadmap to archive.""" - id: String! - ): RoadmapArchivePayload! @deprecated(reason: "Roadmaps are deprecated, use initiatives instead.") - - """Creates a new roadmap.""" - roadmapCreate( - """The properties of the roadmap to create.""" - input: RoadmapCreateInput! - ): RoadmapPayload! @deprecated(reason: "Roadmaps are deprecated, use initiatives instead.") - - """Deletes a roadmap.""" - roadmapDelete( - """The identifier of the roadmap to delete.""" - id: String! - ): DeletePayload! @deprecated(reason: "Roadmaps are deprecated, use initiatives instead.") - - """Creates a new roadmapToProject join.""" - roadmapToProjectCreate( - """The properties of the roadmapToProject to create.""" - input: RoadmapToProjectCreateInput! - ): RoadmapToProjectPayload! - - """Deletes a roadmapToProject.""" - roadmapToProjectDelete( - """The identifier of the roadmapToProject to delete.""" - id: String! - ): DeletePayload! - - """Updates a roadmapToProject.""" - roadmapToProjectUpdate( - """The identifier of the roadmapToProject to update.""" - id: String! - - """The properties of the roadmapToProject to update.""" - input: RoadmapToProjectUpdateInput! - ): RoadmapToProjectPayload! - - """Unarchives a roadmap.""" - roadmapUnarchive( - """The identifier of the roadmap to unarchive.""" - id: String! - ): RoadmapArchivePayload! @deprecated(reason: "Roadmaps are deprecated, use initiatives instead.") - - """Updates a roadmap.""" - roadmapUpdate( - """The identifier of the roadmap to update.""" - id: String! - - """The properties of the roadmap to update.""" - input: RoadmapUpdateInput! - ): RoadmapPayload! @deprecated(reason: "Roadmaps are deprecated, use initiatives instead.") - - """ - Authenticates a user account via email and authentication token for SAML. - """ - samlTokenUserAccountAuth( - """The data used for token authentication.""" - input: TokenUserAccountAuthInput! - ): AuthResolverResponse! - - """ - Creates a new team. The user who creates the team will automatically be added as a member to the newly created team. - """ - teamCreate( - """The team id to copy settings from, if any.""" - copySettingsFromTeamId: String - - """The team object to create.""" - input: TeamCreateInput! - ): TeamPayload! - - """Deletes team's cycles data""" - teamCyclesDelete( - """The identifier of the team, which cycles will be deleted.""" - id: String! - ): TeamPayload! - - """Deletes a team.""" - teamDelete( - """The identifier of the team to delete.""" - id: String! - ): DeletePayload! - - """Deletes a previously used team key.""" - teamKeyDelete( - """The identifier of the team key to delete.""" - id: String! - ): DeletePayload! - - """Creates a new team membership.""" - teamMembershipCreate( - """The team membership object to create.""" - input: TeamMembershipCreateInput! - ): TeamMembershipPayload! - - """Deletes a team membership.""" - teamMembershipDelete( - """Whether to leave the parent teams.""" - alsoLeaveParentTeams: Boolean - - """The identifier of the team membership to delete.""" - id: String! - ): DeletePayload! - - """Updates a team membership.""" - teamMembershipUpdate( - """The identifier of the team membership to update.""" - id: String! - - """A partial team membership object to update the team membership with.""" - input: TeamMembershipUpdateInput! - ): TeamMembershipPayload! - - """Unarchives a team and cancels deletion.""" - teamUnarchive( - """The identifier of the team to delete.""" - id: String! - ): TeamArchivePayload! - - """Updates a team.""" - teamUpdate( - """The identifier of the team to update.""" - id: String! - - """A partial team object to update the team with.""" - input: TeamUpdateInput! - - """ - [INTERNAL] Mapping of existing team entities to those inherited from the parent team - """ - mapping: InheritanceEntityMapping - ): TeamPayload! - - """Creates a new template.""" - templateCreate( - """The template object to create.""" - input: TemplateCreateInput! - ): TemplatePayload! - - """Deletes a template.""" - templateDelete( - """The identifier of the template to delete.""" - id: String! - ): DeletePayload! - - """Updates an existing template.""" - templateUpdate( - """The identifier of the template.""" - id: String! - - """The properties of the template to update.""" - input: TemplateUpdateInput! - ): TemplatePayload! - - """Creates a new time schedule.""" - timeScheduleCreate( - """The properties of the time schedule to create.""" - input: TimeScheduleCreateInput! - ): TimeSchedulePayload! - - """Deletes a time schedule.""" - timeScheduleDelete( - """The identifier of the time schedule to delete.""" - id: String! - ): DeletePayload! - - """Refresh the integration schedule information.""" - timeScheduleRefreshIntegrationSchedule( - """The identifier of the time schedule to refresh.""" - id: String! - ): TimeSchedulePayload! - - """Updates a time schedule.""" - timeScheduleUpdate( - """The identifier of the time schedule to update.""" - id: String! - - """The properties of the time schedule to update.""" - input: TimeScheduleUpdateInput! - ): TimeSchedulePayload! - - """Upsert an external time schedule.""" - timeScheduleUpsertExternal( - """The unique identifier of the external schedule.""" - externalId: String! - - """The properties of the time schedule to insert or update.""" - input: TimeScheduleUpdateInput! - ): TimeSchedulePayload! - - """Creates a new triage responsibility.""" - triageResponsibilityCreate( - """The properties of the triage responsibility to create.""" - input: TriageResponsibilityCreateInput! - ): TriageResponsibilityPayload! - - """Deletes a triage responsibility.""" - triageResponsibilityDelete( - """The identifier of the triage responsibility to delete.""" - id: String! - ): DeletePayload! - - """Updates an existing triage responsibility.""" - triageResponsibilityUpdate( - """The identifier of the triage responsibility to update.""" - id: String! - - """The properties of the triage responsibility to update.""" - input: TriageResponsibilityUpdateInput! - ): TriageResponsibilityPayload! - - """[Internal] Updates existing Slack integration scopes.""" - updateIntegrationSlackScopes( - """The Slack OAuth code.""" - code: String! - - """The ID of the existing Slack integration""" - integrationId: String! - - """The Slack OAuth redirect URI.""" - redirectUri: String! - ): IntegrationPayload! - - """[INTERNAL] Updates the summary of an issue.""" - updateIssueSummary( - """The identifier of the issue to update.""" - id: String! - ): IssuePayload! - - """Makes user a regular user. Can only be called by an admin.""" - userDemoteAdmin( - """The identifier of the user to make a regular user.""" - id: String! - ): UserAdminPayload! - - """Makes user a guest. Can only be called by an admin.""" - userDemoteMember( - """The identifier of the user to make a guest.""" - id: String! - ): UserAdminPayload! - - """Connects the Discord user to this Linear account via OAuth2.""" - userDiscordConnect( - """The Discord OAuth code.""" - code: String! - - """The Discord OAuth redirect URI.""" - redirectUri: String! - ): UserPayload! - - """Disconnects the external user from this Linear account.""" - userExternalUserDisconnect( - """The external service to disconnect.""" - service: String! - ): UserPayload! - - """Updates a user's settings flag.""" - userFlagUpdate( - """Settings flag to increment.""" - flag: UserFlagType! - - """Flag operation to perform.""" - operation: UserFlagUpdateOperation! - ): UserSettingsFlagPayload! - - """Makes user an admin. Can only be called by an admin.""" - userPromoteAdmin( - """The identifier of the user to make an admin.""" - id: String! - ): UserAdminPayload! - - """Makes user a regular user. Can only be called by an admin.""" - userPromoteMember( - """The identifier of the user to make a regular user.""" - id: String! - ): UserAdminPayload! - - """Resets user's setting flags.""" - userSettingsFlagsReset( - """The flags to reset. If not provided all flags will be reset.""" - flags: [UserFlagType!] - ): UserSettingsFlagsResetPayload! - - """Updates the user's settings.""" - userSettingsUpdate( - """The identifier of the userSettings to update.""" - id: String! - - """A partial notification object to update the settings with.""" - input: UserSettingsUpdateInput! - ): UserSettingsPayload! - - """Suspends a user. Can only be called by an admin.""" - userSuspend( - """The identifier of the user to suspend.""" - id: String! - ): UserAdminPayload! - - """ - Unlinks a guest user from their identity provider. Can only be called by an admin when SCIM is enabled. - """ - userUnlinkFromIdentityProvider( - """ - The identifier of the guest user to unlink from their identity provider. - """ - id: String! - ): UserAdminPayload! - - """Un-suspends a user. Can only be called by an admin.""" - userUnsuspend( - """The identifier of the user to unsuspend.""" - id: String! - ): UserAdminPayload! - - """ - Updates a user. Only available to organization admins and the user themselves. - """ - userUpdate( - """ - The identifier of the user to update. Use `me` to reference currently authenticated user. - """ - id: String! - - """A partial user object to update the user with.""" - input: UserUpdateInput! - ): UserPayload! - - """Creates a new ViewPreferences object.""" - viewPreferencesCreate( - """The ViewPreferences object to create.""" - input: ViewPreferencesCreateInput! - ): ViewPreferencesPayload! - - """Deletes a ViewPreferences.""" - viewPreferencesDelete( - """The identifier of the ViewPreferences to delete.""" - id: String! - ): DeletePayload! - - """Updates an existing ViewPreferences object.""" - viewPreferencesUpdate( - """The identifier of the ViewPreferences object.""" - id: String! - - """The properties of the view preferences.""" - input: ViewPreferencesUpdateInput! - ): ViewPreferencesPayload! - - """Creates a new webhook.""" - webhookCreate( - """The webhook object to create.""" - input: WebhookCreateInput! - ): WebhookPayload! - - """Deletes a Webhook.""" - webhookDelete( - """The identifier of the Webhook to delete.""" - id: String! - ): DeletePayload! - - """Updates an existing Webhook.""" - webhookUpdate( - """The identifier of the Webhook.""" - id: String! - - """The properties of the Webhook.""" - input: WebhookUpdateInput! - ): WebhookPayload! - - """ - Archives a state. Only states with issues that have all been archived can be archived. - """ - workflowStateArchive( - """The identifier of the state to archive.""" - id: String! - ): WorkflowStateArchivePayload! - - """Creates a new state, adding it to the workflow of a team.""" - workflowStateCreate( - """The state to create.""" - input: WorkflowStateCreateInput! - ): WorkflowStatePayload! - - """Updates a state.""" - workflowStateUpdate( - """The identifier of the state to update.""" - id: String! - - """A partial state object to update.""" - input: WorkflowStateUpdateInput! - ): WorkflowStatePayload! -} - -"""Customer name sorting options.""" -input NameSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -interface Node { - """The unique identifier of the entity.""" - id: ID! -} - -"""A notification sent to a user.""" -interface Notification implements Entity & Node { - """The user that caused the notification.""" - actor: User - - """[Internal] Notification actor initials if avatar is not available.""" - actorAvatarColor: String! - - """[Internal] Notification avatar URL.""" - actorAvatarUrl: String - - """[Internal] Notification actor initials if avatar is not available.""" - actorInitials: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The bot that caused the notification.""" - botActor: ActorBot - - """The category of the notification.""" - category: NotificationCategory! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The time at when an email reminder for this notification was sent to the user. Null, if no email - reminder has been sent. - """ - emailedAt: DateTime - - """The external user that caused the notification.""" - externalUserActor: ExternalUser - - """ - [Internal] Notifications with the same grouping key will be grouped together in the UI. - """ - groupingKey: String! - - """ - [Internal] Priority of the notification with the same grouping key. Higher number means higher priority. If priority is the same, notifications should be sorted by `createdAt`. - """ - groupingPriority: Float! - - """The unique identifier of the entity.""" - id: ID! - - """[Internal] Inbox URL for the notification.""" - inboxUrl: String! - - """[Internal] If notification actor was Linear.""" - isLinearActor: Boolean! - - """[Internal] Issue's status type for issue notifications.""" - issueStatusType: String - - """[Internal] Project update health for new updates.""" - projectUpdateHealth: String - - """ - The time at when the user marked the notification as read. Null, if the the user hasn't read the notification - """ - readAt: DateTime - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime - - """[Internal] Notification subtitle.""" - subtitle: String! - - """[Internal] Notification title.""" - title: String! - - """Notification type.""" - type: String! - - """The time at which a notification was unsnoozed..""" - unsnoozedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """[Internal] URL to the target of the notification.""" - url: String! - - """The user that received the notification.""" - user: User! -} - -"""A generic payload return from entity archive mutations.""" -type NotificationArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: Notification - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type NotificationBatchActionPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The notifications that were updated.""" - notifications: [Notification!]! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""The categories of notifications a user can subscribe to.""" -enum NotificationCategory { - appsAndIntegrations - assignments - commentsAndReplies - customers - documentChanges - feed - mentions - postsAndUpdates - reactions - reminders - reviews - statusChanges - subscriptions - system - triage -} - -"""A user's notification category preferences.""" -type NotificationCategoryPreferences { - """The preferences for notifications about apps and integrations.""" - appsAndIntegrations: NotificationChannelPreferences! - - """The preferences for notifications about assignments.""" - assignments: NotificationChannelPreferences! - - """The preferences for notifications about comments and replies.""" - commentsAndReplies: NotificationChannelPreferences! - - """The preferences for customer notifications.""" - customers: NotificationChannelPreferences! - - """The preferences for notifications about document changes.""" - documentChanges: NotificationChannelPreferences! - - """The preferences for feed summary notifications.""" - feed: NotificationChannelPreferences! - - """The preferences for notifications about mentions.""" - mentions: NotificationChannelPreferences! - - """The preferences for notifications about posts and updates.""" - postsAndUpdates: NotificationChannelPreferences! - - """The preferences for notifications about reactions.""" - reactions: NotificationChannelPreferences! - - """The preferences for notifications about reminders.""" - reminders: NotificationChannelPreferences! - - """The preferences for notifications about reviews.""" - reviews: NotificationChannelPreferences! - - """The preferences for notifications about status changes.""" - statusChanges: NotificationChannelPreferences! - - """The preferences for notifications about subscriptions.""" - subscriptions: NotificationChannelPreferences! - - """The preferences for system notifications.""" - system: NotificationChannelPreferences! - - """The preferences for triage notifications.""" - triage: NotificationChannelPreferences! -} - -input NotificationCategoryPreferencesInput { - """The preferences for notifications about apps and integrations.""" - appsAndIntegrations: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about assignments.""" - assignments: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about comments and replies.""" - commentsAndReplies: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about customers.""" - customers: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about document changes.""" - documentChanges: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about feed summaries.""" - feed: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about mentions.""" - mentions: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about posts and updates.""" - postsAndUpdates: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about reactions.""" - reactions: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about reminders.""" - reminders: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about reviews.""" - reviews: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about status changes.""" - statusChanges: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about subscriptions.""" - subscriptions: PartialNotificationChannelPreferencesInput - - """The preferences for notifications about triage.""" - triage: PartialNotificationChannelPreferencesInput -} - -"""The delivery channels a user can receive notifications in.""" -enum NotificationChannel { - desktop - email - mobile - slack -} - -""" -A user's notification channel preferences, indicating if a channel is enabled or not -""" -type NotificationChannelPreferences { - """Whether notifications are currently enabled for desktop.""" - desktop: Boolean! - - """Whether notifications are currently enabled for email.""" - email: Boolean! - - """Whether notifications are currently enabled for mobile.""" - mobile: Boolean! - - """Whether notifications are currently enabled for Slack.""" - slack: Boolean! -} - -type NotificationConnection { - edges: [NotificationEdge!]! - nodes: [Notification!]! - pageInfo: PageInfo! -} - -"""A user's notification delivery preferences.""" -type NotificationDeliveryPreferences { - """The delivery preferences for the mobile channel.""" - mobile: NotificationDeliveryPreferencesChannel -} - -"""A user's notification delivery preferences.""" -type NotificationDeliveryPreferencesChannel { - """ - [DEPRECATED] Whether notifications are enabled for this channel. Use notificationChannelPreferences instead. - """ - notificationsDisabled: Boolean @deprecated(reason: "This field has been replaced by notificationChannelPreferences") - - """The schedule for notifications on this channel.""" - schedule: NotificationDeliveryPreferencesSchedule -} - -input NotificationDeliveryPreferencesChannelInput { - """ - [DEPRECATED] Whether notifications are enabled for this channel. Use notificationChannelPreferences instead. - """ - notificationsDisabled: Boolean @deprecated(reason: "This field has been replaced by notificationChannelPreferences") - - """The schedule for notifications on this channel.""" - schedule: NotificationDeliveryPreferencesScheduleInput -} - -"""A user's notification delivery schedule for a particular day.""" -type NotificationDeliveryPreferencesDay { - """The time notifications end.""" - end: String - - """The time notifications start.""" - start: String -} - -input NotificationDeliveryPreferencesDayInput { - """The time notifications end.""" - end: String - - """The time notifications start.""" - start: String -} - -input NotificationDeliveryPreferencesInput { - """The delivery preferences for the mobile channel.""" - mobile: NotificationDeliveryPreferencesChannelInput -} - -"""A user's notification delivery schedule for a particular day.""" -type NotificationDeliveryPreferencesSchedule { - """Whether the schedule is disabled.""" - disabled: Boolean - - """Delivery preferences for Friday.""" - friday: NotificationDeliveryPreferencesDay! - - """Delivery preferences for Monday.""" - monday: NotificationDeliveryPreferencesDay! - - """Delivery preferences for Saturday.""" - saturday: NotificationDeliveryPreferencesDay! - - """Delivery preferences for Sunday.""" - sunday: NotificationDeliveryPreferencesDay! - - """Delivery preferences for Thursday.""" - thursday: NotificationDeliveryPreferencesDay! - - """Delivery preferences for Tuesday.""" - tuesday: NotificationDeliveryPreferencesDay! - - """Delivery preferences for Wednesday.""" - wednesday: NotificationDeliveryPreferencesDay! -} - -input NotificationDeliveryPreferencesScheduleInput { - """Whether the schedule is disabled.""" - disabled: Boolean - - """Delivery preferences for Friday.""" - friday: NotificationDeliveryPreferencesDayInput! - - """Delivery preferences for Monday.""" - monday: NotificationDeliveryPreferencesDayInput! - - """Delivery preferences for Saturday.""" - saturday: NotificationDeliveryPreferencesDayInput! - - """Delivery preferences for Sunday.""" - sunday: NotificationDeliveryPreferencesDayInput! - - """Delivery preferences for Thursday.""" - thursday: NotificationDeliveryPreferencesDayInput! - - """Delivery preferences for Tuesday.""" - tuesday: NotificationDeliveryPreferencesDayInput! - - """Delivery preferences for Wednesday.""" - wednesday: NotificationDeliveryPreferencesDayInput! -} - -type NotificationEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Notification! -} - -"""Describes the type and id of the entity to target for notifications.""" -input NotificationEntityInput { - """The id of the notification.""" - id: String - - """The id of the initiative related to the notification.""" - initiativeId: String - - """The id of the initiative update related to the notification.""" - initiativeUpdateId: String - - """The id of the issue related to the notification.""" - issueId: String - - """The id of the OAuth client approval related to the notification.""" - oauthClientApprovalId: String - - """[DEPRECATED] The id of the project related to the notification.""" - projectId: String - - """The id of the project update related to the notification.""" - projectUpdateId: String -} - -"""Notification filtering options.""" -input NotificationFilter { - """Compound filters, all of which need to be matched by the notification.""" - and: [NotificationFilter!] - - """Comparator for the archived at date.""" - archivedAt: DateComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Compound filters, one of which need to be matched by the notification.""" - or: [NotificationFilter!] - - """Comparator for the notification type.""" - type: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type NotificationPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The notification that was created or updated.""" - notification: Notification! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Notification subscriptions for models.""" -interface NotificationSubscription implements Entity & Node { - """Whether the subscription is active or not.""" - active: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The type of view to which the notification subscription context is associated with. - """ - contextViewType: ContextViewType - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The contextual custom view associated with the notification subscription. - """ - customView: CustomView - - """The customer associated with the notification subscription.""" - customer: Customer - - """ - The contextual cycle view associated with the notification subscription. - """ - cycle: Cycle - - """The unique identifier of the entity.""" - id: ID! - - """ - The contextual initiative view associated with the notification subscription. - """ - initiative: Initiative - - """ - The contextual label view associated with the notification subscription. - """ - label: IssueLabel - - """ - The contextual project view associated with the notification subscription. - """ - project: Project - - """The user that subscribed to receive notifications.""" - subscriber: User! - - """The team associated with the notification subscription.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user view associated with the notification subscription.""" - user: User - - """ - The type of user view to which the notification subscription context is associated with. - """ - userContextViewType: UserContextViewType -} - -type NotificationSubscriptionConnection { - edges: [NotificationSubscriptionEdge!]! - nodes: [NotificationSubscription!]! - pageInfo: PageInfo! -} - -input NotificationSubscriptionCreateInput { - """Whether the subscription is active.""" - active: Boolean - - """ - The type of view to which the notification subscription context is associated with. - """ - contextViewType: ContextViewType - - """The identifier of the custom view to subscribe to.""" - customViewId: String - - """The identifier of the customer to subscribe to.""" - customerId: String - - """The identifier of the cycle to subscribe to.""" - cycleId: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The identifier of the initiative to subscribe to.""" - initiativeId: String - - """The identifier of the label to subscribe to.""" - labelId: String - - """The types of notifications of the subscription.""" - notificationSubscriptionTypes: [String!] - - """The identifier of the project to subscribe to.""" - projectId: String - - """The identifier of the team to subscribe to.""" - teamId: String - - """ - The type of user view to which the notification subscription context is associated with. - """ - userContextViewType: UserContextViewType - - """The identifier of the user to subscribe to.""" - userId: String -} - -type NotificationSubscriptionEdge { - """Used in `before` and `after` args""" - cursor: String! - node: NotificationSubscription! -} - -type NotificationSubscriptionPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The notification subscription that was created or updated.""" - notificationSubscription: NotificationSubscription! - - """Whether the operation was successful.""" - success: Boolean! -} - -input NotificationSubscriptionUpdateInput { - """Whether the subscription is active.""" - active: Boolean - - """The types of notifications of the subscription.""" - notificationSubscriptionTypes: [String!] -} - -input NotificationUpdateInput { - """The id of the project update related to the notification.""" - initiativeUpdateId: String - - """The id of the project update related to the notification.""" - projectUpdateId: String - - """The time when notification was marked as read.""" - readAt: DateTime - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime -} - -input NotionSettingsInput { - """The ID of the Notion workspace being connected.""" - workspaceId: String! - - """The name of the Notion workspace being connected.""" - workspaceName: String! -} - -"""Comment filtering options.""" -input NullableCommentFilter { - """Compound filters, all of which need to be matched by the comment.""" - and: [NullableCommentFilter!] - - """Comparator for the comment's body.""" - body: StringComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the comment's document content must satisfy.""" - documentContent: NullableDocumentContentFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the comment's issue must satisfy.""" - issue: NullableIssueFilter - - """Filters that the comment's customer needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Filter based on the existence of the relation.""" - null: Boolean - - """Compound filters, one of which need to be matched by the comment.""" - or: [NullableCommentFilter!] - - """Filters that the comment parent must satisfy.""" - parent: NullableCommentFilter - - """Filters that the comment's project update must satisfy.""" - projectUpdate: NullableProjectUpdateFilter - - """Filters that the comment's reactions must satisfy.""" - reactions: ReactionCollectionFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator - - """Filters that the comment's creator must satisfy.""" - user: UserFilter -} - -"""Customer filtering options.""" -input NullableCustomerFilter { - """Compound filters, all of which need to be matched by the customer.""" - and: [NullableCustomerFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the customer's domains.""" - domains: StringArrayComparator - - """Comparator for the customer's external IDs.""" - externalIds: StringArrayComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the customer name.""" - name: StringComparator - - """Filters that the customer's needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Filter based on the existence of the relation.""" - null: Boolean - - """Compound filters, one of which need to be matched by the customer.""" - or: [NullableCustomerFilter!] - - """Filters that the customer owner must satisfy.""" - owner: NullableUserFilter - - """Comparator for the customer generated revenue.""" - revenue: NumberComparator - - """Comparator for the customer size.""" - size: NumberComparator - - """Comparator for the customer slack channel ID.""" - slackChannelId: StringComparator - - """Filters that the customer's status must satisfy.""" - status: CustomerStatusFilter - - """Filters that the customer's tier must satisfy.""" - tier: CustomerTierFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Cycle filtering options.""" -input NullableCycleFilter { - """Compound filters, all of which need to be matched by the cycle.""" - and: [NullableCycleFilter!] - - """Comparator for the cycle completed at date.""" - completedAt: DateComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the cycle ends at date.""" - endsAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the filtering active cycle.""" - isActive: BooleanComparator - - """Comparator for the filtering future cycles.""" - isFuture: BooleanComparator - - """ - Comparator for filtering for whether the cycle is currently in cooldown. - """ - isInCooldown: BooleanComparator - - """Comparator for the filtering next cycle.""" - isNext: BooleanComparator - - """Comparator for the filtering past cycles.""" - isPast: BooleanComparator - - """Comparator for the filtering previous cycle.""" - isPrevious: BooleanComparator - - """Filters that the cycles issues must satisfy.""" - issues: IssueCollectionFilter - - """Comparator for the cycle name.""" - name: StringComparator - - """Filter based on the existence of the relation.""" - null: Boolean - - """Comparator for the cycle number.""" - number: NumberComparator - - """Compound filters, one of which need to be matched by the cycle.""" - or: [NullableCycleFilter!] - - """Comparator for the cycle start date.""" - startsAt: DateComparator - - """Filters that the cycles team must satisfy.""" - team: TeamFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Comparator for optional dates.""" -input NullableDateComparator { - """Equals constraint.""" - eq: DateTimeOrDuration - - """ - Greater-than constraint. Matches any values that are greater than the given value. - """ - gt: DateTimeOrDuration - - """ - Greater-than-or-equal constraint. Matches any values that are greater than or equal to the given value. - """ - gte: DateTimeOrDuration - - """In-array constraint.""" - in: [DateTimeOrDuration!] - - """ - Less-than constraint. Matches any values that are less than the given value. - """ - lt: DateTimeOrDuration - - """ - Less-than-or-equal constraint. Matches any values that are less than or equal to the given value. - """ - lte: DateTimeOrDuration - - """Not-equals constraint.""" - neq: DateTimeOrDuration - - """Not-in-array constraint.""" - nin: [DateTimeOrDuration!] - - """ - Null constraint. Matches any non-null values if the given value is false, otherwise it matches null values. - """ - null: Boolean -} - -"""Document content filtering options.""" -input NullableDocumentContentFilter { - """Compound filters, all of which need to be matched by the user.""" - and: [NullableDocumentContentFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the document content document must satisfy.""" - document: DocumentFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Filter based on the existence of the relation.""" - null: Boolean - - """Compound filters, one of which need to be matched by the user.""" - or: [NullableDocumentContentFilter!] - - """Filters that the document content project must satisfy.""" - project: ProjectFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Nullable comparator for optional durations.""" -input NullableDurationComparator { - """Equals constraint.""" - eq: Duration - - """ - Greater-than constraint. Matches any values that are greater than the given value. - """ - gt: Duration - - """ - Greater-than-or-equal constraint. Matches any values that are greater than or equal to the given value. - """ - gte: Duration - - """In-array constraint.""" - in: [Duration!] - - """ - Less-than constraint. Matches any values that are less than the given value. - """ - lt: Duration - - """ - Less-than-or-equal constraint. Matches any values that are less than or equal to the given value. - """ - lte: Duration - - """Not-equals constraint.""" - neq: Duration - - """Not-in-array constraint.""" - nin: [Duration!] - - """ - Null constraint. Matches any non-null values if the given value is false, otherwise it matches null values. - """ - null: Boolean -} - -"""Issue filtering options.""" -input NullableIssueFilter { - """Comparator for the issues added to cycle at date.""" - addedToCycleAt: NullableDateComparator - - """Comparator for the period when issue was added to a cycle.""" - addedToCyclePeriod: CyclePeriodComparator - - """ - [Internal] Age (created -> now) comparator, defined if the issue is still open. - """ - ageTime: NullableDurationComparator - - """Compound filters, all of which need to be matched by the issue.""" - and: [NullableIssueFilter!] - - """Comparator for the issues archived at date.""" - archivedAt: NullableDateComparator - - """Filters that the issues assignee must satisfy.""" - assignee: NullableUserFilter - - """Filters that the issues attachments must satisfy.""" - attachments: AttachmentCollectionFilter - - """Comparator for the issues auto archived at date.""" - autoArchivedAt: NullableDateComparator - - """Comparator for the issues auto closed at date.""" - autoClosedAt: NullableDateComparator - - """Comparator for the issues canceled at date.""" - canceledAt: NullableDateComparator - - """Filters that the child issues must satisfy.""" - children: IssueCollectionFilter - - """Filters that the issues comments must satisfy.""" - comments: CommentCollectionFilter - - """Comparator for the issues completed at date.""" - completedAt: NullableDateComparator - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the issues creator must satisfy.""" - creator: NullableUserFilter - - """Count of customers""" - customerCount: NumberComparator - - """Count of important customers""" - customerImportantCount: NumberComparator - - """Filters that the issues cycle must satisfy.""" - cycle: NullableCycleFilter - - """[Internal] Cycle time (started -> completed) comparator.""" - cycleTime: NullableDurationComparator - - """Filters that the issue's delegated agent must satisfy.""" - delegate: NullableUserFilter - - """Comparator for the issues description.""" - description: NullableStringComparator - - """Comparator for the issues due date.""" - dueDate: NullableTimelessDateComparator - - """Comparator for the issues estimate.""" - estimate: EstimateComparator - - """Comparator for filtering issues which are blocked.""" - hasBlockedByRelations: RelationExistsComparator - - """Comparator for filtering issues which are blocking.""" - hasBlockingRelations: RelationExistsComparator - - """Comparator for filtering issues which are duplicates.""" - hasDuplicateRelations: RelationExistsComparator - - """Comparator for filtering issues with relations.""" - hasRelatedRelations: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested assignees. - """ - hasSuggestedAssignees: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested labels. - """ - hasSuggestedLabels: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested projects. - """ - hasSuggestedProjects: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested related issues. - """ - hasSuggestedRelatedIssues: RelationExistsComparator - - """ - [Internal] Comparator for filtering issues which have suggested similar issues. - """ - hasSuggestedSimilarIssues: RelationExistsComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that issue labels must satisfy.""" - labels: IssueLabelCollectionFilter - - """Filters that the last applied template must satisfy.""" - lastAppliedTemplate: NullableTemplateFilter - - """[Internal] Lead time (created -> completed) comparator.""" - leadTime: NullableDurationComparator - - """Filters that the issue's customer needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Filter based on the existence of the relation.""" - null: Boolean - - """Comparator for the issues number.""" - number: NumberComparator - - """Compound filters, one of which need to be matched by the issue.""" - or: [NullableIssueFilter!] - - """Filters that the issue parent must satisfy.""" - parent: NullableIssueFilter - - """ - Comparator for the issues priority. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: NullableNumberComparator - - """Filters that the issues project must satisfy.""" - project: NullableProjectFilter - - """Filters that the issues project milestone must satisfy.""" - projectMilestone: NullableProjectMilestoneFilter - - """Filters that the issues reactions must satisfy.""" - reactions: ReactionCollectionFilter - - """[ALPHA] Filters that the recurring issue template must satisfy.""" - recurringIssueTemplate: NullableTemplateFilter - - """[Internal] Comparator for the issues content.""" - searchableContent: ContentComparator - - """Comparator for the issues sla status.""" - slaStatus: SlaStatusComparator - - """Filters that the issues snoozer must satisfy.""" - snoozedBy: NullableUserFilter - - """Comparator for the issues snoozed until date.""" - snoozedUntilAt: NullableDateComparator - - """Filters that the source must satisfy.""" - sourceMetadata: SourceMetadataComparator - - """Comparator for the issues started at date.""" - startedAt: NullableDateComparator - - """Filters that the issues state must satisfy.""" - state: WorkflowStateFilter - - """Filters that issue subscribers must satisfy.""" - subscribers: UserCollectionFilter - - """[Internal] Filters that the issue's suggestions must satisfy.""" - suggestions: IssueSuggestionCollectionFilter - - """Filters that the issues team must satisfy.""" - team: TeamFilter - - """Comparator for the issues title.""" - title: StringComparator - - """[Internal] Triage time (entered triaged -> triaged) comparator.""" - triageTime: NullableDurationComparator - - """Comparator for the issues triaged at date.""" - triagedAt: NullableDateComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Comparator for optional numbers.""" -input NullableNumberComparator { - """Equals constraint.""" - eq: Float - - """ - Greater-than constraint. Matches any values that are greater than the given value. - """ - gt: Float - - """ - Greater-than-or-equal constraint. Matches any values that are greater than or equal to the given value. - """ - gte: Float - - """In-array constraint.""" - in: [Float!] - - """ - Less-than constraint. Matches any values that are less than the given value. - """ - lt: Float - - """ - Less-than-or-equal constraint. Matches any values that are less than or equal to the given value. - """ - lte: Float - - """Not-equals constraint.""" - neq: Float - - """Not-in-array constraint.""" - nin: [Float!] - - """ - Null constraint. Matches any non-null values if the given value is false, otherwise it matches null values. - """ - null: Boolean -} - -"""Project filtering options.""" -input NullableProjectFilter { - """Filters that the project's team must satisfy.""" - accessibleTeams: TeamCollectionFilter - - """ - [ALPHA] Comparator for the project activity type: buzzin, active, some, none - """ - activityType: StringComparator - - """Compound filters, all of which need to be matched by the project.""" - and: [NullableProjectFilter!] - - """Comparator for the project cancelation date.""" - canceledAt: NullableDateComparator - - """Comparator for the project completion date.""" - completedAt: NullableDateComparator - - """Filters that the project's completed milestones must satisfy.""" - completedProjectMilestones: ProjectMilestoneCollectionFilter - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the projects creator must satisfy.""" - creator: UserFilter - - """Count of customers""" - customerCount: NumberComparator - - """Count of important customers""" - customerImportantCount: NumberComparator - - """Comparator for filtering projects which are blocked.""" - hasBlockedByRelations: RelationExistsComparator - - """Comparator for filtering projects which are blocking.""" - hasBlockingRelations: RelationExistsComparator - - """ - [Deprecated] Comparator for filtering projects which this is depended on by. - """ - hasDependedOnByRelations: RelationExistsComparator - - """[Deprecated]Comparator for filtering projects which this depends on.""" - hasDependsOnRelations: RelationExistsComparator - - """Comparator for filtering projects with relations.""" - hasRelatedRelations: RelationExistsComparator - - """Comparator for filtering projects with violated dependencies.""" - hasViolatedRelations: RelationExistsComparator - - """Comparator for the project health: onTrack, atRisk, offTrack""" - health: StringComparator - - """ - Comparator for the project health (with age): onTrack, atRisk, offTrack, outdated, noUpdate - """ - healthWithAge: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the projects initiatives must satisfy.""" - initiatives: InitiativeCollectionFilter - - """Filters that the projects issues must satisfy.""" - issues: IssueCollectionFilter - - """Filters that project labels must satisfy.""" - labels: ProjectLabelCollectionFilter - - """Filters that the last applied template must satisfy.""" - lastAppliedTemplate: NullableTemplateFilter - - """Filters that the projects lead must satisfy.""" - lead: NullableUserFilter - - """Filters that the projects members must satisfy.""" - members: UserCollectionFilter - - """Comparator for the project name.""" - name: StringComparator - - """Filters that the project's customer needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Filters that the project's next milestone must satisfy.""" - nextProjectMilestone: ProjectMilestoneFilter - - """Filter based on the existence of the relation.""" - null: Boolean - - """Compound filters, one of which need to be matched by the project.""" - or: [NullableProjectFilter!] - - """Comparator for the projects priority.""" - priority: NullableNumberComparator - - """Filters that the project's milestones must satisfy.""" - projectMilestones: ProjectMilestoneCollectionFilter - - """Comparator for the project updates.""" - projectUpdates: ProjectUpdatesCollectionFilter - - """Filters that the projects roadmaps must satisfy.""" - roadmaps: RoadmapCollectionFilter - - """[Internal] Comparator for the project's content.""" - searchableContent: ContentComparator - - """Comparator for the project slug ID.""" - slugId: StringComparator - - """Comparator for the project start date.""" - startDate: NullableDateComparator - - """[DEPRECATED] Comparator for the project state.""" - state: StringComparator - - """Filters that the project's status must satisfy.""" - status: ProjectStatusFilter - - """Comparator for the project target date.""" - targetDate: NullableDateComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Project milestone filtering options.""" -input NullableProjectMilestoneFilter { - """ - Compound filters, all of which need to be matched by the project milestone. - """ - and: [NullableProjectMilestoneFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the project milestone name.""" - name: NullableStringComparator - - """Filter based on the existence of the relation.""" - null: Boolean - - """ - Compound filters, one of which need to be matched by the project milestone. - """ - or: [NullableProjectMilestoneFilter!] - - """Comparator for the project milestone target date.""" - targetDate: NullableDateComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Nullable project update filtering options.""" -input NullableProjectUpdateFilter { - """ - Compound filters, all of which need to be matched by the project update. - """ - and: [NullableProjectUpdateFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filter based on the existence of the relation.""" - null: Boolean - - """ - Compound filters, one of which need to be matched by the project update. - """ - or: [NullableProjectUpdateFilter!] - - """Filters that the project update project must satisfy.""" - project: ProjectFilter - - """Filters that the project updates reactions must satisfy.""" - reactions: ReactionCollectionFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator - - """Filters that the project update creator must satisfy.""" - user: UserFilter -} - -"""Comparator for optional strings.""" -input NullableStringComparator { - """Contains constraint. Matches any values that contain the given string.""" - contains: String - - """ - Contains case insensitive constraint. Matches any values that contain the given string case insensitive. - """ - containsIgnoreCase: String - - """ - Contains case and accent insensitive constraint. Matches any values that contain the given string case and accent insensitive. - """ - containsIgnoreCaseAndAccent: String - - """ - Ends with constraint. Matches any values that end with the given string. - """ - endsWith: String - - """Equals constraint.""" - eq: String - - """ - Equals case insensitive. Matches any values that matches the given string case insensitive. - """ - eqIgnoreCase: String - - """In-array constraint.""" - in: [String!] - - """Not-equals constraint.""" - neq: String - - """ - Not-equals case insensitive. Matches any values that don't match the given string case insensitive. - """ - neqIgnoreCase: String - - """Not-in-array constraint.""" - nin: [String!] - - """ - Doesn't contain constraint. Matches any values that don't contain the given string. - """ - notContains: String - - """ - Doesn't contain case insensitive constraint. Matches any values that don't contain the given string case insensitive. - """ - notContainsIgnoreCase: String - - """ - Doesn't end with constraint. Matches any values that don't end with the given string. - """ - notEndsWith: String - - """ - Doesn't start with constraint. Matches any values that don't start with the given string. - """ - notStartsWith: String - - """ - Null constraint. Matches any non-null values if the given value is false, otherwise it matches null values. - """ - null: Boolean - - """ - Starts with constraint. Matches any values that start with the given string. - """ - startsWith: String - - """ - Starts with case insensitive constraint. Matches any values that start with the given string. - """ - startsWithIgnoreCase: String -} - -"""Team filtering options.""" -input NullableTeamFilter { - """Compound filters, all of which need to be matched by the team.""" - and: [NullableTeamFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the team description.""" - description: NullableStringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the teams issues must satisfy.""" - issues: IssueCollectionFilter - - """Comparator for the team key.""" - key: StringComparator - - """Comparator for the team name.""" - name: StringComparator - - """Filter based on the existence of the relation.""" - null: Boolean - - """Compound filters, one of which need to be matched by the team.""" - or: [NullableTeamFilter!] - - """Filters that the teams parent must satisfy.""" - parent: NullableTeamFilter - - """Comparator for the team privacy.""" - private: BooleanComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Template filtering options.""" -input NullableTemplateFilter { - """Compound filters, all of which need to be matched by the template.""" - and: [NullableTemplateFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the template's name.""" - name: StringComparator - - """Filter based on the existence of the relation.""" - null: Boolean - - """Compound filters, one of which need to be matched by the template.""" - or: [NullableTemplateFilter!] - - """Comparator for the template's type.""" - type: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Comparator for optional timeless dates.""" -input NullableTimelessDateComparator { - """Equals constraint.""" - eq: TimelessDateOrDuration - - """ - Greater-than constraint. Matches any values that are greater than the given value. - """ - gt: TimelessDateOrDuration - - """ - Greater-than-or-equal constraint. Matches any values that are greater than or equal to the given value. - """ - gte: TimelessDateOrDuration - - """In-array constraint.""" - in: [TimelessDateOrDuration!] - - """ - Less-than constraint. Matches any values that are less than the given value. - """ - lt: TimelessDateOrDuration - - """ - Less-than-or-equal constraint. Matches any values that are less than or equal to the given value. - """ - lte: TimelessDateOrDuration - - """Not-equals constraint.""" - neq: TimelessDateOrDuration - - """Not-in-array constraint.""" - nin: [TimelessDateOrDuration!] - - """ - Null constraint. Matches any non-null values if the given value is false, otherwise it matches null values. - """ - null: Boolean -} - -"""User filtering options.""" -input NullableUserFilter { - """Comparator for the user's activity status.""" - active: BooleanComparator - - """Comparator for the user's admin status.""" - admin: BooleanComparator - - """Compound filters, all of which need to be matched by the user.""" - and: [NullableUserFilter!] - - """Comparator for the user's app status.""" - app: BooleanComparator - - """Filters that the users assigned issues must satisfy.""" - assignedIssues: IssueCollectionFilter - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the user's display name.""" - displayName: StringComparator - - """Comparator for the user's email.""" - email: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the user's invited status.""" - invited: BooleanComparator - - """ - Filter based on the currently authenticated user. Set to true to filter for the authenticated user, false for any other user. - """ - isMe: BooleanComparator - - """Comparator for the user's name.""" - name: StringComparator - - """Filter based on the existence of the relation.""" - null: Boolean - - """Compound filters, one of which need to be matched by the user.""" - or: [NullableUserFilter!] - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Comparator for numbers.""" -input NumberComparator { - """Equals constraint.""" - eq: Float - - """ - Greater-than constraint. Matches any values that are greater than the given value. - """ - gt: Float - - """ - Greater-than-or-equal constraint. Matches any values that are greater than or equal to the given value. - """ - gte: Float - - """In-array constraint.""" - in: [Float!] - - """ - Less-than constraint. Matches any values that are less than the given value. - """ - lt: Float - - """ - Less-than-or-equal constraint. Matches any values that are less than or equal to the given value. - """ - lte: Float - - """Not-equals constraint.""" - neq: Float - - """Not-in-array constraint.""" - nin: [Float!] -} - -""" -The different requests statuses possible for an OAuth client approval request. -""" -enum OAuthClientApprovalStatus { - approved - denied - requested -} - -""" -Request to install OAuth clients on organizations and the response to the request. -""" -type OauthClientApproval implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The reason the request for the OAuth client approval was denied.""" - denyReason: String - - """The unique identifier of the entity.""" - id: ID! - - """The uuid of the OAuth client being requested for installation.""" - oauthClientId: String! - - """The reason the person wants to install this OAuth client.""" - requestReason: String - - """The person who requested installing the OAuth client.""" - requesterId: String! - - """The person who responded to the request to install the OAuth client.""" - responderId: String - - """The scopes the app has requested.""" - scopes: [String!]! - - """The status for the OAuth client approval request.""" - status: OAuthClientApprovalStatus! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""An oauth client approval related notification.""" -type OauthClientApprovalNotification implements Entity & Node & Notification { - """The user that caused the notification.""" - actor: User - - """[Internal] Notification actor initials if avatar is not available.""" - actorAvatarColor: String! - - """[Internal] Notification avatar URL.""" - actorAvatarUrl: String - - """[Internal] Notification actor initials if avatar is not available.""" - actorInitials: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The bot that caused the notification.""" - botActor: ActorBot - - """The category of the notification.""" - category: NotificationCategory! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The time at when an email reminder for this notification was sent to the user. Null, if no email - reminder has been sent. - """ - emailedAt: DateTime - - """The external user that caused the notification.""" - externalUserActor: ExternalUser - - """ - [Internal] Notifications with the same grouping key will be grouped together in the UI. - """ - groupingKey: String! - - """ - [Internal] Priority of the notification with the same grouping key. Higher number means higher priority. If priority is the same, notifications should be sorted by `createdAt`. - """ - groupingPriority: Float! - - """The unique identifier of the entity.""" - id: ID! - - """[Internal] Inbox URL for the notification.""" - inboxUrl: String! - - """[Internal] If notification actor was Linear.""" - isLinearActor: Boolean! - - """[Internal] Issue's status type for issue notifications.""" - issueStatusType: String - - """The OAuth client approval request related to the notification.""" - oauthClientApproval: OauthClientApproval! - - """Related OAuth client approval request ID.""" - oauthClientApprovalId: String! - - """[Internal] Project update health for new updates.""" - projectUpdateHealth: String - - """ - The time at when the user marked the notification as read. Null, if the the user hasn't read the notification - """ - readAt: DateTime - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime - - """[Internal] Notification subtitle.""" - subtitle: String! - - """[Internal] Notification title.""" - title: String! - - """Notification type.""" - type: String! - - """The time at which a notification was unsnoozed..""" - unsnoozedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """[Internal] URL to the target of the notification.""" - url: String! - - """The user that received the notification.""" - user: User! -} - -input OnboardingCustomerSurvey { - companyRole: String - companySize: String -} - -input OpsgenieInput { - """The date when the Opsgenie API failed with an unauthorized error.""" - apiFailedWithUnauthorizedErrorAt: DateTime -} - -""" -An organization. Organizations are root-level objects that contain user accounts and teams. -""" -type Organization implements Node { - """[INTERNAL] Whether the organization has enabled the AI add-on.""" - aiAddonEnabled: Boolean! - - """Whether member users are allowed to send invites.""" - allowMembersToInvite: Boolean - - """Allowed authentication providers, empty array means all are allowed.""" - allowedAuthServices: [String!]! - - """Allowed file upload content types""" - allowedFileUploadContentTypes: [String!] - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """Number of issues in the organization.""" - createdIssueCount: Int! - - """Number of customers in the organization.""" - customerCount: Int! - - """Configuration settings for the Customers feature.""" - customersConfiguration: JSONObject! - - """Whether the organization is using Customers.""" - customersEnabled: Boolean! - - """Default schedule for how often feed summaries are generated.""" - defaultFeedSummarySchedule: FeedSummarySchedule - - """The time at which deletion of the organization was requested.""" - deletionRequestedAt: DateTime - - """[Internal] Facets associated with the organization.""" - facets: [Facet!]! - - """Whether the organization has enabled the feed feature.""" - feedEnabled: Boolean! - - """The month at which the fiscal year starts. Defaults to January (0).""" - fiscalYearStartMonth: Float! - - """ - How git branches are formatted. If null, default formatting will be used. - """ - gitBranchFormat: String - - """ - Whether the Git integration linkback messages should be sent to private repositories. - """ - gitLinkbackMessagesEnabled: Boolean! - - """ - Whether the Git integration linkback messages should be sent to public repositories. - """ - gitPublicLinkbackMessagesEnabled: Boolean! - - """The unique identifier of the entity.""" - id: ID! - - """ - The n-weekly frequency at which to prompt for initiative updates. When not set, reminders are off. - """ - initiativeUpdateReminderFrequencyInWeeks: Float - - """The day at which to prompt for initiative updates.""" - initiativeUpdateRemindersDay: Day! - - """The hour at which to prompt for initiative updates.""" - initiativeUpdateRemindersHour: Float! - - """Integrations associated with the organization.""" - integrations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IntegrationConnection! - - """IP restriction configurations.""" - ipRestrictions: [OrganizationIpRestriction!] - - """Labels associated with the organization.""" - labels( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issue labels.""" - filter: IssueLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueLabelConnection! - - """The organization's logo URL.""" - logoUrl: String - - """The organization's name.""" - name: String! - - """Rolling 30-day total upload volume for the organization, in megabytes.""" - periodUploadVolume: Float! - - """ - Previously used URL keys for the organization (last 3 are kept and redirected). - """ - previousUrlKeys: [String!]! - - """Project labels associated with the organization.""" - projectLabels( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned project labels.""" - filter: ProjectLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectLabelConnection! - - """The organization's project statuses.""" - projectStatuses: [ProjectStatus!]! - - """ - The n-weekly frequency at which to prompt for project updates. When not set, reminders are off. - """ - projectUpdateReminderFrequencyInWeeks: Float - - """The day at which to prompt for project updates.""" - projectUpdateRemindersDay: Day! - - """The hour at which to prompt for project updates.""" - projectUpdateRemindersHour: Float! - - """[DEPRECATED] The frequency at which to prompt for project updates.""" - projectUpdatesReminderFrequency: ProjectUpdateReminderFrequency! @deprecated(reason: "Use organization.projectUpdatesReminderFrequencyInWeeks instead") - - """The feature release channel the organization belongs to.""" - releaseChannel: ReleaseChannel! - - """ - Whether workspace label creation, update, and deletion is restricted to admins. - """ - restrictLabelManagementToAdmins: Boolean - - """Whether team creation is restricted to admins.""" - restrictTeamCreationToAdmins: Boolean - - """Whether the organization is using a roadmap.""" - roadmapEnabled: Boolean! - - """Whether SAML authentication is enabled for organization.""" - samlEnabled: Boolean! - - """[INTERNAL] SAML settings.""" - samlSettings: JSONObject - - """Whether SCIM provisioning is enabled for organization.""" - scimEnabled: Boolean! - - """[INTERNAL] SCIM settings.""" - scimSettings: JSONObject - - """[DEPRECATED] Which day count to use for SLA calculations.""" - slaDayCount: SLADayCountType! @deprecated(reason: "No longer in use") - - """The organization's subscription to a paid plan.""" - subscription: PaidSubscription - - """Teams associated with the organization.""" - teams( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned teams.""" - filter: TeamFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TeamConnection! - - """Templates associated with the organization.""" - templates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned templates.""" - filter: NullableTemplateFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TemplateConnection! - - """[ALPHA] Theme settings for the organization.""" - themeSettings: JSONObject - - """The time at which the trial will end.""" - trialEndsAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The organization's unique URL key.""" - urlKey: String! - - """Number of active users in the organization.""" - userCount: Int! - - """Users associated with the organization.""" - users( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Should query return disabled/suspended users (default: false).""" - includeDisabled: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): UserConnection! - - """[Internal] The list of working days. Sunday is 0, Monday is 1, etc.""" - workingDays: [Float!]! -} - -type OrganizationAcceptedOrExpiredInviteDetailsPayload { - """The status of the invite.""" - status: OrganizationInviteStatus! -} - -type OrganizationCancelDeletePayload { - """Whether the operation was successful.""" - success: Boolean! -} - -type OrganizationDeletePayload { - """Whether the operation was successful.""" - success: Boolean! -} - -"""Defines the use of a domain by an organization.""" -type OrganizationDomain implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """What type of auth is the domain used for.""" - authType: OrganizationDomainAuthType! - - """ - Whether the domains was claimed by the organization through DNS verification. - """ - claimed: Boolean - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who added the domain.""" - creator: User - - """Prevent users with this domain to create new workspaces.""" - disableOrganizationCreation: Boolean - - """The unique identifier of the entity.""" - id: ID! - - """The identity provider the domain belongs to.""" - identityProvider: IdentityProvider - - """Domain name.""" - name: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """E-mail used to verify this domain.""" - verificationEmail: String - - """Is this domain verified.""" - verified: Boolean! -} - -"""What type of auth is the domain used for.""" -enum OrganizationDomainAuthType { - general - saml -} - -"""[INTERNAL] Domain claim request response.""" -type OrganizationDomainClaimPayload { - """String to put into DNS for verification.""" - verificationString: String! -} - -input OrganizationDomainCreateInput { - """The authentication type this domain is for.""" - authType: String = "general" - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The identity provider to which to add the domain.""" - identityProviderId: String - - """The domain name to add.""" - name: String! - - """The email address to which to send the verification code.""" - verificationEmail: String -} - -"""[INTERNAL] Organization domain operation response.""" -type OrganizationDomainPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The organization domain that was created or updated.""" - organizationDomain: OrganizationDomain! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""[INTERNAL] Organization domain operation response.""" -type OrganizationDomainSimplePayload { - """Whether the operation was successful.""" - success: Boolean! -} - -input OrganizationDomainUpdateInput { - """ - Prevent users with this domain to create new workspaces. Only allowed to set on claimed domains! - """ - disableOrganizationCreation: Boolean -} - -input OrganizationDomainVerificationInput { - """The identifier in UUID v4 format of the domain being verified.""" - organizationDomainId: String! - - """The verification code sent via email.""" - verificationCode: String! -} - -type OrganizationExistsPayload { - """Whether the organization exists.""" - exists: Boolean! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""An invitation to the organization that has been sent via email.""" -type OrganizationInvite implements Node { - """ - The time at which the invite was accepted. Null, if the invite hasn't been accepted. - """ - acceptedAt: DateTime - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The invitees email address.""" - email: String! - - """ - The time at which the invite will be expiring. Null, if the invite shouldn't expire. - """ - expiresAt: DateTime - - """The invite was sent to external address.""" - external: Boolean! - - """The unique identifier of the entity.""" - id: ID! - - """ - The user who has accepted the invite. Null, if the invite hasn't been accepted. - """ - invitee: User - - """The user who created the invitation.""" - inviter: User! - - """Extra metadata associated with the organization invite.""" - metadata: JSONObject - - """The organization that the invite is associated with.""" - organization: Organization! - - """The user role that the invitee will receive upon accepting the invite.""" - role: UserRoleType! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type OrganizationInviteConnection { - edges: [OrganizationInviteEdge!]! - nodes: [OrganizationInvite!]! - pageInfo: PageInfo! -} - -input OrganizationInviteCreateInput { - """The email of the invitee.""" - email: String! - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """[INTERNAL] Optional metadata about the invite.""" - metadata: JSONObject - - """What user role the invite should grant.""" - role: UserRoleType = user - - """The teams that the user has been invited to.""" - teamIds: [String!] -} - -union OrganizationInviteDetailsPayload = OrganizationAcceptedOrExpiredInviteDetailsPayload | OrganizationInviteFullDetailsPayload - -type OrganizationInviteEdge { - """Used in `before` and `after` args""" - cursor: String! - node: OrganizationInvite! -} - -type OrganizationInviteFullDetailsPayload { - """Whether the invite has already been accepted.""" - accepted: Boolean! - - """Allowed authentication providers, empty array means all are allowed.""" - allowedAuthServices: [String!]! - - """When the invite was created.""" - createdAt: DateTime! - - """The email of the invitee.""" - email: String! - - """Whether the invite has expired.""" - expired: Boolean! - - """The name of the inviter.""" - inviter: String! - - """ID of the workspace the invite is for.""" - organizationId: String! - - """URL of the workspace logo the invite is for.""" - organizationLogoUrl: String - - """Name of the workspace the invite is for.""" - organizationName: String! - - """What user role the invite should grant.""" - role: UserRoleType! - - """The status of the invite.""" - status: OrganizationInviteStatus! -} - -type OrganizationInvitePayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The organization invite that was created or updated.""" - organizationInvite: OrganizationInvite! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""The different statuses possible for an organization invite.""" -enum OrganizationInviteStatus { - accepted - expired - pending -} - -input OrganizationInviteUpdateInput { - """The teams that the user has been invited to.""" - teamIds: [String!]! -} - -type OrganizationIpRestriction { - """Optional restriction description.""" - description: String - - """Whether the restriction is enabled.""" - enabled: Boolean! - - """IP range in CIDR format.""" - range: String! - - """Restriction type.""" - type: String! -} - -"""[INTERNAL] Organization IP restriction configuration.""" -input OrganizationIpRestrictionInput { - """Optional restriction description.""" - description: String - - """Whether the restriction is enabled.""" - enabled: Boolean! - - """IP range in CIDR format.""" - range: String! - - """Restriction type.""" - type: String! -} - -type OrganizationMeta { - """Allowed authentication providers, empty array means all are allowed.""" - allowedAuthServices: [String!]! - - """The region the organization is hosted in.""" - region: String! -} - -type OrganizationPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The organization that was created or updated.""" - organization: Organization - - """Whether the operation was successful.""" - success: Boolean! -} - -input OrganizationStartTrialInput { - """The plan type to trial.""" - planType: String! -} - -type OrganizationStartTrialPayload { - """Whether the operation was successful.""" - success: Boolean! -} - -input OrganizationUpdateInput { - """[INTERNAL] Whether the organization has enabled the AI add-on.""" - aiAddonEnabled: Boolean - - """[INTERNAL] Whether the organization has opted in to AI telemetry.""" - aiTelemetryEnabled: Boolean - - """Whether member users are allowed to send invites.""" - allowMembersToInvite: Boolean - - """List of services that are allowed to be used for login.""" - allowedAuthServices: [String!] - - """Allowed file upload content types.""" - allowedFileUploadContentTypes: [String!] - - """[INTERNAL] Configuration settings for the Customers feature.""" - customersConfiguration: JSONObject - - """[INTERNAL] Whether the organization is using customers.""" - customersEnabled: Boolean - - """Default schedule for how often feed summaries are generated.""" - defaultFeedSummarySchedule: FeedSummarySchedule - - """Whether the organization has enabled the feed feature.""" - feedEnabled: Boolean - - """The month at which the fiscal year starts.""" - fiscalYearStartMonth: Float - - """ - How git branches are formatted. If null, default formatting will be used. - """ - gitBranchFormat: String - - """ - Whether the Git integration linkback messages should be sent for private repositories. - """ - gitLinkbackMessagesEnabled: Boolean - - """ - Whether the Git integration linkback messages should be sent for public repositories. - """ - gitPublicLinkbackMessagesEnabled: Boolean - - """ - [ALPHA] The n-weekly frequency at which to prompt for initiative updates. - """ - initiativeUpdateReminderFrequencyInWeeks: Float - - """[ALPHA] The day at which initiative updates are sent.""" - initiativeUpdateRemindersDay: Day - - """[ALPHA] The hour at which initiative updates are sent.""" - initiativeUpdateRemindersHour: Float - - """ - IP restriction configurations controlling allowed access the workspace. - """ - ipRestrictions: [OrganizationIpRestrictionInput!] - - """The logo of the organization.""" - logoUrl: String - - """The name of the organization.""" - name: String - - """ - Whether the organization has opted for having to approve all OAuth applications for install. - """ - oauthAppReview: Boolean - - """[INTERNAL] Whether the organization has enabled the member API keys.""" - personalApiKeysEnabled: Boolean - - """The n-weekly frequency at which to prompt for project updates.""" - projectUpdateReminderFrequencyInWeeks: Float - - """The day at which project updates are sent.""" - projectUpdateRemindersDay: Day - - """The hour at which project updates are sent.""" - projectUpdateRemindersHour: Float - - """ - Whether the organization has opted for reduced customer support attachment information. - """ - reducedPersonalInformation: Boolean - - """Whether agent invocation is restricted to full workspace members.""" - restrictAgentInvocationToMembers: Boolean - - """Whether label creation is restricted to admins.""" - restrictLabelManagementToAdmins: Boolean - - """Whether team creation is restricted to admins.""" - restrictTeamCreationToAdmins: Boolean - - """Whether the organization is using roadmap.""" - roadmapEnabled: Boolean - - """Internal. Whether SLAs have been enabled for the organization.""" - slaEnabled: Boolean - - """[ALPHA] Theme settings for the organization.""" - themeSettings: JSONObject - - """The URL key of the organization.""" - urlKey: String - - """[Internal] The list of working days. Sunday is 0, Monday is 1, etc.""" - workingDays: [Float!] -} - -"""Customer owner sorting options.""" -input OwnerSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -type PageInfo { - """Cursor representing the last result in the paginated results.""" - endCursor: String - - """Indicates if there are more results when paginating forward.""" - hasNextPage: Boolean! - - """Indicates if there are more results when paginating backward.""" - hasPreviousPage: Boolean! - - """Cursor representing the first result in the paginated results.""" - startCursor: String -} - -input PagerDutyInput { - """The date when the PagerDuty API failed with an unauthorized error.""" - apiFailedWithUnauthorizedErrorAt: DateTime -} - -"""How to treat NULL values, whether they should appear first or last""" -enum PaginationNulls { - first - last -} - -"""By which field should the pagination order by""" -enum PaginationOrderBy { - createdAt - updatedAt -} - -"""Whether to sort in ascending or descending order""" -enum PaginationSortOrder { - Ascending - Descending -} - -"""The paid subscription of an organization.""" -type PaidSubscription implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The date the subscription is scheduled to be canceled, if any.""" - cancelAt: DateTime - - """The date the subscription was canceled, if any.""" - canceledAt: DateTime - - """ - The collection method for this subscription, either automatically charged or invoiced. - """ - collectionMethod: String! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The creator of the subscription.""" - creator: User - - """The unique identifier of the entity.""" - id: ID! - - """The date the subscription will be billed next.""" - nextBillingAt: DateTime - - """The organization that the subscription is associated with.""" - organization: Organization! - - """The subscription type of a pending change. Null if no change pending.""" - pendingChangeType: String - - """The number of seats in the subscription.""" - seats: Float! - - """The maximum number of seats that will be billed in the subscription.""" - seatsMaximum: Float - - """The minimum number of seats that will be billed in the subscription.""" - seatsMinimum: Float - - """The subscription type.""" - type: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -input PartialNotificationChannelPreferencesInput { - """Whether notifications are currently enabled for desktop.""" - desktop: Boolean - - """Whether notifications are currently enabled for email.""" - email: Boolean - - """Whether notifications are currently enabled for mobile.""" - mobile: Boolean - - """Whether notifications are currently enabled for Slack.""" - slack: Boolean -} - -type PasskeyLoginStartResponse { - options: JSONObject! - success: Boolean! -} - -"""[Internal] A generic post.""" -type Post implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The update content summarized for audio consumption.""" - audioSummary: String - - """The update content in markdown format.""" - body: String! - - """[Internal] The content of the post as a Prosemirror document.""" - bodyData: String! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who wrote the post.""" - creator: User - - """The time the post was edited.""" - editedAt: DateTime - - """The log id of the ai response.""" - evalLogId: String - - """Schedule used to create a post summary.""" - feedSummaryScheduleAtCreate: FeedSummarySchedule - - """The unique identifier of the entity.""" - id: ID! - - """Emoji reaction summary, grouped by emoji type.""" - reactionData: JSONObject! - - """The post's unique URL slug.""" - slugId: String! - - """The team that the post is associated with.""" - team: Team - - """The post's title.""" - title: String - - """A URL of the TTL (text-to-language) for the body.""" - ttlUrl: String - - """The type of the post.""" - type: PostType - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user that the post is associated with.""" - user: User - - """[Internal] The written update data used to compose the written post.""" - writtenSummaryData: JSONObject -} - -"""A post related notification.""" -type PostNotification implements Entity & Node & Notification { - """The user that caused the notification.""" - actor: User - - """[Internal] Notification actor initials if avatar is not available.""" - actorAvatarColor: String! - - """[Internal] Notification avatar URL.""" - actorAvatarUrl: String - - """[Internal] Notification actor initials if avatar is not available.""" - actorInitials: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The bot that caused the notification.""" - botActor: ActorBot - - """The category of the notification.""" - category: NotificationCategory! - - """ - Related comment ID. Null if the notification is not related to a comment. - """ - commentId: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The time at when an email reminder for this notification was sent to the user. Null, if no email - reminder has been sent. - """ - emailedAt: DateTime - - """The external user that caused the notification.""" - externalUserActor: ExternalUser - - """ - [Internal] Notifications with the same grouping key will be grouped together in the UI. - """ - groupingKey: String! - - """ - [Internal] Priority of the notification with the same grouping key. Higher number means higher priority. If priority is the same, notifications should be sorted by `createdAt`. - """ - groupingPriority: Float! - - """The unique identifier of the entity.""" - id: ID! - - """[Internal] Inbox URL for the notification.""" - inboxUrl: String! - - """[Internal] If notification actor was Linear.""" - isLinearActor: Boolean! - - """[Internal] Issue's status type for issue notifications.""" - issueStatusType: String - - """ - Related parent comment ID. Null if the notification is not related to a comment. - """ - parentCommentId: String - - """Related post ID.""" - postId: String! - - """[Internal] Project update health for new updates.""" - projectUpdateHealth: String - - """Name of the reaction emoji related to the notification.""" - reactionEmoji: String - - """ - The time at when the user marked the notification as read. Null, if the the user hasn't read the notification - """ - readAt: DateTime - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime - - """[Internal] Notification subtitle.""" - subtitle: String! - - """[Internal] Notification title.""" - title: String! - - """Notification type.""" - type: String! - - """The time at which a notification was unsnoozed..""" - unsnoozedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """[Internal] URL to the target of the notification.""" - url: String! - - """The user that received the notification.""" - user: User! -} - -"""Type of Post""" -enum PostType { - summary - update -} - -"""Issue priority sorting options.""" -input PrioritySort { - """Whether to consider no priority as the highest or lowest priority""" - noPriorityFirst: Boolean = false - - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -""" -[Internal] The scope of product intelligence suggestion data for a team. -""" -enum ProductIntelligenceScope { - none - team - teamHierarchy - workspace -} - -"""A project.""" -type Project implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The time at which the project was automatically archived by the auto pruning process. - """ - autoArchivedAt: DateTime - - """The time at which the project was moved into canceled state.""" - canceledAt: DateTime - - """The project's color.""" - color: String! - - """Comments associated with the project overview.""" - comments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned comments.""" - filter: CommentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CommentConnection! - - """The time at which the project was moved into completed state.""" - completedAt: DateTime - - """The number of completed issues in the project after each week.""" - completedIssueCountHistory: [Float!]! - - """The number of completed estimation points after each week.""" - completedScopeHistory: [Float!]! - - """The project's content in markdown format.""" - content: String - - """[Internal] The project's content as YJS state.""" - contentState: String - - """The project was created based on this issue.""" - convertedFromIssue: Issue - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the project.""" - creator: User - - """[INTERNAL] The current progress of the project.""" - currentProgress: JSONObject! - - """The project's description.""" - description: String! - - """The content of the project description.""" - documentContent: DocumentContent - - """Documents associated with the project.""" - documents( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned documents.""" - filter: DocumentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): DocumentConnection! - - """External links associated with the project.""" - externalLinks( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): EntityExternalLinkConnection! - - """[Internal] Facets associated with the project.""" - facets: [Facet!]! - - """The user's favorite associated with this project.""" - favorite: Favorite - - """The resolution of the reminder frequency.""" - frequencyResolution: FrequencyResolutionType! - - """The health of the project.""" - health: ProjectUpdateHealthType - - """The time at which the project health was updated.""" - healthUpdatedAt: DateTime - - """History entries associated with the project.""" - history( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectHistoryConnection! - - """The icon of the project.""" - icon: String - - """The unique identifier of the entity.""" - id: ID! - - """The number of in progress estimation points after each week.""" - inProgressScopeHistory: [Float!]! - - """Initiatives that this project belongs to.""" - initiatives( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): InitiativeConnection! - - """Settings for all integrations associated with that project.""" - integrationsSettings: IntegrationsSettings - - """Inverse relations associated with this project.""" - inverseRelations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectRelationConnection! - - """The total number of issues in the project after each week.""" - issueCountHistory: [Float!]! - - """Issues associated with the project.""" - issues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """Id of the labels associated with this project.""" - labelIds: [String!]! - - """Labels associated with this project.""" - labels( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned project labels.""" - filter: ProjectLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectLabelConnection! - - """The last template that was applied to this project.""" - lastAppliedTemplate: Template - - """The last project update posted for this project.""" - lastUpdate: ProjectUpdate - - """The project lead.""" - lead: User - - """Users that are members of the project.""" - members( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned users.""" - filter: UserFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Should query return disabled/suspended users (default: false).""" - includeDisabled: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): UserConnection! - - """The project's name.""" - name: String! - - """Customer needs associated with the project.""" - needs( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned needs.""" - filter: CustomerNeedFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CustomerNeedConnection! - - """ - The priority of the project. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: Int! - - """The priority of the project as a label.""" - priorityLabel: String! - - """ - The sort order for the project within the organization, when ordered by priority. - """ - prioritySortOrder: Float! - - """ - The overall progress of the project. This is the (completed estimate points + 0.25 * in progress estimate points) / total estimate points. - """ - progress: Float! - - """[INTERNAL] The progress history of the project.""" - progressHistory: JSONObject! - - """Milestones associated with the project.""" - projectMilestones( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned milestones.""" - filter: ProjectMilestoneFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectMilestoneConnection! - - """The time until which project update reminders are paused.""" - projectUpdateRemindersPausedUntilAt: DateTime - - """Project updates associated with the project.""" - projectUpdates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectUpdateConnection! - - """Relations associated with this project.""" - relations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectRelationConnection! - - """The overall scope (total estimate points) of the project.""" - scope: Float! - - """The total number of estimation points after each week.""" - scopeHistory: [Float!]! - - """Whether to send new issue comment notifications to Slack.""" - slackIssueComments: Boolean! @deprecated(reason: "No longer in use") - - """Whether to send new issue status updates to Slack.""" - slackIssueStatuses: Boolean! @deprecated(reason: "No longer is use") - - """Whether to send new issue notifications to Slack.""" - slackNewIssue: Boolean! @deprecated(reason: "No longer in use") - - """The project's unique URL slug.""" - slugId: String! - - """The sort order for the project within the organization.""" - sortOrder: Float! - - """The estimated start date of the project.""" - startDate: TimelessDate - - """The resolution of the project's start date.""" - startDateResolution: DateResolutionType - - """The time at which the project was moved into started state.""" - startedAt: DateTime - - """[DEPRECATED] The type of the state.""" - state: String! @deprecated(reason: "Use project.status instead") - - """The status that the project is associated with.""" - status: ProjectStatus! - - """The estimated completion date of the project.""" - targetDate: TimelessDate - - """The resolution of the project's estimated completion date.""" - targetDateResolution: DateResolutionType - - """Teams associated with this project.""" - teams( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned teams.""" - filter: TeamFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TeamConnection! - - """A flag that indicates whether the project is in the trash bin.""" - trashed: Boolean - - """ - The frequency at which to prompt for updates. When not set, reminders are inherited from workspace. - """ - updateReminderFrequency: Float - - """ - The n-weekly frequency at which to prompt for updates. When not set, reminders are inherited from workspace. - """ - updateReminderFrequencyInWeeks: Float - - """The day at which to prompt for updates.""" - updateRemindersDay: Day - - """The hour at which to prompt for updates.""" - updateRemindersHour: Float - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Project URL.""" - url: String! -} - -"""A generic payload return from entity archive mutations.""" -type ProjectArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: Project - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Project attachment""" -type ProjectAttachment implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The creator of the attachment.""" - creator: User - - """The unique identifier of the entity.""" - id: ID! - - """Custom metadata related to the attachment.""" - metadata: JSONObject! - - """Information about the external source which created the attachment.""" - source: JSONObject - - """ - An accessor helper to source.type, defines the source type of the attachment. - """ - sourceType: String - - """Optional subtitle of the attachment""" - subtitle: String - - """Title of the attachment.""" - title: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """URL of the attachment.""" - url: String! -} - -"""Project filtering options.""" -input ProjectCollectionFilter { - """Filters that the project's team must satisfy.""" - accessibleTeams: TeamCollectionFilter - - """ - [ALPHA] Comparator for the project activity type: buzzin, active, some, none - """ - activityType: StringComparator - - """Compound filters, all of which need to be matched by the project.""" - and: [ProjectCollectionFilter!] - - """Comparator for the project cancelation date.""" - canceledAt: NullableDateComparator - - """Comparator for the project completion date.""" - completedAt: NullableDateComparator - - """Filters that the project's completed milestones must satisfy.""" - completedProjectMilestones: ProjectMilestoneCollectionFilter - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the projects creator must satisfy.""" - creator: UserFilter - - """Count of customers""" - customerCount: NumberComparator - - """Count of important customers""" - customerImportantCount: NumberComparator - - """Filters that needs to be matched by all projects.""" - every: ProjectFilter - - """Comparator for filtering projects which are blocked.""" - hasBlockedByRelations: RelationExistsComparator - - """Comparator for filtering projects which are blocking.""" - hasBlockingRelations: RelationExistsComparator - - """ - [Deprecated] Comparator for filtering projects which this is depended on by. - """ - hasDependedOnByRelations: RelationExistsComparator - - """[Deprecated]Comparator for filtering projects which this depends on.""" - hasDependsOnRelations: RelationExistsComparator - - """Comparator for filtering projects with relations.""" - hasRelatedRelations: RelationExistsComparator - - """Comparator for filtering projects with violated dependencies.""" - hasViolatedRelations: RelationExistsComparator - - """Comparator for the project health: onTrack, atRisk, offTrack""" - health: StringComparator - - """ - Comparator for the project health (with age): onTrack, atRisk, offTrack, outdated, noUpdate - """ - healthWithAge: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the projects initiatives must satisfy.""" - initiatives: InitiativeCollectionFilter - - """Filters that the projects issues must satisfy.""" - issues: IssueCollectionFilter - - """Filters that project labels must satisfy.""" - labels: ProjectLabelCollectionFilter - - """Filters that the last applied template must satisfy.""" - lastAppliedTemplate: NullableTemplateFilter - - """Filters that the projects lead must satisfy.""" - lead: NullableUserFilter - - """Comparator for the collection length.""" - length: NumberComparator - - """Filters that the projects members must satisfy.""" - members: UserCollectionFilter - - """Comparator for the project name.""" - name: StringComparator - - """Filters that the project's customer needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Filters that the project's next milestone must satisfy.""" - nextProjectMilestone: ProjectMilestoneFilter - - """Compound filters, one of which need to be matched by the project.""" - or: [ProjectCollectionFilter!] - - """Comparator for the projects priority.""" - priority: NullableNumberComparator - - """Filters that the project's milestones must satisfy.""" - projectMilestones: ProjectMilestoneCollectionFilter - - """Comparator for the project updates.""" - projectUpdates: ProjectUpdatesCollectionFilter - - """Filters that the projects roadmaps must satisfy.""" - roadmaps: RoadmapCollectionFilter - - """[Internal] Comparator for the project's content.""" - searchableContent: ContentComparator - - """Comparator for the project slug ID.""" - slugId: StringComparator - - """Filters that needs to be matched by some projects.""" - some: ProjectFilter - - """Comparator for the project start date.""" - startDate: NullableDateComparator - - """[DEPRECATED] Comparator for the project state.""" - state: StringComparator - - """Filters that the project's status must satisfy.""" - status: ProjectStatusFilter - - """Comparator for the project target date.""" - targetDate: NullableDateComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type ProjectConnection { - edges: [ProjectEdge!]! - nodes: [Project!]! - pageInfo: PageInfo! -} - -input ProjectCreateInput { - """The color of the project.""" - color: String - - """The project content as markdown.""" - content: String - - """The ID of the issue from which that project is created.""" - convertedFromIssueId: String - - """The description for the project.""" - description: String - - """The icon of the project.""" - icon: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """ - [Internal]The identifiers of the project labels associated with this project. - """ - labelIds: [String!] - - """The ID of the last template applied to the project.""" - lastAppliedTemplateId: String - - """The identifier of the project lead.""" - leadId: String - - """The identifiers of the members of this project.""" - memberIds: [String!] - - """The name of the project.""" - name: String! - - """ - The priority of the project. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: Int - - """ - The sort order for the project within shared views, when ordered by priority. - """ - prioritySortOrder: Float - - """The sort order for the project within shared views.""" - sortOrder: Float - - """The planned start date of the project.""" - startDate: TimelessDate - - """The resolution of the project's start date.""" - startDateResolution: DateResolutionType - - """[DEPRECATED] The state of the project.""" - state: String @deprecated(reason: "Use statusId instead") - - """The ID of the project status.""" - statusId: String - - """The planned target date of the project.""" - targetDate: TimelessDate - - """The resolution of the project's estimated completion date.""" - targetDateResolution: DateResolutionType - - """The identifiers of the teams this project is associated with.""" - teamIds: [String!]! -} - -"""Project creation date sorting options.""" -input ProjectCreatedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -type ProjectEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Project! -} - -"""Project filtering options.""" -input ProjectFilter { - """Filters that the project's team must satisfy.""" - accessibleTeams: TeamCollectionFilter - - """ - [ALPHA] Comparator for the project activity type: buzzin, active, some, none - """ - activityType: StringComparator - - """Compound filters, all of which need to be matched by the project.""" - and: [ProjectFilter!] - - """Comparator for the project cancelation date.""" - canceledAt: NullableDateComparator - - """Comparator for the project completion date.""" - completedAt: NullableDateComparator - - """Filters that the project's completed milestones must satisfy.""" - completedProjectMilestones: ProjectMilestoneCollectionFilter - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the projects creator must satisfy.""" - creator: UserFilter - - """Count of customers""" - customerCount: NumberComparator - - """Count of important customers""" - customerImportantCount: NumberComparator - - """Comparator for filtering projects which are blocked.""" - hasBlockedByRelations: RelationExistsComparator - - """Comparator for filtering projects which are blocking.""" - hasBlockingRelations: RelationExistsComparator - - """ - [Deprecated] Comparator for filtering projects which this is depended on by. - """ - hasDependedOnByRelations: RelationExistsComparator - - """[Deprecated]Comparator for filtering projects which this depends on.""" - hasDependsOnRelations: RelationExistsComparator - - """Comparator for filtering projects with relations.""" - hasRelatedRelations: RelationExistsComparator - - """Comparator for filtering projects with violated dependencies.""" - hasViolatedRelations: RelationExistsComparator - - """Comparator for the project health: onTrack, atRisk, offTrack""" - health: StringComparator - - """ - Comparator for the project health (with age): onTrack, atRisk, offTrack, outdated, noUpdate - """ - healthWithAge: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the projects initiatives must satisfy.""" - initiatives: InitiativeCollectionFilter - - """Filters that the projects issues must satisfy.""" - issues: IssueCollectionFilter - - """Filters that project labels must satisfy.""" - labels: ProjectLabelCollectionFilter - - """Filters that the last applied template must satisfy.""" - lastAppliedTemplate: NullableTemplateFilter - - """Filters that the projects lead must satisfy.""" - lead: NullableUserFilter - - """Filters that the projects members must satisfy.""" - members: UserCollectionFilter - - """Comparator for the project name.""" - name: StringComparator - - """Filters that the project's customer needs must satisfy.""" - needs: CustomerNeedCollectionFilter - - """Filters that the project's next milestone must satisfy.""" - nextProjectMilestone: ProjectMilestoneFilter - - """Compound filters, one of which need to be matched by the project.""" - or: [ProjectFilter!] - - """Comparator for the projects priority.""" - priority: NullableNumberComparator - - """Filters that the project's milestones must satisfy.""" - projectMilestones: ProjectMilestoneCollectionFilter - - """Comparator for the project updates.""" - projectUpdates: ProjectUpdatesCollectionFilter - - """Filters that the projects roadmaps must satisfy.""" - roadmaps: RoadmapCollectionFilter - - """[Internal] Comparator for the project's content.""" - searchableContent: ContentComparator - - """Comparator for the project slug ID.""" - slugId: StringComparator - - """Comparator for the project start date.""" - startDate: NullableDateComparator - - """[DEPRECATED] Comparator for the project state.""" - state: StringComparator - - """Filters that the project's status must satisfy.""" - status: ProjectStatusFilter - - """Comparator for the project target date.""" - targetDate: NullableDateComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type ProjectFilterSuggestionPayload { - """The json filter that is suggested.""" - filter: JSONObject - - """The log id of the prompt, that created this filter.""" - logId: String -} - -"""Project health sorting options.""" -input ProjectHealthSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""An history associated with a project.""" -type ProjectHistory implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The events that happened while recording that history.""" - entries: JSONObject! - - """The unique identifier of the entity.""" - id: ID! - - """The project that the history is associated with.""" - project: Project! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type ProjectHistoryConnection { - edges: [ProjectHistoryEdge!]! - nodes: [ProjectHistory!]! - pageInfo: PageInfo! -} - -type ProjectHistoryEdge { - """Used in `before` and `after` args""" - cursor: String! - node: ProjectHistory! -} - -"""Labels that can be associated with projects.""" -type ProjectLabel implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """Children of the label.""" - children( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned project labels.""" - filter: ProjectLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectLabelConnection! - - """The label's color as a HEX string.""" - color: String! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the label.""" - creator: User - - """The label's description.""" - description: String - - """The unique identifier of the entity.""" - id: ID! - - """Whether the label is a group.""" - isGroup: Boolean! - - """The date when the label was last applied to an issue or project.""" - lastAppliedAt: DateTime - - """The label's name.""" - name: String! - organization: Organization! - - """The parent label.""" - parent: ProjectLabel - - """Projects associated with the label.""" - projects( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned projects.""" - filter: ProjectFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned projects.""" - sort: [ProjectSortInput!] - ): ProjectConnection! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""Project label filtering options.""" -input ProjectLabelCollectionFilter { - """Compound filters, all of which need to be matched by the label.""" - and: [ProjectLabelCollectionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the project labels creator must satisfy.""" - creator: NullableUserFilter - - """Filters that needs to be matched by all project labels.""" - every: ProjectLabelFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for whether the label is a group label.""" - isGroup: BooleanComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Comparator for the name.""" - name: StringComparator - - """Filter based on the existence of the relation.""" - null: Boolean - - """Compound filters, one of which need to be matched by the label.""" - or: [ProjectLabelCollectionFilter!] - - """Filters that the project label's parent label must satisfy.""" - parent: ProjectLabelFilter - - """Filters that needs to be matched by some project labels.""" - some: ProjectLabelCollectionFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type ProjectLabelConnection { - edges: [ProjectLabelEdge!]! - nodes: [ProjectLabel!]! - pageInfo: PageInfo! -} - -input ProjectLabelCreateInput { - """The color of the label.""" - color: String - - """The description of the label.""" - description: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """Whether the label is a group.""" - isGroup: Boolean - - """The name of the label.""" - name: String! - - """The identifier of the parent label.""" - parentId: String -} - -type ProjectLabelEdge { - """Used in `before` and `after` args""" - cursor: String! - node: ProjectLabel! -} - -"""Project label filtering options.""" -input ProjectLabelFilter { - """Compound filters, all of which need to be matched by the label.""" - and: [ProjectLabelFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the project labels creator must satisfy.""" - creator: NullableUserFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for whether the label is a group label.""" - isGroup: BooleanComparator - - """Comparator for the name.""" - name: StringComparator - - """Compound filters, one of which need to be matched by the label.""" - or: [ProjectLabelFilter!] - - """Filters that the project label's parent label must satisfy.""" - parent: ProjectLabelFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type ProjectLabelPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The label that was created or updated.""" - projectLabel: ProjectLabel! - - """Whether the operation was successful.""" - success: Boolean! -} - -input ProjectLabelUpdateInput { - """The color of the label.""" - color: String - - """The description of the label.""" - description: String - - """Whether the label is a group.""" - isGroup: Boolean - - """The name of the label.""" - name: String - - """The identifier of the parent label.""" - parentId: String -} - -"""Project lead sorting options.""" -input ProjectLeadSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Project manual order sorting options.""" -input ProjectManualSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A milestone for a project.""" -type ProjectMilestone implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """[Internal] The current progress of the project milestone.""" - currentProgress: JSONObject! - - """The project milestone's description in markdown format.""" - description: String - - """[Internal] The project milestone's description as YJS state.""" - descriptionState: String - - """The content of the project milestone description.""" - documentContent: DocumentContent - - """The unique identifier of the entity.""" - id: ID! - - """Issues associated with the project milestone.""" - issues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """The name of the project milestone.""" - name: String! - - """The progress % of the project milestone.""" - progress: Float! - - """[Internal] The progress history of the project milestone.""" - progressHistory: JSONObject! - - """The project of the milestone.""" - project: Project! - - """ - The order of the milestone in relation to other milestones within a project. - """ - sortOrder: Float! - - """The status of the project milestone.""" - status: ProjectMilestoneStatus! - - """The planned completion date of the milestone.""" - targetDate: TimelessDate - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""Milestone collection filtering options.""" -input ProjectMilestoneCollectionFilter { - """Compound filters, all of which need to be matched by the milestone.""" - and: [ProjectMilestoneCollectionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that needs to be matched by all milestones.""" - every: ProjectMilestoneFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Comparator for the project milestone name.""" - name: NullableStringComparator - - """Compound filters, one of which need to be matched by the milestone.""" - or: [ProjectMilestoneCollectionFilter!] - - """Filters that needs to be matched by some milestones.""" - some: ProjectMilestoneFilter - - """Comparator for the project milestone target date.""" - targetDate: NullableDateComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type ProjectMilestoneConnection { - edges: [ProjectMilestoneEdge!]! - nodes: [ProjectMilestone!]! - pageInfo: PageInfo! -} - -input ProjectMilestoneCreateInput { - """The description of the project milestone in markdown format.""" - description: String - - """ - [Internal] The description of the project milestone as a Prosemirror document. - """ - descriptionData: JSONObject - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The name of the project milestone.""" - name: String! - - """Related project for the project milestone.""" - projectId: String! - - """The sort order for the project milestone within a project.""" - sortOrder: Float - - """The planned target date of the project milestone.""" - targetDate: TimelessDate -} - -type ProjectMilestoneEdge { - """Used in `before` and `after` args""" - cursor: String! - node: ProjectMilestone! -} - -"""Project milestone filtering options.""" -input ProjectMilestoneFilter { - """ - Compound filters, all of which need to be matched by the project milestone. - """ - and: [ProjectMilestoneFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the project milestone name.""" - name: NullableStringComparator - - """ - Compound filters, one of which need to be matched by the project milestone. - """ - or: [ProjectMilestoneFilter!] - - """Comparator for the project milestone target date.""" - targetDate: NullableDateComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -input ProjectMilestoneMoveInput { - """ - Whether to add each milestone issue's team to the project. This is needed when there is a mismatch between a project's teams and the milestone's issues' teams. Either this or newIssueTeamId is required in that situation to resolve constraints. - """ - addIssueTeamToProject: Boolean - - """ - The team id to move the attached issues to. This is needed when there is a mismatch between a project's teams and the milestone's issues' teams. Either this or addIssueTeamToProject is required in that situation to resolve constraints. - """ - newIssueTeamId: String - - """The identifier of the project to move the milestone to.""" - projectId: String! - - """ - A list of issue id to team ids, used for undoing a previous milestone move where the specified issues were moved from the specified teams. - """ - undoIssueTeamIds: [ProjectMilestoneMoveIssueToTeamInput!] - - """ - A mapping of project id to a previous set of team ids, used for undoing a previous milestone move where the specified teams were added to the project. - """ - undoProjectTeamIds: ProjectMilestoneMoveProjectTeamsInput -} - -type ProjectMilestoneMoveIssueToTeam { - """ - The issue id in this relationship, you can use * as wildcard if all issues are being moved to the same team - """ - issueId: String! - - """The team id in this relationship""" - teamId: String! -} - -""" -[Internal] Used for ProjectMilestoneMoveInput to describe a mapping between an issue and its team. -""" -input ProjectMilestoneMoveIssueToTeamInput { - """ - The issue id in this relationship, you can use * as wildcard if all issues are being moved to the same team - """ - issueId: String! - - """The team id in this relationship""" - teamId: String! -} - -type ProjectMilestoneMovePayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """ - A snapshot of the issues that were moved to new teams, if the user selected to do it, containing an array of mappings between an issue and its previous team. Store on the client to use for undoing a previous milestone move. - """ - previousIssueTeamIds: [ProjectMilestoneMoveIssueToTeam!] - - """ - A snapshot of the project that had new teams added to it, if the user selected to do it, containing an array of mappings between a project and its previous teams. Store on the client to use for undoing a previous milestone move. - """ - previousProjectTeamIds: ProjectMilestoneMoveProjectTeams - - """The project milestone that was created or updated.""" - projectMilestone: ProjectMilestone! - - """Whether the operation was successful.""" - success: Boolean! -} - -type ProjectMilestoneMoveProjectTeams { - """The project id""" - projectId: String! - - """The team ids for the project""" - teamIds: [String!]! -} - -""" -[Internal] Used for ProjectMilestoneMoveInput to describe a snapshot of a project and its team ids -""" -input ProjectMilestoneMoveProjectTeamsInput { - """The project id""" - projectId: String! - - """The team ids for the project""" - teamIds: [String!]! -} - -type ProjectMilestonePayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The project milestone that was created or updated.""" - projectMilestone: ProjectMilestone! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""The status of a project milestone.""" -enum ProjectMilestoneStatus { - done - next - overdue - unstarted -} - -input ProjectMilestoneUpdateInput { - """The description of the project milestone in markdown format.""" - description: String - - """ - [Internal] The description of the project milestone as a Prosemirror document. - """ - descriptionData: JSONObject - - """The name of the project milestone.""" - name: String - - """Related project for the project milestone.""" - projectId: String - - """The sort order for the project milestone within a project.""" - sortOrder: Float - - """The planned target date of the project milestone.""" - targetDate: TimelessDate -} - -"""Project name sorting options.""" -input ProjectNameSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A project related notification.""" -type ProjectNotification implements Entity & Node & Notification { - """The user that caused the notification.""" - actor: User - - """[Internal] Notification actor initials if avatar is not available.""" - actorAvatarColor: String! - - """[Internal] Notification avatar URL.""" - actorAvatarUrl: String - - """[Internal] Notification actor initials if avatar is not available.""" - actorInitials: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The bot that caused the notification.""" - botActor: ActorBot - - """The category of the notification.""" - category: NotificationCategory! - - """The comment related to the notification.""" - comment: Comment - - """ - Related comment ID. Null if the notification is not related to a comment. - """ - commentId: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The document related to the notification.""" - document: Document - - """ - The time at when an email reminder for this notification was sent to the user. Null, if no email - reminder has been sent. - """ - emailedAt: DateTime - - """The external user that caused the notification.""" - externalUserActor: ExternalUser - - """ - [Internal] Notifications with the same grouping key will be grouped together in the UI. - """ - groupingKey: String! - - """ - [Internal] Priority of the notification with the same grouping key. Higher number means higher priority. If priority is the same, notifications should be sorted by `createdAt`. - """ - groupingPriority: Float! - - """The unique identifier of the entity.""" - id: ID! - - """[Internal] Inbox URL for the notification.""" - inboxUrl: String! - - """[Internal] If notification actor was Linear.""" - isLinearActor: Boolean! - - """[Internal] Issue's status type for issue notifications.""" - issueStatusType: String - - """ - The parent comment related to the notification, if a notification is a reply comment notification. - """ - parentComment: Comment - - """ - Related parent comment ID. Null if the notification is not related to a comment. - """ - parentCommentId: String - - """The project related to the notification.""" - project: Project! - - """Related project ID.""" - projectId: String! - - """Related project milestone ID.""" - projectMilestoneId: String - - """The project update related to the notification.""" - projectUpdate: ProjectUpdate - - """[Internal] Project update health for new updates.""" - projectUpdateHealth: String - - """Related project update ID.""" - projectUpdateId: String - - """Name of the reaction emoji related to the notification.""" - reactionEmoji: String - - """ - The time at when the user marked the notification as read. Null, if the the user hasn't read the notification - """ - readAt: DateTime - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime - - """[Internal] Notification subtitle.""" - subtitle: String! - - """[Internal] Notification title.""" - title: String! - - """Notification type.""" - type: String! - - """The time at which a notification was unsnoozed..""" - unsnoozedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """[Internal] URL to the target of the notification.""" - url: String! - - """The user that received the notification.""" - user: User! -} - -"""A project notification subscription.""" -type ProjectNotificationSubscription implements Entity & Node & NotificationSubscription { - """Whether the subscription is active or not.""" - active: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The type of view to which the notification subscription context is associated with. - """ - contextViewType: ContextViewType - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The contextual custom view associated with the notification subscription. - """ - customView: CustomView - - """The customer associated with the notification subscription.""" - customer: Customer - - """ - The contextual cycle view associated with the notification subscription. - """ - cycle: Cycle - - """The unique identifier of the entity.""" - id: ID! - - """ - The contextual initiative view associated with the notification subscription. - """ - initiative: Initiative - - """ - The contextual label view associated with the notification subscription. - """ - label: IssueLabel - - """The type of subscription.""" - notificationSubscriptionTypes: [String!]! - - """The project subscribed to.""" - project: Project! - - """The user that subscribed to receive notifications.""" - subscriber: User! - - """The team associated with the notification subscription.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user view associated with the notification subscription.""" - user: User - - """ - The type of user view to which the notification subscription context is associated with. - """ - userContextViewType: UserContextViewType -} - -type ProjectPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The project that was created or updated.""" - project: Project - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Project priority sorting options.""" -input ProjectPrioritySort { - """Whether to consider no priority as the highest or lowest priority""" - noPriorityFirst: Boolean = false - - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A relation between two projects.""" -type ProjectRelation implements Node { - """The type of anchor on the project end of the relation.""" - anchorType: String! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The project whose relationship is being described.""" - project: Project! - - """ - The milestone within the project whose relationship is being described. - """ - projectMilestone: ProjectMilestone - - """The type of anchor on the relatedProject end of the relation.""" - relatedAnchorType: String! - - """The related project.""" - relatedProject: Project! - - """ - The milestone within the related project whose relationship is being described. - """ - relatedProjectMilestone: ProjectMilestone - - """The relationship of the project with the related project.""" - type: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The last user who created or modified the relation.""" - user: User -} - -type ProjectRelationConnection { - edges: [ProjectRelationEdge!]! - nodes: [ProjectRelation!]! - pageInfo: PageInfo! -} - -input ProjectRelationCreateInput { - """The type of the anchor for the project.""" - anchorType: String! - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The identifier of the project that is related to another project.""" - projectId: String! - - """The identifier of the project milestone.""" - projectMilestoneId: String - - """The type of the anchor for the related project.""" - relatedAnchorType: String! - - """The identifier of the related project.""" - relatedProjectId: String! - - """The identifier of the related project milestone.""" - relatedProjectMilestoneId: String - - """The type of relation of the project to the related project.""" - type: String! -} - -type ProjectRelationEdge { - """Used in `before` and `after` args""" - cursor: String! - node: ProjectRelation! -} - -type ProjectRelationPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The project relation that was created or updated.""" - projectRelation: ProjectRelation! - - """Whether the operation was successful.""" - success: Boolean! -} - -input ProjectRelationUpdateInput { - """The type of the anchor for the project.""" - anchorType: String - - """The identifier of the project that is related to another project.""" - projectId: String - - """The identifier of the project milestone.""" - projectMilestoneId: String - - """The type of the anchor for the related project.""" - relatedAnchorType: String - - """The identifier of the related project.""" - relatedProjectId: String - - """The identifier of the related project milestone.""" - relatedProjectMilestoneId: String - - """The type of relation of the project to the related project.""" - type: String -} - -type ProjectSearchPayload { - """ - Archived entities matching the search term along with all their dependencies. - """ - archivePayload: ArchiveResponse! - edges: [ProjectSearchResultEdge!]! - nodes: [ProjectSearchResult!]! - pageInfo: PageInfo! - - """Total number of results for query without filters applied.""" - totalCount: Float! -} - -type ProjectSearchResult implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The time at which the project was automatically archived by the auto pruning process. - """ - autoArchivedAt: DateTime - - """The time at which the project was moved into canceled state.""" - canceledAt: DateTime - - """The project's color.""" - color: String! - - """Comments associated with the project overview.""" - comments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned comments.""" - filter: CommentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CommentConnection! - - """The time at which the project was moved into completed state.""" - completedAt: DateTime - - """The number of completed issues in the project after each week.""" - completedIssueCountHistory: [Float!]! - - """The number of completed estimation points after each week.""" - completedScopeHistory: [Float!]! - - """The project's content in markdown format.""" - content: String - - """[Internal] The project's content as YJS state.""" - contentState: String - - """The project was created based on this issue.""" - convertedFromIssue: Issue - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the project.""" - creator: User - - """[INTERNAL] The current progress of the project.""" - currentProgress: JSONObject! - - """The project's description.""" - description: String! - - """The content of the project description.""" - documentContent: DocumentContent - - """Documents associated with the project.""" - documents( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned documents.""" - filter: DocumentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): DocumentConnection! - - """External links associated with the project.""" - externalLinks( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): EntityExternalLinkConnection! - - """[Internal] Facets associated with the project.""" - facets: [Facet!]! - - """The user's favorite associated with this project.""" - favorite: Favorite - - """The resolution of the reminder frequency.""" - frequencyResolution: FrequencyResolutionType! - - """The health of the project.""" - health: ProjectUpdateHealthType - - """The time at which the project health was updated.""" - healthUpdatedAt: DateTime - - """History entries associated with the project.""" - history( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectHistoryConnection! - - """The icon of the project.""" - icon: String - - """The unique identifier of the entity.""" - id: ID! - - """The number of in progress estimation points after each week.""" - inProgressScopeHistory: [Float!]! - - """Initiatives that this project belongs to.""" - initiatives( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): InitiativeConnection! - - """Settings for all integrations associated with that project.""" - integrationsSettings: IntegrationsSettings - - """Inverse relations associated with this project.""" - inverseRelations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectRelationConnection! - - """The total number of issues in the project after each week.""" - issueCountHistory: [Float!]! - - """Issues associated with the project.""" - issues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """Id of the labels associated with this project.""" - labelIds: [String!]! - - """Labels associated with this project.""" - labels( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned project labels.""" - filter: ProjectLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectLabelConnection! - - """The last template that was applied to this project.""" - lastAppliedTemplate: Template - - """The last project update posted for this project.""" - lastUpdate: ProjectUpdate - - """The project lead.""" - lead: User - - """Users that are members of the project.""" - members( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned users.""" - filter: UserFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Should query return disabled/suspended users (default: false).""" - includeDisabled: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): UserConnection! - - """Metadata related to search result.""" - metadata: JSONObject! - - """The project's name.""" - name: String! - - """Customer needs associated with the project.""" - needs( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned needs.""" - filter: CustomerNeedFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CustomerNeedConnection! - - """ - The priority of the project. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: Int! - - """The priority of the project as a label.""" - priorityLabel: String! - - """ - The sort order for the project within the organization, when ordered by priority. - """ - prioritySortOrder: Float! - - """ - The overall progress of the project. This is the (completed estimate points + 0.25 * in progress estimate points) / total estimate points. - """ - progress: Float! - - """[INTERNAL] The progress history of the project.""" - progressHistory: JSONObject! - - """Milestones associated with the project.""" - projectMilestones( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned milestones.""" - filter: ProjectMilestoneFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectMilestoneConnection! - - """The time until which project update reminders are paused.""" - projectUpdateRemindersPausedUntilAt: DateTime - - """Project updates associated with the project.""" - projectUpdates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectUpdateConnection! - - """Relations associated with this project.""" - relations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectRelationConnection! - - """The overall scope (total estimate points) of the project.""" - scope: Float! - - """The total number of estimation points after each week.""" - scopeHistory: [Float!]! - - """Whether to send new issue comment notifications to Slack.""" - slackIssueComments: Boolean! @deprecated(reason: "No longer in use") - - """Whether to send new issue status updates to Slack.""" - slackIssueStatuses: Boolean! @deprecated(reason: "No longer is use") - - """Whether to send new issue notifications to Slack.""" - slackNewIssue: Boolean! @deprecated(reason: "No longer in use") - - """The project's unique URL slug.""" - slugId: String! - - """The sort order for the project within the organization.""" - sortOrder: Float! - - """The estimated start date of the project.""" - startDate: TimelessDate - - """The resolution of the project's start date.""" - startDateResolution: DateResolutionType - - """The time at which the project was moved into started state.""" - startedAt: DateTime - - """[DEPRECATED] The type of the state.""" - state: String! @deprecated(reason: "Use project.status instead") - - """The status that the project is associated with.""" - status: ProjectStatus! - - """The estimated completion date of the project.""" - targetDate: TimelessDate - - """The resolution of the project's estimated completion date.""" - targetDateResolution: DateResolutionType - - """Teams associated with this project.""" - teams( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned teams.""" - filter: TeamFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TeamConnection! - - """A flag that indicates whether the project is in the trash bin.""" - trashed: Boolean - - """ - The frequency at which to prompt for updates. When not set, reminders are inherited from workspace. - """ - updateReminderFrequency: Float - - """ - The n-weekly frequency at which to prompt for updates. When not set, reminders are inherited from workspace. - """ - updateReminderFrequencyInWeeks: Float - - """The day at which to prompt for updates.""" - updateRemindersDay: Day - - """The hour at which to prompt for updates.""" - updateRemindersHour: Float - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Project URL.""" - url: String! -} - -type ProjectSearchResultEdge { - """Used in `before` and `after` args""" - cursor: String! - node: ProjectSearchResult! -} - -"""Issue project sorting options.""" -input ProjectSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Project sorting options.""" -input ProjectSortInput { - """Sort by project creation date""" - createdAt: ProjectCreatedAtSort - - """Sort by project health status.""" - health: ProjectHealthSort - - """Sort by project lead name.""" - lead: ProjectLeadSort - - """Sort by manual order""" - manual: ProjectManualSort - - """Sort by project name""" - name: ProjectNameSort - - """Sort by project priority""" - priority: ProjectPrioritySort - - """Sort by project start date""" - startDate: StartDateSort - - """Sort by project status""" - status: ProjectStatusSort - - """Sort by project target date""" - targetDate: TargetDateSort - - """Sort by project update date""" - updatedAt: ProjectUpdatedAtSort -} - -"""A project status.""" -type ProjectStatus implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The UI color of the status as a HEX string.""" - color: String! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """Description of the status.""" - description: String - - """The unique identifier of the entity.""" - id: ID! - - """Whether or not a project can be in this status indefinitely.""" - indefinite: Boolean! - - """The name of the status.""" - name: String! - - """The position of the status in the workspace's project flow.""" - position: Float! - - """The type of the project status.""" - type: ProjectStatusType! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""A generic payload return from entity archive mutations.""" -type ProjectStatusArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: ProjectStatus - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type ProjectStatusConnection { - edges: [ProjectStatusEdge!]! - nodes: [ProjectStatus!]! - pageInfo: PageInfo! -} - -type ProjectStatusCountPayload { - """ - Total number of projects using this project status that are not visible to the user because they are in an archived team. - """ - archivedTeamCount: Float! - - """Total number of projects using this project status.""" - count: Float! - - """ - Total number of projects using this project status that are not visible to the user because they are in a private team. - """ - privateCount: Float! -} - -input ProjectStatusCreateInput { - """The UI color of the status as a HEX string.""" - color: String! - - """Description of the status.""" - description: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """Whether or not a project can be in this status indefinitely.""" - indefinite: Boolean = false - - """The name of the status.""" - name: String! - - """The position of the status in the workspace's project flow.""" - position: Float! - - """The type of the project status.""" - type: ProjectStatusType! -} - -type ProjectStatusEdge { - """Used in `before` and `after` args""" - cursor: String! - node: ProjectStatus! -} - -"""Project status filtering options.""" -input ProjectStatusFilter { - """ - Compound filters, all of which need to be matched by the project status. - """ - and: [ProjectStatusFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the project status description.""" - description: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the project status name.""" - name: StringComparator - - """ - Compound filters, one of which needs to be matched by the project status. - """ - or: [ProjectStatusFilter!] - - """Comparator for the project status position.""" - position: NumberComparator - - """Filters that the project status projects must satisfy.""" - projects: ProjectCollectionFilter - - """Comparator for the project status type.""" - type: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type ProjectStatusPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The project status that was created or updated.""" - status: ProjectStatus! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Project status sorting options.""" -input ProjectStatusSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A type of project status.""" -enum ProjectStatusType { - backlog - canceled - completed - paused - planned - started -} - -input ProjectStatusUpdateInput { - """The UI color of the status as a HEX string.""" - color: String - - """Description of the status.""" - description: String - - """Whether or not a project can be in this status indefinitely.""" - indefinite: Boolean - - """The name of the status.""" - name: String - - """The position of the status in the workspace's project flow.""" - position: Float - - """The type of the project status.""" - type: ProjectStatusType -} - -"""Different tabs available inside a project.""" -enum ProjectTab { - customers - documents - issues - updates -} - -"""An update associated with a project.""" -type ProjectUpdate implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The update content in markdown format.""" - body: String! - - """[Internal] The content of the update as a Prosemirror document.""" - bodyData: String! - - """Comments associated with the project update.""" - comments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned comments.""" - filter: CommentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CommentConnection! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The diff between the current update and the previous one.""" - diff: JSONObject - - """ - The diff between the current update and the previous one, formatted as markdown. - """ - diffMarkdown: String - - """The time the update was edited.""" - editedAt: DateTime - - """The health of the project at the time of the update.""" - health: ProjectUpdateHealthType! - - """The unique identifier of the entity.""" - id: ID! - - """ - [Internal] Serialized JSON representing current state of the project properties when posting the project update. - """ - infoSnapshot: JSONObject - - """Whether project update diff should be hidden.""" - isDiffHidden: Boolean! - - """Whether the project update is stale.""" - isStale: Boolean! - - """The project that the update is associated with.""" - project: Project! - - """Emoji reaction summary, grouped by emoji type.""" - reactionData: JSONObject! - - """Reactions associated with the project update.""" - reactions: [Reaction!]! - - """The update's unique URL slug.""" - slugId: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The URL to the project update.""" - url: String! - - """The user who wrote the update.""" - user: User! -} - -"""A generic payload return from entity archive mutations.""" -type ProjectUpdateArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: ProjectUpdate - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type ProjectUpdateConnection { - edges: [ProjectUpdateEdge!]! - nodes: [ProjectUpdate!]! - pageInfo: PageInfo! -} - -input ProjectUpdateCreateInput { - """The content of the project update in markdown format.""" - body: String - - """ - [Internal] The content of the project update as a Prosemirror document. - """ - bodyData: JSON - - """The health of the project at the time of the update.""" - health: ProjectUpdateHealthType - - """The identifier. If none is provided, the backend will generate one.""" - id: String - - """ - Whether the diff between the current update and the previous one should be hidden. - """ - isDiffHidden: Boolean - - """The project to associate the project update with.""" - projectId: String! -} - -type ProjectUpdateEdge { - """Used in `before` and `after` args""" - cursor: String! - node: ProjectUpdate! -} - -"""Options for filtering project updates.""" -input ProjectUpdateFilter { - """ - Compound filters, all of which need to be matched by the ProjectUpdate. - """ - and: [ProjectUpdateFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the identifier.""" - id: IDComparator - - """ - Compound filters, one of which need to be matched by the ProjectUpdate. - """ - or: [ProjectUpdateFilter!] - - """Filters that the project update project must satisfy.""" - project: ProjectFilter - - """Filters that the project updates reactions must satisfy.""" - reactions: ReactionCollectionFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator - - """Filters that the project update creator must satisfy.""" - user: UserFilter -} - -"""The health type when the project update is created.""" -enum ProjectUpdateHealthType { - atRisk - offTrack - onTrack -} - -input ProjectUpdateInput { - """The date when the project was canceled.""" - canceledAt: DateTime - - """The color of the project.""" - color: String - - """The date when the project was completed.""" - completedAt: DateTime - - """The project content as markdown.""" - content: String - - """The ID of the issue from which that project is created.""" - convertedFromIssueId: String - - """The description for the project.""" - description: String - - """The frequency resolution.""" - frequencyResolution: FrequencyResolutionType - - """The icon of the project.""" - icon: String - - """The identifiers of the project labels associated with this project.""" - labelIds: [String!] - - """The ID of the last template applied to the project.""" - lastAppliedTemplateId: String - - """The identifier of the project lead.""" - leadId: String - - """The identifiers of the members of this project.""" - memberIds: [String!] - - """The name of the project.""" - name: String - - """ - The priority of the project. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. - """ - priority: Int - - """ - The sort order for the project within shared views, when ordered by priority. - """ - prioritySortOrder: Float - - """The time until which project update reminders are paused.""" - projectUpdateRemindersPausedUntilAt: DateTime - - """Whether to send new issue comment notifications to Slack.""" - slackIssueComments: Boolean - - """Whether to send issue status update notifications to Slack.""" - slackIssueStatuses: Boolean - - """Whether to send new issue notifications to Slack.""" - slackNewIssue: Boolean - - """The sort order for the project in shared views.""" - sortOrder: Float - - """The planned start date of the project.""" - startDate: TimelessDate - - """The resolution of the project's start date.""" - startDateResolution: DateResolutionType - - """[DEPRECATED] The state of the project.""" - state: String @deprecated(reason: "Use statusId instead") - - """The ID of the project status.""" - statusId: String - - """The planned target date of the project.""" - targetDate: TimelessDate - - """The resolution of the project's estimated completion date.""" - targetDateResolution: DateResolutionType - - """The identifiers of the teams this project is associated with.""" - teamIds: [String!] - - """Whether the project has been trashed.""" - trashed: Boolean - - """ - The frequency at which to prompt for updates. When not set, reminders are inherited from workspace. - """ - updateReminderFrequency: Float - - """ - The n-weekly frequency at which to prompt for updates. When not set, reminders are inherited from workspace. - """ - updateReminderFrequencyInWeeks: Float - - """The day at which to prompt for updates.""" - updateRemindersDay: Day - - """The hour at which to prompt for updates.""" - updateRemindersHour: Int -} - -type ProjectUpdatePayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The project update that was created or updated.""" - projectUpdate: ProjectUpdate! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""The frequency at which to send project update reminders.""" -enum ProjectUpdateReminderFrequency { - month - never - twoWeeks - week -} - -type ProjectUpdateReminderPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -input ProjectUpdateUpdateInput { - """The content of the project update in markdown format.""" - body: String - - """The content of the project update as a Prosemirror document.""" - bodyData: JSON - - """The health of the project at the time of the update.""" - health: ProjectUpdateHealthType - - """ - Whether the diff between the current update and the previous one should be hidden. - """ - isDiffHidden: Boolean -} - -"""Project update date sorting options.""" -input ProjectUpdatedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -""" -Collection filtering options for filtering projects by project updates. -""" -input ProjectUpdatesCollectionFilter { - """ - Compound filters, all of which need to be matched by the project update. - """ - and: [ProjectUpdatesCollectionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that needs to be matched by all updates.""" - every: ProjectUpdatesFilter - - """Comparator for the project update health.""" - health: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Compound filters, one of which need to be matched by the update.""" - or: [ProjectUpdatesCollectionFilter!] - - """Filters that needs to be matched by some updates.""" - some: ProjectUpdatesFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Options for filtering projects by project updates.""" -input ProjectUpdatesFilter { - """ - Compound filters, all of which need to be matched by the project updates. - """ - and: [ProjectUpdatesFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the project update health.""" - health: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """ - Compound filters, one of which need to be matched by the project updates. - """ - or: [ProjectUpdatesFilter!] - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""[Internal] A pull request in a version control system.""" -type PullRequest implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """[ALPHA] The commits associated with the pull request.""" - commits: [PullRequestCommit!]! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """Merge settings for this pull request.""" - mergeSettings: PullRequestMergeSettings - - """The number of the pull request in the version control system.""" - number: Float! - - """The source branch of the pull request.""" - sourceBranch: String! - - """The status of the pull request.""" - status: PullRequestStatus! - - """The target branch of the pull request.""" - targetBranch: String! - - """The title of the pull request.""" - title: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The URL of the pull request in the version control system.""" - url: String! -} - -"""[ALPHA] A pull request commit.""" -type PullRequestCommit { - """Number of additions in this commit.""" - additions: Float! - - """External user IDs for commit authors (includes co-authors).""" - authorExternalUserIds: [String!]! - - """Linear user IDs for commit authors (includes co-authors).""" - authorUserIds: [String!]! - - """The number of changed files if available.""" - changedFiles: Float - - """The timestamp when the commit was committed (ISO 8601 string).""" - committedAt: String! - - """Number of deletions in this commit.""" - deletions: Float! - - """The full commit message.""" - message: String! - - """The Git commit SHA.""" - sha: String! -} - -"""The method used to merge a pull request.""" -enum PullRequestMergeMethod { - MERGE - REBASE - SQUASH -} - -"""[Internal] Merge settings for a pull request""" -type PullRequestMergeSettings { - """Whether auto-merge is allowed for the PR's repository.""" - autoMergeAllowed: Boolean! - - """Whether the branch will be deleted when the pull request is merged.""" - deleteBranchOnMerge: Boolean! - - """Whether merge queue is enabled for this repository.""" - isMergeQueueEnabled: Boolean! - - """Whether merge commits are allowed for pull requests PR's repository.""" - mergeCommitAllowed: Boolean! - - """The method used to merge a pull request.""" - mergeQueueMergeMethod: PullRequestMergeMethod - - """Whether rebase merge is allowed for pull requests PR's repository.""" - rebaseMergeAllowed: Boolean! - - """Whether squash merge is allowed for this pull request's repository.""" - squashMergeAllowed: Boolean! -} - -"""A pull request related notification.""" -type PullRequestNotification implements Entity & Node & Notification { - """The user that caused the notification.""" - actor: User - - """[Internal] Notification actor initials if avatar is not available.""" - actorAvatarColor: String! - - """[Internal] Notification avatar URL.""" - actorAvatarUrl: String - - """[Internal] Notification actor initials if avatar is not available.""" - actorInitials: String - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The bot that caused the notification.""" - botActor: ActorBot - - """The category of the notification.""" - category: NotificationCategory! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The time at when an email reminder for this notification was sent to the user. Null, if no email - reminder has been sent. - """ - emailedAt: DateTime - - """The external user that caused the notification.""" - externalUserActor: ExternalUser - - """ - [Internal] Notifications with the same grouping key will be grouped together in the UI. - """ - groupingKey: String! - - """ - [Internal] Priority of the notification with the same grouping key. Higher number means higher priority. If priority is the same, notifications should be sorted by `createdAt`. - """ - groupingPriority: Float! - - """The unique identifier of the entity.""" - id: ID! - - """[Internal] Inbox URL for the notification.""" - inboxUrl: String! - - """[Internal] If notification actor was Linear.""" - isLinearActor: Boolean! - - """[Internal] Issue's status type for issue notifications.""" - issueStatusType: String - - """[Internal] Project update health for new updates.""" - projectUpdateHealth: String - - """The pull request related to the notification.""" - pullRequest: PullRequest! - - """ - Related pull request comment ID. Null if the notification is not related to a pull request comment. - """ - pullRequestCommentId: String - - """Related pull request.""" - pullRequestId: String! - - """ - The time at when the user marked the notification as read. Null, if the the user hasn't read the notification - """ - readAt: DateTime - - """ - The time until a notification will be snoozed. After that it will appear in the inbox again. - """ - snoozedUntilAt: DateTime - - """[Internal] Notification subtitle.""" - subtitle: String! - - """[Internal] Notification title.""" - title: String! - - """Notification type.""" - type: String! - - """The time at which a notification was unsnoozed..""" - unsnoozedAt: DateTime - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """[Internal] URL to the target of the notification.""" - url: String! - - """The user that received the notification.""" - user: User! -} - -enum PullRequestReviewTool { - graphite - source -} - -"""The status of a pull request.""" -enum PullRequestStatus { - approved - closed - draft - inReview - merged - open -} - -"""A user's web or mobile push notification subscription.""" -type PushSubscription implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -input PushSubscriptionCreateInput { - """The data of the subscription in stringified JSON format.""" - data: String! - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """ - Whether this is a subscription payload for Google Cloud Messaging or Apple Push Notification service. - """ - type: PushSubscriptionType = web - - """The user identifier of the subscription.""" - userId: String @deprecated(reason: "Not needed anymore.") -} - -type PushSubscriptionPayload { - """The push subscription that was created or updated.""" - entity: PushSubscription! - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type PushSubscriptionTestPayload { - """Whether the operation was successful.""" - success: Boolean! -} - -"""The different push subscription types.""" -enum PushSubscriptionType { - apple - appleDevelopment - firebase - web -} - -type Query { - """ - All teams you the user can administrate. Administrable teams are teams whose settings the user can change, but to whose issues the user doesn't necessarily have access to. - """ - administrableTeams( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned teams.""" - filter: TeamFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TeamConnection! - - """All agent activities.""" - agentActivities( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned agent activities.""" - filter: AgentActivityFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): AgentActivityConnection! - - """A specific agent activity.""" - agentActivity( - """The identifier of the agent activity to retrieve.""" - id: String! - ): AgentActivity! - - """A specific agent session.""" - agentSession( - """The identifier of the agent session to retrieve.""" - id: String! - ): AgentSession! - - """All agent sessions.""" - agentSessions( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): AgentSessionConnection! - - """All API keys for the user.""" - apiKeys( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ApiKeyConnection! - - """Get basic information for an application.""" - applicationInfo( - """The client ID of the application.""" - clientId: String! - ): Application! - - """[INTERNAL] Get basic information for a list of applications.""" - applicationInfoByIds( - """The IDs of the applications.""" - ids: [String!]! - ): [Application!]! - - """ - Get information for an application and whether a user has approved it for the given scopes. - """ - applicationWithAuthorization( - """Actor mode used for the authorization.""" - actor: String = "user" - - """The client ID of the application.""" - clientId: String! - - """Redirect URI for the application.""" - redirectUri: String - - """Scopes being requested by the application.""" - scope: [String!]! - ): UserAuthorizedApplication! - - """[Internal] All archived teams of the organization.""" - archivedTeams: [Team!]! - - "\nOne specific issue attachment.\n[Deprecated] 'url' can no longer be used as the 'id' parameter. Use 'attachmentsForUrl' instead" - attachment(id: String!): Attachment! - - "\nQuery an issue by its associated attachment, and its id.\n" - attachmentIssue( - """ - `id` of the attachment for which you'll want to get the issue for. [Deprecated] `url` as the `id` parameter. - """ - id: String! - ): Issue! @deprecated(reason: "Will be removed in near future, please use `attachmentsForURL` to get attachments and their issues instead.") - - """ - [Internal] Get a list of all unique attachment sources in the workspace. - """ - attachmentSources( - """ - (optional) if provided will only return attachment sources for the given team. - """ - teamId: String - ): AttachmentSourcesPayload! - - """ - All issue attachments. - - To get attachments for a given URL, use `attachmentsForURL` query. - """ - attachments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned attachments.""" - filter: AttachmentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): AttachmentConnection! - - """Returns issue attachments for a given `url`.""" - attachmentsForURL( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """The attachment URL.""" - url: String! - ): AttachmentConnection! - - """All audit log entries.""" - auditEntries( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned audit entries.""" - filter: AuditEntryFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): AuditEntryConnection! - - """List of audit entry types.""" - auditEntryTypes: [AuditEntryType!]! - - """User's active sessions.""" - authenticationSessions: [AuthenticationSessionResponse!]! - - """[INTERNAL] Get all authorized applications for a user.""" - authorizedApplications: [AuthorizedApplication!]! - - """Fetch users belonging to this user account.""" - availableUsers: AuthResolverResponse! - - """A specific comment.""" - comment( - """The hash of the comment to retrieve.""" - hash: String - - """The identifier of the comment to retrieve.""" - id: String - - """[Deprecated] The issue for which to find the comment.""" - issueId: String @deprecated(reason: "Not in use anymore, please use `id` or `hash` directly.") - ): Comment! - - """All comments.""" - comments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned comments.""" - filter: CommentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CommentConnection! - - """One specific custom view.""" - customView(id: String!): CustomView! - - """[INTERNAL] Suggests metadata for a view based on it's filters.""" - customViewDetailsSuggestion(filter: JSONObject!, modelName: String): CustomViewSuggestionPayload! - - """ - Whether a custom view has other subscribers than the current user in the organization. - """ - customViewHasSubscribers( - """The identifier of the custom view.""" - id: String! - ): CustomViewHasSubscribersPayload! - - """Custom views for the user.""" - customViews( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned custom views.""" - filter: CustomViewFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned custom views.""" - sort: [CustomViewSortInput!] - ): CustomViewConnection! - - """One specific customer.""" - customer(id: String!): Customer! - - """One specific customer need""" - customerNeed( - """The hash of the need to retrieve.""" - hash: String - - """The identifier of the need to retrieve.""" - id: String - ): CustomerNeed! - - """All customer needs.""" - customerNeeds( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned customers needs.""" - filter: CustomerNeedFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CustomerNeedConnection! - - """One specific customer status.""" - customerStatus(id: String!): CustomerStatus! - - """All customer statuses.""" - customerStatuses( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CustomerStatusConnection! - - """One specific customer tier.""" - customerTier(id: String!): CustomerTier! - - """All customer tiers.""" - customerTiers( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CustomerTierConnection! - - """All customers.""" - customers( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned customers.""" - filter: CustomerFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """Sort returned customers.""" - sorts: [CustomerSortInput!] - ): CustomerConnection! - - """One specific cycle.""" - cycle(id: String!): Cycle! - - """All cycles.""" - cycles( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned users.""" - filter: CycleFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CycleConnection! - - """One specific document.""" - document(id: String!): Document! - - """A collection of document content history entries.""" - documentContentHistory(id: String!): DocumentContentHistoryPayload! - - """All documents in the workspace.""" - documents( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned documents.""" - filter: DocumentFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): DocumentConnection! - - """One specific email intake address.""" - emailIntakeAddress(id: String!): EmailIntakeAddress! - - """A specific emoji.""" - emoji( - """The identifier or the name of the emoji to retrieve.""" - id: String! - ): Emoji! - - """All custom emojis.""" - emojis( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): EmojiConnection! - - """One specific entity link.""" - entityExternalLink(id: String!): EntityExternalLink! - - """One specific external user.""" - externalUser( - """The identifier of the external user to retrieve.""" - id: String! - ): ExternalUser! - - """All external users for the organization.""" - externalUsers( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ExternalUserConnection! - - """ - [INTERNAL] Webhook failure events for webhooks that belong to an OAuth application. (last 50) - """ - failuresForOauthWebhooks( - """The identifier of the OAuth client to retrieve failures for.""" - oauthClientId: String! - ): [WebhookFailureEvent!]! - - """One specific favorite.""" - favorite(id: String!): Favorite! - - """The user's favorites.""" - favorites( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): FavoriteConnection! - - """ - [Internal] Fetch an arbitrary set of data using natural language query. Be specific about what you want including properties for each entity, sort order, filters, limit and properties. - """ - fetchData( - """ - Natural language query describing what data to fetch. - - Examples: - - "All issues for the project with id 12345678-1234-1234-1234-123456789abc including comments" - - "The latest project update for each project that's a part of the initiative with id 12345678-1234-1234-1234-123456789abc, including it's sub-initiatives" - """ - query: String! - ): FetchDataPayload! - - """One specific initiative.""" - initiative(id: String!): Initiative! - - """One specific initiative relation.""" - initiativeRelation(id: String!): ProjectRelation! - - """All initiative relationships.""" - initiativeRelations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): InitiativeRelationConnection! - - """One specific initiativeToProject.""" - initiativeToProject(id: String!): InitiativeToProject! - - """returns a list of initiative to project entities.""" - initiativeToProjects( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): InitiativeToProjectConnection! - - """A specific initiative update.""" - initiativeUpdate( - """The identifier of the initiative update to retrieve.""" - id: String! - ): InitiativeUpdate! - - """All InitiativeUpdates.""" - initiativeUpdates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned initiative updates.""" - filter: InitiativeUpdateFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): InitiativeUpdateConnection! - - """All initiatives in the workspace.""" - initiatives( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned initiatives.""" - filter: InitiativeFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned initiatives.""" - sort: [InitiativeSortInput!] - ): InitiativeConnection! - - """One specific integration.""" - integration(id: String!): Integration! - - """Checks if the integration has all required scopes.""" - integrationHasScopes( - """The integration ID.""" - integrationId: String! - - """Required scopes.""" - scopes: [String!]! - ): IntegrationHasScopesPayload! - - """One specific integrationTemplate.""" - integrationTemplate(id: String!): IntegrationTemplate! - - """Template and integration connections.""" - integrationTemplates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IntegrationTemplateConnection! - - """All integrations.""" - integrations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IntegrationConnection! - - """One specific set of settings.""" - integrationsSettings(id: String!): IntegrationsSettings! - - """One specific issue.""" - issue(id: String!): Issue! - - """Find issues that are related to a given Figma file key.""" - issueFigmaFileKeySearch( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """The Figma file key.""" - fileKey: String! - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """Suggests filters for an issue view based on a text prompt.""" - issueFilterSuggestion( - """The ID of the project if filtering a project view""" - projectId: String - prompt: String! - ): IssueFilterSuggestionPayload! - - """Checks a CSV file validity against a specific import service.""" - issueImportCheckCSV( - """CSV storage url.""" - csvUrl: String! - - """The service the CSV containing data from.""" - service: String! - ): IssueImportCheckPayload! - - """ - Checks whether it will be possible to setup sync for this project or repository at the end of import - """ - issueImportCheckSync( - """The ID of the issue import for which to check sync eligibility""" - issueImportId: String! - ): IssueImportSyncCheckPayload! - - """ - Checks whether a custom JQL query is valid and can be used to filter issues of a Jira import - """ - issueImportJqlCheck( - """Jira user account email.""" - jiraEmail: String! - - """Jira installation or cloud hostname.""" - jiraHostname: String! - - """Jira project key to use as the base filter of the query.""" - jiraProject: String! - - """Jira personal access token to access Jira REST API.""" - jiraToken: String! - - """The JQL query to validate.""" - jql: String! - ): IssueImportJqlCheckPayload! - - """One specific label.""" - issueLabel(id: String!): IssueLabel! - - """All issue labels.""" - issueLabels( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issue labels.""" - filter: IssueLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueLabelConnection! - - """Issue priority values and corresponding labels.""" - issuePriorityValues: [IssuePriorityValue!]! - - """One specific issue relation.""" - issueRelation(id: String!): IssueRelation! - - """All issue relationships.""" - issueRelations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueRelationConnection! - - """ - [DEPRECATED] Search issues. This endpoint is deprecated and will be removed in the future – use `searchIssues` instead. - """ - issueSearch( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[Deprecated] Search string to look for.""" - query: String - ): IssueConnection! - - """Suggests issue title based on a customer request.""" - issueTitleSuggestionFromCustomerRequest(request: String!): IssueTitleSuggestionFromCustomerRequestPayload! - - """Find issue based on the VCS branch name.""" - issueVcsBranchSearch( - """The VCS branch name to search for.""" - branchName: String! - ): Issue - - """All issues.""" - issues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned issues.""" - sort: [IssueSortInput!] - ): IssueConnection! - - """One specific notification.""" - notification(id: String!): Notification! - - """One specific notification subscription.""" - notificationSubscription(id: String!): NotificationSubscription! - - """The user's notification subscriptions.""" - notificationSubscriptions( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): NotificationSubscriptionConnection! - - """All notifications.""" - notifications( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filters returned notifications.""" - filter: NotificationFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): NotificationConnection! - - """[Internal] A number of unread notifications.""" - notificationsUnreadCount: Int! - - """The user's organization.""" - organization: Organization! - - """[INTERNAL] Checks whether the domain can be claimed.""" - organizationDomainClaimRequest( - """The ID of the organization domain to claim.""" - id: String! - ): OrganizationDomainClaimPayload! - - """Does the organization exist.""" - organizationExists(urlKey: String!): OrganizationExistsPayload! - - """One specific organization invite.""" - organizationInvite(id: String!): OrganizationInvite! - - """One specific organization invite.""" - organizationInviteDetails(id: String!): OrganizationInviteDetailsPayload! - - """All invites for the organization.""" - organizationInvites( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): OrganizationInviteConnection! - - """[INTERNAL] Get organization metadata by urlKey or organization id.""" - organizationMeta(urlKey: String!): OrganizationMeta - - """One specific project.""" - project(id: String!): Project! - - """Suggests filters for a project view based on a text prompt.""" - projectFilterSuggestion(prompt: String!): ProjectFilterSuggestionPayload! - - """One specific label.""" - projectLabel(id: String!): ProjectLabel! - - """All project labels.""" - projectLabels( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned project labels.""" - filter: ProjectLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectLabelConnection! - - """One specific project milestone.""" - projectMilestone(id: String!): ProjectMilestone! - - """All milestones for the project.""" - projectMilestones( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned project milestones.""" - filter: ProjectMilestoneFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectMilestoneConnection! - - """One specific project relation.""" - projectRelation(id: String!): ProjectRelation! - - """All project relationships.""" - projectRelations( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectRelationConnection! - - """One specific project status.""" - projectStatus(id: String!): ProjectStatus! - - """ - [INTERNAL] Count of projects using this project status across the organization. - """ - projectStatusProjectCount( - """The identifier of the project status to find the project count for.""" - id: String! - ): ProjectStatusCountPayload! - - """All project statuses.""" - projectStatuses( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectStatusConnection! - - """A specific project update.""" - projectUpdate( - """The identifier of the project update to retrieve.""" - id: String! - ): ProjectUpdate! - - """All project updates.""" - projectUpdates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned project updates.""" - filter: ProjectUpdateFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectUpdateConnection! - - """All projects.""" - projects( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned projects.""" - filter: ProjectFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned projects.""" - sort: [ProjectSortInput!] - ): ProjectConnection! - - """Sends a test push message.""" - pushSubscriptionTest( - """The send strategy to use.""" - sendStrategy: SendStrategy = push - - """Whether to send to mobile devices.""" - targetMobile: Boolean = false - ): PushSubscriptionTestPayload! - - """The status of the rate limiter.""" - rateLimitStatus: RateLimitPayload! - - """One specific roadmap.""" - roadmap(id: String!): Roadmap! @deprecated(reason: "Roadmaps are deprecated, use initiatives instead.") - - """One specific roadmapToProject.""" - roadmapToProject(id: String!): RoadmapToProject! @deprecated(reason: "RoadmapToProject is deprecated, use InitiativeToProject instead.") - roadmapToProjects( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): RoadmapToProjectConnection! @deprecated(reason: "RoadmapToProject is deprecated, use InitiativeToProject instead.") - - """All roadmaps in the workspace.""" - roadmaps( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): RoadmapConnection! @deprecated(reason: "Roadmaps are deprecated, use initiatives instead.") - - """Search documents.""" - searchDocuments( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Should associated comments be searched (default: false).""" - includeComments: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """Size of search snippet to return (default: 100)""" - snippetSize: Float @deprecated(reason: "No longer supported.") - - """UUID of a team to use as a boost.""" - teamId: String - - """Search string to look for.""" - term: String! - ): DocumentSearchPayload! - - """Search issues.""" - searchIssues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Should associated comments be searched (default: false).""" - includeComments: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """Size of search snippet to return (default: 100)""" - snippetSize: Float @deprecated(reason: "No longer supported.") - - """UUID of a team to use as a boost.""" - teamId: String - - """Search string to look for.""" - term: String! - ): IssueSearchPayload! - - """Search projects.""" - searchProjects( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Should associated comments be searched (default: false).""" - includeComments: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """Size of search snippet to return (default: 100)""" - snippetSize: Float @deprecated(reason: "No longer supported.") - - """UUID of a team to use as a boost.""" - teamId: String - - """Search string to look for.""" - term: String! - ): ProjectSearchPayload! - - """[INTERNAL] Search for various resources using natural language.""" - semanticSearch( - """Whether to include archived results in the search (default: false).""" - includeArchived: Boolean - - """The maximum number of results to return (default: 10).""" - maxResults: Int - - """Search query to look for.""" - query: String! - - """The types of results to return (default: all).""" - types: [SemanticSearchResultType!] - ): SemanticSearchPayload! @deprecated(reason: "Use specific search endpoints like searchIssues, searchProjects, searchDocuments instead.") - - """Fetch SSO login URL for the email provided.""" - ssoUrlFromEmail( - """Email to query the SSO login URL by.""" - email: String! - - """Whether the client is the desktop app.""" - isDesktop: Boolean - ): SsoUrlFromEmailResponse! - - """ - [Internal] AI summary of the latest project updates for the given projects - """ - summarizeProjectUpdates( - """The identifiers of the projects to summarize.""" - ids: [String!]! - ): SummaryPayload! - - """One specific team.""" - team(id: String!): Team! - - """One specific team membership.""" - teamMembership(id: String!): TeamMembership! - - """All team memberships.""" - teamMemberships( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TeamMembershipConnection! - - """ - All teams whose issues can be accessed by the user. This might be different from `administrableTeams`, which also includes teams whose settings can be changed by the user. - """ - teams( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned teams.""" - filter: TeamFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TeamConnection! - - """A specific template.""" - template( - """The identifier of the template to retrieve.""" - id: String! - ): Template! - - """All templates from all users.""" - templates: [Template!]! - - """Returns all templates that are associated with the integration type.""" - templatesForIntegration( - """The type of integration for which to return associated templates.""" - integrationType: String! - ): [Template!]! - - """A specific time schedule.""" - timeSchedule( - """The identifier of the time schedule to retrieve.""" - id: String! - ): TimeSchedule! - - """All time schedules.""" - timeSchedules( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TimeScheduleConnection! - - """All triage responsibilities.""" - triageResponsibilities( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TriageResponsibilityConnection! - - """A specific triage responsibility.""" - triageResponsibility( - """The identifier of the triage responsibility to retrieve.""" - id: String! - ): TriageResponsibility! - - """One specific user.""" - user( - """ - The identifier of the user to retrieve. To retrieve the authenticated user, use `viewer` query. - """ - id: String! - ): User! - - """The user's settings.""" - userSettings: UserSettings! - - """All users for the organization.""" - users( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned users.""" - filter: UserFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Should query return disabled/suspended users (default: false).""" - includeDisabled: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned users.""" - sort: [UserSortInput!] - ): UserConnection! - - """ - Verify that we received the correct response from the GitHub Enterprise Server. - """ - verifyGitHubEnterpriseServerInstallation( - """The integration ID.""" - integrationId: String! - ): GitHubEnterpriseServerInstallVerificationPayload! - - """The currently authenticated user.""" - viewer: User! - - """A specific webhook.""" - webhook( - """The identifier of the webhook to retrieve.""" - id: String! - ): Webhook! - - """All webhooks.""" - webhooks( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): WebhookConnection! - - """One specific state.""" - workflowState(id: String!): WorkflowState! - - """All issue workflow states.""" - workflowStates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned workflow states.""" - filter: WorkflowStateFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): WorkflowStateConnection! - - """ - [INTERNAL] Get a specific non-internal authorized application (with limited fields) for a workspace - """ - workspaceAuthorizedApplication( - """The client ID of the application.""" - clientId: String! - ): WorkspaceAuthorizedApplicationWithMemberships! - - """ - [INTERNAL] Get non-internal authorized applications for a workspace, including each application's app user. - """ - workspaceAuthorizedApplicationsWithAppUser( - """ - Client IDs of specific applications to return. If not provided, all workspace-authorized applications will be returned. - """ - clientIds: [String!] - ): [WorkspaceAuthorizedApplicationWithAppUser!]! -} - -type RateLimitPayload { - """The identifier we rate limit on.""" - identifier: String - - """The kind of rate limit selected for this request.""" - kind: String! - - """The state of the rate limit.""" - limits: [RateLimitResultPayload!]! -} - -type RateLimitResultPayload { - """The total allowed quantity for this type of limit.""" - allowedAmount: Float! - - """The period in which the rate limit is fully replenished in ms.""" - period: Float! - - """The remaining quantity for this type of limit after this request.""" - remainingAmount: Float! - - """The requested quantity for this type of limit.""" - requestedAmount: Float! - - """ - The timestamp after the rate limit is fully replenished as a UNIX timestamp. - """ - reset: Float! - - """What is being rate limited.""" - type: String! -} - -"""A reaction associated with a comment or a project update.""" -type Reaction implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The comment that the reaction is associated with.""" - comment: Comment - - """The time at which the entity was created.""" - createdAt: DateTime! - - """Name of the reaction's emoji.""" - emoji: String! - - """The external user that created the reaction.""" - externalUser: ExternalUser - - """The unique identifier of the entity.""" - id: ID! - - """The initiative update that the reaction is associated with.""" - initiativeUpdate: InitiativeUpdate - - """The issue that the reaction is associated with.""" - issue: Issue - - """The post that the reaction is associated with.""" - post: Post - - """The project update that the reaction is associated with.""" - projectUpdate: ProjectUpdate - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user that created the reaction.""" - user: User -} - -"""Reaction filtering options.""" -input ReactionCollectionFilter { - """Compound filters, all of which need to be matched by the reaction.""" - and: [ReactionCollectionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the reactions custom emoji.""" - customEmojiId: IDComparator - - """Comparator for the reactions emoji.""" - emoji: StringComparator - - """Filters that needs to be matched by all reactions.""" - every: ReactionFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Compound filters, one of which need to be matched by the reaction.""" - or: [ReactionCollectionFilter!] - - """Filters that needs to be matched by some reactions.""" - some: ReactionFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -input ReactionCreateInput { - """The comment to associate the reaction with.""" - commentId: String - - """The emoji the user reacted with.""" - emoji: String! - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The update to associate the reaction with.""" - initiativeUpdateId: String - - """The issue to associate the reaction with.""" - issueId: String - - """[Internal] The post to associate the reaction with.""" - postId: String - - """The project update to associate the reaction with.""" - projectUpdateId: String - - """[Internal] The pull request comment to associate the reaction with.""" - pullRequestCommentId: String - - """[Internal] The pull request to associate the reaction with.""" - pullRequestId: String -} - -"""Reaction filtering options.""" -input ReactionFilter { - """Compound filters, all of which need to be matched by the reaction.""" - and: [ReactionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the reactions custom emoji.""" - customEmojiId: IDComparator - - """Comparator for the reactions emoji.""" - emoji: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Compound filters, one of which need to be matched by the reaction.""" - or: [ReactionFilter!] - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type ReactionPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - reaction: Reaction! - success: Boolean! -} - -"""Comparator for relation existence.""" -input RelationExistsComparator { - """Equals constraint.""" - eq: Boolean - - """Not equals constraint.""" - neq: Boolean -} - -"""Features release channel.""" -enum ReleaseChannel { - beta - development - internal - preRelease - public -} - -"""Customer revenue sorting options.""" -input RevenueSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""[Deprecated] A roadmap for projects.""" -type Roadmap implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The roadmap's color.""" - color: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the roadmap.""" - creator: User! - - """The description of the roadmap.""" - description: String - - """The unique identifier of the entity.""" - id: ID! - - """The name of the roadmap.""" - name: String! - - """The organization of the roadmap.""" - organization: Organization! - - """The user who owns the roadmap.""" - owner: User - - """Projects associated with the roadmap.""" - projects( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned projects.""" - filter: ProjectFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): ProjectConnection! - - """The roadmap's unique URL slug.""" - slugId: String! - - """The sort order of the roadmap within the organization.""" - sortOrder: Float! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The canonical url for the roadmap.""" - url: String! -} - -"""A generic payload return from entity archive mutations.""" -type RoadmapArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: Roadmap - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Roadmap collection filtering options.""" -input RoadmapCollectionFilter { - """Compound filters, all of which need to be matched by the roadmap.""" - and: [RoadmapCollectionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the roadmap creator must satisfy.""" - creator: UserFilter - - """Filters that needs to be matched by all roadmaps.""" - every: RoadmapFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Comparator for the roadmap name.""" - name: StringComparator - - """Compound filters, one of which need to be matched by the roadmap.""" - or: [RoadmapCollectionFilter!] - - """Comparator for the roadmap slug ID.""" - slugId: StringComparator - - """Filters that needs to be matched by some roadmaps.""" - some: RoadmapFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type RoadmapConnection { - edges: [RoadmapEdge!]! - nodes: [Roadmap!]! - pageInfo: PageInfo! -} - -input RoadmapCreateInput { - """The roadmap's color.""" - color: String - - """The description of the roadmap.""" - description: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The name of the roadmap.""" - name: String! - - """The owner of the roadmap.""" - ownerId: String - - """The sort order of the roadmap within the organization.""" - sortOrder: Float -} - -type RoadmapEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Roadmap! -} - -"""Roadmap filtering options.""" -input RoadmapFilter { - """Compound filters, all of which need to be matched by the roadmap.""" - and: [RoadmapFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that the roadmap creator must satisfy.""" - creator: UserFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the roadmap name.""" - name: StringComparator - - """Compound filters, one of which need to be matched by the roadmap.""" - or: [RoadmapFilter!] - - """Comparator for the roadmap slug ID.""" - slugId: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type RoadmapPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The roadmap that was created or updated.""" - roadmap: Roadmap! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""[Deprecated] Join table between projects and roadmaps.""" -type RoadmapToProject implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The project that the roadmap is associated with.""" - project: Project! - - """The roadmap that the project is associated with.""" - roadmap: Roadmap! - - """The sort order of the project within the roadmap.""" - sortOrder: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type RoadmapToProjectConnection { - edges: [RoadmapToProjectEdge!]! - nodes: [RoadmapToProject!]! - pageInfo: PageInfo! -} - -input RoadmapToProjectCreateInput { - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The identifier of the project.""" - projectId: String! - - """The identifier of the roadmap.""" - roadmapId: String! - - """The sort order for the project within its organization.""" - sortOrder: Float -} - -type RoadmapToProjectEdge { - """Used in `before` and `after` args""" - cursor: String! - node: RoadmapToProject! -} - -type RoadmapToProjectPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """The roadmapToProject that was created or updated.""" - roadmapToProject: RoadmapToProject! - - """Whether the operation was successful.""" - success: Boolean! -} - -input RoadmapToProjectUpdateInput { - """The sort order for the project within its organization.""" - sortOrder: Float -} - -input RoadmapUpdateInput { - """The roadmap's color.""" - color: String - - """The description of the roadmap.""" - description: String - - """The name of the roadmap.""" - name: String - - """The owner of the roadmap.""" - ownerId: String - - """The sort order of the roadmap within the organization.""" - sortOrder: Float -} - -"""Issue root-issue sorting options.""" -input RootIssueSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder - - """The sort to apply to the root issues""" - sort: IssueSortInput! -} - -enum SLADayCountType { - all - onlyBusinessDays -} - -input SalesforceSettingsInput { - """ - Whether a ticket should be automatically reopened when its linked Linear issue is cancelled. - """ - automateTicketReopeningOnCancellation: Boolean - - """ - Whether a ticket should be automatically reopened when a comment is posted on its linked Linear issue - """ - automateTicketReopeningOnComment: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear issue is completed. - """ - automateTicketReopeningOnCompletion: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear project is cancelled. - """ - automateTicketReopeningOnProjectCancellation: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear project is completed. - """ - automateTicketReopeningOnProjectCompletion: Boolean - - """The Salesforce team to use when a template doesn't specify a team.""" - defaultTeam: String - - """ - [ALPHA] Whether customer and customer requests should not be automatically created when conversations are linked to a Linear issue. - """ - disableCustomerRequestsAutoCreation: Boolean - - """The Salesforce case status to use to reopen cases.""" - reopenCaseStatus: String - - """ - Whether to restrict visibility of the integration to issues that have been either created from Salesforce or linked to Salesforce. - """ - restrictVisibility: Boolean - - """ - Whether an internal message should be added when someone comments on an issue. - """ - sendNoteOnComment: Boolean - - """ - Whether an internal message should be added when a Linear issue changes status (for status types except completed or canceled). - """ - sendNoteOnStatusChange: Boolean - - """The Salesforce subdomain.""" - subdomain: String - - """The Salesforce instance URL.""" - url: String -} - -"""[INTERNAL] Payload returned by semantic search.""" -type SemanticSearchPayload { - enabled: Boolean! - results: [SemanticSearchResult!]! -} - -"""[INTERNAL] A semantic search result reference.""" -type SemanticSearchResult implements Node { - """The document related to the semantic search result.""" - document: Document - - """The unique identifier of the entity.""" - id: ID! - - """The initiative related to the semantic search result.""" - initiative: Initiative - - """The issue related to the semantic search result.""" - issue: Issue - - """The project related to the semantic search result.""" - project: Project - - """The type of the semantic search result.""" - type: SemanticSearchResultType! -} - -"""[INTERNAL] The type of the semantic search result.""" -enum SemanticSearchResultType { - document - initiative - issue - project -} - -enum SendStrategy { - desktop - desktopAndPush - desktopThenPush - push -} - -input SentrySettingsInput { - """The ID of the Sentry organization being connected.""" - organizationId: ID! - - """The slug of the Sentry organization being connected.""" - organizationSlug: String! - - """Whether Sentry issues resolving completes Linear issues.""" - resolvingCompletesIssues: Boolean! - - """Whether Sentry issues unresolving reopens Linear issues.""" - unresolvingReopensIssues: Boolean! -} - -"""SES domain identity used for sending emails from a custom domain.""" -type SesDomainIdentity implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - Whether the domain is fully verified and can be used for sending emails. - """ - canSendFromCustomDomain: Boolean! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the SES domain identity.""" - creator: User - - """The DNS records for the SES domain identity.""" - dnsRecords: [SesDomainIdentityDnsRecord!]! - - """The domain of the SES domain identity.""" - domain: String! - - """The unique identifier of the entity.""" - id: ID! - - """The organization of the SES domain identity.""" - organization: Organization! - - """The AWS region of the SES domain identity.""" - region: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""A DNS record for a SES domain identity.""" -type SesDomainIdentityDnsRecord { - """The content of the DNS record.""" - content: String! - - """Whether the DNS record is verified in the domain's DNS configuration.""" - isVerified: Boolean! - - """The name of the DNS record.""" - name: String! - - """The type of the DNS record.""" - type: String! -} - -"""Customer size sorting options.""" -input SizeSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -enum SlaStatus { - Breached - Completed - Failed - HighRisk - LowRisk - MediumRisk -} - -"""Comparator for sla status.""" -input SlaStatusComparator { - """Equals constraint.""" - eq: SlaStatus - - """In-array constraint.""" - in: [SlaStatus!] - - """Not-equals constraint.""" - neq: SlaStatus - - """Not-in-array constraint.""" - nin: [SlaStatus!] - - """ - Null constraint. Matches any non-null values if the given value is false, otherwise it matches null values. - """ - null: Boolean -} - -"""Issue SLA status sorting options.""" -input SlaStatusSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -input SlackAsksSettingsInput { - """The user role type that is allowed to manage Asks settings.""" - canAdministrate: UserRoleType! - - """Enterprise id of the connected Slack enterprise""" - enterpriseId: String - - """Enterprise name of the connected Slack enterprise""" - enterpriseName: String - - """Whether to show unfurl previews in Slack""" - shouldUnfurl: Boolean - - """ - The mapping of Slack channel ID => Slack channel name for connected channels. - """ - slackChannelMapping: [SlackChannelNameMappingInput!] - - """Slack workspace id""" - teamId: String - - """Slack workspace name""" - teamName: String -} - -"""Tuple for mapping Slack channel IDs to names.""" -type SlackAsksTeamSettings { - """ - Whether the default Asks template is enabled in the given channel for this team. - """ - hasDefaultAsk: Boolean! - - """The Linear team ID.""" - id: String! -} - -input SlackAsksTeamSettingsInput { - """ - Whether the default Asks template is enabled in the given channel for this team. - """ - hasDefaultAsk: Boolean! - - """The Linear team ID.""" - id: String! -} - -type SlackChannelConnectPayload { - """Whether the bot needs to be manually added to the channel.""" - addBot: Boolean! - - """The integration that was created or updated.""" - integration: Integration - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether it's recommended to connect main Slack integration.""" - nudgeToConnectMainSlackIntegration: Boolean - - """Whether it's recommended to update main Slack integration.""" - nudgeToUpdateMainSlackIntegration: Boolean - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Object for mapping Slack channel IDs to names and other settings.""" -type SlackChannelNameMapping { - """ - Whether or not to use AI to generate titles for Asks created in this channel. - """ - aiTitles: Boolean - - """ - Whether or not @-mentioning the bot should automatically create an Ask with the message. - """ - autoCreateOnBotMention: Boolean - - """ - Whether or not using the :ticket: emoji in this channel should automatically create Asks. - """ - autoCreateOnEmoji: Boolean - - """ - Whether or not top-level messages in this channel should automatically create Asks. - """ - autoCreateOnMessage: Boolean - - """ - The optional template ID to use for Asks auto-created in this channel. If not set, auto-created Asks won't use any template. - """ - autoCreateTemplateId: String - - """ - Whether or not the Linear Asks bot has been added to this Slack channel. - """ - botAdded: Boolean - - """The Slack channel ID.""" - id: String! - - """Whether or not the Slack channel is private.""" - isPrivate: Boolean - - """Whether or not the Slack channel is shared with an external org.""" - isShared: Boolean - - """The Slack channel name.""" - name: String! - - """ - Whether or not synced Slack threads should be updated with a message when their Ask is accepted from triage. - """ - postAcceptedFromTriageUpdates: Boolean - - """ - Whether or not synced Slack threads should be updated with a message and emoji when their Ask is canceled. - """ - postCancellationUpdates: Boolean - - """ - Whether or not synced Slack threads should be updated with a message and emoji when their Ask is completed. - """ - postCompletionUpdates: Boolean - - """Which teams are connected to the channel and settings for those teams.""" - teams: [SlackAsksTeamSettings!]! -} - -input SlackChannelNameMappingInput { - """ - Whether or not to use AI to generate titles for Asks created in this channel. - """ - aiTitles: Boolean - - """ - Whether or not @-mentioning the bot should automatically create an Ask with the message. - """ - autoCreateOnBotMention: Boolean - - """ - Whether or not using the :ticket: emoji in this channel should automatically create Asks. - """ - autoCreateOnEmoji: Boolean - - """ - Whether or not top-level messages in this channel should automatically create Asks. - """ - autoCreateOnMessage: Boolean - - """ - The optional template ID to use for Asks auto-created in this channel. If not set, auto-created Asks won't use any template. - """ - autoCreateTemplateId: String - - """ - Whether or not the Linear Asks bot has been added to this Slack channel. - """ - botAdded: Boolean - - """The Slack channel ID.""" - id: String! - - """Whether or not the Slack channel is private.""" - isPrivate: Boolean - - """Whether or not the Slack channel is shared with an external org.""" - isShared: Boolean - - """The Slack channel name.""" - name: String! - - """ - Whether or not synced Slack threads should be updated with a message when their Ask is accepted from triage. - """ - postAcceptedFromTriageUpdates: Boolean - - """ - Whether or not synced Slack threads should be updated with a message and emoji when their Ask is canceled. - """ - postCancellationUpdates: Boolean - - """ - Whether or not synced Slack threads should be updated with a message and emoji when their Ask is completed. - """ - postCompletionUpdates: Boolean - - """Which teams are connected to the channel and settings for those teams.""" - teams: [SlackAsksTeamSettingsInput!]! -} - -enum SlackChannelType { - DirectMessage - MultiPersonDirectMessage - Private - Public -} - -input SlackPostSettingsInput { - channel: String! - channelId: String! - channelType: SlackChannelType - configurationUrl: String! - - """Slack workspace id""" - teamId: String -} - -input SlackSettingsInput { - """Enterprise id of the connected Slack enterprise""" - enterpriseId: String - - """Enterprise name of the connected Slack enterprise""" - enterpriseName: String - - """ - Whether Linear should automatically respond with issue unfurls when an issue identifier is mentioned in a Slack message. - """ - linkOnIssueIdMention: Boolean! - - """Whether to show unfurl previews in Slack""" - shouldUnfurl: Boolean - - """Slack workspace id""" - teamId: String - - """Slack workspace name""" - teamName: String -} - -"""Comparator for issue source type.""" -input SourceMetadataComparator { - """Equals constraint.""" - eq: String - - """In-array constraint.""" - in: [String!] - - """Not-equals constraint.""" - neq: String - - """Not-in-array constraint.""" - nin: [String!] - - """ - Null constraint. Matches any non-null values if the given value is false, otherwise it matches null values. - """ - null: Boolean - - """Compound filters, all of which need to be matched by the sub type.""" - subType: SubTypeComparator -} - -"""Comparator for `sourceType` field.""" -input SourceTypeComparator { - """Contains constraint. Matches any values that contain the given string.""" - contains: String - - """ - Contains case insensitive constraint. Matches any values that contain the given string case insensitive. - """ - containsIgnoreCase: String - - """ - Contains case and accent insensitive constraint. Matches any values that contain the given string case and accent insensitive. - """ - containsIgnoreCaseAndAccent: String - - """ - Ends with constraint. Matches any values that end with the given string. - """ - endsWith: String - - """Equals constraint.""" - eq: String - - """ - Equals case insensitive. Matches any values that matches the given string case insensitive. - """ - eqIgnoreCase: String - - """In-array constraint.""" - in: [String!] - - """Not-equals constraint.""" - neq: String - - """ - Not-equals case insensitive. Matches any values that don't match the given string case insensitive. - """ - neqIgnoreCase: String - - """Not-in-array constraint.""" - nin: [String!] - - """ - Doesn't contain constraint. Matches any values that don't contain the given string. - """ - notContains: String - - """ - Doesn't contain case insensitive constraint. Matches any values that don't contain the given string case insensitive. - """ - notContainsIgnoreCase: String - - """ - Doesn't end with constraint. Matches any values that don't end with the given string. - """ - notEndsWith: String - - """ - Doesn't start with constraint. Matches any values that don't start with the given string. - """ - notStartsWith: String - - """ - Starts with constraint. Matches any values that start with the given string. - """ - startsWith: String - - """ - Starts with case insensitive constraint. Matches any values that start with the given string. - """ - startsWithIgnoreCase: String -} - -type SsoUrlFromEmailResponse { - """SAML SSO sign-in URL.""" - samlSsoUrl: String! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Project start date sorting options.""" -input StartDateSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Comparator for strings.""" -input StringArrayComparator { - """Compound filters, all of which need to be matched.""" - every: StringItemComparator - - """Length of the array. Matches any values that have the given length.""" - length: NumberComparator - - """Compound filters, one of which needs to be matched.""" - some: StringItemComparator -} - -"""Comparator for strings.""" -input StringComparator { - """Contains constraint. Matches any values that contain the given string.""" - contains: String - - """ - Contains case insensitive constraint. Matches any values that contain the given string case insensitive. - """ - containsIgnoreCase: String - - """ - Contains case and accent insensitive constraint. Matches any values that contain the given string case and accent insensitive. - """ - containsIgnoreCaseAndAccent: String - - """ - Ends with constraint. Matches any values that end with the given string. - """ - endsWith: String - - """Equals constraint.""" - eq: String - - """ - Equals case insensitive. Matches any values that matches the given string case insensitive. - """ - eqIgnoreCase: String - - """In-array constraint.""" - in: [String!] - - """Not-equals constraint.""" - neq: String - - """ - Not-equals case insensitive. Matches any values that don't match the given string case insensitive. - """ - neqIgnoreCase: String - - """Not-in-array constraint.""" - nin: [String!] - - """ - Doesn't contain constraint. Matches any values that don't contain the given string. - """ - notContains: String - - """ - Doesn't contain case insensitive constraint. Matches any values that don't contain the given string case insensitive. - """ - notContainsIgnoreCase: String - - """ - Doesn't end with constraint. Matches any values that don't end with the given string. - """ - notEndsWith: String - - """ - Doesn't start with constraint. Matches any values that don't start with the given string. - """ - notStartsWith: String - - """ - Starts with constraint. Matches any values that start with the given string. - """ - startsWith: String - - """ - Starts with case insensitive constraint. Matches any values that start with the given string. - """ - startsWithIgnoreCase: String -} - -"""Comparator for strings in arrays.""" -input StringItemComparator { - """Contains constraint. Matches any values that contain the given string.""" - contains: String - - """ - Contains case insensitive constraint. Matches any values that contain the given string case insensitive. - """ - containsIgnoreCase: String - - """ - Contains case and accent insensitive constraint. Matches any values that contain the given string case and accent insensitive. - """ - containsIgnoreCaseAndAccent: String - - """ - Ends with constraint. Matches any values that end with the given string. - """ - endsWith: String - - """Equals constraint.""" - eq: String - - """ - Equals case insensitive. Matches any values that matches the given string case insensitive. - """ - eqIgnoreCase: String - - """In-array constraint.""" - in: [String!] - - """Not-equals constraint.""" - neq: String - - """ - Not-equals case insensitive. Matches any values that don't match the given string case insensitive. - """ - neqIgnoreCase: String - - """Not-in-array constraint.""" - nin: [String!] - - """ - Doesn't contain constraint. Matches any values that don't contain the given string. - """ - notContains: String - - """ - Doesn't contain case insensitive constraint. Matches any values that don't contain the given string case insensitive. - """ - notContainsIgnoreCase: String - - """ - Doesn't end with constraint. Matches any values that don't end with the given string. - """ - notEndsWith: String - - """ - Doesn't start with constraint. Matches any values that don't start with the given string. - """ - notStartsWith: String - - """ - Starts with constraint. Matches any values that start with the given string. - """ - startsWith: String - - """ - Starts with case insensitive constraint. Matches any values that start with the given string. - """ - startsWithIgnoreCase: String -} - -"""Comparator for source type.""" -input SubTypeComparator { - """Equals constraint.""" - eq: String - - """In-array constraint.""" - in: [String!] - - """Not-equals constraint.""" - neq: String - - """Not-in-array constraint.""" - nin: [String!] - - """ - Null constraint. Matches any non-null values if the given value is false, otherwise it matches null values. - """ - null: Boolean -} - -type SuccessPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type SummaryPayload { - """Summary for project updates.""" - summary: String! -} - -"""A comment thread that is synced with an external source.""" -type SyncedExternalThread { - """The display name of the thread.""" - displayName: String - id: ID - - """Whether this thread is syncing with the external service.""" - isConnected: Boolean! - - """ - Whether the current user has the corresponding personal integration connected for the external service. - """ - isPersonalIntegrationConnected: Boolean! - - """ - Whether a connected personal integration is required to comment in this thread. - """ - isPersonalIntegrationRequired: Boolean! - - """The display name of the source.""" - name: String - - """The sub type of the external source.""" - subType: String - - """The type of the external source.""" - type: String! - - """The external url of the thread.""" - url: String -} - -"""Project target date sorting options.""" -input TargetDateSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""An organizational unit that contains issues.""" -type Team implements Node { - """Team's currently active cycle.""" - activeCycle: Cycle - - """Whether to enable resolved thread AI summaries.""" - aiThreadSummariesEnabled: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - Period after which automatically closed and completed issues are automatically archived in months. - """ - autoArchivePeriod: Float! - - """ - Whether child issues should automatically close when their parent issue is closed - """ - autoCloseChildIssues: Boolean - - """ - Whether parent issues should automatically close when all child issues are closed - """ - autoCloseParentIssues: Boolean - - """ - Period after which issues are automatically closed in months. Null/undefined means disabled. - """ - autoClosePeriod: Float - - """ - The canceled workflow state which auto closed issues will be set to. Defaults to the first canceled state. - """ - autoCloseStateId: String - - """[Internal] The team's sub-teams.""" - children: [Team!]! - - """The team's color.""" - color: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """[Internal] The current progress of the team.""" - currentProgress: JSONObject! - - """Calendar feed URL (iCal) for cycles.""" - cycleCalenderUrl: String! - - """The cooldown time after each cycle in weeks.""" - cycleCooldownTime: Float! - - """The duration of a cycle in weeks.""" - cycleDuration: Float! - - """Auto assign completed issues to current cycle.""" - cycleIssueAutoAssignCompleted: Boolean! - - """Auto assign started issues to current cycle.""" - cycleIssueAutoAssignStarted: Boolean! - - """Auto assign issues to current cycle if in active status.""" - cycleLockToActive: Boolean! - - """The day of the week that a new cycle starts.""" - cycleStartDay: Float! - - """Cycles associated with the team.""" - cycles( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned cycles.""" - filter: CycleFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): CycleConnection! - - """Whether the team uses cycles.""" - cyclesEnabled: Boolean! - - """What to use as a default estimate for unestimated issues.""" - defaultIssueEstimate: Float! - - """ - The default workflow state into which issues are set when they are opened by team members. - """ - defaultIssueState: WorkflowState - - """The default template to use for new projects created for the team.""" - defaultProjectTemplate: Template - - """ - The default template to use for new issues created by members of the team. - """ - defaultTemplateForMembers: Template - - """ - The id of the default template to use for new issues created by members of the team. - """ - defaultTemplateForMembersId: String @deprecated(reason: "Use defaultTemplateForMembers instead") - - """ - The default template to use for new issues created by non-members of the team. - """ - defaultTemplateForNonMembers: Template - - """ - The id of the default template to use for new issues created by non-members of the team. - """ - defaultTemplateForNonMembersId: String @deprecated(reason: "Use defaultTemplateForNonMembers instead") - - """The team's description.""" - description: String - - """The name of the team including its parent team name if it has one.""" - displayName: String! - - """ - The workflow state into which issues are moved when a PR has been opened as draft. - """ - draftWorkflowState: WorkflowState @deprecated(reason: "Use team.gitAutomationStates instead.") - - """[Internal] Facets associated with the team.""" - facets: [Facet!]! - - """The Git automation states for the team.""" - gitAutomationStates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): GitAutomationStateConnection! - - """Whether to group recent issue history entries.""" - groupIssueHistory: Boolean! - - """The icon of the team.""" - icon: String - - """The unique identifier of the entity.""" - id: ID! - - """ - Whether the team should inherit its estimation settings from its parent. Only applies to sub-teams. - """ - inheritIssueEstimation: Boolean! - - """ - Whether the team should inherit its workflow statuses from its parent. Only applies to sub-teams. - """ - inheritWorkflowStatuses: Boolean! - - """Settings for all integrations associated with that team.""" - integrationsSettings: IntegrationsSettings - - """Unique hash for the team to be used in invite URLs.""" - inviteHash: String! - - """Number of issues in the team.""" - issueCount( - """Include archived issues in the count.""" - includeArchived: Boolean = false - ): Int! - - """Whether to allow zeros in issues estimates.""" - issueEstimationAllowZero: Boolean! - - """Whether to add additional points to the estimate scale.""" - issueEstimationExtended: Boolean! - - """ - The issue estimation type to use. Must be one of "notUsed", "exponential", "fibonacci", "linear", "tShirt". - """ - issueEstimationType: String! - - """[DEPRECATED] Whether issues without priority should be sorted first.""" - issueOrderingNoPriorityFirst: Boolean! @deprecated(reason: "This setting is no longer in use.") - - """ - [DEPRECATED] Whether to move issues to bottom of the column when changing state. - """ - issueSortOrderDefaultToBottom: Boolean! @deprecated(reason: "Use setIssueSortOrderOnStateChange instead.") - - """Issues associated with the team.""" - issues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Include issues from sub-teams.""" - includeSubTeams: Boolean = false - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """[Internal] Whether new users should join this team by default.""" - joinByDefault: Boolean - - """The team's unique key. The key is used in URLs.""" - key: String! - - """Labels associated with the team.""" - labels( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issue labels.""" - filter: IssueLabelFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueLabelConnection! - - """ - The workflow state into which issues are moved when they are marked as a duplicate of another issue. Defaults to the first canceled state. - """ - markedAsDuplicateWorkflowState: WorkflowState - - """Users who are members of this team.""" - members( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned users.""" - filter: UserFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Should query return disabled/suspended users (default: false).""" - includeDisabled: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): UserConnection! - - """[ALPHA] The membership of the given user in the team.""" - membership( - """The user ID.""" - userId: String! - ): TeamMembership - - """ - Memberships associated with the team. For easier access of the same data, use `members` query. - """ - memberships( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TeamMembershipConnection! - - """ - The workflow state into which issues are moved when a PR has been merged. - """ - mergeWorkflowState: WorkflowState @deprecated(reason: "Use team.gitAutomationStates instead.") - - """ - The workflow state into which issues are moved when a PR is ready to be merged. - """ - mergeableWorkflowState: WorkflowState @deprecated(reason: "Use team.gitAutomationStates instead.") - - """The team's name.""" - name: String! - - """The organization that the team is associated with.""" - organization: Organization! - - """[Internal] The team's parent team.""" - parent: Team - - """[Internal] Posts associated with the team.""" - posts: [Post!]! - - """Whether the team is private or not.""" - private: Boolean! - - """[Internal] The progress history of the team.""" - progressHistory: JSONObject! - - """Projects associated with the team.""" - projects( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned projects.""" - filter: ProjectFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """Include projects from sub-teams.""" - includeSubTeams: Boolean = false - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - - """[INTERNAL] Sort returned projects.""" - sort: [ProjectSortInput!] - ): ProjectConnection! - - """Whether an issue needs to have a priority set before leaving triage.""" - requirePriorityToLeaveTriage: Boolean! - - """ - The workflow state into which issues are moved when a review has been requested for the PR. - """ - reviewWorkflowState: WorkflowState @deprecated(reason: "Use team.gitAutomationStates instead.") - - """The SCIM group name for the team.""" - scimGroupName: String - - """Whether the team is managed by SCIM integration.""" - scimManaged: Boolean! - - """Where to move issues when changing state.""" - setIssueSortOrderOnStateChange: String! - - """Whether to send new issue comment notifications to Slack.""" - slackIssueComments: Boolean! @deprecated(reason: "No longer in use") - - """Whether to send new issue status updates to Slack.""" - slackIssueStatuses: Boolean! @deprecated(reason: "No longer in use") - - """Whether to send new issue notifications to Slack.""" - slackNewIssue: Boolean! @deprecated(reason: "No longer is use") - - """ - The workflow state into which issues are moved when a PR has been opened. - """ - startWorkflowState: WorkflowState @deprecated(reason: "Use team.gitAutomationStates instead.") - - """The states that define the workflow associated with the team.""" - states( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned workflow states.""" - filter: WorkflowStateFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): WorkflowStateConnection! - - """Templates associated with the team.""" - templates( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned templates.""" - filter: NullableTemplateFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TemplateConnection! - - """ - The timezone of the team. Defaults to "America/Los_Angeles" - """ - timezone: String! - - """Whether triage mode is enabled for the team or not.""" - triageEnabled: Boolean! - - """ - The workflow state into which issues are set when they are opened by non-team members or integrations if triage is enabled. - """ - triageIssueState: WorkflowState - - """Team's triage responsibility.""" - triageResponsibility: TriageResponsibility - - """How many upcoming cycles to create.""" - upcomingCycleCount: Float! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Webhooks associated with the team.""" - webhooks( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): WebhookConnection! -} - -"""A generic payload return from entity archive mutations.""" -type TeamArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: Team - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -"""Team collection filtering options.""" -input TeamCollectionFilter { - """Compound filters, all of which need to be matched by the team.""" - and: [TeamCollectionFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Filters that needs to be matched by all teams.""" - every: TeamFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Compound filters, one of which need to be matched by the team.""" - or: [TeamCollectionFilter!] - - """Filters that needs to be matched by some teams.""" - some: TeamFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type TeamConnection { - edges: [TeamEdge!]! - nodes: [Team!]! - pageInfo: PageInfo! -} - -input TeamCreateInput { - """ - Period after which closed and completed issues are automatically archived, in months. 0 means disabled. - """ - autoArchivePeriod: Float - - """Period after which issues are automatically closed, in months.""" - autoClosePeriod: Float - - """The canceled workflow state which auto closed issues will be set to.""" - autoCloseStateId: String - - """The color of the team.""" - color: String - - """The cooldown time after each cycle in weeks.""" - cycleCooldownTime: Int - - """The duration of each cycle in weeks.""" - cycleDuration: Int - - """Auto assign completed issues to current active cycle setting.""" - cycleIssueAutoAssignCompleted: Boolean - - """Auto assign started issues to current active cycle setting.""" - cycleIssueAutoAssignStarted: Boolean - - """Only allow issues issues with cycles in Active Issues.""" - cycleLockToActive: Boolean - - """The day of the week that a new cycle starts.""" - cycleStartDay: Float - - """Whether the team uses cycles.""" - cyclesEnabled: Boolean - - """What to use as an default estimate for unestimated issues.""" - defaultIssueEstimate: Float - - """The identifier of the default project template of this team.""" - defaultProjectTemplateId: String - - """The identifier of the default template for members of this team.""" - defaultTemplateForMembersId: String - - """The identifier of the default template for non-members of this team.""" - defaultTemplateForNonMembersId: String - - """The description of the team.""" - description: String - - """Whether to group recent issue history entries.""" - groupIssueHistory: Boolean - - """The icon of the team.""" - icon: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """ - Whether the team should inherit estimation settings from its parent. Only applies to sub-teams. - """ - inheritIssueEstimation: Boolean - - """ - [Internal] Whether the team should inherit its product intelligence scope from its parent. Only applies to sub-teams. - """ - inheritProductIntelligenceScope: Boolean - - """ - [Internal] Whether the team should inherit workflow statuses from its parent. - """ - inheritWorkflowStatuses: Boolean - - """Whether to allow zeros in issues estimates.""" - issueEstimationAllowZero: Boolean - - """Whether to add additional points to the estimate scale.""" - issueEstimationExtended: Boolean - - """ - The issue estimation type to use. Must be one of "notUsed", "exponential", "fibonacci", "linear", "tShirt". - """ - issueEstimationType: String - - """[DEPRECATED] Whether issues without priority should be sorted first.""" - issueOrderingNoPriorityFirst: Boolean @deprecated(reason: "This setting is no longer in use.") - - """ - The key of the team. If not given, the key will be generated based on the name of the team. - """ - key: String - - """ - The workflow state into which issues are moved when they are marked as a duplicate of another issue. - """ - markedAsDuplicateWorkflowStateId: String - - """The name of the team.""" - name: String! - - """The organization associated with the team.""" - organizationId: String @deprecated(reason: "The request context is used to determine the organization.") - - """The parent team ID.""" - parentId: String - - """Internal. Whether the team is private or not.""" - private: Boolean - - """ - [Internal] The scope of product intelligence suggestion data for the team. - """ - productIntelligenceScope: ProductIntelligenceScope - - """Whether an issue needs to have a priority set before leaving triage.""" - requirePriorityToLeaveTriage: Boolean - - """Whether to move issues to bottom of the column when changing state.""" - setIssueSortOrderOnStateChange: String - - """The timezone of the team.""" - timezone: String - - """Whether triage mode is enabled for the team.""" - triageEnabled: Boolean - - """How many upcoming cycles to create.""" - upcomingCycleCount: Float -} - -type TeamEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Team! -} - -"""Team filtering options.""" -input TeamFilter { - """Compound filters, all of which need to be matched by the team.""" - and: [TeamFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the team description.""" - description: NullableStringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the teams issues must satisfy.""" - issues: IssueCollectionFilter - - """Comparator for the team key.""" - key: StringComparator - - """Comparator for the team name.""" - name: StringComparator - - """Compound filters, one of which need to be matched by the team.""" - or: [TeamFilter!] - - """Filters that the teams parent must satisfy.""" - parent: NullableTeamFilter - - """Comparator for the team privacy.""" - private: BooleanComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""Defines the membership of a user to a team.""" -type TeamMembership implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """Whether the user is the owner of the team.""" - owner: Boolean! - - """The order of the item in the users team list.""" - sortOrder: Float! - - """The team that the membership is associated with.""" - team: Team! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user that the membership is associated with.""" - user: User! -} - -type TeamMembershipConnection { - edges: [TeamMembershipEdge!]! - nodes: [TeamMembership!]! - pageInfo: PageInfo! -} - -input TeamMembershipCreateInput { - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """Internal. Whether the user is the owner of the team.""" - owner: Boolean - - """The position of the item in the users list.""" - sortOrder: Float - - """The identifier of the team associated with the membership.""" - teamId: String! - - """The identifier of the user associated with the membership.""" - userId: String! -} - -type TeamMembershipEdge { - """Used in `before` and `after` args""" - cursor: String! - node: TeamMembership! -} - -type TeamMembershipPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The team membership that was created or updated.""" - teamMembership: TeamMembership -} - -input TeamMembershipUpdateInput { - """Internal. Whether the user is the owner of the team.""" - owner: Boolean - - """The position of the item in the users list.""" - sortOrder: Float -} - -"""A team notification subscription.""" -type TeamNotificationSubscription implements Entity & Node & NotificationSubscription { - """Whether the subscription is active or not.""" - active: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The type of view to which the notification subscription context is associated with. - """ - contextViewType: ContextViewType - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The contextual custom view associated with the notification subscription. - """ - customView: CustomView - - """The customer associated with the notification subscription.""" - customer: Customer - - """ - The contextual cycle view associated with the notification subscription. - """ - cycle: Cycle - - """The unique identifier of the entity.""" - id: ID! - - """ - The contextual initiative view associated with the notification subscription. - """ - initiative: Initiative - - """ - The contextual label view associated with the notification subscription. - """ - label: IssueLabel - - """The type of subscription.""" - notificationSubscriptionTypes: [String!]! - - """ - The contextual project view associated with the notification subscription. - """ - project: Project - - """The user that subscribed to receive notifications.""" - subscriber: User! - - """The team subscribed to.""" - team: Team! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user view associated with the notification subscription.""" - user: User - - """ - The type of user view to which the notification subscription context is associated with. - """ - userContextViewType: UserContextViewType -} - -type TeamPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The team that was created or updated.""" - team: Team -} - -"""Issue team sorting options.""" -input TeamSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -input TeamUpdateInput { - """Whether to enable resolved thread AI summaries.""" - aiThreadSummariesEnabled: Boolean - - """ - Period after which closed and completed issues are automatically archived, in months. - """ - autoArchivePeriod: Float - - """ - [INTERNAL] Whether to automatically close all sub-issues when a parent issue in this team is closed. - """ - autoCloseChildIssues: Boolean - - """ - [INTERNAL] Whether to automatically close a parent issue in this team if all its sub-issues are closed. - """ - autoCloseParentIssues: Boolean - - """Period after which issues are automatically closed, in months.""" - autoClosePeriod: Float - - """The canceled workflow state which auto closed issues will be set to.""" - autoCloseStateId: String - - """The color of the team.""" - color: String - - """The cooldown time after each cycle in weeks.""" - cycleCooldownTime: Int - - """The duration of each cycle in weeks.""" - cycleDuration: Int - - """The date to begin cycles on.""" - cycleEnabledStartDate: DateTime - - """Auto assign completed issues to current active cycle setting.""" - cycleIssueAutoAssignCompleted: Boolean - - """Auto assign started issues to current active cycle setting.""" - cycleIssueAutoAssignStarted: Boolean - - """Only allow issues with cycles in Active Issues.""" - cycleLockToActive: Boolean - - """The day of the week that a new cycle starts.""" - cycleStartDay: Float - - """Whether the team uses cycles.""" - cyclesEnabled: Boolean - - """What to use as an default estimate for unestimated issues.""" - defaultIssueEstimate: Float - - """Default status for newly created issues.""" - defaultIssueStateId: String - - """The identifier of the default project template of this team.""" - defaultProjectTemplateId: String - - """The identifier of the default template for members of this team.""" - defaultTemplateForMembersId: String - - """The identifier of the default template for non-members of this team.""" - defaultTemplateForNonMembersId: String - - """The description of the team.""" - description: String - - """Whether to group recent issue history entries.""" - groupIssueHistory: Boolean - - """The icon of the team.""" - icon: String - - """ - Whether the team should inherit estimation settings from its parent. Only applies to sub-teams. - """ - inheritIssueEstimation: Boolean - - """ - [Internal] Whether the team should inherit its product intelligence scope from its parent. Only applies to sub-teams. - """ - inheritProductIntelligenceScope: Boolean - - """ - [Internal] Whether the team should inherit workflow statuses from its parent. - """ - inheritWorkflowStatuses: Boolean - - """Whether to allow zeros in issues estimates.""" - issueEstimationAllowZero: Boolean - - """Whether to add additional points to the estimate scale.""" - issueEstimationExtended: Boolean - - """ - The issue estimation type to use. Must be one of "notUsed", "exponential", "fibonacci", "linear", "tShirt". - """ - issueEstimationType: String - - """[DEPRECATED] Whether issues without priority should be sorted first.""" - issueOrderingNoPriorityFirst: Boolean @deprecated(reason: "This setting is no longer in use.") - - """ - Whether new users should join this team by default. Mutation restricted to workspace admins! - """ - joinByDefault: Boolean - - """The key of the team.""" - key: String - - """ - The workflow state into which issues are moved when they are marked as a duplicate of another issue. - """ - markedAsDuplicateWorkflowStateId: String - - """The name of the team.""" - name: String - - """The parent team ID.""" - parentId: String - - """Whether the team is private or not.""" - private: Boolean - - """ - [Internal] The scope of product intelligence suggestion data for the team. - """ - productIntelligenceScope: ProductIntelligenceScope - - """Whether an issue needs to have a priority set before leaving triage.""" - requirePriorityToLeaveTriage: Boolean - - """ - Whether the team is managed by SCIM integration. Mutation restricted to workspace admins and only unsetting is allowed! - """ - scimManaged: Boolean - - """Whether to move issues to bottom of the column when changing state.""" - setIssueSortOrderOnStateChange: String - - """Whether to send new issue comment notifications to Slack.""" - slackIssueComments: Boolean - - """Whether to send issue status update notifications to Slack.""" - slackIssueStatuses: Boolean - - """Whether to send new issue notifications to Slack.""" - slackNewIssue: Boolean - - """The timezone of the team.""" - timezone: String - - """Whether triage mode is enabled for the team.""" - triageEnabled: Boolean - - """How many upcoming cycles to create.""" - upcomingCycleCount: Float -} - -"""A template object used for creating entities faster.""" -type Template implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the template.""" - creator: User - - """Template description.""" - description: String - - """The unique identifier of the entity.""" - id: ID! - - """The original template inherited from.""" - inheritedFrom: Template - - """The user who last updated the template.""" - lastUpdatedBy: User - - """The name of the template.""" - name: String! - - """ - The organization that the template is associated with. If null, the template is associated with a particular team. - """ - organization: Organization! - - """The sort order of the template.""" - sortOrder: Float! - - """ - The team that the template is associated with. If null, the template is global to the workspace. - """ - team: Team - - """Template data.""" - templateData: JSON! - - """The entity type this template is for.""" - type: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type TemplateConnection { - edges: [TemplateEdge!]! - nodes: [Template!]! - pageInfo: PageInfo! -} - -input TemplateCreateInput { - """The template description.""" - description: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The template name.""" - name: String! - - """The position of the template in the templates list.""" - sortOrder: Float - - """ - The identifier or key of the team associated with the template. If not given, the template will be shared across all teams. - """ - teamId: String - - """ - The template data as JSON encoded attributes of the type of entity, such as an issue. - """ - templateData: JSON! - - """The template type, e.g. 'issue'.""" - type: String! -} - -type TemplateEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Template! -} - -type TemplatePayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The template that was created or updated.""" - template: Template! -} - -input TemplateUpdateInput { - """The template description.""" - description: String - - """The template name.""" - name: String - - """The position of the template in the templates list.""" - sortOrder: Float - - """ - The identifier or key of the team associated with the template. If set to null, the template will be shared across all teams. - """ - teamId: String - - """ - The template data as JSON encoded attributes of the type of entity, such as an issue. - """ - templateData: JSON -} - -"""Customer tier sorting options.""" -input TierSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A time schedule.""" -type TimeSchedule implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The schedule entries.""" - entries: [TimeScheduleEntry!] - - """The identifier of the external schedule.""" - externalId: String - - """The URL to the external schedule.""" - externalUrl: String - - """The unique identifier of the entity.""" - id: ID! - - """The identifier of the Linear integration populating the schedule.""" - integration: Integration - - """The name of the schedule.""" - name: String! - - """The organization of the schedule.""" - organization: Organization! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -type TimeScheduleConnection { - edges: [TimeScheduleEdge!]! - nodes: [TimeSchedule!]! - pageInfo: PageInfo! -} - -input TimeScheduleCreateInput { - """The schedule entries.""" - entries: [TimeScheduleEntryInput!]! - - """The unique identifier of the external schedule.""" - externalId: String - - """The URL to the external schedule.""" - externalUrl: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The name of the schedule.""" - name: String! -} - -type TimeScheduleEdge { - """Used in `before` and `after` args""" - cursor: String! - node: TimeSchedule! -} - -type TimeScheduleEntry { - """The end date of the schedule in ISO 8601 date-time format.""" - endsAt: DateTime! - - """The start date of the schedule in ISO 8601 date-time format.""" - startsAt: DateTime! - - """ - The email, name or reference to the user on schedule. This is used in case the external user could not be mapped to a Linear user id. - """ - userEmail: String - - """ - The Linear user id of the user on schedule. If the user cannot be mapped to a Linear user then `userEmail` can be used as a reference. - """ - userId: String -} - -input TimeScheduleEntryInput { - """The end date of the schedule in ISO 8601 date-time format.""" - endsAt: DateTime! - - """The start date of the schedule in ISO 8601 date-time format.""" - startsAt: DateTime! - - """ - The email, name or reference to the user on schedule. This is used in case the external user could not be mapped to a Linear user id. - """ - userEmail: String - - """ - The Linear user id of the user on schedule. If the user cannot be mapped to a Linear user then `userEmail` can be used as a reference. - """ - userId: String -} - -type TimeSchedulePayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - timeSchedule: TimeSchedule! -} - -input TimeScheduleUpdateInput { - """The schedule entries.""" - entries: [TimeScheduleEntryInput!] - - """The unique identifier of the external schedule.""" - externalId: String - - """The URL to the external schedule.""" - externalUrl: String - - """The name of the schedule.""" - name: String -} - -""" -Represents a date in ISO 8601 format. Accepts shortcuts like `2021` to represent midnight Fri Jan 01 2021. Also accepts ISO 8601 durations strings which are added to the current date to create the represented date (e.g '-P2W1D' represents the date that was two weeks and 1 day ago) -""" -scalar TimelessDate - -""" -Represents a date in ISO 8601 format or a duration. Accepts shortcuts like `2021` to represent midnight Fri Jan 01 2021. Also accepts ISO 8601 durations strings (e.g '-P2W1D'), which are not converted to dates. -""" -scalar TimelessDateOrDuration - -"""Issue title sorting options.""" -input TitleSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -input TokenUserAccountAuthInput { - """The email which to login via the magic login code.""" - email: String! - - """An optional invite link for an organization.""" - inviteLink: String - - """The identifiers of the teams to auto-join.""" - teamIdsToJoin: [String!] @deprecated(reason: "Not used anymore") - - """The timezone of the user's browser.""" - timezone: String! - - """The magic login code.""" - token: String! -} - -"""A team's triage responsibility.""" -type TriageResponsibility implements Node { - """The action to take when an issue is added to triage.""" - action: TriageResponsibilityAction! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user currently responsible for triage.""" - currentUser: User - - """The unique identifier of the entity.""" - id: ID! - - """Set of users used for triage responsibility.""" - manualSelection: TriageResponsibilityManualSelection - - """The team to which the triage responsibility belongs to.""" - team: Team! - - """The time schedule used for scheduling.""" - timeSchedule: TimeSchedule - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""Which action should be taken after an issue is added to triage.""" -enum TriageResponsibilityAction { - assign - notify -} - -type TriageResponsibilityConnection { - edges: [TriageResponsibilityEdge!]! - nodes: [TriageResponsibility!]! - pageInfo: PageInfo! -} - -input TriageResponsibilityCreateInput { - """The action to take when an issue is added to triage.""" - action: String! - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The manual selection of users responsible for triage.""" - manualSelection: TriageResponsibilityManualSelectionInput - - """The identifier of the team associated with the triage responsibility.""" - teamId: String! - - """ - The identifier of the time schedule used for scheduling triage responsibility - """ - timeScheduleId: String -} - -type TriageResponsibilityEdge { - """Used in `before` and `after` args""" - cursor: String! - node: TriageResponsibility! -} - -type TriageResponsibilityManualSelection { - """ - [Internal] The index of the current userId used for the assign action when having more than one user. - """ - assignmentIndex: Int - - """The set of users responsible for triage.""" - userIds: [String!]! -} - -"""Manual triage responsibility using a set of users.""" -input TriageResponsibilityManualSelectionInput { - """ - [Internal] The index of the current userId used for the assign action when having more than one user. - """ - assignmentIndex: Int - - """The set of users responsible for triage.""" - userIds: [String!]! -} - -type TriageResponsibilityPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - triageResponsibility: TriageResponsibility! -} - -input TriageResponsibilityUpdateInput { - """The action to take when an issue is added to triage.""" - action: String - - """The manual selection of users responsible for triage.""" - manualSelection: TriageResponsibilityManualSelectionInput - - """ - The identifier of the time schedule used for scheduling triage responsibility. - """ - timeScheduleId: String -} - -"""A universally unique identifier as specified by RFC 4122.""" -scalar UUID - -"""Issue update date sorting options.""" -input UpdatedAtSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""Object representing Google Cloud upload policy, plus additional data.""" -type UploadFile { - """The asset URL for the uploaded file. (assigned automatically).""" - assetUrl: String! - - """The content type.""" - contentType: String! - - """The filename.""" - filename: String! - headers: [UploadFileHeader!]! - metaData: JSONObject - - """The size of the uploaded file.""" - size: Int! - - """The signed URL the for the uploaded file. (assigned automatically).""" - uploadUrl: String! -} - -type UploadFileHeader { - """Upload file header key.""" - key: String! - - """Upload file header value.""" - value: String! -} - -type UploadPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """Object describing the file to be uploaded.""" - uploadFile: UploadFile -} - -"""A user that has access to the the resources of an organization.""" -type User implements Node { - """Whether the user account is active or disabled (suspended).""" - active: Boolean! - - """Whether the user is an organization administrator.""" - admin: Boolean! - - """Whether the user is an app.""" - app: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """Issues assigned to the user.""" - assignedIssues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """The background color of the avatar for users without set avatar.""" - avatarBackgroundColor: String! - - """An URL to the user's avatar image.""" - avatarUrl: String - - """[DEPRECATED] Hash for the user to be used in calendar URLs.""" - calendarHash: String - - """Whether this user can access any public team in the organization.""" - canAccessAnyPublicTeam: Boolean! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """Number of issues created.""" - createdIssueCount: Int! - - """Issues created by the user.""" - createdIssues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """Issues delegated to this user.""" - delegatedIssues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """A short description of the user, either its title or bio.""" - description: String - - """Reason why is the account disabled.""" - disableReason: String - - """The user's display (nick) name. Unique within each organization.""" - displayName: String! - - """The user's drafts""" - drafts( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): DraftConnection! - - """The user's email address.""" - email: String! - - """The user's GitHub user ID.""" - gitHubUserId: String - - """ - Whether the user is a guest in the workspace and limited to accessing a subset of teams. - """ - guest: Boolean! - - """The unique identifier of the entity.""" - id: ID! - - """[INTERNAL] Identity provider the user is managed by.""" - identityProvider: IdentityProvider - - """The initials of the user.""" - initials: String! - - """[DEPRECATED] Unique hash for the user to be used in invite URLs.""" - inviteHash: String! @deprecated(reason: "This hash is not in use anymore, this value will always be empty.") - - """Whether the user is assignable.""" - isAssignable: Boolean! - - """Whether the user is the currently authenticated user.""" - isMe: Boolean! - - """Whether the user is mentionable.""" - isMentionable: Boolean! - - """The user's issue drafts""" - issueDrafts( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueDraftConnection! - - """The last time the user was seen online.""" - lastSeen: DateTime - - """The user's full name.""" - name: String! - - """Organization the user belongs to.""" - organization: Organization! - - """The emoji to represent the user current status.""" - statusEmoji: String - - """The label of the user current status.""" - statusLabel: String - - """A date at which the user current status should be cleared.""" - statusUntilAt: DateTime - - """ - Memberships associated with the user. For easier access of the same data, use `teams` query. - """ - teamMemberships( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TeamMembershipConnection! - - """Teams the user is part of.""" - teams( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned teams.""" - filter: TeamFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): TeamConnection! - - """The local timezone of the user.""" - timezone: String - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """User's profile URL.""" - url: String! -} - -type UserAdminPayload { - """Whether the operation was successful.""" - success: Boolean! -} - -""" -Public information of the OAuth application, plus whether the application has been authorized for the given scopes. -""" -type UserAuthorizedApplication { - """Details of the app user's existing token, if any.""" - appUserAuthentication: AppUserAuthentication - - """ - Error associated with the application needing to be requested for approval in the workspace. - """ - approvalErrorCode: String - - """OAuth application's client ID.""" - clientId: String! - - """Whether the application was created by Linear.""" - createdByLinear: Boolean! - - """Information about the application.""" - description: String - - """Name of the developer.""" - developer: String! - - """Url of the developer (homepage or docs).""" - developerUrl: String! - - """OAuth application's ID.""" - id: String! - - """Image of the application.""" - imageUrl: String - - """Whether the user has authorized the application for the given scopes.""" - isAuthorized: Boolean! - - """Application name.""" - name: String! - - """Whether or not webhooks are enabled for the application.""" - webhooksEnabled: Boolean! -} - -"""User filtering options.""" -input UserCollectionFilter { - """Comparator for the user's activity status.""" - active: BooleanComparator - - """Comparator for the user's admin status.""" - admin: BooleanComparator - - """Compound filters, all of which need to be matched by the user.""" - and: [UserCollectionFilter!] - - """Comparator for the user's app status.""" - app: BooleanComparator - - """Filters that the users assigned issues must satisfy.""" - assignedIssues: IssueCollectionFilter - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the user's display name.""" - displayName: StringComparator - - """Comparator for the user's email.""" - email: StringComparator - - """Filters that needs to be matched by all users.""" - every: UserFilter - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the user's invited status.""" - invited: BooleanComparator - - """ - Filter based on the currently authenticated user. Set to true to filter for the authenticated user, false for any other user. - """ - isMe: BooleanComparator - - """Comparator for the collection length.""" - length: NumberComparator - - """Comparator for the user's name.""" - name: StringComparator - - """Compound filters, one of which need to be matched by the user.""" - or: [UserCollectionFilter!] - - """Filters that needs to be matched by some users.""" - some: UserFilter - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type UserConnection { - edges: [UserEdge!]! - nodes: [User!]! - pageInfo: PageInfo! -} - -enum UserContextViewType { - assigned -} - -"""User display name sorting options.""" -input UserDisplayNameSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -type UserEdge { - """Used in `before` and `after` args""" - cursor: String! - node: User! -} - -"""User filtering options.""" -input UserFilter { - """Comparator for the user's activity status.""" - active: BooleanComparator - - """Comparator for the user's admin status.""" - admin: BooleanComparator - - """Compound filters, all of which need to be matched by the user.""" - and: [UserFilter!] - - """Comparator for the user's app status.""" - app: BooleanComparator - - """Filters that the users assigned issues must satisfy.""" - assignedIssues: IssueCollectionFilter - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the user's display name.""" - displayName: StringComparator - - """Comparator for the user's email.""" - email: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Comparator for the user's invited status.""" - invited: BooleanComparator - - """ - Filter based on the currently authenticated user. Set to true to filter for the authenticated user, false for any other user. - """ - isMe: BooleanComparator - - """Comparator for the user's name.""" - name: StringComparator - - """Compound filters, one of which need to be matched by the user.""" - or: [UserFilter!] - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -"""The types of flags that the user can have.""" -enum UserFlagType { - all - analyticsWelcomeDismissed - canPlaySnake - canPlayTetris - commandMenuClearShortcutTip - completedOnboarding - cycleWelcomeDismissed - desktopDownloadToastDismissed - desktopInstalled - desktopTabsOnboardingDismissed - dueDateShortcutMigration - editorSlashCommandUsed - emptyActiveIssuesDismissed - emptyBacklogDismissed - emptyCustomViewsDismissed - emptyMyIssuesDismissed - emptyParagraphSlashCommandTip - figmaPluginBannerDismissed - figmaPromptDismissed - helpIslandFeatureInsightsDismissed - importBannerDismissed - initiativesBannerDismissed - insightsHelpDismissed - insightsWelcomeDismissed - issueLabelSuggestionUsed - issueMovePromptCompleted - joinTeamIntroductionDismissed - listSelectionTip - migrateThemePreference - milestoneOnboardingIsSeenAndDismissed - projectBacklogWelcomeDismissed - projectBoardOnboardingIsSeenAndDismissed - projectUpdatesWelcomeDismissed - projectWelcomeDismissed - pulseWelcomeDismissed - rewindBannerDismissed - slackBotWelcomeMessageShown - slackCommentReactionTipShown - teamsPageIntroductionDismissed - threadedCommentsNudgeIsSeen - triageWelcomeDismissed - tryCyclesDismissed - tryGithubDismissed - tryInvitePeopleDismissed - tryRoadmapsDismissed - tryTriageDismissed - updatedSlackThreadSyncIntegration -} - -"""Operations that can be applied to UserFlagType.""" -enum UserFlagUpdateOperation { - clear - decr - incr - lock -} - -"""User name sorting options.""" -input UserNameSort { - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -"""A user notification subscription.""" -type UserNotificationSubscription implements Entity & Node & NotificationSubscription { - """Whether the subscription is active or not.""" - active: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - The type of view to which the notification subscription context is associated with. - """ - contextViewType: ContextViewType - - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The contextual custom view associated with the notification subscription. - """ - customView: CustomView - - """The customer associated with the notification subscription.""" - customer: Customer - - """ - The contextual cycle view associated with the notification subscription. - """ - cycle: Cycle - - """The unique identifier of the entity.""" - id: ID! - - """ - The contextual initiative view associated with the notification subscription. - """ - initiative: Initiative - - """ - The contextual label view associated with the notification subscription. - """ - label: IssueLabel - - """The type of subscription.""" - notificationSubscriptionTypes: [String!]! - - """ - The contextual project view associated with the notification subscription. - """ - project: Project - - """The user that subscribed to receive notifications.""" - subscriber: User! - - """The team associated with the notification subscription.""" - team: Team - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user subscribed to.""" - user: User! - - """ - The type of user view to which the notification subscription context is associated with. - """ - userContextViewType: UserContextViewType -} - -type UserPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The user that was created or updated.""" - user: User -} - -"""The different permission roles available to users on an organization.""" -enum UserRoleType { - admin - app - guest - user -} - -"""The settings of a user as a JSON object.""" -type UserSettings implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """ - Whether to auto-assign newly created issues to the current user by default. - """ - autoAssignToSelf: Boolean! - - """Hash for the user to be used in calendar URLs.""" - calendarHash: String - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The user's notification category preferences.""" - notificationCategoryPreferences: NotificationCategoryPreferences! - - """The user's notification channel preferences.""" - notificationChannelPreferences: NotificationChannelPreferences! - - """ - The notification delivery preferences for the user. Note: notificationDisabled field is deprecated in favor of notificationChannelPreferences. - """ - notificationDeliveryPreferences: NotificationDeliveryPreferences! - - """Whether to show full user names instead of display names.""" - showFullUserNames: Boolean! - - """Whether this user is subscribed to changelog email or not.""" - subscribedToChangelog: Boolean! - - """Whether this user is subscribed to DPA emails or not.""" - subscribedToDPA: Boolean! - - """Whether this user is subscribed to invite accepted emails or not.""" - subscribedToInviteAccepted: Boolean! - - """ - Whether this user is subscribed to privacy and legal update emails or not. - """ - subscribedToPrivacyLegalUpdates: Boolean! - - """The email types the user has unsubscribed from.""" - unsubscribedFrom: [String!]! @deprecated(reason: "Use individual subscription fields instead. This field's value is now outdated.") - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The user associated with these settings.""" - user: User! -} - -type UserSettingsFlagPayload { - """The flag key which was updated.""" - flag: String - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The flag value after update.""" - value: Int -} - -type UserSettingsFlagsResetPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type UserSettingsPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The user's settings.""" - userSettings: UserSettings! -} - -input UserSettingsUpdateInput { - """[Internal] How often to generate a feed summary.""" - feedSummarySchedule: FeedSummarySchedule - - """The user's notification category preferences.""" - notificationCategoryPreferences: NotificationCategoryPreferencesInput - - """The user's notification channel preferences.""" - notificationChannelPreferences: PartialNotificationChannelPreferencesInput - - """The user's notification delivery preferences.""" - notificationDeliveryPreferences: NotificationDeliveryPreferencesInput - - """The user's settings.""" - settings: JSONObject - - """Whether this user is subscribed to changelog email or not.""" - subscribedToChangelog: Boolean - - """Whether this user is subscribed to DPA emails or not.""" - subscribedToDPA: Boolean - - """ - Whether this user is subscribed to general marketing communications or not. - """ - subscribedToGeneralMarketingCommunications: Boolean - - """Whether this user is subscribed to invite accepted emails or not.""" - subscribedToInviteAccepted: Boolean - - """ - Whether this user is subscribed to privacy and legal update emails or not. - """ - subscribedToPrivacyLegalUpdates: Boolean - - """The types of emails the user has unsubscribed from.""" - unsubscribedFrom: [String!] @deprecated(reason: "Use individual subscription fields instead. This field is now ignored.") - - """[Internal] The user's usage warning history.""" - usageWarningHistory: JSONObject -} - -"""User sorting options.""" -input UserSortInput { - """Sort by user display name""" - displayName: UserDisplayNameSort - - """Sort by user name""" - name: UserNameSort -} - -input UserUpdateInput { - """The avatar image URL of the user.""" - avatarUrl: String - - """The user description or a short bio.""" - description: String - - """The display name of the user.""" - displayName: String - - """The name of the user.""" - name: String - - """The emoji part of the user status.""" - statusEmoji: String - - """The label part of the user status.""" - statusLabel: String - - """When the user status should be cleared.""" - statusUntilAt: DateTime - - """The local timezone of the user.""" - timezone: String -} - -"""View preferences.""" -type ViewPreferences implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The unique identifier of the entity.""" - id: ID! - - """The view preferences""" - preferences: ViewPreferencesValues! - - """The view preference type.""" - type: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """The view type.""" - viewType: String! -} - -input ViewPreferencesCreateInput { - """The custom view these view preferences are associated with.""" - customViewId: String - - """The cycle these view preferences are associated with.""" - cycleId: String @deprecated(reason: "Not used") - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """[Internal] The initiative these view preferences are associated with.""" - initiativeId: String - - """The default parameters for the insight on that view.""" - insights: JSONObject - - """The label these view preferences are associated with.""" - labelId: String - - """View preferences object.""" - preferences: JSONObject! - - """The project these view preferences are associated with.""" - projectId: String - - """The project label these view preferences are associated with.""" - projectLabelId: String - - """The team these view preferences are associated with.""" - teamId: String - - """ - The type of view preferences (either user or organization level preferences). - """ - type: ViewPreferencesType! - - """The user profile these view preferences are associated with.""" - userId: String - - """The view type of the view preferences are associated with.""" - viewType: ViewType! -} - -type ViewPreferencesPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The view preferences entity being mutated.""" - viewPreferences: ViewPreferences! -} - -""" -The type of view preferences (either user or organization level preferences). -""" -enum ViewPreferencesType { - organization - user -} - -input ViewPreferencesUpdateInput { - """The default parameters for the insight on that view.""" - insights: JSONObject - - """View preferences.""" - preferences: JSONObject -} - -type ViewPreferencesValues { - """The issue grouping.""" - issueGrouping: String - - """Whether to show completed issues.""" - showCompletedIssues: String - - """The issue ordering.""" - viewOrdering: String -} - -"""The client view this custom view is targeting.""" -enum ViewType { - activeIssues - allIssues - archive - backlog - board - completedCycle - customView - customViews - customer - customers - cycle - dashboards - embeddedCustomerNeeds - feedAll - feedCreated - feedFollowing - feedPopular - inbox - initiative - initiativeOverview - initiativeOverviewSubInitiatives - initiatives - initiativesCompleted - initiativesPlanned - issueIdentifiers - label - myIssues - myIssuesActivity - myIssuesCreatedByMe - myIssuesSubscribedTo - myReviews - project - projectCustomerNeeds - projectDocuments - projectLabel - projects - projectsAll - projectsBacklog - projectsClosed - quickView - reviews - roadmap - roadmapAll - roadmapBacklog - roadmapClosed - roadmaps - search - splitSearch - subIssues - teams - triage - userProfile - userProfileCreatedByUser - workspaceMembers -} - -"""A webhook used to send HTTP notifications over data updates.""" -type Webhook implements Node { - """ - Whether the Webhook is enabled for all public teams, including teams created after the webhook was created. - """ - allPublicTeams: Boolean! - - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The time at which the entity was created.""" - createdAt: DateTime! - - """The user who created the webhook.""" - creator: User - - """Whether the Webhook is enabled.""" - enabled: Boolean! - - """ - [INTERNAL] Webhook failure events associated with the webhook (last 50). - """ - failures: [WebhookFailureEvent!]! - - """The unique identifier of the entity.""" - id: ID! - - """Webhook label.""" - label: String - - """The resource types this webhook is subscribed to.""" - resourceTypes: [String!]! - - """Secret token for verifying the origin on the recipient side.""" - secret: String - - """ - The team that the webhook is associated with. If null, the webhook is associated with all public teams of the organization or multiple teams. - """ - team: Team - - """ - [INTERNAL] The teams that the webhook is associated with. Used to represent a webhook that targets multiple teams, potentially in addition to all public teams of the organization. - """ - teamIds: [String!] - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! - - """Webhook URL.""" - url: String -} - -type WebhookConnection { - edges: [WebhookEdge!]! - nodes: [Webhook!]! - pageInfo: PageInfo! -} - -input WebhookCreateInput { - """Whether this webhook is enabled for all public teams.""" - allPublicTeams: Boolean - - """Whether this webhook is enabled.""" - enabled: Boolean = true - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """Label for the webhook.""" - label: String - - """List of resources the webhook should subscribe to.""" - resourceTypes: [String!]! - - """A secret token used to sign the webhook payload.""" - secret: String - - """The identifier or key of the team associated with the Webhook.""" - teamId: String - - """The URL that will be called on data changes.""" - url: String! -} - -type WebhookEdge { - """Used in `before` and `after` args""" - cursor: String! - node: Webhook! -} - -"""Entity representing a webhook execution failure.""" -type WebhookFailureEvent { - """The time at which the entity was created.""" - createdAt: DateTime! - - """ - The unique execution ID of the webhook push. This is retained between retries of the same push. - """ - executionId: String! - - """The HTTP status code returned by the recipient.""" - httpStatus: Float - - """The unique identifier of the entity.""" - id: ID! - - """The HTTP response body returned by the recipient or error occured.""" - responseOrError: String - - """The URL that the webhook was trying to push to.""" - url: String! - - """The webhook that this failure event is associated with.""" - webhook: Webhook! -} - -type WebhookPayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The webhook entity being mutated.""" - webhook: Webhook! -} - -input WebhookUpdateInput { - """Whether this webhook is enabled.""" - enabled: Boolean - - """Label for the webhook.""" - label: String - - """List of resources the webhook should subscribe to.""" - resourceTypes: [String!] - - """A secret token used to sign the webhook payload.""" - secret: String - - """The URL that will be called on data changes.""" - url: String -} - -"""A state in a team workflow.""" -type WorkflowState implements Node { - """ - The time at which the entity was archived. Null if the entity has not been archived. - """ - archivedAt: DateTime - - """The state's UI color as a HEX string.""" - color: String! - - """The time at which the entity was created.""" - createdAt: DateTime! - - """Description of the state.""" - description: String - - """The unique identifier of the entity.""" - id: ID! - - """The state inherited from""" - inheritedFrom: WorkflowState - - """Issues belonging in this state.""" - issues( - """A cursor to be used with first for forward pagination""" - after: String - - """A cursor to be used with last for backward pagination.""" - before: String - - """Filter returned issues.""" - filter: IssueFilter - - """ - The number of items to forward paginate (used with after). Defaults to 50. - """ - first: Int - - """Should archived resources be included (default: false)""" - includeArchived: Boolean - - """ - The number of items to backward paginate (used with before). Defaults to 50. - """ - last: Int - - """ - By which field should the pagination order by. Available options are createdAt (default) and updatedAt. - """ - orderBy: PaginationOrderBy - ): IssueConnection! - - """The state's name.""" - name: String! - - """The position of the state in the team flow.""" - position: Float! - - """The team to which this state belongs to.""" - team: Team! - - """ - The type of the state. One of "triage", "backlog", "unstarted", "started", "completed", "canceled". - """ - type: String! - - """ - The last time at which the entity was meaningfully updated. This is the same as the creation time if the entity hasn't - been updated after creation. - """ - updatedAt: DateTime! -} - -"""A generic payload return from entity archive mutations.""" -type WorkflowStateArchivePayload implements ArchivePayload { - """The archived/unarchived entity. Null if entity was deleted.""" - entity: WorkflowState - - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! -} - -type WorkflowStateConnection { - edges: [WorkflowStateEdge!]! - nodes: [WorkflowState!]! - pageInfo: PageInfo! -} - -input WorkflowStateCreateInput { - """The color of the state.""" - color: String! - - """The description of the state.""" - description: String - - """ - The identifier in UUID v4 format. If none is provided, the backend will generate one. - """ - id: String - - """The name of the state.""" - name: String! - - """The position of the state.""" - position: Float - - """The team associated with the state.""" - teamId: String! - - """The workflow type.""" - type: String! -} - -type WorkflowStateEdge { - """Used in `before` and `after` args""" - cursor: String! - node: WorkflowState! -} - -"""Workflow state filtering options.""" -input WorkflowStateFilter { - """ - Compound filters, all of which need to be matched by the workflow state. - """ - and: [WorkflowStateFilter!] - - """Comparator for the created at date.""" - createdAt: DateComparator - - """Comparator for the workflow state description.""" - description: StringComparator - - """Comparator for the identifier.""" - id: IDComparator - - """Filters that the workflow states issues must satisfy.""" - issues: IssueCollectionFilter - - """Comparator for the workflow state name.""" - name: StringComparator - - """ - Compound filters, one of which need to be matched by the workflow state. - """ - or: [WorkflowStateFilter!] - - """Comparator for the workflow state position.""" - position: NumberComparator - - """Filters that the workflow states team must satisfy.""" - team: TeamFilter - - """ - Comparator for the workflow state type. Possible values are "triage", "backlog", "unstarted", "started", "completed", "canceled". - """ - type: StringComparator - - """Comparator for the updated at date.""" - updatedAt: DateComparator -} - -type WorkflowStatePayload { - """The identifier of the last sync operation.""" - lastSyncId: Float! - - """Whether the operation was successful.""" - success: Boolean! - - """The state that was created or updated.""" - workflowState: WorkflowState! -} - -"""Issue workflow state sorting options.""" -input WorkflowStateSort { - """Whether to sort closed issues by recency""" - closedIssuesOrderedByRecency: Boolean = false - - """Whether nulls should be sorted first or last""" - nulls: PaginationNulls = last - - """The order for the individual sort""" - order: PaginationSortOrder -} - -input WorkflowStateUpdateInput { - """The color of the state.""" - color: String - - """The description of the state.""" - description: String - - """The name of the state.""" - name: String - - """The position of the state.""" - position: Float -} - -""" -[INTERNAL] Public information of the OAuth application, plus the app user and aggregate membership count. -""" -type WorkspaceAuthorizedApplicationWithAppUser { - """OAuth application's ID.""" - appId: String! - - """The app user associated with this client, if one exists.""" - appUser: AuthMembership - - """OAuth application's client ID.""" - clientId: String! - - """Description of the application.""" - description: String - - """Developer of the application.""" - developer: String - - """Developer URL of the application.""" - developerUrl: String - - """Image of the application.""" - imageUrl: String - - """Application name.""" - name: String! - - """Scopes that are authorized for this application for a given user.""" - scope: [String!]! - - """ - Total number of members (including the app user, if it exists) that authorized the application. - """ - totalMembers: Float! - - """Whether or not webhooks are enabled for the application.""" - webhooksEnabled: Boolean! -} - -""" -[INTERNAL] Public information of the OAuth application with its memberships -""" -type WorkspaceAuthorizedApplicationWithMemberships { - """Details of the OAuth application""" - client: AuthorizedApplication! - - """ - UserIds and membership dates of everyone who has authorized the application - """ - memberships: [AuthMembership!]! - - """Total number of members that authorized the application.""" - totalMembers: Float! -} - -input ZendeskSettingsInput { - """ - Whether a ticket should be automatically reopened when its linked Linear issue is cancelled. - """ - automateTicketReopeningOnCancellation: Boolean - - """ - Whether a ticket should be automatically reopened when a comment is posted on its linked Linear issue - """ - automateTicketReopeningOnComment: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear issue is completed. - """ - automateTicketReopeningOnCompletion: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear project is cancelled. - """ - automateTicketReopeningOnProjectCancellation: Boolean - - """ - Whether a ticket should be automatically reopened when its linked Linear project is completed. - """ - automateTicketReopeningOnProjectCompletion: Boolean - - """The ID of the Linear bot user.""" - botUserId: String - - """ - [INTERNAL] Temporary flag indicating if the integration has the necessary scopes for Customers - """ - canReadCustomers: Boolean - - """ - [ALPHA] Whether customer and customer requests should not be automatically created when conversations are linked to a Linear issue. - """ - disableCustomerRequestsAutoCreation: Boolean - - """ - Whether an internal message should be added when someone comments on an issue. - """ - sendNoteOnComment: Boolean - - """ - Whether an internal message should be added when a Linear issue changes status (for status types except completed or canceled). - """ - sendNoteOnStatusChange: Boolean - - """The subdomain of the Zendesk organization being connected.""" - subdomain: String! - - """ - [INTERNAL] Flag indicating if the integration supports OAuth refresh tokens - """ - supportsOAuthRefresh: Boolean - - """The URL of the connected Zendesk organization.""" - url: String! -} \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md index c0d1a87..918af88 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,34 +1,163 @@ -<!-- Generated: 2025-01-09T12:34:56+00:00 --> - # Architecture -Linearis follows a modular, service-oriented architecture with clear separation of concerns. The application uses a command-based structure with Commander.js, optimized GraphQL service layers for Linear API integration, and utility modules for cross-cutting concerns like authentication and output formatting. +Linearis follows a modular, five-layer architecture with clear separation of concerns. The application uses a command-based structure with Commander.js, typed GraphQL operations, standalone resolver functions, and service functions that eliminate code duplication. + +The architecture emphasizes performance through GraphQL batch operations, single-query optimizations, and smart ID resolution for user convenience. All components are fully typed with TypeScript - no `any` types in the new architecture. The system uses both direct GraphQL queries (via typed client) and Linear SDK (for ID resolution). + +## Five-Layer Architecture + +### 1. Client Layer (`src/client/`) + +Thin wrappers around GraphQL and Linear SDK with no business logic. + +- **graphql-client.ts** - Typed GraphQL client + - Takes `DocumentNode` from codegen + - Returns typed results via generics + - Handles error transformation + - No ID resolution or business logic + +- **linear-client.ts** - Linear SDK wrapper + - Simple wrapper exposing `sdk` property + - Used by resolvers for lookups + - No business logic + +### 2. Resolver Layer (`src/resolvers/`) + +Pure functions that convert human-friendly identifiers to UUIDs. + +- **team-resolver.ts** - `resolveTeamId(client, keyOrNameOrId)` + - Tries team key first, falls back to name + - Returns UUID + +- **project-resolver.ts** - `resolveProjectId(client, nameOrId)` +- **label-resolver.ts** - `resolveLabelId(client, nameOrId)`, `resolveLabelIds(client, namesOrIds)` +- **issue-resolver.ts** - `resolveIssueId(client, issueIdOrIdentifier)` - Parses ABC-123 format +- **status-resolver.ts** - `resolveStatusId(client, nameOrId, teamId?)` +- **cycle-resolver.ts** - `resolveCycleId(client, nameOrId, teamFilter?)` - Complex disambiguation +- **milestone-resolver.ts** - `resolveMilestoneId(gqlClient, sdkClient, nameOrId, projectNameOrId?)` + +**Pattern:** +- Accept SDK or GraphQL client +- Check if input is UUID (early return) +- Query Linear API for name/key match +- Throw descriptive error if not found +- Return UUID string + +### 3. Service Layer (`src/services/`) + +Pure, typed functions for CRUD operations. Receive pre-resolved UUIDs. + +- **issue-service.ts** - `listIssues()`, `getIssue()`, `searchIssues()`, `createIssue()`, `updateIssue()` +- **document-service.ts** - `getDocument()`, `createDocument()`, `updateDocument()`, `listDocuments()`, `deleteDocument()` +- **attachment-service.ts** - `createAttachment()`, `deleteAttachment()`, `listAttachments()` +- **milestone-service.ts** - `listMilestones()`, `getMilestone()`, `createMilestone()`, `updateMilestone()` +- **cycle-service.ts** - `listCycles()`, `getCycle()` +- **team-service.ts** - `listTeams()` +- **user-service.ts** - `listUsers()` +- **project-service.ts** - `listProjects()` +- **label-service.ts** - `listLabels()` +- **comment-service.ts** - `createComment()` +- **file-service.ts** - File upload/download operations + +**Pattern:** +- Accept `GraphQLClient` or `LinearSdkClient` +- Take pre-resolved UUIDs in inputs +- Use codegen `DocumentNode` types +- Return typed results +- Throw on failure + +### 4. Command Layer (`src/commands/`) + +Thin orchestration layer that composes resolvers and services. + +- **auth.ts** - Authentication commands (login, status, logout) — interactive, for humans +- **issues.ts** - Issue commands (list, search, read, create, update) +- **documents.ts** - Document commands with attachment operations +- **project-milestones.ts** - Milestone commands +- **cycles.ts** - Cycle commands +- **teams.ts** - Team listing +- **users.ts** - User listing +- **projects.ts** - Project listing +- **labels.ts** - Label listing +- **comments.ts** - Comment creation +- **embeds.ts** - File download operations + +**Pattern:** +```typescript +.action( + handleCommand( + async (...args: unknown[]) => { + const [options, command] = args as [OptionsType, Command]; + const ctx = await createContext(command.parent!.parent!.opts()); + + // Resolve IDs + const teamId = await resolveTeamId(ctx.sdk, options.team); + const labelIds = await resolveLabelIds(ctx.sdk, options.labels.split(',')); + + // Call service + const result = await createIssue(ctx.gql, { + teamId, + labelIds, + title: options.title, + }); + + outputSuccess(result); + } + ) +) +``` + +### 5. Common Layer (`src/common/`) + +Shared utilities used across layers. -The architecture emphasizes performance through GraphQL batch operations, single-query optimizations, and smart ID resolution for user convenience. All components are fully typed with TypeScript interfaces, ensuring type safety throughout the application. The system uses both direct GraphQL queries and SDK fallbacks for optimal performance. +- **context.ts** - `createContext(options)` - Creates `{ gql, sdk }` from auth +- **auth.ts** - `resolveApiToken(options)` - Multi-source authentication (flag, env, encrypted storage, legacy file) +- **output.ts** - `outputSuccess(data)`, `outputError(error)`, `handleCommand(fn)` +- **errors.ts** - `notFoundError()`, `multipleMatchesError()`, `invalidParameterError()` +- **identifier.ts** - `isUuid()`, `parseIssueIdentifier()`, `tryParseIssueIdentifier()` +- **types.ts** - Type aliases from codegen (Issue, Document, Attachment, etc.) +- **embed-parser.ts** - Linear upload URL parsing utilities +- **usage.ts** - Token-optimized two-tier usage system (`formatOverview()`, `formatDomainUsage()`, `DomainMeta`) ## Component Map ### Command Layer - CLI Interface - **src/main.ts** - Main program setup with Commander.js, command routing, and global options -- **src/commands/issues.ts** - Issue management commands (list, search, create, read, update) with enhanced label and parent management -- **src/commands/projects.ts** - Project operations commands (list, read) -- **src/commands/comments.ts** - Comment operations (create) with lightweight issue ID resolution -- **src/commands/teams.ts** - Team operations (list) with workspace team discovery -- **src/commands/users.ts** - User operations (list) with active user filtering +- **src/commands/auth.ts** - Authentication management (interactive, for humans) +- **src/commands/issues.ts** - Issue management with resolvers and service composition +- **src/commands/documents.ts** - Document operations with attachment support +- **src/commands/project-milestones.ts** - Milestone CRUD operations +- **src/commands/cycles.ts** - Cycle listing and reading +- **src/commands/teams.ts** - Team listing +- **src/commands/users.ts** - User listing +- **src/commands/projects.ts** - Project listing +- **src/commands/labels.ts** - Label listing +- **src/commands/comments.ts** - Comment creation +- **src/commands/embeds.ts** - File operations + +### Client Layer - API Wrappers + +- **src/client/graphql-client.ts** - Typed GraphQL client with error handling +- **src/client/linear-client.ts** - Linear SDK wrapper + +### Resolver Layer - ID Resolution + +- **src/resolvers/** - Pure resolver functions for converting names/identifiers to UUIDs ### Service Layer - Business Logic -- **src/utils/graphql-service.ts** - GraphQL client wrapper with error handling and batch operation support -- **src/utils/graphql-issues-service.ts** - Optimized GraphQL operations for issues (single queries, batch resolving) -- **src/utils/linear-service.ts** - Complete Linear API service with smart ID resolution and SDK operations -- **src/queries/** - GraphQL query definitions and fragments for optimized operations -- **src/utils/auth.ts** - Authentication handling with multiple token source support -- **src/utils/output.ts** - JSON output formatting and error handling utilities +- **src/services/** - Pure, typed functions for CRUD operations + +### Common Layer - Shared Utilities + +- **src/common/** - Authentication, output formatting, error handling, types ### Type System - Data Contracts -- **src/utils/linear-types.d.ts** - TypeScript interfaces for Linear entities (LinearIssue, LinearProject, etc.) +- **src/gql/graphql.ts** - Generated TypeScript types and DocumentNode exports from GraphQL schema +- **src/common/types.ts** - Convenient type aliases derived from codegen types ## Key Files @@ -36,72 +165,95 @@ The architecture emphasizes performance through GraphQL batch operations, single **Main Entry Point** -- src/main.ts (lines 1-25) - Sets up Commander.js program with global options and subcommand registration +- src/main.ts - Sets up Commander.js program with global options and subcommand registration + +**Client Layer** -**GraphQL Service Layer** +- src/client/graphql-client.ts - GraphQLClient class with typed request method +- src/client/linear-client.ts - LinearSdkClient wrapper -- src/utils/graphql-service.ts (lines 8-62) - GraphQLService class with raw GraphQL execution and batch operations -- src/utils/graphql-issues-service.ts (lines 25-604) - GraphQLIssuesService with single-query optimized operations -- src/queries/issues.ts (lines 13-301) - Optimized GraphQL queries and mutations for issue operations +**Resolver Layer** -**Legacy Service Layer** +- src/resolvers/team-resolver.ts - Team key/name → UUID +- src/resolvers/issue-resolver.ts - ABC-123 → UUID +- src/resolvers/cycle-resolver.ts - Cycle name → UUID with disambiguation -- src/utils/linear-service.ts (lines 11-484) - LinearService class with SDK- based API methods and fallback operations -- src/utils/auth.ts (lines 18-38) - getApiToken function with fallback authentication sources +**Service Layer** -**Command Handlers** +- src/services/issue-service.ts - Issue CRUD operations +- src/services/document-service.ts - Document CRUD operations +- src/services/milestone-service.ts - Milestone CRUD operations -- src/commands/issues.ts (lines 10-210) - setupIssuesCommands with all issue operations -- src/commands/projects.ts (lines 9-30) - setupProjectsCommands with project operations -- src/commands/teams.ts (lines 8-47) - setupTeamsCommands with team listing operations -- src/commands/users.ts (lines 8-49) - setupUsersCommands with user listing operations +**Common Layer** + +- src/common/context.ts - createContext factory +- src/common/auth.ts - resolveApiToken with fallback sources (flag, env, encrypted storage, legacy file) +- src/common/token-storage.ts - Encrypted token storage (saveToken, getStoredToken, clearToken) +- src/common/output.ts - outputSuccess, outputError, handleCommand + +**Query Definitions** + +- graphql/queries/*.graphql - GraphQL operation definitions +- graphql/mutations/*.graphql - GraphQL mutation definitions +- src/gql/graphql.ts - Generated types and DocumentNode exports ## Data Flow -### Command Execution Flow with File References +### Command Execution Flow -1. **Command Parsing** - src/main.ts (lines 23-24) parses CLI arguments via Commander.js -2. **Authentication** - src/utils/auth.ts (lines 18-38) resolves API token from multiple sources -3. **Service Creation** - src/utils/linear-service.ts (lines 479-484) creates authenticated LinearService -4. **API Operations** - Service methods execute optimized GraphQL queries with parallel fetching -5. **Response Formatting** - src/utils/output.ts (lines 5-7) outputs structured JSON responses +1. **Command Parsing** - src/main.ts parses CLI arguments via Commander.js +2. **Context Creation** - src/common/context.ts creates `{ gql, sdk }` from auth options +3. **Authentication** - src/common/auth.ts resolves API token from multiple sources +4. **ID Resolution** - src/resolvers/* convert human inputs to UUIDs via SDK +5. **Service Operations** - src/services/* execute typed GraphQL operations +6. **Response Formatting** - src/common/output.ts outputs structured JSON ### Smart ID Resolution Process -Linear API uses UUIDs internally, but users prefer human-readable identifiers: +Linear API uses UUIDs internally, but users prefer human-readable identifiers. Resolution happens in the resolver layer: -**Issue Resolution** (src/utils/linear-service.ts lines 193-290) +**Issue Resolution** (src/resolvers/issue-resolver.ts) - Input: "ABC-123" → Parse team key and issue number → Query by team.key + issue.number → Return UUID -**Project Resolution** (lines 398-415) +**Project Resolution** (src/resolvers/project-resolver.ts) -- Input: "Mobile App" → Query projects by name → Return project UUID +- Input: "Mobile App" → Query projects by case-insensitive name → Return project UUID -**Team Resolution** (lines 449-473) +**Team Resolution** (src/resolvers/team-resolver.ts) -- Input: "ABC" → Try team key first, then team name → Return team UUID +- Input: "ABC" → Try team key first, fall back to team name → Return team UUID + +**Cycle Resolution** (src/resolvers/cycle-resolver.ts) + +- Input: "Sprint 1" → Query cycles by name → Disambiguate by active/next/previous → Return UUID ### GraphQL Optimization Pattern -**Single Query Strategy** (src/utils/graphql-issues-service.ts lines 32-46) +**Single Query Strategy** (all services) ```typescript // Replaces 1 + (5 × N) API calls with single GraphQL query -const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, { - first: limit, - orderBy: "updatedAt" as any, -}); +const result = await client.request<GetIssuesQuery>( + GetIssuesDocument, + { first: limit, orderBy: "updatedAt" } +); ``` -**Batch Resolution Pattern** (src/utils/graphql-issues-service.ts lines 149-153) +**Typed Operations** -```typescript -// Single query to resolve all IDs (labels, projects, teams) -const resolveResult = await this.graphQLService.rawRequest( - BATCH_RESOLVE_FOR_UPDATE_QUERY, - resolveVariables, -); -``` +All GraphQL operations use codegen types: +- Import `DocumentNode` from `src/gql/graphql.ts` +- Pass to `client.request<ResultType>(Document, variables)` +- Get fully typed results This eliminates N+1 query problems by using GraphQL's ability to fetch complex relationships in single requests. + +## Architectural Benefits + +1. **No Code Duplication** - ID resolution logic centralized in resolvers +2. **Type Safety** - No `any` types, everything derived from GraphQL schema +3. **Testability** - Pure functions at every layer, easy to unit test +4. **Maintainability** - Clear separation of concerns, easy to locate logic +5. **Performance** - Single-query fetches, batch operations via GraphQL +6. **Developer Experience** - Functions over classes, simple imports, clear data flow diff --git a/docs/build-system.md b/docs/build-system.md index 617dbbc..6b2c49e 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -1,149 +1,189 @@ -<!-- Generated: 2025-08-31T18:51:03+02:00 --> - # Build System -Linearis uses a TypeScript compilation-based build system optimized for both development productivity and production performance. The project features automatic builds during installation, and cross-platform clean scripts. - -The build system leverages TypeScript's compiler for production builds while maintaining tsx for development convenience. All builds output to the dist/ directory with automated preparation during npm install, ensuring consistent deployment across platforms. - -## Build Workflows - -### Production Build Process - -**TypeScript Compilation** - tsconfig.json (lines 7-8) outputs to dist/: - -```bash -npm run build -# Executes: tsc (compiles src/ → dist/) -``` - -**Automated Build During Install** - package.json (line 13): +Linearis uses TypeScript compilation for production builds, GraphQL code generation for type-safe API access, and Vitest for testing. Development runs directly via tsx without a compilation step. -```bash -npm install # Automatically runs prepare script -# Executes: npm run clean && npm run build -``` - -**Cross-Platform Clean** - package.json (line 12): - -```bash -npm run clean -# Executes: node -e "require('fs').rmSync('dist', {recursive: true, force: true})" -``` - -### Development Execution - -**Development Command** - package.json (line 14): +## Prerequisites -```bash -npm start <command> -# Executes: tsx src/main.ts <command> -``` +- **Node.js >= 22.0.0** -- required for ES module support and modern language features +- **mise** (optional) -- manages tool versions via `mise.toml`; run `mise install` to set up Node.js 22 and Deno 2.2.8 automatically -**Production Execution** - package.json (lines 5, 8): +## Getting Started ```bash -node dist/main.js <command> +npm install # Install dependencies and run GraphQL codegen (postinstall hook) +npm start # Run in development mode (regenerates types, then executes via tsx) ``` -### Package Management Workflows +After `npm install`, the project is ready for development. The `postinstall` hook runs GraphQL codegen automatically, so `src/gql/graphql.ts` is always up to date. -**Installation with Build**: +## GraphQL Code Generation -```bash -npm install # Install dependencies and automatically build -npm update # Update to latest versions within constraints -``` +The project uses [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) to produce TypeScript types and typed document nodes from `.graphql` files. -**Dependency Management** +**How it works:** -- **Runtime dependencies** - package.json (lines 24-27) - @linear/sdk, commander -- **Development dependencies** - package.json (lines 28-32) - @types/node, tsx, typescript +1. GraphQL queries and mutations are defined in `graphql/**/*.graphql`. +2. Running `npm run generate` introspects the Linear API schema and generates typed output into `src/gql/`. +3. Services import the generated `DocumentNode` constants and result types from `src/gql/graphql.ts`. -### Development Environment Setup +**When codegen runs automatically:** -**Environment Tool Configuration** - mise.toml (lines 1-3) - -```bash -mise install # Install Node.js 22 and Deno 2.2.8 -mise use # Activate configured tool versions -``` +- On `npm install` (postinstall hook) +- On `npm start` (prestart hook) -## Platform Setup +**Configuration:** `codegen.config.ts` -- uses the `client` preset with fragment masking disabled, pointing at the Linear API schema. -### Node.js Requirements +> **Important:** Never edit files in `src/gql/` by hand. They are regenerated and any manual changes will be lost. -**Version Constraint** - package.json (lines 11-13) +## Usage Documentation Generation -- Node.js >= 22.0.0 required for ES modules and modern features -- TypeScript 5.0.0 for latest language features +The project auto-generates token-optimized usage documentation for LLM agents. -### TypeScript Configuration +**How it works:** -**Build Configuration** - tsconfig.json (lines 2-16): +1. Each command file exports a `DomainMeta` object with domain name, summary, context, arguments, and cross-references. +2. Running `npm run generate:usage` executes `linearis usage --all` and captures output to `USAGE.md`. +3. The generated file contains two tiers: overview (~200 tokens) + per-domain detail (~300-500 tokens each). -- Target: ES2023 with modern Node.js features -- Module: ESNext with ES modules output -- Output: dist/ directory with declaration files -- Optimization: Remove comments, no source maps for production +**When usage generation runs automatically:** -**Module System** - package.json (line 6): +- On `npm run build` (prebuild hook) +- Before publishing (via prebuild in prepublishOnly chain) -- ES modules enabled with "type": "module" -- Binary points to compiled dist/main.js (line 8) -- All imports use .js extensions for ES module compatibility +**Generated output:** `USAGE.md` -- Token-optimized usage documentation committed to the repository and shipped with the package. Typical agent cost: overview + 1 domain = ~500-700 tokens (vs ~3000+ for traditional help text). -### Package Manager Lock +> **Important:** USAGE.md is auto-generated. Edit `DomainMeta` objects in command files instead. The file is regenerated on every build. -**Reproducible Builds** - package-lock.json +## Build Workflows -- Exact dependency versions locked for consistent installations +### Development -## Reference +```bash +npm start <command> # Runs codegen, then executes src/main.ts via tsx +``` -### Build Targets and Commands +tsx provides on-the-fly TypeScript execution without a separate compilation step. Startup is slower than compiled output (~0.64s vs ~0.15s) but avoids the build cycle during development. -| Command | File Reference | Purpose | -| ----------------- | -------------------- | ----------------------------------------- | -| `npm run build` | package.json line 11 | Compile TypeScript to JavaScript (tsc) | -| `npm run clean` | package.json line 12 | Remove dist/ directory (cross-platform) | -| `npm run prepare` | package.json line 13 | Auto-build during install (clean + build) | -| `npm start` | package.json line 14 | Development execution with tsx | -| `npm test` | package.json line 15 | Run test suite | +### Production Build -### Configuration Files +```bash +npm run build # Compiles TypeScript to dist/ and marks dist/main.js as executable +``` -- **package.json** - Main project configuration with dependencies, scripts, and binary setup -- **tsconfig.json** - TypeScript compilation configuration targeting ES2023 -- **package-lock.json** - Dependency lock file for reproducible builds -- **mise.toml** - Development environment tool versions +The compiled binary entry point is `dist/main.js`: -### Troubleshooting Build Issues +```bash +node dist/main.js <command> +``` -**Build Failures** - TypeScript compilation errors: +### Clean ```bash -# Clean and rebuild -npm run clean -npm run build +npm run clean # rm -rf dist/ ``` -**Performance Comparison** - Execution timing: +### Publishing -- Compiled JavaScript: ~0.15s startup (production) -- tsx TypeScript: ~0.64s startup (development only) +```bash +npm publish # Triggers prepublishOnly: build, test, and verify dist/main.js is executable +``` -**Import Resolution** - All imports in TypeScript files use .js extensions: +## Testing -- src/main.ts imports use .js extensions for ES modules compatibility -- TypeScript compiler resolves .js → .ts during compilation +Linearis uses [Vitest](https://vitest.dev) for unit and integration tests. Test files live in `tests/` and follow the pattern `tests/**/*.test.ts`. -**Missing dist/ Directory**: +```bash +npm test # Run all tests once +npm run test:watch # Run tests in watch mode +npm run test:ui # Open the Vitest browser UI +npm run test:coverage # Run tests with V8 coverage reporting +npm run test:commands # Run command coverage analysis +``` -- Run `npm run prepare` to build after fresh clone -- dist/ directory auto-created during npm install +**Configuration:** `vitest.config.ts` -- uses the Node environment with V8 coverage. Coverage reports are generated in text, JSON, and HTML formats. Source files in `src/` are included; declaration files, `src/main.ts`, and `dist/` are excluded from coverage. + +## Scripts Reference + +| Script | Command | Purpose | +|---|---|---| +| `build` | `tsc && chmod +x dist/main.js` | Compile TypeScript and make entry point executable | +| `clean` | `rm -rf dist/` | Remove compiled output | +| `start` | `tsx src/main.ts` | Run in development mode | +| `test` | `vitest run` | Run test suite once | +| `test:watch` | `vitest` | Run tests in watch mode | +| `test:ui` | `vitest --ui` | Open Vitest browser UI | +| `test:coverage` | `vitest run --coverage` | Run tests with coverage | +| `test:commands` | `tsx tests/command-coverage.ts` | Check command test coverage | +| `generate` | `graphql-codegen --config codegen.config.ts` | Generate TypeScript types from GraphQL | +| `generate:usage` | `tsx src/main.ts usage --all > USAGE.md` | Generate token-optimized usage documentation | +| `prebuild` | `npm run generate && npm run generate:usage` | Auto-run codegen and usage generation before build | +| `prestart` | `npm run generate` | Auto-run codegen before `npm start` | +| `postinstall` | `npm run generate` | Auto-run codegen after `npm install` | +| `prepublishOnly` | `npm run build && npm run test && test -x dist/main.js` | Validate before publish | + +## Configuration Files + +| File | Purpose | +|---|---| +| `package.json` | Project metadata, scripts, and dependencies | +| `tsconfig.json` | TypeScript compiler options (ES2022 target, ESNext modules, strict mode, output to `dist/`) | +| `codegen.config.ts` | GraphQL Code Generator configuration (Linear API schema, client preset) | +| `vitest.config.ts` | Vitest test runner and coverage settings | +| `mise.toml` | Development tool versions (Node.js 22, Deno 2.2.8) | + +## Dependencies + +### Runtime + +| Package | Version | Purpose | +|---|---|---| +| `@linear/sdk` | ^58.1.0 | Linear API SDK for ID resolution | +| `commander` | ^14.0.0 | CLI argument parsing | + +### Development + +| Package | Version | Purpose | +|---|---|---| +| `@graphql-codegen/cli` | ^6.1.1 | GraphQL code generation CLI | +| `@graphql-codegen/client-preset` | ^5.2.2 | Typed document node generation | +| `@graphql-codegen/introspection` | 5.0.0 | Schema introspection plugin | +| `@graphql-codegen/schema-ast` | ^5.0.0 | Schema AST generation | +| `@types/node` | ^22.0.0 | Node.js type definitions | +| `@vitest/coverage-v8` | ^2.1.8 | V8-based code coverage | +| `@vitest/ui` | ^2.1.8 | Browser-based test UI | +| `tsx` | ^4.20.5 | TypeScript execution for development | +| `typescript` | ^5.0.0 | TypeScript compiler | +| `vitest` | ^2.1.8 | Test runner | + +## TypeScript Configuration + +Key `tsconfig.json` settings: + +- **Target:** ES2022 +- **Module system:** ESNext with Node module resolution +- **Strict mode:** Enabled +- **Output directory:** `dist/` +- **Source maps:** Disabled (production builds only) +- **Comments:** Stripped from output +- **Excluded from compilation:** `node_modules`, `dist`, `tests`, test files, `vitest.config.ts` + +All imports use `.js` extensions for ES module compatibility. TypeScript resolves `.js` to `.ts` during compilation. + +## Troubleshooting + +**TypeScript errors after changing GraphQL files:** +Run `npm run generate` to regenerate types, then rebuild. + +**Missing `src/gql/graphql.ts`:** +Run `npm run generate` or `npm install` (the postinstall hook handles this). + +**Build failures:** +```bash +npm run clean && npm run generate && npm run build +``` -**Node.js Version Issues** +**Node.js version issues:** +Verify you are running Node.js >= 22 with `node --version`. Use mise (`mise install`) or nvm to manage versions. -- Verify Node.js >= 22.0.0 with `node --version` -- Use mise or nvm to manage Node.js versions +**Missing `dist/` directory:** +Run `npm run build`. The `dist/` directory is not checked into version control. diff --git a/docs/deployment.md b/docs/deployment.md index 898e77d..3c87c1b 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,169 +1,110 @@ -<!-- Generated: 2025-08-31T18:51:03+02:00 --> - # Deployment -Linearis deploys as a compiled Node.js application with automatic builds during installation. Distribution supports npm packages, git-based installation, and standalone executables with automated TypeScript compilation ensuring consistency across platforms. - -The deployment strategy leverages npm's prepare script for automatic builds, compiled JavaScript for production performance, and cross-platform clean scripts for reliable distribution. All installations automatically compile TypeScript to optimized JavaScript in the dist/ directory. +Linearis is a CLI tool for Linear.app that compiles from TypeScript to JavaScript during installation. It runs on Node.js 22+ and outputs JSON for all commands. -## Package Types +## Installation -### Git-Based Installation +### From Git -**Direct Repository Install with Auto-Build** - Primary deployment method: +Clone and install: ```bash -npm install git+https://github.com/czottmann/linearis.git -# Automatically runs prepare script: clean + build -# Creates dist/ with compiled JavaScript +git clone https://github.com/czottmann/linearis.git +cd linearis +npm install ``` -**Development Clone** - For local development: +`npm install` handles the full setup automatically: -```bash -git clone <repository> -cd linearis -npm install # Auto-builds via prepare script -``` +- `postinstall` runs `npm run generate` (GraphQL codegen) +- `prepare` is not used; build manually with `npm run build` -**Global CLI Access** - package.json (lines 5, 8): +After building, link the CLI globally: ```bash -npm link # Creates global 'linear' command -# Uses main: "dist/main.js" and bin: "dist/main.js" +npm run build +npm link ``` -### Package Distribution Options - -**NPM Package** - package.json configured for npm publishing: +This creates the `linearis` command, pointing to `dist/main.js`. -- Name: "linearis" (line 2) -- Version: "1.0.0" (line 3) -- Author: "Carlo Zottmann <carlo@zottmann.dev>" (line 15) -- License: "MIT" (line 16) - -**Standalone Executable** - Using compiled JavaScript: +### Direct Git Install ```bash -# Create standalone binary from compiled output -npx pkg dist/main.js --targets node22-linux-x64,node22-macos-x64,node22-win-x64 +npm install git+https://github.com/czottmann/linearis.git ``` -## Platform Deployment +This runs `postinstall` to generate GraphQL types. You still need to run `npm run build` separately to compile TypeScript. -### Cross-Platform Compatibility +## Build Scripts -**Node.js Runtime** - package.json (lines 11-13): +| Command | Description | +| ------------------ | ------------------------------------------ | +| `npm run generate` | Generate GraphQL types from schema | +| `npm run build` | Compile TypeScript and make entry executable | +| `npm run clean` | Remove `dist/` directory (`rm -rf dist/`) | +| `npm run start` | Run in development mode via tsx | +| `npm test` | Run test suite | -- Requires Node.js >= 22.0.0 on all platforms -- ES modules support ensures modern JavaScript compatibility +The build script runs `tsc && chmod +x dist/main.js`. The clean script uses `rm -rf dist/`. -**File System Dependencies**: +## Authentication -- Authentication file: `$HOME/.linear_api_token` (src/utils/auth.ts line 30) -- Works on Windows (`%USERPROFILE%`), macOS/Linux (`$HOME`) +For interactive use (humans), run `linearis auth login` — it opens Linear in the browser and stores the token encrypted in `~/.linearis/token`. -### Environment Setup +Linearis checks for an API token in this order: -**Development Environment** - mise.toml configuration: +1. `--api-token` flag on the command line +2. `LINEAR_API_TOKEN` environment variable +3. `~/.linearis/token` (encrypted, set up via `linearis auth login`) +4. `~/.linear_api_token` (deprecated) -```bash -# Install development tools -mise install # Installs Node.js 22 and Deno 2.2.8 -``` +For automated environments (CI, containers), set the environment variable. -**Production Environment**: +Authentication is handled in `src/common/auth.ts` and `src/common/token-storage.ts`. -```bash -# Minimal production setup -node --version # Verify >= 22.0.0 -npm --version # Verify npm available -``` +## Platform Requirements -### Container Deployment +- Node.js >= 22.0.0 +- ES modules support (package uses `"type": "module"`) +- Works on Linux, macOS, and Windows +- The token file path resolves via `os.homedir()`, so it works across platforms (`$HOME` on Unix, `%USERPROFILE%` on Windows) -**Docker Option** - Optimized Dockerfile with build: +## Container Deployment + +Example Dockerfile: ```dockerfile FROM node:22-alpine WORKDIR /app -COPY package.json package-lock.json tsconfig.json ./ +COPY package.json package-lock.json tsconfig.json codegen.config.ts ./ COPY src/ ./src/ -RUN npm install # Auto-builds via prepare script +COPY graphql/ ./graphql/ +RUN npm install ENTRYPOINT ["node", "dist/main.js"] ``` -## Reference - -### Deployment Scripts and Commands +`npm install` triggers `postinstall` (which runs `npm run generate`). The `graphql/` directory is required because codegen reads the query and mutation definitions from it. Run `npm run build` separately to compile TypeScript. -**Installation Commands**: +Pass the API token as an environment variable: -| Command | Purpose | File Reference | -| ------------------- | -------------------------------- | ---------------------- | -| `npm install` | Install + auto-build via prepare | package.json scripts | -| `npm run build` | Manual TypeScript compilation | package.json line 11 | -| `npm link` | Global CLI access (compiled) | package.json bin field | -| `node dist/main.js` | Direct production execution | Compiled output | - -### Distribution Formats - -**Current Format** - Compiled distribution: - -- TypeScript source files in src/ directory -- Automated compilation to dist/ during install -- Production execution via `node dist/main.js` - -**Distribution Methods**: - -- Git install with auto-build: `npm install git+https://...` - -### Configuration Files for Deployment - -**Runtime Configuration**: - -- **package.json** - Dependencies, scripts, binary configuration, and prepare script -- **tsconfig.json** - TypeScript compilation settings for production build -- **package-lock.json** - Exact dependency versions for reproducible builds -- **dist/main.js** - Compiled entry point for production execution - -**Environment Configuration**: - -- **mise.toml** - Development environment tools (not needed for production) -- **LINEAR_API_TOKEN** - Environment variable for authentication -- **~/.linear_api_token** - File-based authentication option - -### Authentication in Deployment - -**Production Authentication** - src/utils/auth.ts (lines 18-38): - -1. **Container/CI**: Use `LINEAR_API_TOKEN` environment variable -2. **Server**: Place token in `/home/user/.linear_api_token` file -3. **Desktop**: Use `--api-token` flag for interactive use +```bash +docker build -t linearis . +docker run -e LINEAR_API_TOKEN=lin_api_... linearis issue list +``` -### Performance Considerations +## Troubleshooting -**Runtime Performance** - Compilation benchmarks: +**Missing `dist/` directory** -- Run `npm run build` to compile TypeScript. -- Compiled JavaScript startup: ~0.15s -- Development tsx startup: ~0.64s (development only) -- Production runtime: Sub-second for most operations -- Memory usage: Minimal Node.js footprint +**GraphQL type errors after schema changes** -- Run `npm run generate` to regenerate types. -**Deployment Size**: +**Node.js version mismatch** -- Verify you have Node.js 22.0.0 or later with `node --version`. -- Source code: ~50KB TypeScript files -- Compiled output: ~40KB JavaScript files in dist/ -- Dependencies: ~10-20MB node_modules (runtime only) -- Full installation: ~25MB including dev dependencies +**Command not found after `npm link`** -- Make sure `npm run build` completed successfully and `dist/main.js` exists. -### Troubleshooting Deployment +**Authentication failures** -- Confirm your Linear API token is valid and provided through one of the three supported methods. -**Common Issues**: +## Version -- Missing dist/ directory: Run `npm install` to trigger prepare script -- Build failures: Check TypeScript compilation with `npm run build` -- Node.js version incompatibility: Verify >= 22.0.0 requirement -- Binary not found: Ensure package.json bin points to `dist/main.js` -- Authentication failures: Verify Linear API token is valid and has required permissions -- Performance issues: Use compiled `node dist/main.js` instead of `tsx src/main.ts` +Current version: 2025.12.3 (defined in `package.json`). diff --git a/docs/development.md b/docs/development.md index c77f59c..21308ce 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,316 +1,395 @@ -<!-- Generated: 2025-01-09T12:34:56+00:00 --> - # Development -Linearis follows TypeScript-first development practices with strict typing, modular architecture, and GraphQL-optimized design patterns. Development emphasizes code clarity, maintainability, and efficient GraphQL operations for optimal Linear integration performance. - -The codebase uses modern ES modules, async/await patterns throughout, and leverages TypeScript's type system for compile-time safety. All development follows the principle of smart defaults with explicit user control when needed. Recent optimization work focuses on replacing SDK-heavy operations with direct GraphQL queries. - -## Code Style +Linearis is a CLI tool for [Linear.app](https://linear.app) that outputs structured JSON. It uses a layered architecture with strict TypeScript, GraphQL code generation, and ES modules. -### TypeScript Standards +## Prerequisites -**Strict Typing** - All files use comprehensive TypeScript interfaces: +- Node.js >= 22.0.0 +- A Linear API token (see [Authentication](#authentication)) -```typescript -// From src/utils/linear-types.d.ts lines 1-41 -export interface LinearIssue { - id: string; - identifier: string; - title: string; - description?: string; - state: { id: string; name: string }; - // ... complete type definitions -} -``` +## Getting Started -**Interface-Driven Development** - src/utils/linear-types.d.ts (lines 63-96): +```bash +# Install dependencies (also runs GraphQL codegen) +npm install -- CreateIssueArgs interface for issue creation parameters -- UpdateIssueArgs interface for issue updates -- SearchIssuesArgs interface for search operations +# Run in development mode (uses tsx) +npm start issues list -l 5 -### Async/Await Patterns +# Run with explicit token +npx tsx src/main.ts --api-token <token> issues list -**Consistent Promise Handling** - Throughout src/utils/linear-service.ts: +# Build for production +npm run build -```typescript -// Example from lines 128-137 - Parallel API calls -const [state, team, assignee, project, labels] = await Promise.all([ - issue.state, - issue.team, - issue.assignee, - issue.project, - issue.labels(), -]); +# Run tests +npm test ``` -**Error Handling Pattern** - src/utils/output.ts (lines 23-33): +## Architecture Overview -```typescript -export function handleAsyncCommand( - asyncFn: (...args: any[]) => Promise<void>, -): (...args: any[]) => Promise<void> { - return async (...args: any[]) => { - try { - await asyncFn(...args); - } catch (error) { - outputError(error instanceof Error ? error : new Error(String(error))); - } - }; -} +The codebase is organized into five layers, each with a single responsibility: + +``` +CLI Input --> Command --> Resolver --> Service --> JSON Output + | | + SDK client GraphQL client + (ID lookup) (data operations) ``` -### ES Modules Convention +| Layer | Directory | Client | Responsibility | +|-------|-----------|--------|----------------| +| Client | `src/client/` | -- | API client wrappers | +| Resolver | `src/resolvers/` | `LinearSdkClient` | Convert human IDs to UUIDs | +| Service | `src/services/` | `GraphQLClient` | Business logic and CRUD | +| Command | `src/commands/` | Both (via `createContext()`) | CLI orchestration | +| Common | `src/common/` | -- | Shared utilities and types | -**Import/Export Style** - All files use ES module syntax: +Two separate clients exist because the Linear SDK is convenient for ID lookups (resolvers), while direct GraphQL queries are more efficient for data operations (services). Commands get both clients through `createContext()`. -- src/main.ts (lines 3-5) - Named imports with .js extensions -- src/utils/auth.ts (lines 18, 38) - Interface exports and async functions -- All imports use .js extensions for ES module compatibility +## Code Style -## Common Patterns +### TypeScript Rules -### Command Setup Pattern +- **No `any` types.** Use `unknown`, codegen types, or explicit interfaces. +- **Strict mode** is enabled in tsconfig.json. +- **Explicit return types** on all exported functions. +- **ES module imports** use `.js` extensions, even when importing `.ts` files. -**Commander.js Integration** - src/commands/issues.ts (lines 9-16): +### Functions Over Classes + +Resolvers and services are stateless exported functions, not class methods. This keeps them simple and easy to test. ```typescript -export function setupIssuesCommands(program: Command): void { - const issues = program.command("issues") - .description("Issue operations"); +// Good: plain function +export async function listIssues(client: GraphQLClient, limit?: number): Promise<Issue[]> { ... } - // Show help when no subcommand - issues.action(() => { - issues.help(); - }); +// Avoid: class with methods +class IssueService { async listIssues(...) { ... } } ``` -### Service Layer Pattern +## Patterns + +### Command Pattern -**Authentication Integration** - src/utils/linear-service.ts (lines 479-484): +Commands are thin orchestration layers. They create the client context, resolve IDs, call services, and output results. No business logic belongs here. ```typescript -export async function createLinearService( - options: CommandOptions, -): Promise<LinearService> { - const apiToken = await getApiToken(options); - return new LinearService(apiToken); +import { Command } from "commander"; +import { createContext } from "../common/context.js"; +import { handleCommand, outputSuccess } from "../common/output.js"; +import { resolveTeamId } from "../resolvers/team-resolver.js"; +import { createIssue } from "../services/issue-service.js"; + +export function setupIssuesCommands(program: Command): void { + const issues = program.command("issues"); + + issues + .command("create <title>") + .option("--team <id>", "Team key, name, or UUID") + .action(handleCommand(async (title, options, command) => { + const ctx = await createContext(command.parent!.parent!.opts()); + const teamId = options.team + ? await resolveTeamId(ctx.sdk, options.team) + : undefined; + const result = await createIssue(ctx.gql, { title, teamId }); + outputSuccess(result); + })); } ``` -### Smart ID Resolution Pattern +Every `.action()` handler must be wrapped with `handleCommand()`, which catches errors and outputs them as JSON. -**UUID Validation Helper** - src/utils/uuid.ts: +Register new command groups in `src/main.ts`: ```typescript -// Generic UUID validation using proper regex -export function isUuid(value: string): boolean { - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - return uuidRegex.test(value); -} +import { setupEntityCommands } from "./commands/entity.js"; +setupEntityCommands(program); ``` -**Flexible Identifier Handling** - src/utils/linear-service.ts (lines 196-227): +### Resolver Pattern + +Resolvers convert human-friendly identifiers (team keys, names, issue identifiers like `ENG-123`) into UUIDs. They use the `LinearSdkClient` and live in `src/resolvers/`. ```typescript -// Check if UUID or identifier format using helper -if (isUuid(issueId)) { - issue = await this.client.issue(issueId); -} else { - // Parse team-number format like "ABC-123" - const parts = issueId.split("-"); - // ... resolve to internal UUID +import type { LinearSdkClient } from "../client/linear-client.js"; +import { isUuid } from "../common/identifier.js"; + +export async function resolveTeamId( + client: LinearSdkClient, + keyOrNameOrId: string, +): Promise<string> { + if (isUuid(keyOrNameOrId)) return keyOrNameOrId; + + const byKey = await client.sdk.teams({ + filter: { key: { eq: keyOrNameOrId } }, + first: 1, + }); + if (byKey.nodes.length > 0) return byKey.nodes[0].id; + + const byName = await client.sdk.teams({ + filter: { name: { eq: keyOrNameOrId } }, + first: 1, + }); + if (byName.nodes.length > 0) return byName.nodes[0].id; + + throw new Error(`Team "${keyOrNameOrId}" not found`); } ``` -### GraphQL Optimization Pattern +Rules for resolvers: +- Always accept a UUID passthrough as the first check. +- Return a UUID string, never an object. +- Use `LinearSdkClient` only (not `GraphQLClient`). +- No CRUD operations or data transformations. + +### Service Pattern -**Single Query Strategy** - Used throughout GraphQL service layer: +Services contain business logic and perform CRUD operations using the `GraphQLClient`. They accept pre-resolved UUIDs -- never human-friendly identifiers. ```typescript -// From src/utils/graphql-issues-service.ts lines 32-46 -async getIssues(limit: number = 25): Promise<LinearIssue[]> { - const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, { +import type { GraphQLClient } from "../client/graphql-client.js"; +import { + GetIssuesDocument, + type GetIssuesQuery, + CreateIssueDocument, + type CreateIssueMutation, + type IssueCreateInput, +} from "../gql/graphql.js"; + +export async function listIssues( + client: GraphQLClient, + limit: number = 25, +): Promise<Issue[]> { + const result = await client.request<GetIssuesQuery>(GetIssuesDocument, { first: limit, - orderBy: "updatedAt" as any, }); - // Complete data in single response - no N+1 queries + return result.issues.nodes; } -``` -**Batch Resolution Pattern** - Resolve multiple IDs in single operation: - -```typescript -// From src/utils/graphql-issues-service.ts lines 294-301 -const resolveResult = await this.graphQLService.rawRequest( - BATCH_RESOLVE_FOR_CREATE_QUERY, - { teamName, projectName, labelNames }, -); -// All IDs resolved in single query +export async function createIssue( + client: GraphQLClient, + input: IssueCreateInput, +): Promise<CreatedIssue> { + const result = await client.request<CreateIssueMutation>( + CreateIssueDocument, + { input }, + ); + return result.issueCreate.issue; +} ``` -**Enhanced Label Management** - Supporting both adding and overwriting modes: +Rules for services: +- Use `GraphQLClient` only (not `LinearSdkClient`). +- Accept UUIDs, not human-friendly identifiers. +- Import `DocumentNode` constants and types from `src/gql/graphql.js`. +- Always type the `client.request<T>()` call. -```typescript -// From src/utils/graphql-issues-service.ts lines 188-196 -if (labelMode === "adding") { - // Merge with current labels - finalLabelIds = [...new Set([...currentIssueLabels, ...resolvedLabels])]; -} else { - // Overwrite mode - replace all existing labels - finalLabelIds = resolvedLabels; -} -``` +## GraphQL Workflow -## Workflows +Linearis uses [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) to produce typed query documents and result types. Never write raw GraphQL strings in TypeScript. -### Adding New Commands +### Adding or Changing a Query -1. **Define Interfaces** - Add to src/utils/linear-types.d.ts -2. **Create GraphQL Queries** - Add optimized queries to src/queries/ -3. **Implement GraphQL Service Methods** - Add to src/utils/graphql-issues-service.ts or create new GraphQL service -4. **Create Command Handler** - Add to appropriate src/commands/ file -5. **Register Command** - Import and setup in src/main.ts +1. **Edit the `.graphql` file** in `graphql/queries/` or `graphql/mutations/`: -### GraphQL Development Workflow + ```graphql + # graphql/queries/issues.graphql + query GetIssues($first: Int) { + issues(first: $first, orderBy: updatedAt) { + nodes { + id + identifier + title + ... + } + } + } + ``` -1. **Design Query Strategy** - Single query vs batch resolution approach -2. **Create Query Fragments** - Reuse existing fragments from src/queries/common.ts -3. **Implement Service Method** - Use GraphQLService for raw execution -4. **Add Error Handling** - Transform GraphQL errors to user-friendly messages -5. **Test Performance** - Compare against SDK-based approach for improvements +2. **Run code generation:** -**Example Command Addition Pattern** - src/commands/issues.ts (lines 138-152): + ```bash + npm run generate + ``` -```typescript -issues.command("read <issueId>") - .description( - "Get issue details (supports both UUID and identifier like ABC-123)", - ) - .action( - handleAsyncCommand( - async (issueId: string, options: any, command: Command) => { - const service = await createLinearService( - command.parent!.parent!.opts(), - ); - const result = await service.getIssueById(issueId); - outputSuccess(result); - }, - ), - ); -``` + This regenerates `src/gql/graphql.ts`. Do not edit that file by hand. -### Development Server Setup +3. **Import and use in a service:** -**Development Mode** - package.json (line 14): + ```typescript + import { + GetIssuesDocument, // DocumentNode constant + type GetIssuesQuery, // Result type + } from "../gql/graphql.js"; -```bash -# Run with TypeScript execution via tsx (development only) -npm start issues list -l 5 + const result = await client.request<GetIssuesQuery>( + GetIssuesDocument, + { first: 10 }, + ); + ``` -# Direct execution for debugging -npx tsx src/main.ts --api-token <token> issues read ABC-123 +### File Layout + +``` +graphql/ + queries/ # .graphql query definitions + mutations/ # .graphql mutation definitions +src/gql/ # Generated output (DO NOT EDIT) ``` -**Production Build Workflow**: +## Error Handling -```bash -# Clean and compile for production -npm run clean && npm run build +### In Commands -# Test compiled output (creates executable dist/main.js) -chmod +x dist/main.js -./dist/main.js issues list -l 5 +Use the `handleCommand()` wrapper. It catches any thrown error and outputs it as JSON to stderr before exiting with code 1. No manual try/catch is needed in command handlers. -# Time comparison (compiled is significantly faster) -time ./dist/main.js --help -time npx tsx src/main.ts --help -``` +### In Resolvers and Services -### Authentication Development +Throw descriptive errors using the helpers from `src/common/errors.ts`: -**Multiple Token Sources** - src/utils/auth.ts (lines 18-38): +```typescript +import { notFoundError, multipleMatchesError } from "../common/errors.js"; -1. Command flag: `--api-token <token>` -2. Environment: `LINEAR_API_TOKEN=<token>` -3. File: `echo "<token>" > ~/.linear_api_token` +// Entity not found +throw notFoundError("Team", "ABC-123"); -### Error Handling Development +// Ambiguous match +throw multipleMatchesError("Cycle", "Sprint 1", ["id1", "id2"], "specify a team with --team"); -**Consistent Error Response** - src/utils/output.ts (lines 13-16): +// Invalid input +throw invalidParameterError("priority", "must be between 0 and 4"); -```typescript -export function outputError(error: Error): void { - console.error(JSON.stringify({ error: error.message }, null, 2)); - process.exit(1); -} +// Missing required companion flag +throw requiresParameterError("--cycle", "--team"); ``` -## Reference +### Output Format + +All command output is JSON: + +```typescript +// Success: written to stdout +outputSuccess(data); // JSON.stringify(data, null, 2) + +// Error: written to stderr, exits with code 1 +outputError(error); // { "error": "message" } +``` -### File Organization Patterns +## Authentication -**Service Layer** - `src/utils/` directory: +For interactive setup, run `linearis auth login` — it opens Linear in the browser and stores the token encrypted in `~/.linearis/token`. -- graphql-service.ts - GraphQL client wrapper with batch operations -- graphql-issues-service.ts - Optimized GraphQL issue operations -- linear-service.ts - Legacy SDK-based business logic and fallback operations -- auth.ts - Authentication handling -- output.ts - Response formatting -- linear-types.d.ts - Type definitions -- uuid.ts - UUID validation utilities +The API token is resolved in this order: -**Command Layer** - `src/commands/` directory: +1. `--api-token <token>` command-line flag +2. `LINEAR_API_TOKEN` environment variable +3. `~/.linearis/token` (encrypted, set up via `linearis auth login`) +4. `~/.linear_api_token` (deprecated) -- issues.ts - Issue-related commands with enhanced label and parent management -- projects.ts - Project-related commands -- comments.ts - Comment operations with lightweight ID resolution -- teams.ts - Team operations (list) with workspace team discovery -- users.ts - User operations (list) with active user filtering -- Pattern: Each domain gets its own command file +For local development, the interactive login is the most convenient: -**Query Layer** - `src/queries/` directory: +```bash +linearis auth login +``` -- common.ts - Reusable GraphQL fragments -- issues.ts - Optimized issue-specific GraphQL queries and mutations -- index.ts - Query exports and organization +## Adding New Functionality -### Naming Conventions +A typical feature addition touches four layers. Here is the sequence: -**Functions** - camelCase with descriptive names: +1. **GraphQL operations** -- Define queries and mutations in `graphql/queries/` or `graphql/mutations/`, then run `npm run generate`. -- `getApiToken()`, `createLinearService()`, `handleAsyncCommand()` -- Service methods: `getIssues()`, `searchIssues()`, `createIssue()` +2. **Resolver** (if new entity types need ID resolution) -- Add a `resolve*Id()` function in `src/resolvers/`. Use `LinearSdkClient`, return a UUID string. -**Interfaces** - PascalCase with descriptive prefixes: +3. **Service** -- Add functions in `src/services/`. Use `GraphQLClient`, accept UUIDs, import codegen types. -- `LinearIssue`, `LinearProject` for data models -- `CreateIssueArgs`, `UpdateIssueArgs` for operation parameters +4. **Command** -- Add a `setup*Commands()` function in `src/commands/`. Use `createContext()`, resolve IDs, call services, output with `outputSuccess()`. Register in `src/main.ts`. -### Development Best Practices +5. **Tests** -- Add unit tests in `tests/unit/` mirroring the source structure. Mock one layer deep (see [testing docs](testing.md)). -**Type Safety** - Every function parameter and return type explicitly typed **Error Boundaries** - All async operations wrapped with error handling\ -**GraphQL First** - New operations use GraphQL service for optimal performance **User Experience** - Smart defaults with explicit override options **Build Automation** - npm prepare script ensures consistent builds +## Available Scripts -### Build System Integration +| Script | Description | +|--------|-------------| +| `npm start` | Run in dev mode via tsx (also runs codegen) | +| `npm run build` | Compile TypeScript to `dist/` | +| `npm run clean` | Remove `dist/` | +| `npm test` | Run tests with vitest | +| `npm run test:watch` | Run tests in watch mode | +| `npm run test:coverage` | Run tests with coverage | +| `npm run test:commands` | Check command coverage | +| `npm run generate` | Regenerate GraphQL types | -**Automated Building** - package.json (line 13): +## Project Structure -```bash -# prepare script runs automatically during install -npm install # Triggers: npm run clean && npm run build +``` +src/ + main.ts # Entry point, registers all command groups + client/ + graphql-client.ts # GraphQLClient - direct GraphQL execution + linear-client.ts # LinearSdkClient - SDK wrapper for resolvers + resolvers/ # Human ID to UUID resolution + team-resolver.ts + project-resolver.ts + label-resolver.ts + cycle-resolver.ts + status-resolver.ts + issue-resolver.ts + milestone-resolver.ts + services/ # Business logic and CRUD + issue-service.ts + document-service.ts + attachment-service.ts + milestone-service.ts + cycle-service.ts + team-service.ts + user-service.ts + project-service.ts + label-service.ts + comment-service.ts + file-service.ts + commands/ # CLI command definitions + auth.ts # Authentication (interactive, for humans) + issues.ts + documents.ts + project-milestones.ts + cycles.ts + teams.ts + users.ts + projects.ts + labels.ts + comments.ts + embeds.ts + common/ # Shared utilities + context.ts # CommandContext and createContext() + auth.ts # API token resolution (flag, env, encrypted, legacy) + token-storage.ts # Encrypted token storage + encryption.ts # AES-256-CBC encryption + output.ts # JSON output and handleCommand() + errors.ts # Error factory functions + identifier.ts # UUID validation and issue identifier parsing + types.ts # Type aliases from codegen + embed-parser.ts # Embed extraction utilities + usage.ts # Two-tier usage system (DomainMeta, formatOverview, formatDomainUsage) + gql/ # GraphQL codegen output (DO NOT EDIT) +graphql/ + queries/ # GraphQL query definitions + mutations/ # GraphQL mutation definitions +tests/ + unit/ + resolvers/ # Resolver tests (mock SDK) + services/ # Service tests (mock GraphQL) + common/ # Pure function tests ``` -**TypeScript Configuration** - tsconfig.json optimizations: - -- Target: ES2023 for modern Node.js features -- Output: dist/ directory with declaration files -- Remove comments and source maps for production -- Strict mode enabled for type safety +## Dependencies -### Common Development Issues +**Runtime:** +- `@linear/sdk` -- Linear SDK, used by resolvers for ID lookups +- `commander` -- CLI framework -**ES Module Imports** - Always use .js extensions in imports, even for .ts files **Authentication Testing** - Use token file method for local development **GraphQL vs SDK** - Prefer GraphQL service for new operations, use SDK for fallbacks\ -**API Rate Limits** - Linear API has reasonable limits, but GraphQL batch operations help **Development vs Production** - Use tsx for development, compiled JS for production (significantly faster) **Missing dist/** - Run `npm install` or `npm run build` to create executable compiled output\ -**Build creates executable** - npm run build automatically makes dist/main.js executable +**Development:** +- `typescript` -- Compiler +- `tsx` -- TypeScript execution for development +- `vitest` -- Test runner +- `@graphql-codegen/*` -- GraphQL code generation suite diff --git a/docs/files.md b/docs/files.md index dffd0fc..d634791 100644 --- a/docs/files.md +++ b/docs/files.md @@ -1,163 +1,145 @@ -<!-- Generated: 2025-01-09T12:34:56+00:00 --> +# File Catalog -# Files Catalog +A reference of every file in the Linearis codebase, organized by architectural layer. -Linearis project follows a clean, modular structure with TypeScript source files organized by function. The codebase separates concerns into command handlers, optimized GraphQL service layers, and type definitions, making it easy to locate functionality and understand system relationships. +## Entry Point -All source files use modern ES modules with TypeScript for type safety. The project maintains clear boundaries between CLI interface logic, GraphQL operations, SDK fallback operations, and data access patterns. Configuration and documentation files provide comprehensive project context and development guidance. +- **src/main.ts** -- CLI setup with Commander.js. Registers all command groups and parses global options. -## Core Source Files +## Client Layer (`src/client/`) -### Main Application Logic +Thin wrappers around the Linear API. No business logic. -**src/main.ts** - CLI entry point and program setup with Commander.js framework **src/commands/issues.ts** - Complete issue management commands (list, search, create, read, update) with enhanced label and parent relationship management **src/commands/projects.ts** - Project operations commands (list, read) with simplified interface **src/commands/comments.ts** - Comment operations (create) with lightweight issue ID resolution **src/commands/teams.ts** - Team operations (list) with workspace team discovery **src/commands/users.ts** - User operations (list) with active user filtering **src/commands/embeds.ts** - File download command for Linear uploaded files with signed URL support +- **graphql-client.ts** -- `GraphQLClient` class with a typed `request<TResult>(document: DocumentNode, variables?: Record<string, unknown>)` method for direct GraphQL execution. +- **linear-client.ts** -- `LinearSdkClient` wrapper exposing a readonly `sdk: LinearClient` property for SDK-based lookups. -### Service Layer +## Resolver Layer (`src/resolvers/`) -**src/utils/graphql-service.ts** - GraphQL client wrapper with raw query execution and batch operation support **src/utils/graphql-issues-service.ts** +Each resolver converts a human-friendly identifier (name, key, or slug) into a UUID. Resolvers use `LinearSdkClient` exclusively. -- Optimized GraphQL operations for issues with single-query strategy and batch ID resolution **src/utils/linear-service.ts** - Legacy SDK-based Linear API integration with smart ID resolution and fallback operations **src/utils/auth.ts** - Multi-source authentication handling (API token flag, environment variable, token file) **src/utils/output.ts** - JSON response formatting and standardized error handling with async command wrapping **src/utils/embed-parser.ts** - Markdown parsing for Linear upload URL extraction with embed info and expiration tracking **src/utils/file-service.ts** - Authenticated file download service with signed URL support and smart authentication detection +- **team-resolver.ts** -- `resolveTeamId(client, keyOrNameOrId)` +- **project-resolver.ts** -- `resolveProjectId(client, nameOrId)` +- **label-resolver.ts** -- `resolveLabelId(client, nameOrId)`, `resolveLabelIds(client, namesOrIds)` +- **cycle-resolver.ts** -- `resolveCycleId(client, nameOrId, teamFilter?)` +- **status-resolver.ts** -- `resolveStatusId(client, nameOrId, teamId?)` +- **issue-resolver.ts** -- `resolveIssueId(client, issueIdOrIdentifier)` +- **milestone-resolver.ts** -- `resolveMilestoneId(gqlClient, sdkClient, nameOrId, projectNameOrId?)` -### Type System +## Service Layer (`src/services/`) -**src/utils/linear-types.d.ts** - Complete TypeScript interfaces for Linear entities (LinearIssue, LinearProject) and operation parameters (CreateIssueArgs, UpdateIssueArgs, SearchIssuesArgs) **src/utils/uuid.ts** - UUID validation utilities for smart ID resolution +Business logic and CRUD operations. Services use `GraphQLClient` exclusively and accept pre-resolved UUIDs. -### Query Definitions +- **issue-service.ts** -- `listIssues`, `getIssue`, `searchIssues`, `createIssue`, `updateIssue` +- **document-service.ts** -- `getDocument`, `createDocument`, `updateDocument`, `listDocuments`, `deleteDocument` +- **attachment-service.ts** -- `createAttachment`, `deleteAttachment`, `listAttachments` +- **milestone-service.ts** -- `listMilestones`, `getMilestone`, `createMilestone`, `updateMilestone` +- **cycle-service.ts** -- `listCycles`, `getCycle` +- **team-service.ts** -- `listTeams` +- **user-service.ts** -- `listUsers` +- **project-service.ts** -- `listProjects` +- **label-service.ts** -- `listLabels` +- **comment-service.ts** -- `createComment` +- **file-service.ts** -- File upload and download operations for Linear uploads -**src/queries/common.ts** - Reusable GraphQL fragments for consistent data fetching across operations **src/queries/issues.ts** - Optimized GraphQL queries and mutations for issue operations (get, create, update, search) **src/queries/index.ts** - Query exports and organization +## Command Layer (`src/commands/`) -## Configuration Files +CLI orchestration. Each file registers a command group via a `setup*Commands(program)` function. Commands use `createContext()` to obtain both clients, call resolvers for ID conversion, then delegate to services. -### Package Management +- **auth.ts** -- `auth login`, `auth status`, `auth logout` — interactive authentication (for humans) +- **issues.ts** -- `issue list`, `issue search`, `issue read`, `issue create`, `issue update` +- **documents.ts** -- Document commands with attachment support +- **project-milestones.ts** -- Milestone CRUD commands +- **cycles.ts** -- Cycle listing and detail reading +- **teams.ts** -- Team listing +- **users.ts** -- User listing +- **projects.ts** -- Project listing +- **labels.ts** -- Label listing +- **comments.ts** -- Comment creation +- **embeds.ts** -- File download from Linear upload URLs -**package.json** - Project configuration with dependencies (@linear/sdk, commander, tsx), scripts, and Node.js >= 22.0.0 requirement **package-lock.json** - Dependency lock file ensuring reproducible builds with exact versions **mise.toml** - Development environment configuration with Node.js 22 and Deno 2.2.8 tool versions +## Common Layer (`src/common/`) -### Documentation and Specifications +Shared utilities used across all layers. -**README.md** - User-facing documentation with installation instructions, usage examples, and performance benchmarks **CLAUDE.md** - AI-specific project instructions, architecture overview, and development guidelines for LLM agents **PERFORMANCE.md** - Detailed performance optimization analysis with before/after benchmarks and optimization techniques +- **context.ts** -- `CommandContext` interface and `createContext()` factory that produces both `GraphQLClient` and `LinearSdkClient`. +- **auth.ts** -- `resolveApiToken()` with multi-source lookup: `--api-token` flag, `LINEAR_API_TOKEN` env var, `~/.linearis/token` (encrypted), `~/.linear_api_token` (deprecated). +- **token-storage.ts** -- `saveToken()`, `getStoredToken()`, `clearToken()` for encrypted token storage in `~/.linearis/token`. +- **encryption.ts** -- AES-256-CBC encryption for token storage. +- **output.ts** -- `outputSuccess()`, `outputError()`, and `handleCommand()` wrapper for consistent JSON output and error handling. +- **errors.ts** -- `notFoundError()`, `multipleMatchesError()`, `invalidParameterError()`, `requiresParameterError()`. +- **identifier.ts** -- `isUuid()`, `parseIssueIdentifier()`, `tryParseIssueIdentifier()`. +- **types.ts** -- Type aliases derived from codegen output (e.g., `Issue`, `IssueDetail`, `Document`). +- **embed-parser.ts** -- `extractEmbeds()`, `isLinearUploadUrl()`, `extractFilenameFromUrl()` for parsing embedded files in markdown content. +- **usage.ts** -- Token-optimized two-tier usage system with `DomainMeta` interface, `formatOverview()` for tier 1 (all domains), and `formatDomainUsage()` for tier 2 (domain detail). Generates USAGE.md via build pipeline. -## Platform Implementation +## Generated Types (`src/gql/`) -### Command Interface Layer +Auto-generated by GraphQL Code Generator. **Do not edit these files manually.** -**src/main.ts (lines 3-25)** - Sets up Commander.js with global options and subcommand registration +- **graphql.ts** -- All generated TypeScript types and `DocumentNode` exports. +- **gql.ts** -- Generated helper functions. +- **fragment-masking.ts** -- Fragment masking support. +- **index.ts** -- Barrel export. -- Global `--api-token` option handling -- Default help action when no subcommand provided -- Modular command setup via imported functions +## GraphQL Definitions (`graphql/`) -**src/commands/*.ts** - Command-specific implementations with consistent patterns: +Source `.graphql` files that feed into code generation. -- Parameter validation and smart ID resolution -- Service layer integration via createLinearService -- Standardized error handling and JSON output +### Queries -### GraphQL Service Layer +- `queries/issues.graphql` +- `queries/documents.graphql` +- `queries/attachments.graphql` +- `queries/cycles.graphql` +- `queries/project-milestones.graphql` -**src/utils/graphql-issues-service.ts** - Optimized GraphQL issue operations: +### Mutations -- Lines 32-46: Single-query issue listing (reduces N+1 queries to 1 query) -- Lines 52-100: Issue retrieval by ID with comprehensive data fetching -- Lines 109-245: Enhanced issue updates with batch resolution and label modes -- Lines 253-390: Optimized issue creation with batch ID resolution -- Lines 398-536: Advanced search with filtering and GraphQL optimization +- `mutations/issues.graphql` +- `mutations/documents.graphql` +- `mutations/attachments.graphql` +- `mutations/files.graphql` +- `mutations/project-milestones.graphql` -**src/utils/graphql-service.ts** - GraphQL client wrapper: +## Tests (`tests/`) -- Lines 8-32: Raw GraphQL query execution with error handling -- Lines 37-44: Batch query operations for parallel execution +Unit tests mirror the source structure. Resolver tests mock the SDK client; service tests mock the GraphQL client; common tests require no mocks. -### Legacy API Integration Layer - -**src/utils/linear-service.ts** - SDK-based Linear API service (fallback operations): - -- Lines 193-290: Smart issue ID resolution supporting both UUIDs and human-readable identifiers -- Lines 354-393: Project operations with relationship fetching -- Lines 398-473: Smart ID resolution methods for projects, labels, and teams - -**docs/Linear-API@current.graphql** - Linear GraphQL API schema, downloaded from https://studio.apollographql.com/public/Linear-API/variant/current/schema/sdl?selectedSchema=%23%40%21api%21%40%23 - -## Build System - -### Execution Environment - -**Development Execution** - TypeScript execution via tsx: - -- tsx handles TypeScript compilation at runtime for development -- ES modules support via package.json "type": "module" -- All imports use .js extensions for ES module compatibility - -**Production Build** - Compiled JavaScript execution: - -- `npm run build` creates executable dist/main.js (chmod +x automatically applied) -- Significantly faster execution than tsx for production use -- Clean build process removes previous dist/ directory - -**Development Scripts** - package.json scripts section: - -- `npm start` executes tsx src/main.ts for development -- `npm run build` compiles to executable dist/main.js -- `npm run clean` removes compiled dist/ directory -- `npm test` runs the test suite - -### Dependencies Structure - -**Production Dependencies** (package.json lines 18-22): - -- @linear/sdk ^58.1.0 - Official Linear GraphQL API client (used for GraphQL client and fallback operations) -- commander ^14.0.0 - CLI framework for command structure - -**Development Dependencies** (package.json lines 23-26): - -- @types/node ^22.0.0 - Node.js type definitions -- tsx ^4.20.5 - TypeScript execution engine for development -- typescript ^5.0.0 - TypeScript compiler and language support - -## Reference - -### File Relationships and Dependencies - -**Modern Command Flow**: src/main.ts → src/commands/*.ts → src/utils/graphql-issues-service.ts → src/utils/graphql-service.ts → @linear/sdk GraphQL client - -**Legacy Command Flow**: src/main.ts → src/commands/*.ts → src/utils/linear-service.ts → @linear/sdk - -**Embeds Command Flow**: src/main.ts → src/commands/embeds.ts → src/utils/file-service.ts → Linear uploads.linear.app - -**Embed Extraction Flow**: GraphQL response → src/utils/graphql-issues-service.ts → src/utils/embed-parser.ts → embeds array in JSON output - -**Authentication Flow**: Command options → src/utils/auth.ts → service layer - -**Response Flow**: GraphQL/Service results → src/utils/output.ts → JSON console output - -**Query Organization**: src/queries/issues.ts → src/queries/common.ts fragments → GraphQL execution - -### Key Entry Points for Development - -**Adding Commands** - Start with src/commands/ files, follow existing patterns\ -**GraphQL Integration** - Add queries to src/queries/ and extend src/utils/graphql-issues-service.ts\ -**Legacy API Integration** - Extend src/utils/linear-service.ts methods for fallback operations\ -**Authentication** - Modify src/utils/auth.ts for new authentication methods\ -**Type Definitions** - Update src/utils/linear-types.d.ts for new data structures - -### File Size and Complexity - -Most files are focused and maintainable: - -- src/main.ts: 25 lines - Minimal CLI setup -- src/utils/auth.ts: 39 lines - Simple authentication logic -- src/utils/output.ts: 34 lines - Utility functions only -- src/commands/embeds.ts: 58 lines - File download command -- src/utils/graphql-service.ts: 62 lines - GraphQL client wrapper -- src/utils/embed-parser.ts: 86 lines - Markdown embed extraction -- src/utils/file-service.ts: 111 lines - File download with auth -- src/commands/issues.ts: 211 lines - Comprehensive but focused -- src/commands/comments.ts: 46 lines - Simple comment operations -- src/queries/issues.ts: 301 lines - GraphQL queries and mutations -- src/utils/graphql-issues-service.ts: 604 lines - Optimized GraphQL operations -- src/utils/linear-service.ts: 485 lines - Legacy SDK operations (could be reduced as GraphQL operations replace them) - -### Naming Conventions - -**Files**: Kebab-case for multi-word names (linear-service.ts, graphql-issues-service.ts, linear-types.d.ts)\ -**Directories**: Lowercase single words (commands, utils, queries)\ -**Exports**: PascalCase classes (LinearService, GraphQLIssuesService), camelCase functions (createLinearService, createGraphQLService) +``` +tests/unit/ + resolvers/ # e.g., team-resolver.test.ts + services/ # e.g., issue-service.test.ts + common/ # e.g., identifier.test.ts +``` + +## Configuration + +- **package.json** -- Project metadata, dependencies, and scripts. Requires Node.js >= 22. +- **package-lock.json** -- Dependency lock file. +- **tsconfig.json** -- TypeScript settings (ES2023 target, strict mode, ES modules). +- **codegen.config.ts** -- GraphQL Code Generator configuration. +- **vitest.config.ts** -- Test runner configuration. +- **mise.toml** -- Development tool versions (Node.js 22). + +## Documentation (`docs/`) + +- **architecture.md** -- Architecture overview and layer contracts +- **build-system.md** -- Build and compilation details +- **deployment.md** -- Deployment guide +- **development.md** -- Development patterns and workflows +- **files.md** -- This file +- **performance.md** -- Performance considerations +- **project-overview.md** -- High-level project summary +- **testing.md** -- Testing approach and conventions +- **Linear-API@current.graphql** -- Linear GraphQL API schema reference + +## Data Flow + +``` +CLI Input --> Command --> Resolver --> Service --> JSON Output + | | | + createContext() SDK GraphQL + (name->UUID) (CRUD) +``` + +Resolvers convert human-friendly identifiers to UUIDs exactly once. Services operate solely on UUIDs. Commands tie the pieces together. diff --git a/docs/performance.md b/docs/performance.md index a1eddad..6b3a82b 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -1,34 +1,30 @@ -<!-- Generated: 2025-01-09T12:34:56+00:00 --> - # Performance Optimizations This document details the performance optimizations implemented in the Linear CLI tool. -## Performance Problems Identified - -### Original N+1 Query Problem +## The N+1 Query Problem -The initial implementation suffered from a classic N+1 query problem: +The initial implementation used the Linear SDK's lazy-loading model, which suffered from a classic N+1 query problem: -1. **Single query** to fetch issues list -2. **N additional queries** for each issue's related data: - - 1 query for state information - - 1 query for team information - - 1 query for assignee information - - 1 query for project information - - 1 query for labels information +1. **1 query** to fetch the issues list +2. **N additional queries** per issue for related data: + - 1 query for state + - 1 query for team + - 1 query for assignee + - 1 query for project + - 1 query for labels -**Result**: For 10 issues, this resulted in 1 + (10 × 5) = 51 API calls, taking 10+ seconds. +For 10 issues, this resulted in 1 + (10 x 5) = **51 API calls**, taking 10+ seconds. -## Solutions Implemented +## Solution: Direct GraphQL with Typed Codegen -### 1. GraphQL Single-Query Strategy +All data-fetching operations now use single, comprehensive GraphQL queries executed through `GraphQLClient.request<T>()`. Query definitions live in `.graphql` files and are processed by codegen into typed `DocumentNode` exports and result types. -**Before** (Multiple API calls): +### Before: SDK Lazy Loading (Slow) ```typescript -// Multiple sequential API calls - SLOW -const issues = await this.client.issues({ first: 10 }); +// N+1 pattern -- each property access triggers a separate API call +const issues = await client.sdk.issues({ first: 10 }); for (const issue of issues.nodes) { const state = await issue.state; const team = await issue.team; @@ -38,153 +34,138 @@ for (const issue of issues.nodes) { } ``` -**After** (Single GraphQL query): +### After: Single GraphQL Query (Fast) ```typescript -// Single comprehensive GraphQL query - FAST -const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, { +// One query fetches issues with all relationships included +const result = await client.request<GetIssuesQuery>(GetIssuesDocument, { first: limit, orderBy: "updatedAt", }); -// All relationships included in single response +// result.issues.nodes already contains state, team, assignee, project, labels ``` -### 2. GraphQL Batch Resolution +### Batch ID Resolution + +Operations that need to resolve multiple human-friendly identifiers (team keys, project names, label names) into UUIDs do so in a single batch query rather than issuing separate lookups. -**Before** (Sequential ID resolution): +**Before** (sequential resolution): ```typescript -// Resolve team name → ID -const team = await this.resolveTeamByName(teamName); -// Resolve project name → ID -const project = await this.resolveProjectByName(projectName); -// Resolve label names → IDs -const labels = await Promise.all(labelNames.map(name => this.resolveLabelByName(name))); -// Then create issue -const issue = await this.createIssue({...}); +const team = await resolveTeamByName(teamName); // 1 API call +const project = await resolveProjectByName(projName); // 1 API call +const labels = await Promise.all( // N API calls + labelNames.map(name => resolveLabelByName(name)) +); +const issue = await createIssue({ ... }); // 1 API call ``` -**After** (Batch GraphQL resolution): +**After** (batch resolution in a single query): ```typescript -// Single query resolves ALL IDs at once -const resolveResult = await this.graphQLService.rawRequest( - BATCH_RESOLVE_FOR_CREATE_QUERY, - { teamName, projectName, labelNames }, +const resolved = await client.request<BatchResolveForCreateQuery>( + BatchResolveForCreateDocument, + { teamKey, projectName, labelNames }, ); -// Then create with resolved IDs +// All IDs resolved -- proceed with creation +const issue = await createIssue(client, { ...resolvedInput }); ``` -This reduces issue creation from **7+ API calls to 2 API calls**. +This reduces issue creation from 7+ API calls down to 2. -### 3. Optimized Query Fragments +## Fragment Reuse -**Comprehensive Data Fetching** (src/queries/common.ts): +GraphQL fragments defined in `graphql/queries/*.graphql` ensure consistent, complete data fetching across operations. For example, `CompleteIssueFields` is shared by list, read, and search queries: ```graphql -fragment CompleteIssue on Issue { - id identifier title description priority estimate +# graphql/queries/issues.graphql + +fragment CompleteIssueFields on Issue { + id + identifier + title + description + priority + estimate + createdAt + updatedAt state { id name } assignee { id name } team { id key name } project { id name } labels { nodes { id name } } - createdAt updatedAt + cycle { id name number } + parent { id identifier title } + children { nodes { id identifier title } } } -``` -All issue operations use shared fragments to ensure consistent, complete data fetching without redundant queries. +query GetIssues($first: Int!, $orderBy: PaginationOrderBy) { + issues(first: $first, orderBy: $orderBy, ...) { + nodes { ...CompleteIssueFields } + } +} +``` -## Performance Results +All services import typed `DocumentNode` and result types from codegen output, so queries are never written as raw strings. -### Benchmarks +## Benchmarks -All tests performed with real Linear API: +All benchmarks performed against the real Linear API: -| Operation | Before | After | Improvement | -| ----------------- | ------------ | ---------------- | --------------- | -| Single issue read | ~10+ seconds | ~0.9-1.1 seconds | **90%+ faster** | -| List 10 issues | ~30+ seconds | ~0.9 seconds | **95%+ faster** | -| Create issue | ~2-3 seconds | ~1.1 seconds | **50%+ faster** | -| Search issues | ~15+ seconds | ~1.0 seconds | **93%+ faster** | +| Operation | Before (SDK) | After (GraphQL) | Improvement | +| ----------------- | ------------ | --------------- | --------------- | +| Single issue read | ~10+ seconds | ~0.9-1.1 seconds | 90%+ faster | +| List 10 issues | ~30+ seconds | ~0.9 seconds | 95%+ faster | +| Create issue | ~2-3 seconds | ~1.1 seconds | 50%+ faster | +| Search issues | ~15+ seconds | ~1.0 seconds | 93%+ faster | -### Test Commands Used +### Test Commands ```bash -# Single issue read time npm start issues read ABC-123 - -# List issues time npm start issues list -l 10 - -# Create issue time npm start issues create --title "Test" --team ABC - -# Search issues time npm start issues search "test" --team ABC ``` -### Real-World Performance - -Example output from `time npm start issues list -l 1`: +### Example Timing ``` npm start issues list -l 1 < /dev/null 0.62s user 0.08s system 77% cpu 0.904 total ``` -**Total time: 0.904 seconds** (including npm overhead and Node.js startup) - -## Technical Implementation Details - -### Code Locations +Total wall time: 0.904 seconds (including npm overhead and Node.js startup). -The GraphQL optimizations are implemented in: +## Code Locations -- **src/utils/graphql-service.ts** - GraphQL client wrapper with batch operations -- **src/utils/graphql-issues-service.ts** - Single-query issue operations (lines 32-536) -- **src/queries/issues.ts** - Optimized GraphQL queries and fragments -- **src/queries/common.ts** - Reusable query fragments for consistent data fetching -- **src/commands/issues.ts** - Enhanced commands using GraphQL service +- `src/client/graphql-client.ts` -- GraphQL client wrapper with typed `request<T>()` method +- `src/services/issue-service.ts` -- Issue CRUD and search operations +- `src/services/` -- Other domain services (documents, attachments, cycles, etc.) +- `graphql/queries/*.graphql` -- Query and fragment definitions +- `graphql/mutations/*.graphql` -- Mutation definitions +- `src/gql/graphql.ts` -- Codegen output (generated, do not edit) +- `src/commands/issues.ts` -- CLI command orchestration -### Key Performance Patterns +## Key Principles -1. **Single GraphQL Queries**: Replace N+1 patterns with comprehensive single queries -2. **Batch ID Resolution**: Resolve multiple identifiers in single operations -3. **Fragment Reuse**: Use consistent GraphQL fragments across operations -4. **Smart Caching**: Leverage GraphQL response structure for efficient data handling -5. **Lightweight Operations**: Use minimal queries for simple operations like comment creation +1. **Single GraphQL queries** -- Replace N+1 SDK patterns with comprehensive queries that fetch all relationships in one round trip. +2. **Batch ID resolution** -- Resolve multiple identifiers in a single query before performing mutations. +3. **Fragment reuse** -- Shared `.graphql` fragments keep field selections consistent and reduce duplication. +4. **Typed operations** -- All queries use codegen `DocumentNode` exports and typed results (`client.request<GetIssuesQuery>(GetIssuesDocument, ...)`), catching schema mismatches at compile time. -## Monitoring Performance - -To monitor performance in production: +## Monitoring ```bash -# Add timing to any command +# Time any command time linearis <command> -# Example: Monitor issue listing performance +# Examples time linearis issues list -l 25 - -# Example: Monitor search performance time linearis issues search "bug" --team ABC ``` -## Future Optimizations - -Potential areas for further improvement: - -1. **Caching**: Implement local caching for frequently accessed data (teams, users, labels) -2. **Connection Pooling**: Optimize HTTP connections to Linear's GraphQL API -3. **Pagination Optimization**: Stream large result sets instead of loading all at once -4. **Background Prefetching**: Pre-load common data in background - -## Impact - -The performance optimizations provide: - -- **90%+ reduction** in API response times -- **Better user experience** with sub-second response times -- **Reduced API load** on Linear's servers -- **More efficient** resource utilization +## Future Considerations -These improvements make the CLI suitable for real-time use and integration into automated workflows. +- **Local caching** for frequently accessed reference data (teams, users, labels) +- **Pagination streaming** for large result sets +- **Connection pooling** for HTTP connections to the Linear API diff --git a/docs/plans/2025-11-09-pr7-post-merge-fixes.md b/docs/plans/2025-11-09-pr7-post-merge-fixes.md deleted file mode 100644 index f1a4867..0000000 --- a/docs/plans/2025-11-09-pr7-post-merge-fixes.md +++ /dev/null @@ -1,1800 +0,0 @@ -# PR #7 Post-Merge Fixes Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Ticket:** ZCO-1576 - -**Goal:** Fix type safety, code quality, and organizational issues introduced in PR #7 (cycles & milestones features) - -**Architecture:** Refactor inline helpers to service layer, add proper TypeScript types, remove development artifacts, fix CI configuration, and improve error handling consistency. - -**Tech Stack:** TypeScript, Node.js, Vitest, GitHub Actions, Linear SDK - ---- - -## Phase 1: Critical Cleanup - -### Task 1: Remove Development Artifacts - -**Files:** - -- Delete: `github-issue-labels-enhancement.md` -- Delete: `github-issue-project-crud.md` -- Delete: `labels-research.md` -- Delete: `labels-summary.md` -- Delete: `pr-7-description.md` -- Delete: `project-labels-research.md` - -**Step 1: Verify files exist and are not referenced** - -Run: `grep -r "github-issue-labels-enhancement\|labels-research\|pr-7-description" src/ tests/ docs/` Expected: No results (these files aren't referenced in code) - -**Step 2: Remove files** - -```bash -git rm github-issue-labels-enhancement.md \ - github-issue-project-crud.md \ - labels-research.md \ - labels-summary.md \ - pr-7-description.md \ - project-labels-research.md -``` - -Expected: Files staged for deletion - -**Step 3: Verify removal** - -Run: `git status` Expected: 6 files deleted, staged for commit - -**Step 4: Commit** - -```bash -git commit -m "[CHORE] Remove development artifacts from repository - -These files were accidentally committed in PR #7: -- Research notes (labels-*, github-issue-*, project-labels-*) -- PR description draft (pr-7-description.md) - -They belong in local notes/drafts, not the repository." -``` - ---- - -### Task 2: Clean Up .gitignore - -**Files:** - -- Modify: `.gitignore:25-30` - -**Step 1: Read current .gitignore section** - -Run: `sed -n '22,35p' .gitignore` Expected: See the Catalyst/test-pr4.sh section - -**Step 2: Update .gitignore** - -Replace lines 25-30 with: - -```gitignore -# Development artifacts -thoughts/ -coverage/ - -# Test scripts (keep locally) -test-*.sh -*.test.sh -``` - -**Step 3: Verify changes** - -Run: `git diff .gitignore` Expected: See simplified comments and pattern-based exclusions - -**Step 4: Test .gitignore patterns** - -```bash -touch test-example.sh -git status -rm test-example.sh -``` - -Expected: `test-example.sh` appears in untracked files (not ignored - that's correct, we want these in .gitignore to ignore them) - -Actually, verify it's ignored: - -```bash -touch test-example.sh -git check-ignore -v test-example.sh -rm test-example.sh -``` - -Expected: Shows `.gitignore:30:test-*.sh test-example.sh` - -**Step 5: Commit** - -```bash -git add .gitignore -git commit -m "[CHORE] Simplify .gitignore patterns - -Changes: -- Remove unexplained 'Catalyst' reference -- Use patterns (test-*.sh) instead of specific filenames -- More concise comments" -``` - ---- - -### Task 3: Fix Redundant CI Test Step - -**Files:** - -- Modify: `.github/workflows/ci.yml:40-45` - -**Step 1: Review current CI configuration** - -Run: `sed -n '35,50p' .github/workflows/ci.yml` Expected: See two test steps (lines 39 and 42) - -**Step 2: Remove redundant integration test step** - -Delete lines 42-47 (the "Run integration tests" step). - -Keep only: - -```yaml -- name: Build project - run: pnpm run build - -- name: Run tests - run: pnpm test -``` - -**Step 3: Verify YAML is valid** - -Run: `cat .github/workflows/ci.yml | head -50` Expected: Valid YAML, single test step - -**Step 4: Commit** - -```bash -git add .github/workflows/ci.yml -git commit -m "[FIX] Remove redundant test step in CI workflow - -The workflow was running 'pnpm test' twice: -1. Unconditionally in 'Run unit tests' -2. Conditionally in 'Run integration tests' - -Vitest already skips integration tests when LINEAR_API_TOKEN -is not set, so the conditional step was unnecessary and wasteful." -``` - -**Step 5: Push and verify CI** - -```bash -git push -``` - -Then check GitHub Actions to ensure CI passes with single test run. - ---- - -## Phase 2: Type Safety - -### Task 4: Add Cycle Type Definitions - -**Files:** - -- Modify: `src/utils/linear-types.d.ts` (add at end) -- Modify: `src/commands/cycles.ts:1` (add import) - -**Step 1: Add LinearCycle interface** - -Add to `src/utils/linear-types.d.ts`: - -```typescript -export interface LinearCycle { - id: string; - name: string; - number: number; - startsAt?: string; - endsAt?: string; - isActive: boolean; - isPrevious?: boolean; - isNext?: boolean; - progress: number; - issueCountHistory: number[]; - team?: { - id: string; - key: string; - name: string; - }; - issues?: LinearIssue[]; -} -``` - -**Step 2: Add cycles command option types** - -Add to `src/utils/linear-types.d.ts`: - -```typescript -export interface CycleListOptions { - team?: string; - active?: boolean; - aroundActive?: string; -} - -export interface CycleReadOptions { - team?: string; - issuesFirst?: string; -} -``` - -**Step 3: Verify types compile** - -Run: `pnpm exec tsc --noEmit` Expected: No errors - -**Step 4: Commit** - -```bash -git add src/utils/linear-types.d.ts -git commit -m "[FEAT] Add TypeScript types for cycles" -``` - ---- - -### Task 5: Apply Cycle Types to Commands - -**Files:** - -- Modify: `src/commands/cycles.ts:1,16,35,37,40,46-47,62` - -**Step 1: Add imports** - -At top of `src/commands/cycles.ts`, after existing imports: - -```typescript -import type { CycleListOptions, CycleReadOptions, LinearCycle } from "../utils/linear-types.js"; -``` - -**Step 2: Replace first `any` (line 16)** - -Change: - -```typescript -async (options: any, command: Command) => { -``` - -To: - -```typescript -async (options: CycleListOptions, command: Command) => { -``` - -**Step 3: Replace cycle type casts (lines 35, 37, 40, 46-47)** - -Change: - -```typescript -const activeCycle = allCycles.find((c: any) => c.isActive); -``` - -To: - -```typescript -const activeCycle = allCycles.find((c: LinearCycle) => c.isActive); -``` - -Change: - -```typescript -const filtered = allCycles - .filter((c: any) => typeof c.number === "number" && c.number >= min && c.number <= max) - .sort((a: any, b: any) => a.number - b.number); -``` - -To: - -```typescript -const filtered = allCycles - .filter((c: LinearCycle) => typeof c.number === "number" && c.number >= min && c.number <= max) - .sort((a: LinearCycle, b: LinearCycle) => a.number - b.number); -``` - -**Step 4: Replace second `any` (line 62)** - -Change: - -```typescript -async (cycleIdOrName: string, options: any, command: Command) => { -``` - -To: - -```typescript -async (cycleIdOrName: string, options: CycleReadOptions, command: Command) => { -``` - -**Step 5: Verify types compile** - -Run: `pnpm exec tsc --noEmit` Expected: No errors - -**Step 6: Verify build succeeds** - -Run: `pnpm run build` Expected: Successful compilation - -**Step 7: Commit** - -```bash -git add src/commands/cycles.ts -git commit -m "[REFACTOR] Apply proper types to cycles commands - -Replaced 'any' types with LinearCycle and option interfaces: -- CycleListOptions for list command options -- CycleReadOptions for read command options -- LinearCycle for cycle objects" -``` - ---- - -### Task 6: Add Milestone Type Definitions - -**Files:** - -- Modify: `src/utils/linear-types.d.ts` (add at end) - -**Step 1: Add LinearProjectMilestone interface** - -Add to `src/utils/linear-types.d.ts`: - -```typescript -export interface LinearProjectMilestone { - id: string; - name: string; - description?: string; - targetDate?: string; - sortOrder?: number; - createdAt: string; - updatedAt: string; - project?: { - id: string; - name: string; - }; - issues?: LinearIssue[]; -} -``` - -**Step 2: Add milestone command option types** - -Add to `src/utils/linear-types.d.ts`: - -```typescript -export interface MilestoneListOptions { - project: string; - limit?: string; -} - -export interface MilestoneReadOptions { - project?: string; - issuesFirst?: string; -} - -export interface MilestoneCreateOptions { - project: string; - description?: string; - targetDate?: string; -} - -export interface MilestoneUpdateOptions { - project?: string; - name?: string; - description?: string; - targetDate?: string; - sortOrder?: string; -} -``` - -**Step 3: Verify types compile** - -Run: `pnpm exec tsc --noEmit` Expected: No errors - -**Step 4: Commit** - -```bash -git add src/utils/linear-types.d.ts -git commit -m "[FEAT] Add TypeScript types for project milestones" -``` - ---- - -### Task 7: Apply Milestone Types to Commands - -**Files:** - -- Modify: `src/commands/projectMilestones.ts:1,15,33,99,127,160,203` - -**Step 1: Add imports** - -At top of `src/commands/projectMilestones.ts`, after existing imports: - -```typescript -import type { LinearProjectMilestone, MilestoneCreateOptions, MilestoneListOptions, MilestoneReadOptions, MilestoneUpdateOptions } from "../utils/linear-types.js"; -``` - -**Step 2: Type helper function parameters** - -Change `resolveProjectId` (line 15): - -```typescript -async function resolveProjectId(projectNameOrId: string, graphQLService: any): Promise<string> { -``` - -To: - -```typescript -import type { GraphQLService } from "../utils/graphql-service.js"; - -async function resolveProjectId(projectNameOrId: string, graphQLService: GraphQLService): Promise<string> { -``` - -Change `resolveMilestoneId` (line 33): - -```typescript -async function resolveMilestoneId( - milestoneNameOrId: string, - graphQLService: any, - projectNameOrId?: string -): Promise<string> { -``` - -To: - -```typescript -async function resolveMilestoneId( - milestoneNameOrId: string, - graphQLService: GraphQLService, - projectNameOrId?: string -): Promise<string> { -``` - -**Step 3: Type milestone nodes in helper** - -In `resolveMilestoneId`, change (line 43): - -```typescript -let nodes: any[] = []; -``` - -To: - -```typescript -let nodes: LinearProjectMilestone[] = []; -``` - -And change the map (line 72): - -```typescript -const projectNames = nodes - .map((m: any) => `"${m.name}" in project "${m.project?.name}"`) - .join(", "); -``` - -To: - -```typescript -const projectNames = nodes - .map((m: LinearProjectMilestone) => `"${m.name}" in project "${m.project?.name}"`) - .join(", "); -``` - -**Step 4: Type command options** - -Change list command (line 99): - -```typescript -async (options: any, command: Command) => { -``` - -To: - -```typescript -async (options: MilestoneListOptions, command: Command) => { -``` - -Change read command (line 127): - -```typescript -async (milestoneIdOrName: string, options: any, command: Command) => { -``` - -To: - -```typescript -async (milestoneIdOrName: string, options: MilestoneReadOptions, command: Command) => { -``` - -Change create command (line 160): - -```typescript -async (name: string, options: any, command: Command) => { -``` - -To: - -```typescript -async (name: string, options: MilestoneCreateOptions, command: Command) => { -``` - -Change update command (line 203): - -```typescript -async (milestoneIdOrName: string, options: any, command: Command) => { -``` - -To: - -```typescript -async (milestoneIdOrName: string, options: MilestoneUpdateOptions, command: Command) => { -``` - -**Step 5: Type updateVars object** - -Change (line 215): - -```typescript -const updateVars: any = { id: milestoneId }; -``` - -To: - -```typescript -const updateVars: Partial<LinearProjectMilestone> & { id: string } = { id: milestoneId }; -``` - -**Step 6: Verify types compile** - -Run: `pnpm exec tsc --noEmit` Expected: No errors (may need to export GraphQLService class) - -**Step 7: Export GraphQLService if needed** - -If step 6 shows error about GraphQLService not being exported, modify `src/utils/graphql-service.ts`: - -Change: - -```typescript -class GraphQLService { -``` - -To: - -```typescript -export class GraphQLService { -``` - -**Step 8: Verify build succeeds** - -Run: `pnpm run build` Expected: Successful compilation - -**Step 9: Commit** - -```bash -git add src/commands/projectMilestones.ts src/utils/graphql-service.ts -git commit -m "[REFACTOR] Apply proper types to project milestones commands - -Replaced 'any' types with proper interfaces: -- MilestoneListOptions, MilestoneReadOptions, etc. -- LinearProjectMilestone for milestone objects -- GraphQLService for service parameters - -Exported GraphQLService class for type imports." -``` - ---- - -### Task 8: Fix Date Handling in LinearService - -**Files:** - -- Modify: `src/utils/linear-service.ts:167-169,397-398,427-428,483-484` - -**Step 1: Investigate Linear SDK date types** - -Run: `grep -A 2 "targetDate\|startsAt\|endsAt" node_modules/@linear/sdk/dist/*.d.ts | head -20` - -Expected: Reveals whether these are Date objects or TimelessDate objects - -**Step 2: Read current implementation** - -Run: `sed -n '165,172p' src/utils/linear-service.ts` Expected: See the `String()` conversions - -**Step 3: Check TimelessDate type** - -Linear SDK uses `TimelessDate` for dates. Check the type: - -```bash -grep -A 5 "class TimelessDate" node_modules/@linear/sdk/dist/*.d.ts -``` - -Expected: TimelessDate has a `toString()` method - -**Step 4: Document why String() is used** - -Based on investigation, if TimelessDate is used, add comment explaining the conversion. - -At line 166, add comment: - -```typescript -return projects.map((project) => ({ - id: project.id, - name: project.name, - state: project.state, - progress: project.progress, - lead: project.lead - ? { - id: project.lead.id, - name: project.lead.name, - } - : undefined, - // Linear SDK returns TimelessDate objects, convert to ISO strings for JSON serialization - targetDate: project.targetDate ? String(project.targetDate) : undefined, - createdAt: project.createdAt ? String(project.createdAt) : new Date().toISOString(), - updatedAt: project.updatedAt ? String(project.updatedAt) : new Date().toISOString(), -})); -``` - -**Step 5: Add same comment to cycle methods** - -At line 394: - -```typescript -return { - id: cycle.id, - name: cycle.name, - number: cycle.number, - // Linear SDK TimelessDate/DateTime objects, convert to strings for JSON - startsAt: cycle.startsAt ? String(cycle.startsAt) : undefined, - endsAt: cycle.endsAt ? String(cycle.endsAt) : undefined, -``` - -At line 479: - -```typescript -nodes.push({ - id: cycle.id, - name: cycle.name, - number: cycle.number, - // Linear SDK DateTime conversion - startsAt: cycle.startsAt, -``` - -Wait, line 479 doesn't convert to String - this is inconsistent! - -**Step 6: Fix inconsistency at line 479-481** - -Change: - -```typescript -startsAt: cycle.startsAt, -isActive: cycle.isActive, -``` - -To: - -```typescript -startsAt: cycle.startsAt ? String(cycle.startsAt) : undefined, -isActive: cycle.isActive, -``` - -**Step 7: Verify changes** - -Run: `git diff src/utils/linear-service.ts` Expected: See comments added and consistency fix - -**Step 8: Test build** - -Run: `pnpm run build` Expected: Successful compilation - -**Step 9: Commit** - -```bash -git add src/utils/linear-service.ts -git commit -m "[DOC] Document date handling and fix inconsistency - -Linear SDK returns TimelessDate/DateTime objects which need -String() conversion for JSON serialization. - -Also fixed inconsistency in resolveCycleId where startsAt -wasn't being converted to string like other date fields." -``` - ---- - -### Task 9: Handle Deprecated Cycles Query File - -**Files:** - -- Modify: `src/queries/cycles.ts:1` (add deprecation notice) -- OR Delete: `src/queries/cycles.ts` and `dist/queries/cycles.js` - -**Step 1: Check if file is imported anywhere** - -Run: `grep -r "from.*queries/cycles" src/` Expected: No results (not imported) - -**Step 2: Decide deletion vs deprecation** - -Since file is not used, deletion is cleaner. - -**Step 3: Delete the files** - -```bash -git rm src/queries/cycles.ts -git rm dist/queries/cycles.js -``` - -**Step 4: Verify no build errors** - -Run: `pnpm run build` Expected: Successful build - -**Step 5: Commit** - -```bash -git commit -m "[CHORE] Remove deprecated cycles GraphQL queries - -These queries were replaced with Linear SDK implementation -in PR #7 to avoid GraphQL complexity errors with large -datasets (500+ cycles). - -The file was not imported or used anywhere in the codebase." -``` - ---- - -## Phase 3: Service Layer Refactoring - -### Task 10: Move resolveProjectId to LinearService - -**Files:** - -- Modify: `src/utils/linear-service.ts:369` (add method) -- Modify: `src/commands/projectMilestones.ts:14-30` (remove helper, use service) - -**Step 1: Add method to LinearService** - -In `src/utils/linear-service.ts`, after the `resolveCycleId` method (around line 537), add: - -```typescript - /** - * Resolve project identifier to UUID - * - * @param projectNameOrId - Project name or UUID - * @returns Project UUID - * @throws Error if project not found - */ - async resolveProjectId(projectNameOrId: string): Promise<string> { - if (isUuid(projectNameOrId)) { - return projectNameOrId; - } - - const filter = buildEqualityFilter("name", projectNameOrId); - const projectsConnection = await this.client.projects({ filter, first: 1 }); - - if (projectsConnection.nodes.length === 0) { - throw new Error(`Project "${projectNameOrId}" not found`); - } - - return projectsConnection.nodes[0].id; - } -``` - -**Step 2: Verify it compiles** - -Run: `pnpm exec tsc --noEmit` Expected: No errors - -**Step 3: Commit service change** - -```bash -git add src/utils/linear-service.ts -git commit -m "[REFACTOR] Add resolveProjectId to LinearService - -Moved from inline helper in projectMilestones command. -Makes it reusable and testable." -``` - ---- - -### Task 11: Update projectMilestones to Use Service - -**Files:** - -- Modify: `src/commands/projectMilestones.ts:1,14-30,104,135,166,206` - -**Step 1: Import LinearService** - -At top of file, add: - -```typescript -import { createLinearService } from "../utils/linear-service.js"; -``` - -**Step 2: Remove resolveProjectId helper function** - -Delete lines 14-30 (the entire `resolveProjectId` function). - -**Step 3: Update resolveMilestoneId to take LinearService** - -The `resolveMilestoneId` function currently creates GraphQLService internally. We need it to call `linearService.resolveProjectId()`. - -But wait - `resolveMilestoneId` uses GraphQL directly. We have an architecture problem: - -- Milestones use GraphQLService -- Projects use LinearService (SDK) - -**Step 4: Decide on approach** - -Option A: Keep both services, pass both to resolveMilestoneId Option B: Add resolveMilestoneId to LinearService too Option C: Keep helper but call linearService.resolveProjectId() - -Let's use Option C for now (minimal change). - -Change `resolveMilestoneId` signature (line 33): - -```typescript -async function resolveMilestoneId( - milestoneNameOrId: string, - graphQLService: GraphQLService, - linearService: any, - projectNameOrId?: string -): Promise<string> { -``` - -**Step 5: Update resolveMilestoneId to use linearService** - -In the function body (line 45-47), change: - -```typescript -if (projectNameOrId) { - // Resolve project ID first - const projectId = await resolveProjectId(projectNameOrId, graphQLService); -``` - -To: - -```typescript -if (projectNameOrId) { - // Resolve project ID using LinearService - const projectId = await linearService.resolveProjectId(projectNameOrId); -``` - -**Step 6: Update all command actions to create and pass linearService** - -In list command (line 99), change: - -```typescript - handleAsyncCommand(async (options: MilestoneListOptions, command: Command) => { - const graphQLService = await createGraphQLService( - command.parent!.parent!.opts(), - ); - - // Resolve project ID if needed - const projectId = await resolveProjectId(options.project, graphQLService); -``` - -To: - -```typescript - handleAsyncCommand(async (options: MilestoneListOptions, command: Command) => { - const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), - ]); - - // Resolve project ID using LinearService - const projectId = await linearService.resolveProjectId(options.project); -``` - -**Step 7: Update read command** - -Change (line 127): - -```typescript -const graphQLService = await createGraphQLService( - command.parent!.parent!.opts(), -); - -const milestoneId = await resolveMilestoneId( - milestoneIdOrName, - graphQLService, - options.project, -); -``` - -To: - -```typescript -const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), -]); - -const milestoneId = await resolveMilestoneId( - milestoneIdOrName, - graphQLService, - linearService, - options.project, -); -``` - -**Step 8: Update create command** - -Change (line 160): - -```typescript -const graphQLService = await createGraphQLService( - command.parent!.parent!.opts(), -); - -// Resolve project ID if needed -const projectId = await resolveProjectId(options.project, graphQLService); -``` - -To: - -```typescript -const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), -]); - -// Resolve project ID using LinearService -const projectId = await linearService.resolveProjectId(options.project); -``` - -**Step 9: Update update command** - -Change (line 203): - -```typescript -const graphQLService = await createGraphQLService( - command.parent!.parent!.opts(), -); - -const milestoneId = await resolveMilestoneId( - milestoneIdOrName, - graphQLService, - options.project, -); -``` - -To: - -```typescript -const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), -]); - -const milestoneId = await resolveMilestoneId( - milestoneIdOrName, - graphQLService, - linearService, - options.project, -); -``` - -**Step 10: Fix type error** - -Change the linearService parameter type in resolveMilestoneId: - -```typescript -async function resolveMilestoneId( - milestoneNameOrId: string, - graphQLService: GraphQLService, - linearService: any, // <-- fix this - projectNameOrId?: string -): Promise<string> { -``` - -To: - -```typescript -import type { LinearService } from "../utils/linear-service.js"; - -async function resolveMilestoneId( - milestoneNameOrId: string, - graphQLService: GraphQLService, - linearService: LinearService, - projectNameOrId?: string -): Promise<string> { -``` - -Wait, LinearService is a class not exported as type. Let's just leave as `any` for now since it's a helper function. - -**Step 11: Verify build** - -Run: `pnpm run build` Expected: Successful build - -**Step 12: Commit** - -```bash -git add src/commands/projectMilestones.ts -git commit -m "[REFACTOR] Use LinearService.resolveProjectId in milestones - -Removed inline resolveProjectId helper function. -Now uses LinearService.resolveProjectId() for consistency. - -Both services (GraphQL and Linear SDK) are created in each -command handler as needed." -``` - ---- - -### Task 12: Add Error Handling Documentation - -**Files:** - -- Modify: `src/utils/linear-service.ts:395` - -**Step 1: Add JSDoc comment before Promise.all** - -At line 393, add: - -```typescript -// Fetch all relationships in parallel for all cycles -// Note: Uses Promise.all - entire operation fails if any team fetch fails. -// This ensures data consistency (all cycles have team data or none do). -// If partial failures are acceptable, use Promise.allSettled instead. -const cyclesWithData = await Promise.all( -``` - -**Step 2: Verify no functional changes** - -Run: `git diff src/utils/linear-service.ts` Expected: Only comment added - -**Step 3: Commit** - -```bash -git add src/utils/linear-service.ts -git commit -m "[DOC] Document Promise.all behavior in getCycles - -Clarifies that operation fails fast if any cycle's team -fetch fails, which ensures data consistency." -``` - ---- - -## Phase 4: Testing - -### Task 13: Add Tests for Cycle Error Cases - -**Files:** - -- Modify: `tests/unit/linear-service-cycles.test.ts` - -**Step 1: Read existing tests** - -Run: `head -50 tests/unit/linear-service-cycles.test.ts` Expected: See existing test structure - -**Step 2: Add test for ambiguous cycle name** - -Add to the test file: - -```typescript -describe("resolveCycleId - error cases", () => { - it("should throw when cycle not found", async () => { - const mockClient = { - cycles: vi.fn().mockResolvedValue({ - nodes: [], - }), - }; - - const service = new LinearService("fake-token"); - (service as any).client = mockClient; - - await expect(service.resolveCycleId("Nonexistent Cycle")).rejects.toThrow( - 'Cycle "Nonexistent Cycle" not found', - ); - }); - - it("should throw when multiple cycles match and none are active/next/previous", async () => { - const mockClient = { - cycles: vi.fn().mockResolvedValue({ - nodes: [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: "2025-01-01", - isActive: false, - isNext: false, - isPrevious: false, - team: Promise.resolve({ id: "team-1", key: "ENG", name: "Engineering" }), - }, - { - id: "cycle-2", - name: "Sprint 1", - number: 2, - startsAt: "2025-02-01", - isActive: false, - isNext: false, - isPrevious: false, - team: Promise.resolve({ id: "team-2", key: "PROD", name: "Product" }), - }, - ], - }), - }; - - const service = new LinearService("fake-token"); - (service as any).client = mockClient; - - await expect(service.resolveCycleId("Sprint 1")).rejects.toThrow( - /Ambiguous cycle name.*multiple matches found/, - ); - }); - - it("should prefer active cycle when multiple matches exist", async () => { - const mockClient = { - cycles: vi.fn().mockResolvedValue({ - nodes: [ - { - id: "cycle-inactive", - name: "Sprint 1", - number: 1, - startsAt: "2025-01-01", - isActive: false, - isNext: false, - isPrevious: false, - team: Promise.resolve({ id: "team-1", key: "ENG", name: "Engineering" }), - }, - { - id: "cycle-active", - name: "Sprint 1", - number: 2, - startsAt: "2025-02-01", - isActive: true, - isNext: false, - isPrevious: false, - team: Promise.resolve({ id: "team-2", key: "PROD", name: "Product" }), - }, - ], - }), - }; - - const service = new LinearService("fake-token"); - (service as any).client = mockClient; - - const result = await service.resolveCycleId("Sprint 1"); - expect(result).toBe("cycle-active"); - }); -}); -``` - -**Step 3: Run tests** - -Run: `pnpm test tests/unit/linear-service-cycles.test.ts` Expected: New tests pass - -**Step 4: Commit** - -```bash -git add tests/unit/linear-service-cycles.test.ts -git commit -m "[TEST] Add error case tests for cycle resolution - -Tests for: -- Cycle not found -- Ambiguous cycle names -- Disambiguation logic (prefers active)" -``` - ---- - -### Task 14: Add Integration Tests for Cycle Errors - -**Files:** - -- Modify: `tests/integration/cycles-cli.test.ts` - -**Step 1: Add test for --around-active without --team** - -Add to the test file: - -```typescript -describe("Cycles CLI - Error Cases", () => { - it("should reject --around-active without --team", async () => { - if (!hasApiToken) return; - - await expect( - execAsync(`node ${CLI_PATH} cycles list --around-active 3`), - ).rejects.toThrow(/--around-active requires --team/); - }); - - it("should reject --around-active with non-numeric value", async () => { - if (!hasApiToken) return; - - await expect( - execAsync(`node ${CLI_PATH} cycles list --around-active abc --team Engineering`), - ).rejects.toThrow(/--around-active requires a non-negative integer/); - }); - - it("should reject --around-active with negative value", async () => { - if (!hasApiToken) return; - - await expect( - execAsync(`node ${CLI_PATH} cycles list --around-active -5 --team Engineering`), - ).rejects.toThrow(/--around-active requires a non-negative integer/); - }); -}); -``` - -**Step 2: Run integration tests** - -Run: `LINEAR_API_TOKEN=xxx pnpm test tests/integration/cycles-cli.test.ts` Expected: New tests pass (or skip if no token) - -**Step 3: Commit** - -```bash -git add tests/integration/cycles-cli.test.ts -git commit -m "[TEST] Add integration tests for cycles error cases - -Tests validation of --around-active flag: -- Requires --team -- Requires non-negative integer" -``` - ---- - -### Task 15: Add Tests for Milestone Create/Update - -**Files:** - -- Modify: `tests/integration/project-milestones-cli.test.ts` - -**Step 1: Add create test with cleanup** - -Add to the test file: - -```typescript -describe("Project Milestones - Create", () => { - const TEST_MILESTONE_NAME = `Test Milestone ${Date.now()}`; - - afterEach(async () => { - // Cleanup: delete test milestone if it exists - // Note: Linear CLI doesn't have delete command, so this is manual cleanup - // In real usage, you'd need to delete via Linear SDK or web UI - }); - - it("should create milestone with all fields", async () => { - if (!hasApiToken) { - console.warn("Skipping create test - no API token"); - return; - } - - // Note: This test requires a valid project name from your Linear workspace - // Replace "Test Project" with an actual project name - const result = await execAsync( - `node ${CLI_PATH} project-milestones create "${TEST_MILESTONE_NAME}" ` + - `--project "Test Project" ` + - `--description "Test milestone created by integration test" ` + - `--target-date 2025-12-31`, - ); - - const output = JSON.parse(result.stdout); - expect(output).toHaveProperty("id"); - expect(output.name).toBe(TEST_MILESTONE_NAME); - expect(output.description).toBe("Test milestone created by integration test"); - expect(output.targetDate).toBe("2025-12-31"); - }); - - it("should create milestone with minimal fields", async () => { - if (!hasApiToken) { - console.warn("Skipping create test - no API token"); - return; - } - - const result = await execAsync( - `node ${CLI_PATH} project-milestones create "${TEST_MILESTONE_NAME} Minimal" ` + - `--project "Test Project"`, - ); - - const output = JSON.parse(result.stdout); - expect(output).toHaveProperty("id"); - expect(output.name).toBe(`${TEST_MILESTONE_NAME} Minimal`); - }); -}); -``` - -**Step 2: Add note about cleanup** - -At top of file, add comment: - -```typescript -/** - * Integration tests for project-milestones CLI command - * - * Note: Create/update tests leave test data in Linear workspace. - * Manual cleanup may be required. Consider using a test workspace. - * - * These tests require LINEAR_API_TOKEN to be set in environment. - * If not set, tests will be skipped. - */ -``` - -**Step 3: Run tests** - -Run: `LINEAR_API_TOKEN=xxx pnpm test tests/integration/project-milestones-cli.test.ts` Expected: Tests pass or skip - -**Step 4: Commit** - -```bash -git add tests/integration/project-milestones-cli.test.ts -git commit -m "[TEST] Add integration tests for milestone create - -Tests create command with: -- All fields (name, description, target date) -- Minimal fields (name only) - -Note: Tests leave data in workspace, manual cleanup needed." -``` - ---- - -## Phase 5: Documentation & Polish - -### Task 16: Document Flag Interactions - -**Files:** - -- Modify: `README.md:97` (add section after cycles examples) - -**Step 1: Find insertion point** - -Run: `sed -n '95,110p' README.md` Expected: See end of cycles section - -**Step 2: Add flag combinations section** - -After the cycles examples (around line 110), add: - -```markdown -#### Flag Combinations - -The `cycles list` command supports several flag combinations: - -**Valid combinations:** - -- `cycles list` - All cycles across all teams -- `cycles list --team Backend` - All Backend cycles -- `cycles list --active` - Active cycles from all teams -- `cycles list --team Backend --active` - Backend's active cycle only -- `cycles list --team Backend --around-active 3` - Backend's active cycle ± 3 cycles - -**Invalid combinations:** - -- `cycles list --around-active 3` - ❌ Error: requires `--team` - -**Note:** Using `--active --around-active` together works but `--active` is redundant since `--around-active` always includes the active cycle. -``` - -**Step 3: Verify markdown formatting** - -Run: `cat README.md | grep -A 15 "Flag Combinations"` Expected: Proper markdown - -**Step 4: Commit** - -```bash -git add README.md -git commit -m "[DOC] Document cycles command flag combinations - -Clarifies valid and invalid flag combinations for cycles list. -Helps users understand how --team, --active, and --around-active -interact." -``` - ---- - -### Task 17: Add Pagination Documentation to Service Methods - -**Files:** - -- Modify: `src/utils/linear-service.ts:415,538` - -**Step 1: Add JSDoc to getCycleById** - -Before `getCycleById` method (line 415), update JSDoc: - -```typescript -/** - * Get single cycle by ID with issues - * - * @param cycleId - Cycle UUID - * @param issuesLimit - Maximum issues to fetch (default 50) - * @returns Cycle with issues - * - * @remarks - * This method does not paginate issues. If a cycle has more issues than - * the limit, only the first N will be returned sorted by creation date. - * - * Linear API limits single requests to 250 items. Values above 250 may - * result in errors or truncation. - * - * To get all issues in a large cycle, either: - * 1. Increase the limit (up to 250) - * 2. Fetch issues separately using the issues API with pagination - * 3. Make multiple requests with cursor-based pagination - */ -async getCycleById(cycleId: string, issuesLimit: number = 50): Promise<any> { -``` - -**Step 2: Add JSDoc to getCycles** - -Before `getCycles` method (line 372), update JSDoc: - -```typescript -/** - * Get all cycles with automatic pagination - * - * @param teamFilter - Optional team key, name, or ID to filter cycles - * @param activeOnly - If true, return only active cycles - * @returns Array of cycles with team information - * - * @remarks - * Uses Linear SDK automatic pagination with 250 cycles per request. - * This method will make multiple API calls if necessary to fetch all - * matching cycles. - * - * For workspaces with hundreds of cycles, consider using team filtering - * to reduce result set size and improve performance. - */ -async getCycles(teamFilter?: string, activeOnly?: boolean): Promise<any[]> { -``` - -**Step 3: Verify no functional changes** - -Run: `git diff src/utils/linear-service.ts` Expected: Only JSDoc comments added - -**Step 4: Commit** - -```bash -git add src/utils/linear-service.ts -git commit -m "[DOC] Add pagination documentation to cycle methods - -Documents: -- Issue fetch limits (250 max per request) -- Automatic pagination in getCycles -- How to handle large datasets" -``` - ---- - -### Task 18: Standardize Error Messages - -**Files:** - -- Modify: `src/commands/cycles.ts:21,32,37` -- Modify: `src/commands/projectMilestones.ts:27,69,76` -- Modify: `src/utils/linear-service.ts:518,531` - -**Step 1: Define error message patterns** - -Create `src/utils/error-messages.ts`: - -```typescript -/** - * Standard error message formatters - */ - -export function notFoundError(entityType: string, identifier: string, context?: string): Error { - const contextStr = context ? ` ${context}` : ""; - return new Error(`${entityType} "${identifier}"${contextStr} not found`); -} - -export function multipleMatchesError( - entityType: string, - identifier: string, - matches: string[], - disambiguation: string, -): Error { - const matchList = matches.join(", "); - return new Error( - `Multiple ${entityType}s found matching "${identifier}". ` + - `Candidates: ${matchList}. ` + - `Please ${disambiguation}.`, - ); -} - -export function invalidParameterError(parameter: string, reason: string): Error { - return new Error(`Invalid ${parameter}: ${reason}`); -} - -export function requiresParameterError(flag: string, requiredFlag: string): Error { - return new Error(`${flag} requires ${requiredFlag} to be specified`); -} -``` - -**Step 2: Use in cycles.ts** - -At top of `src/commands/cycles.ts`, add: - -```typescript -import { invalidParameterError, notFoundError, requiresParameterError } from "../utils/error-messages.js"; -``` - -Change line 21: - -```typescript -throw new Error("--around-active requires --team to be specified"); -``` - -To: - -```typescript -throw requiresParameterError("--around-active", "--team"); -``` - -Change line 32: - -```typescript -throw new Error("--around-active requires a non-negative integer"); -``` - -To: - -```typescript -throw invalidParameterError("--around-active", "requires a non-negative integer"); -``` - -Change line 37: - -```typescript -throw new Error(`No active cycle found for team "${options.team}"`); -``` - -To: - -```typescript -throw notFoundError("Active cycle", options.team, `for team`); -``` - -**Step 3: Use in projectMilestones.ts** - -At top of file, add: - -```typescript -import { multipleMatchesError, notFoundError } from "../utils/error-messages.js"; -``` - -Change line 27: - -```typescript -throw new Error(`Project "${projectNameOrId}" not found`); -``` - -To: - -```typescript -throw notFoundError("Project", projectNameOrId); -``` - -Change line 69: - -```typescript -throw new Error(`Milestone "${milestoneNameOrId}" not found`); -``` - -To: - -```typescript -throw notFoundError("Milestone", milestoneNameOrId); -``` - -Change lines 72-77: - -```typescript -if (nodes.length > 1) { - const projectNames = nodes - .map((m: LinearProjectMilestone) => `"${m.name}" in project "${m.project?.name}"`) - .join(", "); - throw new Error( - `Multiple milestones found with name "${milestoneNameOrId}": ${projectNames}. Please specify --project or use the milestone ID`, - ); -} -``` - -To: - -```typescript -if (nodes.length > 1) { - const matches = nodes.map((m: LinearProjectMilestone) => `"${m.name}" in project "${m.project?.name}"`); - throw multipleMatchesError( - "milestone", - milestoneNameOrId, - matches, - "specify --project or use the milestone ID", - ); -} -``` - -**Step 4: Use in linear-service.ts** - -At top of file, add: - -```typescript -import { multipleMatchesError, notFoundError } from "./error-messages.js"; -``` - -Change line 518: - -```typescript -const context = teamFilter ? ` for team ${teamFilter}` : ""; -throw new Error(`Cycle "${cycleNameOrId}"${context} not found`); -``` - -To: - -```typescript -throw notFoundError("Cycle", cycleNameOrId, teamFilter ? `for team ${teamFilter}` : undefined); -``` - -Change lines 527-532: - -```typescript -if (!chosen) { - const list = nodes.map((n: any) => `${n.id} (${n.team?.key || "?"} / #${n.number} / ${n.startsAt})`).join("; "); - throw new Error( - `Ambiguous cycle name "${cycleNameOrId}" — multiple matches found: ${list}. Please use an ID or scope with --team.`, - ); -} -``` - -To: - -```typescript -if (!chosen) { - const matches = nodes.map((n: any) => `${n.id} (${n.team?.key || "?"} / #${n.number} / ${n.startsAt})`); - throw multipleMatchesError( - "cycle", - cycleNameOrId, - matches, - "use an ID or scope with --team", - ); -} -``` - -**Step 5: Build to check compilation** - -Run: `pnpm run build` Expected: Successful build - -**Step 6: Run tests to ensure error messages still work** - -Run: `pnpm test` Expected: All tests pass (error message content may differ slightly) - -**Step 7: Update tests if needed** - -If tests check exact error message strings, update them to match new format. - -**Step 8: Commit** - -```bash -git add src/utils/error-messages.ts src/commands/cycles.ts src/commands/projectMilestones.ts src/utils/linear-service.ts tests/ -git commit -m "[REFACTOR] Standardize error message formatting - -Created error-messages.ts with standard formatters: -- notFoundError() -- multipleMatchesError() -- invalidParameterError() -- requiresParameterError() - -Applied consistently across cycles and milestones commands." -``` - ---- - -### Task 19: Review and Update tsconfig Test Exclusion - -**Files:** - -- Modify: `tsconfig.json:29-38` (potentially) - -**Step 1: Test current type checking** - -Run: `pnpm exec tsc --noEmit` Expected: No errors (tests excluded) - -**Step 2: Check if tests have type errors** - -Temporarily remove test exclusions from tsconfig.json: - -```json -"exclude": [ - "node_modules", - "dist" -] -``` - -Run: `pnpm exec tsc --noEmit` Expected: Check if tests have type errors - -**Step 3: Analyze results** - -If errors: - -- Are they trivial (vitest globals)? → Add `types: ["vitest/globals"]` to compilerOptions -- Are they real issues? → Fix them -- Are they unavoidable? → Keep tests excluded but document why - -If no errors: - -- Remove test exclusions (better type safety) - -**Step 4: Decision** - -Assuming vitest globals cause issues, update tsconfig.json: - -```json -{ - "compilerOptions": { - "types": [ - "node", - "vitest/globals" - ] - }, - "include": [ - "src/**/*", - "tests/**/*" - ], - "exclude": [ - "node_modules", - "dist" - ] -} -``` - -**Step 5: Verify** - -Run: `pnpm exec tsc --noEmit` Expected: No errors - -**Step 6: Commit** - -```bash -git add tsconfig.json -git commit -m "[CHORE] Include tests in TypeScript type checking - -Added vitest/globals to types and removed test exclusions. -Tests are now type-checked along with source code." -``` - -**Alternative Step 6 (if keeping excluded):** - -Add comment in tsconfig.json: - -```json -"exclude": [ - "node_modules", - "dist", - // Tests excluded from type checking due to vitest globals conflicts - // Tests are validated at runtime by vitest - "tests" -] -``` - -```bash -git add tsconfig.json -git commit -m "[DOC] Document why tests excluded from type checking - -Tests excluded due to vitest globals type conflicts. -Runtime validation by vitest is sufficient." -``` - ---- - -## Execution Plan Complete - -**Total estimated time:** - -- Phase 1 (Critical): 6 minutes -- Phase 2 (Type Safety): 1 hour 5 minutes -- Phase 3 (Service Layer): 55 minutes -- Phase 4 (Testing): 3.5 hours -- Phase 5 (Documentation): 1.5 hours - -**Grand total: ~7 hours of focused work** - ---- - -## Plan complete and saved to `docs/plans/2025-11-09-pr7-post-merge-fixes.md`. - -**Two execution options:** - -**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration - -**2. Parallel Session (separate)** - Open new session with executing-plans skill, batch execution with checkpoints - -**Which approach, Carlo?** diff --git a/docs/plans/2025-11-11-npm-publishing-setup.md b/docs/plans/2025-11-11-npm-publishing-setup.md deleted file mode 100644 index 8f129d6..0000000 --- a/docs/plans/2025-11-11-npm-publishing-setup.md +++ /dev/null @@ -1,378 +0,0 @@ -# npm Publishing Setup for Linearis - -**Date:** 2025-11-11 **Status:** Design Complete - -## Overview - -Set up automated npm publishing for `linearis` using GitHub Actions, triggered by version tags on the main branch. Includes an interactive fish script using gum for streamlined local release workflow. - -## Goals - -- Enable global installation via `npm install -g linearis` -- Automate build and publish process through GitHub Actions -- Maintain clean git history (no compiled artifacts) -- Provide interactive release workflow for version management -- Support date-based versioning scheme: `YYYY.MM.<number>` - -## Package Configuration - -### dist/ Strategy - -**Decision:** Remove dist/ from git control, build on-demand. - -**Rationale:** - -- Git tracks source code, not build artifacts -- GitHub Actions builds fresh for each release -- Reduces merge conflicts and repo bloat -- npm package still contains dist/ via `files` array - -### package.json Changes - -```json -{ - "name": "linearis", - "repository": { - "type": "git", - "url": "https://github.com/czottmann/linearis.git" - }, - "bugs": { - "url": "https://github.com/czottmann/linearis/issues" - }, - "homepage": "https://github.com/czottmann/linearis#readme", - "scripts": { - "prepublishOnly": "pnpm build && pnpm test" - } -} -``` - -**Changes:** - -- Rename package to scoped name `linearis` -- Add repository, bugs, homepage for npm page -- Add `prepublishOnly` script as safety net - -**Already correct:** - -- `files` array includes `dist/` -- Keywords optimize for discoverability -- Author and license fields present - -### .gitignore Changes - -Add to .gitignore: - -``` -# Build output (published to npm but not tracked in git) -dist/ -``` - -## GitHub Actions Workflow - -### Workflow File - -**Location:** `.github/workflows/publish.yml` - -**Trigger:** Tags matching `v*.*.*` on main branch only - -### Workflow Steps - -1. **Tag Validation** - - Extract version from tag (e.g., `v2025.11.3` → `2025.11.3`) - - Validate format matches date-based versioning - -2. **Version Sync Check** - - Verify tag version matches `package.json` version - - Fail if mismatch (prevents accidental publishes) - -3. **Checkout & Setup** - - Checkout repository - - Setup Node.js 22.x - - Install pnpm 10.20.0 - -4. **Install Dependencies** - - Run `pnpm install --frozen-lockfile` - -5. **Build** - - Run `pnpm build` to compile TypeScript to dist/ - -6. **Test** - - Run `pnpm test` to ensure all tests pass - - Fail publish if any tests fail - -7. **Publish to npm** - - Run `npm publish --access public` - - Use `NPM_TOKEN` secret for authentication - - `--access public` required for scoped packages - -### Required Secrets - -**NPM_TOKEN:** npm access token with publish permissions - -**Setup:** - -1. Generate token at npmjs.com (Account Settings → Access Tokens) -2. Add to GitHub repository secrets (Settings → Secrets and variables → Actions) - -### Safety Features - -- Only runs on main branch -- Validates version consistency -- Runs full test suite before publish -- Clear error messages for each validation step - -## Release Workflow - -### Manual Steps (Developer) - -1. **Update version** - Edit `package.json` version field -2. **Update changelog** - Add release notes to `CHANGELOG.md` -3. **Commit changes** - Commit version bump and changelog -4. **Create tag** - `git tag -a v2025.11.3 -m "Release 2025.11.3"` -5. **Push tag** - `git push origin v2025.11.3` -6. **Verify** - Check GitHub Actions and npmjs.com - -### Automated Steps (GitHub Actions) - -1. Build fresh dist/ from source -2. Run test suite -3. Validate versions match -4. Publish to npm registry -5. Create GitHub release (optional future enhancement) - -## Interactive Release Script - -### Script Details - -**Location:** `scripts/release.fish` - -**Dependencies:** - -- fish shell -- gum (https://github.com/charmbracelet/gum) - -### Script Flow - -1. **Prerequisites Validation** - - Check on main branch - - Verify working tree is clean - - Confirm gum is installed - - Exit with clear error if any check fails - -2. **Version Calculation** - - Parse current version from package.json - - Calculate next version suggestion: - - Same month: increment last number (2025.11.2 → 2025.11.3) - - New month: reset to .1 (2025.11.3 → 2025.12.1) - -3. **Interactive Version Input** - - Display current version - - Show suggested next version - - Use `gum input` with default value - - Validate format matches YYYY.MM.N - -4. **Changelog Entry** - - Use `gum write` for multi-line release notes - - Support markdown formatting - - ESC to cancel, Ctrl+D to finish - -5. **File Updates** - - Update package.json version field - - Prepend changelog entry with date and version - - Show summary of changes - -6. **Preview Changes** - - Display git diff of modified files - - Use `gum confirm` "Proceed with commit and tag?" - - Cancel option available - -7. **Commit & Tag** - - Commit with message: `[CHORE] Release YYYY.MM.N` - - Create annotated tag: `git tag -a vYYYY.MM.N -m "Release YYYY.MM.N"` - -8. **Push Confirmation** - - Use `gum confirm` "Push tag to trigger publish?" - - Shows what will happen (commit + tag push) - -9. **Push to Origin** - - Push commit to main - - Push tag (triggers GitHub Actions) - - Display success message with next steps - -### Error Handling - -- Exit gracefully at any step -- Clear error messages for validation failures -- Rollback option if user cancels after commit but before push - -### Visual Polish - -- Use gum styling for consistent UX -- Spinners for long operations -- Color coding for errors/success -- Formatted output sections - -## Testing Strategy - -### Pre-Publish Testing - -1. **Local Package Test** - ```bash - npm pack - npm install -g ./czottmann-linearis-2025.11.2.tgz - linearis --help - ``` - -2. **Dry-Run Publish** - ```bash - npm publish --dry-run - ``` - - Verify files list includes dist/ - - Check package size is reasonable - - Confirm no sensitive files included - -3. **Test Release Script** - - Create test branch - - Run `scripts/release.fish` - - Verify all prompts work correctly - - Test cancellation at various stages - -### Post-Publish Verification - -1. **npm Registry** - - Visit https://www.npmjs.com/package/linearis - - Verify version, description, keywords - - Check repository links work - -2. **Installation Test** - ```bash - npm install -g linearis - linearis --help - linearis usage - ``` - -3. **GitHub Actions** - - Review workflow run logs - - Confirm all steps passed - - Verify timing is reasonable - -## Documentation Updates - -### README.md - -Add installation section: - -````markdown -## Installation - -### npm (recommended) - -```bash -npm install -g linearis -``` -```` - -### From source - -```bash -git clone https://github.com/czottmann/linearis.git -cd linearis -pnpm install -pnpm build -npm link -``` - -```` -### Release Process Documentation - -Add to CONTRIBUTING.md or README: - -```markdown -## Releasing - -Use the interactive release script: - -```bash -./scripts/release.fish -```` - -The script will: - -1. Suggest the next version number -2. Prompt for changelog entry -3. Update package.json and CHANGELOG.md -4. Create commit and tag -5. Push to trigger automated npm publish - -### Manual release process - -1. Update version in `package.json` -2. Add entry to `CHANGELOG.md` -3. Commit: `git commit -m "[CHORE] Release YYYY.MM.N"` -4. Tag: `git tag -a vYYYY.MM.N -m "Release YYYY.MM.N"` -5. Push: `git push origin main && git push origin vYYYY.MM.N` - -``` -## Implementation Checklist - -### Initial Setup - -- [ ] Add `dist/` to .gitignore -- [ ] Remove dist/ from git: `git rm -r --cached dist/` -- [ ] Update package.json (name, repository, bugs, homepage, prepublishOnly) -- [ ] Create `.github/workflows/publish.yml` -- [ ] Generate npm access token -- [ ] Add `NPM_TOKEN` to GitHub repository secrets -- [ ] Create `scripts/release.fish` -- [ ] Update README.md with installation instructions -- [ ] Document release process - -### Pre-First-Publish Testing - -- [ ] Run `npm pack` and test local installation -- [ ] Run `npm publish --dry-run` -- [ ] Test release script on test branch -- [ ] Verify GitHub Actions workflow syntax - -### First Publish - -- [ ] Run release script for real -- [ ] Monitor GitHub Actions workflow -- [ ] Verify package on npmjs.com -- [ ] Test global installation -- [ ] Verify binary works correctly - -### Post-Publish - -- [ ] Update CHANGELOG.md with publish date -- [ ] Create GitHub release (optional) -- [ ] Announce to users (optional) - -## Version Scheme Reference - -**Format:** `YYYY.MM.<number>` - -**Rules:** -- Year and month are calendar-based -- Number starts at 1 each month -- Number increments for each release in the month -- Number resets to 1 on new month - -**Examples:** -- `2025.11.1` - First release in November 2025 -- `2025.11.2` - Second release in November 2025 -- `2025.12.1` - First release in December 2025 - -**Git tags:** Prefix with `v` (e.g., `v2025.11.1`) - -## Future Enhancements - -Potential improvements for later: - -- Automated GitHub release creation with changelog -- npm provenance statements (SLSA attestation) -- Automated version bump detection from commits -- Pre-release/beta publishing workflow -- Automated changelog generation from commits -- Package size monitoring and alerts -``` diff --git a/docs/project-overview.md b/docs/project-overview.md index 88a8648..027cebf 100644 --- a/docs/project-overview.md +++ b/docs/project-overview.md @@ -1,56 +1,70 @@ -<!-- Generated: 2025-01-09T12:34:56+00:00 --> - # Project Overview -Linearis is a high-performance Command Line Interface (CLI) tool for Linear.app that outputs structured JSON data. It's specifically designed for LLM agents and users who prefer structured output over web interfaces. Built with TypeScript and Node.js, the tool provides complete Linear API coverage with smart ID resolution and optimized performance. +Linearis is a command-line interface for [Linear.app](https://linear.app) that outputs structured JSON. It is built for automation, scripting, and integration with other tools, including LLM agents. -The CLI eliminates common performance bottlenecks found in API integrations, achieving 90%+ speed improvements over parallel direct Linear SDK calls through optimized GraphQL batch operations and single-query strategies. All commands return JSON-formatted responses, making it ideal for automation, scripting, and integration with other tools. +All commands return JSON-formatted responses. Human-friendly identifiers (such as team keys like `ENG` or issue identifiers like `ENG-42`) are automatically resolved to internal UUIDs before any API call is made. -The tool supports comprehensive issue management (create, read, update, list, search), project operations, comment handling, and enhanced label management with intelligent conversion between user-friendly identifiers (like ABC-123) and internal UUIDs. +## Architecture -## Key Files +The codebase follows a five-layer architecture. Each layer has a specific responsibility and a strict client contract. -### Main Entry Points +| Layer | Directory | Responsibility | Client | +|-------|-----------|---------------|--------| +| Client | `src/client/` | Low-level API wrappers | -- | +| Resolver | `src/resolvers/` | Human ID to UUID conversion | LinearSdkClient | +| Service | `src/services/` | Business logic and CRUD operations | GraphQLClient | +| Command | `src/commands/` | CLI orchestration via Commander.js | Both (via `createContext()`) | +| Common | `src/common/` | Shared utilities, types, error handling | -- | -- **src/main.ts** - CLI entry point with Commander.js setup and command routing -- **package.json** - Project configuration with Node.js >= 22.0.0 requirement +Data flows in one direction: -### Core Configuration Files +``` +CLI Input -> Command -> Resolver -> Service -> JSON Output +``` -- **CLAUDE.md** - AI-specific project instructions and development guidelines -- **README.md** - User-facing documentation with usage examples and setup instructions +Commands receive user input, resolve any identifiers to UUIDs through the resolver layer, then delegate to services for the actual API operations. Services never perform ID resolution, and resolvers never perform data mutations. ## Technology Stack -### Core Technologies with File Examples +- **TypeScript** with strict mode enabled and no `any` types +- **Node.js** >= 22.0.0, ES modules throughout +- **Commander.js** v14.0.0 for CLI structure +- **Linear SDK** v58.1.0 for the SDK client used in resolvers +- **GraphQL Codegen** for type-safe query and mutation documents +- **Vitest** for unit testing +- **tsx** for development execution + +## Key Entry Points -- **TypeScript** - Full type safety implementation (all .ts files in src/) -- **Node.js >= 22.0.0** - Modern runtime with ES modules support (package.json engines field) -- **Commander.js v14.0.0** - CLI framework used in src/main.ts for command structure -- **Linear SDK v58.1.0** - GraphQL API integration with optimized service layer in src/utils/graphql-service.ts and src/utils/linear-service.ts -- **tsx v4.20.5** - TypeScript execution engine for development (package.json scripts.start) +- `src/main.ts` -- CLI entry point, registers all commands +- `src/common/context.ts` -- `createContext()` factory that provides both clients +- `src/common/auth.ts` -- authentication resolution -### Package Management +## Authentication -- **npm** - Package manager -- **package-lock.json** - Lock file ensuring reproducible builds +Interactive setup (for humans): `linearis auth login` — opens Linear in the browser and stores the token encrypted in `~/.linearis/token`. -## Platform Support +Token resolution order: -### Development Environment Requirements +1. `--api-token` CLI flag +2. `LINEAR_API_TOKEN` environment variable +3. `~/.linearis/token` (encrypted, set up via `linearis auth login`) +4. `~/.linear_api_token` (deprecated) -- **Node.js >= 22.0.0** - Required runtime version specified in package.json engines -- **mise.toml** - Development environment tool configuration with Node.js 22 and Deno 2.2.8 -- **TypeScript 5.0.0** - Type system and compilation support (devDependencies) +## Build and Development -### Operating System Support +| Command | Description | +|---------|-------------| +| `npm start` | Run in development mode via tsx | +| `npm run build` | Compile to `dist/` | +| `npm test` | Run the test suite | +| `npm run generate` | Regenerate GraphQL types from `.graphql` files | -- **Cross-platform compatibility** - Node.js application runs on Windows, macOS, and Linux -- **Authentication file support** - `$HOME/.linear_api_token` works across all platforms +The compiled binary is `dist/main.js`. -### Build and Execution +## Package Information -- **Direct execution** - `npm start <command>` for development (package.json scripts) -- **Production build** - `npm run build` creates executable dist/main.js with optimized performance -- **TypeScript compilation** - `npx tsx src/main.ts <command>` for manual execution -- **ES modules** - Modern module system enabled via package.json type: "module" +- **Name:** linearis +- **License:** MIT +- **Node.js:** >= 22.0.0 +- **Module system:** ES modules diff --git a/docs/testing.md b/docs/testing.md index 8cd56f9..bb2b8b5 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,144 +1,182 @@ # Testing -Linearis uses [Vitest](https://vitest.dev/) for automated testing, combining unit tests with mocks and integration tests against the compiled CLI. The testing framework was introduced in PR #4 to establish automated testing practices. +Linearis uses [Vitest](https://vitest.dev/) for unit and integration tests. Tests enforce the layered architecture by mocking one layer deep, keeping each layer testable in isolation. -## Overview - -Testing approach combines multiple strategies: - -- **Unit tests**: Test individual functions/methods in isolation with mocks -- **Integration tests**: Test CLI commands end-to-end with compiled binary -- **Type safety**: TypeScript compile-time validation -- **Performance testing**: Manual benchmarking against Linear API - -## Quick Start +## Running Tests ```bash -# Install dependencies -npm install - -# Run all tests -npm test - -# Run tests in watch mode -npm test:watch +npm test # Run all tests once +npm run test:watch # Watch mode (re-runs on changes) +npm run test:ui # Interactive UI +npm run test:coverage # Generate coverage report +npm run test:commands # CLI command coverage report +``` -# Run with UI -npm test:ui +Run a specific file or suite: -# Generate coverage report -npm test:coverage +```bash +npx vitest run tests/unit/resolvers +npx vitest run tests/unit/services/issue-service.test.ts +npx vitest run -t "should resolve team by key" ``` ## Test Structure ``` tests/ -├── unit/ # Unit tests (fast, use mocks) -│ └── linear-service-cycles.test.ts -└── integration/ # Integration tests (slower, real CLI) - ├── cycles-cli.test.ts - └── project-milestones-cli.test.ts -``` + unit/ + client/ + graphql-client.test.ts + resolvers/ + team-resolver.test.ts + project-resolver.test.ts + issue-resolver.test.ts + label-resolver.test.ts + cycle-resolver.test.ts + status-resolver.test.ts + milestone-resolver.test.ts + services/ + issue-service.test.ts + document-service.test.ts + attachment-service.test.ts + common/ + identifier.test.ts + errors.test.ts + output.test.ts + integration/ + cycles-cli.test.ts + documents-cli.test.ts + issues-cli.test.ts + project-milestones-cli.test.ts + teams-cli.test.ts + users-cli.test.ts + command-coverage.ts +``` + +The test directory mirrors `src/`. Each layer has its own mock strategy described below. + +## Mock Patterns + +Each architectural layer uses a different mock target. The rule is simple: mock the dependency one layer down. + +### Resolver Tests + +Resolvers depend on `LinearSdkClient`. Mock the SDK methods it calls: -## Running Tests +```typescript +import type { LinearSdkClient } from "../../src/client/linear-client.js"; -### All Tests +const mockSdk = { + teams: vi.fn().mockResolvedValue({ + nodes: [{ id: "uuid-123", key: "ABC" }], + }), +}; +const client = { sdk: mockSdk } as unknown as LinearSdkClient; +``` -```bash -# Run all tests once -npm test +### Service Tests + +Services depend on `GraphQLClient`. Mock the `request` method: -# Run in watch mode (re-runs on changes) -npm test:watch +```typescript +import type { GraphQLClient } from "../../src/client/graphql-client.js"; -# Run with interactive UI -npm test:ui +const mockRequest = vi.fn().mockResolvedValue({ + issues: { nodes: [{ id: "123", title: "Bug" }] }, +}); +const client = { request: mockRequest } as unknown as GraphQLClient; ``` -### Specific Test Suites +### Common Tests -```bash -# Unit tests only -npx vitest run tests/unit +Functions in `common/` are pure and need no mocks: -# Integration tests only -npx vitest run tests/integration +```typescript +import { isUuid } from "../../src/common/identifier.js"; + +expect(isUuid("550e8400-e29b-41d4-a716-446655440000")).toBe(true); +expect(isUuid("ABC-123")).toBe(false); +``` + +### Client Tests -# Specific test file -npx vitest run tests/unit/linear-service-cycles.test.ts +Client tests mock the underlying network layer: -# Run single test by name -npx vitest run -t "should fetch cycles without filters" +```typescript +const mockClient = { rawRequest: vi.fn() }; ``` -## Unit Tests +## Writing a New Test -Unit tests verify individual functions and methods in isolation using mocks to avoid external dependencies. +1. Create a test file in the directory matching the source file's layer (`tests/unit/resolvers/`, `tests/unit/services/`, etc.). +2. Mock the client type that the layer depends on (see patterns above). +3. Cover at least the happy path and the primary error case (e.g., entity not found). -### Example: Testing LinearService +Example resolver test: ```typescript -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { LinearService } from "../../src/utils/linear-service.js"; +import { describe, expect, it, vi } from "vitest"; +import type { LinearSdkClient } from "../../../src/client/linear-client.js"; +import { resolveTeamId } from "../../../src/resolvers/team-resolver.js"; + +describe("resolveTeamId", () => { + it("should return UUID as-is", async () => { + const client = { sdk: {} } as unknown as LinearSdkClient; + const result = await resolveTeamId(client, "550e8400-e29b-41d4-a716-446655440000"); + expect(result).toBe("550e8400-e29b-41d4-a716-446655440000"); + }); -describe("LinearService - getCycles()", () => { - let mockClient: any; - let service: LinearService; + it("should resolve team by key", async () => { + const mockSdk = { + teams: vi.fn().mockResolvedValue({ + nodes: [{ id: "uuid-456", key: "ENG" }], + }), + }; + const client = { sdk: mockSdk } as unknown as LinearSdkClient; - beforeEach(() => { - mockClient = { cycles: vi.fn() }; - service = new LinearService("fake-token"); - service.client = mockClient; + const result = await resolveTeamId(client, "ENG"); + expect(result).toBe("uuid-456"); }); - it("should fetch cycles without filters", async () => { - mockClient.cycles.mockResolvedValue({ - nodes: [{ id: "cycle-1", name: "Sprint 1" }], - }); - - const result = await service.getCycles(); + it("should throw when team is not found", async () => { + const mockSdk = { + teams: vi.fn().mockResolvedValue({ nodes: [] }), + }; + const client = { sdk: mockSdk } as unknown as LinearSdkClient; - expect(result).toHaveLength(1); - expect(result[0].name).toBe("Sprint 1"); + await expect(resolveTeamId(client, "NOPE")).rejects.toThrow(); }); }); ``` -### Running Unit Tests +## Coverage -```bash -# Run all unit tests -npx vitest run tests/unit +Generate an HTML coverage report: -# Watch mode for development -npx vitest tests/unit +```bash +npm run test:coverage +open coverage/index.html ``` -**No API token required** - unit tests use mocks and run offline. +Code coverage tracks unit tests only. Integration tests run the CLI in a subprocess and are not captured in coverage reports. -## Integration Tests +The command coverage report (`npm run test:commands`) shows which CLI commands have integration test coverage and which ones still need it. -Integration tests verify CLI commands work end-to-end by executing the compiled binary and validating JSON output. +## Integration Tests -### Setup for Integration Tests +Integration tests execute the compiled CLI binary and validate its JSON output. They require a real Linear API token. -Integration tests require a Linear API token: +### Setup ```bash -# Set your Linear API token export LINEAR_API_TOKEN="lin_api_..." - -# Build the CLI first npm run build - -# Run integration tests npx vitest run tests/integration ``` If `LINEAR_API_TOKEN` is not set, integration tests are automatically skipped. -### Example: Testing CLI Commands +### Example ```typescript import { describe, expect, it } from "vitest"; @@ -149,373 +187,37 @@ const execAsync = promisify(exec); const hasApiToken = !!process.env.LINEAR_API_TOKEN; describe("Cycles CLI", () => { - it.skipIf(!hasApiToken)("should list cycles", async () => { - const { stdout, stderr } = await execAsync( - "node ./dist/main.js cycles list", - ); - - // Verify no complexity errors (PR #4 bug fix) - expect(stderr).not.toContain("query too complex"); - - // Verify valid JSON output + it.skipIf(!hasApiToken)("should list cycles as JSON", async () => { + const { stdout } = await execAsync("node ./dist/main.js cycles list"); const cycles = JSON.parse(stdout); expect(Array.isArray(cycles)).toBe(true); }); }); ``` -## Coverage Reports - -### Code Coverage (Unit Tests) - -Generate code coverage reports to track which source code lines are executed: - -```bash -# Run tests with coverage -npm test:coverage -``` - -Coverage reports generated: - -- `coverage/index.html` - Visual HTML report -- `coverage/coverage-final.json` - JSON data - -View the report: - -```bash -open coverage/index.html -``` - -**Note**: Code coverage only tracks unit tests. Integration tests run CLI in separate processes and don't show up in code coverage reports. - -### Command Coverage (Integration Tests) - -See which CLI commands have integration test coverage: - -```bash -# Run command coverage report -npm test:commands -``` - -This shows: - -- ✅ Which commands have integration tests -- ⚠️ Which commands need testing -- 📊 Overall % of commands covered -- 📋 List of untested commands - -Example output: - -``` -📊 CLI Command Coverage Report - -✅ cycles (cycles.ts) - ✅ ├─ list - ✅ ├─ read - -❌ issues (issues.ts) - ⚠️ ├─ create - ⚠️ ├─ list - ⚠️ ├─ read - -📈 Summary -Commands: 3/6 tested (50.0%) -Subcommands: 4/14 tested (28.6%) -Overall: 7/20 tested (35.0%) -``` - -**This is the metric you care about for CLI tools!** It shows which commands users can actually run that are verified by tests. - -## Continuous Integration - -Tests run automatically on every push and pull request via GitHub Actions. - -### CI Workflow (`.github/workflows/ci.yml`) - -**Test Job**: - -1. Installs dependencies with npm -2. Builds the project -3. Runs all tests -4. Runs integration tests if `LINEAR_API_TOKEN` secret is configured - -**Lint Job**: - -1. Type checks with TypeScript -2. Verifies clean build - -### Configuring CI Secrets - -To enable integration tests in CI: - -1. Go to: Repository Settings → Secrets and variables → Actions -2. Add: `LINEAR_API_TOKEN` with your Linear API token -3. Integration tests will run automatically on all PRs - -**Note**: Be careful with API tokens in CI - they grant access to your Linear workspace. - -## Test Examples from PR #4 - -### Unit Tests (linear-service-cycles.test.ts) - -Tests for new cycle methods added in PR #4: - -- ✅ `getCycles()` fetches cycles without filters -- ✅ `getCycles()` fetches cycles with team filter -- ✅ `getCycles()` fetches only active cycles -- ✅ `getCycles()` converts dates to strings -- ✅ `getCycleById()` fetches cycle with issues -- ✅ `getCycleById()` uses default issues limit -- ✅ `resolveCycleId()` returns UUID as-is -- ✅ `resolveCycleId()` resolves cycle by name -- ✅ `resolveCycleId()` resolves with team filter -- ✅ `resolveCycleId()` throws error when not found -- ✅ `resolveCycleId()` disambiguates by preferring active -- ✅ `resolveCycleId()` disambiguates by preferring next -- ✅ `resolveCycleId()` throws error for ambiguous names - -### Integration Tests (cycles-cli.test.ts) - -Tests for cycles command functionality: - -- ✅ `cycles --help` displays help text -- ✅ `cycles list` works without complexity errors -- ✅ `cycles list` returns valid JSON structure -- ✅ `cycles list --active` filters active cycles -- ✅ `cycles list --around-active` works correctly -- ✅ `cycles list --around-active` requires --team flag -- ✅ `cycles read <id>` reads cycle by ID -- ✅ `cycles read <name>` reads cycle by name with team - -### Integration Tests (project-milestones-cli.test.ts) - -Tests for command naming fix: - -- ✅ `project-milestones --help` displays help -- ✅ Command appears in main help as `project-milestones` -- ✅ Old camelCase `projectMilestones` fails appropriately -- ✅ `project-milestones list` requires --project flag -- ✅ `project-milestones list` works with valid project - -## Writing New Tests - -### When to Write Unit Tests - -Write unit tests for: - -- Complex business logic -- Data transformations -- Error handling -- Edge cases and boundary conditions - -### When to Write Integration Tests - -Write integration tests for: - -- New CLI commands -- New command flags -- Critical user workflows -- Bug fixes (regression prevention) - -### Test Naming Convention - -```typescript -describe("ComponentName - methodName()", () => { - it("should do something specific", async () => { - // Arrange - const input = { data: "test" }; - - // Act - const result = await methodName(input); - - // Assert - expect(result).toBe(expected); - }); -}); -``` - -### Testing Best Practices - -1. **Descriptive names**: Test names should clearly describe behavior -2. **One concept per test**: Each test verifies one specific behavior -3. **Arrange-Act-Assert**: Structure tests in three clear phases -4. **Mock external dependencies**: Unit tests shouldn't call real APIs -5. **Test error cases**: Always test both success and failure paths -6. **Keep tests fast**: Unit tests should complete in milliseconds -7. **Make tests deterministic**: Avoid flaky tests with random data or timing - -## Manual Testing - -While automated tests are preferred, some scenarios still require manual testing: - -### Issue Operations - -```bash -# Test issue listing -npm start issues list -l 5 - -# Test issue reading with ID resolution -npm start issues read ABC-123 - -# Test issue creation -npm start issues create --title "Test Issue" --team ABC +## CI -# Test issue search with filters -npm start issues search "bug" --team ABC --project "Mobile App" -``` - -### Project Operations - -```bash -# Test project listing -npm start projects list - -# Test project reading with name resolution -npm start projects read "Mobile App" -``` - -### Authentication Testing - -```bash -# Test with API token flag -npm start --api-token <token> issues list - -# Test with environment variable -LINEAR_API_TOKEN=<token> npm start issues list - -# Test with token file -echo "<token>" > ~/.linear_api_token && npm start issues list -``` - -## Performance Testing - -### Benchmark Commands - -Performance benchmarks from PERFORMANCE.md: - -```bash -# Time command execution -time npm start issues list -l 10 - -# Monitor single issue performance -time npm start issues read ABC-123 - -# Test search performance -time npm start issues search "test" --team ABC - -# Cycles performance test (PR #4 fix verification) -time npm start cycles list --team Backend -``` +GitHub Actions runs on every push and pull request: -### Current Benchmarks +1. Install dependencies +2. Build the project +3. Run all unit tests +4. Run integration tests (only if the `LINEAR_API_TOKEN` secret is configured in the repository) -- Single issue read: ~0.9-1.1 seconds (90%+ improvement) -- List 10 issues: ~0.9 seconds (95%+ improvement) -- Create issue: ~1.1 seconds (50%+ improvement) - -## Debugging Tests - -### Run with Verbose Output - -```bash -npx vitest run --reporter=verbose -``` - -### Debug in VS Code - -Add to `.vscode/launch.json`: - -```json -{ - "type": "node", - "request": "launch", - "name": "Debug Vitest Tests", - "runtimeExecutable": "npx", - "runtimeArgs": ["vitest", "run", "--no-coverage"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen" -} -``` - -Set breakpoints in test files and press F5 to debug. +To enable integration tests in CI, add `LINEAR_API_TOKEN` under Repository Settings > Secrets and variables > Actions. ## Troubleshooting -### "Cannot find module" Errors - -Ensure project is built: - -```bash -npm run build -``` - -### Integration Tests Skipped - -Set your Linear API token: - -```bash -export LINEAR_API_TOKEN="lin_api_..." -``` - -### Tests Timeout - -Integration tests have 30-second timeout. If timing out: +**Tests fail with "Cannot find module"** -- Run `npm run build` to compile the project. Integration tests need the compiled output in `dist/`. -- Check internet connection -- Verify Linear API is accessible -- Confirm API token is valid +**Integration tests are skipped** -- Set `LINEAR_API_TOKEN` in your environment. -Increase timeout for specific test: +**Tests time out** -- Integration tests default to a 30-second timeout. Check your network connection and API token validity. You can increase the timeout for a specific test: ```typescript -it("slow test", async () => { - // test code -}, { timeout: 60000 }); // 60 seconds +it("slow operation", async () => { + // ... +}, { timeout: 60000 }); ``` -### Mock Not Working - -Use Vitest's `vi.fn()`, not Jest's `jest.fn()`: - -```typescript -import { vi } from "vitest"; - -const mockFn = vi.fn(); -mockFn.mockResolvedValue({ data: "test" }); -``` - -### Type Errors in Tests - -Ensure you're importing from correct paths with `.js` extension: - -```typescript -import { LinearService } from "../../src/utils/linear-service.js"; -``` - -## Test Coverage Goals - -Current coverage (as of PR #4): - -- Unit tests: LinearService cycle methods -- Integration tests: Cycles and project-milestones commands - -Future coverage goals: - -- Authentication flows (src/utils/auth.ts) -- Smart ID resolution (src/utils/linear-service.ts) -- All command handlers (src/commands/*.ts) -- Error handling (src/utils/output.ts) -- GraphQL service methods (src/utils/graphql-service.ts) - -## Resources - -- [Vitest Documentation](https://vitest.dev/) -- [Vitest API Reference](https://vitest.dev/api/) -- [Testing Best Practices](https://vitest.dev/guide/testing-patterns.html) -- [GitHub Actions Documentation](https://docs.github.com/en/actions) - -## Configuration Files - -- `vitest.config.ts` - Vitest configuration -- `.github/workflows/ci.yml` - CI/CD workflow -- `package.json` - Test scripts and dependencies +**Type errors in test imports** -- Use `.js` extensions in import paths, matching the ES module convention used throughout the project. diff --git a/graphql/mutations/attachments.graphql b/graphql/mutations/attachments.graphql new file mode 100644 index 0000000..ad4a9f5 --- /dev/null +++ b/graphql/mutations/attachments.graphql @@ -0,0 +1,32 @@ +# ------------------------------------------------------------ +# GraphQL mutations for Linear attachments +# +# Attachments allow linking any URL to an issue. This is the mechanism +# to associate documents (or any external resource) with issues, since +# documents cannot be directly linked to issues in Linear's data model. +# +# Key behavior: Attachments are idempotent - creating an attachment with +# the same url + issueId will update the existing attachment. +# ------------------------------------------------------------ + +# Create an attachment on an issue +# +# If an attachment with the same url and issueId already exists, +# the existing record is updated instead of creating a duplicate. +mutation AttachmentCreate($input: AttachmentCreateInput!) { + attachmentCreate(input: $input) { + success + attachment { + ...AttachmentFields + } + } +} + +# Delete an attachment +# +# Deletes an attachment and returns success status. +mutation AttachmentDelete($id: String!) { + attachmentDelete(id: $id) { + success + } +} diff --git a/graphql/mutations/comments.graphql b/graphql/mutations/comments.graphql new file mode 100644 index 0000000..264144f --- /dev/null +++ b/graphql/mutations/comments.graphql @@ -0,0 +1,41 @@ +# ------------------------------------------------------------ +# GraphQL mutations for Linear issue comments +# +# Comments are discussion threads on issues. Each comment has a body +# (markdown-formatted text), author, and timestamp. Comments cannot +# be nested - all comments on an issue are at the same level. +# +# Key behavior: Comment body supports full markdown including code +# blocks, links, and mentions (@user). Comments cannot be edited via +# API - only created and deleted. Use commentUpdate mutation if it +# becomes available. +# ------------------------------------------------------------ + +# Comment fields fragment +# +# Defines the basic fields for a comment entity. Body is markdown, +# createdAt is ISO 8601 timestamp. +fragment CommentFields on Comment { + id + body + createdAt +} + +# Create a comment on an issue +# +# Creates a new comment on the specified issue. The comment body +# supports markdown formatting including mentions and code blocks. +# +# Input fields (CommentCreateInput): +# issueId: UUID of the issue to comment on (required) +# body: Comment text in markdown format (required) +# +# Returns: success flag and the created comment with its fields +mutation CreateComment($input: CommentCreateInput!) { + commentCreate(input: $input) { + success + comment { + ...CommentFields + } + } +} diff --git a/graphql/mutations/documents.graphql b/graphql/mutations/documents.graphql new file mode 100644 index 0000000..e0aa43b --- /dev/null +++ b/graphql/mutations/documents.graphql @@ -0,0 +1,40 @@ +# ------------------------------------------------------------ +# GraphQL mutations for Linear documents +# +# Documents are standalone entities that can be associated with projects, +# initiatives, or teams. To link a document to an issue, use the +# attachments API (see attachments.graphql). +# ------------------------------------------------------------ + +# Create a new document mutation +# +# Creates a new document and returns the created document data. +mutation DocumentCreate($input: DocumentCreateInput!) { + documentCreate(input: $input) { + success + document { + ...DocumentFields + } + } +} + +# Update an existing document mutation +# +# Updates an existing document and returns the updated document data. +mutation DocumentUpdate($id: String!, $input: DocumentUpdateInput!) { + documentUpdate(id: $id, input: $input) { + success + document { + ...DocumentFields + } + } +} + +# Delete a document mutation +# +# Deletes a document and returns success status. +mutation DocumentDelete($id: String!) { + documentDelete(id: $id) { + success + } +} diff --git a/graphql/mutations/files.graphql b/graphql/mutations/files.graphql new file mode 100644 index 0000000..f3ea2a1 --- /dev/null +++ b/graphql/mutations/files.graphql @@ -0,0 +1,22 @@ +# ------------------------------------------------------------ +# GraphQL mutations for Linear files +# +# Uploads a file to Linear's cloud storage. +# ------------------------------------------------------------ + +# Upload a file to Linear's cloud storage. +# +# Returns the upload URL and asset URL for the uploaded file. +mutation FileUpload($contentType: String!, $filename: String!, $size: Int!) { + fileUpload(contentType: $contentType, filename: $filename, size: $size) { + success + uploadFile { + uploadUrl + assetUrl + headers { + key + value + } + } + } +} diff --git a/graphql/mutations/issue-relations.graphql b/graphql/mutations/issue-relations.graphql new file mode 100644 index 0000000..021bc34 --- /dev/null +++ b/graphql/mutations/issue-relations.graphql @@ -0,0 +1,58 @@ +# ------------------------------------------------------------ +# GraphQL operations for issue relation management +# ------------------------------------------------------------ + +# Fragment for relation fields used in issue output +fragment IssueRelationFields on IssueRelation { + id + type + relatedIssue { + id + identifier + } +} + +# Fragment for inverse relation fields used in issue output +fragment InverseIssueRelationFields on IssueRelation { + id + type + issue { + id + identifier + } +} + +# Create a relation between two issues +mutation CreateIssueRelation($input: IssueRelationCreateInput!) { + issueRelationCreate(input: $input) { + success + issueRelation { + ...IssueRelationFields + } + } +} + +# Delete a relation by ID +mutation DeleteIssueRelation($id: String!) { + issueRelationDelete(id: $id) { + success + entityId + } +} + +# Find relations between a specific issue and another issue +# Used by --remove-relation to locate the relation ID before deletion +query GetIssueRelations($issueId: String!) { + issue(id: $issueId) { + relations { + nodes { + ...IssueRelationFields + } + } + inverseRelations { + nodes { + ...InverseIssueRelationFields + } + } + } +} diff --git a/graphql/mutations/issues.graphql b/graphql/mutations/issues.graphql new file mode 100644 index 0000000..ce1dc8d --- /dev/null +++ b/graphql/mutations/issues.graphql @@ -0,0 +1,34 @@ +# ------------------------------------------------------------ +# Optimized GraphQL mutations for issue operations +# +# This module contains highly optimized GraphQL mutations that create +# and update issues with complete response data including all relationships. +# ------------------------------------------------------------ + +# Create issue mutation with complete response +# +# Creates a new issue and returns complete issue data including +# all relationships. Uses the comprehensive fragment to ensure +# consistent data structure with read operations. +mutation CreateIssue($input: IssueCreateInput!) { + issueCreate(input: $input) { + success + issue { + ...CompleteIssueWithCommentsFields + } + } +} + +# Update issue mutation with complete response +# +# Updates an existing issue and returns complete issue data with +# all relationships. Ensures consistency between update and read +# operations by using the same fragment structure. +mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) { + issueUpdate(id: $id, input: $input) { + success + issue { + ...CompleteIssueWithCommentsFields + } + } +} diff --git a/graphql/mutations/project-milestones.graphql b/graphql/mutations/project-milestones.graphql new file mode 100644 index 0000000..7889675 --- /dev/null +++ b/graphql/mutations/project-milestones.graphql @@ -0,0 +1,74 @@ +# ------------------------------------------------------------ +# GraphQL mutations for Linear project milestones +# +# Creates and updates project milestones with complete response data including all relationships. +# ------------------------------------------------------------ + +# Create a new project milestone +# +# Creates a new project milestone and returns the created project milestone data. +mutation CreateProjectMilestone( + $projectId: String! + $name: String! + $description: String + $targetDate: TimelessDate +) { + projectMilestoneCreate( + input: { + projectId: $projectId + name: $name + description: $description + targetDate: $targetDate + } + ) { + success + projectMilestone { + id + name + description + targetDate + sortOrder + createdAt + updatedAt + project { + id + name + } + } + } +} + +# Update an existing project milestone +# +# Updates an existing project milestone and returns the updated project milestone data. +mutation UpdateProjectMilestone( + $id: String! + $name: String + $description: String + $targetDate: TimelessDate + $sortOrder: Float +) { + projectMilestoneUpdate( + id: $id + input: { + name: $name + description: $description + targetDate: $targetDate + sortOrder: $sortOrder + } + ) { + success + projectMilestone { + id + name + description + targetDate + sortOrder + updatedAt + project { + id + name + } + } + } +} diff --git a/graphql/queries/attachments.graphql b/graphql/queries/attachments.graphql new file mode 100644 index 0000000..511a6ae --- /dev/null +++ b/graphql/queries/attachments.graphql @@ -0,0 +1,35 @@ +# ------------------------------------------------------------ +# GraphQL queries for Linear attachments +# +# Attachments allow linking any URL to an issue. This is the mechanism +# to associate documents (or any external resource) with issues, since +# documents cannot be directly linked to issues in Linear's data model. +# +# Key behavior: Attachments are idempotent - creating an attachment with +# the same url + issueId will update the existing attachment. +# ------------------------------------------------------------ + +# Attachment fields fragment +# +# Defines the basic fields for an attachment entity. +fragment AttachmentFields on Attachment { + id + title + subtitle + url + createdAt + updatedAt +} + +# List attachments on an issue +# +# Fetches a list of attachments for a given issue. +query ListAttachments($issueId: String!) { + issue(id: $issueId) { + attachments { + nodes { + ...AttachmentFields + } + } + } +} diff --git a/graphql/queries/cycles.graphql b/graphql/queries/cycles.graphql new file mode 100644 index 0000000..e6e621f --- /dev/null +++ b/graphql/queries/cycles.graphql @@ -0,0 +1,143 @@ +# ------------------------------------------------------------ +# GraphQL queries for Linear cycles +# +# Cycles are time-boxed iterations (sprints) belonging to teams. +# Each team can have one active cycle at a time, plus past and future +# cycles. Cycles are numbered sequentially per team (Cycle 1, 2, 3...) +# and can have custom names. +# +# Key behavior: +# - isActive, isNext, isPrevious are mutually exclusive flags +# - Only one cycle per team can have isActive=true at any time +# - Cycle.name is nullable - use "Cycle {number}" as fallback +# - Issues can belong to exactly one cycle +# ------------------------------------------------------------ + +# Cycle fields fragment +# +# Defines the basic fields for a cycle entity. Number is sequential +# per team, startsAt/endsAt are ISO 8601 timestamps. +fragment CycleFields on Cycle { + id + number + name + startsAt + endsAt + isActive + isNext + isPrevious +} + +# Cycle with issues fragment +# +# Extends CycleFields to include issues belonging to the cycle. +# Issues are paginated - use $first variable to control limit. +fragment CycleWithIssuesFields on Cycle { + ...CycleFields + issues(first: $first) { + nodes { + id + identifier + title + state { + name + } + } + } +} + +# List cycles with optional filtering +# +# Fetches a list of cycles with optional team and status filtering. +# Without filters, returns cycles from all teams. +# +# Common filters: +# { team: { id: { eq: "uuid" } } } - cycles for specific team +# { isActive: { eq: true } } - only active cycles +# +# Variables: +# $first: Maximum number of cycles to return (default: 50) +# $filter: Optional CycleFilter for team/status filtering +query GetCycles($first: Int = 50, $after: String, $filter: CycleFilter) { + cycles(first: $first, after: $after, filter: $filter) { + nodes { + ...CycleFields + } + pageInfo { + hasNextPage + endCursor + } + } +} + +# Get cycle by ID with its issues +# +# Fetches a single cycle by UUID and includes its issues. +# Use this for cycle detail views where issue list is needed. +# +# Variables: +# $id: Cycle UUID (String! not ID! due to Linear API quirk) +# $first: Maximum number of issues to return (default: 50) +query GetCycleById($id: String!, $first: Int = 50) { + cycle(id: $id) { + ...CycleWithIssuesFields + } +} + +# Find a cycle by name scoped to a team +# +# Resolves a cycle name to UUID within a specific team context. +# Used when resolving human-friendly identifiers like "Sprint 1". +# Returns up to 10 matches (typically only 1). +# +# Variables: +# $name: Cycle name to search for (case-sensitive) +# $teamId: Team UUID to scope the search +query FindCycleScoped($name: String!, $teamId: ID!) { + cycles( + filter: { + and: [{ name: { eq: $name } }, { team: { id: { eq: $teamId } } }] + } + first: 10 + ) { + nodes { + id + name + number + startsAt + isActive + isNext + isPrevious + team { + id + key + } + } + } +} + +# Find a cycle by name across all teams +# +# Resolves a cycle name to UUID globally (not team-scoped). +# Used when team context is not available. May return multiple +# matches if multiple teams have cycles with the same name. +# +# Variables: +# $name: Cycle name to search for (case-sensitive) +query FindCycleGlobal($name: String!) { + cycles(filter: { name: { eq: $name } }, first: 10) { + nodes { + id + name + number + startsAt + isActive + isNext + isPrevious + team { + id + key + } + } + } +} diff --git a/graphql/queries/documents.graphql b/graphql/queries/documents.graphql new file mode 100644 index 0000000..38d7407 --- /dev/null +++ b/graphql/queries/documents.graphql @@ -0,0 +1,48 @@ +# ------------------------------------------------------------ +# GraphQL queries for Linear documents +# +# Documents are standalone entities that can be associated with projects, +# initiatives, or teams. To link a document to an issue, use the +# attachments API (see attachments.graphql). +# ------------------------------------------------------------ + +# Document fields fragment +# +# Defines the basic fields for a document entity. The trashed field +# indicates soft-deletion status (documents are not hard-deleted). +fragment DocumentFields on Document { + id + title + content + slugId + url + icon + color + createdAt + updatedAt + trashed +} + +# Get a single document by ID +# +# Fetches a document by its unique identifier. +query GetDocument($id: String!) { + document(id: $id) { + ...DocumentFields + } +} + +# List documents with optional filtering +# +# Fetches a list of documents with optional filtering criteria. +query ListDocuments($first: Int!, $after: String, $filter: DocumentFilter) { + documents(first: $first, after: $after, filter: $filter) { + nodes { + ...DocumentFields + } + pageInfo { + hasNextPage + endCursor + } + } +} diff --git a/graphql/queries/issues.graphql b/graphql/queries/issues.graphql new file mode 100644 index 0000000..b6a4ee8 --- /dev/null +++ b/graphql/queries/issues.graphql @@ -0,0 +1,500 @@ +# ------------------------------------------------------------ +# Optimized GraphQL queries for issue operations +# +# This module contains highly optimized GraphQL queries that fetch +# all necessary issue data in single requests, eliminating N+1 query +# problem common with Linear SDK. Each query uses comprehensive +# fragments to ensure consistent data structures. +# ------------------------------------------------------------ + +# Complete issue fragment with all relationships +# +# Combines all issue fragments into a comprehensive field selection. +# This is used when full issue details are needed including all +# relationships (state, assignee, team, project, labels, comments). +fragment CompleteIssueFields on Issue { + id + identifier + title + description + branchName + priority + estimate + createdAt + updatedAt + state { + id + name + } + assignee { + id + name + } + team { + id + key + name + } + project { + id + name + } + labels { + nodes { + id + name + } + } + cycle { + id + name + number + } + projectMilestone { + id + name + targetDate + } + parent { + id + identifier + title + } + children { + nodes { + id + identifier + title + } + } + relations { + nodes { + id + type + relatedIssue { + id + identifier + } + } + } + inverseRelations { + nodes { + id + type + issue { + id + identifier + } + } + } +} + +# Complete issue fragment with all relationships and comments +# +# Combines all issue fragments into a comprehensive field selection. +# This is used when full issue details are needed including all +# relationships (state, assignee, team, project, labels, comments). +fragment CompleteIssueWithCommentsFields on Issue { + ...CompleteIssueFields + comments { + nodes { + id + body + } + } +} + +# Complete issue search fragment with all relationships +# +# Combines all issue fragments into a comprehensive field selection. +# This is used when full issue details are needed including all +# relationships (state, assignee, team, project, labels, comments). +fragment CompleteIssueSearchFields on IssueSearchResult { + id + identifier + title + description + branchName + priority + estimate + createdAt + updatedAt + state { + id + name + } + assignee { + id + name + } + team { + id + key + name + } + project { + id + name + } + labels { + nodes { + id + name + } + } + cycle { + id + name + number + } + projectMilestone { + id + name + targetDate + } + parent { + id + identifier + title + } + children { + nodes { + id + identifier + title + } + } + relations { + nodes { + id + type + relatedIssue { + id + identifier + } + } + } + inverseRelations { + nodes { + id + type + issue { + id + identifier + } + } + } +} + +# Get issues list with all relationships in single query +# +# Fetches paginated issues excluding completed ones, +# ordered by most recently updated. Includes all relationships +# for comprehensive issue data. +query GetIssues($first: Int!, $after: String, $orderBy: PaginationOrderBy) { + issues( + first: $first + after: $after + orderBy: $orderBy + filter: { state: { type: { neq: "completed" } } } + ) { + nodes { + ...CompleteIssueFields + } + pageInfo { + hasNextPage + endCursor + } + } +} + +# Get single issue by UUID with comments and all relationships +# +# Fetches complete issue data including comments by direct UUID lookup. +# Uses the comprehensive fragment with comment data for detailed view. +query GetIssueById($id: String!) { + issue(id: $id) { + ...CompleteIssueWithCommentsFields + } +} + +# Get issue by identifier (team key + number) +# +# Fetches issue using TEAM-123 format. Resolves team key and +# issue number to find the exact issue, returning complete data with comments. +query GetIssueByIdentifier($teamKey: String!, $number: Float!) { + issues( + filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } } + first: 1 + ) { + nodes { + ...CompleteIssueWithCommentsFields + } + } +} + +# Get issue team by issue ID +# +# Fetches the team associated with an issue by its ID. +query GetIssueTeam($issueId: String!) { + issue(id: $issueId) { + team { + id + } + } +} + +# Search issues with text search and all relationships in single query +# +# Provides full-text search across Linear issues with complete +# relationship data for each match. +query SearchIssues($term: String!, $first: Int!, $after: String) { + searchIssues( + term: $term + first: $first + after: $after + includeArchived: false + ) { + nodes { + ...CompleteIssueSearchFields + } + pageInfo { + hasNextPage + endCursor + } + } +} + +# Search issues with advanced filters and all relationships in single query +# +# Supports filtering by team, assignee, project, and states. +# Used by the advanced search functionality with multiple criteria. +query FilteredSearchIssues( + $first: Int! + $after: String + $filter: IssueFilter + $orderBy: PaginationOrderBy +) { + issues( + first: $first + after: $after + filter: $filter + orderBy: $orderBy + includeArchived: false + ) { + nodes { + ...CompleteIssueFields + } + pageInfo { + hasNextPage + endCursor + } + } +} + +# Batch resolve query for search filters +# +# Resolves human-readable identifiers to UUIDs in a single batch query. +# Used to pre-resolve teams, projects, and assignees before executing +# main search query to avoid N+1 queries. +query BatchResolveForSearch( + $teamKey: String + $teamName: String + $projectName: String + $assigneeEmail: String +) { + teams( + filter: { or: [{ key: { eq: $teamKey } }, { name: { eq: $teamName } }] } + first: 1 + ) { + nodes { + id + key + name + } + } + projects(filter: { name: { eqIgnoreCase: $projectName } }, first: 1) { + nodes { + id + name + } + } + users(filter: { email: { eq: $assigneeEmail } }, first: 1) { + nodes { + id + name + email + } + } +} + +# Comprehensive batch resolve for update operations +# +# Resolves all necessary entity references in a single batch query +# before issue update. Includes labels, projects, teams, and parent issues. +# This prevents N+1 queries during update operations. +query BatchResolveForUpdate( + $labelNames: [String!] + $projectName: String + $teamKey: String + $issueNumber: Float + $milestoneName: String +) { + # Resolve labels if provided + labels: issueLabels(filter: { name: { in: $labelNames } }) { + nodes { + id + name + isGroup + parent { + id + name + } + children { + nodes { + id + name + } + } + } + } + + # Resolve project if provided (case-insensitive to be user-friendly) + projects(filter: { name: { eqIgnoreCase: $projectName } }, first: 1) { + nodes { + id + name + projectMilestones { + nodes { + id + name + } + } + } + } + + # Resolve milestone if provided (standalone query in case no project context) + milestones: projectMilestones( + filter: { name: { eq: $milestoneName } } + first: 1 + ) { + nodes { + id + name + } + } + + # Resolve issue identifier if provided + issues( + filter: { + and: [ + { team: { key: { eq: $teamKey } } } + { number: { eq: $issueNumber } } + ] + } + first: 1 + ) { + nodes { + id + identifier + labels { + nodes { + id + name + } + } + team { + id + key + name + } + project { + id + projectMilestones { + nodes { + id + name + } + } + } + } + } +} + +# Comprehensive batch resolve for create operations +# +# Resolves all entity references needed for issue creation in a single +# batch query. Prevents N+1 queries during issue creation by +# pre-resolving teams, projects, labels, and parent issues. +query BatchResolveForCreate( + $teamKey: String + $teamName: String + $projectName: String + $labelNames: [String!] + $parentTeamKey: String + $parentIssueNumber: Float +) { + # Resolve team if provided + teams( + filter: { or: [{ key: { eq: $teamKey } }, { name: { eq: $teamName } }] } + first: 1 + ) { + nodes { + id + key + name + } + } + + # Resolve project if provided (case-insensitive to be user-friendly) + projects(filter: { name: { eqIgnoreCase: $projectName } }, first: 1) { + nodes { + id + name + projectMilestones { + nodes { + id + name + } + } + # Projects don't own cycles directly, but include teams for context if needed + } + } + + # Resolve labels if provided + labels: issueLabels(filter: { name: { in: $labelNames } }) { + nodes { + id + name + isGroup + parent { + id + name + } + children { + nodes { + id + name + } + } + } + } + + # Resolve parent issue if provided + parentIssues: issues( + filter: { + and: [ + { team: { key: { eq: $parentTeamKey } } } + { number: { eq: $parentIssueNumber } } + ] + } + first: 1 + ) { + nodes { + id + identifier + } + } + + # Resolve cycles by name (team-scoped lookup is preferred but we also provide global fallback) +} diff --git a/graphql/queries/labels.graphql b/graphql/queries/labels.graphql new file mode 100644 index 0000000..384c7f1 --- /dev/null +++ b/graphql/queries/labels.graphql @@ -0,0 +1,44 @@ +# ------------------------------------------------------------ +# GraphQL queries for Linear issue labels +# +# Labels categorize and tag issues for organization and filtering. +# Labels can be workspace-wide (visible to all teams) or scoped to +# a specific team. Each label has a color (hex code) and optional +# description. +# +# Key behavior: Label names are unique within their scope (workspace +# or team). When filtering by team, only labels scoped to that team +# plus workspace-wide labels are returned. +# ------------------------------------------------------------ + +# Label fields fragment +# +# Defines the basic fields for a label entity. Color is a hex code +# (e.g., "#ff0000"), and description is optional. +fragment LabelFields on IssueLabel { + id + name + color + description +} + +# List labels in the workspace +# +# Fetches a list of issue labels with optional team filtering. +# Without a filter, returns all workspace-wide labels and team-scoped +# labels across all teams. +# +# Variables: +# $first: Maximum number of labels to return (default: 50) +# $filter: Optional filter (e.g., { team: { id: { eq: "team-uuid" } } }) +query GetLabels($first: Int = 50, $after: String, $filter: IssueLabelFilter) { + issueLabels(first: $first, after: $after, filter: $filter) { + nodes { + ...LabelFields + } + pageInfo { + hasNextPage + endCursor + } + } +} diff --git a/graphql/queries/project-milestones.graphql b/graphql/queries/project-milestones.graphql new file mode 100644 index 0000000..24ed9a5 --- /dev/null +++ b/graphql/queries/project-milestones.graphql @@ -0,0 +1,93 @@ +# ------------------------------------------------------------ +# GraphQL queries for Linear project milestones +# +# These fragments define reusable field selections for Linear entities, +# ensuring consistent data structure and reducing duplication. +# ------------------------------------------------------------ + +# List project milestones in a project +# +# Fetches a list of project milestones for a given project. +query ListProjectMilestones($projectId: String!, $first: Int!, $after: String) { + project(id: $projectId) { + id + name + projectMilestones(first: $first, after: $after) { + nodes { + id + name + description + targetDate + sortOrder + createdAt + updatedAt + } + pageInfo { + hasNextPage + endCursor + } + } + } +} + +# Get single project milestone by ID with full issue details +# +# Fetches a single project milestone by its unique identifier. +query GetProjectMilestoneById($id: String!, $issuesFirst: Int) { + projectMilestone(id: $id) { + id + name + description + targetDate + sortOrder + createdAt + updatedAt + project { + id + name + } + issues(first: $issuesFirst) { + nodes { + ...CompleteIssueFields + } + } + } +} + +# Find project milestone by name within project context (scoped lookup) +# +# Searches for a project milestone by name within a specific project. +query FindProjectMilestoneScoped($name: String!, $projectId: String!) { + project(id: $projectId) { + projectMilestones(filter: { name: { eq: $name } }, first: 10) { + nodes { + id + name + targetDate + sortOrder + project { + id + name + } + } + } + } +} + +# Find project milestone by name globally (fallback) +# +# Searches for a project milestone by name across all projects. +query FindProjectMilestoneGlobal($name: String!) { + projectMilestones(filter: { name: { eq: $name } }, first: 10) { + nodes { + id + name + targetDate + sortOrder + project { + id + name + } + } + } +} diff --git a/graphql/queries/projects.graphql b/graphql/queries/projects.graphql new file mode 100644 index 0000000..4b2cbc3 --- /dev/null +++ b/graphql/queries/projects.graphql @@ -0,0 +1,45 @@ +# ------------------------------------------------------------ +# GraphQL queries for Linear projects +# +# Projects group related issues across teams toward a common goal. +# Each project has a state (planned, started, paused, completed, +# canceled), optional target date, and can contain milestones. +# Projects are workspace-level and can span multiple teams. +# +# Key behavior: Project slugId is a URL-friendly identifier used +# in Linear's web UI. The state field is a string enum, not a +# relationship to a State entity. +# ------------------------------------------------------------ + +# Project fields fragment +# +# Defines the basic fields for a project entity. State is one of: +# planned, started, paused, completed, canceled. TargetDate is nullable. +fragment ProjectFields on Project { + id + name + description + state + targetDate + slugId +} + +# List all projects in the workspace +# +# Fetches a list of projects across all teams. Projects are typically +# limited in number (active projects < 50 is common), so pagination +# is rarely needed. +# +# Variables: +# $first: Maximum number of projects to return (default: 50) +query GetProjects($first: Int = 50, $after: String) { + projects(first: $first, after: $after) { + nodes { + ...ProjectFields + } + pageInfo { + hasNextPage + endCursor + } + } +} diff --git a/graphql/queries/teams.graphql b/graphql/queries/teams.graphql new file mode 100644 index 0000000..372a76c --- /dev/null +++ b/graphql/queries/teams.graphql @@ -0,0 +1,41 @@ +# ------------------------------------------------------------ +# GraphQL queries for Linear teams +# +# Teams are organizational units that own issues, cycles, and statuses. +# Each team has a unique key (e.g., "ENG", "DESIGN") used for issue +# identifiers (e.g., ENG-123). Teams serve as the primary scoping +# mechanism for most Linear entities. +# +# Key behavior: Teams are workspace-level entities. All users see all +# teams, but permission controls determine which teams users can +# create issues in or modify. +# ------------------------------------------------------------ + +# Team fields fragment +# +# Defines the basic fields for a team entity. The key is used in +# issue identifiers (TEAM-123), and the name is the display name. +fragment TeamFields on Team { + id + key + name +} + +# List all teams in the workspace +# +# Fetches a list of all teams. Teams are typically limited in number +# (most workspaces have < 50), so pagination is rarely needed. +# +# Variables: +# $first: Maximum number of teams to return (default: 50) +query GetTeams($first: Int = 50, $after: String) { + teams(first: $first, after: $after) { + nodes { + ...TeamFields + } + pageInfo { + hasNextPage + endCursor + } + } +} diff --git a/graphql/queries/users.graphql b/graphql/queries/users.graphql new file mode 100644 index 0000000..253c6ca --- /dev/null +++ b/graphql/queries/users.graphql @@ -0,0 +1,44 @@ +# ------------------------------------------------------------ +# GraphQL queries for Linear users +# +# Users are workspace members who can be assigned to issues, own teams, +# and create/modify Linear entities. Each user has a unique email and +# can be active or inactive (deactivated users are retained for history). +# +# Key behavior: Inactive users are hidden from most UI surfaces but +# remain accessible via API for historical data integrity. Use the +# active filter to exclude deactivated users. +# ------------------------------------------------------------ + +# User fields fragment +# +# Defines the basic fields for a user entity. The active flag indicates +# whether the user is currently active in the workspace. +fragment UserFields on User { + id + name + email + active +} + +# List users in the workspace +# +# Fetches a list of workspace members with optional filtering. +# Common use case: filter by active: true to show only current members. +# Note: Results are sorted alphabetically by name in the service layer +# to match Linear SDK behavior. +# +# Variables: +# $first: Maximum number of users to return (default: 50) +# $filter: Optional filter (e.g., { active: { eq: true } }) +query GetUsers($first: Int = 50, $after: String, $filter: UserFilter) { + users(first: $first, after: $after, filter: $filter) { + nodes { + ...UserFields + } + pageInfo { + hasNextPage + endCursor + } + } +} diff --git a/graphql/queries/viewer.graphql b/graphql/queries/viewer.graphql new file mode 100644 index 0000000..d404f98 --- /dev/null +++ b/graphql/queries/viewer.graphql @@ -0,0 +1,11 @@ +# Viewer query for token validation +# +# Returns the authenticated user's basic info. +# Used by `linearis auth` to validate tokens. +query GetViewer { + viewer { + id + name + email + } +} diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..a63c448 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,11 @@ +pre-commit: + commands: + biome: + glob: "*.{ts,js,json,jsonc,graphql}" + run: npx biome check --staged --write {staged_files} + stage_fixed: true + +commit-msg: + commands: + commitlint: + run: npx commitlint --edit {1} diff --git a/package-lock.json b/package-lock.json index 8ea92ee..6d2938d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { "name": "linearis", - "version": "2025.11.3", + "version": "2025.12.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "linearis", - "version": "2025.11.3", + "version": "2025.12.3", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@linear/sdk": "^58.1.0", @@ -16,9 +17,17 @@ "linearis": "dist/main.js" }, "devDependencies": { + "@biomejs/biome": "^2.3.14", + "@commitlint/cli": "^20.4.1", + "@commitlint/config-conventional": "^20.4.1", + "@graphql-codegen/cli": "^6.1.1", + "@graphql-codegen/client-preset": "^5.2.2", + "@graphql-codegen/introspection": "5.0.0", + "@graphql-codegen/schema-ast": "^5.0.0", "@types/node": "^22.0.0", "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "^2.1.8", + "lefthook": "^2.1.0", "tsx": "^4.20.5", "typescript": "^5.0.0", "vitest": "^2.1.8" @@ -41,6 +50,203 @@ "node": ">=6.0.0" } }, + "node_modules/@ardatan/relay-compiler": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.3.tgz", + "integrity": "sha512-mBDFOGvAoVlWaWqs3hm1AciGHSQE1rqFc/liZTyYz/Oek9yZdT5H26pH2zAFuEiTiBVPPyMuqf5VjOFPI2DGsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/runtime": "^7.26.10", + "chalk": "^4.0.0", + "fb-watchman": "^2.0.0", + "immutable": "~3.7.6", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "relay-runtime": "12.0.0", + "signedsource": "^1.0.0" + }, + "bin": { + "relay-compiler": "bin/relay-compiler" + }, + "peerDependencies": { + "graphql": "*" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -61,14 +267,38 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -77,10 +307,70 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -98,231 +388,493 @@ "dev": true, "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "node_modules/@biomejs/biome": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.14.tgz", + "integrity": "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.14", + "@biomejs/cli-darwin-x64": "2.3.14", + "@biomejs/cli-linux-arm64": "2.3.14", + "@biomejs/cli-linux-arm64-musl": "2.3.14", + "@biomejs/cli-linux-x64": "2.3.14", + "@biomejs/cli-linux-x64-musl": "2.3.14", + "@biomejs/cli-win32-arm64": "2.3.14", + "@biomejs/cli-win32-x64": "2.3.14" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.14.tgz", + "integrity": "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ - "aix" + "darwin" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.14.tgz", + "integrity": "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA==", "cpu": [ - "arm" + "x64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.14.tgz", + "integrity": "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ - "android" + "linux" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.14.tgz", + "integrity": "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ - "android" + "linux" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.14.tgz", + "integrity": "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.14.tgz", + "integrity": "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.14.tgz", + "integrity": "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ - "freebsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.14.tgz", + "integrity": "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ - "freebsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", - "cpu": [ - "arm" - ], + "node_modules/@commitlint/cli": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.4.1.tgz", + "integrity": "sha512-uuFKKpc7OtQM+6SRqT+a4kV818o1pS+uvv/gsRhyX7g4x495jg+Q7P0+O9VNGyLXBYP0syksS7gMRDJKcekr6A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@commitlint/format": "^20.4.0", + "@commitlint/lint": "^20.4.1", + "@commitlint/load": "^20.4.0", + "@commitlint/read": "^20.4.0", + "@commitlint/types": "^20.4.0", + "tinyexec": "^1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", - "cpu": [ - "arm64" - ], + "node_modules/@commitlint/cli/node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", - "cpu": [ - "ia32" - ], + "node_modules/@commitlint/config-conventional": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.4.1.tgz", + "integrity": "sha512-0YUvIeBtpi86XriqrR+TCULVFiyYTIOEPjK7tTRMxjcBm1qlzb+kz7IF2WxL6Fq5DaundG8VO37BNgMkMTBwqA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@commitlint/types": "^20.4.0", + "conventional-changelog-conventionalcommits": "^9.1.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", - "cpu": [ - "loong64" - ], + "node_modules/@commitlint/config-validator": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.4.0.tgz", + "integrity": "sha512-zShmKTF+sqyNOfAE0vKcqnpvVpG0YX8F9G/ZIQHI2CoKyK+PSdladXMSns400aZ5/QZs+0fN75B//3Q5CHw++w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@commitlint/types": "^20.4.0", + "ajv": "^8.11.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", - "cpu": [ - "mips64el" - ], + "node_modules/@commitlint/ensure": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.4.1.tgz", + "integrity": "sha512-WLQqaFx1pBooiVvBrA1YfJNFqZF8wS/YGOtr5RzApDbV9tQ52qT5VkTsY65hFTnXhW8PcDfZLaknfJTmPejmlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.4.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", + "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.4.0.tgz", + "integrity": "sha512-i3ki3WR0rgolFVX6r64poBHXM1t8qlFel1G1eCBvVgntE3fCJitmzSvH5JD/KVJN/snz6TfaX2CLdON7+s4WVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.4.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.4.1.tgz", + "integrity": "sha512-In5EO4JR1lNsAv1oOBBO24V9ND1IqdAJDKZiEpdfjDl2HMasAcT7oA+5BKONv1pRoLG380DGPE2W2RIcUwdgLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.4.0", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/lint": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.4.1.tgz", + "integrity": "sha512-g94LrGl/c6UhuhDQqNqU232aslLEN2vzc7MPfQTHzwzM4GHNnEAwVWWnh0zX8S5YXecuLXDwbCsoGwmpAgPWKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^20.4.1", + "@commitlint/parse": "^20.4.1", + "@commitlint/rules": "^20.4.1", + "@commitlint/types": "^20.4.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.4.0.tgz", + "integrity": "sha512-Dauup/GfjwffBXRJUdlX/YRKfSVXsXZLnINXKz0VZkXdKDcaEILAi9oflHGbfydonJnJAbXEbF3nXPm9rm3G6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^20.4.0", + "@commitlint/execute-rule": "^20.0.0", + "@commitlint/resolve-extends": "^20.4.0", + "@commitlint/types": "^20.4.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^6.1.0", + "is-plain-obj": "^4.1.0", + "lodash.mergewith": "^4.6.2", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/message": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.4.0.tgz", + "integrity": "sha512-B5lGtvHgiLAIsK5nLINzVW0bN5hXv+EW35sKhYHE8F7V9Uz1fR4tx3wt7mobA5UNhZKUNgB/+ldVMQE6IHZRyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.4.1.tgz", + "integrity": "sha512-XNtZjeRcFuAfUnhYrCY02+mpxwY4OmnvD3ETbVPs25xJFFz1nRo/25nHj+5eM+zTeRFvWFwD4GXWU2JEtoK1/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.4.0", + "conventional-changelog-angular": "^8.1.0", + "conventional-commits-parser": "^6.2.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.4.0.tgz", + "integrity": "sha512-QfpFn6/I240ySEGv7YWqho4vxqtPpx40FS7kZZDjUJ+eHxu3azfhy7fFb5XzfTqVNp1hNoI3tEmiEPbDB44+cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/top-level": "^20.4.0", + "@commitlint/types": "^20.4.0", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8", + "tinyexec": "^1.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read/node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-ppc64": { + "node_modules/@commitlint/resolve-extends": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.4.0.tgz", + "integrity": "sha512-ay1KM8q0t+/OnlpqXJ+7gEFQNlUtSU5Gxr8GEwnVf2TPN3+ywc5DzL3JCxmpucqxfHBTFwfRMXxPRRnR5Ki20g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^20.4.0", + "@commitlint/types": "^20.4.0", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.4.1.tgz", + "integrity": "sha512-WtqypKEPbQEuJwJS4aKs0OoJRBKz1HXPBC9wRtzVNH68FLhPWzxXlF09hpUXM9zdYTpm4vAdoTGkWiBgQ/vL0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^20.4.1", + "@commitlint/message": "^20.4.0", + "@commitlint/to-lines": "^20.0.0", + "@commitlint/types": "^20.4.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz", + "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.4.0.tgz", + "integrity": "sha512-NDzq8Q6jmFaIIBC/GG6n1OQEaHdmaAAYdrZRlMgW6glYWGZ+IeuXmiymDvQNXPc82mVxq2KiE3RVpcs+1OeDeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.4.0.tgz", + "integrity": "sha512-aO5l99BQJ0X34ft8b0h7QFkQlqxC6e7ZPVmBKz13xM9O8obDaM1Cld4sQlJDXXU/VFuUzQ30mVtHjVz74TuStw==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-commits-parser": "^6.2.1", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@envelop/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.5.0.tgz", + "integrity": "sha512-nsU1EyJQAStaKHR1ZkB/ug9XBm+WPTliYtdedbJ/L1ykrp7dbbn0srqBeDnZ2mbZVp4hH3d0Fy+Og9OgPWZx+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@envelop/instrumentation": "^1.0.0", + "@envelop/types": "^5.2.1", + "@whatwg-node/promise-helpers": "^1.2.4", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@envelop/instrumentation": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@envelop/instrumentation/-/instrumentation-1.0.0.tgz", + "integrity": "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.2.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@envelop/types": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.2.1.tgz", + "integrity": "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", "cpu": [ "ppc64" ], @@ -330,50 +882,50 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-riscv64": { + "node_modules/@esbuild/android-arm": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", "cpu": [ - "riscv64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-s390x": { + "node_modules/@esbuild/android-arm64": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-x64": { + "node_modules/@esbuild/android-x64": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", "cpu": [ "x64" ], @@ -381,16 +933,16 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/netbsd-arm64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", "cpu": [ "arm64" ], @@ -398,16 +950,16 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { + "node_modules/@esbuild/darwin-x64": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", "cpu": [ "x64" ], @@ -415,16 +967,16 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-arm64": { + "node_modules/@esbuild/freebsd-arm64": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", "cpu": [ "arm64" ], @@ -432,16 +984,16 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { + "node_modules/@esbuild/freebsd-x64": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", "cpu": [ "x64" ], @@ -449,238 +1001,203 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { + "node_modules/@esbuild/linux-arm": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { + "node_modules/@esbuild/linux-arm64": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "sunos" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { + "node_modules/@esbuild/linux-ia32": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { + "node_modules/@esbuild/linux-loong64": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", "cpu": [ - "ia32" + "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { + "node_modules/@esbuild/linux-mips64el": { "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", "cpu": [ - "x64" + "mips64el" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "license": "MIT", - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@linear/sdk": { - "version": "58.1.0", - "resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-58.1.0.tgz", - "integrity": "sha512-sqzo1j+uZsxeJlMTV2mrBH3yukB/liev7IySmkZil0ka7ic6b4RE9Jk3x+ohw8YgYB52IRR3SPWzhWu96E6W9g==", - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.0", - "graphql": "^15.4.0", - "isomorphic-unfetch": "^3.1.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.x", - "yarn": "1.x" + "node": ">=18" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@esbuild/linux-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" + "node": ">=18" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" - ] + "netbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" - ] + "netbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", "cpu": [ "arm64" ], @@ -688,13 +1205,16 @@ "license": "MIT", "optional": true, "os": [ - "darwin" - ] + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", "cpu": [ "x64" ], @@ -702,13 +1222,16 @@ "license": "MIT", "optional": true, "os": [ - "darwin" - ] + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", "cpu": [ "arm64" ], @@ -716,13 +1239,16 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "openharmony" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", "cpu": [ "x64" ], @@ -730,139 +1256,5045 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", "cpu": [ - "arm" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "node_modules/@esbuild/win32-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", - "cpu": [ - "arm64" + "win32" ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@graphql-codegen/add": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-6.0.0.tgz", + "integrity": "sha512-biFdaURX0KTwEJPQ1wkT6BRgNasqgQ5KbCI1a3zwtLtO7XTo7/vKITPylmiU27K5DSOWYnY/1jfSqUAEBuhZrQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.0.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", - "cpu": [ - "loong64" - ], + "node_modules/@graphql-codegen/cli": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-6.1.1.tgz", + "integrity": "sha512-Ni8UdZ6D/UTvLvDtPb6PzshI0lTqtLDnmv/2t1w2SYP92H0MMEdAzxB/ujDWwIXm2LzVPvvrGvzzCTMsyXa+mA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@babel/generator": "^7.18.13", + "@babel/template": "^7.18.10", + "@babel/types": "^7.18.13", + "@graphql-codegen/client-preset": "^5.2.0", + "@graphql-codegen/core": "^5.0.0", + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-tools/apollo-engine-loader": "^8.0.0", + "@graphql-tools/code-file-loader": "^8.0.0", + "@graphql-tools/git-loader": "^8.0.0", + "@graphql-tools/github-loader": "^9.0.0", + "@graphql-tools/graphql-file-loader": "^8.0.0", + "@graphql-tools/json-file-loader": "^8.0.0", + "@graphql-tools/load": "^8.1.0", + "@graphql-tools/url-loader": "^9.0.0", + "@graphql-tools/utils": "^10.0.0", + "@inquirer/prompts": "^7.8.2", + "@whatwg-node/fetch": "^0.10.0", + "chalk": "^4.1.0", + "cosmiconfig": "^9.0.0", + "debounce": "^2.0.0", + "detect-indent": "^6.0.0", + "graphql-config": "^5.1.1", + "is-glob": "^4.0.1", + "jiti": "^2.3.0", + "json-to-pretty-yaml": "^1.2.2", + "listr2": "^9.0.0", + "log-symbols": "^4.0.0", + "micromatch": "^4.0.5", + "shell-quote": "^1.7.3", + "string-env-interpolation": "^1.0.1", + "ts-log": "^2.2.3", + "tslib": "^2.4.0", + "yaml": "^2.3.1", + "yargs": "^17.0.0" + }, + "bin": { + "gql-gen": "cjs/bin.js", + "graphql-code-generator": "cjs/bin.js", + "graphql-codegen": "cjs/bin.js", + "graphql-codegen-esm": "esm/bin.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@parcel/watcher": "^2.1.0", + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "@parcel/watcher": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", - "cpu": [ - "ppc64" - ], + "node_modules/@graphql-codegen/client-preset": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.2.2.tgz", + "integrity": "sha512-1xufIJZr04ylx0Dnw49m8Jrx1s1kujUNVm+Tp5cPRsQmgPN9VjB7wWY7CGD8ArStv6Vjb0a31Xnm5I+VzZM+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/template": "^7.20.7", + "@graphql-codegen/add": "^6.0.0", + "@graphql-codegen/gql-tag-operations": "5.1.2", + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-codegen/typed-document-node": "^6.1.5", + "@graphql-codegen/typescript": "^5.0.7", + "@graphql-codegen/typescript-operations": "^5.0.7", + "@graphql-codegen/visitor-plugin-common": "^6.2.2", + "@graphql-tools/documents": "^1.0.0", + "@graphql-tools/utils": "^10.0.0", + "@graphql-typed-document-node/core": "3.2.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "graphql-sock": "^1.0.0" + }, + "peerDependenciesMeta": { + "graphql-sock": { + "optional": true + } + } + }, + "node_modules/@graphql-codegen/core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-5.0.0.tgz", + "integrity": "sha512-vLTEW0m8LbE4xgRwbFwCdYxVkJ1dBlVJbQyLb9Q7bHnVFgHAP982Xo8Uv7FuPBmON+2IbTjkCqhFLHVZbqpvjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-tools/schema": "^10.0.0", + "@graphql-tools/utils": "^10.0.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/gql-tag-operations": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.1.2.tgz", + "integrity": "sha512-BIv66VJ2bKlpfXBeVakJxihBSKnBIdGFLMaFdnGPxqYlKIzaGffjsGbhViPwwBinmBChW4Se6PU4Py7eysYEiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-codegen/visitor-plugin-common": "6.2.2", + "@graphql-tools/utils": "^10.0.0", + "auto-bind": "~4.0.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/introspection": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/introspection/-/introspection-5.0.0.tgz", + "integrity": "sha512-7GaV4o8J3yllz7hdvQVAwB8L5oizeLCRKCU5vEq6XyFsoi4mSVMAPhvDPkNgt4wtXPyEh59NU7QwG84JChrqHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-codegen/visitor-plugin-common": "^6.0.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/plugin-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.1.0.tgz", + "integrity": "sha512-JJypehWTcty9kxKiqH7TQOetkGdOYjY78RHlI+23qB59cV2wxjFFVf8l7kmuXS4cpGVUNfIjFhVr7A1W7JMtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^10.0.0", + "change-case-all": "1.0.15", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "lodash": "~4.17.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/schema-ast": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-5.0.0.tgz", + "integrity": "sha512-jn7Q3PKQc0FxXjbpo9trxzlz/GSFQWxL042l0iC8iSbM/Ar+M7uyBwMtXPsev/3Razk+osQyreghIz0d2+6F7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-tools/utils": "^10.0.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/typed-document-node": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.1.5.tgz", + "integrity": "sha512-6dgEPz+YRMzSPpATj7tsKh/L6Y8OZImiyXIUzvSq/dRAEgoinahrES5y/eZQyc7CVxfoFCyHF9KMQQ9jiLn7lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-codegen/visitor-plugin-common": "6.2.2", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.15", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/typescript": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.7.tgz", + "integrity": "sha512-kZwcu9Iat5RWXxLGPnDbG6qVbGTigF25/aGqCG/DCQ1Al8RufSjVXhIOkJBp7QWAqXn3AupHXL1WTMXP7xs4dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-codegen/schema-ast": "^5.0.0", + "@graphql-codegen/visitor-plugin-common": "6.2.2", + "auto-bind": "~4.0.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/typescript-operations": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.7.tgz", + "integrity": "sha512-5N3myNse1putRQlp8+l1k9ayvc98oq2mPJx0zN8MTOlTBxcb2grVPFRLy5wJJjuv9NffpyCkVJ9LvUaf8mqQgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-codegen/typescript": "^5.0.7", + "@graphql-codegen/visitor-plugin-common": "6.2.2", + "auto-bind": "~4.0.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "graphql-sock": "^1.0.0" + }, + "peerDependenciesMeta": { + "graphql-sock": { + "optional": true + } + } + }, + "node_modules/@graphql-codegen/visitor-plugin-common": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.2.2.tgz", + "integrity": "sha512-wEJ4zJj58PKlXISItZfr0xIHyM1lAuRfoflPegsb1L17Mx5+YzNOy0WAlLele3yzyV89WvCiprFKMcVQ7KfDXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-tools/optimize": "^2.0.0", + "@graphql-tools/relay-operation-optimizer": "^7.0.0", + "@graphql-tools/utils": "^10.0.0", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.15", + "dependency-graph": "^1.0.0", + "graphql-tag": "^2.11.0", + "parse-filepath": "^1.0.2", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-hive/signal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@graphql-hive/signal/-/signal-2.0.0.tgz", + "integrity": "sha512-Pz8wB3K0iU6ae9S1fWfsmJX24CcGeTo6hE7T44ucmV/ALKRj+bxClmqrYcDT7v3f0d12Rh4FAXBb6gon+WkDpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@graphql-tools/apollo-engine-loader": { + "version": "8.0.28", + "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.28.tgz", + "integrity": "sha512-MzgDrUuoxp6dZeo54zLBL3cEJKJtM3N/2RqK0rbPxPq5X2z6TUA7EGg8vIFTUkt5xelAsUrm8/4ai41ZDdxOng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.0.0", + "@whatwg-node/fetch": "^0.10.13", + "sync-fetch": "0.6.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/apollo-engine-loader/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/batch-execute": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-10.0.5.tgz", + "integrity": "sha512-dL13tXkfGvAzLq2XfzTKAy9logIcltKYRuPketxdh3Ok3U6PN1HKMCHfrE9cmtAsxD96/8Hlghz5AtM+LRv/ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.0.0", + "@whatwg-node/promise-helpers": "^1.3.2", + "dataloader": "^2.2.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/batch-execute/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/batch-execute/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@graphql-tools/code-file-loader": { + "version": "8.1.28", + "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.28.tgz", + "integrity": "sha512-BL3Ft/PFlXDE5nNuqA36hYci7Cx+8bDrPDc8X3VSpZy9iKFBY+oQ+IwqnEHCkt8OSp2n2V0gqTg4u3fcQP1Kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/graphql-tag-pluck": "8.3.27", + "@graphql-tools/utils": "^11.0.0", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/code-file-loader/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/delegate": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-12.0.5.tgz", + "integrity": "sha512-VsaynCBuZTznRzYjT3VutwIjZ0j4Bi5p0SUDiY5Qi07fW7RBXtStfH7O+YdnTQvxJecJUXA4iiKVWi+3qKXvhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/batch-execute": "^10.0.5", + "@graphql-tools/executor": "^1.4.13", + "@graphql-tools/schema": "^10.0.29", + "@graphql-tools/utils": "^11.0.0", + "@repeaterjs/repeater": "^3.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "dataloader": "^2.2.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/delegate/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/delegate/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@graphql-tools/documents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/documents/-/documents-1.0.1.tgz", + "integrity": "sha512-aweoMH15wNJ8g7b2r4C4WRuJxZ0ca8HtNO54rkye/3duxTkW4fGBEutCx03jCIr5+a1l+4vFJNP859QnAVBVCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.5.1.tgz", + "integrity": "sha512-n94Qcu875Mji9GQ52n5UbgOTxlgvFJicBPYD+FRks9HKIQpdNPjkkrKZUYNG51XKa+bf03rxNflm4+wXhoHHrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.0.0", + "@graphql-typed-document-node/core": "^3.2.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-common": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-1.0.6.tgz", + "integrity": "sha512-23/K5C+LSlHDI0mj2SwCJ33RcELCcyDUgABm1Z8St7u/4Z5+95i925H/NAjUyggRjiaY8vYtNiMOPE49aPX1sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@envelop/core": "^5.4.0", + "@graphql-tools/utils": "^11.0.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-common/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-graphql-ws": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-3.1.4.tgz", + "integrity": "sha512-wCQfWYLwg1JZmQ7rGaFy74AQyVFxpeqz19WWIGRgANiYlm+T0K3Hs6POgi0+nL3HvwxJIxhUlaRLFvkqm1zxSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/executor-common": "^1.0.6", + "@graphql-tools/utils": "^11.0.0", + "@whatwg-node/disposablestack": "^0.0.6", + "graphql-ws": "^6.0.6", + "isows": "^1.0.7", + "tslib": "^2.8.1", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-graphql-ws/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-graphql-ws/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@graphql-tools/executor-http": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-3.1.0.tgz", + "integrity": "sha512-DTaNU1rT2sxffwQlt+Aw68cHQWfGkjsaRk1D8nvG+DcCR8RNQo0d9qYt7pXIcfXYcQLb/OkABcGSuCfkopvHJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-hive/signal": "^2.0.0", + "@graphql-tools/executor-common": "^1.0.6", + "@graphql-tools/utils": "^11.0.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/fetch": "^0.10.13", + "@whatwg-node/promise-helpers": "^1.3.2", + "meros": "^1.3.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-http/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-http/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@graphql-tools/executor-legacy-ws": { + "version": "1.1.25", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.25.tgz", + "integrity": "sha512-6uf4AEXO0QMxJ7AWKVPqEZXgYBJaiz5vf29X0boG8QtcqWy8mqkXKWLND2Swdx0SbEx0efoGFcjuKufUcB0ASQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.0.0", + "@types/ws": "^8.0.0", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.4.0", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-legacy-ws/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/git-loader": { + "version": "8.0.32", + "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.32.tgz", + "integrity": "sha512-H5HTp2vevv0rRMEnCJBVmVF8md3LpJI1C1+d6OtzvmuONJ8mOX2mkf9rtoqwiztynVegaDUekvMFsc9k5iE2WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/graphql-tag-pluck": "8.3.27", + "@graphql-tools/utils": "^11.0.0", + "is-glob": "4.0.3", + "micromatch": "^4.0.8", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/git-loader/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/github-loader": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-9.0.6.tgz", + "integrity": "sha512-hhlt2MMkRcvDva/qyzqFddXzaMmRnriJ0Ts+/LcNeYnB8hcEqRMpF9RCsHYjo1mFRaiu8i4PSIpXyyFu3To7Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/executor-http": "^3.0.6", + "@graphql-tools/graphql-tag-pluck": "^8.3.27", + "@graphql-tools/utils": "^11.0.0", + "@whatwg-node/fetch": "^0.10.13", + "@whatwg-node/promise-helpers": "^1.0.0", + "sync-fetch": "0.6.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/github-loader/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/graphql-file-loader": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.1.9.tgz", + "integrity": "sha512-rkLK46Q62Zxift8B6Kfw6h8SH3pCR3DPCfNeC/lpLwYReezZz+2ARuLDFZjQGjW+4lpMwiAw8CIxDyQAUgqU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/import": "7.1.9", + "@graphql-tools/utils": "^11.0.0", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/graphql-file-loader/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/graphql-tag-pluck": { + "version": "8.3.27", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.27.tgz", + "integrity": "sha512-CJ0WVXhGYsfFngpRrAAcjRHyxSDHx4dEz2W15bkwvt9he/AWhuyXm07wuGcoLrl0q0iQp1BiRjU7D8SxWZo3JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "@graphql-tools/utils": "^11.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/graphql-tag-pluck/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/import": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.1.9.tgz", + "integrity": "sha512-mHzOgyfzsAgstaZPIFEtKg4GVH4FbDHeHYrSs73mAPKS5F59/FlRuUJhAoRnxbVnc3qIZ6EsWBjOjNbnPK8viA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.0.0", + "@theguild/federation-composition": "^0.21.1", + "resolve-from": "5.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/import/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/json-file-loader": { + "version": "8.0.26", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.26.tgz", + "integrity": "sha512-kwy9IFi5QtXXTLBgWkvA1RqsZeJDn0CxsTbhNlziCzmga9fNo7qtZ18k9FYIq3EIoQQlok+b7W7yeyJATA2xhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.0.0", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/json-file-loader/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/load": { + "version": "8.1.8", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.1.8.tgz", + "integrity": "sha512-gxO662b64qZSToK3N6XUxWG5E6HOUjlg5jEnmGvD4bMtGJ0HwEe/BaVZbBQemCfLkxYjwRIBiVfOY9o0JyjZJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/schema": "^10.0.31", + "@graphql-tools/utils": "^11.0.0", + "p-limit": "3.1.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/load/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/merge": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.7.tgz", + "integrity": "sha512-Y5E1vTbTabvcXbkakdFUt4zUIzB1fyaEnVmIWN0l0GMed2gdD01TpZWLUm4RNAxpturvolrb24oGLQrBbPLSoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/merge/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/optimize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-2.0.0.tgz", + "integrity": "sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/relay-operation-optimizer": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.27.tgz", + "integrity": "sha512-rdkL1iDMFaGDiHWd7Bwv7hbhrhnljkJaD0MXeqdwQlZVgVdUDlMot2WuF7CEKVgijpH6eSC6AxXMDeqVgSBS2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ardatan/relay-compiler": "^12.0.3", + "@graphql-tools/utils": "^11.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/relay-operation-optimizer/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/schema": { + "version": "10.0.31", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.31.tgz", + "integrity": "sha512-ZewRgWhXef6weZ0WiP7/MV47HXiuFbFpiDUVLQl6mgXsWSsGELKFxQsyUCBos60Qqy1JEFAIu3Ns6GGYjGkqkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/merge": "^9.1.7", + "@graphql-tools/utils": "^11.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/schema/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/url-loader": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-9.0.6.tgz", + "integrity": "sha512-QdJI3f7ANDMYfYazRgJzzybznjOrQAOuDXweC9xmKgPZoTqNxEAsatiy69zcpTf6092taJLyrqRH6R7xUTzf4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/executor-graphql-ws": "^3.1.2", + "@graphql-tools/executor-http": "^3.0.6", + "@graphql-tools/executor-legacy-ws": "^1.1.25", + "@graphql-tools/utils": "^11.0.0", + "@graphql-tools/wrap": "^11.1.1", + "@types/ws": "^8.0.0", + "@whatwg-node/fetch": "^0.10.13", + "@whatwg-node/promise-helpers": "^1.0.0", + "isomorphic-ws": "^5.0.0", + "sync-fetch": "0.6.0", + "tslib": "^2.4.0", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/url-loader/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/utils": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.11.0.tgz", + "integrity": "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/wrap": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-11.1.5.tgz", + "integrity": "sha512-eVE+YTMimFOxQAiayMGQhIMVA6F1pRstAmtrd82KX8qJqdZ5QXZrbIPitAhEFmxD0qC6afUEgsLlTOwOJGg1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/delegate": "^12.0.5", + "@graphql-tools/schema": "^10.0.29", + "@graphql-tools/utils": "^11.0.0", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/wrap/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/wrap/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@linear/sdk": { + "version": "58.1.0", + "resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-58.1.0.tgz", + "integrity": "sha512-sqzo1j+uZsxeJlMTV2mrBH3yukB/liev7IySmkZil0ka7ic6b4RE9Jk3x+ohw8YgYB52IRR3SPWzhWu96E6W9g==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.0", + "graphql": "^15.4.0", + "isomorphic-unfetch": "^3.1.0" + }, + "engines": { + "node": ">=12.x", + "yarn": "1.x" + } + }, + "node_modules/@linear/sdk/node_modules/graphql": { + "version": "15.10.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz", + "integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==", + "license": "MIT", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@repeaterjs/repeater": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz", + "integrity": "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@theguild/federation-composition": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@theguild/federation-composition/-/federation-composition-0.21.3.tgz", + "integrity": "sha512-+LlHTa4UbRpZBog3ggAxjYIFvdfH3UMvvBUptur19TMWkqU4+n3GmN+mDjejU+dyBXIG27c25RsiQP1HyvM99g==", + "dev": true, + "license": "MIT", + "dependencies": { + "constant-case": "^3.0.4", + "debug": "4.4.3", + "json5": "^2.2.3", + "lodash.sortby": "^4.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "graphql": "^16.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", + "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.9.tgz", + "integrity": "sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "fflate": "^0.8.2", + "flatted": "^3.3.1", + "pathe": "^1.1.2", + "sirv": "^3.0.0", + "tinyglobby": "^0.2.10", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "2.1.9" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@whatwg-node/disposablestack": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz", + "integrity": "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/fetch": { + "version": "0.10.13", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.13.tgz", + "integrity": "sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@whatwg-node/node-fetch": "^0.8.3", + "urlpattern-polyfill": "^10.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/node-fetch": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.8.5.tgz", + "integrity": "sha512-4xzCl/zphPqlp9tASLVeUhB5+WJHbuWGYpfoC2q1qh5dw0AqZBW7L27V5roxYWijPxj4sspRAAoOH3d2ztaHUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^3.1.1", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/promise-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz", + "integrity": "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/auto-bind": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", + "integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001767", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", + "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/change-case-all": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.15.tgz", + "integrity": "sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "change-case": "^4.1.2", + "is-lower-case": "^2.0.2", + "is-upper-case": "^2.0.2", + "lower-case": "^2.0.2", + "lower-case-first": "^2.0.2", + "sponge-case": "^1.0.1", + "swap-case": "^2.0.2", + "title-case": "^3.0.3", + "upper-case": "^2.0.2", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", + "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.1.0.tgz", + "integrity": "sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.1.0.tgz", + "integrity": "sha512-MnbEysR8wWa8dAEvbj5xcBgJKQlX/m0lhS8DsyAAWDHdfs2faDJxTgzRYlRYpXSe7UiKrIIlB4TrBKU9q9DgkA==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-commits-parser": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.2.1.tgz", + "integrity": "sha512-20pyHgnO40rvfI0NGF/xiEoFMkXDtkF8FwHvk5BokoFoCuTQRI8vrNCNFWUOfuolKJMm1tPCHc8GgYEtr1XRNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "meow": "^13.0.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", + "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" + } + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-inspect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", + "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/debounce": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", + "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fbjs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", + "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.1.5", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^1.0.35" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/git-raw-commits": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/git-raw-commits/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphql": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", + "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-config": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-5.1.5.tgz", + "integrity": "sha512-mG2LL1HccpU8qg5ajLROgdsBzx/o2M6kgI3uAmoaXiSH9PCUbtIyLomLqUtCFaAeG2YCFsl0M5cfQ9rKmDoMVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/graphql-file-loader": "^8.0.0", + "@graphql-tools/json-file-loader": "^8.0.0", + "@graphql-tools/load": "^8.1.0", + "@graphql-tools/merge": "^9.0.0", + "@graphql-tools/url-loader": "^8.0.0", + "@graphql-tools/utils": "^10.0.0", + "cosmiconfig": "^8.1.0", + "jiti": "^2.0.0", + "minimatch": "^9.0.5", + "string-env-interpolation": "^1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "cosmiconfig-toml-loader": "^1.0.0", + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "cosmiconfig-toml-loader": { + "optional": true + } + } + }, + "node_modules/graphql-config/node_modules/@graphql-hive/signal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@graphql-hive/signal/-/signal-1.0.0.tgz", + "integrity": "sha512-RiwLMc89lTjvyLEivZ/qxAC5nBHoS2CtsWFSOsN35sxG9zoo5Z+JsFHM8MlvmO9yt+MJNIyC5MLE1rsbOphlag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/graphql-config/node_modules/@graphql-tools/batch-execute": { + "version": "9.0.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.19.tgz", + "integrity": "sha512-VGamgY4PLzSx48IHPoblRw0oTaBa7S26RpZXt0Y4NN90ytoE0LutlpB2484RbkfcTjv9wa64QD474+YP1kEgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^10.9.1", + "@whatwg-node/promise-helpers": "^1.3.0", + "dataloader": "^2.2.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-config/node_modules/@graphql-tools/delegate": { + "version": "10.2.23", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.23.tgz", + "integrity": "sha512-xrPtl7f1LxS+B6o+W7ueuQh67CwRkfl+UKJncaslnqYdkxKmNBB4wnzVcW8ZsRdwbsla/v43PtwAvSlzxCzq2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/batch-execute": "^9.0.19", + "@graphql-tools/executor": "^1.4.9", + "@graphql-tools/schema": "^10.0.25", + "@graphql-tools/utils": "^10.9.1", + "@repeaterjs/repeater": "^3.0.6", + "@whatwg-node/promise-helpers": "^1.3.0", + "dataloader": "^2.2.3", + "dset": "^3.1.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-config/node_modules/@graphql-tools/executor-common": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.6.tgz", + "integrity": "sha512-JAH/R1zf77CSkpYATIJw+eOJwsbWocdDjY+avY7G+P5HCXxwQjAjWVkJI1QJBQYjPQDVxwf1fmTZlIN3VOadow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@envelop/core": "^5.3.0", + "@graphql-tools/utils": "^10.9.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-config/node_modules/@graphql-tools/executor-graphql-ws": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.7.tgz", + "integrity": "sha512-J27za7sKF6RjhmvSOwOQFeNhNHyP4f4niqPnerJmq73OtLx9Y2PGOhkXOEB0PjhvPJceuttkD2O1yMgEkTGs3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/executor-common": "^0.0.6", + "@graphql-tools/utils": "^10.9.1", + "@whatwg-node/disposablestack": "^0.0.6", + "graphql-ws": "^6.0.6", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.8.1", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-config/node_modules/@graphql-tools/executor-http": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-1.3.3.tgz", + "integrity": "sha512-LIy+l08/Ivl8f8sMiHW2ebyck59JzyzO/yF9SFS4NH6MJZUezA1xThUXCDIKhHiD56h/gPojbkpcFvM2CbNE7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-hive/signal": "^1.0.0", + "@graphql-tools/executor-common": "^0.0.4", + "@graphql-tools/utils": "^10.8.1", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/fetch": "^0.10.4", + "@whatwg-node/promise-helpers": "^1.3.0", + "meros": "^1.2.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-config/node_modules/@graphql-tools/executor-http/node_modules/@graphql-tools/executor-common": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.4.tgz", + "integrity": "sha512-SEH/OWR+sHbknqZyROCFHcRrbZeUAyjCsgpVWCRjqjqRbiJiXq6TxNIIOmpXgkrXWW/2Ev4Wms6YSGJXjdCs6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@envelop/core": "^5.2.3", + "@graphql-tools/utils": "^10.8.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-config/node_modules/@graphql-tools/url-loader": { + "version": "8.0.33", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.33.tgz", + "integrity": "sha512-Fu626qcNHcqAj8uYd7QRarcJn5XZ863kmxsg1sm0fyjyfBJnsvC7ddFt6Hayz5kxVKfsnjxiDfPMXanvsQVBKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/executor-graphql-ws": "^2.0.1", + "@graphql-tools/executor-http": "^1.1.9", + "@graphql-tools/executor-legacy-ws": "^1.1.19", + "@graphql-tools/utils": "^10.9.1", + "@graphql-tools/wrap": "^10.0.16", + "@types/ws": "^8.0.0", + "@whatwg-node/fetch": "^0.10.0", + "@whatwg-node/promise-helpers": "^1.0.0", + "isomorphic-ws": "^5.0.0", + "sync-fetch": "0.6.0-2", + "tslib": "^2.4.0", + "ws": "^8.17.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-config/node_modules/@graphql-tools/wrap": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.4.tgz", + "integrity": "sha512-7pyNKqXProRjlSdqOtrbnFRMQAVamCmEREilOXtZujxY6kYit3tvWWSjUrcIOheltTffoRh7EQSjpy2JDCzasg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/delegate": "^10.2.23", + "@graphql-tools/schema": "^10.0.25", + "@graphql-tools/utils": "^10.9.1", + "@whatwg-node/promise-helpers": "^1.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-config/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/graphql-config/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/graphql-config/node_modules/sync-fetch": { + "version": "0.6.0-2", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.6.0-2.tgz", + "integrity": "sha512-c7AfkZ9udatCuAy9RSfiGPpeOKKUAUK5e1cXadLOGUjasdxqYqAK0jTNkM/FSEyJ3a5Ra27j/tw/PS0qLmaF/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^3.3.2", + "timeout-signal": "^2.0.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/graphql-config/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/graphql-ws": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.7.tgz", + "integrity": "sha512-yoLRW+KRlDmnnROdAu7sX77VNLC0bsFoZyGQJLy1cF+X/SkLg/fWkRGrEEYQK8o2cafJ2wmEaMqMEZB3U3DYDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@fastify/websocket": "^10 || ^11", + "crossws": "~0.3", + "graphql": "^15.10.1 || ^16", + "ws": "^8" + }, + "peerDependenciesMeta": { + "@fastify/websocket": { + "optional": true + }, + "crossws": { + "optional": true + }, + "ws": { + "optional": true + } + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", + "integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz", + "integrity": "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz", + "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", + "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-unfetch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", + "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "unfetch": "^4.2.0" + } + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-to-pretty-yaml": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz", + "integrity": "sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "remedial": "^1.0.7", + "remove-trailing-spaces": "^1.0.6" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lefthook": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook/-/lefthook-2.1.0.tgz", + "integrity": "sha512-+vS+yywGQW6CN1J1hbGkez//6ixGHIQqfxDN/d3JDm531w9GfGt2lAWTDfZTw/CEl80XsN0raFcnEraR3ldw9g==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "bin": { + "lefthook": "bin/index.js" + }, + "optionalDependencies": { + "lefthook-darwin-arm64": "2.1.0", + "lefthook-darwin-x64": "2.1.0", + "lefthook-freebsd-arm64": "2.1.0", + "lefthook-freebsd-x64": "2.1.0", + "lefthook-linux-arm64": "2.1.0", + "lefthook-linux-x64": "2.1.0", + "lefthook-openbsd-arm64": "2.1.0", + "lefthook-openbsd-x64": "2.1.0", + "lefthook-windows-arm64": "2.1.0", + "lefthook-windows-x64": "2.1.0" + } + }, + "node_modules/lefthook-darwin-arm64": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook-darwin-arm64/-/lefthook-darwin-arm64-2.1.0.tgz", + "integrity": "sha512-u2hjHLQXWSFfzO7ln2n/uEydSzfC9sc5cDC7tvKSuOdhvBwaJ0AQ7ZeuqqCQ4YfVIJfYOom1SVE9CBd10FVyig==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ] }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "node_modules/lefthook-darwin-x64": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook-darwin-x64/-/lefthook-darwin-x64-2.1.0.tgz", + "integrity": "sha512-zz5rcyrtOZpxon7uE+c0KC/o2ypJeLZql5CL0Y9oaTuECbmhfokm8glsGnyWstW/++PuMpZYYr/qsCJA5elxkQ==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ] }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "node_modules/lefthook-freebsd-arm64": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-2.1.0.tgz", + "integrity": "sha512-+mXNCNuFHNGYLrDqYWDeHH7kWCLCJFPpspx5PAAm+PD37PRMZJrTqDbaNK9qCghC1tdmT4/Lvilf/ewXHPlaKw==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ] }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "node_modules/lefthook-freebsd-x64": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook-freebsd-x64/-/lefthook-freebsd-x64-2.1.0.tgz", + "integrity": "sha512-+AU2HD7szuDsUdHue/E3OnF84B2ae/h7CGKpuIUHJntgoJ4kxf89oDvq2/xl8kDCn9cT76UUjgeZUgFYLRj+6Q==", "cpu": [ "x64" ], @@ -870,15 +6302,15 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ] }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "node_modules/lefthook-linux-arm64": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook-linux-arm64/-/lefthook-linux-arm64-2.1.0.tgz", + "integrity": "sha512-KM70eV1tsEib1/tk+3TFxIdH84EaYlIg5KTQWAg+LB1N23nTQ7lL4Dnh1je6f6KW4tf21nmoMUqsh0xvMkQk8Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", @@ -887,24 +6319,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "node_modules/lefthook-linux-x64": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook-linux-x64/-/lefthook-linux-x64-2.1.0.tgz", + "integrity": "sha512-6Bxmv+l7LiYq9W0IE6v2lmlRtBp6pisnlzhcouMGvH3rDwEGw11NAyRJZA3IPGEMAkIuhnlnVTUwAUzKomfJLg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "node_modules/lefthook-openbsd-arm64": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-2.1.0.tgz", + "integrity": "sha512-ppJNK0bBSPLC8gqksRw5zI/0uLeMA5cK+hmZ4ofcuGNmdrN1dfl2Tx84fdeef0NcQY0ii9Y3j3icIKngIoid/g==", "cpu": [ "arm64" ], @@ -912,29 +6344,29 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "openbsd" ] }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "node_modules/lefthook-openbsd-x64": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook-openbsd-x64/-/lefthook-openbsd-x64-2.1.0.tgz", + "integrity": "sha512-8k9lQsMYqQGu4spaQ8RNSOJidxIcOyfaoF2FPZhthtBfRV3cgVFGrsQ0hbIi5pvQRGUlCqYuCN79qauXHmnL3Q==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "openbsd" ] }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "node_modules/lefthook-windows-arm64": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook-windows-arm64/-/lefthook-windows-arm64-2.1.0.tgz", + "integrity": "sha512-0WN+grrxt9zP9NGRcztoPXcz25tteem91rfLWgQFab+50csJ47zldlsB7/eOS/eHG5mUg5g5NPR4XefnXtjOcQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", @@ -943,10 +6375,10 @@ "win32" ] }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "node_modules/lefthook-windows-x64": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lefthook-windows-x64/-/lefthook-windows-x64-2.1.0.tgz", + "integrity": "sha512-XbO/5nAZQLpUn0tPpgCYfFBFJHnymSglQ73jD6wymNrR1j8I5EcXGlP6YcLhnZ83yzsdLC+gup+N6IqUeiyRdw==", "cpu": [ "x64" ], @@ -957,809 +6389,723 @@ "win32" ] }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "22.19.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", - "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "undici-types": "~6.21.0" + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@vitest/coverage-v8": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", - "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.7", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.12", - "magicast": "^0.3.5", - "std-env": "^3.8.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "2.1.9", - "vitest": "2.1.9" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } + "license": "MIT" }, - "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" + "engines": { + "node": ">=18" }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "license": "MIT" }, - "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "license": "MIT" }, - "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "license": "MIT" }, - "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "license": "MIT" }, - "node_modules/@vitest/ui": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.9.tgz", - "integrity": "sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==", + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vitest/utils": "2.1.9", - "fflate": "^0.8.2", - "flatted": "^3.3.1", - "pathe": "^1.1.2", - "sirv": "^3.0.0", - "tinyglobby": "^0.2.10", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "2.1.9" - } + "license": "MIT" }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "license": "MIT" }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } + "license": "MIT" }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" + "js-tokens": "^3.0.0 || ^4.0.0" }, - "engines": { - "node": ">=18" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 16" + "dependencies": { + "tslib": "^2.0.3" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/lower-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-2.0.2.tgz", + "integrity": "sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "tslib": "^2.0.3" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=20" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "semver": "^7.5.3" }, "engines": { - "node": ">=6.0" + "node": ">=10" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { "node": ">=18" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.1", - "@esbuild/android-arm": "0.27.1", - "@esbuild/android-arm64": "0.27.1", - "@esbuild/android-x64": "0.27.1", - "@esbuild/darwin-arm64": "0.27.1", - "@esbuild/darwin-x64": "0.27.1", - "@esbuild/freebsd-arm64": "0.27.1", - "@esbuild/freebsd-x64": "0.27.1", - "@esbuild/linux-arm": "0.27.1", - "@esbuild/linux-arm64": "0.27.1", - "@esbuild/linux-ia32": "0.27.1", - "@esbuild/linux-loong64": "0.27.1", - "@esbuild/linux-mips64el": "0.27.1", - "@esbuild/linux-ppc64": "0.27.1", - "@esbuild/linux-riscv64": "0.27.1", - "@esbuild/linux-s390x": "0.27.1", - "@esbuild/linux-x64": "0.27.1", - "@esbuild/netbsd-arm64": "0.27.1", - "@esbuild/netbsd-x64": "0.27.1", - "@esbuild/openbsd-arm64": "0.27.1", - "@esbuild/openbsd-x64": "0.27.1", - "@esbuild/openharmony-arm64": "0.27.1", - "@esbuild/sunos-x64": "0.27.1", - "@esbuild/win32-arm64": "0.27.1", - "@esbuild/win32-ia32": "0.27.1", - "@esbuild/win32-x64": "0.27.1" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", "engines": { - "node": ">=12.0.0" + "node": ">= 8" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/meros": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.2.tgz", + "integrity": "sha512-Q3mobPbvEx7XbwhnC1J1r60+5H6EZyNccdzSz0eGexJRwouUtTZxPVRGdqKtxlpD84ScK4+tIGldkqDtCKdI0A==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=13" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "@types/node": ">=13" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, - "license": "MIT" - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8.6" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "brace-expansion": "^2.0.1" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/graphql": { - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz", - "integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", - "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", "engines": { - "node": ">= 10.x" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=8" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, - "license": "ISC" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, - "node_modules/isomorphic-unfetch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", - "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, "license": "MIT", "dependencies": { - "node-fetch": "^2.6.1", - "unfetch": "^4.2.0" + "lower-case": "^2.0.2", + "tslib": "^2.0.3" } }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10.5.0" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" + "remove-trailing-separator": "^1.0.1" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "mimic-function": "^5.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=18" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0" }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "dot-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.5.3" + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8" } }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=8" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "path-root-regex": "^0.1.0" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/path-scurry": { @@ -1779,6 +7125,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -1809,7 +7165,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1846,6 +7201,103 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/relay-runtime": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", + "integrity": "sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.0.0", + "fbjs": "^3.0.0", + "invariant": "^2.2.4" + } + }, + "node_modules/remedial": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz", + "integrity": "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "engines": { + "node": "*" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/remove-trailing-spaces": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.9.tgz", + "integrity": "sha512-xzG7w5IRijvIkHIjDk65URsJJ7k4J95wmcArY5PRcmjldIOl7oTvG8+X2Ag690R7SfwiOcHrWZKVc1Pp5WIOzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1856,6 +7308,41 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", @@ -1898,6 +7385,37 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -1911,6 +7429,25 @@ "node": ">=10" } }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1934,6 +7471,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1954,6 +7504,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/signedsource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/signedsource/-/signedsource-1.0.0.tgz", + "integrity": "sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -1969,6 +7526,60 @@ "node": ">=18" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1979,6 +7590,26 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sponge-case": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-1.0.1.tgz", + "integrity": "sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -1993,6 +7624,13 @@ "dev": true, "license": "MIT" }, + "node_modules/string-env-interpolation": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz", + "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -2110,6 +7748,50 @@ "node": ">=8" } }, + "node_modules/swap-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-2.0.2.tgz", + "integrity": "sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/sync-fetch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.6.0.tgz", + "integrity": "sha512-IELLEvzHuCfc1uTsshPK58ViSdNqXxlml1U+fmwJIKLYKOr/rAtBrorE2RYm5IHaMpDNlmC0fr1LAvdXvyheEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^3.3.2", + "timeout-signal": "^2.0.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sync-fetch/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -2125,6 +7807,16 @@ "node": ">=18" } }, + "node_modules/timeout-signal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/timeout-signal/-/timeout-signal-2.0.0.tgz", + "integrity": "sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2186,6 +7878,29 @@ "node": ">=14.0.0" } }, + "node_modules/title-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", + "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -2202,6 +7917,20 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/ts-log": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.7.tgz", + "integrity": "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -2236,6 +7965,43 @@ "node": ">=14.17" } }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -2249,13 +8015,83 @@ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", "license": "MIT" }, + "node_modules/unixify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz", + "integrity": "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "normalize-path": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -2769,7 +8605,6 @@ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", @@ -2830,12 +8665,32 @@ } } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -2976,6 +8831,161 @@ "engines": { "node": ">=8" } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 33b8887..15912b5 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "files": [ "dist/", "README.md", - "LICENSE.md" + "LICENSE.md", + "USAGE.md" ], "scripts": { "build": "tsc && chmod +x dist/main.js", @@ -21,6 +22,19 @@ "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "test:commands": "tsx tests/command-coverage.ts", + "generate": "graphql-codegen --config codegen.config.ts", + "generate:usage": "tsx src/main.ts usage --all > USAGE.md", + "format": "biome format --write .", + "format:check": "biome format .", + "lint": "biome lint --write .", + "lint:check": "biome lint .", + "check": "biome check --write .", + "check:ci": "biome check .", + "prestart": "npm run generate", + "predev": "npm run generate", + "postinstall": "npm run generate && lefthook install", + "prebuild": "npm run generate && npm run generate:usage", + "prepare": "lefthook install", "prepublishOnly": "npm run build && npm run test && test -x dist/main.js" }, "engines": { @@ -48,11 +62,23 @@ "commander": "^14.0.0" }, "devDependencies": { + "@biomejs/biome": "^2.3.14", + "@commitlint/cli": "^20.4.1", + "@commitlint/config-conventional": "^20.4.1", + "@graphql-codegen/cli": "^6.1.1", + "@graphql-codegen/client-preset": "^5.2.2", + "@graphql-codegen/introspection": "5.0.0", + "@graphql-codegen/schema-ast": "^5.0.0", "@types/node": "^22.0.0", "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "^2.1.8", + "lefthook": "^2.1.0", "tsx": "^4.20.5", "typescript": "^5.0.0", "vitest": "^2.1.8" + }, + "graphql": { + "schema": "https://api.linear.app/graphql", + "documents": "**/*.graphql" } } diff --git a/skills/crafting-effective-readmes b/skills/crafting-effective-readmes new file mode 120000 index 0000000..a1aa3ed --- /dev/null +++ b/skills/crafting-effective-readmes @@ -0,0 +1 @@ +../.agents/skills/crafting-effective-readmes \ No newline at end of file diff --git a/src/client/graphql-client.ts b/src/client/graphql-client.ts new file mode 100644 index 0000000..d598365 --- /dev/null +++ b/src/client/graphql-client.ts @@ -0,0 +1,52 @@ +import { LinearClient } from "@linear/sdk"; +import { type DocumentNode, print } from "graphql"; +import { AuthenticationError, isAuthError } from "../common/errors.js"; + +interface GraphQLErrorResponse { + response?: { + errors?: Array<{ message: string }>; + }; + message?: string; +} + +export class GraphQLClient { + private rawClient: InstanceType<typeof LinearClient>["client"]; + + constructor(apiToken: string) { + const linearClient = new LinearClient({ + apiKey: apiToken, + headers: { + // Request 1-hour signed URLs for file downloads (see file-service.ts) + "public-file-urls-expire-in": "3600", + }, + }); + this.rawClient = linearClient.client; + } + + async request<TResult>( + document: DocumentNode, + variables?: Record<string, unknown>, + ): Promise<TResult> { + try { + const response = await this.rawClient.rawRequest( + print(document), + variables, + ); + return response.data as TResult; + } catch (error: unknown) { + const gqlError = error as GraphQLErrorResponse; + const errorMessage = gqlError.response?.errors?.[0]?.message ?? ""; + + if (isAuthError(new Error(errorMessage))) { + throw new AuthenticationError(errorMessage || undefined); + } + + if (errorMessage) { + throw new Error(errorMessage); + } + throw new Error( + `GraphQL request failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } +} diff --git a/src/client/linear-client.ts b/src/client/linear-client.ts new file mode 100644 index 0000000..96b936f --- /dev/null +++ b/src/client/linear-client.ts @@ -0,0 +1,9 @@ +import { LinearClient } from "@linear/sdk"; + +export class LinearSdkClient { + readonly sdk: LinearClient; + + constructor(apiToken: string) { + this.sdk = new LinearClient({ apiKey: apiToken }); + } +} diff --git a/src/commands/auth.ts b/src/commands/auth.ts new file mode 100644 index 0000000..1ed3acb --- /dev/null +++ b/src/commands/auth.ts @@ -0,0 +1,269 @@ +import { exec } from "node:child_process"; +import { createInterface } from "node:readline"; +import type { Command } from "commander"; +import { + type CommandOptions, + resolveApiToken, + type TokenSource, +} from "../common/auth.js"; +import { createGraphQLClient } from "../common/context.js"; +import { handleCommand, outputSuccess } from "../common/output.js"; +import { clearToken, saveToken } from "../common/token-storage.js"; +import type { Viewer } from "../common/types.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import { validateToken } from "../services/auth-service.js"; + +const LINEAR_API_KEY_URL = + "https://linear.app/settings/account/security/api-keys/new"; + +const SOURCE_LABELS: Record<TokenSource, string> = { + flag: "--api-token flag", + env: "LINEAR_API_TOKEN env var", + stored: "~/.linearis/token", + legacy: "~/.linear_api_token (deprecated)", +}; + +export const AUTH_META: DomainMeta = { + name: "auth", + summary: "authenticate with Linear API (interactive, for humans)", + context: [ + "linearis requires a Linear API token for all operations.", + "the auth command guides you through creating and storing a token.", + "tokens are encrypted and stored in ~/.linearis/token.", + "token resolution order: --api-token flag, LINEAR_API_TOKEN env,", + "~/.linearis/token (encrypted), ~/.linear_api_token (deprecated).", + ].join("\n"), + arguments: {}, + seeAlso: [], +}; + +function openBrowser(url: string): void { + const cmd = + process.platform === "darwin" + ? `open "${url}"` + : process.platform === "win32" + ? `start "" "${url}"` + : `xdg-open "${url}"`; + + exec(cmd, () => { + // Browser open failed — URL is already printed, user can open manually + }); +} + +function promptToken(): Promise<string> { + return new Promise((resolve, reject) => { + const rl = createInterface({ + input: process.stdin, + output: process.stderr, + }); + + process.stderr.write("Paste your Linear API token: "); + + if (process.stdin.isTTY) { + // Raw mode: read character by character, mask with * + process.stdin.setRawMode?.(true); + process.stdin.resume(); + process.stdin.setEncoding("utf8"); + + let token = ""; + const onData = (char: string): void => { + if (char === "\n" || char === "\r") { + process.stdin.setRawMode?.(false); + process.stdin.pause(); + process.stdin.removeListener("data", onData); + process.stderr.write("\n"); + rl.close(); + resolve(token.trim()); + } else if (char === "\u0003") { + // Ctrl+C + process.stdin.setRawMode?.(false); + process.stdin.pause(); + process.stdin.removeListener("data", onData); + process.stderr.write("\n"); + rl.close(); + reject(new Error("Cancelled")); + } else if (char === "\u007F" || char === "\b") { + // Backspace + if (token.length > 0) { + token = token.slice(0, -1); + process.stderr.write("\b \b"); + } + } else { + token += char; + process.stderr.write("*"); + } + }; + process.stdin.on("data", onData); + } else { + // Non-TTY: read line normally (piped input) + rl.question("", (answer) => { + rl.close(); + resolve(answer.trim()); + }); + } + }); +} + +function validateApiToken(token: string): Promise<Viewer> { + return validateToken(createGraphQLClient(token)); +} + +export function setupAuthCommands(program: Command): void { + const auth = program + .command("auth") + .description("Authenticate with Linear API"); + + auth.action(() => auth.help()); + + // Login bypasses handleCommand() — interactive UX with raw stdin and process.exit + auth + .command("login") + .description("set up or refresh authentication") + .option("--force", "reauthenticate even if already authenticated") + .action(async (options: { force?: boolean }, command: Command) => { + try { + if (!options.force) { + try { + const rootOpts = command.parent!.parent!.opts() as CommandOptions; + const { token, source } = resolveApiToken(rootOpts); + try { + const viewer = await validateApiToken(token); + console.error( + `Already authenticated as ${viewer.name} (${viewer.email}) via ${SOURCE_LABELS[source]}.`, + ); + console.error("Run with --force to reauthenticate."); + return; + } catch { + // Token is invalid, proceed with new auth + console.error( + "Existing token is invalid. Starting new authentication...", + ); + } + } catch { + // No token found anywhere, proceed with login + } + } + + console.error(""); + console.error("To authenticate, create a new Linear API key:"); + console.error(""); + console.error( + " 1. Open the link below (or it will open automatically)", + ); + console.error(" 2. Set key name to: linearis-cli"); + console.error(" 3. Keep 'Full access' selected (default)"); + console.error(" 4. Keep 'All teams' selected (default)"); + console.error(" 5. Click 'Create'"); + console.error(" 6. Copy the generated token"); + console.error(""); + console.error(` ${LINEAR_API_KEY_URL}`); + console.error(""); + + openBrowser(LINEAR_API_KEY_URL); + + const token = await promptToken(); + + if (!token) { + console.error("No token provided. Authentication cancelled."); + process.exit(1); + return; + } + + console.error("Validating token..."); + let viewer: Viewer; + try { + viewer = await validateApiToken(token); + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + console.error(`Token validation failed: ${detail}`); + process.exit(1); + return; + } + + saveToken(token); + + console.error(""); + console.error( + `Authentication successful. Logged in as ${viewer.name} (${viewer.email}).`, + ); + console.error("Token encrypted and stored in ~/.linearis/token"); + } catch (error) { + console.error( + `Authentication failed: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); + return; + } + }); + + auth + .command("status") + .description("check current authentication status") + .action( + handleCommand(async (...args: unknown[]) => { + const [, command] = args as [CommandOptions, Command]; + const rootOpts = command.parent!.parent!.opts() as CommandOptions; + + let token: string; + let source: TokenSource; + try { + const resolved = resolveApiToken(rootOpts); + token = resolved.token; + source = resolved.source; + } catch { + outputSuccess({ + authenticated: false, + message: + "No API token found. Run 'linearis auth login' to authenticate.", + }); + return; + } + + try { + const viewer = await validateApiToken(token); + outputSuccess({ + authenticated: true, + source: SOURCE_LABELS[source], + user: { id: viewer.id, name: viewer.name, email: viewer.email }, + }); + } catch { + outputSuccess({ + authenticated: false, + source: SOURCE_LABELS[source], + message: + "Token is invalid or expired. Run 'linearis auth login' to reauthenticate.", + }); + } + }), + ); + + auth + .command("logout") + .description("remove stored authentication token") + .action( + handleCommand(async (...args: unknown[]) => { + const [, command] = args as [CommandOptions, Command]; + const rootOpts = command.parent!.parent!.opts() as CommandOptions; + + clearToken(); + + // Warn if a token is still active from another source + try { + const { source } = resolveApiToken(rootOpts); + outputSuccess({ + message: "Authentication token removed.", + warning: `A token is still active via ${SOURCE_LABELS[source]}.`, + }); + } catch { + outputSuccess({ message: "Authentication token removed." }); + } + }), + ); + + auth + .command("usage") + .description("show detailed usage for auth") + .action(() => { + console.log(formatDomainUsage(auth, AUTH_META)); + }); +} diff --git a/src/commands/comments.ts b/src/commands/comments.ts index f75e1d3..d594844 100644 --- a/src/commands/comments.ts +++ b/src/commands/comments.ts @@ -1,68 +1,66 @@ -import { Command } from "commander"; -import { createLinearService } from "../utils/linear-service.js"; -import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; +import type { Command } from "commander"; +import { type CommandOptions, createContext } from "../common/context.js"; +import { handleCommand, outputSuccess } from "../common/output.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import { resolveIssueId } from "../resolvers/issue-resolver.js"; +import { createComment } from "../services/comment-service.js"; + +interface CreateCommentOptions extends CommandOptions { + body?: string; +} + +export const COMMENTS_META: DomainMeta = { + name: "comments", + summary: "discussion threads on issues", + context: "a comment is a text entry on an issue. comments support markdown.", + arguments: { + issue: "issue identifier (UUID or ABC-123)", + }, + seeAlso: ["issues read <issue>"], +}; -/** - * Setup comments commands on the program - * - * Registers the `comments` command group and its subcommands for managing - * Linear issue comments. Provides create operations for adding comments - * to issues with smart ID resolution. - * - * @param program - Commander.js program instance to register commands on - * - * @example - * ```typescript - * // In main.ts - * setupCommentsCommands(program); - * // Enables: linearis comments create ABC-123 --body "My comment" - * ``` - */ export function setupCommentsCommands(program: Command): void { - const comments = program.command("comments") + const comments = program + .command("comments") .description("Comment operations"); - // Show comments help when no subcommand - comments.action(() => { - comments.help(); - }); + comments.action(() => comments.help()); - /** - * Create new comment on issue - * - * Command: `linearis comments create <issueId> --body <comment>` - * - * Supports both UUID and TEAM-123 format issue identifiers. - * Resolves identifiers to UUIDs before creating the comment. - */ - comments.command("create <issueId>") - .description("Create new comment on issue.") - .addHelpText('after', `\nWhen passing issue IDs, both UUID and identifiers like ABC-123 are supported.`) - .option("--body <body>", "comment body (required)") + comments + .command("create <issue>") + .description("create a comment on an issue") + .addHelpText( + "after", + `\nWhen passing issue IDs, both UUID and identifiers like ABC-123 are supported.`, + ) + .option("--body <text>", "comment body (required, markdown supported)") .action( - handleAsyncCommand( - async (issueId: string, options: any, command: Command) => { - // Initialize Linear service with authentication - const service = await createLinearService( - command.parent!.parent!.opts(), - ); + handleCommand(async (...args: unknown[]) => { + const [issue, options, command] = args as [ + string, + CreateCommentOptions, + Command, + ]; + const ctx = createContext(command.parent!.parent!.opts()); - // Validate required body flag - if (!options.body) { - throw new Error("--body is required"); - } + if (!options.body) { + throw new Error("--body is required"); + } - // Resolve issue ID if it's an identifier (TEAM-123 -> UUID) - const resolvedIssueId = await service.resolveIssueId(issueId); + const resolvedIssueId = await resolveIssueId(ctx.sdk, issue); + const result = await createComment(ctx.gql, { + issueId: resolvedIssueId, + body: options.body, + }); - // Create comment using Linear SDK - const result = await service.createComment({ - issueId: resolvedIssueId, - body: options.body, - }); - - outputSuccess(result); - }, - ), + outputSuccess(result); + }), ); + + comments + .command("usage") + .description("show detailed usage for comments") + .action(() => { + console.log(formatDomainUsage(comments, COMMENTS_META)); + }); } diff --git a/src/commands/cycles.ts b/src/commands/cycles.ts index 90db1e5..85cd8a1 100644 --- a/src/commands/cycles.ts +++ b/src/commands/cycles.ts @@ -1,115 +1,146 @@ -import { Command } from "commander"; -import { createLinearService } from "../utils/linear-service.js"; -import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; -import type { - CycleListOptions, - CycleReadOptions, - LinearCycle, -} from "../utils/linear-types.js"; +import type { Command } from "commander"; +import { type CommandOptions, createContext } from "../common/context.js"; import { invalidParameterError, notFoundError, requiresParameterError, -} from "../utils/error-messages.js"; +} from "../common/errors.js"; +import { handleCommand, outputSuccess, parseLimit } from "../common/output.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import { resolveCycleId } from "../resolvers/cycle-resolver.js"; +import { resolveTeamId } from "../resolvers/team-resolver.js"; +import { type Cycle, getCycle, listCycles } from "../services/cycle-service.js"; + +interface CycleListOptions extends CommandOptions { + team?: string; + active?: boolean; + window?: string; + limit: string; + after?: string; +} + +interface CycleReadOptions extends CommandOptions { + team?: string; + limit?: string; +} + +export const CYCLES_META: DomainMeta = { + name: "cycles", + summary: "time-boxed iterations (sprints) per team", + context: [ + "a cycle is a sprint belonging to one team. each team can have one", + "active cycle at a time. cycles contain issues and have start/end dates.", + ].join("\n"), + arguments: { + cycle: "cycle identifier (UUID or name)", + }, + seeAlso: ["issues create --cycle", "issues update --cycle"], +}; export function setupCyclesCommands(program: Command): void { const cycles = program.command("cycles").description("Cycle operations"); cycles.action(() => cycles.help()); - cycles.command("list") - .description("List cycles") - .option("--team <team>", "team key, name, or ID") - .option("--active", "only active cycles") - .option( - "--around-active <n>", - "return active +/- n cycles (requires --team)", - ) + cycles + .command("list") + .description("list cycles") + .option("--team <team>", "filter by team (key, name, or UUID)") + .option("--active", "only show active cycles") + .option("--window <n>", "active cycle +/- n neighbors (requires --team)") + .option("-l, --limit <n>", "max results", "50") + .option("--after <cursor>", "cursor for next page") .action( - handleAsyncCommand( - async (options: CycleListOptions, command: Command) => { - // around-active requires a team to determine the current team's active cycle - // Validate this before authentication to provide better error messages - if (options.aroundActive && !options.team) { - throw requiresParameterError("--around-active", "--team"); - } - - const linearService = await createLinearService( - command.parent!.parent!.opts(), + handleCommand(async (...args: unknown[]) => { + const [options, command] = args as [CycleListOptions, Command]; + if (options.window && !options.team) { + throw requiresParameterError("--window", "--team"); + } + if (options.window && options.after) { + throw invalidParameterError( + "--after", + "cannot be used with --window", ); + } - // Fetch cycles with automatic pagination - const allCycles = await linearService.getCycles( - options.team, - options.active ? true : undefined, - ); + const ctx = createContext(command.parent!.parent!.opts()); + + // Resolve team filter if provided + const teamId = options.team + ? await resolveTeamId(ctx.sdk, options.team) + : undefined; + + // Fetch cycles + const result = await listCycles( + ctx.gql, + teamId, + options.active || false, + { limit: parseLimit(options.limit), after: options.after }, + ); - // If around-active is requested, filter by cycle number range - if (options.aroundActive) { - const n = parseInt(options.aroundActive); - if (isNaN(n) || n < 0) { - throw invalidParameterError( - "--around-active", - "requires a non-negative integer", - ); - } - - const activeCycle = allCycles.find((c: LinearCycle) => c.isActive); - if (!activeCycle) { - throw notFoundError("Active cycle", options.team!, "for team"); - } - - const activeNumber = Number(activeCycle.number || 0); - const min = activeNumber - n; - const max = activeNumber + n; - - const filtered = allCycles - .filter((c: LinearCycle) => - typeof c.number === "number" && c.number >= min && - c.number <= max - ) - .sort((a: LinearCycle, b: LinearCycle) => a.number - b.number); - - outputSuccess(filtered); - return; + if (options.window) { + const n = parseInt(options.window, 10); + if (Number.isNaN(n) || n < 0) { + throw invalidParameterError( + "--window", + "requires a non-negative integer", + ); } - outputSuccess(allCycles); - }, - ), + const activeCycle = result.nodes.find((c: Cycle) => c.isActive); + if (!activeCycle) { + throw notFoundError("Active cycle", options.team ?? "", "for team"); + } + + const activeNumber = activeCycle.number; + const min = activeNumber - n; + const max = activeNumber + n; + + const filteredNodes = result.nodes + .filter((c: Cycle) => c.number >= min && c.number <= max) + .sort((a: Cycle, b: Cycle) => a.number - b.number); + + outputSuccess({ + nodes: filteredNodes, + pageInfo: { hasNextPage: false, endCursor: null }, + }); + return; + } + + outputSuccess(result); + }), ); - cycles.command("read <cycleIdOrName>") - .description( - "Get cycle details including issues. Accepts UUID or cycle name (optionally scoped by --team)", - ) - .option("--team <team>", "team key, name, or ID to scope name lookup") - .option("--issues-first <n>", "how many issues to fetch (default 50)", "50") + cycles + .command("read <cycle>") + .description("get cycle details including issues") + .option("--team <team>", "scope name lookup to team") + .option("--limit <n>", "max issues to fetch", "50") .action( - handleAsyncCommand( - async ( - cycleIdOrName: string, - options: CycleReadOptions, - command: Command, - ) => { - const linearService = await createLinearService( - command.parent!.parent!.opts(), - ); + handleCommand(async (...args: unknown[]) => { + const [cycle, options, command] = args as [ + string, + CycleReadOptions, + Command, + ]; + const ctx = createContext(command.parent!.parent!.opts()); - // Resolve cycle ID (handles both UUID and name-based lookup) - const cycleId = await linearService.resolveCycleId( - cycleIdOrName, - options.team, - ); + const cycleId = await resolveCycleId(ctx.sdk, cycle, options.team); - // Fetch cycle with issues - const cycle = await linearService.getCycleById( - cycleId, - parseInt(options.issuesFirst || "50"), - ); + const cycleResult = await getCycle( + ctx.gql, + cycleId, + parseLimit(options.limit || "50"), + ); - outputSuccess(cycle); - }, - ), + outputSuccess(cycleResult); + }), ); + + cycles + .command("usage") + .description("show detailed usage for cycles") + .action(() => { + console.log(formatDomainUsage(cycles, CYCLES_META)); + }); } diff --git a/src/commands/documents.ts b/src/commands/documents.ts index 5809c0d..fcf4526 100644 --- a/src/commands/documents.ts +++ b/src/commands/documents.ts @@ -1,14 +1,24 @@ -import { Command } from "commander"; -import { createLinearService } from "../utils/linear-service.js"; -import { createGraphQLDocumentsService } from "../utils/graphql-documents-service.js"; +import type { Command } from "commander"; +import { createContext } from "../common/context.js"; +import { handleCommand, outputSuccess, parseLimit } from "../common/output.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import type { DocumentUpdateInput } from "../gql/graphql.js"; +import { resolveIssueId } from "../resolvers/issue-resolver.js"; +import { resolveProjectId } from "../resolvers/project-resolver.js"; +import { resolveTeamId } from "../resolvers/team-resolver.js"; import { - createGraphQLAttachmentsService, -} from "../utils/graphql-attachments-service.js"; -import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; + createAttachment, + listAttachments, +} from "../services/attachment-service.js"; +import { + createDocument, + deleteDocument, + getDocument, + listDocuments, + listDocumentsBySlugIds, + updateDocument, +} from "../services/document-service.js"; -/** - * Options for document create command - */ interface DocumentCreateOptions { title: string; content?: string; @@ -16,12 +26,9 @@ interface DocumentCreateOptions { team?: string; icon?: string; color?: string; - attachTo?: string; + issue?: string; } -/** - * Options for document update command - */ interface DocumentUpdateOptions { title?: string; content?: string; @@ -30,26 +37,14 @@ interface DocumentUpdateOptions { color?: string; } -/** - * Options for document list command - */ interface DocumentListOptions { project?: string; issue?: string; limit?: string; + after?: string; } -/** - * Extract document slug ID from a Linear document URL - * - * Linear document URLs have the format: - * https://linear.app/[workspace]/document/[title-slug]-[slugId] - * - * The slugId is the last segment after the final hyphen in the document path. - * - * @param url URL to parse - * @returns Document slug ID if URL is a Linear document, null otherwise - */ +/** Extracts slug ID from a Linear document URL (e.g. /workspace/document/title-slug-abc123 -> abc123). */ export function extractDocumentIdFromUrl(url: string): string | null { try { const parsed = new URL(url); @@ -57,41 +52,37 @@ export function extractDocumentIdFromUrl(url: string): string | null { return null; } - // Path format: /[workspace]/document/[title-slug]-[slugId] const pathParts = parsed.pathname.split("/"); const docIndex = pathParts.indexOf("document"); if (docIndex === -1 || docIndex >= pathParts.length - 1) { return null; } - // The slug is the part after "document", like "my-doc-title-abc123" - // The slugId is the last segment after the final hyphen const docSlug = pathParts[docIndex + 1]; const lastHyphenIndex = docSlug.lastIndexOf("-"); if (lastHyphenIndex === -1) { - // No hyphen found - the entire slug might be the ID return docSlug || null; } return docSlug.substring(lastHyphenIndex + 1) || null; } catch { - // URL constructor throws on malformed URLs - treat as non-Linear URL - // This is intentional: attachments may contain arbitrary URLs that aren't - // valid, and we simply skip them rather than failing the entire operation return null; } } -/** - * Setup documents commands on the program - * - * Documents in Linear are standalone entities that can be associated with - * projects, initiatives, or teams. They cannot be directly linked to issues. - * To link a document to an issue, use the --attach-to option which creates - * an attachment pointing to the document's URL. - * - * @param program - Commander.js program instance to register commands on - */ +export const DOCUMENTS_META: DomainMeta = { + name: "documents", + summary: "long-form markdown docs attached to projects or issues", + context: [ + "a document is a markdown page. it can belong to a project and/or be", + "attached to an issue. documents support icons and colors.", + ].join("\n"), + arguments: { + document: "document identifier (UUID)", + }, + seeAlso: ["issues read <issue>", "projects list"], +}; + export function setupDocumentsCommands(program: Command): void { const documents = program .command("documents") @@ -99,254 +90,198 @@ export function setupDocumentsCommands(program: Command): void { documents.action(() => documents.help()); - /** - * Create a new document - * - * Command: `linearis documents create --title <title> [options]` - */ documents - .command("create") - .description("Create a new document") - .requiredOption("--title <title>", "document title") - .option("--content <content>", "document content (markdown)") - .option("--project <project>", "project name or ID") - .option("--team <team>", "team key or name") - .option("--icon <icon>", "document icon") - .option("--color <color>", "icon color") + .command("list") + .description("list documents") + .option("--project <project>", "filter by project name or ID") .option( - "--attach-to <issue>", - "also attach document to issue (e.g., ABC-123)", + "--issue <issue>", + "filter by issue (shows documents attached to the issue)", ) + .option("-l, --limit <n>", "max results", "50") + .option("--after <cursor>", "cursor for next page") .action( - handleAsyncCommand( - async (options: DocumentCreateOptions, command: Command) => { - const rootOpts = command.parent!.parent!.opts(); - const [documentsService, linearService] = await Promise.all([ - createGraphQLDocumentsService(rootOpts), - createLinearService(rootOpts), - ]); - - // Resolve project ID if provided - let projectId: string | undefined; - if (options.project) { - projectId = await linearService.resolveProjectId(options.project); - } + handleCommand(async (...args: unknown[]) => { + const [options, command] = args as [DocumentListOptions, Command]; + if (options.project && options.issue) { + throw new Error( + "Cannot use --project and --issue together. Choose one filter.", + ); + } - // Resolve team ID if provided - let teamId: string | undefined; - if (options.team) { - teamId = await linearService.resolveTeamId(options.team); + const rootOpts = command.parent!.parent!.opts(); + const ctx = createContext(rootOpts); + + const limit = parseLimit(options.limit || "50"); + + if (options.issue) { + const issueId = await resolveIssueId(ctx.sdk, options.issue); + const attachments = await listAttachments(ctx.gql, issueId); + + const documentSlugIds = [ + ...new Set( + attachments + .map((att) => extractDocumentIdFromUrl(att.url)) + .filter((id): id is string => id !== null), + ), + ]; + + if (documentSlugIds.length === 0) { + outputSuccess({ + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }); + return; } - // Create the document - const document = await documentsService.createDocument({ - title: options.title, - content: options.content, - projectId, - teamId, - icon: options.icon, - color: options.color, + const documents = await listDocumentsBySlugIds( + ctx.gql, + documentSlugIds, + ); + outputSuccess({ + nodes: documents, + pageInfo: { hasNextPage: false, endCursor: null }, }); + return; + } + + let projectId: string | undefined; + if (options.project) { + projectId = await resolveProjectId(ctx.sdk, options.project); + } + + const documents = await listDocuments(ctx.gql, { + limit, + after: options.after, + filter: projectId + ? { project: { id: { eq: projectId } } } + : undefined, + }); + + outputSuccess(documents); + }), + ); - // Optionally attach to issue - if (options.attachTo) { - const attachmentsService = - await createGraphQLAttachmentsService(rootOpts); - const issueId = await linearService.resolveIssueId(options.attachTo); - - try { - await attachmentsService.createAttachment({ - issueId, - url: document.url, - title: document.title, - }); - } catch (attachError) { - // Document was created but attachment failed - provide actionable error - const errorMessage = - attachError instanceof Error - ? attachError.message - : String(attachError); - throw new Error( - `Document created (${document.id}) but failed to attach to issue "${options.attachTo}": ${errorMessage}.`, - ); - } - } + documents + .command("read <document>") + .description("get document content") + .action( + handleCommand(async (...args: unknown[]) => { + const [document, , command] = args as [string, unknown, Command]; + const rootOpts = command.parent!.parent!.opts(); + const ctx = createContext(rootOpts); - outputSuccess(document); - }, - ), + const documentResult = await getDocument(ctx.gql, document); + outputSuccess(documentResult); + }), ); - /** - * Update an existing document - * - * Command: `linearis documents update <document-id> [options]` - */ documents - .command("update <documentId>") - .description("Update an existing document") - .option("--title <title>", "new document title") - .option("--content <content>", "new document content (markdown)") - .option("--project <project>", "move to different project") + .command("create") + .description("create a new document") + .requiredOption("--title <title>", "document title (required)") + .option("--content <text>", "document content (markdown)") + .option("--project <project>", "project name or ID") + .option("--team <team>", "team key or name") .option("--icon <icon>", "document icon") .option("--color <color>", "icon color") + .option("--issue <issue>", "also attach document to issue (e.g., ABC-123)") .action( - handleAsyncCommand( - async ( - documentId: string, - options: DocumentUpdateOptions, - command: Command, - ) => { - const rootOpts = command.parent!.parent!.opts(); - const [documentsService, linearService] = await Promise.all([ - createGraphQLDocumentsService(rootOpts), - createLinearService(rootOpts), - ]); - - // Build input with only provided fields - const input: Record<string, unknown> = {}; - if (options.title) input.title = options.title; - if (options.content) input.content = options.content; - if (options.project) { - input.projectId = await linearService.resolveProjectId( - options.project, + handleCommand(async (...args: unknown[]) => { + const [options, command] = args as [DocumentCreateOptions, Command]; + const rootOpts = command.parent!.parent!.opts(); + const ctx = createContext(rootOpts); + + const projectId = options.project + ? await resolveProjectId(ctx.sdk, options.project) + : undefined; + const teamId = options.team + ? await resolveTeamId(ctx.sdk, options.team) + : undefined; + + const document = await createDocument(ctx.gql, { + title: options.title, + content: options.content, + projectId, + teamId, + icon: options.icon, + color: options.color, + }); + + if (options.issue) { + const issueId = await resolveIssueId(ctx.sdk, options.issue); + + try { + await createAttachment(ctx.gql, { + issueId, + url: document.url, + title: document.title, + }); + } catch (attachError) { + const errorMessage = + attachError instanceof Error + ? attachError.message + : String(attachError); + throw new Error( + `Document created (${document.id}) but failed to attach to issue "${options.issue}": ${errorMessage}.`, ); } - if (options.icon) input.icon = options.icon; - if (options.color) input.color = options.color; + } - const document = await documentsService.updateDocument( - documentId, - input, - ); - outputSuccess(document); - }, - ), + outputSuccess(document); + }), ); - /** - * Read a document - * - * Command: `linearis documents read <document-id>` - */ documents - .command("read <documentId>") - .description("Read a document") + .command("update <document>") + .description("update an existing document") + .option("--title <title>", "new title") + .option("--content <text>", "new content (markdown)") + .option("--project <project>", "move to project") + .option("--icon <icon>", "new icon") + .option("--color <color>", "new icon color") .action( - // Note: _options parameter is required by Commander.js signature (arg, options, command) - handleAsyncCommand(async (documentId: string, _options: unknown, command: Command) => { + handleCommand(async (...args: unknown[]) => { + const [document, options, command] = args as [ + string, + DocumentUpdateOptions, + Command, + ]; const rootOpts = command.parent!.parent!.opts(); - const documentsService = await createGraphQLDocumentsService(rootOpts); - - const document = await documentsService.getDocument(documentId); - outputSuccess(document); + const ctx = createContext(rootOpts); + + const input: DocumentUpdateInput = {}; + if (options.title) input.title = options.title; + if (options.content) input.content = options.content; + if (options.project) { + input.projectId = await resolveProjectId(ctx.sdk, options.project); + } + if (options.icon) input.icon = options.icon; + if (options.color) input.color = options.color; + + const updatedDocument = await updateDocument(ctx.gql, document, input); + outputSuccess(updatedDocument); }), ); - /** - * List documents - * - * Command: `linearis documents list [options]` - * - * Can filter by project OR by issue. When filtering by issue, the command - * finds all attachments on that issue, identifies which point to Linear - * documents, and fetches those documents. - */ documents - .command("list") - .description("List documents") - .option("--project <project>", "filter by project name or ID") - .option("--issue <issue>", "filter by issue (shows documents attached to the issue)") - .option("-l, --limit <limit>", "maximum number of documents", "50") + .command("delete <document>") + .description("trash a document") .action( - handleAsyncCommand( - async (options: DocumentListOptions, command: Command) => { - // Validate mutually exclusive options - if (options.project && options.issue) { - throw new Error( - "Cannot use --project and --issue together. Choose one filter.", - ); - } - - const rootOpts = command.parent!.parent!.opts(); - const [documentsService, linearService] = await Promise.all([ - createGraphQLDocumentsService(rootOpts), - createLinearService(rootOpts), - ]); - - // Validate limit option - const limit = parseInt(options.limit || "50", 10); - if (isNaN(limit) || limit < 1) { - throw new Error( - `Invalid limit "${options.limit}": must be a positive number`, - ); - } - - // Handle --issue filter: find documents via attachments - if (options.issue) { - const attachmentsService = - await createGraphQLAttachmentsService(rootOpts); - const issueId = await linearService.resolveIssueId(options.issue); - const attachments = await attachmentsService.listAttachments(issueId); - - // Extract document slug IDs from Linear document URLs and deduplicate - const documentSlugIds = [ - ...new Set( - attachments - .map((att) => extractDocumentIdFromUrl(att.url)) - .filter((id): id is string => id !== null), - ), - ]; - - if (documentSlugIds.length === 0) { - outputSuccess([]); - return; - } - - const documents = await documentsService.listDocumentsBySlugIds( - documentSlugIds, - limit, - ); - outputSuccess(documents); - return; - } - - // Handle --project filter or no filter - let projectId: string | undefined; - if (options.project) { - projectId = await linearService.resolveProjectId(options.project); - } - - const documents = await documentsService.listDocuments({ - projectId, - first: limit, - }); + handleCommand(async (...args: unknown[]) => { + const [document, , command] = args as [string, unknown, Command]; + const rootOpts = command.parent!.parent!.opts(); + const ctx = createContext(rootOpts); - outputSuccess(documents); - }, - ), + await deleteDocument(ctx.gql, document); + outputSuccess({ success: true, message: "Document moved to trash" }); + }), ); - /** - * Delete (trash) a document - * - * Command: `linearis documents delete <document-id>` - * - * This is a soft delete - the document is moved to trash. - */ documents - .command("delete <documentId>") - .description("Delete (trash) a document") - .action( - // Note: _options parameter is required by Commander.js signature (arg, options, command) - handleAsyncCommand( - async (documentId: string, _options: unknown, command: Command) => { - const rootOpts = command.parent!.parent!.opts(); - const documentsService = await createGraphQLDocumentsService(rootOpts); - - await documentsService.deleteDocument(documentId); - outputSuccess({ success: true, message: "Document moved to trash" }); - }, - ), - ); + .command("usage") + .description("show detailed usage for documents") + .action(() => { + console.log(formatDomainUsage(documents, DOCUMENTS_META)); + }); } diff --git a/src/commands/embeds.ts b/src/commands/embeds.ts deleted file mode 100644 index e8a1a07..0000000 --- a/src/commands/embeds.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Command } from "commander"; -import { getApiToken } from "../utils/auth.js"; -import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; -import { FileService } from "../utils/file-service.js"; - -/** - * Setup embeds commands on the program - * - * Registers `embeds` command group for uploading and downloading embedded files - * from Linear's private cloud storage. Handles file operations with - * authentication and error reporting. - * - * @param program - Commander.js program instance to register commands on - * - * @example - * ```typescript - * // In main.ts - * setupEmbedsCommands(program); - * // Enables: - * // linearis embeds upload <file> - * // linearis embeds download <url> [--output path] [--overwrite] - * ``` - */ -export function setupEmbedsCommands(program: Command): void { - const embeds = program - .command("embeds") - .description("Upload and download files from Linear storage."); - - // Show embeds help when no subcommand - embeds.action(() => { - embeds.help(); - }); - - /** - * Download file from Linear storage - * - * Command: `linearis embeds download <url> [--output <path>] [--overwrite]` - * - * Downloads files from Linear's private cloud storage with automatic - * authentication handling. Supports signed URLs and creates directories - * as needed. - */ - embeds - .command("download <url>") - .description("Download a file from Linear storage.") - .option("--output <path>", "output file path") - .option("--overwrite", "overwrite existing file", false) - .action( - handleAsyncCommand( - async (url: string, options: any, command: Command) => { - // Get API token from parent command options for authentication - const apiToken = await getApiToken(command.parent!.parent!.opts()); - - // Create file service and initiate download - const fileService = new FileService(apiToken); - const result = await fileService.downloadFile(url, { - output: options.output, - overwrite: options.overwrite, - }); - - if (result.success) { - // Successful download with file path - outputSuccess({ - success: true, - filePath: result.filePath, - message: `File downloaded successfully to ${result.filePath}`, - }); - } else { - // Include status code for debugging authentication issues - const error: any = { - success: false, - error: result.error, - }; - if (result.statusCode) { - error.statusCode = result.statusCode; - } - outputSuccess(error); - } - }, - ), - ); - - /** - * Upload file to Linear storage - * - * Command: `linearis embeds upload <file>` - * - * Uploads a local file to Linear's cloud storage using the fileUpload - * GraphQL mutation. Returns the asset URL which can be used in markdown - * for comments, descriptions, etc. - */ - embeds - .command("upload <file>") - .description("Upload a file to Linear storage.") - .action( - handleAsyncCommand( - async (filePath: string, _options: any, command: Command) => { - // Get API token from parent command options for authentication - const apiToken = await getApiToken(command.parent!.parent!.opts()); - - // Create file service and initiate upload - const fileService = new FileService(apiToken); - const result = await fileService.uploadFile(filePath); - - if (result.success) { - // Successful upload with asset URL - outputSuccess({ - success: true, - assetUrl: result.assetUrl, - filename: result.filename, - message: `File uploaded successfully: ${result.assetUrl}`, - }); - } else { - // Include status code for debugging - const error: any = { - success: false, - error: result.error, - }; - if (result.statusCode) { - error.statusCode = result.statusCode; - } - outputSuccess(error); - } - }, - ), - ); -} diff --git a/src/commands/files.ts b/src/commands/files.ts new file mode 100644 index 0000000..b35f15e --- /dev/null +++ b/src/commands/files.ts @@ -0,0 +1,96 @@ +import type { Command } from "commander"; +import { type CommandOptions, getApiToken } from "../common/auth.js"; +import { handleCommand, outputSuccess } from "../common/output.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import { FileService } from "../services/file-service.js"; + +export const FILES_META: DomainMeta = { + name: "files", + summary: "upload/download file attachments", + context: [ + "files are binary attachments stored in Linear's storage. upload returns", + "a URL that can be referenced in issue descriptions or comments.", + ].join("\n"), + arguments: { + url: "Linear storage URL", + file: "local file path", + }, + seeAlso: [], +}; + +export function setupFilesCommands(program: Command): void { + const files = program + .command("files") + .description("Upload and download files from Linear storage."); + + files.action(() => files.help()); + + files + .command("download <url>") + .description("download a file from Linear storage") + .option("--output <path>", "output file path") + .option("--overwrite", "overwrite existing file", false) + .action( + handleCommand(async (...args: unknown[]) => { + const [url, options, command] = args as [ + string, + CommandOptions & { output?: string; overwrite?: boolean }, + Command, + ]; + const apiToken = getApiToken(command.parent!.parent!.opts()); + const fileService = new FileService(apiToken); + const result = await fileService.downloadFile(url, { + output: options.output, + overwrite: options.overwrite, + }); + + if (result.success) { + outputSuccess({ + success: true, + filePath: result.filePath, + message: `File downloaded successfully to ${result.filePath}`, + }); + } else { + outputSuccess({ + success: false, + error: result.error || "Download failed", + statusCode: result.statusCode, + }); + } + }), + ); + + files + .command("upload <file>") + .description("upload a file to Linear storage") + .action( + handleCommand(async (...args: unknown[]) => { + const [filePath, , command] = args as [string, CommandOptions, Command]; + const apiToken = getApiToken(command.parent!.parent!.opts()); + const fileService = new FileService(apiToken); + const result = await fileService.uploadFile(filePath); + + if (result.success) { + outputSuccess({ + success: true, + assetUrl: result.assetUrl, + filename: result.filename, + message: `File uploaded successfully: ${result.assetUrl}`, + }); + } else { + outputSuccess({ + success: false, + error: result.error || "Upload failed", + statusCode: result.statusCode, + }); + } + }), + ); + + files + .command("usage") + .description("show detailed usage for files") + .action(() => { + console.log(formatDomainUsage(files, FILES_META)); + }); +} diff --git a/src/commands/issues.ts b/src/commands/issues.ts index e571b21..dadab50 100644 --- a/src/commands/issues.ts +++ b/src/commands/issues.ts @@ -1,353 +1,523 @@ -import { Command } from "commander"; -import { createGraphQLService } from "../utils/graphql-service.js"; -import { GraphQLIssuesService } from "../utils/graphql-issues-service.js"; -import { createLinearService } from "../utils/linear-service.js"; -import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; - -/** - * Setup issues commands on the program - * - * Registers the `issues` command group with comprehensive issue management - * operations including create, read, list, search, and update functionality. - * Uses optimized GraphQL queries for efficient data retrieval. - * - * @param program - Commander.js program instance to register commands on - * - * @example - * ```typescript - * // In main.ts - * setupIssuesCommands(program); - * // Enables: linearis issues list|read|search|create|update ... - * ``` - */ +import type { Command } from "commander"; +import type { CommandContext } from "../common/context.js"; +import { createContext } from "../common/context.js"; +import { isUuid, parseIssueIdentifier } from "../common/identifier.js"; +import { handleCommand, outputSuccess, parseLimit } from "../common/output.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import { + type IssueCreateInput, + IssueRelationType, + type IssueUpdateInput, +} from "../gql/graphql.js"; +import { resolveCycleId } from "../resolvers/cycle-resolver.js"; +import { resolveIssueId } from "../resolvers/issue-resolver.js"; +import { resolveLabelIds } from "../resolvers/label-resolver.js"; +import { resolveMilestoneId } from "../resolvers/milestone-resolver.js"; +import { resolveProjectId } from "../resolvers/project-resolver.js"; +import { resolveStatusId } from "../resolvers/status-resolver.js"; +import { resolveTeamId } from "../resolvers/team-resolver.js"; +import { resolveUserId } from "../resolvers/user-resolver.js"; +import { + createIssueRelation, + deleteIssueRelation, + findIssueRelation, +} from "../services/issue-relation-service.js"; +import { + createIssue, + getIssue, + getIssueByIdentifier, + listIssues, + searchIssues, + updateIssue, +} from "../services/issue-service.js"; + +interface ListOptions { + query?: string; + limit: string; + after?: string; +} + +interface CreateOptions { + description?: string; + assignee?: string; + priority?: string; + project?: string; + team?: string; + labels?: string; + projectMilestone?: string; + cycle?: string; + status?: string; + parentTicket?: string; + blocks?: string; + blockedBy?: string; + relatesTo?: string; + duplicateOf?: string; +} + +interface UpdateOptions { + title?: string; + description?: string; + status?: string; + priority?: string; + assignee?: string; + project?: string; + labels?: string; + labelMode?: string; + clearLabels?: boolean; + parentTicket?: string; + clearParentTicket?: boolean; + projectMilestone?: string; + clearProjectMilestone?: boolean; + cycle?: string; + clearCycle?: boolean; + blocks?: string; + blockedBy?: string; + relatesTo?: string; + duplicateOf?: string; + removeRelation?: string; +} + +export const ISSUES_META: DomainMeta = { + name: "issues", + summary: "work items with status, priority, assignee, labels", + context: [ + "an issue belongs to exactly one team. it has a status (e.g. backlog,", + "todo, in progress, done — configurable per team), a priority (1-4),", + "and can be assigned to a user. issues can have labels, belong to a", + "project, be part of a cycle (sprint), and reference a project milestone.", + "parent-child relationships and issue relations (blocks, blocked-by,", + "relates-to, duplicate-of) are supported.", + ].join("\n"), + arguments: { + issue: "issue identifier (UUID or ABC-123)", + title: "string", + }, + seeAlso: ["comments create <issue>", "documents list --issue <issue>"], +}; + +interface RelationFlags { + blocks?: string; + blockedBy?: string; + relatesTo?: string; + duplicateOf?: string; + removeRelation?: string; +} + +function validateRelationFlags(flags: RelationFlags): void { + const active = [ + flags.blocks, + flags.blockedBy, + flags.relatesTo, + flags.duplicateOf, + flags.removeRelation, + ].filter(Boolean); + if (active.length > 1) { + throw new Error("Only one relation flag can be used at a time"); + } +} + +async function resolveRelationTarget( + ctx: CommandContext, + flags: RelationFlags, +): Promise<string | undefined> { + const target = + flags.blocks ?? + flags.blockedBy ?? + flags.relatesTo ?? + flags.duplicateOf ?? + flags.removeRelation; + return target ? resolveIssueId(ctx.sdk, target) : undefined; +} + +async function applyRelation( + ctx: CommandContext, + issueId: string, + targetId: string, + flags: RelationFlags, +): Promise<void> { + if (flags.blocks) { + await createIssueRelation(ctx.gql, { + issueId, + relatedIssueId: targetId, + type: IssueRelationType.Blocks, + }); + } else if (flags.blockedBy) { + await createIssueRelation(ctx.gql, { + issueId: targetId, + relatedIssueId: issueId, + type: IssueRelationType.Blocks, + }); + } else if (flags.relatesTo) { + await createIssueRelation(ctx.gql, { + issueId, + relatedIssueId: targetId, + type: IssueRelationType.Related, + }); + } else if (flags.duplicateOf) { + await createIssueRelation(ctx.gql, { + issueId, + relatedIssueId: targetId, + type: IssueRelationType.Duplicate, + }); + } else if (flags.removeRelation) { + const relationId = await findIssueRelation(ctx.gql, issueId, targetId); + await deleteIssueRelation(ctx.gql, relationId); + } +} + export function setupIssuesCommands(program: Command): void { - const issues = program.command("issues") - .description("Issue operations"); - - // Show issues help when no subcommand - issues.action(() => { - issues.help(); - }); - - /** - * List issues - * - * Command: `linearis issues list [--limit <number>]` - * - * Lists issues with all relationships in a single optimized GraphQL query. - * Includes comments, assignees, projects, labels, and state information. - */ - issues.command("list") - .description("List issues.") - .option("-l, --limit <number>", "limit results", "25") + const issues = program.command("issues").description("Issue operations"); + + issues.action(() => issues.help()); + + issues + .command("list") + .description("list issues with optional filters") + .option("--query <text>", "filter by text search") + .option("-l, --limit <n>", "max results", "50") + .option("--after <cursor>", "cursor for next page") .action( - handleAsyncCommand( - async (options: any, command: Command) => { - // Initialize both services for comprehensive issue data - const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), - ]); - const issuesService = new GraphQLIssuesService( - graphQLService, - linearService, - ); + handleCommand(async (...args: unknown[]) => { + const [options, command] = args as [ListOptions, Command]; + const ctx = createContext(command.parent!.parent!.opts()); + + const paginationOptions = { + limit: parseLimit(options.limit), + after: options.after, + }; - // Fetch issues with optimized single query - const result = await issuesService.getIssues(parseInt(options.limit)); + if (options.query) { + const result = await searchIssues( + ctx.gql, + options.query, + paginationOptions, + ); outputSuccess(result); - }, - ), + } else { + const result = await listIssues(ctx.gql, paginationOptions); + outputSuccess(result); + } + }), ); - /** - * Search issues - * - * Command: `linearis issues search <query> [options]` - * - * Searches issues with optional filtering by team, assignee, project, - * and workflow states. Uses optimized GraphQL queries with batch resolution. - */ - issues.command("search <query>") - .description("Search issues.") - .option("--team <team>", "filter by team key, name, or ID") - .option("--assignee <assigneeId>", "filter by assignee ID") - .option("--project <project>", "filter by project name or ID") - .option("--status <status>", "filter by status (comma-separated)") - .option("-l, --limit <number>", "limit results", "10") + issues + .command("read <issue>") + .description("get full issue details including description") + .addHelpText( + "after", + `\nWhen passing issue IDs, both UUID and identifiers like ABC-123 are supported.`, + ) .action( - handleAsyncCommand( - async (query: string, options: any, command: Command) => { - const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), - ]); - const issuesService = new GraphQLIssuesService( - graphQLService, - linearService, - ); + handleCommand(async (...args: unknown[]) => { + const [issue, , command] = args as [string, unknown, Command]; + const ctx = createContext(command.parent!.parent!.opts()); - const searchArgs = { - query, - teamId: options.team, // GraphQL service handles team resolution - assigneeId: options.assignee, // GraphQL service handles assignee resolution - projectId: options.project, // GraphQL service handles project resolution - status: options.status ? options.status.split(",") : undefined, - limit: parseInt(options.limit), - }; - const result = await issuesService.searchIssues(searchArgs); + if (isUuid(issue)) { + const result = await getIssue(ctx.gql, issue); outputSuccess(result); - }, - ), + } else { + const { teamKey, issueNumber } = parseIssueIdentifier(issue); + const result = await getIssueByIdentifier( + ctx.gql, + teamKey, + issueNumber, + ); + outputSuccess(result); + } + }), ); - /** - * Create new issue - * - * Command: `linearis issues create <title> [options]` - * - * Creates a new issue with optional description, assignee, priority, - * project, labels, and milestone. Uses smart ID resolution for all - * entity references (teams, projects, labels, etc.). - */ - issues.command("create <title>") - .description("Create new issue.") - .option("-d, --description <desc>", "issue description") - .option("-a, --assignee <assigneeId>", "assign to user ID") - .option("-p, --priority <priority>", "priority level (1-4)") - .option("--project <project>", "add to project (name or ID)") - .option( - "--team <team>", - "team key, name, or ID (required if not specified)", - ) - .option("--labels <labels>", "labels (comma-separated names or IDs)") - .option( - "--project-milestone <milestone>", - "project milestone name or ID (requires --project)", - ) - .option( - "--cycle <cycle>", - "cycle name or ID (requires --team)", - ) - .option("--status <status>", "status name or ID") - .option("--parent-ticket <parentId>", "parent issue ID or identifier") + issues + .command("create <title>") + .description("create new issue") + .option("--description <text>", "issue body") + .option("--assignee <user>", "assign to user") + .option("--priority <1-4>", "1=urgent 2=high 3=medium 4=low") + .option("--project <project>", "add to project") + .option("--team <team>", "target team (required)") + .option("--labels <labels>", "comma-separated label names or UUIDs") + .option("--project-milestone <ms>", "set milestone (requires --project)") + .option("--cycle <cycle>", "add to cycle (requires --team)") + .option("--status <status>", "set status") + .option("--parent-ticket <issue>", "set parent issue") + .option("--blocks <issue>", "this issue blocks <issue>") + .option("--blocked-by <issue>", "this issue is blocked by <issue>") + .option("--relates-to <issue>", "this issue relates to <issue>") + .option("--duplicate-of <issue>", "this issue duplicates <issue>") .action( - handleAsyncCommand( - async (title: string, options: any, command: Command) => { - const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), - ]); - const issuesService = new GraphQLIssuesService( - graphQLService, - linearService, - ); + handleCommand(async (...args: unknown[]) => { + const [title, options, command] = args as [ + string, + CreateOptions, + Command, + ]; + const ctx = createContext(command.parent!.parent!.opts()); + + validateRelationFlags(options); + + if (!options.team) { + throw new Error("--team is required"); + } + const teamId = await resolveTeamId(ctx.sdk, options.team); + + const input: IssueCreateInput = { + title, + teamId, + }; + + if (options.description) { + input.description = options.description; + } + + if (options.assignee) { + input.assigneeId = await resolveUserId(ctx.sdk, options.assignee); + } + + if (options.priority) { + input.priority = parseInt(options.priority, 10); + } + + if (options.project) { + input.projectId = await resolveProjectId(ctx.sdk, options.project); + } - // Prepare labels array if provided - let labelIds: string[] | undefined; - if (options.labels) { - labelIds = options.labels.split(",").map((l: string) => l.trim()); + if (options.labels) { + const labelNames = options.labels.split(",").map((l) => l.trim()); + input.labelIds = await resolveLabelIds(ctx.sdk, labelNames); + } + + if (options.projectMilestone) { + if (!options.project) { + throw new Error( + "--project-milestone requires --project to be specified", + ); } + input.projectMilestoneId = await resolveMilestoneId( + ctx.gql, + ctx.sdk, + options.projectMilestone, + options.project, + ); + } - const createArgs = { - title, - teamId: options.team, // GraphQL service handles team resolution - description: options.description, - assigneeId: options.assignee, - priority: options.priority ? parseInt(options.priority) : undefined, - projectId: options.project, // GraphQL service handles project resolution - statusId: options.status, - labelIds, // GraphQL service handles label resolution - parentId: options.parentTicket, // GraphQL service handles parent resolution - milestoneId: options.projectMilestone, - cycleId: options.cycle, - }; - - const result = await issuesService.createIssue(createArgs); - outputSuccess(result); - }, - ), - ); + if (options.cycle) { + input.cycleId = await resolveCycleId( + ctx.sdk, + options.cycle, + options.team, + ); + } - /** - * Get issue details - * - * Command: `linearis issues read <issueId>` - * - * Retrieves complete issue details including all relationships and comments - * in a single optimized GraphQL query. Supports both UUID and TEAM-123 formats. - */ - issues.command("read <issueId>") - .description("Get issue details.") - .addHelpText( - "after", - `\nWhen passing issue IDs, both UUID and identifiers like ABC-123 are supported.`, - ) - .action( - handleAsyncCommand( - async (issueId: string, _options: any, command: Command) => { - // Initialize both services for comprehensive issue data - const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), - ]); - const issuesService = new GraphQLIssuesService( - graphQLService, - linearService, + if (options.status) { + input.stateId = await resolveStatusId( + ctx.sdk, + options.status, + teamId, ); + } - // Get issue with all relationships and comments - const result = await issuesService.getIssueById(issueId); - outputSuccess(result); - }, - ), + if (options.parentTicket) { + input.parentId = await resolveIssueId(ctx.sdk, options.parentTicket); + } + + const relationTargetId = await resolveRelationTarget(ctx, options); + + const result = await createIssue(ctx.gql, input); + + if (relationTargetId) { + await applyRelation(ctx, result.id, relationTargetId, options); + } + + outputSuccess(result); + }), ); - /** - * Update an issue - * - * Command: `linearis issues update <issueId> [options]` - * - * Updates issue properties including title, description, state, priority, - * assignee, project, labels, and parent relationship. Supports both - * label adding and overwriting modes. - */ - issues.command("update <issueId>") - .description("Update an issue.") + issues + .command("update <issue>") + .description("update an existing issue") .addHelpText( "after", `\nWhen passing issue IDs, both UUID and identifiers like ABC-123 are supported.`, ) - .option("-t, --title <title>", "new title") - .option("-d, --description <desc>", "new description") - .option("-s, --status <status>", "new status name or ID") - .option("-p, --priority <priority>", "new priority (1-4)") - .option("--assignee <assigneeId>", "new assignee ID") - .option("--project <project>", "new project (name or ID)") - .optionsGroup("Labels-related options:") - .option( - "--labels <labels>", - "labels to work with (comma-separated names or IDs)", - ) - .option( - "--label-by <mode>", - "how to apply labels: 'adding' (default) or 'overwriting'", - ) - .option("--clear-labels", "remove all labels from issue") - .optionsGroup("Parent ticket-related options:") - .option("--parent-ticket <parentId>", "set parent issue ID or identifier") - .option("--clear-parent-ticket", "clear existing parent relationship") - .optionsGroup("Project milestone-related options:") - .option( - "--project-milestone <milestone>", - "set project milestone (can use name or ID, will try to resolve within project context first)", - ) - .option( - "--clear-project-milestone", - "clear existing project milestone assignment", - ) - .optionsGroup("Cycle-related options:") - .option( - "--cycle <cycle>", - "set cycle (can use name or ID, will try to resolve within team context first)", - ) - .option("--clear-cycle", "clear existing cycle assignment") + .option("--title <text>", "new title") + .option("--description <text>", "new description") + .option("--status <status>", "new status") + .option("--priority <1-4>", "new priority") + .option("--assignee <user>", "new assignee") + .option("--project <project>", "new project") + .option("--labels <labels>", "labels to apply (comma-separated)") + .option("--label-mode <mode>", "add | overwrite") + .option("--clear-labels", "remove all labels") + .option("--parent-ticket <issue>", "set parent issue") + .option("--clear-parent-ticket", "clear parent") + .option("--project-milestone <ms>", "set project milestone") + .option("--clear-project-milestone", "clear project milestone") + .option("--cycle <cycle>", "set cycle") + .option("--clear-cycle", "clear cycle") + .option("--blocks <issue>", "add blocks relation") + .option("--blocked-by <issue>", "add blocked-by relation") + .option("--relates-to <issue>", "add relates-to relation") + .option("--duplicate-of <issue>", "add duplicate relation") + .option("--remove-relation <issue>", "remove relation with <issue>") .action( - handleAsyncCommand( - async (issueId: string, options: any, command: Command) => { - // Check for mutually exclusive parent flags - if (options.parentTicket && options.clearParentTicket) { - throw new Error( - "Cannot use --parent-ticket and --clear-parent-ticket together", - ); - } + handleCommand(async (...args: unknown[]) => { + const [issue, options, command] = args as [ + string, + UpdateOptions, + Command, + ]; + if (options.parentTicket && options.clearParentTicket) { + throw new Error( + "Cannot use --parent-ticket and --clear-parent-ticket together", + ); + } - // Check for mutually exclusive milestone flags - if (options.projectMilestone && options.clearProjectMilestone) { - throw new Error( - "Cannot use --project-milestone and --clear-project-milestone together", - ); - } + if (options.projectMilestone && options.clearProjectMilestone) { + throw new Error( + "Cannot use --project-milestone and --clear-project-milestone together", + ); + } - // Check for mutually exclusive cycle flags - if (options.cycle && options.clearCycle) { - throw new Error( - "Cannot use --cycle and --clear-cycle together", - ); - } + if (options.cycle && options.clearCycle) { + throw new Error("Cannot use --cycle and --clear-cycle together"); + } - // Validate label operation flags - if (options.labelBy && !options.labels) { - throw new Error( - "--label-by requires --labels to be specified", - ); - } + if (options.labelMode && !options.labels) { + throw new Error("--label-mode requires --labels to be specified"); + } - if (options.clearLabels && options.labels) { - throw new Error( - "--clear-labels cannot be used with --labels", - ); - } + if (options.clearLabels && options.labels) { + throw new Error("--clear-labels cannot be used with --labels"); + } - if (options.clearLabels && options.labelBy) { - throw new Error( - "--clear-labels cannot be used with --label-by", - ); - } + if (options.clearLabels && options.labelMode) { + throw new Error("--clear-labels cannot be used with --label-mode"); + } - // Validate label-by mode values - if ( - options.labelBy && - !["adding", "overwriting"].includes(options.labelBy) - ) { - throw new Error( - "--label-by must be either 'adding' or 'overwriting'", - ); - } + if ( + options.labelMode && + !["add", "overwrite"].includes(options.labelMode) + ) { + throw new Error("--label-mode must be either 'add' or 'overwrite'"); + } + + validateRelationFlags(options); + + const ctx = createContext(command.parent!.parent!.opts()); + + const resolvedIssueId = await resolveIssueId(ctx.sdk, issue); + + const needsContext = + options.status || + options.projectMilestone || + options.cycle || + (options.labels && options.labelMode === "add"); + const issueContext = needsContext + ? await getIssue(ctx.gql, resolvedIssueId) + : undefined; + + const input: IssueUpdateInput = {}; + + if (options.title) { + input.title = options.title; + } + + if (options.description) { + input.description = options.description; + } - const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), - ]); - const issuesService = new GraphQLIssuesService( - graphQLService, - linearService, + if (options.status) { + const teamId = + issueContext && "team" in issueContext && issueContext.team + ? issueContext.team.id + : undefined; + input.stateId = await resolveStatusId( + ctx.sdk, + options.status, + teamId, ); + } - // Prepare update arguments for GraphQL service - let labelIds: string[] | undefined; - if (options.clearLabels) { - labelIds = []; - } else if (options.labels) { - const labelNames = options.labels.split(",").map((l: string) => - l.trim() - ); - labelIds = labelNames; + if (options.priority) { + input.priority = parseInt(options.priority, 10); + } + + if (options.assignee) { + input.assigneeId = await resolveUserId(ctx.sdk, options.assignee); + } + + if (options.project) { + input.projectId = await resolveProjectId(ctx.sdk, options.project); + } + + if (options.clearLabels) { + input.labelIds = []; + } else if (options.labels) { + const labelNames = options.labels.split(",").map((l) => l.trim()); + const labelIds = await resolveLabelIds(ctx.sdk, labelNames); + + if (options.labelMode === "add") { + const currentLabels = + issueContext && + "labels" in issueContext && + issueContext.labels?.nodes + ? issueContext.labels.nodes.map((l) => l.id) + : []; + input.labelIds = [...new Set([...currentLabels, ...labelIds])]; + } else { + input.labelIds = labelIds; } + } + + if (options.clearParentTicket) { + input.parentId = null; + } else if (options.parentTicket) { + input.parentId = await resolveIssueId(ctx.sdk, options.parentTicket); + } - const updateArgs = { - id: issueId, // GraphQL service handles ID resolution - title: options.title, - description: options.description, - statusId: options.status, - priority: options.priority ? parseInt(options.priority) : undefined, - assigneeId: options.assignee, - projectId: options.project, // GraphQL service handles project resolution - labelIds, - parentId: options.parentTicket || - (options.clearParentTicket ? null : undefined), - milestoneId: options.projectMilestone || - (options.clearProjectMilestone ? null : undefined), - cycleId: options.cycle || (options.clearCycle ? null : undefined), - }; - - const labelMode = options.labelBy || "adding"; - const result = await issuesService.updateIssue( - updateArgs, - labelMode as "adding" | "overwriting", + if (options.clearProjectMilestone) { + input.projectMilestoneId = null; + } else if (options.projectMilestone) { + const projectName = + issueContext && + "project" in issueContext && + issueContext.project?.name + ? issueContext.project.name + : undefined; + input.projectMilestoneId = await resolveMilestoneId( + ctx.gql, + ctx.sdk, + options.projectMilestone, + projectName, ); - outputSuccess(result); - }, - ), + } + + if (options.clearCycle) { + input.cycleId = null; + } else if (options.cycle) { + const teamKey = + issueContext && "team" in issueContext && issueContext.team?.key + ? issueContext.team.key + : undefined; + input.cycleId = await resolveCycleId(ctx.sdk, options.cycle, teamKey); + } + + const relationTargetId = await resolveRelationTarget(ctx, options); + + const result = await updateIssue(ctx.gql, resolvedIssueId, input); + + if (relationTargetId) { + await applyRelation(ctx, resolvedIssueId, relationTargetId, options); + } + + outputSuccess(result); + }), ); + + issues + .command("usage") + .description("show detailed usage for issues") + .action(() => { + console.log(formatDomainUsage(issues, ISSUES_META)); + }); } diff --git a/src/commands/labels.ts b/src/commands/labels.ts index b41dbdf..eaffce6 100644 --- a/src/commands/labels.ts +++ b/src/commands/labels.ts @@ -1,48 +1,59 @@ -import { Command } from "commander"; -import { createLinearService } from "../utils/linear-service.js"; -import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; - -/** - * Setup labels commands on the program - * - * Registers `labels` command group for listing and managing Linear issue labels. - * Provides filtering capabilities by team and comprehensive label information. - * - * @param program - Commander.js program instance to register commands on - * - * @example - * ```typescript - * // In main.ts - * setupLabelsCommands(program); - * // Enables: linearis labels list [--team <team>] - * ``` - */ +import type { Command } from "commander"; +import { type CommandOptions, createContext } from "../common/context.js"; +import { handleCommand, outputSuccess, parseLimit } from "../common/output.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import { resolveTeamId } from "../resolvers/team-resolver.js"; +import { listLabels } from "../services/label-service.js"; + +interface ListLabelsOptions extends CommandOptions { + team?: string; + limit: string; + after?: string; +} + +export const LABELS_META: DomainMeta = { + name: "labels", + summary: "categorization tags, workspace-wide or team-scoped", + context: [ + "labels categorize issues. they can exist at workspace level or be", + "scoped to a specific team. use with issues create/update --labels.", + ].join("\n"), + arguments: {}, + seeAlso: ["issues create --labels", "issues update --labels"], +}; + export function setupLabelsCommands(program: Command): void { - const labels = program.command("labels") - .description("Label operations"); - - // Show labels help when no subcommand - labels.action(() => { - labels.help(); - }); - - /** - * List all available labels - * - * Command: `linearis labels list [--team <team>]` - * - * Lists all workspace and team-specific labels with optional team filtering. - * Excludes group labels (containers) and includes parent relationships. - */ - labels.command("list") - .description("List all available labels") - .option("--team <team>", "filter by team key, name, or ID") - .action(handleAsyncCommand(async (options: any, command: Command) => { - // Initialize Linear service for label operations - const service = await createLinearService(command.parent!.parent!.opts()); - - // Fetch labels with optional team filtering - const result = await service.getLabels(options.team); - outputSuccess(result); - })); + const labels = program.command("labels").description("Label operations"); + + labels.action(() => labels.help()); + + labels + .command("list") + .description("list available labels") + .option("--team <team>", "filter by team (key, name, or UUID)") + .option("-l, --limit <n>", "max results", "50") + .option("--after <cursor>", "cursor for next page") + .action( + handleCommand(async (...args: unknown[]) => { + const [options, command] = args as [ListLabelsOptions, Command]; + const ctx = createContext(command.parent!.parent!.opts()); + + const teamId = options.team + ? await resolveTeamId(ctx.sdk, options.team) + : undefined; + + const result = await listLabels(ctx.gql, teamId, { + limit: parseLimit(options.limit), + after: options.after, + }); + outputSuccess(result); + }), + ); + + labels + .command("usage") + .description("show detailed usage for labels") + .action(() => { + console.log(formatDomainUsage(labels, LABELS_META)); + }); } diff --git a/src/commands/milestones.ts b/src/commands/milestones.ts new file mode 100644 index 0000000..efe8af4 --- /dev/null +++ b/src/commands/milestones.ts @@ -0,0 +1,208 @@ +import type { Command } from "commander"; +import { createContext } from "../common/context.js"; +import { handleCommand, outputSuccess, parseLimit } from "../common/output.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import type { ProjectMilestoneUpdateInput } from "../gql/graphql.js"; +import { resolveMilestoneId } from "../resolvers/milestone-resolver.js"; +import { resolveProjectId } from "../resolvers/project-resolver.js"; +import { + createMilestone, + getMilestone, + listMilestones, + updateMilestone, +} from "../services/milestone-service.js"; + +// Option interfaces for commands +interface MilestoneListOptions { + project: string; + limit?: string; + after?: string; +} + +interface MilestoneReadOptions { + project?: string; + limit?: string; +} + +interface MilestoneCreateOptions { + project: string; + description?: string; + targetDate?: string; +} + +interface MilestoneUpdateOptions { + project?: string; + name?: string; + description?: string; + targetDate?: string; + sortOrder?: string; +} + +export const MILESTONES_META: DomainMeta = { + name: "milestones", + summary: "progress checkpoints within projects", + context: [ + "a milestone marks a phase or deadline within a project. milestones", + "can have target dates and contain issues assigned to them.", + ].join("\n"), + arguments: { + milestone: "milestone identifier (UUID or name)", + name: "string", + }, + seeAlso: [ + "issues create --project-milestone", + "issues update --project-milestone", + ], +}; + +export function setupMilestonesCommands(program: Command): void { + const milestones = program + .command("milestones") + .description("Project milestone operations"); + + milestones.action(() => milestones.help()); + + // List milestones in a project + milestones + .command("list") + .description("list milestones in a project") + .requiredOption("--project <project>", "target project (required)") + .option("-l, --limit <n>", "max results", "50") + .option("--after <cursor>", "cursor for next page") + .action( + handleCommand(async (...args: unknown[]) => { + const [options, command] = args as [MilestoneListOptions, Command]; + const ctx = createContext(command.parent!.parent!.opts()); + + // Resolve project ID + const projectId = await resolveProjectId(ctx.sdk, options.project); + + const milestones = await listMilestones(ctx.gql, projectId, { + limit: parseLimit(options.limit || "50"), + after: options.after, + }); + + outputSuccess(milestones); + }), + ); + + // Get milestone details with issues + milestones + .command("read <milestone>") + .description("get milestone details including issues") + .option("--project <project>", "scope name lookup to project") + .option("--limit <n>", "max issues to fetch", "50") + .action( + handleCommand(async (...args: unknown[]) => { + const [milestone, options, command] = args as [ + string, + MilestoneReadOptions, + Command, + ]; + const ctx = createContext(command.parent!.parent!.opts()); + + const milestoneId = await resolveMilestoneId( + ctx.gql, + ctx.sdk, + milestone, + options.project, + ); + + const milestoneResult = await getMilestone( + ctx.gql, + milestoneId, + parseLimit(options.limit || "50"), + ); + + outputSuccess(milestoneResult); + }), + ); + + // Create a new milestone + milestones + .command("create <name>") + .description("create a new milestone") + .requiredOption("--project <project>", "target project (required)") + .option("-d, --description <text>", "milestone description") + .option("--target-date <date>", "target date in ISO format (YYYY-MM-DD)") + .action( + handleCommand(async (...args: unknown[]) => { + const [name, options, command] = args as [ + string, + MilestoneCreateOptions, + Command, + ]; + const ctx = createContext(command.parent!.parent!.opts()); + + // Resolve project ID + const projectId = await resolveProjectId(ctx.sdk, options.project); + + const milestone = await createMilestone(ctx.gql, { + projectId, + name, + description: options.description, + targetDate: options.targetDate, + }); + + outputSuccess(milestone); + }), + ); + + // Update an existing milestone + milestones + .command("update <milestone>") + .description("update an existing milestone") + .option("--project <project>", "scope name lookup to project") + .option("-n, --name <name>", "new name") + .option("--description <text>", "new description") + .option( + "--target-date <date>", + "new target date in ISO format (YYYY-MM-DD)", + ) + .option("--sort-order <n>", "display order") + .action( + handleCommand(async (...args: unknown[]) => { + const [milestone, options, command] = args as [ + string, + MilestoneUpdateOptions, + Command, + ]; + const ctx = createContext(command.parent!.parent!.opts()); + + const milestoneId = await resolveMilestoneId( + ctx.gql, + ctx.sdk, + milestone, + options.project, + ); + + // Build update input (only include provided fields) + const updateInput: ProjectMilestoneUpdateInput = {}; + if (options.name !== undefined) updateInput.name = options.name; + if (options.description !== undefined) { + updateInput.description = options.description; + } + if (options.targetDate !== undefined) { + updateInput.targetDate = options.targetDate; + } + if (options.sortOrder !== undefined) { + updateInput.sortOrder = parseFloat(options.sortOrder); + } + + const updated = await updateMilestone( + ctx.gql, + milestoneId, + updateInput, + ); + + outputSuccess(updated); + }), + ); + + milestones + .command("usage") + .description("show detailed usage for milestones") + .action(() => { + console.log(formatDomainUsage(milestones, MILESTONES_META)); + }); +} diff --git a/src/commands/project-milestones.ts b/src/commands/project-milestones.ts deleted file mode 100644 index 3e0aab6..0000000 --- a/src/commands/project-milestones.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { Command } from "commander"; -import { createGraphQLService } from "../utils/graphql-service.js"; -import { createLinearService } from "../utils/linear-service.js"; -import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; -import { - CREATE_PROJECT_MILESTONE_MUTATION, - FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, - FIND_PROJECT_MILESTONE_BY_NAME_SCOPED, - GET_PROJECT_MILESTONE_BY_ID_QUERY, - LIST_PROJECT_MILESTONES_QUERY, - UPDATE_PROJECT_MILESTONE_MUTATION, -} from "../queries/project-milestones.js"; -import { isUuid } from "../utils/uuid.js"; -import type { - LinearProjectMilestone, - MilestoneCreateOptions, - MilestoneListOptions, - MilestoneReadOptions, - MilestoneUpdateOptions, -} from "../utils/linear-types.js"; -import type { GraphQLService } from "../utils/graphql-service.js"; -import { - multipleMatchesError, - notFoundError, -} from "../utils/error-messages.js"; - -// Helper function to resolve milestone ID from name -async function resolveMilestoneId( - milestoneNameOrId: string, - graphQLService: GraphQLService, - linearService: any, - projectNameOrId?: string, -): Promise<string> { - if (isUuid(milestoneNameOrId)) { - return milestoneNameOrId; - } - - let nodes: LinearProjectMilestone[] = []; - - if (projectNameOrId) { - // Resolve project ID using LinearService - const projectId = await linearService.resolveProjectId(projectNameOrId); - - // Scoped lookup - const findRes = await graphQLService.rawRequest( - FIND_PROJECT_MILESTONE_BY_NAME_SCOPED, - { - name: milestoneNameOrId, - projectId, - }, - ); - nodes = findRes.project?.projectMilestones?.nodes || []; - } - - // Fall back to global search if no project scope or not found - if (nodes.length === 0) { - const globalRes = await graphQLService.rawRequest( - FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, - { name: milestoneNameOrId }, - ); - nodes = globalRes.projectMilestones?.nodes || []; - } - - if (nodes.length === 0) { - throw notFoundError("Milestone", milestoneNameOrId); - } - - if (nodes.length > 1) { - const matches = nodes.map((m: LinearProjectMilestone) => - `"${m.name}" in project "${m.project?.name}"` - ); - throw multipleMatchesError( - "milestone", - milestoneNameOrId, - matches, - "specify --project or use the milestone ID", - ); - } - - return nodes[0].id; -} - -export function setupProjectMilestonesCommands(program: Command): void { - const projectMilestones = program - .command("project-milestones") - .description("Project milestone operations"); - - projectMilestones.action(() => projectMilestones.help()); - - // List milestones in a project - projectMilestones - .command("list") - .description("List milestones in a project") - .requiredOption("--project <project>", "project name or ID") - .option("-l, --limit <number>", "limit results", "50") - .action( - handleAsyncCommand( - async (options: MilestoneListOptions, command: Command) => { - const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), - ]); - - // Resolve project ID using LinearService - const projectId = await linearService.resolveProjectId( - options.project, - ); - - const result = await graphQLService.rawRequest( - LIST_PROJECT_MILESTONES_QUERY, - { - projectId, - first: parseInt(options.limit || "50"), - }, - ); - - outputSuccess(result.project?.projectMilestones?.nodes || []); - }, - ), - ); - - // Get milestone details with issues - projectMilestones - .command("read <milestoneIdOrName>") - .description( - "Get milestone details including issues. Accepts UUID or milestone name (optionally scoped by --project)", - ) - .option("--project <project>", "project name or ID to scope name lookup") - .option("--issues-first <n>", "how many issues to fetch (default 50)", "50") - .action( - handleAsyncCommand( - async ( - milestoneIdOrName: string, - options: MilestoneReadOptions, - command: Command, - ) => { - const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), - ]); - - const milestoneId = await resolveMilestoneId( - milestoneIdOrName, - graphQLService, - linearService, - options.project, - ); - - const result = await graphQLService.rawRequest( - GET_PROJECT_MILESTONE_BY_ID_QUERY, - { - id: milestoneId, - issuesFirst: parseInt(options.issuesFirst || "50"), - }, - ); - - outputSuccess(result.projectMilestone); - }, - ), - ); - - // Create a new milestone - projectMilestones - .command("create <name>") - .description("Create a new project milestone") - .requiredOption("--project <project>", "project name or ID") - .option("-d, --description <description>", "milestone description") - .option("--target-date <date>", "target date in ISO format (YYYY-MM-DD)") - .action( - handleAsyncCommand( - async ( - name: string, - options: MilestoneCreateOptions, - command: Command, - ) => { - const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), - ]); - - // Resolve project ID using LinearService - const projectId = await linearService.resolveProjectId( - options.project, - ); - - const result = await graphQLService.rawRequest( - CREATE_PROJECT_MILESTONE_MUTATION, - { - projectId, - name, - description: options.description, - targetDate: options.targetDate, - }, - ); - - if (!result.projectMilestoneCreate?.success) { - throw new Error("Failed to create project milestone"); - } - - outputSuccess(result.projectMilestoneCreate.projectMilestone); - }, - ), - ); - - // Update an existing milestone - projectMilestones - .command("update <milestoneIdOrName>") - .description( - "Update an existing project milestone. Accepts UUID or milestone name (optionally scoped by --project)", - ) - .option("--project <project>", "project name or ID to scope name lookup") - .option("-n, --name <name>", "new milestone name") - .option("-d, --description <description>", "new milestone description") - .option( - "--target-date <date>", - "new target date in ISO format (YYYY-MM-DD)", - ) - .option("--sort-order <number>", "new sort order") - .action( - handleAsyncCommand( - async ( - milestoneIdOrName: string, - options: MilestoneUpdateOptions, - command: Command, - ) => { - const [graphQLService, linearService] = await Promise.all([ - createGraphQLService(command.parent!.parent!.opts()), - createLinearService(command.parent!.parent!.opts()), - ]); - - const milestoneId = await resolveMilestoneId( - milestoneIdOrName, - graphQLService, - linearService, - options.project, - ); - - // Build update input (only include provided fields) - const updateVars: Partial<LinearProjectMilestone> & { id: string } = { - id: milestoneId, - }; - if (options.name !== undefined) updateVars.name = options.name; - if (options.description !== undefined) { - updateVars.description = options.description; - } - if (options.targetDate !== undefined) { - updateVars.targetDate = options.targetDate; - } - if (options.sortOrder !== undefined) { - updateVars.sortOrder = parseFloat(options.sortOrder); - } - - const result = await graphQLService.rawRequest( - UPDATE_PROJECT_MILESTONE_MUTATION, - updateVars, - ); - - if (!result.projectMilestoneUpdate?.success) { - throw new Error("Failed to update project milestone"); - } - - outputSuccess(result.projectMilestoneUpdate.projectMilestone); - }, - ), - ); -} diff --git a/src/commands/projects.ts b/src/commands/projects.ts index c82b0f3..2ad8bbb 100644 --- a/src/commands/projects.ts +++ b/src/commands/projects.ts @@ -1,53 +1,51 @@ -import { Command } from "commander"; -import { createLinearService } from "../utils/linear-service.js"; -import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; +import type { Command } from "commander"; +import { createContext } from "../common/context.js"; +import { handleCommand, outputSuccess, parseLimit } from "../common/output.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import { listProjects } from "../services/project-service.js"; + +export const PROJECTS_META: DomainMeta = { + name: "projects", + summary: "groups of issues toward a goal", + context: [ + "a project collects related issues across teams. projects can have", + "milestones to track progress toward deadlines or phases.", + ].join("\n"), + arguments: {}, + seeAlso: ["milestones list --project", "documents list --project"], +}; -/** - * Setup projects commands on the program - * - * Registers `projects` command group for Linear project management. - * Provides listing functionality with comprehensive project information - * including teams, progress, and leadership details. - * - * @param program - Commander.js program instance to register commands on - * - * @example - * ```typescript - * // In main.ts - * setupProjectsCommands(program); - * // Enables: linearis projects list [--limit <number>] - * ``` - */ export function setupProjectsCommands(program: Command): void { - const projects = program.command("projects") + const projects = program + .command("projects") .description("Project operations"); - // Show projects help when no subcommand - projects.action(() => { - projects.help(); - }); + projects.action(() => projects.help()); + + projects + .command("list") + .description("list projects") + .option("-l, --limit <n>", "max results", "100") + .option("--after <cursor>", "cursor for next page") + .action( + handleCommand(async (...args: unknown[]) => { + const [options, command] = args as [ + { limit: string; after?: string }, + Command, + ]; + const ctx = createContext(command.parent!.parent!.opts()); + const result = await listProjects(ctx.gql, { + limit: parseLimit(options.limit), + after: options.after, + }); + outputSuccess(result); + }), + ); - /** - * List projects - * - * Command: `linearis projects list [--limit <number>]` - * - * Lists all projects with their teams, leads, and progress information. - * Note: Linear SDK doesn't implement pagination, so all projects are shown. - */ - projects.command("list") - .description("List projects") - .option( - "-l, --limit <number>", - "limit results (not implemented by Linear SDK, showing all)", - "100", - ) - .action(handleAsyncCommand(async (_options: any, command: Command) => { - // Initialize Linear service for project operations - const service = await createLinearService(command.parent!.parent!.opts()); - - // Fetch all projects with their relationships - const result = await service.getProjects(); - outputSuccess(result); - })); + projects + .command("usage") + .description("show detailed usage for projects") + .action(() => { + console.log(formatDomainUsage(projects, PROJECTS_META)); + }); } diff --git a/src/commands/teams.ts b/src/commands/teams.ts index 64f2ddf..70b9d83 100644 --- a/src/commands/teams.ts +++ b/src/commands/teams.ts @@ -1,50 +1,49 @@ -import { Command } from "commander"; -import { createLinearService } from "../utils/linear-service.js"; -import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; +import type { Command } from "commander"; +import { createContext } from "../common/context.js"; +import { handleCommand, outputSuccess, parseLimit } from "../common/output.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import { listTeams } from "../services/team-service.js"; + +export const TEAMS_META: DomainMeta = { + name: "teams", + summary: "organizational units owning issues and cycles", + context: [ + "a team is a group of users that owns issues, cycles, statuses, and", + "labels. teams are identified by a short key (e.g. ENG), name, or UUID.", + ].join("\n"), + arguments: {}, + seeAlso: [], +}; -/** - * Setup teams commands on the program - * - * Registers `teams` command group for listing Linear teams. - * Provides team information including key, name, and description. - * - * @param program - Commander.js program instance to register commands on - * - * @example - * ```typescript - * // In main.ts - * setupTeamsCommands(program); - * // Enables: linearis teams list - * ``` - */ export function setupTeamsCommands(program: Command): void { - const teams = program - .command("teams") - .description("Team operations"); + const teams = program.command("teams").description("Team operations"); - // Show teams help when no subcommand - teams.action(() => { - teams.help(); - }); + teams.action(() => teams.help()); - /** - * List all teams - * - * Command: `linearis teams list` - * - * Lists all teams in the workspace with their key, name, and description. - */ teams .command("list") - .description("List all teams") + .description("list all teams") + .option("-l, --limit <n>", "max results", "50") + .option("--after <cursor>", "cursor for next page") .action( - handleAsyncCommand(async (options: any, command: Command) => { - // Initialize Linear service for team operations - const service = await createLinearService(command.parent!.parent!.opts()); - - // Fetch all teams from the workspace - const result = await service.getTeams(); + handleCommand(async (...args: unknown[]) => { + const [options, command] = args as [ + { limit: string; after?: string }, + Command, + ]; + const ctx = createContext(command.parent!.parent!.opts()); + const result = await listTeams(ctx.gql, { + limit: parseLimit(options.limit), + after: options.after, + }); outputSuccess(result); - }) + }), ); + + teams + .command("usage") + .description("show detailed usage for teams") + .action(() => { + console.log(formatDomainUsage(teams, TEAMS_META)); + }); } diff --git a/src/commands/users.ts b/src/commands/users.ts index f6d1efa..59df6c0 100644 --- a/src/commands/users.ts +++ b/src/commands/users.ts @@ -1,52 +1,53 @@ -import { Command } from "commander"; -import { createLinearService } from "../utils/linear-service.js"; -import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; +import type { Command } from "commander"; +import { type CommandOptions, createContext } from "../common/context.js"; +import { handleCommand, outputSuccess, parseLimit } from "../common/output.js"; +import { type DomainMeta, formatDomainUsage } from "../common/usage.js"; +import { listUsers } from "../services/user-service.js"; + +interface ListUsersOptions extends CommandOptions { + active?: boolean; + limit: string; + after?: string; +} + +export const USERS_META: DomainMeta = { + name: "users", + summary: "workspace members and assignees", + context: [ + "a user is a member of the Linear workspace. users can be assigned to", + "issues and belong to teams.", + ].join("\n"), + arguments: {}, + seeAlso: [], +}; -/** - * Setup users commands on the program - * - * Registers `users` command group for listing Linear users. - * Provides user information including id, name, displayName, email, and active status. - * - * @param program - Commander.js program instance to register commands on - * - * @example - * ```typescript - * // In main.ts - * setupUsersCommands(program); - * // Enables: linearis users list - * ``` - */ export function setupUsersCommands(program: Command): void { - const users = program - .command("users") - .description("User operations"); + const users = program.command("users").description("User operations"); - // Show users help when no subcommand - users.action(() => { - users.help(); - }); + users.action(() => users.help()); - /** - * List all users - * - * Command: `linearis users list` - * - * Lists all users in the workspace with their id, name, displayName, email, and active status. - * Can filter to show only active users with --active flag. - */ users .command("list") - .description("List all users") - .option("--active", "Only show active users") + .description("list workspace members") + .option("--active", "only show active users") + .option("-l, --limit <n>", "max results", "50") + .option("--after <cursor>", "cursor for next page") .action( - handleAsyncCommand(async (options: any, command: Command) => { - // Initialize Linear service for user operations - const service = await createLinearService(command.parent!.parent!.opts()); - - // Fetch all users from the workspace - const result = await service.getUsers(options.active); + handleCommand(async (...args: unknown[]) => { + const [options, command] = args as [ListUsersOptions, Command]; + const ctx = createContext(command.parent!.parent!.opts()); + const result = await listUsers(ctx.gql, options.active || false, { + limit: parseLimit(options.limit), + after: options.after, + }); outputSuccess(result); - }) + }), ); + + users + .command("usage") + .description("show detailed usage for users") + .action(() => { + console.log(formatDomainUsage(users, USERS_META)); + }); } diff --git a/src/common/auth.ts b/src/common/auth.ts new file mode 100644 index 0000000..86c94c0 --- /dev/null +++ b/src/common/auth.ts @@ -0,0 +1,55 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { getStoredToken } from "./token-storage.js"; + +export interface CommandOptions { + apiToken?: string; +} + +export type TokenSource = "flag" | "env" | "stored" | "legacy"; + +export interface ResolvedToken { + token: string; + source: TokenSource; +} + +/** @throws Error if no token found in any source */ +export function resolveApiToken(options: CommandOptions): ResolvedToken { + // 1. CLI flag + if (options.apiToken) { + return { token: options.apiToken, source: "flag" }; + } + + // 2. Environment variable + if (process.env.LINEAR_API_TOKEN) { + return { token: process.env.LINEAR_API_TOKEN, source: "env" }; + } + + // 3. Encrypted stored token (~/.linearis/token) + const storedToken = getStoredToken(); + if (storedToken) { + return { token: storedToken, source: "stored" }; + } + + // 4. Legacy plaintext file (~/.linear_api_token) — deprecated + const legacyFile = path.join(os.homedir(), ".linear_api_token"); + if (fs.existsSync(legacyFile)) { + console.error( + "Warning: ~/.linear_api_token is deprecated. Run 'linearis auth' to migrate.", + ); + return { + token: fs.readFileSync(legacyFile, "utf8").trim(), + source: "legacy", + }; + } + + throw new Error( + "No API token found. Run 'linearis auth' to set up authentication.", + ); +} + +export function getApiToken(options: CommandOptions): string { + const { token } = resolveApiToken(options); + return token; +} diff --git a/src/common/context.ts b/src/common/context.ts new file mode 100644 index 0000000..82a93ce --- /dev/null +++ b/src/common/context.ts @@ -0,0 +1,22 @@ +import { GraphQLClient } from "../client/graphql-client.js"; +import { LinearSdkClient } from "../client/linear-client.js"; +import { type CommandOptions, getApiToken } from "./auth.js"; + +export type { CommandOptions }; + +export interface CommandContext { + gql: GraphQLClient; + sdk: LinearSdkClient; +} + +export function createContext(options: CommandOptions): CommandContext { + const token = getApiToken(options); + return { + gql: new GraphQLClient(token), + sdk: new LinearSdkClient(token), + }; +} + +export function createGraphQLClient(token: string): GraphQLClient { + return new GraphQLClient(token); +} diff --git a/src/common/embed-parser.ts b/src/common/embed-parser.ts new file mode 100644 index 0000000..e94a9fe --- /dev/null +++ b/src/common/embed-parser.ts @@ -0,0 +1,74 @@ +export interface EmbedInfo { + label: string; + url: string; + /** ISO timestamp when the signed URL expires (1 hour from generation) */ + expiresAt: string; +} + +/** Removes code blocks and inline code to avoid extracting URLs from code examples. */ +function stripCodeContexts(content: string): string { + // Remove escaped backticks + let cleaned = content.replace(/\\`/g, ""); + + // Remove fenced code blocks (```...```) - greedy match with dotall behavior + cleaned = cleaned.replace(/```[\s\S]*?```/g, ""); + + // Remove inline code (`...`) + cleaned = cleaned.replace(/`[^`]+`/g, ""); + + return cleaned; +} + +/** Extracts Linear upload URLs from markdown image and link syntax. */ +export function extractEmbeds(content: string): EmbedInfo[] { + if (!content) { + return []; + } + + // Strip code contexts to avoid extracting URLs from code examples + const cleanedContent = stripCodeContexts(content); + + const embeds: EmbedInfo[] = []; + const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString(); + + // Match both image ![label](url) and link [label](url) syntax + const patterns = [ + /!\[([^\]]*)\]\(([^)]+)\)/g, // images + /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, // links + ]; + + for (const regex of patterns) { + for (const match of cleanedContent.matchAll(regex)) { + const label = match[1] || "file"; + const url = match[2]; + + if (isLinearUploadUrl(url)) { + embeds.push({ label, url, expiresAt }); + } + } + } + + return embeds; +} + +export function isLinearUploadUrl(url: string): boolean { + if (!url) { + return false; + } + + try { + const urlObj = new URL(url); + return urlObj.hostname === "uploads.linear.app"; + } catch { + return false; + } +} + +export function extractFilenameFromUrl(url: string): string { + try { + const parts = new URL(url).pathname.split("/"); + return parts[parts.length - 1] || "download"; + } catch { + return "download"; + } +} diff --git a/src/common/encryption.ts b/src/common/encryption.ts new file mode 100644 index 0000000..5caf46c --- /dev/null +++ b/src/common/encryption.ts @@ -0,0 +1,64 @@ +import { + createCipheriv, + createDecipheriv, + createHash, + randomBytes, +} from "node:crypto"; + +const VERSION_PREFIX = "v1"; +const ALGORITHM = "aes-256-cbc"; + +// Hardcoded key material — provides obfuscation-level protection against +// accidental token exposure (browsing files, git commits). +// Does NOT protect against determined attackers with access to the binary. +const KEY_MATERIAL = "linearis-v1-token-encryption-key"; + +function deriveKey(): Buffer { + return createHash("sha256").update(KEY_MATERIAL).digest(); +} + +export function encryptToken(token: string): string { + const key = deriveKey(); + const iv = randomBytes(16); + const cipher = createCipheriv(ALGORITHM, key, iv); + const encrypted = Buffer.concat([ + cipher.update(token, "utf8"), + cipher.final(), + ]); + // Store as version:iv:ciphertext, all hex-encoded except version + return `${VERSION_PREFIX}:${iv.toString("hex")}:${encrypted.toString("hex")}`; +} + +export function decryptToken(encrypted: string): string { + const parts = encrypted.split(":"); + + // Support unversioned legacy format (iv:ciphertext) + if (parts.length === 2 && parts[0] && parts[1]) { + return decryptV1(parts[0], parts[1]); + } + + // Versioned format (version:iv:ciphertext) + if (parts.length === 3 && parts[0] && parts[1] && parts[2]) { + if (parts[0] !== VERSION_PREFIX) { + throw new Error(`Unsupported token encryption version: ${parts[0]}`); + } + return decryptV1(parts[1], parts[2]); + } + + throw new Error("Invalid encrypted token format"); +} + +function decryptV1(ivHex: string, ciphertextHex: string): string { + const key = deriveKey(); + const iv = Buffer.from(ivHex, "hex"); + if (iv.length !== 16) { + throw new Error("Invalid encrypted token: corrupted IV"); + } + const ciphertext = Buffer.from(ciphertextHex, "hex"); + const decipher = createDecipheriv(ALGORITHM, key, iv); + const decrypted = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), + ]); + return decrypted.toString("utf8"); +} diff --git a/src/utils/error-messages.ts b/src/common/errors.ts similarity index 55% rename from src/utils/error-messages.ts rename to src/common/errors.ts index 28bcf4a..0cd9743 100644 --- a/src/utils/error-messages.ts +++ b/src/common/errors.ts @@ -1,7 +1,3 @@ -/** - * Standard error message formatters - */ - export function notFoundError( entityType: string, identifier: string, @@ -38,3 +34,29 @@ export function requiresParameterError( ): Error { return new Error(`${flag} requires ${requiredFlag} to be specified`); } + +export const AUTH_ERROR_CODE = 42; + +export class AuthenticationError extends Error { + readonly details: string; + + constructor(details?: string) { + super("Linear API authentication failed."); + this.name = "AuthenticationError"; + this.details = details ?? "Your stored token is invalid or expired."; + } +} + +const AUTH_ERROR_PATTERNS: ReadonlyArray<string> = [ + "authentication required", + "unauthorized", +]; + +export function isAuthError(error: unknown): boolean { + if (error instanceof AuthenticationError) return true; + if (error instanceof Error) { + const msg = error.message.toLowerCase().trim(); + return AUTH_ERROR_PATTERNS.some((pattern) => msg === pattern); + } + return false; +} diff --git a/src/common/identifier.ts b/src/common/identifier.ts new file mode 100644 index 0000000..e2614df --- /dev/null +++ b/src/common/identifier.ts @@ -0,0 +1,41 @@ +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export function isUuid(value: string): boolean { + return UUID_REGEX.test(value); +} + +export interface IssueIdentifier { + teamKey: string; + issueNumber: number; +} + +/** @throws Error if identifier format is invalid */ +export function parseIssueIdentifier(identifier: string): IssueIdentifier { + const parts = identifier.split("-"); + + if (parts.length !== 2) { + throw new Error( + `Invalid issue identifier format: "${identifier}". Expected format: TEAM-123`, + ); + } + + const teamKey = parts[0]; + const issueNumber = parseInt(parts[1], 10); + + if (Number.isNaN(issueNumber)) { + throw new Error(`Invalid issue number in identifier: "${identifier}"`); + } + + return { teamKey, issueNumber }; +} + +export function tryParseIssueIdentifier( + identifier: string, +): IssueIdentifier | null { + try { + return parseIssueIdentifier(identifier); + } catch { + return null; + } +} diff --git a/src/common/output.ts b/src/common/output.ts new file mode 100644 index 0000000..3bb42ea --- /dev/null +++ b/src/common/output.ts @@ -0,0 +1,57 @@ +import { + AUTH_ERROR_CODE, + AuthenticationError, + invalidParameterError, +} from "./errors.js"; + +export function outputSuccess(data: unknown): void { + console.log(JSON.stringify(data, null, 2)); +} + +export function outputError(error: Error): void { + console.error(JSON.stringify({ error: error.message }, null, 2)); + process.exit(1); +} + +export function outputAuthError(error: AuthenticationError): void { + console.error( + JSON.stringify( + { + error: "AUTHENTICATION_REQUIRED", + message: error.message, + details: error.details, + action: "USER_ACTION_REQUIRED", + instruction: + "Run 'linearis auth' to set up or refresh your authentication token.", + exit_code: AUTH_ERROR_CODE, + }, + null, + 2, + ), + ); + process.exit(AUTH_ERROR_CODE); +} + +export function parseLimit(value: string): number { + const limit = parseInt(value, 10); + if (Number.isNaN(limit) || limit < 1) { + throw invalidParameterError("--limit", "must be a positive integer"); + } + return limit; +} + +export function handleCommand( + asyncFn: (...args: unknown[]) => Promise<void>, +): (...args: unknown[]) => Promise<void> { + return async (...args: unknown[]) => { + try { + await asyncFn(...args); + } catch (error) { + if (error instanceof AuthenticationError) { + outputAuthError(error); + return; + } + outputError(error instanceof Error ? error : new Error(String(error))); + } + }; +} diff --git a/src/common/token-storage.ts b/src/common/token-storage.ts new file mode 100644 index 0000000..17f78cc --- /dev/null +++ b/src/common/token-storage.ts @@ -0,0 +1,52 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { decryptToken, encryptToken } from "./encryption.js"; + +const DIR_NAME = ".linearis"; +const TOKEN_FILE = "token"; + +export function getTokenDir(): string { + return path.join(os.homedir(), DIR_NAME); +} + +function getTokenPath(): string { + return path.join(getTokenDir(), TOKEN_FILE); +} + +export function ensureTokenDir(): void { + const dir = getTokenDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); + } else { + fs.chmodSync(dir, 0o700); + } +} + +export function saveToken(token: string): void { + ensureTokenDir(); + const tokenPath = getTokenPath(); + const encrypted = encryptToken(token); + fs.writeFileSync(tokenPath, encrypted, "utf8"); + fs.chmodSync(tokenPath, 0o600); +} + +export function getStoredToken(): string | null { + const tokenPath = getTokenPath(); + if (!fs.existsSync(tokenPath)) { + return null; + } + try { + const encrypted = fs.readFileSync(tokenPath, "utf8").trim(); + return decryptToken(encrypted); + } catch { + return null; + } +} + +export function clearToken(): void { + const tokenPath = getTokenPath(); + if (fs.existsSync(tokenPath)) { + fs.unlinkSync(tokenPath); + } +} diff --git a/src/common/types.ts b/src/common/types.ts new file mode 100644 index 0000000..49aa4f8 --- /dev/null +++ b/src/common/types.ts @@ -0,0 +1,79 @@ +import type { + AttachmentCreateMutation, + CreateIssueMutation, + CreateIssueRelationMutation, + CreateProjectMilestoneMutation, + DocumentCreateMutation, + DocumentUpdateMutation, + GetDocumentQuery, + GetIssueByIdentifierQuery, + GetIssueByIdQuery, + GetIssuesQuery, + GetProjectMilestoneByIdQuery, + GetViewerQuery, + ListAttachmentsQuery, + ListDocumentsQuery, + ListProjectMilestonesQuery, + SearchIssuesQuery, + UpdateIssueMutation, + UpdateProjectMilestoneMutation, +} from "../gql/graphql.js"; + +// Pagination types +export type PageInfo = GetIssuesQuery["issues"]["pageInfo"]; + +export interface PaginatedResult<T> { + nodes: T[]; + pageInfo: PageInfo; +} + +export interface PaginationOptions { + limit?: number; + after?: string; +} + +// Issue types +export type Issue = GetIssuesQuery["issues"]["nodes"][0]; +export type IssueDetail = NonNullable<GetIssueByIdQuery["issue"]>; +export type IssueByIdentifier = GetIssueByIdentifierQuery["issues"]["nodes"][0]; +export type IssueSearchResult = SearchIssuesQuery["searchIssues"]["nodes"][0]; +export type CreatedIssue = NonNullable< + CreateIssueMutation["issueCreate"]["issue"] +>; +export type UpdatedIssue = NonNullable< + UpdateIssueMutation["issueUpdate"]["issue"] +>; + +// Issue relation types +export type CreatedIssueRelation = + CreateIssueRelationMutation["issueRelationCreate"]["issueRelation"]; + +// Document types +export type Document = NonNullable<GetDocumentQuery["document"]>; +export type DocumentListItem = ListDocumentsQuery["documents"]["nodes"][0]; +export type CreatedDocument = + DocumentCreateMutation["documentCreate"]["document"]; +export type UpdatedDocument = + DocumentUpdateMutation["documentUpdate"]["document"]; + +// Attachment types +export type Attachment = + ListAttachmentsQuery["issue"]["attachments"]["nodes"][0]; +export type CreatedAttachment = + AttachmentCreateMutation["attachmentCreate"]["attachment"]; + +// Milestone types +export type MilestoneDetail = NonNullable< + GetProjectMilestoneByIdQuery["projectMilestone"] +>; +export type MilestoneListItem = + ListProjectMilestonesQuery["project"]["projectMilestones"]["nodes"][0]; +export type CreatedMilestone = NonNullable< + CreateProjectMilestoneMutation["projectMilestoneCreate"]["projectMilestone"] +>; +export type UpdatedMilestone = NonNullable< + UpdateProjectMilestoneMutation["projectMilestoneUpdate"]["projectMilestone"] +>; + +// Viewer types +export type Viewer = GetViewerQuery["viewer"]; diff --git a/src/common/usage.ts b/src/common/usage.ts new file mode 100644 index 0000000..902fa43 --- /dev/null +++ b/src/common/usage.ts @@ -0,0 +1,111 @@ +import type { Command } from "commander"; + +export interface DomainMeta { + name: string; + summary: string; + context: string; + arguments: Record<string, string>; + seeAlso: string[]; +} + +export function formatOverview(version: string, metas: DomainMeta[]): string { + const lines: string[] = []; + lines.push( + `linearis v${version} — CLI for Linear.app (project management / issue tracking)`, + ); + lines.push( + "auth: linearis auth login | --api-token <token> | LINEAR_API_TOKEN | ~/.linearis/token", + ); + lines.push("output: JSON"); + lines.push("ids: UUID or human-readable (team key, issue ABC-123, name)"); + lines.push(""); + lines.push("domains:"); + for (const meta of metas) { + lines.push(` ${meta.name.padEnd(14)}${meta.summary}`); + } + lines.push(""); + lines.push("detail: linearis <domain> usage"); + return lines.join("\n"); +} + +function extractLongFlag(flags: string): string { + const parts = flags.split(",").map((s) => s.trim()); + const longPart = parts.find((p) => p.startsWith("--")); + return longPart || flags; +} + +function formatCommandSignature(cmd: Command): string { + const args = cmd.registeredArguments; + const parts: string[] = [cmd.name()]; + + if (args.length > 0) { + for (const arg of args) { + parts.push(arg.required ? `<${arg.name()}>` : `[${arg.name()}]`); + } + } else if (cmd.options.length > 0) { + parts.push("[options]"); + } + + return parts.join(" "); +} + +export function formatDomainUsage(command: Command, meta: DomainMeta): string { + const lines: string[] = []; + + lines.push(`linearis ${meta.name} — ${meta.summary}`); + lines.push(""); + lines.push(meta.context); + lines.push(""); + + const subcommands = command.commands.filter((c) => c.name() !== "usage"); + lines.push("commands:"); + + const signatures = subcommands.map((c) => formatCommandSignature(c)); + const maxSigLen = Math.max(...signatures.map((s) => s.length)); + + for (let i = 0; i < subcommands.length; i++) { + const sig = signatures[i]; + const desc = subcommands[i].description(); + lines.push(` ${sig.padEnd(maxSigLen + 2)}${desc}`); + } + + const argEntries = Object.entries(meta.arguments); + if (argEntries.length > 0) { + lines.push(""); + lines.push("arguments:"); + const maxArgLen = Math.max( + ...argEntries.map(([name]) => `<${name}>`.length), + ); + for (const [name, desc] of argEntries) { + lines.push(` ${`<${name}>`.padEnd(maxArgLen + 2)}${desc}`); + } + } + + for (const cmd of subcommands) { + const opts = cmd.options.filter((o) => !o.hidden); + if (opts.length === 0) continue; + + lines.push(""); + lines.push(`${cmd.name()} options:`); + + const flags = opts.map((o) => extractLongFlag(o.flags)); + const maxFlagLen = Math.max(...flags.map((f) => f.length)); + + for (let j = 0; j < opts.length; j++) { + const flag = flags[j]; + let desc = opts[j].description; + const defaultVal = opts[j].defaultValue; + if (defaultVal !== undefined && defaultVal !== false) { + desc += ` (default: ${defaultVal})`; + } + lines.push(` ${flag.padEnd(maxFlagLen + 2)}${desc}`); + } + } + + if (meta.seeAlso.length > 0) { + lines.push(""); + lines.push(`see also: ${meta.seeAlso.join(", ")}`); + } + + return lines.join("\n"); +} diff --git a/src/main.ts b/src/main.ts index 538dbdc..a5b355c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,61 +1,83 @@ #!/usr/bin/env node -/** - * Linearis CLI - A command-line tool for Linear.app with structured JSON output - * - * This tool provides optimized GraphQL operations for Linear API interactions, - * smart ID resolution (UUID and TEAM-123 formats), and comprehensive - * entity management capabilities. - * - * Key features: - * - Single-query GraphQL operations with batch resolving - * - Human-friendly ID resolution (TEAM-123 → UUID) - * - Structured JSON output for LLM consumption - * - Complete API coverage with optimized queries - */ - -import { program } from "commander"; +import { Option, program } from "commander"; import pkg from "../package.json" with { type: "json" }; -import { setupCommentsCommands } from "./commands/comments.js"; -import { setupEmbedsCommands } from "./commands/embeds.js"; -import { setupIssuesCommands } from "./commands/issues.js"; -import { setupLabelsCommands } from "./commands/labels.js"; -import { setupProjectsCommands } from "./commands/projects.js"; -import { setupCyclesCommands } from "./commands/cycles.js"; -import { setupProjectMilestonesCommands } from "./commands/project-milestones.js"; -import { setupTeamsCommands } from "./commands/teams.js"; -import { setupUsersCommands } from "./commands/users.js"; -import { setupDocumentsCommands } from "./commands/documents.js"; -import { outputUsageInfo } from "./utils/usage.js"; +import { AUTH_META, setupAuthCommands } from "./commands/auth.js"; +import { COMMENTS_META, setupCommentsCommands } from "./commands/comments.js"; +import { CYCLES_META, setupCyclesCommands } from "./commands/cycles.js"; +import { + DOCUMENTS_META, + setupDocumentsCommands, +} from "./commands/documents.js"; +import { FILES_META, setupFilesCommands } from "./commands/files.js"; +import { ISSUES_META, setupIssuesCommands } from "./commands/issues.js"; +import { LABELS_META, setupLabelsCommands } from "./commands/labels.js"; +import { + MILESTONES_META, + setupMilestonesCommands, +} from "./commands/milestones.js"; +import { PROJECTS_META, setupProjectsCommands } from "./commands/projects.js"; +import { setupTeamsCommands, TEAMS_META } from "./commands/teams.js"; +import { setupUsersCommands, USERS_META } from "./commands/users.js"; +import { + type DomainMeta, + formatDomainUsage, + formatOverview, +} from "./common/usage.js"; -// Setup main program program .name("linearis") .description("CLI for Linear.app with JSON output") .version(pkg.version) .option("--api-token <token>", "Linear API token"); -// Default action - show help when no subcommand -program.action(() => { - program.help(); -}); +const allMetas: DomainMeta[] = [ + AUTH_META, + ISSUES_META, + COMMENTS_META, + LABELS_META, + PROJECTS_META, + CYCLES_META, + MILESTONES_META, + DOCUMENTS_META, + FILES_META, + TEAMS_META, + USERS_META, +]; + +program.action(() => console.log(formatOverview(pkg.version, allMetas))); -// Setup all subcommand groups +setupAuthCommands(program); setupIssuesCommands(program); setupCommentsCommands(program); setupLabelsCommands(program); setupProjectsCommands(program); setupCyclesCommands(program); -setupProjectMilestonesCommands(program); -setupEmbedsCommands(program); +setupMilestonesCommands(program); +setupFilesCommands(program); setupTeamsCommands(program); setupUsersCommands(program); setupDocumentsCommands(program); -// Add usage command -program.command("usage") - .description("show usage info for *all* tools") - .action(() => outputUsageInfo(program)); +program + .command("usage") + .description("show overview of all domains") + .addOption( + new Option("--all", "output all domain usages concatenated") + .default(false) + .hideHelp(), + ) + .action((options: { all: boolean }) => { + console.log(formatOverview(pkg.version, allMetas)); + if (options.all) { + for (const meta of allMetas) { + console.log("\n---\n"); + const cmd = program.commands.find((c) => c.name() === meta.name); + if (cmd) { + console.log(formatDomainUsage(cmd, meta)); + } + } + } + }); -// Parse command line arguments program.parse(); diff --git a/src/queries/attachments.ts b/src/queries/attachments.ts deleted file mode 100644 index 206a6c1..0000000 --- a/src/queries/attachments.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * GraphQL queries and mutations for Linear attachments - * - * Attachments allow linking any URL to an issue. This is the mechanism - * to associate documents (or any external resource) with issues, since - * documents cannot be directly linked to issues in Linear's data model. - * - * Key behavior: Attachments are idempotent - creating an attachment with - * the same url + issueId will update the existing attachment. - */ - -export const ATTACHMENT_FRAGMENT = ` - id - title - subtitle - url - createdAt - updatedAt - issue { - id - identifier - title - } - creator { - id - name - } -`; - -/** - * Create an attachment on an issue - * - * If an attachment with the same url and issueId already exists, - * the existing record is updated instead of creating a duplicate. - */ -export const CREATE_ATTACHMENT_MUTATION = ` - mutation AttachmentCreate($input: AttachmentCreateInput!) { - attachmentCreate(input: $input) { - success - attachment { - ${ATTACHMENT_FRAGMENT} - } - } - } -`; - -/** - * Delete an attachment - */ -export const DELETE_ATTACHMENT_MUTATION = ` - mutation AttachmentDelete($id: String!) { - attachmentDelete(id: $id) { - success - } - } -`; - -/** - * List attachments on an issue - */ -export const LIST_ATTACHMENTS_QUERY = ` - query ListAttachments($issueId: String!) { - issue(id: $issueId) { - attachments { - nodes { - ${ATTACHMENT_FRAGMENT} - } - } - } - } -`; diff --git a/src/queries/common.ts b/src/queries/common.ts deleted file mode 100644 index 97ab002..0000000 --- a/src/queries/common.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Common GraphQL fragments for reuse across queries - * - * These fragments define reusable field selections for Linear entities, - * ensuring consistent data structure and reducing duplication in query definitions. - * Each fragment focuses on specific entity relationships for optimal querying. - */ - -/** - * Core issue fields that are always needed - * Includes basic metadata and timestamps - */ -export const ISSUE_CORE_FIELDS = ` - id - identifier - title - description - branchName - priority - estimate - createdAt - updatedAt -`; - -/** - * Issue workflow state relationship - * Provides current status information for the issue - */ -export const ISSUE_STATE_FRAGMENT = ` - state { - id - name - } -`; - -/** - * Issue assignee relationship - * Provides user information for the assigned person - */ -export const ISSUE_ASSIGNEE_FRAGMENT = ` - assignee { - id - name - } -`; - -/** - * Issue team relationship - * Provides team information including key and name - */ -export const ISSUE_TEAM_FRAGMENT = ` - team { - id - key - name - } -`; - -/** - * Issue project relationship - * Provides project information for issue association - */ -export const ISSUE_PROJECT_FRAGMENT = ` - project { - id - name - } -`; - -/** - * Issue labels relationship - * Provides all label nodes associated with the issue - */ -export const ISSUE_LABELS_FRAGMENT = ` - labels { - nodes { - id - name - } - } -`; - -export const ISSUE_CYCLE_FRAGMENT = ` - cycle { - id - name - number - } -`; - -export const ISSUE_PROJECT_MILESTONE_FRAGMENT = ` - projectMilestone { - id - name - targetDate - } -`; - -/** - * Issue comments relationship - * Provides comment content and user information - */ -export const ISSUE_COMMENTS_FRAGMENT = ` - comments { - nodes { - id - body - createdAt - updatedAt - user { - id - name - } - } - } -`; - -/** - * Issue parent relationship - * Provides basic information about the parent issue if this is a sub-issue - */ -export const ISSUE_PARENT_FRAGMENT = ` - parent { - id - identifier - title - } -`; - -/** - * Issue children relationship - * Provides basic information about immediate child issues - */ -export const ISSUE_CHILDREN_FRAGMENT = ` - children { - nodes { - id - identifier - title - } - } -`; - -/** - * Complete issue fragment with all relationships - * - * Combines all issue fragments into a comprehensive field selection. - * This is used when full issue details are needed including all - * relationships (state, assignee, team, project, labels, comments). - */ -export const COMPLETE_ISSUE_FRAGMENT = ` - ${ISSUE_CORE_FIELDS} - ${ISSUE_STATE_FRAGMENT} - ${ISSUE_ASSIGNEE_FRAGMENT} - ${ISSUE_TEAM_FRAGMENT} - ${ISSUE_PROJECT_FRAGMENT} - ${ISSUE_LABELS_FRAGMENT} - ${ISSUE_CYCLE_FRAGMENT} - ${ISSUE_PROJECT_MILESTONE_FRAGMENT} - ${ISSUE_PARENT_FRAGMENT} - ${ISSUE_CHILDREN_FRAGMENT} -`; - -/** - * Complete issue fragment including comments - */ -export const COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT = ` - ${COMPLETE_ISSUE_FRAGMENT} - ${ISSUE_COMMENTS_FRAGMENT} -`; diff --git a/src/queries/documents.ts b/src/queries/documents.ts deleted file mode 100644 index ae809dc..0000000 --- a/src/queries/documents.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * GraphQL queries and mutations for Linear documents - * - * Documents are standalone entities that can be associated with projects, - * initiatives, or teams. To link a document to an issue, use the - * attachments API (see attachments.ts). - */ - -export const DOCUMENT_FRAGMENT = ` - id - title - content - slugId - url - icon - color - createdAt - updatedAt - creator { - id - name - } - project { - id - name - } - trashed -`; - -/** - * Create a new document - */ -export const CREATE_DOCUMENT_MUTATION = ` - mutation DocumentCreate($input: DocumentCreateInput!) { - documentCreate(input: $input) { - success - document { - ${DOCUMENT_FRAGMENT} - } - } - } -`; - -/** - * Update an existing document - */ -export const UPDATE_DOCUMENT_MUTATION = ` - mutation DocumentUpdate($id: String!, $input: DocumentUpdateInput!) { - documentUpdate(id: $id, input: $input) { - success - document { - ${DOCUMENT_FRAGMENT} - } - } - } -`; - -/** - * Get a single document by ID - */ -export const GET_DOCUMENT_QUERY = ` - query GetDocument($id: String!) { - document(id: $id) { - ${DOCUMENT_FRAGMENT} - } - } -`; - -/** - * List documents with optional filtering - */ -export const LIST_DOCUMENTS_QUERY = ` - query ListDocuments($first: Int!, $filter: DocumentFilter) { - documents(first: $first, filter: $filter) { - nodes { - ${DOCUMENT_FRAGMENT} - } - } - } -`; - -/** - * Delete (trash) a document - * - * Note: This is a soft delete - the document is moved to trash. - * Use documentUnarchive to restore. - */ -export const DELETE_DOCUMENT_MUTATION = ` - mutation DocumentDelete($id: String!) { - documentDelete(id: $id) { - success - } - } -`; diff --git a/src/queries/index.ts b/src/queries/index.ts deleted file mode 100644 index 8947595..0000000 --- a/src/queries/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Export all GraphQL queries and fragments - * - * This barrel export provides access to all GraphQL queries and - * fragments from a single entry point. Includes common fragments, - * issue queries, and optimized batch resolution queries. - * - * Structure: - * - common.js: Reusable fragments for consistent data structures - * - issues.js: Optimized queries for issue operations - */ - -// Common fragments for reusable field selections -export * from "./common.js"; - -// Optimized queries for issue CRUD operations and batch resolution -export * from "./issues.js"; - -// Document queries and mutations -export * from "./documents.js"; - -// Attachment queries and mutations -export * from "./attachments.js"; diff --git a/src/queries/issues.ts b/src/queries/issues.ts deleted file mode 100644 index 1bf5177..0000000 --- a/src/queries/issues.ts +++ /dev/null @@ -1,376 +0,0 @@ -/** - * Optimized GraphQL queries for issue operations - * - * This module contains highly optimized GraphQL queries that fetch - * all necessary issue data in single requests, eliminating N+1 query - * problem common with Linear SDK. Each query uses comprehensive - * fragments to ensure consistent data structures. - */ - -import { - COMPLETE_ISSUE_FRAGMENT, - COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT, -} from "./common.js"; - -/** - * Get issues list with all relationships in single query - * - * Fetches paginated issues excluding completed ones, - * ordered by most recently updated. Includes all relationships - * for comprehensive issue data. - */ -export const GET_ISSUES_QUERY = ` - query GetIssues($first: Int!, $orderBy: PaginationOrderBy) { - issues( - first: $first - orderBy: $orderBy - filter: { - state: { type: { neq: "completed" } } - } - ) { - nodes { - ${COMPLETE_ISSUE_FRAGMENT} - } - } - } -`; - -/** - * Search issues with text search and all relationships in single query - * - * Provides full-text search across Linear issues with complete - * relationship data for each match. - */ -export const SEARCH_ISSUES_QUERY = ` - query SearchIssues($term: String!, $first: Int!) { - searchIssues(term: $term, first: $first, includeArchived: false) { - nodes { - ${COMPLETE_ISSUE_FRAGMENT} - } - } - } -`; - -/** - * Search issues with advanced filters and all relationships in single query - * - * Supports filtering by team, assignee, project, and states. - * Used by the advanced search functionality with multiple criteria. - */ -export const FILTERED_SEARCH_ISSUES_QUERY = ` - query FilteredSearchIssues( - $first: Int! - $filter: IssueFilter - $orderBy: PaginationOrderBy - ) { - issues( - first: $first - filter: $filter - orderBy: $orderBy - includeArchived: false - ) { - nodes { - ${COMPLETE_ISSUE_FRAGMENT} - } - } - } -`; - -/** - * Batch resolve query for search filters - * - * Resolves human-readable identifiers to UUIDs in a single batch query. - * Used to pre-resolve teams, projects, and assignees before executing - * main search query to avoid N+1 queries. - */ -export const BATCH_RESOLVE_FOR_SEARCH_QUERY = ` - query BatchResolveForSearch( - $teamKey: String - $teamName: String - $projectName: String - $assigneeEmail: String - ) { - # Resolve team if provided - teams( - filter: { - or: [ - { key: { eq: $teamKey } } - { name: { eq: $teamName } } - ] - } - first: 1 - ) { - nodes { - id - key - name - } - } - - # Resolve project if provided (case-insensitive to be user-friendly) - projects(filter: { name: { eqIgnoreCase: $projectName } }, first: 1) { - nodes { - id - name - } - } - - # Resolve user by email if provided - users(filter: { email: { eq: $assigneeEmail } }, first: 1) { - nodes { - id - name - email - } - } - } -`; - -/** - * Get single issue by UUID with comments and all relationships - * - * Fetches complete issue data including comments by direct UUID lookup. - * Uses the comprehensive fragment with comment data for detailed view. - */ -export const GET_ISSUE_BY_ID_QUERY = ` - query GetIssue($id: String!) { - issue(id: $id) { - ${COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT} - } - } -`; - -/** - * Get issue by identifier (team key + number) - * - * Fetches issue using TEAM-123 format. Resolves team key and - * issue number to find the exact issue, returning complete data with comments. - */ -export const GET_ISSUE_BY_IDENTIFIER_QUERY = ` - query GetIssueByIdentifier($teamKey: String!, $number: Float!) { - issues( - filter: { - team: { key: { eq: $teamKey } } - number: { eq: $number } - } - first: 1 - ) { - nodes { - ${COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT} - } - } - } -`; - -/** - * Comprehensive batch resolve for update operations - * - * Resolves all necessary entity references in a single batch query - * before issue update. Includes labels, projects, teams, and parent issues. - * This prevents N+1 queries during update operations. - */ -export const BATCH_RESOLVE_FOR_UPDATE_QUERY = ` - query BatchResolveForUpdate( - $labelNames: [String!] - $projectName: String - $teamKey: String - $issueNumber: Float - $milestoneName: String - ) { - # Resolve labels if provided - labels: issueLabels(filter: { name: { in: $labelNames } }) { - nodes { - id - name - isGroup - parent { - id - name - } - children { - nodes { - id - name - } - } - } - } - - # Resolve project if provided (case-insensitive to be user-friendly) - projects(filter: { name: { eqIgnoreCase: $projectName } }, first: 1) { - nodes { - id - name - projectMilestones { - nodes { - id - name - } - } - } - } - - # Resolve milestone if provided (standalone query in case no project context) - milestones: projectMilestones( - filter: { name: { eq: $milestoneName } } - first: 1 - ) { - nodes { - id - name - } - } - - # Resolve issue identifier if provided - issues( - filter: { - and: [ - { team: { key: { eq: $teamKey } } } - { number: { eq: $issueNumber } } - ] - } - first: 1 - ) { - nodes { - id - identifier - labels { - nodes { - id - name - } - } - project { - id - projectMilestones { - nodes { - id - name - } - } - } - } - } - } -`; - -/** - * Create issue mutation with complete response - * - * Creates a new issue and returns complete issue data including - * all relationships. Uses the comprehensive fragment to ensure - * consistent data structure with read operations. - */ -export const CREATE_ISSUE_MUTATION = ` - mutation CreateIssue($input: IssueCreateInput!) { - issueCreate(input: $input) { - success - issue { - ${COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT} - } - } - } -`; - -/** - * Update issue mutation with complete response - * - * Updates an existing issue and returns complete issue data with - * all relationships. Ensures consistency between update and read - * operations by using the same fragment structure. - */ -export const UPDATE_ISSUE_MUTATION = ` - mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) { - issueUpdate(id: $id, input: $input) { - success - issue { - ${COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT} - } - } - } -`; - -/** - * Comprehensive batch resolve for create operations - * - * Resolves all entity references needed for issue creation in a single - * batch query. Prevents N+1 queries during issue creation by - * pre-resolving teams, projects, labels, and parent issues. - */ -export const BATCH_RESOLVE_FOR_CREATE_QUERY = ` - query BatchResolveForCreate( - $teamKey: String - $teamName: String - $projectName: String - $labelNames: [String!] - $parentTeamKey: String - $parentIssueNumber: Float - ) { - # Resolve team if provided - teams( - filter: { - or: [ - { key: { eq: $teamKey } } - { name: { eq: $teamName } } - ] - } - first: 1 - ) { - nodes { - id - key - name - } - } - - # Resolve project if provided (case-insensitive to be user-friendly) - projects(filter: { name: { eqIgnoreCase: $projectName } }, first: 1) { - nodes { - id - name - projectMilestones { - nodes { id name } - } - # Projects don't own cycles directly, but include teams for context if needed - } - } - - # Resolve labels if provided - labels: issueLabels(filter: { name: { in: $labelNames } }) { - nodes { - id - name - isGroup - parent { - id - name - } - children { - nodes { - id - name - } - } - } - } - - # Resolve parent issue if provided - parentIssues: issues( - filter: { - and: [ - { team: { key: { eq: $parentTeamKey } } } - { number: { eq: $parentIssueNumber } } - ] - } - first: 1 - ) { - nodes { - id - identifier - } - } - - # Resolve cycles by name (team-scoped lookup is preferred but we also provide global fallback) - - } -`; diff --git a/src/queries/project-milestones.ts b/src/queries/project-milestones.ts deleted file mode 100644 index e613983..0000000 --- a/src/queries/project-milestones.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { COMPLETE_ISSUE_FRAGMENT } from "./common.js"; - -/** - * List milestones in a project with minimal fields (optimized) - */ -export const LIST_PROJECT_MILESTONES_QUERY = ` - query ListProjectMilestones($projectId: String!, $first: Int!) { - project(id: $projectId) { - id - name - projectMilestones(first: $first) { - nodes { - id - name - description - targetDate - sortOrder - createdAt - updatedAt - } - } - } - } -`; - -/** - * Get single milestone by ID with full issue details - */ -export const GET_PROJECT_MILESTONE_BY_ID_QUERY = ` - query GetProjectMilestone($id: String!, $issuesFirst: Int) { - projectMilestone(id: $id) { - id - name - description - targetDate - sortOrder - createdAt - updatedAt - project { - id - name - } - issues(first: $issuesFirst) { - nodes { - ${COMPLETE_ISSUE_FRAGMENT} - } - } - } - } -`; - -/** - * Find milestone by name within project context (scoped lookup) - */ -export const FIND_PROJECT_MILESTONE_BY_NAME_SCOPED = ` - query FindProjectMilestoneScoped($name: String!, $projectId: String!) { - project(id: $projectId) { - projectMilestones(filter: { name: { eq: $name } }, first: 10) { - nodes { - id - name - targetDate - sortOrder - project { - id - name - } - } - } - } - } -`; - -/** - * Find milestone by name globally (fallback) - */ -export const FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL = ` - query FindProjectMilestoneGlobal($name: String!) { - projectMilestones(filter: { name: { eq: $name } }, first: 10) { - nodes { - id - name - targetDate - sortOrder - project { - id - name - } - } - } - } -`; - -/** - * Create a new project milestone - */ -export const CREATE_PROJECT_MILESTONE_MUTATION = ` - mutation CreateProjectMilestone($projectId: String!, $name: String!, $description: String, $targetDate: TimelessDate) { - projectMilestoneCreate(input: { - projectId: $projectId - name: $name - description: $description - targetDate: $targetDate - }) { - success - projectMilestone { - id - name - description - targetDate - sortOrder - createdAt - updatedAt - project { - id - name - } - } - } - } -`; - -/** - * Update an existing project milestone - */ -export const UPDATE_PROJECT_MILESTONE_MUTATION = ` - mutation UpdateProjectMilestone($id: String!, $name: String, $description: String, $targetDate: TimelessDate, $sortOrder: Float) { - projectMilestoneUpdate(id: $id, input: { - name: $name - description: $description - targetDate: $targetDate - sortOrder: $sortOrder - }) { - success - projectMilestone { - id - name - description - targetDate - sortOrder - updatedAt - project { - id - name - } - } - } - } -`; diff --git a/src/resolvers/cycle-resolver.ts b/src/resolvers/cycle-resolver.ts new file mode 100644 index 0000000..9f8048c --- /dev/null +++ b/src/resolvers/cycle-resolver.ts @@ -0,0 +1,94 @@ +import type { LinearDocument } from "@linear/sdk"; +import type { LinearSdkClient } from "../client/linear-client.js"; +import { multipleMatchesError, notFoundError } from "../common/errors.js"; +import { isUuid } from "../common/identifier.js"; +import { resolveTeamId } from "./team-resolver.js"; + +/** + * Resolves cycle identifier to UUID. + * + * Accepts UUID or cycle name. When multiple cycles match a name, + * prefers active > next > previous. Use teamFilter to disambiguate. + * + * @param client - Linear SDK client + * @param nameOrId - Cycle name or UUID + * @param teamFilter - Optional team key/name/ID to scope search + * @returns Cycle UUID + * @throws Error if not found or multiple matches without clear preference + */ +export async function resolveCycleId( + client: LinearSdkClient, + nameOrId: string, + teamFilter?: string, +): Promise<string> { + if (isUuid(nameOrId)) return nameOrId; + + const filter: LinearDocument.CycleFilter = { + name: { eq: nameOrId }, + }; + + if (teamFilter) { + const teamId = await resolveTeamId(client, teamFilter); + filter.team = { id: { eq: teamId } }; + } + + const cyclesConnection = await client.sdk.cycles({ + filter, + first: 10, + }); + + const nodes: Array<{ + id: string; + name: string; + number: number; + startsAt?: string; + isActive: boolean; + isNext: boolean; + isPrevious: boolean; + team?: { id: string; key: string; name: string }; + }> = []; + + for (const cycle of cyclesConnection.nodes) { + const team = await cycle.team; + nodes.push({ + id: cycle.id, + name: cycle.name ?? "", + number: cycle.number, + startsAt: cycle.startsAt + ? new Date(cycle.startsAt).toISOString() + : undefined, + isActive: cycle.isActive, + isNext: cycle.isNext, + isPrevious: cycle.isPrevious, + team: team ? { id: team.id, key: team.key, name: team.name } : undefined, + }); + } + + if (nodes.length === 0) { + throw notFoundError( + "Cycle", + nameOrId, + teamFilter ? `for team ${teamFilter}` : undefined, + ); + } + + // Disambiguate: prefer active, then next, then previous + let chosen = nodes.find((n) => n.isActive); + if (!chosen) chosen = nodes.find((n) => n.isNext); + if (!chosen) chosen = nodes.find((n) => n.isPrevious); + if (!chosen && nodes.length === 1) chosen = nodes[0]; + + if (!chosen) { + const matches = nodes.map( + (n) => `${n.id} (${n.team?.key || "?"} / #${n.number} / ${n.startsAt})`, + ); + throw multipleMatchesError( + "cycle", + nameOrId, + matches, + "use an ID or scope with --team", + ); + } + + return chosen.id; +} diff --git a/src/resolvers/issue-resolver.ts b/src/resolvers/issue-resolver.ts new file mode 100644 index 0000000..a8838be --- /dev/null +++ b/src/resolvers/issue-resolver.ts @@ -0,0 +1,36 @@ +import type { LinearSdkClient } from "../client/linear-client.js"; +import { notFoundError } from "../common/errors.js"; +import { isUuid, parseIssueIdentifier } from "../common/identifier.js"; + +/** + * Resolves issue identifier to UUID. + * + * Accepts UUID or issue identifier (e.g., "ENG-123"). + * + * @param client - Linear SDK client + * @param issueIdOrIdentifier - Issue UUID or identifier + * @returns Issue UUID + * @throws Error if issue not found + */ +export async function resolveIssueId( + client: LinearSdkClient, + issueIdOrIdentifier: string, +): Promise<string> { + if (isUuid(issueIdOrIdentifier)) return issueIdOrIdentifier; + + const { teamKey, issueNumber } = parseIssueIdentifier(issueIdOrIdentifier); + + const issues = await client.sdk.issues({ + filter: { + number: { eq: issueNumber }, + team: { key: { eq: teamKey } }, + }, + first: 1, + }); + + if (issues.nodes.length === 0) { + throw notFoundError("Issue", issueIdOrIdentifier); + } + + return issues.nodes[0].id; +} diff --git a/src/resolvers/label-resolver.ts b/src/resolvers/label-resolver.ts new file mode 100644 index 0000000..5fd3b60 --- /dev/null +++ b/src/resolvers/label-resolver.ts @@ -0,0 +1,28 @@ +import type { LinearSdkClient } from "../client/linear-client.js"; +import { notFoundError } from "../common/errors.js"; +import { isUuid } from "../common/identifier.js"; + +export async function resolveLabelId( + client: LinearSdkClient, + nameOrId: string, +): Promise<string> { + if (isUuid(nameOrId)) return nameOrId; + + const result = await client.sdk.issueLabels({ + filter: { name: { eqIgnoreCase: nameOrId } }, + first: 1, + }); + + if (result.nodes.length === 0) { + throw notFoundError("Label", nameOrId); + } + + return result.nodes[0].id; +} + +export async function resolveLabelIds( + client: LinearSdkClient, + namesOrIds: string[], +): Promise<string[]> { + return Promise.all(namesOrIds.map((id) => resolveLabelId(client, id))); +} diff --git a/src/resolvers/milestone-resolver.ts b/src/resolvers/milestone-resolver.ts new file mode 100644 index 0000000..e9d9367 --- /dev/null +++ b/src/resolvers/milestone-resolver.ts @@ -0,0 +1,83 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { LinearSdkClient } from "../client/linear-client.js"; +import { multipleMatchesError, notFoundError } from "../common/errors.js"; +import { isUuid } from "../common/identifier.js"; +import { + FindProjectMilestoneGlobalDocument, + type FindProjectMilestoneGlobalQuery, + FindProjectMilestoneScopedDocument, + type FindProjectMilestoneScopedQuery, +} from "../gql/graphql.js"; +import { resolveProjectId } from "./project-resolver.js"; + +/** + * Resolves milestone identifier to UUID. + * + * Accepts UUID or milestone name. When multiple milestones match a name, + * use projectNameOrId to scope the search to a specific project. + * + * ARCHITECTURAL EXCEPTION: This resolver uses GraphQLClient in addition to + * LinearSdkClient because the Linear SDK does not expose milestone lookup + * by name. The GraphQL client is needed for the FindProjectMilestoneScoped + * and FindProjectMilestoneGlobal queries. This is a documented deviation + * from the standard resolver contract (resolvers normally use SDK only). + * + * @param gqlClient - GraphQL client for querying milestones + * @param sdkClient - SDK client for project resolution + * @param nameOrId - Milestone name or UUID + * @param projectNameOrId - Optional project name/ID to scope search + * @returns Milestone UUID + * @throws Error if not found or multiple matches without project scope + */ +export async function resolveMilestoneId( + gqlClient: GraphQLClient, + sdkClient: LinearSdkClient, + nameOrId: string, + projectNameOrId?: string, +): Promise<string> { + if (isUuid(nameOrId)) return nameOrId; + + type MilestoneNode = { + id: string; + name: string; + project?: { name: string } | null; + }; + let nodes: MilestoneNode[] = []; + + if (projectNameOrId) { + const projectId = await resolveProjectId(sdkClient, projectNameOrId); + const result = await gqlClient.request<FindProjectMilestoneScopedQuery>( + FindProjectMilestoneScopedDocument, + { name: nameOrId, projectId }, + ); + nodes = (result.project?.projectMilestones?.nodes as MilestoneNode[]) || []; + } + + // Fall back to global search if no project scope or not found + if (nodes.length === 0) { + const globalResult = + await gqlClient.request<FindProjectMilestoneGlobalQuery>( + FindProjectMilestoneGlobalDocument, + { name: nameOrId }, + ); + nodes = (globalResult.projectMilestones?.nodes as MilestoneNode[]) || []; + } + + if (nodes.length === 0) { + throw notFoundError("Milestone", nameOrId); + } + + if (nodes.length > 1) { + const matches = nodes.map( + (m) => `"${m.name}" in project "${m.project?.name}"`, + ); + throw multipleMatchesError( + "milestone", + nameOrId, + matches, + "specify --project or use the milestone ID", + ); + } + + return nodes[0].id; +} diff --git a/src/resolvers/project-resolver.ts b/src/resolvers/project-resolver.ts new file mode 100644 index 0000000..4b22bec --- /dev/null +++ b/src/resolvers/project-resolver.ts @@ -0,0 +1,21 @@ +import type { LinearSdkClient } from "../client/linear-client.js"; +import { notFoundError } from "../common/errors.js"; +import { isUuid } from "../common/identifier.js"; + +export async function resolveProjectId( + client: LinearSdkClient, + nameOrId: string, +): Promise<string> { + if (isUuid(nameOrId)) return nameOrId; + + const result = await client.sdk.projects({ + filter: { name: { eqIgnoreCase: nameOrId } }, + first: 1, + }); + + if (result.nodes.length === 0) { + throw notFoundError("Project", nameOrId); + } + + return result.nodes[0].id; +} diff --git a/src/resolvers/status-resolver.ts b/src/resolvers/status-resolver.ts new file mode 100644 index 0000000..9dc9254 --- /dev/null +++ b/src/resolvers/status-resolver.ts @@ -0,0 +1,32 @@ +import type { LinearDocument } from "@linear/sdk"; +import type { LinearSdkClient } from "../client/linear-client.js"; +import { notFoundError } from "../common/errors.js"; +import { isUuid } from "../common/identifier.js"; + +export async function resolveStatusId( + client: LinearSdkClient, + nameOrId: string, + teamId?: string, +): Promise<string> { + if (isUuid(nameOrId)) return nameOrId; + + const filter: LinearDocument.WorkflowStateFilter = { + name: { eqIgnoreCase: nameOrId }, + }; + + if (teamId) { + filter.team = { id: { eq: teamId } }; + } + + const result = await client.sdk.workflowStates({ + filter, + first: 1, + }); + + if (result.nodes.length === 0) { + const context = teamId ? `for team ${teamId}` : undefined; + throw notFoundError("Status", nameOrId, context); + } + + return result.nodes[0].id; +} diff --git a/src/resolvers/team-resolver.ts b/src/resolvers/team-resolver.ts new file mode 100644 index 0000000..ff0769a --- /dev/null +++ b/src/resolvers/team-resolver.ts @@ -0,0 +1,26 @@ +import type { LinearSdkClient } from "../client/linear-client.js"; +import { notFoundError } from "../common/errors.js"; +import { isUuid } from "../common/identifier.js"; + +export async function resolveTeamId( + client: LinearSdkClient, + keyOrNameOrId: string, +): Promise<string> { + if (isUuid(keyOrNameOrId)) return keyOrNameOrId; + + // Try by key first + const byKey = await client.sdk.teams({ + filter: { key: { eq: keyOrNameOrId } }, + first: 1, + }); + if (byKey.nodes.length > 0) return byKey.nodes[0].id; + + // Fall back to name + const byName = await client.sdk.teams({ + filter: { name: { eq: keyOrNameOrId } }, + first: 1, + }); + if (byName.nodes.length > 0) return byName.nodes[0].id; + + throw notFoundError("Team", keyOrNameOrId); +} diff --git a/src/resolvers/user-resolver.ts b/src/resolvers/user-resolver.ts new file mode 100644 index 0000000..98b847a --- /dev/null +++ b/src/resolvers/user-resolver.ts @@ -0,0 +1,37 @@ +import type { LinearSdkClient } from "../client/linear-client.js"; +import { multipleMatchesError, notFoundError } from "../common/errors.js"; +import { isUuid } from "../common/identifier.js"; + +export async function resolveUserId( + client: LinearSdkClient, + nameOrEmailOrId: string, +): Promise<string> { + if (isUuid(nameOrEmailOrId)) return nameOrEmailOrId; + + // Try by display name first (case-insensitive) + const byName = await client.sdk.users({ + filter: { displayName: { eqIgnoreCase: nameOrEmailOrId } }, + first: 10, + }); + + if (byName.nodes.length === 1) return byName.nodes[0].id; + + if (byName.nodes.length > 1) { + throw multipleMatchesError( + "User", + nameOrEmailOrId, + byName.nodes.map((u) => `${u.name} <${u.email}>`), + "Use email or UUID to disambiguate", + ); + } + + // Fall back to email (case-insensitive) + const byEmail = await client.sdk.users({ + filter: { email: { eqIgnoreCase: nameOrEmailOrId } }, + first: 1, + }); + + if (byEmail.nodes.length > 0) return byEmail.nodes[0].id; + + throw notFoundError("User", nameOrEmailOrId); +} diff --git a/src/services/attachment-service.ts b/src/services/attachment-service.ts new file mode 100644 index 0000000..839a06c --- /dev/null +++ b/src/services/attachment-service.ts @@ -0,0 +1,59 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { Attachment, CreatedAttachment } from "../common/types.js"; +import { + AttachmentCreateDocument, + type AttachmentCreateInput, + type AttachmentCreateMutation, + AttachmentDeleteDocument, + type AttachmentDeleteMutation, + ListAttachmentsDocument, + type ListAttachmentsQuery, +} from "../gql/graphql.js"; + +export async function createAttachment( + client: GraphQLClient, + input: AttachmentCreateInput, +): Promise<CreatedAttachment> { + const result = await client.request<AttachmentCreateMutation>( + AttachmentCreateDocument, + { input }, + ); + + if (!result.attachmentCreate.success || !result.attachmentCreate.attachment) { + throw new Error("Failed to create attachment"); + } + + return result.attachmentCreate.attachment; +} + +export async function deleteAttachment( + client: GraphQLClient, + id: string, +): Promise<boolean> { + const result = await client.request<AttachmentDeleteMutation>( + AttachmentDeleteDocument, + { id }, + ); + + if (!result.attachmentDelete.success) { + throw new Error("Failed to delete attachment"); + } + + return true; +} + +export async function listAttachments( + client: GraphQLClient, + issueId: string, +): Promise<Attachment[]> { + const result = await client.request<ListAttachmentsQuery>( + ListAttachmentsDocument, + { issueId }, + ); + + if (!result.issue) { + throw new Error(`Issue with ID "${issueId}" not found`); + } + + return result.issue.attachments?.nodes ?? []; +} diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts new file mode 100644 index 0000000..1807974 --- /dev/null +++ b/src/services/auth-service.ts @@ -0,0 +1,8 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { Viewer } from "../common/types.js"; +import { GetViewerDocument, type GetViewerQuery } from "../gql/graphql.js"; + +export async function validateToken(client: GraphQLClient): Promise<Viewer> { + const result = await client.request<GetViewerQuery>(GetViewerDocument); + return result.viewer; +} diff --git a/src/services/comment-service.ts b/src/services/comment-service.ts new file mode 100644 index 0000000..f764762 --- /dev/null +++ b/src/services/comment-service.ts @@ -0,0 +1,34 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import { + type CommentCreateInput, + CreateCommentDocument, + type CreateCommentMutation, +} from "../gql/graphql.js"; + +export interface Comment { + id: string; + body: string; + createdAt: string; +} + +export async function createComment( + client: GraphQLClient, + input: CommentCreateInput, +): Promise<Comment> { + const result = await client.request<CreateCommentMutation>( + CreateCommentDocument, + { input }, + ); + + if (!result.commentCreate.success || !result.commentCreate.comment) { + throw new Error("Failed to create comment"); + } + + const comment = result.commentCreate.comment; + + return { + id: comment.id, + body: comment.body, + createdAt: comment.createdAt, + }; +} diff --git a/src/services/cycle-service.ts b/src/services/cycle-service.ts new file mode 100644 index 0000000..90cdd4f --- /dev/null +++ b/src/services/cycle-service.ts @@ -0,0 +1,101 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { PaginatedResult, PaginationOptions } from "../common/types.js"; +import { + type CycleFilter, + GetCycleByIdDocument, + type GetCycleByIdQuery, + GetCyclesDocument, + type GetCyclesQuery, +} from "../gql/graphql.js"; + +export interface Cycle { + id: string; + number: number; + name: string; + startsAt: string; + endsAt: string; + isActive: boolean; + isNext: boolean; + isPrevious: boolean; +} + +export interface CycleDetail extends Cycle { + issues: Array<{ + id: string; + identifier: string; + title: string; + state: { name: string }; + }>; +} + +export async function listCycles( + client: GraphQLClient, + teamId?: string, + activeOnly: boolean = false, + options: PaginationOptions = {}, +): Promise<PaginatedResult<Cycle>> { + const { limit = 50, after } = options; + const filter: CycleFilter = {}; + + if (teamId) { + filter.team = { id: { eq: teamId } }; + } + + if (activeOnly) { + filter.isActive = { eq: true }; + } + + const result = await client.request<GetCyclesQuery>(GetCyclesDocument, { + first: limit, + after, + filter, + }); + + return { + nodes: result.cycles.nodes.map((cycle) => ({ + id: cycle.id, + number: cycle.number, + name: cycle.name ?? `Cycle ${cycle.number}`, + startsAt: cycle.startsAt, + endsAt: cycle.endsAt, + isActive: cycle.isActive, + isNext: cycle.isNext, + isPrevious: cycle.isPrevious, + })), + pageInfo: result.cycles.pageInfo, + }; +} + +export async function getCycle( + client: GraphQLClient, + cycleId: string, + issuesLimit: number = 50, +): Promise<CycleDetail> { + const result = await client.request<GetCycleByIdQuery>(GetCycleByIdDocument, { + id: cycleId, + first: issuesLimit, + }); + + const cycle = result.cycle; + + if (!cycle) { + throw new Error(`Cycle with ID "${cycleId}" not found`); + } + + return { + id: cycle.id, + number: cycle.number, + name: cycle.name ?? `Cycle ${cycle.number}`, + startsAt: cycle.startsAt, + endsAt: cycle.endsAt, + isActive: cycle.isActive, + isNext: cycle.isNext, + isPrevious: cycle.isPrevious, + issues: cycle.issues.nodes.map((issue) => ({ + id: issue.id, + identifier: issue.identifier, + title: issue.title, + state: { name: issue.state.name }, + })), + }; +} diff --git a/src/services/document-service.ts b/src/services/document-service.ts new file mode 100644 index 0000000..c65c488 --- /dev/null +++ b/src/services/document-service.ts @@ -0,0 +1,134 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { + CreatedDocument, + Document, + DocumentListItem, + PaginatedResult, + UpdatedDocument, +} from "../common/types.js"; +import { + DocumentCreateDocument, + type DocumentCreateInput, + type DocumentCreateMutation, + DocumentDeleteDocument, + type DocumentDeleteMutation, + type DocumentFilter, + DocumentUpdateDocument, + type DocumentUpdateInput, + type DocumentUpdateMutation, + GetDocumentDocument, + type GetDocumentQuery, + ListDocumentsDocument, + type ListDocumentsQuery, +} from "../gql/graphql.js"; + +export async function getDocument( + client: GraphQLClient, + id: string, +): Promise<Document> { + const result = await client.request<GetDocumentQuery>(GetDocumentDocument, { + id, + }); + + if (!result.document) { + throw new Error(`Document with ID "${id}" not found`); + } + + return result.document; +} + +export async function createDocument( + client: GraphQLClient, + input: DocumentCreateInput, +): Promise<CreatedDocument> { + const result = await client.request<DocumentCreateMutation>( + DocumentCreateDocument, + { input }, + ); + + if (!result.documentCreate.success || !result.documentCreate.document) { + throw new Error("Failed to create document"); + } + + return result.documentCreate.document; +} + +export async function updateDocument( + client: GraphQLClient, + id: string, + input: DocumentUpdateInput, +): Promise<UpdatedDocument> { + const result = await client.request<DocumentUpdateMutation>( + DocumentUpdateDocument, + { id, input }, + ); + + if (!result.documentUpdate.success || !result.documentUpdate.document) { + throw new Error("Failed to update document"); + } + + return result.documentUpdate.document; +} + +export async function listDocuments( + client: GraphQLClient, + options?: { + limit?: number; + after?: string; + filter?: DocumentFilter; + }, +): Promise<PaginatedResult<DocumentListItem>> { + const result = await client.request<ListDocumentsQuery>( + ListDocumentsDocument, + { + first: options?.limit ?? 25, + after: options?.after, + filter: options?.filter, + }, + ); + + return { + nodes: result.documents?.nodes ?? [], + pageInfo: result.documents?.pageInfo ?? { + hasNextPage: false, + endCursor: null, + }, + }; +} + +export async function listDocumentsBySlugIds( + client: GraphQLClient, + slugIds: string[], +): Promise<DocumentListItem[]> { + if (slugIds.length === 0) { + return []; + } + + const result = await client.request<ListDocumentsQuery>( + ListDocumentsDocument, + { + first: slugIds.length, + filter: { + slugId: { in: slugIds }, + }, + }, + ); + + return result.documents?.nodes ?? []; +} + +export async function deleteDocument( + client: GraphQLClient, + id: string, +): Promise<boolean> { + const result = await client.request<DocumentDeleteMutation>( + DocumentDeleteDocument, + { id }, + ); + + if (!result.documentDelete.success) { + throw new Error("Failed to delete document"); + } + + return true; +} diff --git a/src/utils/file-service.ts b/src/services/file-service.ts similarity index 92% rename from src/utils/file-service.ts rename to src/services/file-service.ts index e5aa886..3e3863b 100644 --- a/src/utils/file-service.ts +++ b/src/services/file-service.ts @@ -10,9 +10,14 @@ * - Comprehensive error handling and status reporting */ -import { access, mkdir, readFile, stat, writeFile } from "fs/promises"; -import { basename, dirname, extname } from "path"; -import { extractFilenameFromUrl, isLinearUploadUrl } from "./embed-parser.js"; +import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises"; +import { basename, dirname, extname } from "node:path"; +import { print } from "graphql"; +import { + extractFilenameFromUrl, + isLinearUploadUrl, +} from "../common/embed-parser.js"; +import { FileUploadDocument } from "../gql/graphql.js"; /** * Maximum file size for uploads (20MB) @@ -169,8 +174,7 @@ export class FileService { await access(outputPath); return { success: false, - error: - `File already exists: ${outputPath}. Use --overwrite to replace.`, + error: `File already exists: ${outputPath}. Use --overwrite to replace.`, }; } catch { // File doesn't exist, we can proceed @@ -281,31 +285,14 @@ export class FileService { const actualMB = fileSize / (1024 * 1024); return { success: false, - error: `File too large: ${ - actualMB.toFixed(1) - }MB exceeds limit of ${maxMB}MB`, + error: `File too large: ${actualMB.toFixed( + 1, + )}MB exceeds limit of ${maxMB}MB`, }; } const contentType = getMimeType(filePath); - // Step 1: Request upload URL via GraphQL fileUpload mutation - const query = ` - mutation FileUpload($contentType: String!, $filename: String!, $size: Int!) { - fileUpload(contentType: $contentType, filename: $filename, size: $size) { - success - uploadFile { - uploadUrl - assetUrl - headers { - key - value - } - } - } - } - `; - try { // Make GraphQL request const graphqlResponse = await fetch("https://api.linear.app/graphql", { @@ -315,7 +302,7 @@ export class FileService { Authorization: this.apiToken, }, body: JSON.stringify({ - query, + query: print(FileUploadDocument), variables: { contentType, filename, diff --git a/src/services/issue-relation-service.ts b/src/services/issue-relation-service.ts new file mode 100644 index 0000000..8779a70 --- /dev/null +++ b/src/services/issue-relation-service.ts @@ -0,0 +1,72 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import { notFoundError } from "../common/errors.js"; +import type { CreatedIssueRelation } from "../common/types.js"; +import { + CreateIssueRelationDocument, + type CreateIssueRelationMutation, + DeleteIssueRelationDocument, + type DeleteIssueRelationMutation, + GetIssueRelationsDocument, + type GetIssueRelationsQuery, + type IssueRelationType, +} from "../gql/graphql.js"; + +export async function createIssueRelation( + client: GraphQLClient, + input: { + issueId: string; + relatedIssueId: string; + type: IssueRelationType; + }, +): Promise<CreatedIssueRelation> { + const result = await client.request<CreateIssueRelationMutation>( + CreateIssueRelationDocument, + { input }, + ); + if (!result.issueRelationCreate.success) { + throw new Error("Failed to create issue relation"); + } + return result.issueRelationCreate.issueRelation; +} + +export async function findIssueRelation( + client: GraphQLClient, + issueId: string, + relatedIssueId: string, +): Promise<string> { + const result = await client.request<GetIssueRelationsQuery>( + GetIssueRelationsDocument, + { issueId }, + ); + + if (!result.issue) { + throw notFoundError("Issue", issueId); + } + + // Check forward relations + const forwardMatch = result.issue.relations.nodes.find( + (r) => r.relatedIssue.id === relatedIssueId, + ); + if (forwardMatch) return forwardMatch.id; + + // Check inverse relations + const inverseMatch = result.issue.inverseRelations.nodes.find( + (r) => r.issue.id === relatedIssueId, + ); + if (inverseMatch) return inverseMatch.id; + + throw notFoundError("Relation", `between ${issueId} and ${relatedIssueId}`); +} + +export async function deleteIssueRelation( + client: GraphQLClient, + relationId: string, +): Promise<void> { + const result = await client.request<DeleteIssueRelationMutation>( + DeleteIssueRelationDocument, + { id: relationId }, + ); + if (!result.issueRelationDelete.success) { + throw new Error("Failed to delete issue relation"); + } +} diff --git a/src/services/issue-service.ts b/src/services/issue-service.ts new file mode 100644 index 0000000..a333552 --- /dev/null +++ b/src/services/issue-service.ts @@ -0,0 +1,119 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { + CreatedIssue, + Issue, + IssueByIdentifier, + IssueDetail, + IssueSearchResult, + PaginatedResult, + PaginationOptions, + UpdatedIssue, +} from "../common/types.js"; +import { + CreateIssueDocument, + type CreateIssueMutation, + GetIssueByIdDocument, + GetIssueByIdentifierDocument, + type GetIssueByIdentifierQuery, + type GetIssueByIdQuery, + GetIssuesDocument, + type GetIssuesQuery, + type IssueCreateInput, + type IssueUpdateInput, + SearchIssuesDocument, + type SearchIssuesQuery, + UpdateIssueDocument, + type UpdateIssueMutation, +} from "../gql/graphql.js"; + +export async function listIssues( + client: GraphQLClient, + options: PaginationOptions = {}, +): Promise<PaginatedResult<Issue>> { + const { limit = 25, after } = options; + const result = await client.request<GetIssuesQuery>(GetIssuesDocument, { + first: limit, + after, + orderBy: "updatedAt", + }); + return { + nodes: result.issues?.nodes ?? [], + pageInfo: result.issues.pageInfo, + }; +} + +export async function getIssue( + client: GraphQLClient, + id: string, +): Promise<IssueDetail> { + const result = await client.request<GetIssueByIdQuery>(GetIssueByIdDocument, { + id, + }); + if (!result.issue) { + throw new Error(`Issue with ID "${id}" not found`); + } + return result.issue; +} + +export async function getIssueByIdentifier( + client: GraphQLClient, + teamKey: string, + issueNumber: number, +): Promise<IssueByIdentifier> { + const result = await client.request<GetIssueByIdentifierQuery>( + GetIssueByIdentifierDocument, + { teamKey, number: issueNumber }, + ); + if (!result.issues.nodes.length) { + throw new Error( + `Issue with identifier "${teamKey}-${issueNumber}" not found`, + ); + } + return result.issues.nodes[0]; +} + +export async function searchIssues( + client: GraphQLClient, + term: string, + options: PaginationOptions = {}, +): Promise<PaginatedResult<IssueSearchResult>> { + const { limit = 25, after } = options; + const result = await client.request<SearchIssuesQuery>(SearchIssuesDocument, { + term, + first: limit, + after, + }); + return { + nodes: result.searchIssues?.nodes ?? [], + pageInfo: result.searchIssues.pageInfo, + }; +} + +export async function createIssue( + client: GraphQLClient, + input: IssueCreateInput, +): Promise<CreatedIssue> { + const result = await client.request<CreateIssueMutation>( + CreateIssueDocument, + { input }, + ); + if (!result.issueCreate.success || !result.issueCreate.issue) { + throw new Error("Failed to create issue"); + } + return result.issueCreate.issue; +} + +export async function updateIssue( + client: GraphQLClient, + id: string, + input: IssueUpdateInput, +): Promise<UpdatedIssue> { + const result = await client.request<UpdateIssueMutation>( + UpdateIssueDocument, + { id, input }, + ); + if (!result.issueUpdate.success || !result.issueUpdate.issue) { + throw new Error("Failed to update issue"); + } + return result.issueUpdate.issue; +} diff --git a/src/services/label-service.ts b/src/services/label-service.ts new file mode 100644 index 0000000..c2e3f4d --- /dev/null +++ b/src/services/label-service.ts @@ -0,0 +1,35 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { PaginatedResult, PaginationOptions } from "../common/types.js"; +import { GetLabelsDocument, type GetLabelsQuery } from "../gql/graphql.js"; + +export interface Label { + id: string; + name: string; + color: string; + description?: string; +} + +export async function listLabels( + client: GraphQLClient, + teamId?: string, + options: PaginationOptions = {}, +): Promise<PaginatedResult<Label>> { + const { limit = 50, after } = options; + const filter = teamId ? { team: { id: { eq: teamId } } } : undefined; + + const result = await client.request<GetLabelsQuery>(GetLabelsDocument, { + first: limit, + after, + filter, + }); + + return { + nodes: result.issueLabels.nodes.map((label) => ({ + id: label.id, + name: label.name, + color: label.color, + description: label.description ?? undefined, + })), + pageInfo: result.issueLabels.pageInfo, + }; +} diff --git a/src/services/milestone-service.ts b/src/services/milestone-service.ts new file mode 100644 index 0000000..9a9081a --- /dev/null +++ b/src/services/milestone-service.ts @@ -0,0 +1,97 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { + CreatedMilestone, + MilestoneDetail, + MilestoneListItem, + PaginatedResult, + PaginationOptions, + UpdatedMilestone, +} from "../common/types.js"; +import { + CreateProjectMilestoneDocument, + type CreateProjectMilestoneMutation, + GetProjectMilestoneByIdDocument, + type GetProjectMilestoneByIdQuery, + ListProjectMilestonesDocument, + type ListProjectMilestonesQuery, + type ProjectMilestoneCreateInput, + type ProjectMilestoneUpdateInput, + UpdateProjectMilestoneDocument, + type UpdateProjectMilestoneMutation, +} from "../gql/graphql.js"; + +export async function listMilestones( + client: GraphQLClient, + projectId: string, + options: PaginationOptions = {}, +): Promise<PaginatedResult<MilestoneListItem>> { + const { limit = 50, after } = options; + const result = await client.request<ListProjectMilestonesQuery>( + ListProjectMilestonesDocument, + { projectId, first: limit, after }, + ); + + return { + nodes: result.project?.projectMilestones?.nodes ?? [], + pageInfo: result.project?.projectMilestones?.pageInfo ?? { + hasNextPage: false, + endCursor: null, + }, + }; +} + +export async function getMilestone( + client: GraphQLClient, + id: string, + issuesLimit?: number, +): Promise<MilestoneDetail> { + const result = await client.request<GetProjectMilestoneByIdQuery>( + GetProjectMilestoneByIdDocument, + { id, issuesFirst: issuesLimit }, + ); + + if (!result.projectMilestone) { + throw new Error(`Milestone with ID "${id}" not found`); + } + + return result.projectMilestone; +} + +export async function createMilestone( + client: GraphQLClient, + input: ProjectMilestoneCreateInput, +): Promise<CreatedMilestone> { + const result = await client.request<CreateProjectMilestoneMutation>( + CreateProjectMilestoneDocument, + { input }, + ); + + if ( + !result.projectMilestoneCreate.success || + !result.projectMilestoneCreate.projectMilestone + ) { + throw new Error("Failed to create milestone"); + } + + return result.projectMilestoneCreate.projectMilestone; +} + +export async function updateMilestone( + client: GraphQLClient, + id: string, + input: ProjectMilestoneUpdateInput, +): Promise<UpdatedMilestone> { + const result = await client.request<UpdateProjectMilestoneMutation>( + UpdateProjectMilestoneDocument, + { id, input }, + ); + + if ( + !result.projectMilestoneUpdate.success || + !result.projectMilestoneUpdate.projectMilestone + ) { + throw new Error("Failed to update milestone"); + } + + return result.projectMilestoneUpdate.projectMilestone; +} diff --git a/src/services/project-service.ts b/src/services/project-service.ts new file mode 100644 index 0000000..ec612bb --- /dev/null +++ b/src/services/project-service.ts @@ -0,0 +1,35 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { PaginatedResult, PaginationOptions } from "../common/types.js"; +import { GetProjectsDocument, type GetProjectsQuery } from "../gql/graphql.js"; + +export interface Project { + id: string; + name: string; + description: string; + state: string; + targetDate?: string; + slugId: string; +} + +export async function listProjects( + client: GraphQLClient, + options: PaginationOptions = {}, +): Promise<PaginatedResult<Project>> { + const { limit = 50, after } = options; + const result = await client.request<GetProjectsQuery>(GetProjectsDocument, { + first: limit, + after, + }); + + return { + nodes: result.projects.nodes.map((project) => ({ + id: project.id, + name: project.name, + description: project.description, + state: project.state, + targetDate: project.targetDate ?? undefined, + slugId: project.slugId, + })), + pageInfo: result.projects.pageInfo, + }; +} diff --git a/src/services/team-service.ts b/src/services/team-service.ts new file mode 100644 index 0000000..d58297e --- /dev/null +++ b/src/services/team-service.ts @@ -0,0 +1,24 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { PaginatedResult, PaginationOptions } from "../common/types.js"; +import { GetTeamsDocument, type GetTeamsQuery } from "../gql/graphql.js"; + +export interface Team { + id: string; + key: string; + name: string; +} + +export async function listTeams( + client: GraphQLClient, + options: PaginationOptions = {}, +): Promise<PaginatedResult<Team>> { + const { limit = 50, after } = options; + const result = await client.request<GetTeamsQuery>(GetTeamsDocument, { + first: limit, + after, + }); + return { + nodes: result.teams.nodes, + pageInfo: result.teams.pageInfo, + }; +} diff --git a/src/services/user-service.ts b/src/services/user-service.ts new file mode 100644 index 0000000..bb2a5e4 --- /dev/null +++ b/src/services/user-service.ts @@ -0,0 +1,30 @@ +import type { GraphQLClient } from "../client/graphql-client.js"; +import type { PaginatedResult, PaginationOptions } from "../common/types.js"; +import { GetUsersDocument, type GetUsersQuery } from "../gql/graphql.js"; + +export interface User { + id: string; + name: string; + email: string; + active: boolean; +} + +export async function listUsers( + client: GraphQLClient, + activeOnly: boolean = false, + options: PaginationOptions = {}, +): Promise<PaginatedResult<User>> { + const { limit = 50, after } = options; + const filter = activeOnly ? { active: { eq: true } } : undefined; + const result = await client.request<GetUsersQuery>(GetUsersDocument, { + first: limit, + after, + filter, + }); + + // Sort by name to match Linear SDK behavior + return { + nodes: result.users.nodes.sort((a, b) => a.name.localeCompare(b.name)), + pageInfo: result.users.pageInfo, + }; +} diff --git a/src/utils/auth.ts b/src/utils/auth.ts deleted file mode 100644 index 29b6571..0000000 --- a/src/utils/auth.ts +++ /dev/null @@ -1,50 +0,0 @@ -import fs from "fs"; -import path from "path"; -import os from "os"; - -export interface CommandOptions { - /** API token provided via command line flag */ - apiToken?: string; -} - -/** - * Get Linear API token from multiple sources in priority order: - * - * 1. `--api-token` command flag (highest priority) - * 2. `LINEAR_API_TOKEN` environment variable - * 3. `~/.linear_api_token` file (lowest priority) - * - * @param options - Command options containing potential apiToken - * @returns API token string - * @throws Error if no token is found in any source - * - * @example - * ```typescript - * // From command line flag - * const token1 = await getApiToken({ apiToken: "pat_..." }); - * - * // From environment or file - * const token2 = await getApiToken({}); - * ``` - */ -export async function getApiToken(options: CommandOptions): Promise<string> { - // Priority 1: Check --api-token flag - if (options.apiToken) { - return options.apiToken; - } - - // Priority 2: Check LINEAR_API_TOKEN environment variable - if (process.env.LINEAR_API_TOKEN) { - return process.env.LINEAR_API_TOKEN; - } - - // Priority 3: Read from ~/.linear_api_token file - const tokenFile = path.join(os.homedir(), ".linear_api_token"); - if (fs.existsSync(tokenFile)) { - return fs.readFileSync(tokenFile, "utf8").trim(); - } - - throw new Error( - "No API token found. Use --api-token, LINEAR_API_TOKEN env var, or ~/.linear_api_token file", - ); -} diff --git a/src/utils/embed-parser.ts b/src/utils/embed-parser.ts deleted file mode 100644 index d757390..0000000 --- a/src/utils/embed-parser.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Utility functions for extracting embedded file URLs from markdown content. - * Focuses on Linear's private cloud storage URLs (uploads.linear.app). - * - * This parser handles both image and link markdown syntax, filtering for - * Linear-specific URLs and calculating expiration times for signed URLs. - */ - -export interface EmbedInfo { - /** The alt text or link label from markdown */ - label: string; - /** The direct URL to the Linear uploaded file */ - url: string; - /** ISO timestamp when the signed URL expires (1 hour from generation) */ - expiresAt: string; -} - -/** - * Strips code contexts from markdown to prevent extracting embeds from code examples. - * - * Removes: - * - Escaped backticks (\`) - * - Fenced code blocks (```...```) - * - Inline code (`...`) - * - * @param content - Markdown content to clean - * @returns Content with all code contexts removed - */ -function stripCodeContexts(content: string): string { - // Remove escaped backticks - let cleaned = content.replace(/\\`/g, ""); - - // Remove fenced code blocks (```...```) - greedy match with dotall behavior - cleaned = cleaned.replace(/```[\s\S]*?```/g, ""); - - // Remove inline code (`...`) - cleaned = cleaned.replace(/`[^`]+`/g, ""); - - return cleaned; -} - -/** - * Extracts Linear upload URLs from markdown content. - * - * Parses both image syntax `![label](url)` and link syntax `[label](url)`. - * Only returns URLs from uploads.linear.app domain with calculated expiration times. - * - * Automatically strips code blocks and inline code to avoid extracting URLs from - * code examples or documentation. - * - * @param content - Markdown content to parse for embedded files - * @returns Array of embed information for Linear upload URLs - * - * @example - * ```typescript - * const content = "Check this screenshot ![test](https://uploads.linear.app/abc/file.png)"; - * const embeds = extractEmbeds(content); - * // Returns: [{ label: "test", url: "...", expiresAt: "2025-11-07T12:00:00.000Z" }] - * ``` - */ -export function extractEmbeds(content: string): EmbedInfo[] { - if (!content) { - return []; - } - - // Strip code contexts to avoid extracting URLs from code examples - const cleanedContent = stripCodeContexts(content); - - const embeds: EmbedInfo[] = []; - - // Regex for markdown image syntax: ![label](url) - const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; - - // Regex for markdown link syntax: [label](url) - const linkRegex = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g; - - // Calculate expiration time (1 hour from now) - const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString(); - - // Extract from image syntax - let match; - while ((match = imageRegex.exec(cleanedContent)) !== null) { - const label = match[1] || "file"; - const url = match[2]; - - if (isLinearUploadUrl(url)) { - embeds.push({ label, url, expiresAt }); - } - } - - // Extract from link syntax - while ((match = linkRegex.exec(cleanedContent)) !== null) { - const label = match[1] || "file"; - const url = match[2]; - - if (isLinearUploadUrl(url)) { - embeds.push({ label, url, expiresAt }); - } - } - - return embeds; -} - -/** - * Checks if a URL points to Linear's private cloud storage. - */ -export function isLinearUploadUrl(url: string): boolean { - if (!url) { - return false; - } - - try { - const urlObj = new URL(url); - return urlObj.hostname === "uploads.linear.app"; - } catch { - return false; - } -} - -/** - * Extracts the filename from a Linear upload URL. - * Used for default output filenames when downloading. - */ -export function extractFilenameFromUrl(url: string): string { - try { - const urlObj = new URL(url); - const pathname = urlObj.pathname; - const parts = pathname.split("/"); - return parts[parts.length - 1] || "download"; - } catch { - return "download"; - } -} diff --git a/src/utils/graphql-attachments-service.ts b/src/utils/graphql-attachments-service.ts deleted file mode 100644 index ed7bf03..0000000 --- a/src/utils/graphql-attachments-service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { GraphQLService, createGraphQLService } from "./graphql-service.js"; -import { CommandOptions } from "./auth.js"; -import { - CREATE_ATTACHMENT_MUTATION, - DELETE_ATTACHMENT_MUTATION, - LIST_ATTACHMENTS_QUERY, -} from "../queries/attachments.js"; -import { LinearAttachment, AttachmentCreateInput } from "./linear-types.js"; - -/** - * Attachment entity returned from GraphQL queries - * Re-exported from linear-types for convenience - */ -export type Attachment = LinearAttachment; - -/** - * GraphQL-optimized attachments service for single API call operations - * - * Attachments allow linking any URL to an issue. This is the mechanism - * to associate documents (or any external resource) with issues, since - * documents cannot be directly linked to issues in Linear's data model. - * - * Key behavior: Attachments are idempotent - creating an attachment with - * the same url + issueId will update the existing attachment. - */ -export class GraphQLAttachmentsService { - constructor(private graphqlService: GraphQLService) {} - - /** - * Create an attachment on an issue - * - * If an attachment with the same url and issueId already exists, - * the existing record is updated instead of creating a duplicate. - * - * @param input Attachment creation parameters - * @returns Created or updated attachment - */ - async createAttachment(input: AttachmentCreateInput): Promise<Attachment> { - const result = await this.graphqlService.rawRequest<{ - attachmentCreate: { success: boolean; attachment: Attachment }; - }>(CREATE_ATTACHMENT_MUTATION, { input }); - - if (!result.attachmentCreate.success) { - throw new Error( - `Failed to create attachment on issue ${input.issueId} for URL "${input.url}"`, - ); - } - - return result.attachmentCreate.attachment; - } - - /** - * Delete an attachment - * - * @param id Attachment ID - * @returns true if deletion was successful - * @throws Error if deletion fails - */ - async deleteAttachment(id: string): Promise<boolean> { - const result = await this.graphqlService.rawRequest<{ - attachmentDelete: { success: boolean }; - }>(DELETE_ATTACHMENT_MUTATION, { id }); - - if (!result.attachmentDelete.success) { - throw new Error(`Failed to delete attachment: ${id}`); - } - - return true; - } - - /** - * List attachments on an issue - * - * @param issueId Issue ID (UUID) - * @returns Array of attachments - * @throws Error if issue not found - */ - async listAttachments(issueId: string): Promise<Attachment[]> { - const result = await this.graphqlService.rawRequest<{ - issue: { attachments: { nodes: Attachment[] } } | null; - }>(LIST_ATTACHMENTS_QUERY, { issueId }); - - if (!result.issue) { - throw new Error(`Issue not found: ${issueId}`); - } - - return result.issue.attachments.nodes; - } -} - -/** - * Create GraphQLAttachmentsService instance with authentication - */ -export async function createGraphQLAttachmentsService( - options: CommandOptions, -): Promise<GraphQLAttachmentsService> { - const graphqlService = await createGraphQLService(options); - return new GraphQLAttachmentsService(graphqlService); -} diff --git a/src/utils/graphql-documents-service.ts b/src/utils/graphql-documents-service.ts deleted file mode 100644 index c08f864..0000000 --- a/src/utils/graphql-documents-service.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { GraphQLService, createGraphQLService } from "./graphql-service.js"; -import { CommandOptions } from "./auth.js"; -import { - CREATE_DOCUMENT_MUTATION, - UPDATE_DOCUMENT_MUTATION, - GET_DOCUMENT_QUERY, - LIST_DOCUMENTS_QUERY, - DELETE_DOCUMENT_MUTATION, -} from "../queries/documents.js"; -import { - LinearDocument, - DocumentCreateInput, - DocumentUpdateInput, -} from "./linear-types.js"; - -/** - * Document entity returned from GraphQL queries - * Re-exported from linear-types for convenience - */ -export type Document = LinearDocument; - -/** - * GraphQL-optimized documents service for single API call operations - * - * Documents in Linear are standalone entities that can be associated with - * projects, initiatives, or teams. They cannot be directly linked to issues. - * To link a document to an issue, use the attachments API. - */ -export class GraphQLDocumentsService { - constructor(private graphqlService: GraphQLService) {} - - /** - * Create a new document - * - * @param input Document creation parameters - * @returns Created document with all fields - */ - async createDocument(input: DocumentCreateInput): Promise<Document> { - const result = await this.graphqlService.rawRequest<{ - documentCreate: { success: boolean; document: Document }; - }>(CREATE_DOCUMENT_MUTATION, { input }); - - if (!result.documentCreate.success) { - throw new Error( - `Failed to create document "${input.title}"${input.projectId ? ` in project ${input.projectId}` : ""}${input.teamId ? ` for team ${input.teamId}` : ""}`, - ); - } - - return result.documentCreate.document; - } - - /** - * Update an existing document - * - * @param id Document ID (UUID or slug) - * @param input Update parameters (only provided fields are updated) - * @returns Updated document with all fields - */ - async updateDocument( - id: string, - input: DocumentUpdateInput, - ): Promise<Document> { - const result = await this.graphqlService.rawRequest<{ - documentUpdate: { success: boolean; document: Document }; - }>(UPDATE_DOCUMENT_MUTATION, { id, input }); - - if (!result.documentUpdate.success) { - throw new Error(`Failed to update document: ${id}`); - } - - return result.documentUpdate.document; - } - - /** - * Get a single document by ID - * - * @param id Document ID (UUID or slug) - * @returns Document with all fields - * @throws Error if document not found - */ - async getDocument(id: string): Promise<Document> { - const result = await this.graphqlService.rawRequest<{ - document: Document | null; - }>(GET_DOCUMENT_QUERY, { id }); - - if (!result.document) { - throw new Error(`Document not found: ${id}`); - } - - return result.document; - } - - /** - * List documents with optional filtering - * - * @param options Filter and pagination options - * @returns Array of documents - */ - async listDocuments(options?: { - projectId?: string; - first?: number; - }): Promise<Document[]> { - const filter = options?.projectId - ? { project: { id: { eq: options.projectId } } } - : undefined; - - const result = await this.graphqlService.rawRequest<{ - documents: { nodes: Document[] }; - }>(LIST_DOCUMENTS_QUERY, { - first: options?.first ?? 50, - filter, - }); - - return result.documents.nodes; - } - - /** - * Delete (trash) a document - * - * This is a soft delete - the document is moved to trash. - * - * @param id Document ID - * @returns true if deletion was successful - * @throws Error if deletion fails - */ - async deleteDocument(id: string): Promise<boolean> { - const result = await this.graphqlService.rawRequest<{ - documentDelete: { success: boolean }; - }>(DELETE_DOCUMENT_MUTATION, { id }); - - if (!result.documentDelete.success) { - throw new Error(`Failed to delete document: ${id}`); - } - - return true; - } - - /** - * List documents by their slug IDs - * - * Used for batch-fetching documents, e.g., when retrieving documents - * linked to an issue via URL attachments. - * - * @param slugIds Array of document slug IDs (the short ID at the end of document URLs) - * @param limit Maximum number of documents to return - * @returns Array of documents (may be fewer if some slugIds don't exist or exceed limit) - */ - async listDocumentsBySlugIds( - slugIds: string[], - limit?: number, - ): Promise<Document[]> { - if (slugIds.length === 0) { - return []; - } - - const filter = { - or: slugIds.map((slugId) => ({ slugId: { eq: slugId } })), - }; - - const result = await this.graphqlService.rawRequest<{ - documents: { nodes: Document[] }; - }>(LIST_DOCUMENTS_QUERY, { - first: limit ?? slugIds.length, - filter, - }); - - return result.documents.nodes; - } -} - -/** - * Create GraphQLDocumentsService instance with authentication - */ -export async function createGraphQLDocumentsService( - options: CommandOptions, -): Promise<GraphQLDocumentsService> { - const graphqlService = await createGraphQLService(options); - return new GraphQLDocumentsService(graphqlService); -} diff --git a/src/utils/graphql-issues-service.ts b/src/utils/graphql-issues-service.ts deleted file mode 100644 index 6a5e1fe..0000000 --- a/src/utils/graphql-issues-service.ts +++ /dev/null @@ -1,911 +0,0 @@ -import { GraphQLService } from "./graphql-service.js"; -import { LinearService } from "./linear-service.js"; -import { - BATCH_RESOLVE_FOR_CREATE_QUERY, - BATCH_RESOLVE_FOR_SEARCH_QUERY, - BATCH_RESOLVE_FOR_UPDATE_QUERY, - CREATE_ISSUE_MUTATION, - FILTERED_SEARCH_ISSUES_QUERY, - GET_ISSUE_BY_ID_QUERY, - GET_ISSUE_BY_IDENTIFIER_QUERY, - GET_ISSUES_QUERY, - SEARCH_ISSUES_QUERY, - UPDATE_ISSUE_MUTATION, -} from "../queries/issues.js"; -import type { - CreateIssueArgs, - LinearIssue, - SearchIssuesArgs, - UpdateIssueArgs, -} from "./linear-types.js"; -import { extractEmbeds } from "./embed-parser.js"; -import { isUuid } from "./uuid.js"; -import { - parseIssueIdentifier, - tryParseIssueIdentifier, -} from "./identifier-parser.js"; - -/** - * GraphQL-optimized issues service for single API call operations - */ -export class GraphQLIssuesService { - constructor( - private graphQLService: GraphQLService, - private linearService: LinearService, - ) {} - - /** - * Get issues list with all relationships in single query - * Reduces from 1 + (5 × N issues) API calls to 1 API call - */ - async getIssues(limit: number = 25): Promise<LinearIssue[]> { - const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, { - first: limit, - orderBy: "updatedAt" as any, - }); - - if (!result.issues?.nodes) { - return []; - } - - // Transform all issues using the same transformation logic - return result.issues.nodes.map((issue: any) => - this.transformIssueData(issue) - ); - } - - /** - * Get issue by ID with all relationships and comments in single query - * Reduces from 7 API calls to 1 API call - * - * @param issueId - Either a UUID string or TEAM-123 format identifier - * @returns Complete issue data with all relationships resolved - * @throws Error if issue is not found - * - * @example - * ```typescript - * // Using UUID - * const issue1 = await getIssueById("123e4567-e89b-12d3-a456-426614174000"); - * - * // Using TEAM-123 format - * const issue2 = await getIssueById("ABC-123"); - * ``` - */ - async getIssueById(issueId: string): Promise<LinearIssue> { - let issueData; - - if (isUuid(issueId)) { - // Direct UUID lookup - const result = await this.graphQLService.rawRequest( - GET_ISSUE_BY_ID_QUERY, - { - id: issueId, - }, - ); - - if (!result.issue) { - throw new Error(`Issue with ID "${issueId}" not found`); - } - issueData = result.issue; - } else { - // Parse identifier (ABC-123 format) - const { teamKey, issueNumber } = parseIssueIdentifier(issueId); - - const result = await this.graphQLService.rawRequest( - GET_ISSUE_BY_IDENTIFIER_QUERY, - { - teamKey, - number: issueNumber, - }, - ); - - if (!result.issues.nodes.length) { - throw new Error(`Issue with identifier "${issueId}" not found`); - } - issueData = result.issues.nodes[0]; - } - - // Transform GraphQL response to LinearIssue format - return this.transformIssueData(issueData); - } - - /** - * Update issue with all relationships in optimized GraphQL queries - * Reduces from 5 API calls to 2 API calls (resolve + update) - * - * @param args Update arguments (supports label names and handles adding vs overwriting modes) - * @param labelMode How to handle labels: 'adding' (merge with existing) or 'overwriting' (replace all) - * @returns Updated issue with all relationships resolved - * - * @example - * ```typescript - * const updatedIssue = await updateIssue( - * { - * id: "ABC-123", - * title: "New Title", - * labels: ["Bug", "High Priority"] - * }, - * "adding" - * ); - * ``` - */ - async updateIssue( - args: UpdateIssueArgs, - labelMode: "adding" | "overwriting" = "overwriting", - ): Promise<LinearIssue> { - let resolvedIssueId = args.id; - let currentIssueLabels: string[] = []; - - // Step 1: Batch resolve all IDs and get current issue data if needed - const resolveVariables: any = {}; - - // Parse issue ID if it's an identifier - if (!isUuid(args.id)) { - const { teamKey, issueNumber } = parseIssueIdentifier(args.id); - resolveVariables.teamKey = teamKey; - resolveVariables.issueNumber = issueNumber; - } - - // Add label names for resolution if provided - if (args.labelIds && Array.isArray(args.labelIds)) { - // Filter out UUIDs and collect label names for resolution - const labelNames = args.labelIds.filter((id) => !isUuid(id)); - if (labelNames.length > 0) { - resolveVariables.labelNames = labelNames; - } - } - - // Add project name for resolution if provided and not a UUID - if (args.projectId && !isUuid(args.projectId)) { - resolveVariables.projectName = args.projectId; - } - - // Add milestone name for resolution if provided and not a UUID - if ( - args.milestoneId && typeof args.milestoneId === "string" && - !isUuid(args.milestoneId) - ) { - resolveVariables.milestoneName = args.milestoneId; - } - - // Execute batch resolve query - const resolveResult = await this.graphQLService.rawRequest( - BATCH_RESOLVE_FOR_UPDATE_QUERY, - resolveVariables, - ); - - // Process resolution results - if (!isUuid(args.id)) { - if (!resolveResult.issues.nodes.length) { - throw new Error(`Issue with identifier "${args.id}" not found`); - } - resolvedIssueId = resolveResult.issues.nodes[0].id; - currentIssueLabels = resolveResult.issues.nodes[0].labels.nodes.map(( - l: any, - ) => l.id); - } - - // Resolve label IDs - let finalLabelIds = args.labelIds; - if (args.labelIds && Array.isArray(args.labelIds)) { - const resolvedLabels: string[] = []; - - // Process each label ID/name - for (const labelIdOrName of args.labelIds) { - if (isUuid(labelIdOrName)) { - resolvedLabels.push(labelIdOrName); - } else { - // Find resolved label - const label = resolveResult.labels.nodes.find((l: any) => - l.name === labelIdOrName - ); - if (!label) { - throw new Error(`Label "${labelIdOrName}" not found`); - } - resolvedLabels.push(label.id); - } - } - - // Handle adding vs overwriting modes - if (labelMode === "adding") { - // Merge with current labels (if we have them) - finalLabelIds = [ - ...new Set([...currentIssueLabels, ...resolvedLabels]), - ]; - } else { - // Overwrite mode - replace all existing labels - finalLabelIds = resolvedLabels; - } - } - - // Resolve project ID - let finalProjectId = args.projectId; - if (args.projectId && !isUuid(args.projectId)) { - if (!resolveResult.projects.nodes.length) { - throw new Error(`Project "${args.projectId}" not found`); - } - finalProjectId = resolveResult.projects.nodes[0].id; - } - - // Resolve milestone ID if provided and not a UUID - let finalMilestoneId = args.milestoneId; - if ( - args.milestoneId && typeof args.milestoneId === "string" && - !isUuid(args.milestoneId) - ) { - // First try to find milestone in project being set (if --project is provided) - // IMPORTANT: Only check resolveResult.projects if we actually asked for a project - // (the batch query may return unrelated project data when projectName is undefined) - if ( - args.projectId && - resolveResult.projects?.nodes[0]?.projectMilestones?.nodes - ) { - const projectMilestone = resolveResult.projects.nodes[0] - .projectMilestones.nodes - .find((m: any) => m.name === args.milestoneId); - if (projectMilestone) { - finalMilestoneId = projectMilestone.id; - } - } - - // If not found in project being set, try the issue's current project - if ( - finalMilestoneId && !isUuid(finalMilestoneId) && - resolveResult.issues?.nodes[0]?.project?.projectMilestones?.nodes - ) { - const issueMilestone = resolveResult.issues.nodes[0].project - .projectMilestones.nodes - .find((m: any) => m.name === args.milestoneId); - if (issueMilestone) { - finalMilestoneId = issueMilestone.id; - } - } - - // If still not found, try global milestone lookup (may be ambiguous) - if ( - finalMilestoneId && !isUuid(finalMilestoneId) && - resolveResult.milestones?.nodes?.length - ) { - finalMilestoneId = resolveResult.milestones.nodes[0].id; - } - - if (!finalMilestoneId || !isUuid(finalMilestoneId)) { - throw new Error(`Milestone "${args.milestoneId}" not found`); - } - } - - // Resolve cycle ID if provided (supports name resolution scoped to the issue's team) - let finalCycleId = args.cycleId; - if (args.cycleId !== undefined && args.cycleId !== null) { - if (args.cycleId === null) { - finalCycleId = null; // explicit clear - } else if (typeof args.cycleId === "string" && !isUuid(args.cycleId)) { - // Try to get team context from resolved issue (if available) - let teamIdForCycle: string | undefined = resolveResult.issues?.nodes - ?.[0]?.team?.id; - - // If we don't have team from batch result but we have resolvedIssueId, fetch issue team - if (!teamIdForCycle && resolvedIssueId && isUuid(resolvedIssueId)) { - const issueTeamRes = await this.graphQLService.rawRequest( - `query GetIssueTeam($issueId: String!) { issue(id: $issueId) { team { id } } }`, - { issueId: resolvedIssueId }, - ); - teamIdForCycle = issueTeamRes.issue?.team?.id; - } - - // Try scoped lookup by team first - if (teamIdForCycle) { - const scopedRes = await this.graphQLService.rawRequest( - `query FindCycleScoped($name: String!, $teamId: ID!) { cycles(filter: { and: [ { name: { eq: $name } }, { team: { id: { eq: $teamId } } } ] }, first: 10) { nodes { id name number startsAt isActive isNext isPrevious team { id key } } } }`, - { name: args.cycleId, teamId: teamIdForCycle }, - ); - const scopedNodes = scopedRes.cycles?.nodes || []; - if (scopedNodes.length === 1) { - finalCycleId = scopedNodes[0].id; - } else if (scopedNodes.length > 1) { - // prefer active, next, previous - let chosen = scopedNodes.find((n: any) => n.isActive) || - scopedNodes.find((n: any) => n.isNext) || - scopedNodes.find((n: any) => n.isPrevious); - if (chosen) finalCycleId = chosen.id; - else {throw new Error( - `Ambiguous cycle name "${args.cycleId}" for team ${teamIdForCycle}. Use ID or disambiguate.`, - );} - } - } - - // Fallback to global lookup by name - if (!finalCycleId) { - const globalRes = await this.graphQLService.rawRequest( - `query FindCycleGlobal($name: String!) { cycles(filter: { name: { eq: $name } }, first: 10) { nodes { id name number startsAt isActive isNext isPrevious team { id key } } } }`, - { name: args.cycleId }, - ); - const globalNodes = globalRes.cycles?.nodes || []; - if (globalNodes.length === 1) { - finalCycleId = globalNodes[0].id; - } else if (globalNodes.length > 1) { - let chosen = globalNodes.find((n: any) => n.isActive) || - globalNodes.find((n: any) => n.isNext) || - globalNodes.find((n: any) => n.isPrevious); - if (chosen) finalCycleId = chosen.id; - else {throw new Error( - `Ambiguous cycle name "${args.cycleId}" — multiple matches found across teams. Use ID or scope with team.`, - );} - } - } - - if (!finalCycleId) { - throw new Error(`Cycle "${args.cycleId}" not found`); - } - } - } - - // Resolve status ID if provided and not a UUID - let resolvedStatusId = args.statusId; - if (args.statusId && !isUuid(args.statusId)) { - // Get team ID from the issue for status context - let teamId: string | undefined; - if (resolvedIssueId && isUuid(resolvedIssueId)) { - // We have the resolved issue ID, get the team context - const issueResult = await this.graphQLService.rawRequest( - `query GetIssueTeam($issueId: String!) { - issue(id: $issueId) { - team { id } - } - }`, - { issueId: resolvedIssueId }, - ); - teamId = issueResult.issue?.team?.id; - } - resolvedStatusId = await this.linearService.resolveStatusId( - args.statusId, - teamId, - ); - } - - // Step 2: Execute update mutation with resolved IDs - const updateInput: any = {}; - - if (args.title !== undefined) updateInput.title = args.title; - if (args.description !== undefined) { - updateInput.description = args.description; - } - if (resolvedStatusId !== undefined) updateInput.stateId = resolvedStatusId; - if (args.priority !== undefined) updateInput.priority = args.priority; - if (args.assigneeId !== undefined) { - updateInput.assigneeId = args.assigneeId; - } - if (finalProjectId !== undefined) updateInput.projectId = finalProjectId; - if (finalCycleId !== undefined) updateInput.cycleId = finalCycleId; - if (args.estimate !== undefined) updateInput.estimate = args.estimate; - if (args.parentId !== undefined) updateInput.parentId = args.parentId; - if (finalMilestoneId !== undefined) { - updateInput.projectMilestoneId = finalMilestoneId; - } - if (finalLabelIds !== undefined) { - updateInput.labelIds = finalLabelIds; - } - - const updateResult = await this.graphQLService.rawRequest( - UPDATE_ISSUE_MUTATION, - { - id: resolvedIssueId, - input: updateInput, - }, - ); - - if (!updateResult.issueUpdate.success) { - throw new Error("Failed to update issue"); - } - - if (!updateResult.issueUpdate.issue) { - throw new Error("Failed to retrieve updated issue"); - } - - return this.transformIssueData(updateResult.issueUpdate.issue); - } - - /** - * Create issue with all relationships in optimized GraphQL queries - * Reduces from 7+ API calls to 2 API calls (resolve + create) - * - * @param args Create arguments (supports team names, project names, label names, parent identifiers) - */ - async createIssue(args: CreateIssueArgs): Promise<LinearIssue> { - // Step 1: Batch resolve all IDs - const resolveVariables: any = {}; - - // Parse team if not a UUID - if (args.teamId && !isUuid(args.teamId)) { - // Check if it looks like a team key (short, usually 2-5 chars, alphanumeric) - const isTeamKey = args.teamId.length <= 5 && - /^[A-Z0-9]+$/i.test(args.teamId); - // IMPORTANT: Must explicitly set both teamKey and teamName (one to value, one to null) - // Linear's GraphQL `or` filter with undefined variables matches incorrectly - if (isTeamKey) { - resolveVariables.teamKey = args.teamId; - resolveVariables.teamName = null; - } else { - resolveVariables.teamKey = null; - resolveVariables.teamName = args.teamId; - } - } - - // Add project name for resolution if provided and not a UUID - if (args.projectId && !isUuid(args.projectId)) { - resolveVariables.projectName = args.projectId; - } - - // Add milestone name for resolution if provided and not a UUID - if (args.milestoneId && !isUuid(args.milestoneId)) { - resolveVariables.milestoneName = args.milestoneId; - } - - // Add label names for resolution if provided - if (args.labelIds && Array.isArray(args.labelIds)) { - // Filter out UUIDs and collect label names for resolution - const labelNames = args.labelIds.filter((id) => !isUuid(id)); - if (labelNames.length > 0) { - resolveVariables.labelNames = labelNames; - } - } - - // Parse parent issue identifier if provided - // Uses tryParseIssueIdentifier to silently handle invalid formats (parent will be ignored) - if (args.parentId && !isUuid(args.parentId)) { - const parentParsed = tryParseIssueIdentifier(args.parentId); - if (parentParsed) { - resolveVariables.parentTeamKey = parentParsed.teamKey; - resolveVariables.parentIssueNumber = parentParsed.issueNumber; - } - } - - // Execute batch resolve query if we have anything to resolve - let resolveResult: any = {}; - if (Object.keys(resolveVariables).length > 0) { - resolveResult = await this.graphQLService.rawRequest( - BATCH_RESOLVE_FOR_CREATE_QUERY, - resolveVariables, - ); - } - - // Resolve team ID - let finalTeamId = args.teamId; - if (args.teamId && !isUuid(args.teamId)) { - const resolvedTeam = resolveResult.teams?.nodes?.[0]; - // Validate the returned team actually matches the requested identifier - // (GraphQL `or` filter with undefined variables matches anything) - if ( - !resolvedTeam || - (resolvedTeam.key.toUpperCase() !== args.teamId.toUpperCase() && - resolvedTeam.name.toLowerCase() !== args.teamId.toLowerCase()) - ) { - throw new Error(`Team "${args.teamId}" not found`); - } - finalTeamId = resolvedTeam.id; - } else if (!finalTeamId) { - // If no team specified, we'll let Linear's default behavior handle it - // or the API will return an error - } - - // Resolve project ID - let finalProjectId = args.projectId; - if (args.projectId && !isUuid(args.projectId)) { - if (!resolveResult.projects?.nodes?.length) { - throw new Error(`Project "${args.projectId}" not found`); - } - finalProjectId = resolveResult.projects.nodes[0].id; - } - - // Resolve label IDs - let finalLabelIds = args.labelIds; - if (args.labelIds && Array.isArray(args.labelIds)) { - const resolvedLabels: string[] = []; - - for (const labelIdOrName of args.labelIds) { - if (isUuid(labelIdOrName)) { - resolvedLabels.push(labelIdOrName); - } else { - // Find resolved label - const label = resolveResult.labels?.nodes?.find((l: any) => - l.name === labelIdOrName - ); - if (!label) { - throw new Error(`Label "${labelIdOrName}" not found`); - } - resolvedLabels.push(label.id); - } - } - - finalLabelIds = resolvedLabels; - } - - // Resolve parent ID - let finalParentId = args.parentId; - if (args.parentId && !isUuid(args.parentId)) { - if (!resolveResult.parentIssues?.nodes?.length) { - throw new Error(`Parent issue "${args.parentId}" not found`); - } - finalParentId = resolveResult.parentIssues.nodes[0].id; - } - - // Resolve milestone ID if provided and not a UUID - let finalMilestoneId = args.milestoneId; - if (args.milestoneId && !isUuid(args.milestoneId)) { - // Try to find milestone in project context (milestones must be in same project as issue) - if (resolveResult.projects?.nodes[0]?.projectMilestones?.nodes) { - const projectMilestone = resolveResult.projects.nodes[0] - .projectMilestones.nodes - .find((m: any) => m.name === args.milestoneId); - if (projectMilestone) { - finalMilestoneId = projectMilestone.id; - } - } - - // If not found in project context, try global milestone lookup (may fail if wrong project) - if (!finalMilestoneId && resolveResult.milestones?.nodes?.length) { - finalMilestoneId = resolveResult.milestones.nodes[0].id; - } - - if (!finalMilestoneId) { - const hint = finalProjectId - ? ` in project` - : ` (consider specifying --project)`; - throw new Error(`Milestone "${args.milestoneId}" not found${hint}`); - } - } - - // Resolve cycle ID if provided (supports name resolution scoped to team) - let finalCycleId = args.cycleId; - if ( - args.cycleId && typeof args.cycleId === "string" && !isUuid(args.cycleId) - ) { - // Try scoped lookup within finalTeamId first - if (finalTeamId) { - const scopedRes = await this.graphQLService.rawRequest( - `query FindCycleScoped($name: String!, $teamId: ID!) { cycles(filter: { and: [ { name: { eq: $name } }, { team: { id: { eq: $teamId } } } ] }, first: 1) { nodes { id name } } }`, - { name: args.cycleId, teamId: finalTeamId }, - ); - if (scopedRes.cycles?.nodes?.length) { - finalCycleId = scopedRes.cycles.nodes[0].id; - } - } - - // Fallback to global lookup by name - if (!finalCycleId) { - const globalRes = await this.graphQLService.rawRequest( - `query FindCycleGlobal($name: String!) { cycles(filter: { name: { eq: $name } }, first: 1) { nodes { id name } } }`, - { name: args.cycleId }, - ); - if (globalRes.cycles?.nodes?.length) { - finalCycleId = globalRes.cycles.nodes[0].id; - } - } - - if (!finalCycleId) { - throw new Error(`Cycle "${args.cycleId}" not found`); - } - } - - // Resolve status ID if provided and not a UUID - let resolvedStatusId = args.statusId; - if (args.statusId && !isUuid(args.statusId)) { - resolvedStatusId = await this.linearService.resolveStatusId( - args.statusId, - finalTeamId, - ); - } - - // Step 2: Execute create mutation with resolved IDs - const createInput: any = { - title: args.title, - }; - - if (finalTeamId) createInput.teamId = finalTeamId; - if (args.description) createInput.description = args.description; - if (args.assigneeId) createInput.assigneeId = args.assigneeId; - if (args.priority !== undefined) createInput.priority = args.priority; - if (finalProjectId) createInput.projectId = finalProjectId; - if (resolvedStatusId) createInput.stateId = resolvedStatusId; - if (finalLabelIds && finalLabelIds.length > 0) { - createInput.labelIds = finalLabelIds; - } - if (args.estimate !== undefined) createInput.estimate = args.estimate; - if (finalParentId) createInput.parentId = finalParentId; - if (finalMilestoneId) createInput.projectMilestoneId = finalMilestoneId; - if (finalCycleId) createInput.cycleId = finalCycleId; - - const createResult = await this.graphQLService.rawRequest( - CREATE_ISSUE_MUTATION, - { - input: createInput, - }, - ); - - if (!createResult.issueCreate.success) { - throw new Error("Failed to create issue"); - } - - if (!createResult.issueCreate.issue) { - throw new Error("Failed to retrieve created issue"); - } - - return this.transformIssueData(createResult.issueCreate.issue); - } - - /** - * Search issues with all relationships in optimized GraphQL queries - * Reduces from 1 + (6 × N) API calls to 1-2 API calls total - * - * @param args Search arguments with optional filters - */ - async searchIssues(args: SearchIssuesArgs): Promise<LinearIssue[]> { - // Step 1: Resolve filter IDs if needed - const resolveVariables: any = {}; - let needsResolve = false; - - // Parse team if not a UUID - if (args.teamId && !isUuid(args.teamId)) { - needsResolve = true; - // Check if it looks like a team key (short, usually 2-5 chars, alphanumeric) - // IMPORTANT: Must explicitly set both teamKey and teamName (one to value, one to null) - // Linear's GraphQL `or` filter with undefined variables matches incorrectly - if (args.teamId.length <= 5 && /^[A-Z0-9]+$/i.test(args.teamId)) { - resolveVariables.teamKey = args.teamId; - resolveVariables.teamName = null; - } else { - resolveVariables.teamKey = null; - resolveVariables.teamName = args.teamId; - } - } - - // Add project name for resolution if provided and not a UUID - if (args.projectId && !isUuid(args.projectId)) { - needsResolve = true; - resolveVariables.projectName = args.projectId; - } - - // Add assignee email for resolution if provided and not a UUID - if (args.assigneeId && !isUuid(args.assigneeId)) { - needsResolve = true; - // Assume it's an email if it contains @ - if (args.assigneeId.includes("@")) { - resolveVariables.assigneeEmail = args.assigneeId; - } - } - - // Execute batch resolve query if we have anything to resolve - let resolveResult: any = {}; - if (needsResolve) { - resolveResult = await this.graphQLService.rawRequest( - BATCH_RESOLVE_FOR_SEARCH_QUERY, - resolveVariables, - ); - } - - // Resolve filter IDs - let finalTeamId = args.teamId; - if (args.teamId && !isUuid(args.teamId)) { - const resolvedTeam = resolveResult.teams?.nodes?.[0]; - // Validate the returned team actually matches the requested identifier - // (GraphQL `or` filter with undefined variables matches anything) - if ( - !resolvedTeam || - (resolvedTeam.key.toUpperCase() !== args.teamId.toUpperCase() && - resolvedTeam.name.toLowerCase() !== args.teamId.toLowerCase()) - ) { - throw new Error(`Team "${args.teamId}" not found`); - } - finalTeamId = resolvedTeam.id; - } - - let finalProjectId = args.projectId; - if (args.projectId && !isUuid(args.projectId)) { - if (!resolveResult.projects?.nodes?.length) { - throw new Error(`Project "${args.projectId}" not found`); - } - finalProjectId = resolveResult.projects.nodes[0].id; - } - - let finalAssigneeId = args.assigneeId; - if ( - args.assigneeId && !isUuid(args.assigneeId) && - args.assigneeId.includes("@") - ) { - if (!resolveResult.users?.nodes?.length) { - throw new Error(`User "${args.assigneeId}" not found`); - } - finalAssigneeId = resolveResult.users.nodes[0].id; - } - - // Step 2: Execute search query - if (args.query) { - // Use text search - const searchResult = await this.graphQLService.rawRequest( - SEARCH_ISSUES_QUERY, - { - term: args.query, - first: args.limit || 10, - }, - ); - - if (!searchResult.searchIssues?.nodes) { - return []; - } - - let results = searchResult.searchIssues.nodes.map((issue: any) => - this.transformIssueData(issue) - ); - - // Apply additional filters if provided - if (finalTeamId) { - results = results.filter((issue: LinearIssue) => - issue.team.id === finalTeamId - ); - } - if (finalAssigneeId) { - results = results.filter((issue: LinearIssue) => - issue.assignee?.id === finalAssigneeId - ); - } - if (finalProjectId) { - results = results.filter((issue: LinearIssue) => - issue.project?.id === finalProjectId - ); - } - if (args.status && args.status.length > 0) { - results = results.filter((issue: LinearIssue) => - args.status!.includes(issue.state.name) - ); - } - - return results; - } else { - // Use filtered search - const filter: any = {}; - - if (finalTeamId) filter.team = { id: { eq: finalTeamId } }; - if (finalAssigneeId) filter.assignee = { id: { eq: finalAssigneeId } }; - if (finalProjectId) filter.project = { id: { eq: finalProjectId } }; - if (args.status && args.status.length > 0) { - filter.state = { name: { in: args.status } }; - } - - const searchResult = await this.graphQLService.rawRequest( - FILTERED_SEARCH_ISSUES_QUERY, - { - first: args.limit || 10, - filter: Object.keys(filter).length > 0 ? filter : undefined, - orderBy: "updatedAt" as any, - }, - ); - - if (!searchResult.issues?.nodes) { - return []; - } - - return searchResult.issues.nodes.map((issue: any) => - this.transformIssueData(issue) - ); - } - } - - /** - * Transform GraphQL issue response to LinearIssue format - */ - private transformIssueData(issue: any): LinearIssue { - try { - return this.doTransformIssueData(issue); - } catch (error: any) { - // Diagnostic output: dump raw API response to help debug null field issues - // See: https://github.com/czottmann/linearis/issues/6 - const diagnostic = { - error: error.message, - stack: error.stack, - rawIssueData: issue, - }; - console.error( - "\n[DEBUG] Issue transform failed. Raw API response:\n" + - JSON.stringify(diagnostic, null, 2) + - "\n\nPlease report this output at: https://github.com/czottmann/linearis/issues\n", - ); - throw error; - } - } - - /** - * Internal transform implementation - */ - private doTransformIssueData(issue: any): LinearIssue { - return { - id: issue.id, - identifier: issue.identifier, - title: issue.title, - description: issue.description || undefined, - branchName: issue.branchName || undefined, - embeds: issue.description ? extractEmbeds(issue.description) : undefined, - state: { - id: issue.state.id, - name: issue.state.name, - }, - assignee: issue.assignee - ? { - id: issue.assignee.id, - name: issue.assignee.name, - } - : undefined, - team: { - id: issue.team.id, - key: issue.team.key, - name: issue.team.name, - }, - project: issue.project - ? { - id: issue.project.id, - name: issue.project.name, - } - : undefined, - cycle: issue.cycle - ? { - id: issue.cycle.id, - name: issue.cycle.name, - number: issue.cycle.number, - } - : undefined, - projectMilestone: issue.projectMilestone - ? { - id: issue.projectMilestone.id, - name: issue.projectMilestone.name, - targetDate: issue.projectMilestone.targetDate || undefined, - } - : undefined, - priority: issue.priority, - estimate: issue.estimate || undefined, - labels: issue.labels.nodes.map((label: any) => ({ - id: label.id, - name: label.name, - })), - parentIssue: issue.parent - ? { - id: issue.parent.id, - identifier: issue.parent.identifier, - title: issue.parent.title, - } - : undefined, - subIssues: issue.children?.nodes.map((child: any) => ({ - id: child.id, - identifier: child.identifier, - title: child.title, - })) || undefined, - comments: issue.comments?.nodes.map((comment: any) => ({ - id: comment.id, - body: comment.body, - embeds: extractEmbeds(comment.body), - user: { - id: comment.user.id, - name: comment.user.name, - }, - createdAt: comment.createdAt instanceof Date - ? comment.createdAt.toISOString() - : (comment.createdAt - ? new Date(comment.createdAt).toISOString() - : new Date().toISOString()), - updatedAt: comment.updatedAt instanceof Date - ? comment.updatedAt.toISOString() - : (comment.updatedAt - ? new Date(comment.updatedAt).toISOString() - : new Date().toISOString()), - })) || [], - createdAt: issue.createdAt instanceof Date - ? issue.createdAt.toISOString() - : (issue.createdAt - ? new Date(issue.createdAt).toISOString() - : new Date().toISOString()), - updatedAt: issue.updatedAt instanceof Date - ? issue.updatedAt.toISOString() - : (issue.updatedAt - ? new Date(issue.updatedAt).toISOString() - : new Date().toISOString()), - }; - } -} diff --git a/src/utils/graphql-service.ts b/src/utils/graphql-service.ts deleted file mode 100644 index fff8adf..0000000 --- a/src/utils/graphql-service.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { LinearClient } from "@linear/sdk"; -import { CommandOptions, getApiToken } from "./auth.js"; - -/** - * GraphQL service wrapper around LinearGraphQLClient - * - * Provides optimized direct GraphQL queries with error handling matching LinearService. - * This service enables single-query operations with batch resolving to eliminate - * the N+1 query problem common with the Linear SDK. - * - * Features: - * - Direct GraphQL query execution - * - 1-hour signed URL generation for file downloads - * - Consistent error handling patterns - * - Batch query capabilities - */ -export class GraphQLService { - private graphQLClient: any; - private client: LinearClient; - - /** - * Initialize GraphQL service with authentication - * - * @param apiToken - Linear API token for authentication - */ - constructor(apiToken: string) { - this.client = new LinearClient({ - apiKey: apiToken, - headers: { - "public-file-urls-expire-in": "3600", // 1 hour expiry for signed URLs - }, - }); - this.graphQLClient = this.client.client; - } - - /** - * Execute a raw GraphQL query with error handling - * - * @param query - GraphQL query string - * @param variables - Optional query variables - * @returns Query response data - * @throws Error with descriptive message for GraphQL errors - * - * @example - * ```typescript - * const result = await graphqlService.rawRequest( - * GET_ISSUES_QUERY, - * { first: 10 } - * ); - * ``` - */ - async rawRequest<T = any>(query: string, variables?: any): Promise<T> { - try { - const response = await this.graphQLClient.rawRequest(query, variables); - return response.data as T; - } catch (error: any) { - // Transform GraphQL errors to match LinearService error patterns - if (error.response?.errors) { - const graphQLError = error.response.errors[0]; - throw new Error(graphQLError.message || "GraphQL query failed"); - } - throw new Error(`GraphQL request failed: ${error.message}`); - } - } - - /** - * Execute multiple GraphQL queries in parallel (batching utility) - */ - async batchRequest<T = any[]>( - queries: Array<{ name: string; query: string; variables?: any }>, - ): Promise<T[]> { - const promises = queries.map(({ name, query, variables }) => - this.rawRequest<T>(query, variables) - ); - return Promise.all(promises); - } - - /** - * Get the underlying Linear client for fallback operations - */ - getLinearClient(): LinearClient { - return this.client; - } -} - -/** - * Create GraphQLService instance with authentication - */ -export async function createGraphQLService( - options: CommandOptions, -): Promise<GraphQLService> { - const apiToken = await getApiToken(options); - return new GraphQLService(apiToken); -} diff --git a/src/utils/identifier-parser.ts b/src/utils/identifier-parser.ts deleted file mode 100644 index fac156c..0000000 --- a/src/utils/identifier-parser.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Represents a parsed Linear issue identifier in the format TEAM-123 - */ -export interface ParsedIssueIdentifier { - /** The team key (e.g., "ABC" from "ABC-123") */ - teamKey: string; - /** The issue number (e.g., 123 from "ABC-123") */ - issueNumber: number; -} - -/** - * Parses a Linear issue identifier string in the format TEAM-123 - * - * @param identifier - The issue identifier to parse (e.g., "ABC-123") - * @returns A ParsedIssueIdentifier object with teamKey and issueNumber - * @throws Error if the identifier format is invalid - * - * @example - * ```typescript - * const parsed = parseIssueIdentifier("ABC-123"); - * console.log(parsed.teamKey); // "ABC" - * console.log(parsed.issueNumber); // 123 - * ``` - */ -export function parseIssueIdentifier(identifier: string): ParsedIssueIdentifier { - const parts = identifier.split("-"); - - if (parts.length !== 2) { - throw new Error( - `Invalid issue identifier format: "${identifier}". Expected format: TEAM-123`, - ); - } - - const teamKey = parts[0]; - const issueNumber = parseInt(parts[1]); - - if (isNaN(issueNumber)) { - throw new Error(`Invalid issue number in identifier: "${identifier}"`); - } - - return { teamKey, issueNumber }; -} - -/** - * Safely attempts to parse a Linear issue identifier without throwing errors - * - * @param identifier - The issue identifier to parse (e.g., "ABC-123") - * @returns A ParsedIssueIdentifier object if valid, otherwise null - * - * @example - * ```typescript - * const parsed = tryParseIssueIdentifier("ABC-123"); - * if (parsed) { - * console.log(`Team: ${parsed.teamKey}, Number: ${parsed.issueNumber}`); - * } else { - * console.log("Invalid identifier format"); - * } - * ``` - */ -export function tryParseIssueIdentifier(identifier: string): ParsedIssueIdentifier | null { - try { - return parseIssueIdentifier(identifier); - } catch { - return null; - } -} \ No newline at end of file diff --git a/src/utils/linear-service.ts b/src/utils/linear-service.ts deleted file mode 100644 index 654f2ac..0000000 --- a/src/utils/linear-service.ts +++ /dev/null @@ -1,700 +0,0 @@ -import { LinearClient } from "@linear/sdk"; -import { CommandOptions, getApiToken } from "./auth.js"; -import { - CreateCommentArgs, - LinearComment, - LinearIssue, - LinearLabel, - LinearProject, -} from "./linear-types.js"; -import { isUuid } from "./uuid.js"; -import { parseIssueIdentifier } from "./identifier-parser.js"; -import { multipleMatchesError, notFoundError } from "./error-messages.js"; - -// Default pagination limit for Linear SDK queries to avoid complexity errors -const DEFAULT_CYCLE_PAGINATION_LIMIT = 250; - -/** - * Generic ID resolver that handles UUID validation and passthrough - * - * @param input - Input string that may be a UUID or identifier - * @returns UUID as-is, or original string for non-UUID inputs - */ -function resolveId(input: string): string { - if (isUuid(input)) { - return input; - } - // Return as-is for non-UUID inputs that need further resolution - return input; -} - -/** - * Build common GraphQL filter for name/key equality searches - * - * @param field - GraphQL field name - * @param value - Value to match exactly - * @returns GraphQL filter object - */ -function buildEqualityFilter(field: string, value: string): any { - return { - [field]: { eq: value }, - }; -} - -/** - * Execute a Linear client query and handle "not found" errors consistently - * - * @param queryFn - Function that returns a promise with nodes array - * @param entityName - Human-readable entity name for error messages - * @param identifier - The identifier used in the query - * @returns The first node from the result - * @throws Error if no nodes are found - */ -async function executeLinearQuery<T>( - queryFn: () => Promise<{ nodes: T[] }>, - entityName: string, - identifier: string, -): Promise<T> { - const result = await queryFn(); - if (result.nodes.length === 0) { - throw new Error(`${entityName} "${identifier}" not found`); - } - return result.nodes[0]; -} - -/** - * Linear SDK service with smart ID resolution and optimized operations - * - * Provides fallback operations and comprehensive ID resolution for Linear entities. - * This service handles human-friendly identifiers (TEAM-123, project names, etc.) - * and resolves them to Linear UUIDs for API operations. - * - * Features: - * - Smart ID resolution for teams, projects, labels, and issues - * - Fallback operations when GraphQL optimizations aren't available - * - Consistent error handling and messaging - * - Batch operations where possible - */ -export class LinearService { - private client: LinearClient; - - /** - * Initialize Linear service with authentication - * - * @param apiToken - Linear API token for authentication - */ - constructor(apiToken: string) { - this.client = new LinearClient({ apiKey: apiToken }); - } - - /** - * Resolve issue identifier to UUID (lightweight version for ID-only resolution) - * - * @param issueId - Either a UUID string or TEAM-123 format identifier - * @returns The resolved UUID string - * @throws Error if the issue identifier format is invalid or issue not found - * - * @example - * ```typescript - * // Using UUID - * const uuid1 = await resolveIssueId("123e4567-e89b-12d3-a456-426614174000"); - * - * // Using TEAM-123 format - * const uuid2 = await resolveIssueId("ABC-123"); - * ``` - */ - async resolveIssueId(issueId: string): Promise<string> { - // Return UUID as-is - if (isUuid(issueId)) { - return issueId; - } - - // Parse identifier (ABC-123 format) and resolve to UUID - const { teamKey, issueNumber } = parseIssueIdentifier(issueId); - - const issues = await this.client.issues({ - filter: { - number: { eq: issueNumber }, - team: { key: { eq: teamKey } }, - }, - first: 1, - }); - - if (issues.nodes.length === 0) { - throw new Error(`Issue with identifier "${issueId}" not found`); - } - - return issues.nodes[0].id; - } - - /** - * Get all teams in the workspace - * - * @returns Array of teams with id, key, name, and description - */ - async getTeams(): Promise<any[]> { - const teamsConnection = await this.client.teams({ - first: 100, - }); - - // Sort by name client-side since Linear API doesn't support orderBy: "name" - const teams = teamsConnection.nodes.map((team) => ({ - id: team.id, - key: team.key, - name: team.name, - description: team.description || null, - })); - - return teams.sort((a, b) => a.name.localeCompare(b.name)); - } - - /** - * Get all users in the workspace - * - * @param activeOnly - If true, return only active users - * @returns Array of users with id, name, displayName, email, and active status - */ - async getUsers(activeOnly?: boolean): Promise<any[]> { - const filter: any = {}; - - if (activeOnly) { - filter.active = { eq: true }; - } - - const usersConnection = await this.client.users({ - filter: Object.keys(filter).length > 0 ? filter : undefined, - first: 100, - }); - - // Sort by name client-side since Linear API doesn't support orderBy: "name" - const users = usersConnection.nodes.map((user) => ({ - id: user.id, - name: user.name, - displayName: user.displayName, - email: user.email, - active: user.active, - })); - - return users.sort((a, b) => a.name.localeCompare(b.name)); - } - - /** - * Get all projects - */ - async getProjects(): Promise<LinearProject[]> { - const projects = await this.client.projects({ - first: 100, - orderBy: "updatedAt" as any, - includeArchived: false, - }); - - // Fetch all relationships in parallel for all projects - const projectsWithData = await Promise.all( - projects.nodes.map(async (project) => { - const [teams, lead] = await Promise.all([ - project.teams(), - project.lead, - ]); - return { project, teams, lead }; - }), - ); - - return projectsWithData.map(({ project, teams, lead }) => ({ - id: project.id, - name: project.name, - description: project.description || undefined, - state: project.state, - progress: project.progress, - teams: teams.nodes.map((team: any) => ({ - id: team.id, - key: team.key, - name: team.name, - })), - lead: lead - ? { - id: lead.id, - name: lead.name, - } - : undefined, - // Convert date objects to ISO 8601 strings for JSON serialization - targetDate: project.targetDate - ? new Date(project.targetDate).toISOString() - : undefined, - createdAt: project.createdAt - ? new Date(project.createdAt).toISOString() - : new Date().toISOString(), - updatedAt: project.updatedAt - ? new Date(project.updatedAt).toISOString() - : new Date().toISOString(), - })); - } - - /** - * Resolve team key or name to team ID - */ - async resolveTeamId(teamKeyOrNameOrId: string): Promise<string> { - // Use generic ID resolver - const resolved = resolveId(teamKeyOrNameOrId); - if (resolved === teamKeyOrNameOrId && isUuid(teamKeyOrNameOrId)) { - return teamKeyOrNameOrId; - } - - // Try to find by key first (like "ABC"), then by name - try { - const team = await executeLinearQuery( - () => - this.client.teams({ - filter: buildEqualityFilter("key", teamKeyOrNameOrId), - first: 1, - }), - "Team", - teamKeyOrNameOrId, - ); - return team.id; - } catch { - // If not found by key, try by name - const team = await executeLinearQuery( - () => - this.client.teams({ - filter: buildEqualityFilter("name", teamKeyOrNameOrId), - first: 1, - }), - "Team", - teamKeyOrNameOrId, - ); - return team.id; - } - } - - /** - * Resolve status name to status ID for a specific team - */ - async resolveStatusId(statusName: string, teamId?: string): Promise<string> { - // Return UUID as-is - if (isUuid(statusName)) { - return statusName; - } - - // Build filter for workflow states - const filter: any = { - name: { eqIgnoreCase: statusName }, - }; - - // If teamId is provided, filter by team - if (teamId) { - filter.team = { id: { eq: teamId } }; - } - - const statuses = await this.client.workflowStates({ - filter, - first: 1, - }); - - if (statuses.nodes.length === 0) { - const context = teamId ? ` for team ${teamId}` : ""; - throw new Error(`Status "${statusName}"${context} not found`); - } - - return statuses.nodes[0].id; - } - - /** - * Get all labels (workspace and team-specific) - */ - async getLabels(teamFilter?: string): Promise<{ labels: LinearLabel[] }> { - const labels: LinearLabel[] = []; - - if (teamFilter) { - // Get labels for specific team only - const teamId = await this.resolveTeamId(teamFilter); - const team = await this.client.team(teamId); - const teamLabels = await this.client.issueLabels({ - filter: { team: { id: { eq: teamId } } }, - first: 100, - }); - - for (const label of teamLabels.nodes) { - // Skip group labels (isGroup: true) as they're containers, not actual labels - if (label.isGroup) { - continue; - } - - const parent = await label.parent; - - const labelData: LinearLabel = { - id: label.id, - name: label.name, - color: label.color, - scope: "team", - team: { - id: team.id, - name: team.name, - }, - }; - - // Add group info if this label has a parent group - if (parent) { - // Fetch the parent label details to get the name - const parentLabel = await this.client.issueLabel(parent.id); - labelData.group = { - id: parent.id, - name: parentLabel.name, - }; - } - - labels.push(labelData); - } - } else { - // Get all labels (workspace + team labels) - const allLabels = await this.client.issueLabels({ - first: 100, - }); - - for (const label of allLabels.nodes) { - // Skip group labels (isGroup: true) as they're containers, not actual labels - if (label.isGroup) { - continue; - } - - const [team, parent] = await Promise.all([ - label.team, - label.parent, - ]); - - const labelData: LinearLabel = { - id: label.id, - name: label.name, - color: label.color, - scope: team ? "team" : "workspace", - }; - - // Add team info if this is a team-specific label - if (team) { - labelData.team = { - id: team.id, - name: team.name, - }; - } - - // Add group info if this label has a parent group - if (parent) { - // Fetch the parent label details to get the name - const parentLabel = await this.client.issueLabel(parent.id); - labelData.group = { - id: parent.id, - name: parentLabel.name, - }; - } - - labels.push(labelData); - } - } - - return { labels }; - } - - /** - * Create comment on issue - */ - async createComment(args: CreateCommentArgs): Promise<LinearComment> { - const payload = await this.client.createComment({ - issueId: args.issueId, - body: args.body, - }); - - if (!payload.success) { - throw new Error("Failed to create comment"); - } - - // Fetch the created comment to return full data - const comment = await payload.comment; - if (!comment) { - throw new Error("Failed to retrieve created comment"); - } - - const user = await comment.user; - if (!user) { - throw new Error("Failed to retrieve comment user information"); - } - - return { - id: comment.id, - body: comment.body, - user: { - id: user.id, - name: user.name, - }, - createdAt: comment.createdAt.toISOString(), - updatedAt: comment.updatedAt.toISOString(), - }; - } - - /** - * Get all cycles with automatic pagination - * - * @param teamFilter - Optional team key, name, or ID to filter cycles - * @param activeOnly - If true, return only active cycles - * @returns Array of cycles with team information - * - * @remarks - * Uses Linear SDK automatic pagination with 250 cycles per request. - * This method will make multiple API calls if necessary to fetch all - * matching cycles. - * - * For workspaces with hundreds of cycles, consider using team filtering - * to reduce result set size and improve performance. - */ - async getCycles(teamFilter?: string, activeOnly?: boolean): Promise<any[]> { - const filter: any = {}; - - if (teamFilter) { - const teamId = await this.resolveTeamId(teamFilter); - filter.team = { id: { eq: teamId } }; - } - - if (activeOnly) { - filter.isActive = { eq: true }; - } - - const cyclesConnection = await this.client.cycles({ - filter: Object.keys(filter).length > 0 ? filter : undefined, - orderBy: "createdAt" as any, - first: DEFAULT_CYCLE_PAGINATION_LIMIT, - }); - - // Fetch all relationships in parallel for all cycles - // Note: Uses Promise.all - entire operation fails if any team fetch fails. - // This ensures data consistency (all cycles have team data or none do). - // If partial failures are acceptable, use Promise.allSettled instead. - const cyclesWithData = await Promise.all( - cyclesConnection.nodes.map(async (cycle) => { - const team = await cycle.team; - return { - id: cycle.id, - name: cycle.name, - number: cycle.number, - // Convert date objects to ISO 8601 strings for JSON serialization - startsAt: cycle.startsAt - ? new Date(cycle.startsAt).toISOString() - : undefined, - endsAt: cycle.endsAt - ? new Date(cycle.endsAt).toISOString() - : undefined, - isActive: cycle.isActive, - isPrevious: cycle.isPrevious, - isNext: cycle.isNext, - progress: cycle.progress, - issueCountHistory: cycle.issueCountHistory, - team: team - ? { - id: team.id, - key: team.key, - name: team.name, - } - : undefined, - }; - }), - ); - - return cyclesWithData; - } - - /** - * Get single cycle by ID with issues - * - * @param cycleId - Cycle UUID - * @param issuesLimit - Maximum issues to fetch (default 50) - * @returns Cycle with issues - * - * @remarks - * This method does not paginate issues. If a cycle has more issues than - * the limit, only the first N will be returned sorted by creation date. - * - * Linear API limits single requests to 250 items. Values above 250 may - * result in errors or truncation. - * - * To get all issues in a large cycle, either: - * 1. Increase the limit (up to 250) - * 2. Fetch issues separately using the issues API with pagination - * 3. Make multiple requests with cursor-based pagination - */ - async getCycleById(cycleId: string, issuesLimit: number = 50): Promise<any> { - const cycle = await this.client.cycle(cycleId); - - const [team, issuesConnection] = await Promise.all([ - cycle.team, - cycle.issues({ first: issuesLimit }), - ]); - - const issues = []; - for (const issue of issuesConnection.nodes) { - const [state, assignee, issueTeam, project, labels] = await Promise.all([ - issue.state, - issue.assignee, - issue.team, - issue.project, - issue.labels(), - ]); - - issues.push({ - id: issue.id, - identifier: issue.identifier, - title: issue.title, - description: issue.description || undefined, - priority: issue.priority, - estimate: issue.estimate || undefined, - state: state ? { id: state.id, name: state.name } : undefined, - assignee: assignee - ? { id: assignee.id, name: assignee.name } - : undefined, - team: issueTeam - ? { id: issueTeam.id, key: issueTeam.key, name: issueTeam.name } - : undefined, - project: project ? { id: project.id, name: project.name } : undefined, - labels: labels.nodes.map((label: any) => ({ - id: label.id, - name: label.name, - })), - createdAt: issue.createdAt - ? new Date(issue.createdAt).toISOString() - : new Date().toISOString(), - updatedAt: issue.updatedAt - ? new Date(issue.updatedAt).toISOString() - : new Date().toISOString(), - }); - } - - return { - id: cycle.id, - name: cycle.name, - number: cycle.number, - // Convert date objects to ISO 8601 strings for JSON serialization - startsAt: cycle.startsAt - ? new Date(cycle.startsAt).toISOString() - : undefined, - endsAt: cycle.endsAt ? new Date(cycle.endsAt).toISOString() : undefined, - isActive: cycle.isActive, - progress: cycle.progress, - issueCountHistory: cycle.issueCountHistory, - team: team - ? { - id: team.id, - key: team.key, - name: team.name, - } - : undefined, - issues, - }; - } - - /** - * Resolve cycle by name or ID - */ - async resolveCycleId( - cycleNameOrId: string, - teamFilter?: string, - ): Promise<string> { - // Return UUID as-is - if (isUuid(cycleNameOrId)) { - return cycleNameOrId; - } - - // Build filter for name-based lookup - const filter: any = { - name: { eq: cycleNameOrId }, - }; - - // If teamId is provided, filter by team - if (teamFilter) { - const teamId = await this.resolveTeamId(teamFilter); - filter.team = { id: { eq: teamId } }; - } - - const cyclesConnection = await this.client.cycles({ - filter, - first: 10, - }); - - const cyclesData = cyclesConnection.nodes; - - const nodes = []; - for (const cycle of cyclesData) { - const team = await cycle.team; - nodes.push({ - id: cycle.id, - name: cycle.name, - number: cycle.number, - startsAt: cycle.startsAt - ? new Date(cycle.startsAt).toISOString() - : undefined, - isActive: cycle.isActive, - isNext: cycle.isNext, - isPrevious: cycle.isPrevious, - team: team - ? { id: team.id, key: team.key, name: team.name } - : undefined, - }); - } - - if (nodes.length === 0) { - throw notFoundError( - "Cycle", - cycleNameOrId, - teamFilter ? `for team ${teamFilter}` : undefined, - ); - } - - // Disambiguate: prefer active, then next, then previous - let chosen = nodes.find((n: any) => n.isActive); - if (!chosen) chosen = nodes.find((n: any) => n.isNext); - if (!chosen) chosen = nodes.find((n: any) => n.isPrevious); - if (!chosen && nodes.length === 1) chosen = nodes[0]; - - if (!chosen) { - const matches = nodes.map((n: any) => - `${n.id} (${n.team?.key || "?"} / #${n.number} / ${n.startsAt})` - ); - throw multipleMatchesError( - "cycle", - cycleNameOrId, - matches, - "use an ID or scope with --team", - ); - } - - return chosen.id; - } - - /** - * Resolve project identifier to UUID - * - * @param projectNameOrId - Project name or UUID - * @returns Project UUID - * @throws Error if project not found - */ - async resolveProjectId(projectNameOrId: string): Promise<string> { - if (isUuid(projectNameOrId)) { - return projectNameOrId; - } - - // Use case-insensitive matching for better UX - const filter = { name: { eqIgnoreCase: projectNameOrId } }; - const projectsConnection = await this.client.projects({ filter, first: 1 }); - - if (projectsConnection.nodes.length === 0) { - throw new Error(`Project "${projectNameOrId}" not found`); - } - - return projectsConnection.nodes[0].id; - } -} - -/** - * Create LinearService instance with authentication - */ -export async function createLinearService( - options: CommandOptions, -): Promise<LinearService> { - const apiToken = await getApiToken(options); - return new LinearService(apiToken); -} diff --git a/src/utils/linear-types.d.ts b/src/utils/linear-types.d.ts deleted file mode 100644 index ec24d51..0000000 --- a/src/utils/linear-types.d.ts +++ /dev/null @@ -1,330 +0,0 @@ -export interface LinearIssue { - id: string; - identifier: string; - title: string; - description?: string; - branchName?: string; - embeds?: Array<{ - label: string; - url: string; - expiresAt: string; - }>; - state: { - id: string; - name: string; - }; - assignee?: { - id: string; - name: string; - }; - team: { - id: string; - key: string; - name: string; - }; - project?: { - id: string; - name: string; - }; - cycle?: { - id: string; - name: string; - number: number; - }; - projectMilestone?: { - id: string; - name: string; - targetDate?: string; - }; - priority: number; - estimate?: number; - labels: Array<{ - id: string; - name: string; - }>; - parentIssue?: { - id: string; - identifier: string; - title: string; - }; - subIssues?: Array<{ - id: string; - identifier: string; - title: string; - }>; - comments?: Array<{ - id: string; - body: string; - embeds?: Array<{ - label: string; - url: string; - expiresAt: string; - }>; - user: { - id: string; - name: string; - }; - createdAt: string; - updatedAt: string; - }>; - createdAt: string; - updatedAt: string; -} - -export interface LinearProject { - id: string; - name: string; - description?: string; - state: string; - progress: number; - teams: Array<{ - id: string; - key: string; - name: string; - }>; - lead?: { - id: string; - name: string; - }; - targetDate?: string; - createdAt: string; - updatedAt: string; -} - -export interface CreateIssueArgs { - title: string; - teamId?: string; - description?: string; - assigneeId?: string; - priority?: number; - projectId?: string; - statusId?: string; - labelIds?: string[]; - estimate?: number; - parentId?: string; - milestoneId?: string; - cycleId?: string; -} - -export interface UpdateIssueArgs { - id: string; - title?: string; - description?: string; - statusId?: string; - priority?: number; - assigneeId?: string; - projectId?: string; - labelIds?: string[]; - estimate?: number; - parentId?: string; - milestoneId?: string | null; - cycleId?: string | null; -} - -export interface SearchIssuesArgs { - query?: string; - teamId?: string; - assigneeId?: string; - projectId?: string; - status?: string[]; - limit?: number; -} - -export interface LinearLabel { - id: string; - name: string; - color: string; - scope: "workspace" | "team"; - team?: { - id: string; - name: string; - }; - group?: { - id: string; - name: string; - }; -} - -export interface CreateCommentArgs { - issueId: string; - body: string; -} - -export interface LinearComment { - id: string; - body: string; - user: { - id: string; - name: string; - }; - createdAt: string; - updatedAt: string; -} - -export interface LinearProjectMilestone { - id: string; - name: string; - description?: string; - targetDate?: string; - sortOrder?: number; - createdAt: string; - updatedAt: string; - project?: { - id: string; - name: string; - }; - issues?: LinearIssue[]; -} - -export interface LinearProjectMilestoneWithIssues - extends LinearProjectMilestone { - issues: LinearIssue[]; -} - -export interface ListProjectMilestonesArgs { - projectId: string; // Project name or UUID (will be resolved) - limit?: number; -} - -export interface GetProjectMilestoneArgs { - milestoneId: string; // Milestone name or UUID (will be resolved) - projectId?: string; // Optional project context for name resolution - issuesFirst?: number; // How many issues to fetch -} - -export interface CreateProjectMilestoneArgs { - name: string; - projectId: string; // Project name or UUID (will be resolved) - description?: string; - targetDate?: string; // ISO date string -} - -export interface UpdateProjectMilestoneArgs { - id: string; // Milestone ID or name (will be resolved) - projectId?: string; // Optional project context for name resolution - name?: string; - description?: string; - targetDate?: string; // ISO date string - sortOrder?: number; -} - -export interface LinearCycle { - id: string; - name: string; - number: number; - startsAt?: string; - endsAt?: string; - isActive: boolean; - isPrevious?: boolean; - isNext?: boolean; - progress: number; - issueCountHistory: number[]; - team?: { - id: string; - key: string; - name: string; - }; - issues?: LinearIssue[]; -} - -export interface CycleListOptions { - team?: string; - active?: boolean; - aroundActive?: string; -} - -export interface CycleReadOptions { - team?: string; - issuesFirst?: string; -} - -export interface MilestoneListOptions { - project: string; - limit?: string; -} - -export interface MilestoneReadOptions { - project?: string; - issuesFirst?: string; -} - -export interface MilestoneCreateOptions { - project: string; - description?: string; - targetDate?: string; -} - -export interface MilestoneUpdateOptions { - project?: string; - name?: string; - description?: string; - targetDate?: string; - sortOrder?: string; -} - -// Document types -export interface LinearDocument { - id: string; - title: string; - content?: string; - slugId: string; - url: string; - icon?: string; - color?: string; - createdAt: string; - updatedAt: string; - creator?: { - id: string; - name: string; - }; - project?: { - id: string; - name: string; - }; - trashed?: boolean; -} - -export interface DocumentCreateInput { - title: string; - content?: string; - projectId?: string; - teamId?: string; - icon?: string; - color?: string; -} - -export interface DocumentUpdateInput { - title?: string; - content?: string; - projectId?: string; - icon?: string; - color?: string; -} - -// Attachment types (internal use for documents linking) -export interface LinearAttachment { - id: string; - title: string; - subtitle?: string; - url: string; - createdAt: string; - updatedAt: string; - issue: { - id: string; - identifier: string; - title: string; - }; - creator?: { - id: string; - name: string; - }; -} - -export interface AttachmentCreateInput { - issueId: string; - url: string; - title: string; - subtitle?: string; - commentBody?: string; - iconUrl?: string; -} diff --git a/src/utils/output.ts b/src/utils/output.ts deleted file mode 100644 index 584b51a..0000000 --- a/src/utils/output.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Output successful data as formatted JSON - * - * @param data - Data to output (will be JSON serialized) - * - * @example - * ```typescript - * outputSuccess({ id: "123", title: "Issue title" }); - * // Outputs: { "id": "123", "title": "Issue title" } - * ``` - */ -export function outputSuccess(data: any): void { - console.log(JSON.stringify(data, null, 2)); -} - -/** - * Output error as formatted JSON and exit with error code - * - * @param error - Error to output (will be serialized to error.message) - * - * @example - * ```typescript - * outputError(new Error("Something went wrong")); - * // Outputs to stderr: { "error": "Something went wrong" } - * // Process exits with code 1 - * ``` - */ -export function outputError(error: Error): void { - console.error(JSON.stringify({ error: error.message }, null, 2)); - process.exit(1); -} - -/** - * Wrap an async command handler with error handling - * - * This utility provides consistent error handling for all CLI commands. - * It catches both thrown errors and rejected promises, formats them - * as JSON, and exits with appropriate error codes. - * - * @param asyncFn - Async function to wrap (typically a command handler) - * @returns Wrapped function with error handling - * - * @example - * ```typescript - * export const setupMyCommand = (program: Command) => { - * const cmd = program.command("my-command"); - * cmd.action(handleAsyncCommand(async (command: Command) => { - * // Command logic here - errors will be caught and formatted - * const result = await someAsyncOperation(); - * outputSuccess(result); - * })); - * }; - * ``` - */ -export function handleAsyncCommand( - asyncFn: (...args: any[]) => Promise<void>, -): (...args: any[]) => Promise<void> { - return async (...args: any[]) => { - try { - await asyncFn(...args); - } catch (error) { - outputError(error instanceof Error ? error : new Error(String(error))); - } - }; -} diff --git a/src/utils/usage.ts b/src/utils/usage.ts deleted file mode 100644 index 7ef2b6f..0000000 --- a/src/utils/usage.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Command } from "commander"; - -/** - * Generate usage information for all individual subcommands - * - * This utility traverses the entire command tree and outputs formatted help - * for each leaf command. It collects commands recursively, sorts them - * alphabetically, and outputs their help blocks separated by dividers. - * - * @param program - Commander.js program instance with registered commands - * @returns void (outputs help text to console) - * - * @example - * ```typescript - * // In main.ts usage command setup - * program - * .command("usage") - * .description("show usage info for all tools") - * .action(() => outputUsageInfo(program)); - * ``` - */ -export function outputUsageInfo(program: Command) { - const subcommands: { name: string; command: Command }[] = []; - - /** - * Recursively collect all leaf subcommands (not parent commands) - * - * @param cmd - Current command to process - * @param prefix - Accumulated command name prefix - */ - function collectSubcommands(cmd: Command, prefix: string = "") { - const currentName = prefix ? `${prefix} ${cmd.name()}` : cmd.name(); - - // Get all subcommands - const commands = cmd.commands; - - if (commands.length === 0) { - // This is a leaf command (actual subcommand) - if (prefix) { // Only include commands with a prefix (exclude root) - subcommands.push({ name: currentName, command: cmd }); - } - } else { - // This is a parent command, recurse into its subcommands - commands.forEach((subcmd) => collectSubcommands(subcmd, currentName)); - } - } - - // Start collection from root program - collectSubcommands(program); - - // Sort subcommands alphabetically by full name - subcommands.sort((a, b) => a.name.localeCompare(b.name)); - - // Output full (incl. `.addHelpText()` blocks) help text for each subcommand - subcommands.forEach(({ command }) => { - command.outputHelp(); - console.log("\n---\n") - }); -} diff --git a/src/utils/uuid.ts b/src/utils/uuid.ts deleted file mode 100644 index 6755188..0000000 --- a/src/utils/uuid.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Check if a string is a valid UUID (Universally Unique Identifier) - * - * @param value - The string to validate - * @returns true if the string is a valid UUID format, false otherwise - * - * @example - * ```typescript - * isUuid("123e4567-e89b-12d3-a456-426614174000"); // true - * isUuid("not-a-uuid"); // false - * isUuid("ABC-123"); // false - * ``` - */ -export function isUuid(value: string): boolean { - const uuidRegex = - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - return uuidRegex.test(value); -} diff --git a/tests/command-coverage.ts b/tests/command-coverage.ts index ad2864e..84c0109 100644 --- a/tests/command-coverage.ts +++ b/tests/command-coverage.ts @@ -8,8 +8,8 @@ * integration tests run in separate processes. */ -import { readFileSync, readdirSync } from "fs"; -import { join } from "path"; +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; interface Command { name: string; @@ -17,22 +17,15 @@ interface Command { file: string; } -interface TestCoverage { - command: string; - subcommand?: string; - testFile: string; - tested: boolean; -} - /** * Extract commands from source files */ function extractCommands(commandsDir: string): Command[] { const commands: Command[] = []; - const files = readdirSync(commandsDir).filter(f => f.endsWith('.ts')); + const files = readdirSync(commandsDir).filter((f) => f.endsWith(".ts")); for (const file of files) { - const content = readFileSync(join(commandsDir, file), 'utf-8'); + const content = readFileSync(join(commandsDir, file), "utf-8"); // Extract main command name const mainCommandMatch = content.match(/\.command\("([^"]+)"\)/); @@ -42,13 +35,15 @@ function extractCommands(commandsDir: string): Command[] { const subcommands: string[] = []; // Extract subcommands - const subcommandMatches = content.matchAll(/(?:command|\.command)\("([^"]+)"\)/g); + const subcommandMatches = content.matchAll( + /(?:command|\.command)\("([^"]+)"\)/g, + ); for (const match of subcommandMatches) { const sub = match[1]; // Skip the main command name if (sub !== commandName) { // Extract just the command word, remove parameters like <id> - const subName = sub.split(' ')[0]; + const subName = sub.split(" ")[0]; subcommands.push(subName); } } @@ -56,7 +51,7 @@ function extractCommands(commandsDir: string): Command[] { commands.push({ name: commandName, subcommands: subcommands.filter((v, i, a) => a.indexOf(v) === i), // unique - file: file + file: file, }); } @@ -68,10 +63,10 @@ function extractCommands(commandsDir: string): Command[] { */ function extractTestedCommands(testsDir: string): Set<string> { const tested = new Set<string>(); - const files = readdirSync(testsDir).filter(f => f.endsWith('.test.ts')); + const files = readdirSync(testsDir).filter((f) => f.endsWith(".test.ts")); for (const file of files) { - const content = readFileSync(join(testsDir, file), 'utf-8'); + const content = readFileSync(join(testsDir, file), "utf-8"); // Find all CLI command executions - match various formats // Examples: @@ -90,12 +85,12 @@ function extractTestedCommands(testsDir: string): Set<string> { const subcommand = match[2]; // Skip if it looks like a flag - if (command?.startsWith('-')) continue; - if (subcommand?.startsWith('-')) continue; + if (command?.startsWith("-")) continue; + if (subcommand?.startsWith("-")) continue; - if (command === 'help') continue; // Skip help command itself + if (command === "help") continue; // Skip help command itself - if (subcommand && !subcommand.startsWith('--')) { + if (subcommand && !subcommand.startsWith("--")) { tested.add(`${command} ${subcommand}`); } else if (command) { tested.add(`${command} --help`); @@ -111,14 +106,14 @@ function extractTestedCommands(testsDir: string): Set<string> { * Generate coverage report */ function generateReport() { - const commandsDir = join(process.cwd(), 'src/commands'); - const testsDir = join(process.cwd(), 'tests/integration'); + const commandsDir = join(process.cwd(), "src/commands"); + const testsDir = join(process.cwd(), "tests/integration"); const commands = extractCommands(commandsDir); const tested = extractTestedCommands(testsDir); - console.log('\n📊 CLI Command Coverage Report\n'); - console.log('=' .repeat(70)); + console.log("\n📊 CLI Command Coverage Report\n"); + console.log("=".repeat(70)); console.log(); let totalCommands = 0; @@ -128,12 +123,13 @@ function generateReport() { for (const cmd of commands.sort((a, b) => a.name.localeCompare(b.name))) { totalCommands++; - const cmdTested = tested.has(`${cmd.name} --help`) || - cmd.subcommands.some(sub => tested.has(`${cmd.name} ${sub}`)); + const cmdTested = + tested.has(`${cmd.name} --help`) || + cmd.subcommands.some((sub) => tested.has(`${cmd.name} ${sub}`)); if (cmdTested) testedCommands++; - const status = cmdTested ? '✅' : '❌'; + const status = cmdTested ? "✅" : "❌"; console.log(`${status} ${cmd.name.padEnd(20)} (${cmd.file})`); if (cmd.subcommands.length > 0) { @@ -142,25 +138,38 @@ function generateReport() { const subTested = tested.has(`${cmd.name} ${sub}`); if (subTested) testedSubcommands++; - const subStatus = subTested ? ' ✅' : ' ⚠️ '; + const subStatus = subTested ? " ✅" : " ⚠️ "; console.log(`${subStatus} ├─ ${sub}`); } } console.log(); } - console.log('=' .repeat(70)); - console.log('\n📈 Summary\n'); - - const cmdCoverage = totalCommands > 0 ? (testedCommands / totalCommands * 100).toFixed(1) : '0.0'; - const subCoverage = totalSubcommands > 0 ? (testedSubcommands / totalSubcommands * 100).toFixed(1) : '0.0'; + console.log("=".repeat(70)); + console.log("\n📈 Summary\n"); + + const cmdCoverage = + totalCommands > 0 + ? ((testedCommands / totalCommands) * 100).toFixed(1) + : "0.0"; + const subCoverage = + totalSubcommands > 0 + ? ((testedSubcommands / totalSubcommands) * 100).toFixed(1) + : "0.0"; const totalTests = testedCommands + testedSubcommands; const total = totalCommands + totalSubcommands; - const overallCoverage = total > 0 ? (totalTests / total * 100).toFixed(1) : '0.0'; - - console.log(`Commands: ${testedCommands}/${totalCommands} tested (${cmdCoverage}%)`); - console.log(`Subcommands: ${testedSubcommands}/${totalSubcommands} tested (${subCoverage}%)`); - console.log(`Overall: ${totalTests}/${total} tested (${overallCoverage}%)`); + const overallCoverage = + total > 0 ? ((totalTests / total) * 100).toFixed(1) : "0.0"; + + console.log( + `Commands: ${testedCommands}/${totalCommands} tested (${cmdCoverage}%)`, + ); + console.log( + `Subcommands: ${testedSubcommands}/${totalSubcommands} tested (${subCoverage}%)`, + ); + console.log( + `Overall: ${totalTests}/${total} tested (${overallCoverage}%)`, + ); console.log(); // Show what's not tested @@ -174,7 +183,7 @@ function generateReport() { } if (untested.length > 0) { - console.log('⚠️ Commands without integration tests:\n'); + console.log("⚠️ Commands without integration tests:\n"); for (const cmd of untested) { console.log(` • ${cmd}`); } @@ -182,9 +191,9 @@ function generateReport() { } // Show tested commands - console.log('✅ Commands with integration tests:\n'); + console.log("✅ Commands with integration tests:\n"); const testedList = Array.from(tested) - .filter(t => !t.endsWith('--help')) + .filter((t) => !t.endsWith("--help")) .sort(); for (const cmd of testedList) { @@ -192,13 +201,13 @@ function generateReport() { } console.log(); - console.log('=' .repeat(70)); + console.log("=".repeat(70)); console.log(); // Exit with error if coverage is too low (optional) if (parseFloat(overallCoverage) < 50) { - console.log('⚠️ Command coverage is below 50%'); - console.log(' Consider adding more integration tests\n'); + console.log("⚠️ Command coverage is below 50%"); + console.log(" Consider adding more integration tests\n"); } else { console.log(`✅ Command coverage is ${overallCoverage}%\n`); } diff --git a/tests/integration/cycles-cli.test.ts b/tests/integration/cycles-cli.test.ts index bcd6a00..048697e 100644 --- a/tests/integration/cycles-cli.test.ts +++ b/tests/integration/cycles-cli.test.ts @@ -1,6 +1,6 @@ +import { exec } from "node:child_process"; +import { promisify } from "node:util"; import { beforeAll, describe, expect, it } from "vitest"; -import { exec } from "child_process"; -import { promisify } from "util"; const execAsync = promisify(exec); @@ -67,26 +67,23 @@ describe("Cycles CLI Commands", () => { expect(cycle).toHaveProperty("id"); expect(cycle).toHaveProperty("number"); expect(cycle).toHaveProperty("isActive"); - expect(cycle).toHaveProperty("team"); + expect(cycle).toHaveProperty("name"); + expect(cycle).toHaveProperty("startsAt"); + expect(cycle).toHaveProperty("endsAt"); - // Note: name field is optional - not all cycles have names - - // Verify team structure - expect(cycle.team).toHaveProperty("id"); - expect(cycle.team).toHaveProperty("key"); - expect(cycle.team).toHaveProperty("name"); + // Note: Team data is not included in list view for token optimization } }); it.skipIf(!hasApiToken)("should filter by active cycles", async () => { - // First, get a team key - const { stdout: listOutput } = await execAsync( - `node ${CLI_PATH} cycles list`, + // First, get a team key from teams list + const { stdout: teamsOutput } = await execAsync( + `node ${CLI_PATH} teams list`, ); - const allCycles = JSON.parse(listOutput); + const teams = JSON.parse(teamsOutput); - if (allCycles.length > 0 && allCycles[0].team) { - const teamKey = allCycles[0].team.key; + if (teams.length > 0) { + const teamKey = teams[0].key; // Now test active filter const { stdout } = await execAsync( @@ -95,28 +92,28 @@ describe("Cycles CLI Commands", () => { const activeCycles = JSON.parse(stdout); // All returned cycles should be active - activeCycles.forEach((cycle: any) => { + activeCycles.forEach((cycle: { isActive: boolean }) => { expect(cycle.isActive).toBe(true); }); } }); it.skipIf(!hasApiToken)( - "should work with --around-active flag", + "should work with --window flag", async () => { - // First, get a team key - const { stdout: listOutput } = await execAsync( - `node ${CLI_PATH} cycles list`, + // First, get a team key from teams list + const { stdout: teamsOutput } = await execAsync( + `node ${CLI_PATH} teams list`, ); - const allCycles = JSON.parse(listOutput); + const teams = JSON.parse(teamsOutput); - if (allCycles.length > 0 && allCycles[0].team) { - const teamKey = allCycles[0].team.key; + if (teams.length > 0) { + const teamKey = teams[0].key; - // Test around-active (may fail if no active cycle, which is ok) + // Test window (may fail if no active cycle, which is ok) try { const { stdout, stderr } = await execAsync( - `node ${CLI_PATH} cycles list --around-active 3 --team ${teamKey}`, + `node ${CLI_PATH} cycles list --window 3 --team ${teamKey}`, ); // Should not have complexity errors @@ -124,9 +121,10 @@ describe("Cycles CLI Commands", () => { const cycles = JSON.parse(stdout); expect(Array.isArray(cycles)).toBe(true); - } catch (error: any) { + } catch (error: unknown) { // It's ok if there's no active cycle - if (!error.stderr?.includes("No active cycle")) { + const execError = error as { stderr?: string }; + if (!execError.stderr?.includes("No active cycle")) { throw error; } } @@ -135,12 +133,14 @@ describe("Cycles CLI Commands", () => { { timeout: 30000 }, ); - it("should require --team when using --around-active", async () => { + it("should require --team when using --window", async () => { try { - await execAsync(`node ${CLI_PATH} cycles list --around-active 3`); + await execAsync(`node ${CLI_PATH} cycles list --window 3`); expect.fail("Should have thrown an error"); - } catch (error: any) { - expect(error.stderr).toContain("--around-active requires --team"); + } catch (error: unknown) { + expect((error as { stderr: string }).stderr).toContain( + "--window requires --team", + ); } }); }); @@ -175,18 +175,30 @@ describe("Cycles CLI Commands", () => { }); it.skipIf(!hasApiToken)("should read cycle by name with team", async () => { - // First get a cycle name and team + // Get team key from teams list + const { stdout: teamsOutput } = await execAsync( + `node ${CLI_PATH} teams list`, + ); + const teams = JSON.parse(teamsOutput); + + if (teams.length === 0) { + console.log("Skipping: No teams found in workspace"); + return; + } + + const teamKey = teams[0].key; + + // Get cycles for this team const { stdout: listOutput } = await execAsync( - `node ${CLI_PATH} cycles list`, + `node ${CLI_PATH} cycles list --team ${teamKey}`, ); const cycles = JSON.parse(listOutput); // Find a cycle that has a name - const cycleWithName = cycles.find((c: any) => c.name); + const cycleWithName = cycles.find((c: { name?: string }) => c.name); - if (cycleWithName && cycleWithName.team) { + if (cycleWithName) { const cycleName = cycleWithName.name; - const teamKey = cycleWithName.team.key; const { stdout, stderr } = await execAsync( `node ${CLI_PATH} cycles read "${cycleName}" --team ${teamKey}`, @@ -205,32 +217,68 @@ describe("Cycles CLI Commands", () => { }); describe("Cycles CLI - Error Cases", () => { - it("should reject --around-active without --team", async () => { + it("should reject --window without --team", async () => { if (!hasApiToken) return; await expect( - execAsync(`node ${CLI_PATH} cycles list --around-active 3`), - ).rejects.toThrow(/--around-active requires --team/); + execAsync(`node ${CLI_PATH} cycles list --window 3`), + ).rejects.toThrow(/--window requires --team/); }); - it("should reject --around-active with non-numeric value", async () => { - if (!hasApiToken) return; + it.skipIf(!hasApiToken)( + "should reject --window with non-numeric value", + async () => { + // Get a real team key + const { stdout: teamsOutput } = await execAsync( + `node ${CLI_PATH} teams list`, + ); + const teams = JSON.parse(teamsOutput); - await expect( - execAsync( - `node ${CLI_PATH} cycles list --around-active abc --team Engineering`, - ), - ).rejects.toThrow(/--around-active requires a non-negative integer/); - }); + if (teams.length > 0) { + const teamKey = teams[0].key; - it("should reject --around-active with negative value", async () => { - if (!hasApiToken) return; + try { + await execAsync( + `node ${CLI_PATH} cycles list --window abc --team ${teamKey}`, + ); + expect.fail("Should have thrown an error"); + } catch (error: unknown) { + const execError = error as { stdout?: string; stderr?: string }; + const output = JSON.parse( + execError.stdout || execError.stderr || "{}", + ); + expect(output.error).toContain("requires a non-negative integer"); + } + } + }, + ); - await expect( - execAsync( - `node ${CLI_PATH} cycles list --around-active -5 --team Engineering`, - ), - ).rejects.toThrow(/--around-active requires a non-negative integer/); - }); + it.skipIf(!hasApiToken)( + "should reject --window with negative value", + async () => { + // Get a real team key + const { stdout: teamsOutput } = await execAsync( + `node ${CLI_PATH} teams list`, + ); + const teams = JSON.parse(teamsOutput); + + if (teams.length > 0) { + const teamKey = teams[0].key; + + try { + await execAsync( + `node ${CLI_PATH} cycles list --window -5 --team ${teamKey}`, + ); + expect.fail("Should have thrown an error"); + } catch (error: unknown) { + const execError = error as { stdout?: string; stderr?: string }; + const output = JSON.parse( + execError.stdout || execError.stderr || "{}", + ); + expect(output.error).toContain("requires a non-negative integer"); + } + } + }, + ); }); }); diff --git a/tests/integration/documents-cli.test.ts b/tests/integration/documents-cli.test.ts index 548c022..1538ac1 100644 --- a/tests/integration/documents-cli.test.ts +++ b/tests/integration/documents-cli.test.ts @@ -1,6 +1,6 @@ +import { exec } from "node:child_process"; +import { promisify } from "node:util"; import { beforeAll, describe, expect, it } from "vitest"; -import { exec } from "child_process"; -import { promisify } from "util"; const execAsync = promisify(exec); @@ -83,8 +83,11 @@ describe("Documents CLI Commands", () => { `node ${CLI_PATH} documents read nonexistent-uuid-12345`, ); expect.fail("Should have thrown an error"); - } catch (error: any) { - const output = JSON.parse(error.stdout || error.stderr); + } catch (error: unknown) { + const execError = error as { stdout?: string; stderr?: string }; + const output = JSON.parse( + execError.stdout || execError.stderr || "{}", + ); expect(output.error).toBeDefined(); } }, @@ -96,8 +99,11 @@ describe("Documents CLI Commands", () => { try { await execAsync(`node ${CLI_PATH} documents list --limit abc`); expect.fail("Should have thrown an error"); - } catch (error: any) { - const output = JSON.parse(error.stdout || error.stderr); + } catch (error: unknown) { + const execError = error as { stdout?: string; stderr?: string }; + const output = JSON.parse( + execError.stdout || execError.stderr || "{}", + ); expect(output.error).toContain("Invalid limit"); } }, @@ -109,8 +115,11 @@ describe("Documents CLI Commands", () => { try { await execAsync(`node ${CLI_PATH} documents list --limit -5`); expect.fail("Should have thrown an error"); - } catch (error: any) { - const output = JSON.parse(error.stdout || error.stderr); + } catch (error: unknown) { + const execError = error as { stdout?: string; stderr?: string }; + const output = JSON.parse( + execError.stdout || execError.stderr || "{}", + ); expect(output.error).toContain("Invalid limit"); } }, diff --git a/tests/integration/issues-cli.test.ts b/tests/integration/issues-cli.test.ts index 729a068..3da0630 100644 --- a/tests/integration/issues-cli.test.ts +++ b/tests/integration/issues-cli.test.ts @@ -8,12 +8,12 @@ * to avoid creating test data in production Linear workspaces. */ -import { describe, expect, it } from "vitest"; -import { exec } from "child_process"; -import { promisify } from "util"; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import { describe, it } from "vitest"; -const execAsync = promisify(exec); -const CLI_PATH = "dist/main.js"; +const _execAsync = promisify(exec); +const _CLI_PATH = "dist/main.js"; const hasApiToken = !!process.env.LINEAR_API_TOKEN; if (!hasApiToken) { @@ -24,45 +24,39 @@ if (!hasApiToken) { } describe("Issues CLI - Milestone Resolution", () => { - it.skip( - "should resolve milestone name to issue's current project when updating", - async () => { - // This test documents the expected behavior for ZCO-1578 - // When a user updates an issue with --project-milestone "name", - // the system should: - // 1. First check if --project is provided, use that project's milestone - // 2. If no --project, check the issue's current project for the milestone - // 3. Only fall back to global search if not found in either - - // Example scenario: - // - Project A has milestone "2025.11.2" - // - Project B has milestone "2025.11.2" - // - Issue ZCO-1569 is in Project A - // - Command: issues update ZCO-1569 --project-milestone "2025.11.2" - // - Expected: Uses milestone from Project A (not Project B) - - if (!hasApiToken) return; - - // This would require: - // 1. Finding or creating two projects with same milestone name - // 2. Creating an issue in one project - // 3. Attempting to set milestone by name - // 4. Verifying correct project's milestone was used - - // Skipped to avoid creating test data in production workspace - }, - ); - - it.skip( - "should use specified project's milestone when --project is provided", - async () => { - // This test documents that explicit --project should take precedence - // Command: issues update ZCO-1569 --project "Project B" --project-milestone "2025.11.2" - // Expected: Uses milestone from Project B (even if issue is in Project A) - - if (!hasApiToken) return; - - // Skipped to avoid creating test data in production workspace - }, - ); + it.skip("should resolve milestone name to issue's current project when updating", async () => { + // This test documents the expected behavior for ZCO-1578 + // When a user updates an issue with --project-milestone "name", + // the system should: + // 1. First check if --project is provided, use that project's milestone + // 2. If no --project, check the issue's current project for the milestone + // 3. Only fall back to global search if not found in either + + // Example scenario: + // - Project A has milestone "2025.11.2" + // - Project B has milestone "2025.11.2" + // - Issue ZCO-1569 is in Project A + // - Command: issues update ZCO-1569 --project-milestone "2025.11.2" + // - Expected: Uses milestone from Project A (not Project B) + + if (!hasApiToken) return; + + // This would require: + // 1. Finding or creating two projects with same milestone name + // 2. Creating an issue in one project + // 3. Attempting to set milestone by name + // 4. Verifying correct project's milestone was used + + // Skipped to avoid creating test data in production workspace + }); + + it.skip("should use specified project's milestone when --project is provided", async () => { + // This test documents that explicit --project should take precedence + // Command: issues update ZCO-1569 --project "Project B" --project-milestone "2025.11.2" + // Expected: Uses milestone from Project B (even if issue is in Project A) + + if (!hasApiToken) return; + + // Skipped to avoid creating test data in production workspace + }); }); diff --git a/tests/integration/project-milestones-cli.test.ts b/tests/integration/milestones-cli.test.ts similarity index 70% rename from tests/integration/project-milestones-cli.test.ts rename to tests/integration/milestones-cli.test.ts index 9edc5de..da76cde 100644 --- a/tests/integration/project-milestones-cli.test.ts +++ b/tests/integration/milestones-cli.test.ts @@ -1,14 +1,14 @@ +import { exec } from "node:child_process"; +import { promisify } from "node:util"; import { beforeAll, describe, expect, it } from "vitest"; -import { exec } from "child_process"; -import { promisify } from "util"; const execAsync = promisify(exec); /** - * Integration tests for project-milestones CLI command + * Integration tests for milestones CLI command * * These tests verify the command naming fix from PR #4: - * - Command accessible via kebab-case (project-milestones) + * - Command accessible via kebab-case (milestones) * - Old camelCase (projectMilestones) fails appropriately * - Command functionality unchanged * @@ -22,7 +22,7 @@ const execAsync = promisify(exec); const CLI_PATH = "./dist/main.js"; const hasApiToken = !!process.env.LINEAR_API_TOKEN; -describe("Project Milestones CLI Commands", () => { +describe("Milestones CLI Commands", () => { beforeAll(async () => { if (!hasApiToken) { console.warn( @@ -33,11 +33,9 @@ describe("Project Milestones CLI Commands", () => { describe("command naming", () => { it("should display help with kebab-case naming", async () => { - const { stdout } = await execAsync( - `node ${CLI_PATH} project-milestones --help`, - ); + const { stdout } = await execAsync(`node ${CLI_PATH} milestones --help`); - expect(stdout).toContain("Usage: linearis project-milestones"); + expect(stdout).toContain("Usage: linearis milestones"); expect(stdout).toContain("Project milestone operations"); expect(stdout).toContain("list"); expect(stdout).toContain("read"); @@ -48,7 +46,7 @@ describe("Project Milestones CLI Commands", () => { it("should appear in main help with kebab-case", async () => { const { stdout } = await execAsync(`node ${CLI_PATH} --help`); - expect(stdout).toContain("project-milestones"); + expect(stdout).toContain("milestones"); expect(stdout).not.toContain("projectMilestones"); }); @@ -58,25 +56,25 @@ describe("Project Milestones CLI Commands", () => { // If it doesn't throw, check that it shows main help (unknown command) const { stdout } = await execAsync( `node ${CLI_PATH} projectMilestones --help`, - ).catch( - (e) => e, - ); + ).catch((e) => e); expect(stdout).toContain("Usage: linearis"); - } catch (error: any) { + } catch (error: unknown) { // Expected to fail - old command name not recognized - expect(error.stderr || error.message).toBeTruthy(); + const execError = error as { stderr?: string; message?: string }; + expect(execError.stderr || execError.message).toBeTruthy(); } }); }); - describe("project-milestones list", () => { + describe("milestones list", () => { it.skipIf(!hasApiToken)("should require --project flag", async () => { try { - await execAsync(`node ${CLI_PATH} project-milestones list`); + await execAsync(`node ${CLI_PATH} milestones list`); expect.fail("Should have thrown an error"); - } catch (error: any) { - expect(error.stderr).toContain("required option"); - expect(error.stderr).toContain("--project"); + } catch (error: unknown) { + const execError = error as { stderr: string }; + expect(execError.stderr).toContain("required option"); + expect(execError.stderr).toContain("--project"); } }); @@ -94,17 +92,18 @@ describe("Project Milestones CLI Commands", () => { const projectName = projects[0].name; const { stdout } = await execAsync( - `node ${CLI_PATH} project-milestones list --project "${projectName}"`, + `node ${CLI_PATH} milestones list --project "${projectName}"`, ); const milestones = JSON.parse(stdout); expect(Array.isArray(milestones)).toBe(true); } - } catch (error: any) { + } catch (error: unknown) { // Skip test if network issues or no projects + const execError = error as { stderr?: string }; if ( - error.stderr?.includes("Fetch failed") || - error.stderr?.includes("not found") + execError.stderr?.includes("Fetch failed") || + execError.stderr?.includes("not found") ) { console.log("Skipping: Network issues or no projects available"); } else { diff --git a/tests/integration/teams-cli.test.ts b/tests/integration/teams-cli.test.ts index 79ed524..401e460 100644 --- a/tests/integration/teams-cli.test.ts +++ b/tests/integration/teams-cli.test.ts @@ -1,6 +1,6 @@ +import { exec } from "node:child_process"; +import { promisify } from "node:util"; import { beforeAll, describe, expect, it } from "vitest"; -import { exec } from "child_process"; -import { promisify } from "util"; const execAsync = promisify(exec); @@ -38,9 +38,7 @@ describe("Teams CLI Commands", () => { describe("teams list", () => { it.skipIf(!hasApiToken)("should list teams without error", async () => { - const { stdout, stderr } = await execAsync( - `node ${CLI_PATH} teams list`, - ); + const { stdout, stderr } = await execAsync(`node ${CLI_PATH} teams list`); // Should not have errors expect(stderr).not.toContain("error"); @@ -63,8 +61,7 @@ describe("Teams CLI Commands", () => { expect(team).toHaveProperty("id"); expect(team).toHaveProperty("key"); expect(team).toHaveProperty("name"); - // description is optional - expect(team).toHaveProperty("description"); + // Note: description omitted in new architecture for token optimization }); it.skipIf(!hasApiToken)("should return teams sorted by name", async () => { diff --git a/tests/integration/users-cli.test.ts b/tests/integration/users-cli.test.ts index b26555f..7821ea5 100644 --- a/tests/integration/users-cli.test.ts +++ b/tests/integration/users-cli.test.ts @@ -1,6 +1,6 @@ +import { exec } from "node:child_process"; +import { promisify } from "node:util"; import { beforeAll, describe, expect, it } from "vitest"; -import { exec } from "child_process"; -import { promisify } from "util"; const execAsync = promisify(exec); @@ -38,9 +38,7 @@ describe("Users CLI Commands", () => { describe("users list", () => { it.skipIf(!hasApiToken)("should list users without error", async () => { - const { stdout, stderr } = await execAsync( - `node ${CLI_PATH} users list`, - ); + const { stdout, stderr } = await execAsync(`node ${CLI_PATH} users list`); // Should not have errors expect(stderr).not.toContain("error"); @@ -62,9 +60,9 @@ describe("Users CLI Commands", () => { // Verify user has expected fields expect(user).toHaveProperty("id"); expect(user).toHaveProperty("name"); - expect(user).toHaveProperty("displayName"); expect(user).toHaveProperty("email"); expect(user).toHaveProperty("active"); + // Note: displayName omitted in new architecture for token optimization }); it.skipIf(!hasApiToken)("should filter active users only", async () => { diff --git a/tests/unit/client/graphql-client.test.ts b/tests/unit/client/graphql-client.test.ts new file mode 100644 index 0000000..78c1815 --- /dev/null +++ b/tests/unit/client/graphql-client.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { AuthenticationError } from "../../../src/common/errors.js"; + +// We test the error handling logic by mocking the underlying rawRequest +// The constructor creates a real LinearClient, so we mock at module level +vi.mock("@linear/sdk", () => { + const mockRawRequest = vi.fn(); + return { + LinearClient: vi.fn().mockImplementation(() => ({ + client: { rawRequest: mockRawRequest }, + })), + __mockRawRequest: mockRawRequest, + }; +}); + +describe("GraphQLClient", () => { + it("can be constructed with an API token", () => { + const client = new GraphQLClient("test-token"); + expect(client).toBeDefined(); + }); + + describe("request", () => { + let mockRawRequest: ReturnType<typeof vi.fn>; + + beforeEach(async () => { + const sdk = (await import("@linear/sdk")) as unknown as { + __mockRawRequest: ReturnType<typeof vi.fn>; + }; + mockRawRequest = sdk.__mockRawRequest; + mockRawRequest.mockReset(); + }); + + it("throws AuthenticationError on 'Authentication required' error", async () => { + mockRawRequest.mockRejectedValueOnce({ + response: { + errors: [{ message: "Authentication required" }], + }, + }); + + const client = new GraphQLClient("bad-token"); + const fakeDoc = { kind: "Document", definitions: [] } as Parameters< + typeof client.request + >[0]; + + await expect(client.request(fakeDoc)).rejects.toThrow( + AuthenticationError, + ); + }); + + it("throws AuthenticationError on 'Unauthorized' error message", async () => { + mockRawRequest.mockRejectedValueOnce({ + response: { + errors: [{ message: "Unauthorized" }], + }, + }); + + const client = new GraphQLClient("bad-token"); + const fakeDoc = { kind: "Document", definitions: [] } as Parameters< + typeof client.request + >[0]; + + await expect(client.request(fakeDoc)).rejects.toThrow( + AuthenticationError, + ); + }); + + it("throws regular Error on non-auth errors", async () => { + mockRawRequest.mockRejectedValueOnce({ + response: { + errors: [{ message: "Entity not found" }], + }, + }); + + const client = new GraphQLClient("good-token"); + const fakeDoc = { kind: "Document", definitions: [] } as Parameters< + typeof client.request + >[0]; + + try { + await client.request(fakeDoc); + expect.fail("Should have thrown"); + } catch (error: unknown) { + expect(error).toBeInstanceOf(Error); + expect(error).not.toBeInstanceOf(AuthenticationError); + expect((error as Error).message).toBe("Entity not found"); + } + }); + }); +}); diff --git a/tests/unit/commands/auth.test.ts b/tests/unit/commands/auth.test.ts new file mode 100644 index 0000000..2186f7f --- /dev/null +++ b/tests/unit/commands/auth.test.ts @@ -0,0 +1,295 @@ +import { Command } from "commander"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock all external dependencies before importing the module under test +vi.mock("node:child_process", () => ({ + exec: vi.fn((_cmd: string, cb: () => void) => cb()), +})); + +vi.mock("node:readline", () => ({ + createInterface: vi.fn(() => ({ + question: vi.fn((_q: string, cb: (a: string) => void) => cb("test-token")), + close: vi.fn(), + })), +})); + +vi.mock("../../../src/common/token-storage.js", () => ({ + saveToken: vi.fn(), + clearToken: vi.fn(), +})); + +vi.mock("../../../src/services/auth-service.js", () => ({ + validateToken: vi.fn(), +})); + +vi.mock("../../../src/common/context.js", () => ({ + createGraphQLClient: vi.fn(() => ({})), +})); + +vi.mock("../../../src/common/auth.js", async (importOriginal) => { + const actual = + await importOriginal<typeof import("../../../src/common/auth.js")>(); + return { ...actual, resolveApiToken: vi.fn() }; +}); + +import { setupAuthCommands } from "../../../src/commands/auth.js"; +import { resolveApiToken } from "../../../src/common/auth.js"; +import { clearToken, saveToken } from "../../../src/common/token-storage.js"; +import { validateToken } from "../../../src/services/auth-service.js"; + +const mockViewer = { + id: "user-1", + name: "Test User", + email: "test@example.com", +}; + +function createProgram(): Command { + const program = new Command(); + program.option("--api-token <token>"); + setupAuthCommands(program); + return program; +} + +describe("auth login", () => { + let exitSpy: ReturnType<typeof vi.spyOn>; + let stderrSpy: ReturnType<typeof vi.spyOn>; + + beforeEach(() => { + vi.clearAllMocks(); + // Prevent process.exit from actually exiting + exitSpy = vi + .spyOn(process, "exit") + .mockImplementation(() => undefined as never); + stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + // Default: no token found, stdin is not a TTY + vi.mocked(resolveApiToken).mockImplementation(() => { + throw new Error("No API token found"); + }); + Object.defineProperty(process.stdin, "isTTY", { + value: false, + configurable: true, + }); + }); + + it("skips login when valid token already exists", async () => { + vi.mocked(resolveApiToken).mockReturnValue({ + token: "existing-token", + source: "stored", + }); + vi.mocked(validateToken).mockResolvedValue(mockViewer); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "login"]); + + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining("Already authenticated as Test User"), + ); + expect(saveToken).not.toHaveBeenCalled(); + }); + + it("skips login when valid token exists via env var", async () => { + vi.mocked(resolveApiToken).mockReturnValue({ + token: "env-token", + source: "env", + }); + vi.mocked(validateToken).mockResolvedValue(mockViewer); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "login"]); + + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining("via LINEAR_API_TOKEN env var"), + ); + expect(saveToken).not.toHaveBeenCalled(); + }); + + it("proceeds with login when existing token is invalid", async () => { + vi.mocked(resolveApiToken).mockReturnValue({ + token: "bad-token", + source: "stored", + }); + vi.mocked(validateToken) + .mockRejectedValueOnce(new Error("Authentication failed")) + .mockResolvedValueOnce(mockViewer); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "login"]); + + expect(stderrSpy).toHaveBeenCalledWith( + "Existing token is invalid. Starting new authentication...", + ); + expect(saveToken).toHaveBeenCalledWith("test-token"); + }); + + it("bypasses existing token check with --force", async () => { + vi.mocked(resolveApiToken).mockReturnValue({ + token: "existing-token", + source: "stored", + }); + vi.mocked(validateToken).mockResolvedValue(mockViewer); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "login", "--force"]); + + // Should not check existing token; should prompt and save + expect(saveToken).toHaveBeenCalledWith("test-token"); + }); + + it("shows error detail when token validation fails", async () => { + vi.mocked(validateToken).mockRejectedValue(new Error("Network timeout")); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "login"]); + + expect(stderrSpy).toHaveBeenCalledWith( + "Token validation failed: Network timeout", + ); + expect(exitSpy).toHaveBeenCalledWith(1); + }); + + it("exits when no token is provided", async () => { + // Override readline mock to return empty string + const { createInterface } = await import("node:readline"); + vi.mocked(createInterface).mockReturnValue({ + question: vi.fn((_q: string, cb: (a: string) => void) => cb("")), + close: vi.fn(), + } as unknown as ReturnType<typeof createInterface>); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "login"]); + + expect(stderrSpy).toHaveBeenCalledWith( + "No token provided. Authentication cancelled.", + ); + expect(exitSpy).toHaveBeenCalledWith(1); + }); +}); + +describe("auth status", () => { + let stdoutSpy: ReturnType<typeof vi.spyOn>; + + beforeEach(() => { + vi.clearAllMocks(); + stdoutSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + vi.spyOn(process, "exit").mockImplementation(() => undefined as never); + }); + + it("reports authenticated with user info when token is valid", async () => { + vi.mocked(resolveApiToken).mockReturnValue({ + token: "valid-token", + source: "stored", + }); + vi.mocked(validateToken).mockResolvedValue(mockViewer); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "status"]); + + const output = JSON.parse(stdoutSpy.mock.calls[0][0] as string); + expect(output).toEqual({ + authenticated: true, + source: "~/.linearis/token", + user: { id: "user-1", name: "Test User", email: "test@example.com" }, + }); + }); + + it("reports unauthenticated when no token is found", async () => { + vi.mocked(resolveApiToken).mockImplementation(() => { + throw new Error("No API token found"); + }); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "status"]); + + const output = JSON.parse(stdoutSpy.mock.calls[0][0] as string); + expect(output).toEqual({ + authenticated: false, + message: "No API token found. Run 'linearis auth login' to authenticate.", + }); + }); + + it("reports unauthenticated when token is invalid", async () => { + vi.mocked(resolveApiToken).mockReturnValue({ + token: "bad-token", + source: "env", + }); + vi.mocked(validateToken).mockRejectedValue( + new Error("Authentication failed"), + ); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "status"]); + + const output = JSON.parse(stdoutSpy.mock.calls[0][0] as string); + expect(output).toEqual({ + authenticated: false, + source: "LINEAR_API_TOKEN env var", + message: + "Token is invalid or expired. Run 'linearis auth login' to reauthenticate.", + }); + }); + + it("maps all token sources to human-readable labels", async () => { + vi.mocked(validateToken).mockResolvedValue(mockViewer); + + const sourceLabels: Record<string, string> = { + flag: "--api-token flag", + env: "LINEAR_API_TOKEN env var", + stored: "~/.linearis/token", + legacy: "~/.linear_api_token (deprecated)", + }; + + for (const [source, label] of Object.entries(sourceLabels)) { + vi.mocked(resolveApiToken).mockReturnValue({ + token: "t", + source: source as "flag" | "env" | "stored" | "legacy", + }); + stdoutSpy.mockClear(); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "status"]); + + const output = JSON.parse(stdoutSpy.mock.calls[0][0] as string); + expect(output.source).toBe(label); + } + }); +}); + +describe("auth logout", () => { + let stdoutSpy: ReturnType<typeof vi.spyOn>; + + beforeEach(() => { + vi.clearAllMocks(); + stdoutSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + vi.spyOn(process, "exit").mockImplementation(() => undefined as never); + }); + + it("clears token and outputs success message", async () => { + vi.mocked(resolveApiToken).mockImplementation(() => { + throw new Error("No API token found"); + }); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "logout"]); + + expect(clearToken).toHaveBeenCalled(); + const output = JSON.parse(stdoutSpy.mock.calls[0][0] as string); + expect(output).toEqual({ message: "Authentication token removed." }); + }); + + it("warns when token is still active via env var after logout", async () => { + vi.mocked(resolveApiToken).mockReturnValue({ + token: "env-token", + source: "env", + }); + + const program = createProgram(); + await program.parseAsync(["node", "test", "auth", "logout"]); + + expect(clearToken).toHaveBeenCalled(); + const output = JSON.parse(stdoutSpy.mock.calls[0][0] as string); + expect(output).toEqual({ + message: "Authentication token removed.", + warning: "A token is still active via LINEAR_API_TOKEN env var.", + }); + }); +}); diff --git a/tests/unit/commands/issues.test.ts b/tests/unit/commands/issues.test.ts new file mode 100644 index 0000000..9085056 --- /dev/null +++ b/tests/unit/commands/issues.test.ts @@ -0,0 +1,203 @@ +// tests/unit/commands/issues.test.ts +import { Command } from "commander"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock all external dependencies before importing the module under test +vi.mock("../../../src/common/context.js", () => ({ + createContext: vi.fn(() => ({ + gql: { request: vi.fn() }, + sdk: { sdk: {} }, + })), +})); + +vi.mock("../../../src/common/output.js", async (importOriginal) => { + const actual = + await importOriginal<typeof import("../../../src/common/output.js")>(); + return { + ...actual, + outputSuccess: vi.fn(), + }; +}); + +vi.mock("../../../src/resolvers/user-resolver.js", () => ({ + resolveUserId: vi.fn().mockResolvedValue("resolved-user-uuid"), +})); + +vi.mock("../../../src/resolvers/team-resolver.js", () => ({ + resolveTeamId: vi.fn().mockResolvedValue("resolved-team-uuid"), +})); + +vi.mock("../../../src/resolvers/issue-resolver.js", () => ({ + resolveIssueId: vi.fn().mockResolvedValue("resolved-issue-uuid"), +})); + +vi.mock("../../../src/resolvers/project-resolver.js", () => ({ + resolveProjectId: vi.fn().mockResolvedValue("resolved-project-uuid"), +})); + +vi.mock("../../../src/resolvers/label-resolver.js", () => ({ + resolveLabelIds: vi.fn().mockResolvedValue(["resolved-label-uuid"]), +})); + +vi.mock("../../../src/resolvers/milestone-resolver.js", () => ({ + resolveMilestoneId: vi.fn().mockResolvedValue("resolved-milestone-uuid"), +})); + +vi.mock("../../../src/resolvers/cycle-resolver.js", () => ({ + resolveCycleId: vi.fn().mockResolvedValue("resolved-cycle-uuid"), +})); + +vi.mock("../../../src/resolvers/status-resolver.js", () => ({ + resolveStatusId: vi.fn().mockResolvedValue("resolved-status-uuid"), +})); + +vi.mock("../../../src/services/issue-service.js", () => ({ + createIssue: vi.fn().mockResolvedValue({ id: "new-issue-id" }), + updateIssue: vi.fn().mockResolvedValue({ id: "updated-issue-id" }), + getIssue: vi.fn().mockResolvedValue({ + id: "resolved-issue-uuid", + team: { id: "team-uuid", key: "ENG" }, + project: { name: "My Project" }, + labels: { nodes: [] }, + }), + getIssueByIdentifier: vi.fn(), + listIssues: vi.fn().mockResolvedValue([]), + searchIssues: vi.fn().mockResolvedValue([]), +})); + +vi.mock("../../../src/services/issue-relation-service.js", () => ({ + createIssueRelation: vi.fn(), + deleteIssueRelation: vi.fn(), + findIssueRelation: vi.fn(), +})); + +import { setupIssuesCommands } from "../../../src/commands/issues.js"; +import { resolveUserId } from "../../../src/resolvers/user-resolver.js"; +import { + createIssue, + updateIssue, +} from "../../../src/services/issue-service.js"; + +function createProgram(): Command { + const program = new Command(); + program.option("--api-token <token>"); + setupIssuesCommands(program); + return program; +} + +describe("issues create --assignee", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(console, "log").mockImplementation(() => {}); + vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(process, "exit").mockImplementation(() => undefined as never); + }); + + it("resolves assignee name to UUID before creating issue", async () => { + const program = createProgram(); + await program.parseAsync([ + "node", + "test", + "issues", + "create", + "Fix login bug", + "--team", + "ENG", + "--assignee", + "John Doe", + ]); + + expect(resolveUserId).toHaveBeenCalledWith(expect.anything(), "John Doe"); + expect(createIssue).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ assigneeId: "resolved-user-uuid" }), + ); + }); + + it("resolves assignee email to UUID before creating issue", async () => { + const program = createProgram(); + await program.parseAsync([ + "node", + "test", + "issues", + "create", + "Fix login bug", + "--team", + "ENG", + "--assignee", + "john@example.com", + ]); + + expect(resolveUserId).toHaveBeenCalledWith( + expect.anything(), + "john@example.com", + ); + expect(createIssue).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ assigneeId: "resolved-user-uuid" }), + ); + }); + + it("does not call resolveUserId when --assignee is omitted", async () => { + const program = createProgram(); + await program.parseAsync([ + "node", + "test", + "issues", + "create", + "Fix login bug", + "--team", + "ENG", + ]); + + expect(resolveUserId).not.toHaveBeenCalled(); + expect(createIssue).toHaveBeenCalledWith( + expect.anything(), + expect.not.objectContaining({ assigneeId: expect.anything() }), + ); + }); +}); + +describe("issues update --assignee", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(console, "log").mockImplementation(() => {}); + vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(process, "exit").mockImplementation(() => undefined as never); + }); + + it("resolves assignee name to UUID before updating issue", async () => { + const program = createProgram(); + await program.parseAsync([ + "node", + "test", + "issues", + "update", + "ENG-42", + "--assignee", + "Jane Smith", + ]); + + expect(resolveUserId).toHaveBeenCalledWith(expect.anything(), "Jane Smith"); + expect(updateIssue).toHaveBeenCalledWith( + expect.anything(), + "resolved-issue-uuid", + expect.objectContaining({ assigneeId: "resolved-user-uuid" }), + ); + }); + + it("does not call resolveUserId when --assignee is omitted", async () => { + const program = createProgram(); + await program.parseAsync([ + "node", + "test", + "issues", + "update", + "ENG-42", + "--title", + "New title", + ]); + + expect(resolveUserId).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/common/auth.test.ts b/tests/unit/common/auth.test.ts new file mode 100644 index 0000000..aa73dd9 --- /dev/null +++ b/tests/unit/common/auth.test.ts @@ -0,0 +1,73 @@ +import fs from "node:fs"; +import os from "node:os"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("node:fs"); +vi.mock("node:os"); + +// Mock token-storage module +vi.mock("../../../src/common/token-storage.js", () => ({ + getStoredToken: vi.fn(), +})); + +import { getApiToken } from "../../../src/common/auth.js"; +import { getStoredToken } from "../../../src/common/token-storage.js"; + +describe("getApiToken", () => { + const originalEnv = process.env.LINEAR_API_TOKEN; + + beforeEach(() => { + vi.clearAllMocks(); + delete process.env.LINEAR_API_TOKEN; + vi.mocked(os.homedir).mockReturnValue("/home/testuser"); + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.LINEAR_API_TOKEN = originalEnv; + } else { + delete process.env.LINEAR_API_TOKEN; + } + }); + + it("returns --api-token flag when provided", () => { + const token = getApiToken({ apiToken: "flag-token" }); + expect(token).toBe("flag-token"); + }); + + it("returns LINEAR_API_TOKEN env var as second priority", () => { + process.env.LINEAR_API_TOKEN = "env-token"; + const token = getApiToken({}); + expect(token).toBe("env-token"); + }); + + it("returns decrypted stored token as third priority", () => { + vi.mocked(getStoredToken).mockReturnValue("stored-token"); + vi.mocked(fs.existsSync).mockReturnValue(false); + + const token = getApiToken({}); + expect(token).toBe("stored-token"); + }); + + it("reads legacy ~/.linear_api_token as fourth priority with deprecation warning", () => { + vi.mocked(getStoredToken).mockReturnValue(null); + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue("legacy-token\n"); + const stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + const token = getApiToken({}); + expect(token).toBe("legacy-token"); + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining("deprecated"), + ); + + stderrSpy.mockRestore(); + }); + + it("throws when no token found anywhere", () => { + vi.mocked(getStoredToken).mockReturnValue(null); + vi.mocked(fs.existsSync).mockReturnValue(false); + + expect(() => getApiToken({})).toThrow("No API token found"); + }); +}); diff --git a/tests/unit/common/encryption.test.ts b/tests/unit/common/encryption.test.ts new file mode 100644 index 0000000..145366e --- /dev/null +++ b/tests/unit/common/encryption.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { decryptToken, encryptToken } from "../../../src/common/encryption.js"; + +describe("encryptToken", () => { + it("returns a string different from the input", () => { + const token = "lin_api_abc123def456"; + const encrypted = encryptToken(token); + expect(encrypted).not.toBe(token); + expect(typeof encrypted).toBe("string"); + }); + + it("produces different ciphertext each call (random IV)", () => { + const token = "lin_api_abc123def456"; + const a = encryptToken(token); + const b = encryptToken(token); + expect(a).not.toBe(b); + }); + + it("includes v1 version prefix", () => { + const encrypted = encryptToken("lin_api_abc123def456"); + expect(encrypted).toMatch(/^v1:/); + }); +}); + +describe("decryptToken", () => { + it("round-trips: decrypt(encrypt(token)) === token", () => { + const token = "lin_api_abc123def456"; + const encrypted = encryptToken(token); + const decrypted = decryptToken(encrypted); + expect(decrypted).toBe(token); + }); + + it("decrypts legacy unversioned format (iv:ciphertext)", () => { + // Encrypt with current function, then strip the v1: prefix to simulate legacy + const token = "lin_api_legacy_test"; + const encrypted = encryptToken(token); + const legacy = encrypted.replace(/^v1:/, ""); + expect(decryptToken(legacy)).toBe(token); + }); + + it("throws on malformed input", () => { + expect(() => decryptToken("not-valid-encrypted-data")).toThrow(); + }); + + it("throws on empty string", () => { + expect(() => decryptToken("")).toThrow(); + }); + + it("throws on corrupted IV (wrong length)", () => { + // Valid legacy format (hex:hex) but IV is only 4 bytes instead of 16 + expect(() => decryptToken("aabbccdd:aabbccdd")).toThrow("corrupted IV"); + }); + + it("throws on unsupported version prefix", () => { + const encrypted = encryptToken("lin_api_test"); + const v99 = encrypted.replace(/^v1:/, "v99:"); + expect(() => decryptToken(v99)).toThrow( + "Unsupported token encryption version: v99", + ); + }); +}); diff --git a/tests/unit/common/errors.test.ts b/tests/unit/common/errors.test.ts new file mode 100644 index 0000000..ab21346 --- /dev/null +++ b/tests/unit/common/errors.test.ts @@ -0,0 +1,97 @@ +// tests/unit/common/errors.test.ts +import { describe, expect, it } from "vitest"; +import { + AUTH_ERROR_CODE, + AuthenticationError, + invalidParameterError, + isAuthError, + multipleMatchesError, + notFoundError, + requiresParameterError, +} from "../../../src/common/errors.js"; + +describe("notFoundError", () => { + it("creates error with entity and identifier", () => { + const err = notFoundError("Team", "ABC"); + expect(err.message).toBe('Team "ABC" not found'); + }); + + it("includes context when provided", () => { + const err = notFoundError("Cycle", "Sprint 1", "for team ENG"); + expect(err.message).toBe('Cycle "Sprint 1" for team ENG not found'); + }); +}); + +describe("multipleMatchesError", () => { + it("creates error with matches and disambiguation hint", () => { + const err = multipleMatchesError( + "cycle", + "Sprint", + ["id-1", "id-2"], + "use an ID", + ); + expect(err.message).toContain('Multiple cycles found matching "Sprint"'); + expect(err.message).toContain("id-1, id-2"); + expect(err.message).toContain("use an ID"); + }); +}); + +describe("invalidParameterError", () => { + it("creates error with parameter and reason", () => { + const err = invalidParameterError("--limit", "requires positive integer"); + expect(err.message).toBe("Invalid --limit: requires positive integer"); + }); +}); + +describe("requiresParameterError", () => { + it("creates error with flag dependency", () => { + const err = requiresParameterError("--around-active", "--team"); + expect(err.message).toBe("--around-active requires --team to be specified"); + }); +}); + +describe("AuthenticationError", () => { + it("creates error with default message", () => { + const err = new AuthenticationError(); + expect(err.message).toBe("Linear API authentication failed."); + expect(err.name).toBe("AuthenticationError"); + }); + + it("creates error with custom details", () => { + const err = new AuthenticationError("Token expired"); + expect(err.details).toBe("Token expired"); + }); +}); + +describe("isAuthError", () => { + it("returns true for AuthenticationError", () => { + expect(isAuthError(new AuthenticationError())).toBe(true); + }); + + it("returns true for exact 'Authentication required' message", () => { + expect(isAuthError(new Error("Authentication required"))).toBe(true); + }); + + it("returns true for exact 'Unauthorized' message", () => { + expect(isAuthError(new Error("Unauthorized"))).toBe(true); + }); + + it("returns false for unrelated errors", () => { + expect(isAuthError(new Error("Team not found"))).toBe(false); + }); + + it("returns false for errors that merely contain auth keywords", () => { + expect( + isAuthError(new Error("Failed to update authentication settings")), + ).toBe(false); + expect(isAuthError(new Error("Unauthorized access to resource"))).toBe( + false, + ); + }); +}); + +describe("AUTH_ERROR_CODE", () => { + it("is 42", () => { + expect(AUTH_ERROR_CODE).toBe(42); + }); +}); diff --git a/tests/unit/common/identifier.test.ts b/tests/unit/common/identifier.test.ts new file mode 100644 index 0000000..5e7aacb --- /dev/null +++ b/tests/unit/common/identifier.test.ts @@ -0,0 +1,53 @@ +// tests/unit/common/identifier.test.ts +import { describe, expect, it } from "vitest"; +import { + isUuid, + parseIssueIdentifier, + tryParseIssueIdentifier, +} from "../../../src/common/identifier.js"; + +describe("isUuid", () => { + it("returns true for valid UUID", () => { + expect(isUuid("550e8400-e29b-41d4-a716-446655440000")).toBe(true); + }); + + it("returns false for issue identifier", () => { + expect(isUuid("ABC-123")).toBe(false); + }); + + it("returns false for plain string", () => { + expect(isUuid("not-a-uuid")).toBe(false); + }); +}); + +describe("parseIssueIdentifier", () => { + it("parses valid identifier", () => { + const result = parseIssueIdentifier("ABC-123"); + expect(result).toEqual({ teamKey: "ABC", issueNumber: 123 }); + }); + + it("throws on invalid format", () => { + expect(() => parseIssueIdentifier("invalid")).toThrow( + "Invalid issue identifier", + ); + }); + + it("throws on non-numeric issue number", () => { + expect(() => parseIssueIdentifier("ABC-XYZ")).toThrow( + "Invalid issue number", + ); + }); +}); + +describe("tryParseIssueIdentifier", () => { + it("returns parsed identifier for valid input", () => { + expect(tryParseIssueIdentifier("ABC-123")).toEqual({ + teamKey: "ABC", + issueNumber: 123, + }); + }); + + it("returns null for invalid input", () => { + expect(tryParseIssueIdentifier("invalid")).toBeNull(); + }); +}); diff --git a/tests/unit/common/output.test.ts b/tests/unit/common/output.test.ts new file mode 100644 index 0000000..e82d1e4 --- /dev/null +++ b/tests/unit/common/output.test.ts @@ -0,0 +1,135 @@ +// tests/unit/common/output.test.ts +import { describe, expect, it, vi } from "vitest"; +import { AuthenticationError } from "../../../src/common/errors.js"; +import { + handleCommand, + outputAuthError, + outputError, + outputSuccess, + parseLimit, +} from "../../../src/common/output.js"; + +describe("outputSuccess", () => { + it("writes JSON to stdout", () => { + const spy = vi.spyOn(console, "log").mockImplementation(() => {}); + outputSuccess({ id: "123", title: "Test" }); + expect(spy).toHaveBeenCalledWith( + JSON.stringify({ id: "123", title: "Test" }, null, 2), + ); + spy.mockRestore(); + }); +}); + +describe("outputError", () => { + it("writes error JSON to stderr and exits", () => { + const stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const exitSpy = vi + .spyOn(process, "exit") + .mockImplementation(() => undefined as never); + + outputError(new Error("something failed")); + + expect(stderrSpy).toHaveBeenCalledWith( + JSON.stringify({ error: "something failed" }, null, 2), + ); + expect(exitSpy).toHaveBeenCalledWith(1); + + stderrSpy.mockRestore(); + exitSpy.mockRestore(); + }); +}); + +describe("handleCommand", () => { + it("calls the wrapped function", async () => { + const fn = vi.fn().mockResolvedValue(undefined); + const wrapped = handleCommand(fn); + await wrapped("arg1", "arg2"); + expect(fn).toHaveBeenCalledWith("arg1", "arg2"); + }); + + it("catches errors and outputs them", async () => { + const fn = vi.fn().mockRejectedValue(new Error("boom")); + const stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const exitSpy = vi + .spyOn(process, "exit") + .mockImplementation(() => undefined as never); + + const wrapped = handleCommand(fn); + await wrapped(); + + expect(stderrSpy).toHaveBeenCalledWith( + JSON.stringify({ error: "boom" }, null, 2), + ); + + stderrSpy.mockRestore(); + exitSpy.mockRestore(); + }); +}); + +describe("handleCommand with AuthenticationError", () => { + it("calls outputAuthError for AuthenticationError", async () => { + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const exitSpy = vi + .spyOn(process, "exit") + .mockImplementation(() => undefined as never); + + const handler = handleCommand(async () => { + throw new AuthenticationError("expired"); + }); + + await handler(); + + const output = JSON.parse(consoleSpy.mock.calls[0][0] as string); + expect(output.error).toBe("AUTHENTICATION_REQUIRED"); + expect(exitSpy).toHaveBeenCalledWith(42); + + consoleSpy.mockRestore(); + exitSpy.mockRestore(); + }); +}); + +describe("parseLimit", () => { + it("parses valid integer string", () => { + expect(parseLimit("50")).toBe(50); + }); + + it("parses single digit", () => { + expect(parseLimit("1")).toBe(1); + }); + + it("throws on non-numeric string", () => { + expect(() => parseLimit("foo")).toThrow(); + }); + + it("throws on zero", () => { + expect(() => parseLimit("0")).toThrow(); + }); + + it("throws on negative number", () => { + expect(() => parseLimit("-1")).toThrow(); + }); +}); + +describe("outputAuthError", () => { + it("outputs structured JSON with AUTHENTICATION_REQUIRED", () => { + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const exitSpy = vi + .spyOn(process, "exit") + .mockImplementation(() => undefined as never); + + const err = new AuthenticationError("Token expired"); + outputAuthError(err); + + const output = JSON.parse(consoleSpy.mock.calls[0][0] as string); + expect(output.error).toBe("AUTHENTICATION_REQUIRED"); + expect(output.message).toBe("Linear API authentication failed."); + expect(output.details).toBe("Token expired"); + expect(output.action).toBe("USER_ACTION_REQUIRED"); + expect(output.instruction).toContain("linearis auth"); + expect(output.exit_code).toBe(42); + expect(exitSpy).toHaveBeenCalledWith(42); + + consoleSpy.mockRestore(); + exitSpy.mockRestore(); + }); +}); diff --git a/tests/unit/common/token-storage.test.ts b/tests/unit/common/token-storage.test.ts new file mode 100644 index 0000000..194582d --- /dev/null +++ b/tests/unit/common/token-storage.test.ts @@ -0,0 +1,154 @@ +import fs from "node:fs"; +import os from "node:os"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock fs and os modules +vi.mock("node:fs"); +vi.mock("node:os"); + +// Mock encryption module +vi.mock("../../../src/common/encryption.js", () => ({ + encryptToken: vi.fn((token: string) => `encrypted:${token}`), + decryptToken: vi.fn((encrypted: string) => + encrypted.replace("encrypted:", ""), + ), +})); + +import { decryptToken } from "../../../src/common/encryption.js"; +import { + clearToken, + ensureTokenDir, + getStoredToken, + getTokenDir, + saveToken, +} from "../../../src/common/token-storage.js"; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("getTokenDir", () => { + beforeEach(() => { + vi.mocked(os.homedir).mockReturnValue("/home/testuser"); + }); + + it("returns ~/.linearis path", () => { + expect(getTokenDir()).toBe("/home/testuser/.linearis"); + }); +}); + +describe("ensureTokenDir", () => { + beforeEach(() => { + vi.mocked(os.homedir).mockReturnValue("/home/testuser"); + }); + + it("creates directory with 0700 permissions", () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + vi.mocked(fs.mkdirSync).mockReturnValue(undefined); + + ensureTokenDir(); + + expect(fs.mkdirSync).toHaveBeenCalledWith("/home/testuser/.linearis", { + recursive: true, + mode: 0o700, + }); + }); + + it("fixes permissions if directory exists", () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.chmodSync).mockReturnValue(undefined); + + ensureTokenDir(); + + expect(fs.mkdirSync).not.toHaveBeenCalled(); + expect(fs.chmodSync).toHaveBeenCalledWith( + "/home/testuser/.linearis", + 0o700, + ); + }); +}); + +describe("saveToken", () => { + beforeEach(() => { + vi.mocked(os.homedir).mockReturnValue("/home/testuser"); + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.writeFileSync).mockReturnValue(undefined); + vi.mocked(fs.chmodSync).mockReturnValue(undefined); + }); + + it("writes encrypted token to ~/.linearis/token", () => { + saveToken("my-api-token"); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + "/home/testuser/.linearis/token", + "encrypted:my-api-token", + "utf8", + ); + }); + + it("sets file permissions to 0600", () => { + saveToken("my-api-token"); + + expect(fs.chmodSync).toHaveBeenCalledWith( + "/home/testuser/.linearis/token", + 0o600, + ); + }); +}); + +describe("getStoredToken", () => { + beforeEach(() => { + vi.mocked(os.homedir).mockReturnValue("/home/testuser"); + }); + + it("returns decrypted token when file exists", () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue("encrypted:my-api-token"); + + const token = getStoredToken(); + expect(token).toBe("my-api-token"); + }); + + it("returns null when file does not exist", () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + + const token = getStoredToken(); + expect(token).toBeNull(); + }); + + it("returns null when token file is corrupted", () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue("corrupted-data"); + vi.mocked(decryptToken).mockImplementationOnce(() => { + throw new Error("Invalid encrypted token format"); + }); + + const token = getStoredToken(); + expect(token).toBeNull(); + }); +}); + +describe("clearToken", () => { + beforeEach(() => { + vi.mocked(os.homedir).mockReturnValue("/home/testuser"); + }); + + it("removes token file if it exists", () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.unlinkSync).mockReturnValue(undefined); + + clearToken(); + + expect(fs.unlinkSync).toHaveBeenCalledWith( + "/home/testuser/.linearis/token", + ); + }); + + it("does nothing if token file does not exist", () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + + clearToken(); + + expect(fs.unlinkSync).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/common/usage.test.ts b/tests/unit/common/usage.test.ts new file mode 100644 index 0000000..daebdcc --- /dev/null +++ b/tests/unit/common/usage.test.ts @@ -0,0 +1,235 @@ +import { Command } from "commander"; +import { describe, expect, it } from "vitest"; +import { + type DomainMeta, + formatDomainUsage, + formatOverview, +} from "../../../src/common/usage.js"; + +describe("formatOverview", () => { + it("formats overview with version, auth, and all domain summaries", () => { + const metas: DomainMeta[] = [ + { + name: "issues", + summary: "work items with status, priority, assignee, labels", + context: "", + arguments: {}, + seeAlso: [], + }, + { + name: "teams", + summary: "organizational units owning issues and cycles", + context: "", + arguments: {}, + seeAlso: [], + }, + ]; + + const result = formatOverview("2025.12.3", metas); + + expect(result).toContain("linearis v2025.12.3"); + expect(result).toContain("CLI for Linear.app"); + expect(result).toContain( + "auth: linearis auth login | --api-token <token> | LINEAR_API_TOKEN | ~/.linearis/token", + ); + expect(result).toContain("output: JSON"); + expect(result).toContain("ids: UUID or human-readable"); + expect(result).toContain("domains:"); + expect(result).toContain("issues"); + expect(result).toContain( + "work items with status, priority, assignee, labels", + ); + expect(result).toContain("teams"); + expect(result).toContain("organizational units owning issues and cycles"); + expect(result).toContain("detail: linearis <domain> usage"); + }); + + it("pads domain names for alignment", () => { + const metas: DomainMeta[] = [ + { + name: "issues", + summary: "short", + context: "", + arguments: {}, + seeAlso: [], + }, + { + name: "milestones", + summary: "longer name", + context: "", + arguments: {}, + seeAlso: [], + }, + ]; + + const result = formatOverview("1.0.0", metas); + const lines = result.split("\n"); + const issuesLine = lines.find((l) => l.includes("issues")); + const milestonesLine = lines.find((l) => l.includes("milestones")); + + // Both summaries should start at the same column + expect(issuesLine?.indexOf("short")).toBe( + milestonesLine?.indexOf("longer name"), + ); + }); +}); + +describe("formatDomainUsage", () => { + it("formats domain with commands, arguments, options, and see-also", () => { + const domain = new Command("issues").description("Issue operations"); + domain + .command("list") + .description("list issues with optional filters") + .option("--team <team>", "filter by team") + .option("--limit <number>", "max results", "50"); + domain.command("read <issue>").description("get full issue details"); + domain + .command("create <title>") + .description("create new issue") + .option("--team <team>", "target team"); + // usage subcommand should be excluded from output + domain.command("usage").description("show usage"); + + const meta: DomainMeta = { + name: "issues", + summary: "work items with status, priority, assignee, labels", + context: + "an issue belongs to exactly one team.\nparent-child relationships are supported.", + arguments: { + issue: "issue identifier (UUID or ABC-123)", + title: "string", + }, + seeAlso: ["comments create <issue>", "documents list --issue <issue>"], + }; + + const result = formatDomainUsage(domain, meta); + + // Header + expect(result).toContain( + "linearis issues — work items with status, priority, assignee, labels", + ); + // Context + expect(result).toContain("an issue belongs to exactly one team."); + expect(result).toContain("parent-child relationships are supported."); + // Commands section — should NOT include "usage" subcommand + expect(result).toContain("commands:"); + expect(result).toContain("list [options]"); + expect(result).toContain("list issues with optional filters"); + expect(result).toContain("read <issue>"); + expect(result).toContain("create <title>"); + expect(result).not.toMatch(/^\s+usage\b/m); + // Arguments section + expect(result).toContain("arguments:"); + expect(result).toContain("<issue>"); + expect(result).toContain("issue identifier (UUID or ABC-123)"); + expect(result).toContain("<title>"); + // Options sections + expect(result).toContain("list options:"); + expect(result).toContain("--team <team>"); + expect(result).toContain("--limit <number>"); + expect(result).toContain("(default: 50)"); + expect(result).toContain("create options:"); + // No "read options:" since read has no options + expect(result).not.toContain("read options:"); + // See also + expect(result).toContain( + "see also: comments create <issue>, documents list --issue <issue>", + ); + }); + + it("omits arguments and see-also sections when empty", () => { + const domain = new Command("teams").description("Team operations"); + domain.command("list").description("list all teams"); + + const meta: DomainMeta = { + name: "teams", + summary: "organizational units", + context: "a team owns issues and cycles.", + arguments: {}, + seeAlso: [], + }; + + const result = formatDomainUsage(domain, meta); + + expect(result).toContain("linearis teams — organizational units"); + expect(result).toContain("a team owns issues and cycles."); + expect(result).toContain("list"); + expect(result).not.toContain("arguments:"); + expect(result).not.toContain("see also:"); + }); + + it("handles boolean flags correctly", () => { + const domain = new Command("users").description("User operations"); + domain + .command("list") + .description("list users") + .option("--active", "only show active users"); + + const meta: DomainMeta = { + name: "users", + summary: "workspace members", + context: "users can be assigned to issues.", + arguments: {}, + seeAlso: [], + }; + + const result = formatDomainUsage(domain, meta); + + expect(result).toContain("--active"); + expect(result).toContain("only show active users"); + // Boolean flags should NOT show a default value + expect(result).not.toContain("(default:"); + }); + + it("strips short flags from option display", () => { + const domain = new Command("test").description("Test"); + domain + .command("list") + .description("list items") + .option("-l, --limit <number>", "max results", "25"); + + const meta: DomainMeta = { + name: "test", + summary: "test domain", + context: "test context.", + arguments: {}, + seeAlso: [], + }; + + const result = formatDomainUsage(domain, meta); + + // Should show long flag only + expect(result).toContain("--limit <number>"); + // Should NOT show short flag + expect(result).not.toContain("-l,"); + }); + + it("shows [options] only when command has options but no arguments", () => { + const domain = new Command("test").description("Test"); + domain + .command("list") + .description("with options only") + .option("--team <team>", "filter"); + domain.command("read <id>").description("with arg only"); + domain + .command("create <name>") + .description("with arg and options") + .option("--flag", "a flag"); + + const meta: DomainMeta = { + name: "test", + summary: "test", + context: "test.", + arguments: { id: "identifier", name: "string" }, + seeAlso: [], + }; + + const result = formatDomainUsage(domain, meta); + + expect(result).toContain("list [options]"); + expect(result).toContain("read <id>"); + // create has both args and options — show arg, not [options] + expect(result).toContain("create <name>"); + expect(result).not.toContain("create [options] <name>"); + }); +}); diff --git a/tests/unit/documents-url-parsing.test.ts b/tests/unit/documents-url-parsing.test.ts deleted file mode 100644 index 89dd21d..0000000 --- a/tests/unit/documents-url-parsing.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { extractDocumentIdFromUrl } from "../../src/commands/documents.js"; - -/** - * Unit tests for extractDocumentIdFromUrl - * - * This function extracts the document slug ID from Linear document URLs. - * Linear document URLs follow the format: - * https://linear.app/[workspace]/document/[title-slug]-[slugId] - */ - -describe("extractDocumentIdFromUrl", () => { - describe("valid Linear document URLs", () => { - it("should extract slugId from standard document URL", () => { - const url = "https://linear.app/myworkspace/document/my-document-title-abc123"; - expect(extractDocumentIdFromUrl(url)).toBe("abc123"); - }); - - it("should extract slugId from document URL with long title", () => { - const url = "https://linear.app/workspace/document/this-is-a-very-long-document-title-xyz789"; - expect(extractDocumentIdFromUrl(url)).toBe("xyz789"); - }); - - it("should extract slugId from document URL with numeric slugId", () => { - const url = "https://linear.app/team/document/document-123456"; - expect(extractDocumentIdFromUrl(url)).toBe("123456"); - }); - - it("should handle subdomain linear.app URLs", () => { - const url = "https://app.linear.app/workspace/document/test-doc-slug1"; - expect(extractDocumentIdFromUrl(url)).toBe("slug1"); - }); - - it("should handle URL with query parameters", () => { - const url = "https://linear.app/workspace/document/test-doc-abc?view=full"; - expect(extractDocumentIdFromUrl(url)).toBe("abc"); - }); - - it("should handle URL with hash fragment", () => { - const url = "https://linear.app/workspace/document/test-doc-def#section"; - expect(extractDocumentIdFromUrl(url)).toBe("def"); - }); - }); - - describe("non-Linear URLs", () => { - it("should return null for non-Linear domain", () => { - const url = "https://example.com/workspace/document/test-doc-abc123"; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - - it("should return null for GitHub URLs", () => { - const url = "https://github.com/org/repo/document/readme-abc"; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - - it("should return null for Google Docs URLs", () => { - const url = "https://docs.google.com/document/d/abc123"; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - }); - - describe("Linear URLs without document path", () => { - it("should return null for issue URL", () => { - const url = "https://linear.app/workspace/issue/ABC-123"; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - - it("should return null for project URL", () => { - const url = "https://linear.app/workspace/project/my-project"; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - - it("should return null for settings URL", () => { - const url = "https://linear.app/workspace/settings"; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - - it("should return null for root workspace URL", () => { - const url = "https://linear.app/workspace"; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - }); - - describe("edge cases", () => { - it("should return null for malformed URL", () => { - const url = "not-a-valid-url"; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - - it("should return null for empty string", () => { - const url = ""; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - - it("should return slug when no hyphen in slug (entire slug is ID)", () => { - const url = "https://linear.app/workspace/document/abc123"; - expect(extractDocumentIdFromUrl(url)).toBe("abc123"); - }); - - it("should return null for document path with no slug", () => { - const url = "https://linear.app/workspace/document/"; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - - it("should handle single character slugId", () => { - const url = "https://linear.app/workspace/document/title-x"; - expect(extractDocumentIdFromUrl(url)).toBe("x"); - }); - - it("should handle hyphen at end correctly (returns empty becomes null)", () => { - const url = "https://linear.app/workspace/document/title-"; - expect(extractDocumentIdFromUrl(url)).toBeNull(); - }); - }); -}); diff --git a/tests/unit/file-service-upload.test.ts b/tests/unit/file-service-upload.test.ts deleted file mode 100644 index 0b42320..0000000 --- a/tests/unit/file-service-upload.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { FileService } from "../../src/utils/file-service.js"; - -// Mock fs/promises -vi.mock("fs/promises", () => ({ - access: vi.fn(), - stat: vi.fn(), - readFile: vi.fn(), - mkdir: vi.fn(), - writeFile: vi.fn(), -})); - -// Mock global fetch -const mockFetch = vi.fn(); -vi.stubGlobal("fetch", mockFetch); - -import { access, readFile, stat } from "fs/promises"; - -/** - * Unit tests for FileService.uploadFile() - * - * Tests the file upload functionality including: - * - Successful uploads with proper GraphQL mutation and PUT - * - File not found errors - * - File size validation - * - GraphQL error handling - * - PUT request failures - */ -describe("FileService - uploadFile", () => { - let service: FileService; - const testApiToken = "lin_api_test123"; - - beforeEach(() => { - vi.clearAllMocks(); - service = new FileService(testApiToken); - }); - - describe("successful upload", () => { - it("should upload a file and return the asset URL", async () => { - // Setup file system mocks - vi.mocked(access).mockResolvedValue(undefined); - vi.mocked(stat).mockResolvedValue({ size: 1024 } as any); - vi.mocked(readFile).mockResolvedValue(Buffer.from("test file content")); - - // Setup fetch mocks - GraphQL response, then PUT response - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - data: { - fileUpload: { - success: true, - uploadFile: { - uploadUrl: "https://storage.linear.app/upload/abc123", - assetUrl: "https://uploads.linear.app/abc/file.png", - headers: [ - { key: "x-amz-header", value: "some-value" }, - ], - }, - }, - }, - }), - }) - .mockResolvedValueOnce({ - ok: true, - status: 200, - }); - - const result = await service.uploadFile("/path/to/file.png"); - - expect(result.success).toBe(true); - expect(result.assetUrl).toBe("https://uploads.linear.app/abc/file.png"); - expect(result.filename).toBe("file.png"); - - // Verify GraphQL call - expect(mockFetch).toHaveBeenCalledTimes(2); - const graphqlCall = mockFetch.mock.calls[0]; - expect(graphqlCall[0]).toBe("https://api.linear.app/graphql"); - expect(graphqlCall[1].headers["Authorization"]).toBe(testApiToken); - - // Verify PUT call - const putCall = mockFetch.mock.calls[1]; - expect(putCall[0]).toBe("https://storage.linear.app/upload/abc123"); - expect(putCall[1].method).toBe("PUT"); - expect(putCall[1].headers["x-amz-header"]).toBe("some-value"); - }); - - it("should detect content type from file extension", async () => { - vi.mocked(access).mockResolvedValue(undefined); - vi.mocked(stat).mockResolvedValue({ size: 1024 } as any); - vi.mocked(readFile).mockResolvedValue(Buffer.from("{}")); - - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - data: { - fileUpload: { - success: true, - uploadFile: { - uploadUrl: "https://storage.linear.app/upload/abc123", - assetUrl: "https://uploads.linear.app/abc/data.json", - headers: [], - }, - }, - }, - }), - }) - .mockResolvedValueOnce({ ok: true }); - - await service.uploadFile("/path/to/data.json"); - - // Check that Content-Type was set correctly - const putCall = mockFetch.mock.calls[1]; - expect(putCall[1].headers["Content-Type"]).toBe("application/json"); - }); - - it("should default to application/octet-stream for unknown extensions", async () => { - vi.mocked(access).mockResolvedValue(undefined); - vi.mocked(stat).mockResolvedValue({ size: 1024 } as any); - vi.mocked(readFile).mockResolvedValue(Buffer.from("binary data")); - - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - data: { - fileUpload: { - success: true, - uploadFile: { - uploadUrl: "https://storage.linear.app/upload/abc123", - assetUrl: "https://uploads.linear.app/abc/file.xyz", - headers: [], - }, - }, - }, - }), - }) - .mockResolvedValueOnce({ ok: true }); - - await service.uploadFile("/path/to/file.xyz"); - - const putCall = mockFetch.mock.calls[1]; - expect(putCall[1].headers["Content-Type"]).toBe( - "application/octet-stream", - ); - }); - }); - - describe("file validation errors", () => { - it("should return error when file does not exist", async () => { - vi.mocked(access).mockRejectedValue(new Error("ENOENT")); - - const result = await service.uploadFile("/path/to/nonexistent.png"); - - expect(result.success).toBe(false); - expect(result.error).toContain("File not found"); - expect(mockFetch).not.toHaveBeenCalled(); - }); - - it("should return error when file exceeds size limit", async () => { - vi.mocked(access).mockResolvedValue(undefined); - // 25MB - exceeds 20MB limit - vi.mocked(stat).mockResolvedValue({ size: 25 * 1024 * 1024 } as any); - - const result = await service.uploadFile("/path/to/large-file.zip"); - - expect(result.success).toBe(false); - expect(result.error).toContain("File too large"); - expect(result.error).toContain("25.0MB"); - expect(result.error).toContain("20MB"); - expect(mockFetch).not.toHaveBeenCalled(); - }); - - it("should return error when stat fails", async () => { - vi.mocked(access).mockResolvedValue(undefined); - vi.mocked(stat).mockRejectedValue(new Error("Permission denied")); - - const result = await service.uploadFile("/path/to/file.png"); - - expect(result.success).toBe(false); - expect(result.error).toContain("Cannot read file"); - expect(mockFetch).not.toHaveBeenCalled(); - }); - }); - - describe("GraphQL errors", () => { - it("should return error when GraphQL request fails", async () => { - vi.mocked(access).mockResolvedValue(undefined); - vi.mocked(stat).mockResolvedValue({ size: 1024 } as any); - - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 401, - }); - - const result = await service.uploadFile("/path/to/file.png"); - - expect(result.success).toBe(false); - expect(result.error).toContain("GraphQL request failed"); - expect(result.statusCode).toBe(401); - }); - - it("should return error when GraphQL returns errors array", async () => { - vi.mocked(access).mockResolvedValue(undefined); - vi.mocked(stat).mockResolvedValue({ size: 1024 } as any); - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - errors: [{ message: "Invalid API token" }], - }), - }); - - const result = await service.uploadFile("/path/to/file.png"); - - expect(result.success).toBe(false); - expect(result.error).toContain("Invalid API token"); - }); - - it("should return error when fileUpload returns success=false", async () => { - vi.mocked(access).mockResolvedValue(undefined); - vi.mocked(stat).mockResolvedValue({ size: 1024 } as any); - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - data: { - fileUpload: { - success: false, - }, - }, - }), - }); - - const result = await service.uploadFile("/path/to/file.png"); - - expect(result.success).toBe(false); - expect(result.error).toContain("success=false"); - }); - - it("should return error when uploadUrl is missing", async () => { - vi.mocked(access).mockResolvedValue(undefined); - vi.mocked(stat).mockResolvedValue({ size: 1024 } as any); - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - data: { - fileUpload: { - success: true, - uploadFile: { - assetUrl: "https://uploads.linear.app/abc/file.png", - // uploadUrl missing - }, - }, - }, - }), - }); - - const result = await service.uploadFile("/path/to/file.png"); - - expect(result.success).toBe(false); - expect(result.error).toContain("Missing uploadUrl or assetUrl"); - }); - }); - - describe("PUT request errors", () => { - it("should return error when PUT fails", async () => { - vi.mocked(access).mockResolvedValue(undefined); - vi.mocked(stat).mockResolvedValue({ size: 1024 } as any); - vi.mocked(readFile).mockResolvedValue(Buffer.from("test content")); - - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - data: { - fileUpload: { - success: true, - uploadFile: { - uploadUrl: "https://storage.linear.app/upload/abc123", - assetUrl: "https://uploads.linear.app/abc/file.png", - headers: [], - }, - }, - }, - }), - }) - .mockResolvedValueOnce({ - ok: false, - status: 403, - }); - - const result = await service.uploadFile("/path/to/file.png"); - - expect(result.success).toBe(false); - expect(result.error).toContain("File upload failed"); - expect(result.statusCode).toBe(403); - }); - - it("should handle network errors during PUT", async () => { - vi.mocked(access).mockResolvedValue(undefined); - vi.mocked(stat).mockResolvedValue({ size: 1024 } as any); - vi.mocked(readFile).mockResolvedValue(Buffer.from("test content")); - - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - data: { - fileUpload: { - success: true, - uploadFile: { - uploadUrl: "https://storage.linear.app/upload/abc123", - assetUrl: "https://uploads.linear.app/abc/file.png", - headers: [], - }, - }, - }, - }), - }) - .mockRejectedValueOnce(new Error("Network error")); - - const result = await service.uploadFile("/path/to/file.png"); - - expect(result.success).toBe(false); - expect(result.error).toContain("Network error"); - }); - }); -}); diff --git a/tests/unit/graphql-attachments-service.test.ts b/tests/unit/graphql-attachments-service.test.ts deleted file mode 100644 index 0b13660..0000000 --- a/tests/unit/graphql-attachments-service.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { GraphQLAttachmentsService } from "../../src/utils/graphql-attachments-service.js"; - -/** - * Unit tests for GraphQLAttachmentsService - * - * These tests verify the attachments service methods with mocked GraphQL responses. - * For integration tests with real API, see tests/integration/attachments-cli.test.ts - */ - -describe("GraphQLAttachmentsService", () => { - let mockGraphQLService: any; - let service: GraphQLAttachmentsService; - - const mockAttachment = { - id: "attach-123", - title: "Test Attachment", - subtitle: "Test subtitle", - url: "https://example.com/file.pdf", - createdAt: "2025-01-01T00:00:00.000Z", - updatedAt: "2025-01-01T00:00:00.000Z", - issue: { - id: "issue-1", - identifier: "TEST-123", - title: "Test Issue", - }, - creator: { id: "user-1", name: "Test User" }, - }; - - beforeEach(() => { - mockGraphQLService = { - rawRequest: vi.fn(), - }; - service = new GraphQLAttachmentsService(mockGraphQLService); - }); - - describe("createAttachment()", () => { - it("should create an attachment successfully", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - attachmentCreate: { success: true, attachment: mockAttachment }, - }); - - const result = await service.createAttachment({ - issueId: "issue-1", - url: "https://example.com/file.pdf", - title: "Test Attachment", - }); - - expect(mockGraphQLService.rawRequest).toHaveBeenCalledWith( - expect.stringContaining("mutation AttachmentCreate"), - { - input: { - issueId: "issue-1", - url: "https://example.com/file.pdf", - title: "Test Attachment", - }, - }, - ); - expect(result).toEqual(mockAttachment); - }); - - it("should create attachment with all optional fields", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - attachmentCreate: { success: true, attachment: mockAttachment }, - }); - - await service.createAttachment({ - issueId: "issue-1", - url: "https://example.com/file.pdf", - title: "Test Attachment", - subtitle: "Test subtitle", - commentBody: "Check out this file", - iconUrl: "https://example.com/icon.png", - }); - - expect(mockGraphQLService.rawRequest).toHaveBeenCalledWith( - expect.any(String), - { - input: { - issueId: "issue-1", - url: "https://example.com/file.pdf", - title: "Test Attachment", - subtitle: "Test subtitle", - commentBody: "Check out this file", - iconUrl: "https://example.com/icon.png", - }, - }, - ); - }); - - it("should throw error with context when creation fails", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - attachmentCreate: { success: false, attachment: null }, - }); - - await expect( - service.createAttachment({ - issueId: "issue-1", - url: "https://example.com/file.pdf", - title: "Test Attachment", - }), - ).rejects.toThrow( - 'Failed to create attachment on issue issue-1 for URL "https://example.com/file.pdf"', - ); - }); - }); - - describe("deleteAttachment()", () => { - it("should delete an attachment successfully", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - attachmentDelete: { success: true }, - }); - - const result = await service.deleteAttachment("attach-123"); - - expect(mockGraphQLService.rawRequest).toHaveBeenCalledWith( - expect.stringContaining("mutation AttachmentDelete"), - { id: "attach-123" }, - ); - expect(result).toBe(true); - }); - - it("should throw error with attachment ID when deletion fails", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - attachmentDelete: { success: false }, - }); - - await expect(service.deleteAttachment("attach-123")).rejects.toThrow( - "Failed to delete attachment: attach-123", - ); - }); - }); - - describe("listAttachments()", () => { - it("should list attachments for an issue", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - issue: { attachments: { nodes: [mockAttachment] } }, - }); - - const result = await service.listAttachments("issue-1"); - - expect(mockGraphQLService.rawRequest).toHaveBeenCalledWith( - expect.stringContaining("query ListAttachments"), - { issueId: "issue-1" }, - ); - expect(result).toHaveLength(1); - expect(result[0]).toEqual(mockAttachment); - }); - - it("should return empty array when issue has no attachments", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - issue: { attachments: { nodes: [] } }, - }); - - const result = await service.listAttachments("issue-1"); - - expect(result).toEqual([]); - }); - - it("should throw error when issue not found", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - issue: null, - }); - - await expect(service.listAttachments("nonexistent")).rejects.toThrow( - "Issue not found: nonexistent", - ); - }); - }); -}); diff --git a/tests/unit/graphql-documents-service.test.ts b/tests/unit/graphql-documents-service.test.ts deleted file mode 100644 index 9c8e7de..0000000 --- a/tests/unit/graphql-documents-service.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { GraphQLDocumentsService } from "../../src/utils/graphql-documents-service.js"; - -/** - * Unit tests for GraphQLDocumentsService - * - * These tests verify the documents service methods with mocked GraphQL responses. - * For integration tests with real API, see tests/integration/documents-cli.test.ts - */ - -describe("GraphQLDocumentsService", () => { - let mockGraphQLService: any; - let service: GraphQLDocumentsService; - - const mockDocument = { - id: "doc-123", - title: "Test Document", - content: "Test content", - slugId: "test-slug", - url: "https://linear.app/test/document/test-slug", - icon: null, - color: null, - createdAt: "2025-01-01T00:00:00.000Z", - updatedAt: "2025-01-01T00:00:00.000Z", - creator: { id: "user-1", name: "Test User" }, - project: { id: "proj-1", name: "Test Project" }, - trashed: false, - }; - - beforeEach(() => { - mockGraphQLService = { - rawRequest: vi.fn(), - }; - service = new GraphQLDocumentsService(mockGraphQLService); - }); - - describe("createDocument()", () => { - it("should create a document successfully", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - documentCreate: { success: true, document: mockDocument }, - }); - - const result = await service.createDocument({ - title: "Test Document", - content: "Test content", - projectId: "proj-1", - }); - - expect(mockGraphQLService.rawRequest).toHaveBeenCalledWith( - expect.stringContaining("mutation DocumentCreate"), - { - input: { - title: "Test Document", - content: "Test content", - projectId: "proj-1", - }, - }, - ); - expect(result).toEqual(mockDocument); - }); - - it("should throw error with context when creation fails", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - documentCreate: { success: false, document: null }, - }); - - await expect( - service.createDocument({ - title: "Failed Doc", - projectId: "proj-1", - teamId: "team-1", - }), - ).rejects.toThrow( - 'Failed to create document "Failed Doc" in project proj-1 for team team-1', - ); - }); - - it("should throw error with title only when no project/team", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - documentCreate: { success: false, document: null }, - }); - - await expect( - service.createDocument({ title: "Orphan Doc" }), - ).rejects.toThrow('Failed to create document "Orphan Doc"'); - }); - }); - - describe("updateDocument()", () => { - it("should update a document successfully", async () => { - const updatedDoc = { ...mockDocument, title: "Updated Title" }; - mockGraphQLService.rawRequest.mockResolvedValue({ - documentUpdate: { success: true, document: updatedDoc }, - }); - - const result = await service.updateDocument("doc-123", { - title: "Updated Title", - }); - - expect(mockGraphQLService.rawRequest).toHaveBeenCalledWith( - expect.stringContaining("mutation DocumentUpdate"), - { id: "doc-123", input: { title: "Updated Title" } }, - ); - expect(result.title).toBe("Updated Title"); - }); - - it("should throw error with document ID when update fails", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - documentUpdate: { success: false, document: null }, - }); - - await expect( - service.updateDocument("doc-123", { title: "New Title" }), - ).rejects.toThrow("Failed to update document: doc-123"); - }); - }); - - describe("getDocument()", () => { - it("should get a document by ID", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - document: mockDocument, - }); - - const result = await service.getDocument("doc-123"); - - expect(mockGraphQLService.rawRequest).toHaveBeenCalledWith( - expect.stringContaining("query GetDocument"), - { id: "doc-123" }, - ); - expect(result).toEqual(mockDocument); - }); - - it("should throw error when document not found", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - document: null, - }); - - await expect(service.getDocument("nonexistent")).rejects.toThrow( - "Document not found: nonexistent", - ); - }); - }); - - describe("listDocuments()", () => { - it("should list documents without filter", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - documents: { nodes: [mockDocument] }, - }); - - const result = await service.listDocuments(); - - expect(mockGraphQLService.rawRequest).toHaveBeenCalledWith( - expect.stringContaining("query ListDocuments"), - { first: 50, filter: undefined }, - ); - expect(result).toHaveLength(1); - expect(result[0]).toEqual(mockDocument); - }); - - it("should list documents with project filter", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - documents: { nodes: [mockDocument] }, - }); - - const result = await service.listDocuments({ - projectId: "proj-1", - first: 100, - }); - - expect(mockGraphQLService.rawRequest).toHaveBeenCalledWith( - expect.stringContaining("query ListDocuments"), - { - first: 100, - filter: { project: { id: { eq: "proj-1" } } }, - }, - ); - expect(result).toHaveLength(1); - }); - - it("should return empty array when no documents", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - documents: { nodes: [] }, - }); - - const result = await service.listDocuments(); - - expect(result).toEqual([]); - }); - }); - - describe("deleteDocument()", () => { - it("should delete a document successfully", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - documentDelete: { success: true }, - }); - - const result = await service.deleteDocument("doc-123"); - - expect(mockGraphQLService.rawRequest).toHaveBeenCalledWith( - expect.stringContaining("mutation DocumentDelete"), - { id: "doc-123" }, - ); - expect(result).toBe(true); - }); - - it("should throw error with document ID when deletion fails", async () => { - mockGraphQLService.rawRequest.mockResolvedValue({ - documentDelete: { success: false }, - }); - - await expect(service.deleteDocument("doc-123")).rejects.toThrow( - "Failed to delete document: doc-123", - ); - }); - }); -}); diff --git a/tests/unit/graphql-issues-service-team.test.ts b/tests/unit/graphql-issues-service-team.test.ts deleted file mode 100644 index 4dd9bcd..0000000 --- a/tests/unit/graphql-issues-service-team.test.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { GraphQLIssuesService } from "../../src/utils/graphql-issues-service.js"; -import type { GraphQLService } from "../../src/utils/graphql-service.js"; - -/** - * Unit tests for team resolution validation in GraphQLIssuesService - * - * These tests verify the fix for issue #16: - * - `--team` filter silently matches wrong team when using key/name - * - * Root cause: GraphQL `or` filter with undefined variables matches anything, - * so when teamKey or teamName is undefined, `{ eq: undefined }` matches any team. - * - * The fix: After batch resolve, validate that the returned team actually - * matches the requested identifier before using it. - */ - -describe("GraphQLIssuesService - Team Resolution Validation", () => { - let mockGraphQLService: { - rawRequest: ReturnType<typeof vi.fn>; - }; - let service: GraphQLIssuesService; - - beforeEach(() => { - mockGraphQLService = { - rawRequest: vi.fn(), - }; - - service = new GraphQLIssuesService( - mockGraphQLService as unknown as GraphQLService, - ); - }); - - describe("searchIssues - team validation", () => { - it("should not match wrong team when team key is not found", async () => { - // Setup: batch resolve returns a DIFFERENT team (the bug behaviour) - // This happens because the `or` filter with undefined matches anything - mockGraphQLService.rawRequest.mockResolvedValue({ - teams: { - nodes: [ - { id: "wrong-team-id", key: "OTHER", name: "Other Team" }, - ], - }, - projects: { nodes: [] }, - users: { nodes: [] }, - }); - - // Even though a team was returned, it doesn't match the requested key - await expect( - service.searchIssues({ - query: "test", - teamId: "NONEXISTENT", - limit: 10, - }), - ).rejects.toThrow('Team "NONEXISTENT" not found'); - }); - - it("should not match wrong team when team name is not found", async () => { - // Setup: batch resolve returns a DIFFERENT team - mockGraphQLService.rawRequest.mockResolvedValue({ - teams: { - nodes: [ - { id: "wrong-team-id", key: "OTHER", name: "Other Team" }, - ], - }, - projects: { nodes: [] }, - users: { nodes: [] }, - }); - - // Team name doesn't match what was requested - await expect( - service.searchIssues({ - query: "test", - teamId: "Nonexistent Team", - limit: 10, - }), - ).rejects.toThrow('Team "Nonexistent Team" not found'); - }); - - it("should accept team when key matches exactly", async () => { - // Setup: batch resolve returns the correct team - mockGraphQLService.rawRequest - .mockResolvedValueOnce({ - teams: { - nodes: [ - { id: "correct-team-id", key: "ENG", name: "Engineering" }, - ], - }, - projects: { nodes: [] }, - users: { nodes: [] }, - }) - .mockResolvedValueOnce({ - issues: { nodes: [] }, - }); - - // Should not throw - team key matches - const result = await service.searchIssues({ - query: "test", - teamId: "ENG", - limit: 10, - }); - - expect(result).toEqual([]); - }); - - it("should accept team when name matches exactly", async () => { - // Setup: batch resolve returns the correct team - mockGraphQLService.rawRequest - .mockResolvedValueOnce({ - teams: { - nodes: [ - { id: "correct-team-id", key: "ENG", name: "Engineering" }, - ], - }, - projects: { nodes: [] }, - users: { nodes: [] }, - }) - .mockResolvedValueOnce({ - issues: { nodes: [] }, - }); - - // Should not throw - team name matches - const result = await service.searchIssues({ - query: "test", - teamId: "Engineering", - limit: 10, - }); - - expect(result).toEqual([]); - }); - - it("should accept team when name matches case-insensitively", async () => { - // Setup: batch resolve returns team with different case - mockGraphQLService.rawRequest - .mockResolvedValueOnce({ - teams: { - nodes: [ - { id: "correct-team-id", key: "ENG", name: "Engineering" }, - ], - }, - projects: { nodes: [] }, - users: { nodes: [] }, - }) - .mockResolvedValueOnce({ - issues: { nodes: [] }, - }); - - // Should not throw - team name matches case-insensitively - const result = await service.searchIssues({ - query: "test", - teamId: "engineering", - limit: 10, - }); - - expect(result).toEqual([]); - }); - - it("should accept team key case-insensitively", async () => { - // Setup: batch resolve returns team with uppercase key - mockGraphQLService.rawRequest - .mockResolvedValueOnce({ - teams: { - nodes: [ - { id: "correct-team-id", key: "ENG", name: "Engineering" }, - ], - }, - projects: { nodes: [] }, - users: { nodes: [] }, - }) - .mockResolvedValueOnce({ - issues: { nodes: [] }, - }); - - // Should not throw - team key matches case-insensitively (user typed lowercase) - const result = await service.searchIssues({ - query: "test", - teamId: "eng", - limit: 10, - }); - - expect(result).toEqual([]); - }); - - it("should accept team key containing digits at end", async () => { - // Bug: regex /^[A-Z]+$/ excludes digits, so "ABC1" is treated as team name - // This causes lookup by name "ABC1" instead of key "ABC1" - // The GraphQL query then uses teamName="ABC1", not teamKey="ABC1" - // Since no team has name "ABC1", the `or` filter with undefined teamKey - // matches any team, returning a wrong result. - // - // To test this properly, we need to verify the QUERY is built correctly. - // We do this by checking which variables are passed to rawRequest. - mockGraphQLService.rawRequest - .mockResolvedValueOnce({ - teams: { - nodes: [ - { id: "abc1-team-id", key: "ABC1", name: "Alpha Bravo Charlie" }, - ], - }, - projects: { nodes: [] }, - users: { nodes: [] }, - }) - .mockResolvedValueOnce({ - issues: { nodes: [] }, - }); - - await service.searchIssues({ - query: "test", - teamId: "ABC1", - limit: 10, - }); - - // The key assertion: teamKey should be set to the value, teamName to null - // Bug: code sets teamName="ABC1" instead of teamKey="ABC1" - // Fix: explicitly set both (one to value, one to null) for Linear's GraphQL or filter - const batchResolveCall = mockGraphQLService.rawRequest.mock.calls[0]; - const variables = batchResolveCall[1]; - expect(variables.teamKey).toBe("ABC1"); - expect(variables.teamName).toBeNull(); - }); - - it("should accept team key starting with digits", async () => { - mockGraphQLService.rawRequest - .mockResolvedValueOnce({ - teams: { - nodes: [ - { id: "42x-team-id", key: "42X", name: "Forty Two X" }, - ], - }, - projects: { nodes: [] }, - users: { nodes: [] }, - }) - .mockResolvedValueOnce({ - issues: { nodes: [] }, - }); - - await service.searchIssues({ - query: "test", - teamId: "42X", - limit: 10, - }); - - // The key assertion: teamKey should be set to the value, teamName to null - const batchResolveCall = mockGraphQLService.rawRequest.mock.calls[0]; - const variables = batchResolveCall[1]; - expect(variables.teamKey).toBe("42X"); - expect(variables.teamName).toBeNull(); - }); - - it("should pass through UUID without validation", async () => { - const uuid = "550e8400-e29b-41d4-a716-446655440000"; - - // Setup: no batch resolve needed for UUID - mockGraphQLService.rawRequest.mockResolvedValue({ - issues: { nodes: [] }, - }); - - // UUID should be used directly without batch resolve - const result = await service.searchIssues({ - query: "test", - teamId: uuid, - limit: 10, - }); - - expect(result).toEqual([]); - // Should only call once (the search query), not batch resolve - expect(mockGraphQLService.rawRequest).toHaveBeenCalledTimes(1); - }); - }); - - describe("createIssue - team validation", () => { - it("should not match wrong team when team key is not found", async () => { - // Setup: batch resolve returns a DIFFERENT team - mockGraphQLService.rawRequest.mockResolvedValue({ - teams: { - nodes: [ - { id: "wrong-team-id", key: "OTHER", name: "Other Team" }, - ], - }, - projects: { nodes: [] }, - labels: { nodes: [] }, - parentIssues: { nodes: [] }, - }); - - await expect( - service.createIssue({ - title: "Test Issue", - teamId: "NONEXISTENT", - }), - ).rejects.toThrow('Team "NONEXISTENT" not found'); - }); - - it("should accept team when key matches exactly", async () => { - // Setup: batch resolve returns correct team, then create succeeds - mockGraphQLService.rawRequest - .mockResolvedValueOnce({ - teams: { - nodes: [ - { id: "correct-team-id", key: "ENG", name: "Engineering" }, - ], - }, - projects: { nodes: [] }, - labels: { nodes: [] }, - parentIssues: { nodes: [] }, - }) - .mockResolvedValueOnce({ - issueCreate: { - success: true, - issue: { - id: "new-issue-id", - identifier: "ENG-123", - title: "Test Issue", - description: null, - priority: 0, - estimate: null, - team: { id: "correct-team-id", key: "ENG", name: "Engineering" }, - state: { id: "state-1", name: "Backlog" }, - assignee: null, - project: null, - cycle: null, - projectMilestone: null, - labels: { nodes: [] }, - comments: { nodes: [] }, - parent: null, - children: { nodes: [] }, - createdAt: "2025-01-01T00:00:00Z", - updatedAt: "2025-01-01T00:00:00Z", - }, - }, - }); - - const result = await service.createIssue({ - title: "Test Issue", - teamId: "ENG", - }); - - expect(result.identifier).toBe("ENG-123"); - }); - - it("should accept team key containing digits", async () => { - // Bug: regex /^[A-Z]+$/ excludes digits, so "DEV2" is treated as team name - mockGraphQLService.rawRequest - .mockResolvedValueOnce({ - teams: { - nodes: [ - { id: "dev2-team-id", key: "DEV2", name: "Development Team 2" }, - ], - }, - projects: { nodes: [] }, - labels: { nodes: [] }, - parentIssues: { nodes: [] }, - }) - .mockResolvedValueOnce({ - issueCreate: { - success: true, - issue: { - id: "new-issue-id", - identifier: "DEV2-456", - title: "Test Issue", - description: null, - priority: 0, - estimate: null, - team: { id: "dev2-team-id", key: "DEV2", name: "Development Team 2" }, - state: { id: "state-1", name: "Triage" }, - assignee: null, - project: null, - cycle: null, - projectMilestone: null, - labels: { nodes: [] }, - comments: { nodes: [] }, - parent: null, - children: { nodes: [] }, - createdAt: "2025-01-01T00:00:00Z", - updatedAt: "2025-01-01T00:00:00Z", - }, - }, - }); - - await service.createIssue({ - title: "Test Issue", - teamId: "DEV2", - }); - - // The key assertion: teamKey should be set to the value, teamName to null - // Bug: code sets teamName="DEV2" instead of teamKey="DEV2" - // Fix: explicitly set both (one to value, one to null) for Linear's GraphQL or filter - const batchResolveCall = mockGraphQLService.rawRequest.mock.calls[0]; - const variables = batchResolveCall[1]; - expect(variables.teamKey).toBe("DEV2"); - expect(variables.teamName).toBeNull(); - }); - }); -}); diff --git a/tests/unit/linear-service-cycles.test.ts b/tests/unit/linear-service-cycles.test.ts deleted file mode 100644 index 163a197..0000000 --- a/tests/unit/linear-service-cycles.test.ts +++ /dev/null @@ -1,550 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { LinearService } from "../../src/utils/linear-service.js"; - -/** - * Unit tests for LinearService cycle methods - * - * These tests verify the new cycle-related methods added in PR #4: - * - getCycles() - Fetch cycles with pagination - * - getCycleById() - Fetch single cycle with issues - * - resolveCycleId() - Resolve cycle by name or ID - * - * Note: These tests use mocks to avoid hitting the real Linear API. - * For integration tests with real API, see tests/integration/ - */ - -describe("LinearService - Cycle Methods", () => { - let mockClient: any; - let service: LinearService; - - beforeEach(() => { - // Create mock Linear client - mockClient = { - cycles: vi.fn(), - cycle: vi.fn(), - teams: vi.fn(), - }; - - // Create service with mock client - service = new LinearService("fake-token"); - // @ts-ignore - Replace internal client with mock - service.client = mockClient; - }); - - describe("getCycles()", () => { - it("should fetch cycles without filters", async () => { - const mockCycles = [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01"), - endsAt: new Date("2025-01-15"), - isActive: true, - isPrevious: false, - isNext: false, - progress: 0.5, - issueCountHistory: [], - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - ]; - - mockClient.cycles.mockResolvedValue({ - nodes: mockCycles, - }); - - const result = await service.getCycles(); - - expect(mockClient.cycles).toHaveBeenCalledWith({ - filter: undefined, - orderBy: "createdAt", - first: 250, - }); - expect(result).toHaveLength(1); - expect(result[0].id).toBe("cycle-1"); - expect(result[0].name).toBe("Sprint 1"); - expect(result[0].team.key).toBe("ENG"); - }); - - it("should fetch cycles with team filter", async () => { - const mockTeam = { - id: "team-1", - key: "ENG", - name: "Engineering", - }; - - // Mock resolveTeamId - vi.spyOn(service, "resolveTeamId").mockResolvedValue("team-1"); - - const mockCycles = [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01"), - endsAt: new Date("2025-01-15"), - isActive: true, - isPrevious: false, - isNext: false, - progress: 0.5, - issueCountHistory: [], - team: Promise.resolve(mockTeam), - }, - ]; - - mockClient.cycles.mockResolvedValue({ - nodes: mockCycles, - }); - - const result = await service.getCycles("ENG"); - - expect(service.resolveTeamId).toHaveBeenCalledWith("ENG"); - expect(mockClient.cycles).toHaveBeenCalledWith({ - filter: { team: { id: { eq: "team-1" } } }, - orderBy: "createdAt", - first: 250, - }); - expect(result).toHaveLength(1); - }); - - it("should fetch only active cycles when activeOnly is true", async () => { - const mockCycles = [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01"), - endsAt: new Date("2025-01-15"), - isActive: true, - isPrevious: false, - isNext: false, - progress: 0.5, - issueCountHistory: [], - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - ]; - - mockClient.cycles.mockResolvedValue({ - nodes: mockCycles, - }); - - const result = await service.getCycles(undefined, true); - - expect(mockClient.cycles).toHaveBeenCalledWith({ - filter: { isActive: { eq: true } }, - orderBy: "createdAt", - first: 250, - }); - expect(result).toHaveLength(1); - expect(result[0].isActive).toBe(true); - }); - - it("should convert dates to ISO 8601 strings", async () => { - const mockCycles = [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01T00:00:00Z"), - endsAt: new Date("2025-01-15T23:59:59Z"), - isActive: true, - isPrevious: false, - isNext: false, - progress: 0.5, - issueCountHistory: [], - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - ]; - - mockClient.cycles.mockResolvedValue({ - nodes: mockCycles, - }); - - const result = await service.getCycles(); - - expect(typeof result[0].startsAt).toBe("string"); - expect(typeof result[0].endsAt).toBe("string"); - // Verify ISO 8601 format - expect(result[0].startsAt).toBe("2025-01-01T00:00:00.000Z"); - expect(result[0].endsAt).toBe("2025-01-15T23:59:59.000Z"); - }); - }); - - describe("getCycleById()", () => { - it("should fetch cycle with issues by ID", async () => { - const mockTeam = { - id: "team-1", - key: "ENG", - name: "Engineering", - }; - - const mockIssue = { - id: "issue-1", - identifier: "ENG-123", - title: "Test issue", - description: "Test description", - priority: 1, - estimate: 3, - state: Promise.resolve({ id: "state-1", name: "In Progress" }), - assignee: Promise.resolve({ id: "user-1", name: "John Doe" }), - team: Promise.resolve(mockTeam), - project: Promise.resolve({ id: "proj-1", name: "Project 1" }), - labels: () => - Promise.resolve({ nodes: [{ id: "label-1", name: "bug" }] }), - createdAt: new Date("2025-01-01"), - updatedAt: new Date("2025-01-02"), - }; - - const mockCycle = { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01"), - endsAt: new Date("2025-01-15"), - isActive: true, - progress: 0.5, - issueCountHistory: [], - team: Promise.resolve(mockTeam), - issues: vi.fn().mockResolvedValue({ - nodes: [mockIssue], - }), - }; - - mockClient.cycle.mockResolvedValue(mockCycle); - - const result = await service.getCycleById("cycle-1", 50); - - expect(mockClient.cycle).toHaveBeenCalledWith("cycle-1"); - expect(mockCycle.issues).toHaveBeenCalledWith({ first: 50 }); - expect(result.id).toBe("cycle-1"); - expect(result.issues).toHaveLength(1); - expect(result.issues[0].identifier).toBe("ENG-123"); - expect(result.issues[0].labels).toHaveLength(1); - }); - - it("should use default issues limit of 50", async () => { - const mockCycle = { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01"), - endsAt: new Date("2025-01-15"), - isActive: true, - progress: 0.5, - issueCountHistory: [], - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - issues: vi.fn().mockResolvedValue({ nodes: [] }), - }; - - mockClient.cycle.mockResolvedValue(mockCycle); - - await service.getCycleById("cycle-1"); - - expect(mockCycle.issues).toHaveBeenCalledWith({ first: 50 }); - }); - }); - - describe("resolveCycleId()", () => { - it("should return UUID as-is", async () => { - const uuid = "550e8400-e29b-41d4-a716-446655440000"; - const result = await service.resolveCycleId(uuid); - expect(result).toBe(uuid); - }); - - it("should resolve cycle by name", async () => { - const mockCycles = [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01"), - isActive: true, - isNext: false, - isPrevious: false, - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - ]; - - mockClient.cycles.mockResolvedValue({ - nodes: mockCycles, - }); - - const result = await service.resolveCycleId("Sprint 1"); - - expect(mockClient.cycles).toHaveBeenCalledWith({ - filter: { name: { eq: "Sprint 1" } }, - first: 10, - }); - expect(result).toBe("cycle-1"); - }); - - it("should resolve cycle with team filter", async () => { - vi.spyOn(service, "resolveTeamId").mockResolvedValue("team-1"); - - const mockCycles = [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01"), - isActive: true, - isNext: false, - isPrevious: false, - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - ]; - - mockClient.cycles.mockResolvedValue({ - nodes: mockCycles, - }); - - const result = await service.resolveCycleId("Sprint 1", "ENG"); - - expect(service.resolveTeamId).toHaveBeenCalledWith("ENG"); - expect(mockClient.cycles).toHaveBeenCalledWith({ - filter: { - name: { eq: "Sprint 1" }, - team: { id: { eq: "team-1" } }, - }, - first: 10, - }); - expect(result).toBe("cycle-1"); - }); - - it("should throw error when cycle not found", async () => { - mockClient.cycles.mockResolvedValue({ nodes: [] }); - - await expect(service.resolveCycleId("NonExistent")).rejects.toThrow( - 'Cycle "NonExistent" not found', - ); - }); - - it("should throw error when cycle not found for team", async () => { - vi.spyOn(service, "resolveTeamId").mockResolvedValue("team-1"); - mockClient.cycles.mockResolvedValue({ nodes: [] }); - - await expect(service.resolveCycleId("NonExistent", "ENG")).rejects - .toThrow( - 'Cycle "NonExistent" for team ENG not found', - ); - }); - - it("should disambiguate by preferring active cycle", async () => { - const mockCycles = [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01"), - isActive: false, - isNext: false, - isPrevious: true, - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - { - id: "cycle-2", - name: "Sprint 1", - number: 2, - startsAt: new Date("2025-01-15"), - isActive: true, - isNext: false, - isPrevious: false, - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - ]; - - mockClient.cycles.mockResolvedValue({ nodes: mockCycles }); - - const result = await service.resolveCycleId("Sprint 1"); - - expect(result).toBe("cycle-2"); // Active cycle chosen - }); - - it("should disambiguate by preferring next cycle when no active", async () => { - const mockCycles = [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01"), - isActive: false, - isNext: false, - isPrevious: true, - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - { - id: "cycle-2", - name: "Sprint 1", - number: 2, - startsAt: new Date("2025-01-15"), - isActive: false, - isNext: true, - isPrevious: false, - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - ]; - - mockClient.cycles.mockResolvedValue({ nodes: mockCycles }); - - const result = await service.resolveCycleId("Sprint 1"); - - expect(result).toBe("cycle-2"); // Next cycle chosen - }); - - it("should throw error for ambiguous cycle name", async () => { - const mockCycles = [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: new Date("2025-01-01"), - isActive: false, - isNext: false, - isPrevious: false, - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - { - id: "cycle-2", - name: "Sprint 1", - number: 2, - startsAt: new Date("2025-01-15"), - isActive: false, - isNext: false, - isPrevious: false, - team: Promise.resolve({ id: "team-2", key: "PROD", name: "Product" }), - }, - ]; - - mockClient.cycles.mockResolvedValue({ nodes: mockCycles }); - - await expect(service.resolveCycleId("Sprint 1")).rejects.toThrow( - /Multiple cycles found matching "Sprint 1"/, - ); - }); - }); - - describe("resolveCycleId - error cases", () => { - it("should throw when cycle not found", async () => { - mockClient.cycles.mockResolvedValue({ - nodes: [], - }); - - await expect(service.resolveCycleId("Nonexistent Cycle")).rejects.toThrow( - 'Cycle "Nonexistent Cycle" not found', - ); - }); - - it("should throw when multiple cycles match and none are active/next/previous", async () => { - const mockCycles = [ - { - id: "cycle-1", - name: "Sprint 1", - number: 1, - startsAt: "2025-01-01", - isActive: false, - isNext: false, - isPrevious: false, - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - { - id: "cycle-2", - name: "Sprint 1", - number: 2, - startsAt: "2025-02-01", - isActive: false, - isNext: false, - isPrevious: false, - team: Promise.resolve({ id: "team-2", key: "PROD", name: "Product" }), - }, - ]; - - mockClient.cycles.mockResolvedValue({ nodes: mockCycles }); - - await expect(service.resolveCycleId("Sprint 1")).rejects.toThrow( - /Multiple cycles found matching.*Sprint 1/, - ); - }); - - it("should prefer active cycle when multiple matches exist", async () => { - const mockCycles = [ - { - id: "cycle-inactive", - name: "Sprint 1", - number: 1, - startsAt: "2025-01-01", - isActive: false, - isNext: false, - isPrevious: false, - team: Promise.resolve({ - id: "team-1", - key: "ENG", - name: "Engineering", - }), - }, - { - id: "cycle-active", - name: "Sprint 1", - number: 2, - startsAt: "2025-02-01", - isActive: true, - isNext: false, - isPrevious: false, - team: Promise.resolve({ id: "team-2", key: "PROD", name: "Product" }), - }, - ]; - - mockClient.cycles.mockResolvedValue({ nodes: mockCycles }); - - const result = await service.resolveCycleId("Sprint 1"); - expect(result).toBe("cycle-active"); - }); - }); -}); diff --git a/tests/unit/resolvers/cycle-resolver.test.ts b/tests/unit/resolvers/cycle-resolver.test.ts new file mode 100644 index 0000000..1efb975 --- /dev/null +++ b/tests/unit/resolvers/cycle-resolver.test.ts @@ -0,0 +1,53 @@ +// tests/unit/resolvers/cycle-resolver.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { LinearSdkClient } from "../../../src/client/linear-client.js"; +import { resolveCycleId } from "../../../src/resolvers/cycle-resolver.js"; + +function mockSdkClient( + cycleNodes: Array<{ + id: string; + name?: string; + isActive?: boolean; + isNext?: boolean; + isPrevious?: boolean; + number?: number; + startsAt?: string; + }>, +) { + const teams = vi.fn().mockResolvedValue({ nodes: [{ id: "team-uuid" }] }); + const cycles = vi.fn().mockResolvedValue({ nodes: cycleNodes }); + // Mock cycle.team as a resolved property + cycleNodes.forEach((node) => { + Object.defineProperty(node, "team", { + value: Promise.resolve({ + id: "team-uuid", + key: "ENG", + name: "Engineering", + }), + enumerable: false, + }); + }); + return { sdk: { teams, cycles } } as unknown as LinearSdkClient; +} + +describe("resolveCycleId", () => { + it("returns UUID as-is", async () => { + const client = mockSdkClient([]); + const result = await resolveCycleId( + client, + "550e8400-e29b-41d4-a716-446655440000", + ); + expect(result).toBe("550e8400-e29b-41d4-a716-446655440000"); + }); + + it("resolves single matching cycle by name", async () => { + const client = mockSdkClient([{ id: "cycle-uuid", name: "Sprint 1" }]); + const result = await resolveCycleId(client, "Sprint 1"); + expect(result).toBe("cycle-uuid"); + }); + + it("throws when cycle not found", async () => { + const client = mockSdkClient([]); + await expect(resolveCycleId(client, "Nonexistent")).rejects.toThrow(); + }); +}); diff --git a/tests/unit/resolvers/issue-resolver.test.ts b/tests/unit/resolvers/issue-resolver.test.ts new file mode 100644 index 0000000..930937d --- /dev/null +++ b/tests/unit/resolvers/issue-resolver.test.ts @@ -0,0 +1,36 @@ +// tests/unit/resolvers/issue-resolver.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { LinearSdkClient } from "../../../src/client/linear-client.js"; +import { resolveIssueId } from "../../../src/resolvers/issue-resolver.js"; + +function mockSdkClient(nodes: Array<{ id: string }>) { + return { + sdk: { + issues: vi.fn().mockResolvedValue({ nodes }), + }, + } as unknown as LinearSdkClient; +} + +describe("resolveIssueId", () => { + it("returns UUID as-is", async () => { + const client = mockSdkClient([]); + const result = await resolveIssueId( + client, + "550e8400-e29b-41d4-a716-446655440000", + ); + expect(result).toBe("550e8400-e29b-41d4-a716-446655440000"); + }); + + it("resolves ABC-123 identifier", async () => { + const client = mockSdkClient([{ id: "issue-uuid" }]); + const result = await resolveIssueId(client, "ENG-42"); + expect(result).toBe("issue-uuid"); + }); + + it("throws when issue not found", async () => { + const client = mockSdkClient([]); + await expect(resolveIssueId(client, "ENG-999")).rejects.toThrow( + 'Issue "ENG-999" not found', + ); + }); +}); diff --git a/tests/unit/resolvers/label-resolver.test.ts b/tests/unit/resolvers/label-resolver.test.ts new file mode 100644 index 0000000..a199004 --- /dev/null +++ b/tests/unit/resolvers/label-resolver.test.ts @@ -0,0 +1,53 @@ +// tests/unit/resolvers/label-resolver.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { LinearSdkClient } from "../../../src/client/linear-client.js"; +import { + resolveLabelId, + resolveLabelIds, +} from "../../../src/resolvers/label-resolver.js"; + +function mockSdkClient(nodes: Array<{ id: string; name?: string }>) { + return { + sdk: { + issueLabels: vi.fn().mockResolvedValue({ nodes }), + }, + } as unknown as LinearSdkClient; +} + +describe("resolveLabelId", () => { + it("returns UUID as-is", async () => { + const client = mockSdkClient([]); + const result = await resolveLabelId( + client, + "550e8400-e29b-41d4-a716-446655440000", + ); + expect(result).toBe("550e8400-e29b-41d4-a716-446655440000"); + }); + + it("resolves label by name", async () => { + const client = mockSdkClient([{ id: "label-uuid" }]); + const result = await resolveLabelId(client, "Bug"); + expect(result).toBe("label-uuid"); + }); + + it("throws when label not found", async () => { + const client = mockSdkClient([]); + await expect(resolveLabelId(client, "Nonexistent")).rejects.toThrow( + 'Label "Nonexistent" not found', + ); + }); +}); + +describe("resolveLabelIds", () => { + it("resolves mixed UUIDs and names", async () => { + const client = mockSdkClient([{ id: "label-uuid" }]); + const result = await resolveLabelIds(client, [ + "550e8400-e29b-41d4-a716-446655440000", + "Bug", + ]); + expect(result).toEqual([ + "550e8400-e29b-41d4-a716-446655440000", + "label-uuid", + ]); + }); +}); diff --git a/tests/unit/resolvers/milestone-resolver.test.ts b/tests/unit/resolvers/milestone-resolver.test.ts new file mode 100644 index 0000000..b1485c1 --- /dev/null +++ b/tests/unit/resolvers/milestone-resolver.test.ts @@ -0,0 +1,45 @@ +// tests/unit/resolvers/milestone-resolver.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import type { LinearSdkClient } from "../../../src/client/linear-client.js"; +import { resolveMilestoneId } from "../../../src/resolvers/milestone-resolver.js"; + +function mockGqlClient(...responses: Array<Record<string, unknown>>) { + const request = vi.fn(); + for (const r of responses) { + request.mockResolvedValueOnce(r); + } + return { request } as unknown as GraphQLClient; +} + +function mockSdkClient() { + return { + sdk: { + projects: vi.fn().mockResolvedValue({ nodes: [{ id: "proj-uuid" }] }), + }, + } as unknown as LinearSdkClient; +} + +describe("resolveMilestoneId", () => { + it("returns UUID as-is", async () => { + const gql = mockGqlClient(); + const sdk = mockSdkClient(); + const result = await resolveMilestoneId( + gql, + sdk, + "550e8400-e29b-41d4-a716-446655440000", + ); + expect(result).toBe("550e8400-e29b-41d4-a716-446655440000"); + }); + + it("throws when milestone not found", async () => { + const gql = mockGqlClient( + { project: { projectMilestones: { nodes: [] } } }, + { projectMilestones: { nodes: [] } }, + ); + const sdk = mockSdkClient(); + await expect( + resolveMilestoneId(gql, sdk, "Nonexistent", "My Project"), + ).rejects.toThrow('Milestone "Nonexistent" not found'); + }); +}); diff --git a/tests/unit/resolvers/project-resolver.test.ts b/tests/unit/resolvers/project-resolver.test.ts new file mode 100644 index 0000000..a50999e --- /dev/null +++ b/tests/unit/resolvers/project-resolver.test.ts @@ -0,0 +1,37 @@ +// tests/unit/resolvers/project-resolver.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { LinearSdkClient } from "../../../src/client/linear-client.js"; +import { resolveProjectId } from "../../../src/resolvers/project-resolver.js"; + +function mockSdkClient(nodes: Array<{ id: string }>) { + return { + sdk: { + projects: vi.fn().mockResolvedValue({ nodes }), + }, + } as unknown as LinearSdkClient; +} + +describe("resolveProjectId", () => { + it("returns UUID as-is", async () => { + const client = mockSdkClient([]); + const result = await resolveProjectId( + client, + "550e8400-e29b-41d4-a716-446655440000", + ); + expect(result).toBe("550e8400-e29b-41d4-a716-446655440000"); + expect(client.sdk.projects).not.toHaveBeenCalled(); + }); + + it("resolves project by name", async () => { + const client = mockSdkClient([{ id: "proj-uuid" }]); + const result = await resolveProjectId(client, "Mobile App"); + expect(result).toBe("proj-uuid"); + }); + + it("throws when project not found", async () => { + const client = mockSdkClient([]); + await expect(resolveProjectId(client, "Nonexistent")).rejects.toThrow( + 'Project "Nonexistent" not found', + ); + }); +}); diff --git a/tests/unit/resolvers/status-resolver.test.ts b/tests/unit/resolvers/status-resolver.test.ts new file mode 100644 index 0000000..0d3f193 --- /dev/null +++ b/tests/unit/resolvers/status-resolver.test.ts @@ -0,0 +1,48 @@ +// tests/unit/resolvers/status-resolver.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { LinearSdkClient } from "../../../src/client/linear-client.js"; +import { resolveStatusId } from "../../../src/resolvers/status-resolver.js"; + +function mockSdkClient(nodes: Array<{ id: string }>) { + return { + sdk: { + workflowStates: vi.fn().mockResolvedValue({ nodes }), + }, + } as unknown as LinearSdkClient; +} + +describe("resolveStatusId", () => { + it("returns UUID as-is", async () => { + const client = mockSdkClient([]); + const result = await resolveStatusId( + client, + "550e8400-e29b-41d4-a716-446655440000", + ); + expect(result).toBe("550e8400-e29b-41d4-a716-446655440000"); + }); + + it("resolves status by name", async () => { + const client = mockSdkClient([{ id: "status-uuid" }]); + const result = await resolveStatusId(client, "In Progress"); + expect(result).toBe("status-uuid"); + }); + + it("resolves status by name with team context", async () => { + const client = mockSdkClient([{ id: "status-uuid" }]); + await resolveStatusId(client, "In Progress", "team-uuid"); + expect(client.sdk.workflowStates).toHaveBeenCalledWith({ + filter: { + name: { eqIgnoreCase: "In Progress" }, + team: { id: { eq: "team-uuid" } }, + }, + first: 1, + }); + }); + + it("throws when status not found", async () => { + const client = mockSdkClient([]); + await expect(resolveStatusId(client, "Nonexistent")).rejects.toThrow( + 'Status "Nonexistent" not found', + ); + }); +}); diff --git a/tests/unit/resolvers/team-resolver.test.ts b/tests/unit/resolvers/team-resolver.test.ts new file mode 100644 index 0000000..76b4234 --- /dev/null +++ b/tests/unit/resolvers/team-resolver.test.ts @@ -0,0 +1,50 @@ +// tests/unit/resolvers/team-resolver.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { LinearSdkClient } from "../../../src/client/linear-client.js"; +import { resolveTeamId } from "../../../src/resolvers/team-resolver.js"; + +function mockSdkClient( + ...callResults: Array<{ + nodes: Array<{ id: string; key?: string; name?: string }>; + }> +) { + const teams = vi.fn(); + for (const result of callResults) { + teams.mockResolvedValueOnce(result); + } + return { sdk: { teams } } as unknown as LinearSdkClient; +} + +describe("resolveTeamId", () => { + it("returns UUID as-is without calling SDK", async () => { + const client = mockSdkClient(); + const result = await resolveTeamId( + client, + "550e8400-e29b-41d4-a716-446655440000", + ); + expect(result).toBe("550e8400-e29b-41d4-a716-446655440000"); + expect(client.sdk.teams).not.toHaveBeenCalled(); + }); + + it("resolves team by key", async () => { + const client = mockSdkClient({ nodes: [{ id: "uuid-1", key: "ENG" }] }); + const result = await resolveTeamId(client, "ENG"); + expect(result).toBe("uuid-1"); + }); + + it("falls back to name when key not found", async () => { + const client = mockSdkClient( + { nodes: [] }, + { nodes: [{ id: "uuid-2", name: "Engineering" }] }, + ); + const result = await resolveTeamId(client, "Engineering"); + expect(result).toBe("uuid-2"); + }); + + it("throws when team not found by key or name", async () => { + const client = mockSdkClient({ nodes: [] }, { nodes: [] }); + await expect(resolveTeamId(client, "NOPE")).rejects.toThrow( + 'Team "NOPE" not found', + ); + }); +}); diff --git a/tests/unit/resolvers/user-resolver.test.ts b/tests/unit/resolvers/user-resolver.test.ts new file mode 100644 index 0000000..e66ab16 --- /dev/null +++ b/tests/unit/resolvers/user-resolver.test.ts @@ -0,0 +1,79 @@ +// tests/unit/resolvers/user-resolver.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { LinearSdkClient } from "../../../src/client/linear-client.js"; +import { resolveUserId } from "../../../src/resolvers/user-resolver.js"; + +interface MockUser { + id: string; + name?: string; + email?: string; +} + +function mockSdkClient(...callResults: Array<{ nodes: MockUser[] }>) { + const users = vi.fn(); + for (const result of callResults) { + users.mockResolvedValueOnce(result); + } + return { sdk: { users } } as unknown as LinearSdkClient; +} + +describe("resolveUserId", () => { + it("returns UUID as-is without calling SDK", async () => { + const client = mockSdkClient(); + const result = await resolveUserId( + client, + "550e8400-e29b-41d4-a716-446655440000", + ); + expect(result).toBe("550e8400-e29b-41d4-a716-446655440000"); + expect(client.sdk.users).not.toHaveBeenCalled(); + }); + + it("resolves user by display name", async () => { + const client = mockSdkClient({ + nodes: [ + { id: "user-uuid-1", name: "John Doe", email: "john@example.com" }, + ], + }); + const result = await resolveUserId(client, "John Doe"); + expect(result).toBe("user-uuid-1"); + expect(client.sdk.users).toHaveBeenCalledWith({ + filter: { displayName: { eqIgnoreCase: "John Doe" } }, + first: 10, + }); + }); + + it("falls back to email when name not found", async () => { + const client = mockSdkClient( + { nodes: [] }, + { + nodes: [{ id: "user-uuid-2", name: "Jane", email: "jane@example.com" }], + }, + ); + const result = await resolveUserId(client, "jane@example.com"); + expect(result).toBe("user-uuid-2"); + expect(client.sdk.users).toHaveBeenCalledTimes(2); + expect(client.sdk.users).toHaveBeenNthCalledWith(2, { + filter: { email: { eqIgnoreCase: "jane@example.com" } }, + first: 1, + }); + }); + + it("throws when user not found by name or email", async () => { + const client = mockSdkClient({ nodes: [] }, { nodes: [] }); + await expect(resolveUserId(client, "Nobody")).rejects.toThrow( + 'User "Nobody" not found', + ); + }); + + it("throws when multiple users match by name", async () => { + const client = mockSdkClient({ + nodes: [ + { id: "user-1", name: "Alex Smith", email: "alex1@example.com" }, + { id: "user-2", name: "Alex Smith", email: "alex2@example.com" }, + ], + }); + await expect(resolveUserId(client, "Alex Smith")).rejects.toThrow( + 'Multiple Users found matching "Alex Smith"', + ); + }); +}); diff --git a/tests/unit/services/attachment-service.test.ts b/tests/unit/services/attachment-service.test.ts new file mode 100644 index 0000000..e55a7c2 --- /dev/null +++ b/tests/unit/services/attachment-service.test.ts @@ -0,0 +1,95 @@ +// tests/unit/services/attachment-service.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { + createAttachment, + deleteAttachment, + listAttachments, +} from "../../../src/services/attachment-service.js"; + +function mockGqlClient(response: Record<string, unknown>) { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("createAttachment", () => { + it("returns created attachment", async () => { + const client = mockGqlClient({ + attachmentCreate: { + success: true, + attachment: { + id: "att-1", + title: "Test.pdf", + url: "https://example.com/test.pdf", + }, + }, + }); + const result = await createAttachment(client, { + issueId: "issue-1", + title: "Test.pdf", + url: "https://example.com/test.pdf", + }); + expect(result.id).toBe("att-1"); + }); + + it("throws when creation fails", async () => { + const client = mockGqlClient({ + attachmentCreate: { success: false }, + }); + await expect( + createAttachment(client, { + issueId: "issue-1", + title: "Test.pdf", + url: "https://example.com/test.pdf", + }), + ).rejects.toThrow("Failed to create attachment"); + }); +}); + +describe("deleteAttachment", () => { + it("returns true on success", async () => { + const client = mockGqlClient({ attachmentDelete: { success: true } }); + const result = await deleteAttachment(client, "att-1"); + expect(result).toBe(true); + }); + + it("throws when delete fails", async () => { + const client = mockGqlClient({ attachmentDelete: { success: false } }); + await expect(deleteAttachment(client, "att-1")).rejects.toThrow( + "Failed to delete attachment", + ); + }); +}); + +describe("listAttachments", () => { + it("returns attachments for issue", async () => { + const client = mockGqlClient({ + issue: { + attachments: { + nodes: [ + { id: "1", title: "File1.pdf" }, + { id: "2", title: "File2.pdf" }, + ], + }, + }, + }); + const result = await listAttachments(client, "issue-1"); + expect(result).toHaveLength(2); + }); + + it("returns empty array when no attachments", async () => { + const client = mockGqlClient({ + issue: { attachments: { nodes: [] } }, + }); + const result = await listAttachments(client, "issue-1"); + expect(result).toEqual([]); + }); + + it("throws when issue not found", async () => { + const client = mockGqlClient({ issue: null }); + await expect(listAttachments(client, "missing")).rejects.toThrow( + "not found", + ); + }); +}); diff --git a/tests/unit/services/auth-service.test.ts b/tests/unit/services/auth-service.test.ts new file mode 100644 index 0000000..46469e9 --- /dev/null +++ b/tests/unit/services/auth-service.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { validateToken } from "../../../src/services/auth-service.js"; + +function mockGqlClient(response: Record<string, unknown>): GraphQLClient { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("validateToken", () => { + it("returns viewer on successful validation", async () => { + const client = mockGqlClient({ + viewer: { id: "user-1", name: "Test User", email: "test@example.com" }, + }); + + const result = await validateToken(client); + expect(result).toEqual({ + id: "user-1", + name: "Test User", + email: "test@example.com", + }); + }); + + it("propagates error on invalid token", async () => { + const client = { + request: vi.fn().mockRejectedValue(new Error("Authentication failed")), + } as unknown as GraphQLClient; + + await expect(validateToken(client)).rejects.toThrow( + "Authentication failed", + ); + }); +}); diff --git a/tests/unit/services/comment-service.test.ts b/tests/unit/services/comment-service.test.ts new file mode 100644 index 0000000..0649525 --- /dev/null +++ b/tests/unit/services/comment-service.test.ts @@ -0,0 +1,65 @@ +// tests/unit/services/comment-service.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { createComment } from "../../../src/services/comment-service.js"; + +function mockGqlClient(response: Record<string, unknown>): GraphQLClient { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("createComment", () => { + it("creates comment successfully", async () => { + const client = mockGqlClient({ + commentCreate: { + success: true, + comment: { + id: "comment-1", + body: "This is a comment", + createdAt: "2025-01-15T10:00:00.000Z", + }, + }, + }); + + const result = await createComment(client, { + issueId: "issue-1", + body: "This is a comment", + }); + + expect(result).toEqual({ + id: "comment-1", + body: "This is a comment", + createdAt: "2025-01-15T10:00:00.000Z", + }); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + input: { issueId: "issue-1", body: "This is a comment" }, + }); + }); + + it("throws when creation fails", async () => { + const client = mockGqlClient({ + commentCreate: { + success: false, + comment: null, + }, + }); + + await expect( + createComment(client, { issueId: "issue-1", body: "test" }), + ).rejects.toThrow("Failed to create comment"); + }); + + it("throws when comment is null despite success", async () => { + const client = mockGqlClient({ + commentCreate: { + success: true, + comment: null, + }, + }); + + await expect( + createComment(client, { issueId: "issue-1", body: "test" }), + ).rejects.toThrow("Failed to create comment"); + }); +}); diff --git a/tests/unit/services/cycle-service.test.ts b/tests/unit/services/cycle-service.test.ts new file mode 100644 index 0000000..3f0f83b --- /dev/null +++ b/tests/unit/services/cycle-service.test.ts @@ -0,0 +1,173 @@ +// tests/unit/services/cycle-service.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { getCycle, listCycles } from "../../../src/services/cycle-service.js"; + +function mockGqlClient(response: Record<string, unknown>): GraphQLClient { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("listCycles", () => { + it("returns cycles", async () => { + const client = mockGqlClient({ + cycles: { + nodes: [ + { + id: "cyc-1", + number: 1, + name: "Sprint 1", + startsAt: "2025-01-01", + endsAt: "2025-01-14", + isActive: true, + isNext: false, + isPrevious: false, + }, + ], + pageInfo: { hasNextPage: false, endCursor: "c1" }, + }, + }); + const result = await listCycles(client); + expect(result.nodes).toHaveLength(1); + expect(result.nodes[0].id).toBe("cyc-1"); + expect(result.nodes[0].number).toBe(1); + expect(result.nodes[0].name).toBe("Sprint 1"); + expect(result.nodes[0].startsAt).toBe("2025-01-01"); + expect(result.nodes[0].endsAt).toBe("2025-01-14"); + expect(result.nodes[0].isActive).toBe(true); + expect(result.pageInfo).toEqual({ hasNextPage: false, endCursor: "c1" }); + }); + + it("returns empty result", async () => { + const client = mockGqlClient({ + cycles: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + const result = await listCycles(client); + expect(result.nodes).toEqual([]); + expect(result.pageInfo.hasNextPage).toBe(false); + }); + + it("passes after cursor", async () => { + const client = mockGqlClient({ + cycles: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listCycles(client, undefined, false, { after: "cur1" }); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: "cur1", + filter: {}, + }); + }); + + it("uses default limit of 50", async () => { + const client = mockGqlClient({ + cycles: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listCycles(client); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: undefined, + filter: {}, + }); + }); + + it("filters by team", async () => { + const client = mockGqlClient({ + cycles: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listCycles(client, "team-1"); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: undefined, + filter: { team: { id: { eq: "team-1" } } }, + }); + }); + + it("filters active only", async () => { + const client = mockGqlClient({ + cycles: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listCycles(client, undefined, true); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: undefined, + filter: { isActive: { eq: true } }, + }); + }); + + it("uses fallback name for null name", async () => { + const client = mockGqlClient({ + cycles: { + nodes: [ + { + id: "cyc-2", + number: 3, + name: null, + startsAt: "2025-02-01", + endsAt: "2025-02-14", + isActive: false, + isNext: false, + isPrevious: false, + }, + ], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + const result = await listCycles(client); + expect(result.nodes[0].name).toBe("Cycle 3"); + }); +}); + +describe("getCycle", () => { + it("returns cycle with issues", async () => { + const client = mockGqlClient({ + cycle: { + id: "cyc-1", + number: 1, + name: "Sprint 1", + startsAt: "2025-01-01", + endsAt: "2025-01-14", + isActive: true, + isNext: false, + isPrevious: false, + issues: { + nodes: [ + { + id: "issue-1", + identifier: "ENG-1", + title: "Fix bug", + state: { name: "In Progress" }, + }, + ], + }, + }, + }); + const result = await getCycle(client, "cyc-1"); + expect(result.id).toBe("cyc-1"); + expect(result.name).toBe("Sprint 1"); + expect(result.issues).toHaveLength(1); + expect(result.issues[0].identifier).toBe("ENG-1"); + expect(result.issues[0].state.name).toBe("In Progress"); + }); + + it("throws when cycle not found", async () => { + const client = mockGqlClient({ cycle: null }); + await expect(getCycle(client, "missing-id")).rejects.toThrow("not found"); + }); +}); diff --git a/tests/unit/services/document-service.test.ts b/tests/unit/services/document-service.test.ts new file mode 100644 index 0000000..3ac591f --- /dev/null +++ b/tests/unit/services/document-service.test.ts @@ -0,0 +1,168 @@ +// tests/unit/services/document-service.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { + createDocument, + deleteDocument, + getDocument, + listDocuments, + listDocumentsBySlugIds, + updateDocument, +} from "../../../src/services/document-service.js"; + +function mockGqlClient(response: Record<string, unknown>) { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("getDocument", () => { + it("returns document by ID", async () => { + const client = mockGqlClient({ document: { id: "doc-1", title: "Test" } }); + const result = await getDocument(client, "doc-1"); + expect(result.id).toBe("doc-1"); + }); + + it("throws when not found", async () => { + const client = mockGqlClient({ document: null }); + await expect(getDocument(client, "missing")).rejects.toThrow("not found"); + }); +}); + +describe("createDocument", () => { + it("returns created document", async () => { + const client = mockGqlClient({ + documentCreate: { + success: true, + document: { id: "new-doc", title: "New" }, + }, + }); + const result = await createDocument(client, { title: "New" }); + expect(result.id).toBe("new-doc"); + }); + + it("throws when creation fails", async () => { + const client = mockGqlClient({ + documentCreate: { success: false }, + }); + await expect(createDocument(client, { title: "New" })).rejects.toThrow( + "Failed to create document", + ); + }); +}); + +describe("updateDocument", () => { + it("returns updated document", async () => { + const client = mockGqlClient({ + documentUpdate: { + success: true, + document: { id: "doc-1", title: "Updated" }, + }, + }); + const result = await updateDocument(client, "doc-1", { title: "Updated" }); + expect(result.title).toBe("Updated"); + }); + + it("throws when update fails", async () => { + const client = mockGqlClient({ + documentUpdate: { success: false }, + }); + await expect( + updateDocument(client, "doc-1", { title: "Updated" }), + ).rejects.toThrow("Failed to update document"); + }); +}); + +describe("listDocuments", () => { + it("returns documents list", async () => { + const client = mockGqlClient({ + documents: { + nodes: [{ id: "1" }, { id: "2" }], + pageInfo: { hasNextPage: false, endCursor: "cursor2" }, + }, + }); + const result = await listDocuments(client); + expect(result.nodes).toHaveLength(2); + expect(result.pageInfo).toEqual({ + hasNextPage: false, + endCursor: "cursor2", + }); + }); + + it("returns empty result when no documents", async () => { + const client = mockGqlClient({ + documents: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + const result = await listDocuments(client); + expect(result.nodes).toEqual([]); + expect(result.pageInfo).toEqual({ hasNextPage: false, endCursor: null }); + }); + + it("passes after cursor to GraphQL request", async () => { + const client = mockGqlClient({ + documents: { + nodes: [{ id: "3" }], + pageInfo: { hasNextPage: false, endCursor: "cursor3" }, + }, + }); + await listDocuments(client, { limit: 10, after: "cursor2" }); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 10, + after: "cursor2", + filter: undefined, + }); + }); + + it("returns pageInfo with hasNextPage true", async () => { + const client = mockGqlClient({ + documents: { + nodes: [{ id: "1" }], + pageInfo: { hasNextPage: true, endCursor: "nextCursor" }, + }, + }); + const result = await listDocuments(client, { limit: 1 }); + expect(result.pageInfo).toEqual({ + hasNextPage: true, + endCursor: "nextCursor", + }); + }); +}); + +describe("listDocumentsBySlugIds", () => { + it("returns empty array for empty input", async () => { + const client = mockGqlClient({}); + const result = await listDocumentsBySlugIds(client, []); + expect(result).toEqual([]); + }); + + it("returns documents matching slugIds", async () => { + const client = mockGqlClient({ + documents: { + nodes: [ + { id: "1", slugId: "abc" }, + { id: "2", slugId: "def" }, + ], + }, + }); + const result = await listDocumentsBySlugIds(client, ["abc", "def"]); + expect(result).toHaveLength(2); + }); +}); + +describe("deleteDocument", () => { + it("returns true on success", async () => { + const client = mockGqlClient({ documentDelete: { success: true } }); + const result = await deleteDocument(client, "doc-1"); + expect(result).toBe(true); + }); + + it("throws when delete fails", async () => { + const client = mockGqlClient({ documentDelete: { success: false } }); + await expect(deleteDocument(client, "doc-1")).rejects.toThrow( + "Failed to delete document", + ); + }); +}); diff --git a/tests/unit/services/file-service.test.ts b/tests/unit/services/file-service.test.ts new file mode 100644 index 0000000..6aecf44 --- /dev/null +++ b/tests/unit/services/file-service.test.ts @@ -0,0 +1,222 @@ +// tests/unit/services/file-service.test.ts +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { FileService } from "../../../src/services/file-service.js"; + +// Mock node:fs/promises +vi.mock("node:fs/promises", () => ({ + access: vi.fn(), + mkdir: vi.fn(), + readFile: vi.fn(), + stat: vi.fn(), + writeFile: vi.fn(), +})); + +// Mock embed-parser +vi.mock("../../../src/common/embed-parser.js", () => ({ + isLinearUploadUrl: vi.fn(), + extractFilenameFromUrl: vi.fn(), +})); + +import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises"; +import { + extractFilenameFromUrl, + isLinearUploadUrl, +} from "../../../src/common/embed-parser.js"; + +const mockFetch = vi.fn(); +vi.stubGlobal("fetch", mockFetch); + +const TEST_TOKEN = "lin_api_test_token"; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("downloadFile", () => { + it("rejects non-linear URLs", async () => { + vi.mocked(isLinearUploadUrl).mockReturnValue(false); + + const service = new FileService(TEST_TOKEN); + const result = await service.downloadFile("https://example.com/file.png"); + + expect(result).toEqual({ + success: false, + error: "URL must be from uploads.linear.app domain", + }); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("downloads file successfully", async () => { + vi.mocked(isLinearUploadUrl).mockReturnValue(true); + vi.mocked(extractFilenameFromUrl).mockReturnValue("image.png"); + vi.mocked(access).mockRejectedValue(new Error("ENOENT")); // file doesn't exist + vi.mocked(mkdir).mockResolvedValue(undefined); + vi.mocked(writeFile).mockResolvedValue(undefined); + + const fileContent = new ArrayBuffer(8); + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + arrayBuffer: () => Promise.resolve(fileContent), + }); + + const service = new FileService(TEST_TOKEN); + const result = await service.downloadFile( + "https://uploads.linear.app/org/file.png", + ); + + expect(result).toEqual({ + success: true, + filePath: "image.png", + }); + expect(mockFetch).toHaveBeenCalledWith( + "https://uploads.linear.app/org/file.png", + { + method: "GET", + headers: { Authorization: `Bearer ${TEST_TOKEN}` }, + }, + ); + expect(writeFile).toHaveBeenCalled(); + }); + + it("rejects when file already exists", async () => { + vi.mocked(isLinearUploadUrl).mockReturnValue(true); + vi.mocked(extractFilenameFromUrl).mockReturnValue("image.png"); + vi.mocked(access).mockResolvedValue(undefined); // file exists + + const service = new FileService(TEST_TOKEN); + const result = await service.downloadFile( + "https://uploads.linear.app/org/file.png", + ); + + expect(result).toEqual({ + success: false, + error: "File already exists: image.png. Use --overwrite to replace.", + }); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("handles HTTP error", async () => { + vi.mocked(isLinearUploadUrl).mockReturnValue(true); + vi.mocked(extractFilenameFromUrl).mockReturnValue("image.png"); + vi.mocked(access).mockRejectedValue(new Error("ENOENT")); + + mockFetch.mockResolvedValue({ + ok: false, + status: 403, + statusText: "Forbidden", + }); + + const service = new FileService(TEST_TOKEN); + const result = await service.downloadFile( + "https://uploads.linear.app/org/file.png", + ); + + expect(result).toEqual({ + success: false, + error: "HTTP 403: Forbidden", + statusCode: 403, + }); + }); +}); + +describe("uploadFile", () => { + it("returns error when file not found", async () => { + vi.mocked(access).mockRejectedValue(new Error("ENOENT")); + + const service = new FileService(TEST_TOKEN); + const result = await service.uploadFile("/path/to/missing.png"); + + expect(result).toEqual({ + success: false, + error: "File not found: /path/to/missing.png", + }); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("returns error when file too large", async () => { + vi.mocked(access).mockResolvedValue(undefined); + vi.mocked(stat).mockResolvedValue({ + size: 25 * 1024 * 1024, // 25MB + } as Awaited<ReturnType<typeof stat>>); + + const service = new FileService(TEST_TOKEN); + const result = await service.uploadFile("/path/to/large.png"); + + expect(result.success).toBe(false); + expect(result.error).toMatch(/File too large/); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("uploads file successfully", async () => { + vi.mocked(access).mockResolvedValue(undefined); + vi.mocked(stat).mockResolvedValue({ + size: 1024, + } as Awaited<ReturnType<typeof stat>>); + vi.mocked(readFile).mockResolvedValue(Buffer.from("file-content")); + + // First fetch: GraphQL fileUpload mutation + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + data: { + fileUpload: { + success: true, + uploadFile: { + uploadUrl: "https://storage.example.com/upload", + assetUrl: "https://uploads.linear.app/org/asset.png", + headers: [{ key: "x-amz-header", value: "some-value" }], + }, + }, + }, + }), + }); + + // Second fetch: PUT to pre-signed URL + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + }); + + const service = new FileService(TEST_TOKEN); + const result = await service.uploadFile("/path/to/image.png"); + + expect(result).toEqual({ + success: true, + assetUrl: "https://uploads.linear.app/org/asset.png", + filename: "image.png", + }); + expect(mockFetch).toHaveBeenCalledTimes(2); + + // Verify GraphQL call + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + "https://api.linear.app/graphql", + expect.objectContaining({ + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: TEST_TOKEN, + }, + }), + ); + + // Verify PUT call + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + "https://storage.example.com/upload", + expect.objectContaining({ + method: "PUT", + headers: { + "Content-Type": "image/png", + "x-amz-header": "some-value", + }, + }), + ); + }); +}); diff --git a/tests/unit/services/issue-relation-service.test.ts b/tests/unit/services/issue-relation-service.test.ts new file mode 100644 index 0000000..a3121a0 --- /dev/null +++ b/tests/unit/services/issue-relation-service.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { IssueRelationType } from "../../../src/gql/graphql.js"; +import { + createIssueRelation, + deleteIssueRelation, + findIssueRelation, +} from "../../../src/services/issue-relation-service.js"; + +function mockGqlClient(response: Record<string, unknown>): GraphQLClient { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("createIssueRelation", () => { + it("creates a relation and returns it", async () => { + const relation = { + id: "rel-1", + type: IssueRelationType.Blocks, + relatedIssue: { id: "issue-2", identifier: "ENG-2" }, + }; + const client = mockGqlClient({ + issueRelationCreate: { success: true, issueRelation: relation }, + }); + + const result = await createIssueRelation(client, { + issueId: "issue-1", + relatedIssueId: "issue-2", + type: IssueRelationType.Blocks, + }); + + expect(result).toEqual(relation); + expect(client.request).toHaveBeenCalledOnce(); + }); + + it("throws when creation fails", async () => { + const client = mockGqlClient({ + issueRelationCreate: { success: false, issueRelation: null }, + }); + + await expect( + createIssueRelation(client, { + issueId: "issue-1", + relatedIssueId: "issue-2", + type: IssueRelationType.Blocks, + }), + ).rejects.toThrow("Failed to create issue relation"); + }); +}); + +describe("findIssueRelation", () => { + it("finds relation in forward relations", async () => { + const client = mockGqlClient({ + issue: { + relations: { + nodes: [ + { + id: "rel-1", + type: IssueRelationType.Blocks, + relatedIssue: { id: "target-id", identifier: "ENG-2" }, + }, + ], + }, + inverseRelations: { nodes: [] }, + }, + }); + + const result = await findIssueRelation(client, "source-id", "target-id"); + expect(result).toBe("rel-1"); + }); + + it("finds relation in inverse relations", async () => { + const client = mockGqlClient({ + issue: { + relations: { nodes: [] }, + inverseRelations: { + nodes: [ + { + id: "rel-2", + type: IssueRelationType.Blocks, + issue: { id: "target-id", identifier: "ENG-1" }, + }, + ], + }, + }, + }); + + const result = await findIssueRelation(client, "source-id", "target-id"); + expect(result).toBe("rel-2"); + }); + + it("throws when issue is not found", async () => { + const client = mockGqlClient({ issue: null }); + + await expect( + findIssueRelation(client, "non-existent-id", "target-id"), + ).rejects.toThrow("not found"); + }); + + it("throws when no relation found", async () => { + const client = mockGqlClient({ + issue: { + relations: { nodes: [] }, + inverseRelations: { nodes: [] }, + }, + }); + + await expect( + findIssueRelation(client, "source-id", "target-id"), + ).rejects.toThrow("not found"); + }); +}); + +describe("deleteIssueRelation", () => { + it("deletes a relation by ID", async () => { + const client = mockGqlClient({ + issueRelationDelete: { success: true, entityId: "rel-1" }, + }); + + await deleteIssueRelation(client, "rel-1"); + expect(client.request).toHaveBeenCalledOnce(); + }); + + it("throws when deletion fails", async () => { + const client = mockGqlClient({ + issueRelationDelete: { success: false }, + }); + + await expect(deleteIssueRelation(client, "rel-1")).rejects.toThrow( + "Failed to delete issue relation", + ); + }); +}); diff --git a/tests/unit/services/issue-service.test.ts b/tests/unit/services/issue-service.test.ts new file mode 100644 index 0000000..b5724f0 --- /dev/null +++ b/tests/unit/services/issue-service.test.ts @@ -0,0 +1,159 @@ +// tests/unit/services/issue-service.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { + getIssue, + getIssueByIdentifier, + listIssues, + searchIssues, +} from "../../../src/services/issue-service.js"; + +function mockGqlClient(response: Record<string, unknown>) { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("listIssues", () => { + it("returns issues from query", async () => { + const client = mockGqlClient({ + issues: { + nodes: [{ id: "1", title: "Test" }], + pageInfo: { hasNextPage: false, endCursor: "cursor1" }, + }, + }); + const result = await listIssues(client, { limit: 10 }); + expect(result.nodes).toHaveLength(1); + expect(result.nodes[0].id).toBe("1"); + expect(result.pageInfo).toEqual({ + hasNextPage: false, + endCursor: "cursor1", + }); + }); + + it("returns empty result when no issues", async () => { + const client = mockGqlClient({ + issues: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + const result = await listIssues(client); + expect(result.nodes).toEqual([]); + expect(result.pageInfo).toEqual({ hasNextPage: false, endCursor: null }); + }); + + it("uses default limit of 25 when no options provided", async () => { + const client = mockGqlClient({ + issues: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listIssues(client); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 25, + after: undefined, + orderBy: "updatedAt", + }); + }); + + it("passes after cursor to GraphQL request", async () => { + const client = mockGqlClient({ + issues: { + nodes: [{ id: "2", title: "Next" }], + pageInfo: { hasNextPage: false, endCursor: "cursor2" }, + }, + }); + await listIssues(client, { limit: 5, after: "cursor1" }); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 5, + after: "cursor1", + orderBy: "updatedAt", + }); + }); + + it("returns pageInfo with hasNextPage true", async () => { + const client = mockGqlClient({ + issues: { + nodes: [{ id: "1", title: "Test" }], + pageInfo: { hasNextPage: true, endCursor: "nextCursor" }, + }, + }); + const result = await listIssues(client, { limit: 1 }); + expect(result.pageInfo).toEqual({ + hasNextPage: true, + endCursor: "nextCursor", + }); + }); +}); + +describe("getIssue", () => { + it("returns issue by UUID", async () => { + const client = mockGqlClient({ + issue: { id: "550e8400-e29b-41d4-a716-446655440000", title: "Found" }, + }); + const result = await getIssue( + client, + "550e8400-e29b-41d4-a716-446655440000", + ); + expect(result.id).toBe("550e8400-e29b-41d4-a716-446655440000"); + }); + + it("throws when issue not found by UUID", async () => { + const client = mockGqlClient({ issue: null }); + await expect( + getIssue(client, "550e8400-e29b-41d4-a716-446655440000"), + ).rejects.toThrow("not found"); + }); +}); + +describe("getIssueByIdentifier", () => { + it("returns issue by team key and number", async () => { + const client = mockGqlClient({ + issues: { nodes: [{ id: "issue-1", title: "Found" }] }, + }); + const result = await getIssueByIdentifier(client, "ENG", 42); + expect(result.id).toBe("issue-1"); + }); + + it("throws when issue not found by identifier", async () => { + const client = mockGqlClient({ issues: { nodes: [] } }); + await expect(getIssueByIdentifier(client, "ENG", 999)).rejects.toThrow( + "not found", + ); + }); +}); + +describe("searchIssues", () => { + it("returns search results", async () => { + const client = mockGqlClient({ + searchIssues: { + nodes: [{ id: "1", title: "Match" }], + pageInfo: { hasNextPage: false, endCursor: "cursor1" }, + }, + }); + const result = await searchIssues(client, "test", { limit: 10 }); + expect(result.nodes).toHaveLength(1); + expect(result.nodes[0].id).toBe("1"); + expect(result.pageInfo).toEqual({ + hasNextPage: false, + endCursor: "cursor1", + }); + }); + + it("passes after cursor to GraphQL request", async () => { + const client = mockGqlClient({ + searchIssues: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await searchIssues(client, "query", { limit: 5, after: "prevCursor" }); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + term: "query", + first: 5, + after: "prevCursor", + }); + }); +}); diff --git a/tests/unit/services/label-service.test.ts b/tests/unit/services/label-service.test.ts new file mode 100644 index 0000000..152b7d9 --- /dev/null +++ b/tests/unit/services/label-service.test.ts @@ -0,0 +1,99 @@ +// tests/unit/services/label-service.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { listLabels } from "../../../src/services/label-service.js"; + +function mockGqlClient(response: Record<string, unknown>): GraphQLClient { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("listLabels", () => { + it("returns labels", async () => { + const client = mockGqlClient({ + issueLabels: { + nodes: [ + { id: "lbl-1", name: "Bug", color: "#ff0000", description: "A bug" }, + ], + pageInfo: { hasNextPage: false, endCursor: "c1" }, + }, + }); + const result = await listLabels(client); + expect(result.nodes).toHaveLength(1); + expect(result.nodes[0].id).toBe("lbl-1"); + expect(result.nodes[0].name).toBe("Bug"); + expect(result.nodes[0].color).toBe("#ff0000"); + expect(result.pageInfo).toEqual({ hasNextPage: false, endCursor: "c1" }); + }); + + it("returns empty result", async () => { + const client = mockGqlClient({ + issueLabels: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + const result = await listLabels(client); + expect(result.nodes).toEqual([]); + expect(result.pageInfo.hasNextPage).toBe(false); + }); + + it("passes after cursor", async () => { + const client = mockGqlClient({ + issueLabels: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listLabels(client, undefined, { after: "cur1" }); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: "cur1", + filter: undefined, + }); + }); + + it("uses default limit of 50", async () => { + const client = mockGqlClient({ + issueLabels: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listLabels(client); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: undefined, + filter: undefined, + }); + }); + + it("filters by team when teamId provided", async () => { + const client = mockGqlClient({ + issueLabels: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listLabels(client, "team-1"); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: undefined, + filter: { team: { id: { eq: "team-1" } } }, + }); + }); + + it("converts null description to undefined", async () => { + const client = mockGqlClient({ + issueLabels: { + nodes: [ + { id: "lbl-2", name: "Feature", color: "#00ff00", description: null }, + ], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + const result = await listLabels(client); + expect(result.nodes[0].description).toBeUndefined(); + }); +}); diff --git a/tests/unit/services/milestone-service.test.ts b/tests/unit/services/milestone-service.test.ts new file mode 100644 index 0000000..aa04be9 --- /dev/null +++ b/tests/unit/services/milestone-service.test.ts @@ -0,0 +1,180 @@ +// tests/unit/services/milestone-service.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { + createMilestone, + getMilestone, + listMilestones, + updateMilestone, +} from "../../../src/services/milestone-service.js"; + +function mockGqlClient(response: Record<string, unknown>): GraphQLClient { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("listMilestones", () => { + it("returns milestones", async () => { + const client = mockGqlClient({ + project: { + projectMilestones: { + nodes: [ + { + id: "ms-1", + name: "v1.0", + description: "First release", + targetDate: "2025-06-01", + sortOrder: 0, + }, + ], + pageInfo: { hasNextPage: false, endCursor: "c1" }, + }, + }, + }); + const result = await listMilestones(client, "proj-1"); + expect(result.nodes).toHaveLength(1); + expect(result.nodes[0]).toEqual({ + id: "ms-1", + name: "v1.0", + description: "First release", + targetDate: "2025-06-01", + sortOrder: 0, + }); + expect(result.pageInfo).toEqual({ hasNextPage: false, endCursor: "c1" }); + }); + + it("returns empty when project is null", async () => { + const client = mockGqlClient({ project: null }); + const result = await listMilestones(client, "missing-proj"); + expect(result.nodes).toEqual([]); + expect(result.pageInfo).toEqual({ hasNextPage: false, endCursor: null }); + }); + + it("passes after cursor", async () => { + const client = mockGqlClient({ + project: { + projectMilestones: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }, + }); + await listMilestones(client, "proj-1", { after: "cur1" }); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + projectId: "proj-1", + first: 50, + after: "cur1", + }); + }); + + it("uses default limit of 50", async () => { + const client = mockGqlClient({ + project: { + projectMilestones: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }, + }); + await listMilestones(client, "proj-1"); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + projectId: "proj-1", + first: 50, + after: undefined, + }); + }); +}); + +describe("getMilestone", () => { + it("returns milestone detail", async () => { + const client = mockGqlClient({ + projectMilestone: { + id: "ms-1", + name: "v1.0", + description: "First release", + targetDate: "2025-06-01", + sortOrder: 0, + project: { id: "proj-1", name: "Project Alpha" }, + issues: { nodes: [] }, + }, + }); + const result = await getMilestone(client, "ms-1"); + expect(result.id).toBe("ms-1"); + expect(result.name).toBe("v1.0"); + }); + + it("throws when not found", async () => { + const client = mockGqlClient({ projectMilestone: null }); + await expect(getMilestone(client, "missing-id")).rejects.toThrow( + "not found", + ); + }); +}); + +describe("createMilestone", () => { + it("creates milestone", async () => { + const client = mockGqlClient({ + projectMilestoneCreate: { + success: true, + projectMilestone: { + id: "ms-new", + name: "v2.0", + description: "Second release", + targetDate: "2025-12-01", + sortOrder: 1, + }, + }, + }); + const result = await createMilestone(client, { + projectId: "proj-1", + name: "v2.0", + }); + expect(result.id).toBe("ms-new"); + expect(result.name).toBe("v2.0"); + }); + + it("throws on failure", async () => { + const client = mockGqlClient({ + projectMilestoneCreate: { + success: false, + projectMilestone: null, + }, + }); + await expect( + createMilestone(client, { projectId: "proj-1", name: "Bad" }), + ).rejects.toThrow("Failed to create milestone"); + }); +}); + +describe("updateMilestone", () => { + it("updates milestone", async () => { + const client = mockGqlClient({ + projectMilestoneUpdate: { + success: true, + projectMilestone: { + id: "ms-1", + name: "v1.1", + description: "Updated release", + targetDate: "2025-07-01", + sortOrder: 0, + }, + }, + }); + const result = await updateMilestone(client, "ms-1", { name: "v1.1" }); + expect(result.id).toBe("ms-1"); + expect(result.name).toBe("v1.1"); + }); + + it("throws on failure", async () => { + const client = mockGqlClient({ + projectMilestoneUpdate: { + success: false, + projectMilestone: null, + }, + }); + await expect( + updateMilestone(client, "ms-1", { name: "Bad" }), + ).rejects.toThrow("Failed to update milestone"); + }); +}); diff --git a/tests/unit/services/project-service.test.ts b/tests/unit/services/project-service.test.ts new file mode 100644 index 0000000..c13f4a8 --- /dev/null +++ b/tests/unit/services/project-service.test.ts @@ -0,0 +1,97 @@ +// tests/unit/services/project-service.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { listProjects } from "../../../src/services/project-service.js"; + +function mockGqlClient(response: Record<string, unknown>): GraphQLClient { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("listProjects", () => { + it("returns projects", async () => { + const client = mockGqlClient({ + projects: { + nodes: [ + { + id: "proj-1", + name: "Project Alpha", + description: "A test project", + state: "started", + targetDate: "2025-12-31", + slugId: "alpha", + }, + ], + pageInfo: { hasNextPage: false, endCursor: "c1" }, + }, + }); + const result = await listProjects(client); + expect(result.nodes).toHaveLength(1); + expect(result.nodes[0].id).toBe("proj-1"); + expect(result.nodes[0].name).toBe("Project Alpha"); + expect(result.nodes[0].state).toBe("started"); + expect(result.nodes[0].slugId).toBe("alpha"); + expect(result.pageInfo).toEqual({ hasNextPage: false, endCursor: "c1" }); + }); + + it("returns empty result", async () => { + const client = mockGqlClient({ + projects: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + const result = await listProjects(client); + expect(result.nodes).toEqual([]); + expect(result.pageInfo.hasNextPage).toBe(false); + }); + + it("passes after cursor", async () => { + const client = mockGqlClient({ + projects: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listProjects(client, { after: "cur1" }); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: "cur1", + }); + }); + + it("uses default limit of 50", async () => { + const client = mockGqlClient({ + projects: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listProjects(client); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: undefined, + }); + }); + + it("converts null targetDate to undefined", async () => { + const client = mockGqlClient({ + projects: { + nodes: [ + { + id: "proj-2", + name: "No Date", + description: "", + state: "planned", + targetDate: null, + slugId: "no-date", + }, + ], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + const result = await listProjects(client); + expect(result.nodes[0].targetDate).toBeUndefined(); + }); +}); diff --git a/tests/unit/services/team-service.test.ts b/tests/unit/services/team-service.test.ts new file mode 100644 index 0000000..0093f45 --- /dev/null +++ b/tests/unit/services/team-service.test.ts @@ -0,0 +1,67 @@ +// tests/unit/services/team-service.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { listTeams } from "../../../src/services/team-service.js"; + +function mockGqlClient(response: Record<string, unknown>): GraphQLClient { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("listTeams", () => { + it("returns teams", async () => { + const client = mockGqlClient({ + teams: { + nodes: [{ id: "team-1", key: "ENG", name: "Engineering" }], + pageInfo: { hasNextPage: false, endCursor: "c1" }, + }, + }); + const result = await listTeams(client); + expect(result.nodes).toHaveLength(1); + expect(result.nodes[0].id).toBe("team-1"); + expect(result.nodes[0].key).toBe("ENG"); + expect(result.nodes[0].name).toBe("Engineering"); + expect(result.pageInfo).toEqual({ hasNextPage: false, endCursor: "c1" }); + }); + + it("returns empty result", async () => { + const client = mockGqlClient({ + teams: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + const result = await listTeams(client); + expect(result.nodes).toEqual([]); + expect(result.pageInfo.hasNextPage).toBe(false); + }); + + it("passes after cursor", async () => { + const client = mockGqlClient({ + teams: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listTeams(client, { after: "cur1" }); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: "cur1", + }); + }); + + it("uses default limit of 50", async () => { + const client = mockGqlClient({ + teams: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listTeams(client); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: undefined, + }); + }); +}); diff --git a/tests/unit/services/user-service.test.ts b/tests/unit/services/user-service.test.ts new file mode 100644 index 0000000..0063b18 --- /dev/null +++ b/tests/unit/services/user-service.test.ts @@ -0,0 +1,84 @@ +// tests/unit/services/user-service.test.ts +import { describe, expect, it, vi } from "vitest"; +import type { GraphQLClient } from "../../../src/client/graphql-client.js"; +import { listUsers } from "../../../src/services/user-service.js"; + +function mockGqlClient(response: Record<string, unknown>): GraphQLClient { + return { + request: vi.fn().mockResolvedValue(response), + } as unknown as GraphQLClient; +} + +describe("listUsers", () => { + it("returns users sorted by name", async () => { + const client = mockGqlClient({ + users: { + nodes: [ + { id: "u-2", name: "Zoe", email: "zoe@test.com", active: true }, + { id: "u-1", name: "Alice", email: "alice@test.com", active: true }, + ], + pageInfo: { hasNextPage: false, endCursor: "c1" }, + }, + }); + const result = await listUsers(client); + expect(result.nodes[0].name).toBe("Alice"); + expect(result.nodes[1].name).toBe("Zoe"); + }); + + it("returns empty result", async () => { + const client = mockGqlClient({ + users: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + const result = await listUsers(client); + expect(result.nodes).toEqual([]); + expect(result.pageInfo.hasNextPage).toBe(false); + }); + + it("passes after cursor", async () => { + const client = mockGqlClient({ + users: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listUsers(client, false, { after: "cur1" }); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: "cur1", + filter: undefined, + }); + }); + + it("uses default limit of 50", async () => { + const client = mockGqlClient({ + users: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listUsers(client); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: undefined, + filter: undefined, + }); + }); + + it("filters active users when activeOnly is true", async () => { + const client = mockGqlClient({ + users: { + nodes: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }); + await listUsers(client, true); + expect(client.request).toHaveBeenCalledWith(expect.anything(), { + first: 50, + after: undefined, + filter: { active: { eq: true } }, + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index ae129eb..70befb5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,10 +6,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, - "lib": [ - "ES2022", - "DOM" - ], + "lib": ["ES2022", "DOM"], "module": "ESNext", "moduleResolution": "Node", "noEmitOnError": true, @@ -22,13 +19,9 @@ "sourceMap": false, "strict": true, "target": "ES2022", - "types": [ - "node" - ] + "types": ["node"] }, - "include": [ - "src/**/*" - ], + "include": ["src/**/*"], "exclude": [ "node_modules", "dist", diff --git a/vitest.config.ts b/vitest.config.ts index c323265..092a853 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ test: { globals: true, environment: "node", - include: ["tests/**/*.test.ts"], + include: ["tests/unit/**/*.test.ts"], coverage: { provider: "v8", reporter: ["text", "json", "html"],