@elnora-ai/linear 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +19 -0
  3. package/CHANGELOG.md +29 -0
  4. package/LICENSE +201 -0
  5. package/README.md +61 -0
  6. package/agents/linear-issue-creator.md +45 -0
  7. package/agents/linear-issue-reviewer.md +44 -0
  8. package/agents/linear-issue-updater.md +45 -0
  9. package/agents/linear-url-to-issues.md +51 -0
  10. package/commands/linear-bulk.md +42 -0
  11. package/commands/linear-cleanup.md +45 -0
  12. package/commands/linear-curator-run.md +39 -0
  13. package/commands/linear-my-issues.md +25 -0
  14. package/commands/linear-search.md +32 -0
  15. package/commands/linear-sync.md +54 -0
  16. package/dist/cli.d.ts +3 -0
  17. package/dist/cli.d.ts.map +1 -0
  18. package/dist/cli.js +126 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/client/auth.d.ts +21 -0
  21. package/dist/client/auth.d.ts.map +1 -0
  22. package/dist/client/auth.js +74 -0
  23. package/dist/client/auth.js.map +1 -0
  24. package/dist/client/index.d.ts +3 -0
  25. package/dist/client/index.d.ts.map +1 -0
  26. package/dist/client/index.js +3 -0
  27. package/dist/client/index.js.map +1 -0
  28. package/dist/client/linear-client.d.ts +5 -0
  29. package/dist/client/linear-client.d.ts.map +1 -0
  30. package/dist/client/linear-client.js +18 -0
  31. package/dist/client/linear-client.js.map +1 -0
  32. package/dist/commands/bulk.d.ts +39 -0
  33. package/dist/commands/bulk.d.ts.map +1 -0
  34. package/dist/commands/bulk.js +103 -0
  35. package/dist/commands/bulk.js.map +1 -0
  36. package/dist/commands/cleanup.d.ts +39 -0
  37. package/dist/commands/cleanup.d.ts.map +1 -0
  38. package/dist/commands/cleanup.js +100 -0
  39. package/dist/commands/cleanup.js.map +1 -0
  40. package/dist/commands/curator.d.ts +23 -0
  41. package/dist/commands/curator.d.ts.map +1 -0
  42. package/dist/commands/curator.js +66 -0
  43. package/dist/commands/curator.js.map +1 -0
  44. package/dist/commands/index.d.ts +7 -0
  45. package/dist/commands/index.d.ts.map +1 -0
  46. package/dist/commands/index.js +7 -0
  47. package/dist/commands/index.js.map +1 -0
  48. package/dist/commands/my-issues.d.ts +7 -0
  49. package/dist/commands/my-issues.d.ts.map +1 -0
  50. package/dist/commands/my-issues.js +24 -0
  51. package/dist/commands/my-issues.js.map +1 -0
  52. package/dist/commands/search.d.ts +20 -0
  53. package/dist/commands/search.d.ts.map +1 -0
  54. package/dist/commands/search.js +54 -0
  55. package/dist/commands/search.js.map +1 -0
  56. package/dist/commands/sync.d.ts +87 -0
  57. package/dist/commands/sync.d.ts.map +1 -0
  58. package/dist/commands/sync.js +229 -0
  59. package/dist/commands/sync.js.map +1 -0
  60. package/dist/config/index.d.ts +3 -0
  61. package/dist/config/index.d.ts.map +1 -0
  62. package/dist/config/index.js +4 -0
  63. package/dist/config/index.js.map +1 -0
  64. package/dist/config/loader.d.ts +12 -0
  65. package/dist/config/loader.d.ts.map +1 -0
  66. package/dist/config/loader.js +140 -0
  67. package/dist/config/loader.js.map +1 -0
  68. package/dist/config/types.d.ts +133 -0
  69. package/dist/config/types.d.ts.map +1 -0
  70. package/dist/config/types.js +13 -0
  71. package/dist/config/types.js.map +1 -0
  72. package/dist/output/formatter.d.ts +16 -0
  73. package/dist/output/formatter.d.ts.map +1 -0
  74. package/dist/output/formatter.js +34 -0
  75. package/dist/output/formatter.js.map +1 -0
  76. package/dist/output/index.d.ts +2 -0
  77. package/dist/output/index.d.ts.map +1 -0
  78. package/dist/output/index.js +2 -0
  79. package/dist/output/index.js.map +1 -0
  80. package/dist/signals/external-command.d.ts +30 -0
  81. package/dist/signals/external-command.d.ts.map +1 -0
  82. package/dist/signals/external-command.js +96 -0
  83. package/dist/signals/external-command.js.map +1 -0
  84. package/dist/signals/index.d.ts +4 -0
  85. package/dist/signals/index.d.ts.map +1 -0
  86. package/dist/signals/index.js +4 -0
  87. package/dist/signals/index.js.map +1 -0
  88. package/dist/signals/registry.d.ts +5 -0
  89. package/dist/signals/registry.d.ts.map +1 -0
  90. package/dist/signals/registry.js +29 -0
  91. package/dist/signals/registry.js.map +1 -0
  92. package/dist/signals/types.d.ts +25 -0
  93. package/dist/signals/types.d.ts.map +1 -0
  94. package/dist/signals/types.js +9 -0
  95. package/dist/signals/types.js.map +1 -0
  96. package/package.json +76 -0
  97. package/references/projects.example.json +26 -0
  98. package/references/projects.placeholder.json +6 -0
  99. package/references/repos.example.json +21 -0
  100. package/references/repos.placeholder.json +6 -0
  101. package/references/signal-sources.example.json +42 -0
  102. package/references/signal-sources.placeholder.json +6 -0
  103. package/references/slack.example.json +10 -0
  104. package/references/slack.placeholder.json +8 -0
  105. package/references/teams.example.json +21 -0
  106. package/references/teams.placeholder.json +6 -0
  107. package/references/users.example.json +18 -0
  108. package/references/users.placeholder.json +6 -0
  109. package/references/workflows.example.json +44 -0
  110. package/references/workflows.placeholder.json +7 -0
  111. package/schemas/projects.json +47 -0
  112. package/schemas/repos.json +41 -0
  113. package/schemas/signal-sources.json +90 -0
  114. package/schemas/slack.json +45 -0
  115. package/schemas/teams.json +43 -0
  116. package/schemas/users.json +41 -0
  117. package/schemas/workflows.json +61 -0
  118. package/skills/linear-workspace/SKILL.md +52 -0
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: linear-my-issues
3
+ description: List Linear issues assigned to you, grouped by state
4
+ allowed-tools: Bash, Read
5
+ ---
6
+
7
+ # My Linear Issues
8
+
9
+ List every Linear issue currently assigned to the viewer.
10
+
11
+ ## Run
12
+
13
+ ```bash
14
+ elnora-linear my-issues --limit 50 --output json
15
+ ```
16
+
17
+ ## Present
18
+
19
+ Group by state in this order: `In Progress`, `Todo`, `Backlog`, `Done` (last 7 days only), `Canceled` (hide unless asked). Within each state, sort by `updatedAt` descending. Show identifier, title, team, project.
20
+
21
+ If the list is empty, say so cleanly: "Nothing assigned." Don't fabricate.
22
+
23
+ ## Don't
24
+
25
+ - Don't include closed/cancelled issues older than 7 days — the user wants their active load
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: linear-search
3
+ description: Search Linear issues by keyword, team, assignee, or state
4
+ argument-hint: "<query>"
5
+ allowed-tools: Bash, Read
6
+ ---
7
+
8
+ # Linear Search
9
+
10
+ Search Linear issues for: **{{query}}**
11
+
12
+ ## Run
13
+
14
+ ```bash
15
+ elnora-linear search --query "{{query}}" --limit 25 --output json
16
+ ```
17
+
18
+ For more focused queries, add flags:
19
+ - `--team ENG` — restrict to a team
20
+ - `--assignee me` — only your issues (or `--assignee "Alice Smith"`)
21
+ - `--state "In Progress"` — by workflow state
22
+ - `--limit <n>` — default 25
23
+ - `--output text` — for human-readable; default here is JSON for parsing
24
+
25
+ ## Present
26
+
27
+ Render the JSON as a table: identifier, state, assignee, title. Highlight any issue whose state matches what the user is looking for.
28
+
29
+ ## Don't
30
+
31
+ - Don't paginate; the `--limit` flag controls volume
32
+ - Don't apply mutations — that's `/linear-bulk`, `/linear-cleanup`, or the `linear-issue-updater` agent
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: linear-sync
3
+ description: Populate or refresh the Linear reference files (teams, projects, users, workflows)
4
+ allowed-tools: Bash, Read
5
+ ---
6
+
7
+ # Linear Sync
8
+
9
+ Refresh the reference files the rest of `elnora-linear` reads — teams, projects, users, workflow states. Run this on first install and any time your Linear workspace changes (new team, renamed project, etc.).
10
+
11
+ ## Run
12
+
13
+ The fast path: refresh everything auto-discoverable from the Linear API.
14
+
15
+ ```bash
16
+ elnora-linear sync all
17
+ ```
18
+
19
+ Pick one target:
20
+
21
+ ```bash
22
+ elnora-linear sync teams
23
+ elnora-linear sync projects
24
+ elnora-linear sync users
25
+ elnora-linear sync workflows
26
+ ```
27
+
28
+ Validate references:
29
+
30
+ ```bash
31
+ elnora-linear sync verify
32
+ ```
33
+
34
+ Reports each reference as `user-file` (populated) or `placeholder` (default, empty).
35
+
36
+ Import an existing config bundle:
37
+
38
+ ```bash
39
+ elnora-linear sync import --from /path/to/bundle.json
40
+ ```
41
+
42
+ ## Where it writes
43
+
44
+ In precedence order:
45
+ 1. `--references-dir <path>` flag
46
+ 2. `LINEAR_REFERENCES_DIR` env var
47
+ 3. `~/.config/elnora-linear/` (auto-created if needed)
48
+
49
+ Never writes to the bundled defaults in the installed package — those stay clean.
50
+
51
+ ## Don't
52
+
53
+ - Don't run `sync all` repeatedly in a loop — it issues N×teams API calls
54
+ - Don't commit the populated reference files to a public repo — they're user-specific data
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ // elnora-linear CLI entrypoint.
3
+ //
4
+ // Commander-based subcommand dispatcher. Each subcommand is a thin wrapper
5
+ // over a function in src/commands/*; the heavy lifting (auth, API calls,
6
+ // formatting) happens there.
7
+ import { Command, Option } from "commander";
8
+ import { runBulk } from "./commands/bulk.js";
9
+ import { runCleanup } from "./commands/cleanup.js";
10
+ import { runCurator } from "./commands/curator.js";
11
+ import { runMyIssues } from "./commands/my-issues.js";
12
+ import { runSearch } from "./commands/search.js";
13
+ import { AUTO_SYNC_TARGETS, runSyncAll, runSyncImport, runSyncTarget, runSyncVerify, } from "./commands/sync.js";
14
+ const VERSION = "0.0.0";
15
+ const program = new Command()
16
+ .name("elnora-linear")
17
+ .description("Linear workspace CLI for Claude Code — search, bulk edit, agents, config-driven curator.")
18
+ .version(VERSION, "-v, --version", "Print version")
19
+ .helpOption("-h, --help", "Show this help");
20
+ program
21
+ .command("search")
22
+ .description("Search Linear issues")
23
+ .option("-q, --query <text>", "Search text (matches issue title + description)")
24
+ .option("-t, --team <key>", "Restrict to team key, e.g. ENG")
25
+ .option("-a, --assignee <name>", "Assignee name, email, or 'me'")
26
+ .option("-s, --state <name>", "Workflow state name, e.g. 'In Progress'")
27
+ .option("-l, --limit <n>", "Max results", (v) => Number.parseInt(v, 10), 25)
28
+ .option("-o, --output <mode>", "Output mode: text or json", "text")
29
+ .action(async (opts) => {
30
+ await runSearch(opts);
31
+ });
32
+ program
33
+ .command("my-issues")
34
+ .description("List issues assigned to you")
35
+ .option("-l, --limit <n>", "Max results", (v) => Number.parseInt(v, 10), 25)
36
+ .option("-o, --output <mode>", "Output mode: text or json", "text")
37
+ .action(async (opts) => {
38
+ await runMyIssues(opts);
39
+ });
40
+ program
41
+ .command("bulk")
42
+ .description("Apply the same change to many issues. Default is dry-run; pass --yes to commit.")
43
+ .option("-q, --query <text>", "Filter: search text")
44
+ .option("-t, --team <key>", "Filter: team key")
45
+ .option("-a, --assignee <name>", "Filter: assignee name, email, or 'me'")
46
+ .option("-s, --state <name>", "Filter: workflow state name")
47
+ .option("--set-state <name>", "Mutation: move matching issues to this state")
48
+ .option("--add-comment <text>", "Mutation: add this comment to each matching issue")
49
+ .option("-l, --limit <n>", "Max issues to touch", (v) => Number.parseInt(v, 10), 100)
50
+ .option("-y, --yes", "Commit the mutations (default is dry-run)", false)
51
+ .option("-o, --output <mode>", "Output mode: text or json", "text")
52
+ .action(async (opts) => {
53
+ await runBulk(opts);
54
+ });
55
+ program
56
+ .command("cleanup")
57
+ .description("Find stale issues and act on them. Default is dry-run; pass --yes to commit.")
58
+ .option("-t, --team <key>", "Filter: team key")
59
+ .option("-s, --states <names>", 'Filter: comma-separated state names (default "Todo,Backlog")', (v) => v.split(",").map((s) => s.trim()))
60
+ .option("--inactive-days <n>", "Issues with no activity for at least N days", (v) => Number.parseInt(v, 10), 30)
61
+ .addOption(new Option("--action <action>", "What to do with stale issues")
62
+ .choices(["close", "cancel", "comment"])
63
+ .default("comment"))
64
+ .option("--message <text>", "Comment text (overrides default cleanup message)")
65
+ .option("-l, --limit <n>", "Max issues to consider", (v) => Number.parseInt(v, 10), 100)
66
+ .option("-y, --yes", "Commit the mutations (default is dry-run)", false)
67
+ .option("-o, --output <mode>", "Output mode: text or json", "text")
68
+ .action(async (opts) => {
69
+ await runCleanup(opts);
70
+ });
71
+ const sync = program
72
+ .command("sync")
73
+ .description("Populate or refresh reference files. Run `elnora-linear sync --help` for subcommands.");
74
+ sync
75
+ .command("all")
76
+ .description("Refresh every auto-discoverable target (teams, projects, users, workflows)")
77
+ .option("--references-dir <path>", "Override default references directory")
78
+ .option("-o, --output <mode>", "Output mode: text or json", "text")
79
+ .action(async (opts) => {
80
+ await runSyncAll(opts);
81
+ });
82
+ for (const target of AUTO_SYNC_TARGETS) {
83
+ sync
84
+ .command(target)
85
+ .description(`Fetch ${target} from the Linear API and write references/${target}.json`)
86
+ .option("--references-dir <path>", "Override default references directory")
87
+ .option("-o, --output <mode>", "Output mode: text or json", "text")
88
+ .action(async (opts) => {
89
+ await runSyncTarget(target, opts);
90
+ });
91
+ }
92
+ sync
93
+ .command("verify")
94
+ .description("Validate every reference file against its schema; report which are user-populated vs placeholder.")
95
+ .option("--references-dir <path>", "Override default references directory")
96
+ .option("-o, --output <mode>", "Output mode: text or json", "text")
97
+ .action((opts) => {
98
+ runSyncVerify(opts);
99
+ });
100
+ sync
101
+ .command("import")
102
+ .description("Import a JSON bundle (top-level keys = reference names) into individual reference files.")
103
+ .requiredOption("--from <path>", "Path to the bundle JSON file")
104
+ .option("--references-dir <path>", "Override default references directory")
105
+ .option("-o, --output <mode>", "Output mode: text or json", "text")
106
+ .action((opts) => {
107
+ runSyncImport(opts);
108
+ });
109
+ program
110
+ .command("curator-run")
111
+ .description("Collect signals from configured signal sources and report. (Rule engine coming in a follow-up.)")
112
+ .option("--source <name>", "Run only the named source (matches signal_sources[].name)")
113
+ .option("--references-dir <path>", "Override default references directory")
114
+ .option("-o, --output <mode>", "Output mode: text or json", "text")
115
+ .action(async (opts) => {
116
+ await runCurator(opts);
117
+ });
118
+ try {
119
+ await program.parseAsync(process.argv);
120
+ }
121
+ catch (err) {
122
+ const message = err instanceof Error ? err.message : String(err);
123
+ process.stderr.write(`${message}\n`);
124
+ process.exit(1);
125
+ }
126
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export declare class AuthError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ declare function validateKey(key: string): string;
5
+ declare function readKeyFromEnvFile(path: string): string | null;
6
+ declare function saveKeyToEnvFile(key: string, path?: string): void;
7
+ export interface GetApiKeyOptions {
8
+ /** Override env-file path (default: ~/.config/elnora-linear/.env). */
9
+ envFilePath?: string;
10
+ /** If true and key isn't found, prompt user interactively (requires TTY). */
11
+ allowPrompt?: boolean;
12
+ }
13
+ export declare function getApiKey(opts?: GetApiKeyOptions): Promise<string>;
14
+ export declare const _internal: {
15
+ validateKey: typeof validateKey;
16
+ readKeyFromEnvFile: typeof readKeyFromEnvFile;
17
+ saveKeyToEnvFile: typeof saveKeyToEnvFile;
18
+ DEFAULT_ENV_FILE: string;
19
+ };
20
+ export {};
21
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/client/auth.ts"],"names":[],"mappings":"AAcA,qBAAa,SAAU,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI3B;AAKD,iBAAS,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMxC;AAED,iBAAS,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQvD;AAED,iBAAS,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,MAAyB,GAAG,IAAI,CAI5E;AAED,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAsB,SAAS,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B5E;AAGD,eAAO,MAAM,SAAS;;;;;CAA0E,CAAC"}
@@ -0,0 +1,74 @@
1
+ // Linear API key resolution.
2
+ //
3
+ // Precedence (highest first):
4
+ // 1. LINEAR_API_KEY env var
5
+ // 2. ~/.config/elnora-linear/.env (or path passed via envFilePath)
6
+ // 3. Interactive prompt (only if allowPrompt: true and stdin is a TTY)
7
+ //
8
+ // All Linear keys must start with "lin_api_" — validated before return.
9
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ import { createInterface } from "node:readline/promises";
13
+ export class AuthError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "AuthError";
17
+ }
18
+ }
19
+ const KEY_PREFIX = "lin_api_";
20
+ const DEFAULT_ENV_FILE = join(homedir(), ".config", "elnora-linear", ".env");
21
+ function validateKey(key) {
22
+ const trimmed = key.trim().replace(/^["']|["']$/g, "");
23
+ if (!trimmed.startsWith(KEY_PREFIX)) {
24
+ throw new AuthError(`Linear API key must start with "${KEY_PREFIX}". Got: ${trimmed.slice(0, 12)}…`);
25
+ }
26
+ return trimmed;
27
+ }
28
+ function readKeyFromEnvFile(path) {
29
+ if (!existsSync(path))
30
+ return null;
31
+ const content = readFileSync(path, "utf8");
32
+ for (const line of content.split("\n")) {
33
+ const match = line.match(/^LINEAR_API_KEY\s*=\s*(.+?)\s*$/);
34
+ if (match)
35
+ return match[1];
36
+ }
37
+ return null;
38
+ }
39
+ function saveKeyToEnvFile(key, path = DEFAULT_ENV_FILE) {
40
+ mkdirSync(dirname(path), { recursive: true });
41
+ writeFileSync(path, `LINEAR_API_KEY=${key}\n`, { mode: 0o600 });
42
+ chmodSync(path, 0o600);
43
+ }
44
+ export async function getApiKey(opts = {}) {
45
+ if (process.env.LINEAR_API_KEY) {
46
+ return validateKey(process.env.LINEAR_API_KEY);
47
+ }
48
+ const envFile = opts.envFilePath ?? DEFAULT_ENV_FILE;
49
+ const fileKey = readKeyFromEnvFile(envFile);
50
+ if (fileKey)
51
+ return validateKey(fileKey);
52
+ if (!opts.allowPrompt) {
53
+ throw new AuthError(`Linear API key not found. Set LINEAR_API_KEY env var, or place it in ${envFile} (mode 0600).`);
54
+ }
55
+ if (!process.stdin.isTTY) {
56
+ throw new AuthError("Linear API key not found and stdin is not a TTY; cannot prompt. Set LINEAR_API_KEY env var.");
57
+ }
58
+ process.stdout.write("Linear API key not found.\nGet one at https://linear.app/settings/api\nPaste it here: ");
59
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
60
+ let response;
61
+ try {
62
+ response = (await rl.question("")).trim();
63
+ }
64
+ finally {
65
+ rl.close();
66
+ }
67
+ const validated = validateKey(response);
68
+ saveKeyToEnvFile(validated, envFile);
69
+ process.stdout.write(`Saved to ${envFile} (mode 0600).\n`);
70
+ return validated;
71
+ }
72
+ // Exported for testing.
73
+ export const _internal = { validateKey, readKeyFromEnvFile, saveKeyToEnvFile, DEFAULT_ENV_FILE };
74
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/client/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export * from "./auth.js";
2
+ export * from "./linear-client.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./auth.js";
2
+ export * from "./linear-client.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { LinearClient } from "@linear/sdk";
2
+ import { type GetApiKeyOptions } from "./auth.js";
3
+ export declare function getLinearClient(opts?: GetApiKeyOptions): Promise<LinearClient>;
4
+ export declare function resetLinearClient(): void;
5
+ //# sourceMappingURL=linear-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-client.d.ts","sourceRoot":"","sources":["../../src/client/linear-client.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,KAAK,gBAAgB,EAAa,MAAM,WAAW,CAAC;AAI7D,wBAAsB,eAAe,CAAC,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAKpF;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
@@ -0,0 +1,18 @@
1
+ // Thin singleton wrapper around @linear/sdk's LinearClient.
2
+ //
3
+ // Process-wide cache so a single CLI invocation only auths + constructs once,
4
+ // no matter how many commands invoke it. Tests can reset via resetLinearClient.
5
+ import { LinearClient } from "@linear/sdk";
6
+ import { getApiKey } from "./auth.js";
7
+ let cached = null;
8
+ export async function getLinearClient(opts) {
9
+ if (cached)
10
+ return cached;
11
+ const apiKey = await getApiKey(opts);
12
+ cached = new LinearClient({ apiKey });
13
+ return cached;
14
+ }
15
+ export function resetLinearClient() {
16
+ cached = null;
17
+ }
18
+ //# sourceMappingURL=linear-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-client.js","sourceRoot":"","sources":["../../src/client/linear-client.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAEhF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAyB,SAAS,EAAE,MAAM,WAAW,CAAC;AAE7D,IAAI,MAAM,GAAwB,IAAI,CAAC;AAEvC,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAuB;IAC5D,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,GAAG,IAAI,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,OAAO,MAAM,CAAC;AACf,CAAC;AAED,MAAM,UAAU,iBAAiB;IAChC,MAAM,GAAG,IAAI,CAAC;AACf,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { OutputMode } from "../output/index.js";
2
+ import { type SearchOptions } from "./search.js";
3
+ export interface BulkOptions extends Omit<SearchOptions, "limit" | "output"> {
4
+ setState?: string;
5
+ addComment?: string;
6
+ limit: number;
7
+ yes: boolean;
8
+ output: OutputMode;
9
+ }
10
+ export interface BulkActionPlan {
11
+ issueIdentifier: string;
12
+ issueTitle: string;
13
+ currentState?: string;
14
+ changes: {
15
+ stateChange?: {
16
+ from: string;
17
+ to: string;
18
+ };
19
+ commentAdded?: string;
20
+ };
21
+ skipped?: {
22
+ reason: string;
23
+ };
24
+ }
25
+ export interface BulkPlan {
26
+ actions: BulkActionPlan[];
27
+ totalMatched: number;
28
+ dryRun: boolean;
29
+ }
30
+ /** Pure: given options + matched issues, compute the plan of changes. */
31
+ export declare function buildBulkPlan(opts: BulkOptions, issues: Array<{
32
+ identifier: string;
33
+ title: string;
34
+ state?: string;
35
+ }>): BulkPlan;
36
+ /** Pure: format a plan for the user. */
37
+ export declare function formatBulkPlan(plan: BulkPlan, mode: OutputMode): string;
38
+ export declare function runBulk(opts: BulkOptions): Promise<void>;
39
+ //# sourceMappingURL=bulk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulk.d.ts","sourceRoot":"","sources":["../../src/commands/bulk.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAiC,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEhF,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,QAAQ,CAAC;IAE3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE;QACR,WAAW,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC;QAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,OAAO,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7B;AAED,MAAM,WAAW,QAAQ;IACxB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;CAChB;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAC5B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,KAAK,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAClE,QAAQ,CAwBV;AAED,wCAAwC;AACxC,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAevE;AAUD,wBAAsB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAkC9D"}
@@ -0,0 +1,103 @@
1
+ // `elnora-linear bulk` — apply the same change to many Linear issues at once.
2
+ //
3
+ // Safety design:
4
+ // - Default behaviour is dry-run. The plan is printed; nothing is mutated.
5
+ // - `--yes` is required to commit mutations.
6
+ // - At least one of `--set-state` or `--add-comment` must be specified;
7
+ // otherwise the command errors out early.
8
+ import { getLinearClient } from "../client/index.js";
9
+ import { buildFilter } from "./search.js";
10
+ /** Pure: given options + matched issues, compute the plan of changes. */
11
+ export function buildBulkPlan(opts, issues) {
12
+ if (!opts.setState && !opts.addComment) {
13
+ throw new Error("bulk requires at least one of --set-state or --add-comment.");
14
+ }
15
+ const actions = issues.map((issue) => {
16
+ const action = {
17
+ issueIdentifier: issue.identifier,
18
+ issueTitle: issue.title,
19
+ currentState: issue.state,
20
+ changes: {},
21
+ };
22
+ if (opts.setState) {
23
+ if (issue.state && issue.state.toLowerCase() === opts.setState.toLowerCase()) {
24
+ action.skipped = { reason: `already in state "${issue.state}"` };
25
+ }
26
+ else {
27
+ action.changes.stateChange = { from: issue.state ?? "?", to: opts.setState };
28
+ }
29
+ }
30
+ if (opts.addComment) {
31
+ action.changes.commentAdded = opts.addComment;
32
+ }
33
+ return action;
34
+ });
35
+ return { actions, totalMatched: issues.length, dryRun: !opts.yes };
36
+ }
37
+ /** Pure: format a plan for the user. */
38
+ export function formatBulkPlan(plan, mode) {
39
+ if (mode === "json")
40
+ return JSON.stringify(plan, null, 2);
41
+ const willMutate = plan.actions.filter((a) => !a.skipped).length;
42
+ const header = plan.dryRun
43
+ ? `DRY RUN — ${willMutate} of ${plan.totalMatched} matched issues would change (pass --yes to commit)`
44
+ : `Applied changes to ${willMutate} of ${plan.totalMatched} matched issues`;
45
+ if (plan.actions.length === 0)
46
+ return `${header}\n (no matching issues)`;
47
+ const lines = plan.actions.map((a) => {
48
+ if (a.skipped)
49
+ return ` ${a.issueIdentifier} SKIPPED — ${a.skipped.reason} ${a.issueTitle}`;
50
+ const parts = [];
51
+ if (a.changes.stateChange)
52
+ parts.push(`state: ${a.changes.stateChange.from} → ${a.changes.stateChange.to}`);
53
+ if (a.changes.commentAdded)
54
+ parts.push("+ comment");
55
+ return ` ${a.issueIdentifier} ${parts.join(" | ")} ${a.issueTitle}`;
56
+ });
57
+ return [header, ...lines].join("\n");
58
+ }
59
+ async function findStateIdByName(client, teamId, name) {
60
+ const team = await client.team(teamId);
61
+ const states = await team.states();
62
+ const match = states.nodes.find((s) => s.name.toLowerCase() === name.toLowerCase());
63
+ if (!match)
64
+ throw new Error(`No workflow state named "${name}" in team ${team.key}.`);
65
+ return match.id;
66
+ }
67
+ export async function runBulk(opts) {
68
+ const client = await getLinearClient({ allowPrompt: true });
69
+ const filter = await buildFilter({ ...opts, limit: opts.limit, output: opts.output }, client);
70
+ const issuesResult = await client.issues({ filter, first: opts.limit });
71
+ const fetched = issuesResult.nodes;
72
+ const projected = await Promise.all(fetched.map(async (issue) => {
73
+ const state = await issue.state;
74
+ return { identifier: issue.identifier, title: issue.title, state: state?.name };
75
+ }));
76
+ const plan = buildBulkPlan(opts, projected);
77
+ process.stdout.write(`${formatBulkPlan(plan, opts.output)}\n`);
78
+ if (plan.dryRun)
79
+ return;
80
+ // Apply mutations.
81
+ const indexByIdentifier = new Map(fetched.map((i) => [i.identifier, i]));
82
+ for (const action of plan.actions) {
83
+ if (action.skipped)
84
+ continue;
85
+ const issue = indexByIdentifier.get(action.issueIdentifier);
86
+ if (!issue)
87
+ continue;
88
+ const updates = {};
89
+ if (action.changes.stateChange) {
90
+ const team = await issue.team;
91
+ if (!team)
92
+ throw new Error(`Issue ${issue.identifier} has no team`);
93
+ updates.stateId = await findStateIdByName(client, team.id, action.changes.stateChange.to);
94
+ }
95
+ if (Object.keys(updates).length > 0) {
96
+ await issue.update(updates);
97
+ }
98
+ if (action.changes.commentAdded) {
99
+ await client.createComment({ issueId: issue.id, body: action.changes.commentAdded });
100
+ }
101
+ }
102
+ }
103
+ //# sourceMappingURL=bulk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulk.js","sourceRoot":"","sources":["../../src/commands/bulk.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { OutputMode } from "../output/index.js";
2
+ export type CleanupAction = "close" | "cancel" | "comment";
3
+ export interface CleanupOptions {
4
+ team?: string;
5
+ /** Comma-separated state names; default ["Todo","Backlog"]. */
6
+ states?: string[];
7
+ inactiveDays: number;
8
+ action: CleanupAction;
9
+ message?: string;
10
+ limit: number;
11
+ yes: boolean;
12
+ output: OutputMode;
13
+ }
14
+ export interface CleanupActionPlan {
15
+ issueIdentifier: string;
16
+ issueTitle: string;
17
+ currentState?: string;
18
+ daysInactive: number;
19
+ proposed: {
20
+ setStateType?: "completed" | "canceled";
21
+ addComment?: string;
22
+ };
23
+ }
24
+ export interface CleanupPlan {
25
+ actions: CleanupActionPlan[];
26
+ totalConsidered: number;
27
+ dryRun: boolean;
28
+ }
29
+ /** Pure: given options and the candidate issues, compute the plan. */
30
+ export declare function buildCleanupPlan(opts: CleanupOptions, issues: Array<{
31
+ identifier: string;
32
+ title: string;
33
+ state?: string;
34
+ updatedAt: Date;
35
+ }>, now?: Date): CleanupPlan;
36
+ /** Pure: format a plan for the user. */
37
+ export declare function formatCleanupPlan(plan: CleanupPlan, mode: OutputMode): string;
38
+ export declare function runCleanup(opts: CleanupOptions): Promise<void>;
39
+ //# sourceMappingURL=cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../../src/commands/cleanup.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE;QACT,YAAY,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;QACxC,UAAU,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACF;AAED,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;CAChB;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,KAAK,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,EACrF,GAAG,GAAE,IAAiB,GACpB,WAAW,CAqBb;AAED,wCAAwC;AACxC,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAa7E;AAUD,wBAAsB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCpE"}