@enrichlayer/el-linear 1.10.0 → 1.16.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.
- package/README.md +126 -10
- package/claude-skills/linear-operations/SKILL.md +41 -1
- package/dist/auth/linear-credential.d.ts +27 -0
- package/dist/auth/linear-credential.js +1 -0
- package/dist/auth/oauth-app-config.d.ts +4 -3
- package/dist/auth/oauth-app-config.js +13 -2
- package/dist/auth/oauth-callback.d.ts +2 -3
- package/dist/auth/oauth-callback.js +2 -2
- package/dist/auth/oauth-client.d.ts +8 -2
- package/dist/auth/oauth-client.js +26 -0
- package/dist/auth/oauth-fs.d.ts +2 -1
- package/dist/auth/oauth-headless.d.ts +2 -1
- package/dist/auth/oauth-storage.d.ts +5 -1
- package/dist/auth/oauth-storage.js +1 -1
- package/dist/auth/oauth-token.d.ts +4 -3
- package/dist/auth/oauth-token.js +16 -4
- package/dist/auth/token-resolver.d.ts +14 -5
- package/dist/auth/token-resolver.js +6 -1
- package/dist/commands/batch.js +18 -21
- package/dist/commands/comments.js +27 -6
- package/dist/commands/config.js +178 -5
- package/dist/commands/init/aliases.js +1 -1
- package/dist/commands/init/defaults.d.ts +2 -1
- package/dist/commands/init/index.js +45 -35
- package/dist/commands/init/oauth.d.ts +4 -1
- package/dist/commands/init/oauth.js +22 -4
- package/dist/commands/init/shared.d.ts +24 -2
- package/dist/commands/init/shared.js +35 -4
- package/dist/commands/init/token.d.ts +3 -3
- package/dist/commands/init/token.js +5 -24
- package/dist/commands/init/workspace.d.ts +2 -1
- package/dist/commands/init/workspace.js +1 -1
- package/dist/commands/introspect.d.ts +27 -0
- package/dist/commands/introspect.js +178 -0
- package/dist/commands/issues/branch.js +9 -1
- package/dist/commands/issues/relations.d.ts +3 -14
- package/dist/commands/issues/relations.js +3 -3
- package/dist/commands/issues.js +222 -43
- package/dist/commands/labels.js +2 -1
- package/dist/commands/profile.js +1 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +91 -7
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +67 -8
- package/dist/commands/search.js +30 -5
- package/dist/commands/users.js +4 -2
- package/dist/config/config.d.ts +99 -1
- package/dist/config/config.js +264 -52
- package/dist/config/error-enrichment.d.ts +62 -0
- package/dist/config/error-enrichment.js +417 -0
- package/dist/config/issue-validation.d.ts +37 -0
- package/dist/config/issue-validation.js +63 -1
- package/dist/config/paths.d.ts +2 -8
- package/dist/config/paths.js +4 -2
- package/dist/config/resolver.d.ts +8 -1
- package/dist/config/resolver.js +9 -2
- package/dist/main.js +13 -1
- package/dist/output.d.ts +82 -0
- package/dist/output.js +82 -0
- package/dist/queries/comments-types.d.ts +15 -9
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +4 -3
- package/dist/queries/introspect-types.d.ts +8 -7
- package/dist/queries/issues-types.d.ts +92 -27
- package/dist/queries/issues.d.ts +49 -10
- package/dist/queries/issues.js +125 -5
- package/dist/queries/labels-types.d.ts +7 -6
- package/dist/queries/project-milestones-types.d.ts +5 -4
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +8 -7
- package/dist/queries/releases-types.d.ts +5 -4
- package/dist/queries/search-types.d.ts +28 -12
- package/dist/queries/templates-types.d.ts +3 -2
- package/dist/types/linear.d.ts +13 -1
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +1 -10
- package/dist/utils/extract-field.d.ts +19 -0
- package/dist/utils/extract-field.js +99 -0
- package/dist/utils/file-service.d.ts +6 -13
- package/dist/utils/file-service.js +0 -2
- package/dist/utils/formatters/summary.js +6 -1
- package/dist/utils/graphql-issues-service.d.ts +101 -45
- package/dist/utils/graphql-issues-service.js +252 -39
- package/dist/utils/graphql-service.d.ts +10 -12
- package/dist/utils/graphql-service.js +0 -3
- package/dist/utils/issue-reference-extractor.d.ts +7 -0
- package/dist/utils/issue-reference-extractor.js +5 -3
- package/dist/utils/issues-service-bootstrap.d.ts +28 -0
- package/dist/utils/issues-service-bootstrap.js +27 -0
- package/dist/utils/linear-service.d.ts +21 -14
- package/dist/utils/linear-service.js +73 -11
- package/dist/utils/markdown-prosemirror.js +12 -12
- package/dist/utils/mention-resolver.js +1 -1
- package/dist/utils/output.d.ts +81 -3
- package/dist/utils/output.js +61 -6
- package/dist/utils/project-slug.d.ts +21 -0
- package/dist/utils/project-slug.js +45 -0
- package/dist/utils/protected-ranges.d.ts +14 -0
- package/dist/utils/protected-ranges.js +88 -2
- package/dist/utils/sanitize-for-log.d.ts +24 -0
- package/dist/utils/sanitize-for-log.js +38 -0
- package/dist/utils/table-formatter.js +24 -0
- package/dist/utils/validators.d.ts +7 -2
- package/dist/utils/validators.js +6 -0
- package/dist/utils/workspace-url.js +20 -4
- package/package.json +7 -2
|
@@ -29,8 +29,30 @@ type DeepPartial<T> = T extends Array<infer _U> ? T : T extends object ? {
|
|
|
29
29
|
*/
|
|
30
30
|
export type WizardConfig = DeepPartial<ElLinearConfig>;
|
|
31
31
|
export declare function ensureConfigDir(): Promise<void>;
|
|
32
|
-
export declare function readConfig(): Promise<WizardConfig>;
|
|
33
|
-
export declare function writeConfig(config: WizardConfig): Promise<void>;
|
|
32
|
+
export declare function readConfig(configPath?: string): Promise<WizardConfig>;
|
|
33
|
+
export declare function writeConfig(config: WizardConfig, configPath?: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Run a read-modify-write update on the active profile's config.json under
|
|
36
|
+
* an exclusive file lock. The mutator receives the latest on-disk config
|
|
37
|
+
* (re-read inside the lock), and its return value is written back atomically.
|
|
38
|
+
*
|
|
39
|
+
* Use this anywhere two parallel wizard invocations could race a
|
|
40
|
+
* read → mutate → write sequence. Each `el-linear init <step>` re-reads the
|
|
41
|
+
* config and merges its slice; without serialization, the slower writer's
|
|
42
|
+
* mutation would clobber the faster writer's already-persisted changes.
|
|
43
|
+
*
|
|
44
|
+
* The interactive prompt phase MUST run outside the lock — prompts can sit
|
|
45
|
+
* waiting for user input longer than the lock's stale window. Seed prompts
|
|
46
|
+
* with a cheap pre-read, do the prompts, then call `updateConfig` with a
|
|
47
|
+
* mutator that re-reads and merges your slice on top of the latest state.
|
|
48
|
+
*
|
|
49
|
+
* The active-profile path is snapshotted ONCE at entry and threaded through
|
|
50
|
+
* `readConfig` + `writeConfig`. A theoretical mid-update profile switch
|
|
51
|
+
* (`--profile` is bound by the commander preAction before any subcommand
|
|
52
|
+
* runs, so this can't happen via the CLI today) can't cause a lock-A /
|
|
53
|
+
* read-or-write-B mismatch.
|
|
54
|
+
*/
|
|
55
|
+
export declare function updateConfig(mutator: (current: WizardConfig) => WizardConfig | Promise<WizardConfig>): Promise<void>;
|
|
34
56
|
export declare function readToken(): Promise<string | null>;
|
|
35
57
|
/**
|
|
36
58
|
* Write the token to disk with mode 0600.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { randomBytes } from "node:crypto";
|
|
9
9
|
import fs from "node:fs/promises";
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import { withFileLock } from "../../auth/oauth-fs.js";
|
|
11
12
|
import { ALIASES_PROGRESS_PATH, CONFIG_DIR, CONFIG_PATH, resolveActiveProfile, TOKEN_PATH, } from "../../config/paths.js";
|
|
12
13
|
// Re-export for tests and call sites that already pulled the paths from here.
|
|
13
14
|
export { ALIASES_PROGRESS_PATH, CONFIG_PATH, TOKEN_PATH };
|
|
@@ -65,9 +66,9 @@ export async function ensureConfigDir() {
|
|
|
65
66
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
|
-
export async function readConfig() {
|
|
69
|
+
export async function readConfig(configPath = activePaths().configPath) {
|
|
69
70
|
try {
|
|
70
|
-
const raw = await fs.readFile(
|
|
71
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
71
72
|
return JSON.parse(raw);
|
|
72
73
|
}
|
|
73
74
|
catch (err) {
|
|
@@ -77,11 +78,41 @@ export async function readConfig() {
|
|
|
77
78
|
throw err;
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
|
-
export async function writeConfig(config) {
|
|
81
|
+
export async function writeConfig(config, configPath = activePaths().configPath) {
|
|
81
82
|
await ensureConfigDir();
|
|
82
83
|
// Stable key order so byte-identical config produces byte-identical output.
|
|
83
84
|
const sorted = sortKeys(config);
|
|
84
|
-
await atomicWrite(
|
|
85
|
+
await atomicWrite(configPath, `${JSON.stringify(sorted, null, 2)}\n`, 0o644);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Run a read-modify-write update on the active profile's config.json under
|
|
89
|
+
* an exclusive file lock. The mutator receives the latest on-disk config
|
|
90
|
+
* (re-read inside the lock), and its return value is written back atomically.
|
|
91
|
+
*
|
|
92
|
+
* Use this anywhere two parallel wizard invocations could race a
|
|
93
|
+
* read → mutate → write sequence. Each `el-linear init <step>` re-reads the
|
|
94
|
+
* config and merges its slice; without serialization, the slower writer's
|
|
95
|
+
* mutation would clobber the faster writer's already-persisted changes.
|
|
96
|
+
*
|
|
97
|
+
* The interactive prompt phase MUST run outside the lock — prompts can sit
|
|
98
|
+
* waiting for user input longer than the lock's stale window. Seed prompts
|
|
99
|
+
* with a cheap pre-read, do the prompts, then call `updateConfig` with a
|
|
100
|
+
* mutator that re-reads and merges your slice on top of the latest state.
|
|
101
|
+
*
|
|
102
|
+
* The active-profile path is snapshotted ONCE at entry and threaded through
|
|
103
|
+
* `readConfig` + `writeConfig`. A theoretical mid-update profile switch
|
|
104
|
+
* (`--profile` is bound by the commander preAction before any subcommand
|
|
105
|
+
* runs, so this can't happen via the CLI today) can't cause a lock-A /
|
|
106
|
+
* read-or-write-B mismatch.
|
|
107
|
+
*/
|
|
108
|
+
export async function updateConfig(mutator) {
|
|
109
|
+
await ensureConfigDir();
|
|
110
|
+
const configPath = activePaths().configPath;
|
|
111
|
+
await withFileLock(configPath, async () => {
|
|
112
|
+
const current = await readConfig(configPath);
|
|
113
|
+
const next = await mutator(current);
|
|
114
|
+
await writeConfig(next, configPath);
|
|
115
|
+
});
|
|
85
116
|
}
|
|
86
117
|
export async function readToken() {
|
|
87
118
|
try {
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* before saving. Token is stored at ~/.config/el-linear/token (mode 0600),
|
|
6
6
|
* never embedded in config.json.
|
|
7
7
|
*/
|
|
8
|
+
import { sanitizeForLog } from "../../utils/sanitize-for-log.js";
|
|
9
|
+
export { sanitizeForLog };
|
|
8
10
|
interface ViewerResponse {
|
|
9
11
|
viewer: {
|
|
10
12
|
id: string;
|
|
@@ -17,11 +19,10 @@ interface ViewerResponse {
|
|
|
17
19
|
};
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
|
-
|
|
22
|
+
interface TokenStepResult {
|
|
21
23
|
token: string;
|
|
22
24
|
viewer: ViewerResponse["viewer"];
|
|
23
25
|
}
|
|
24
|
-
export declare function sanitizeForLog(text: string): string;
|
|
25
26
|
/**
|
|
26
27
|
* Validate a Linear API token by fetching the viewer. Throws with a
|
|
27
28
|
* sanitized user-readable message on auth failure — the error string is
|
|
@@ -39,4 +40,3 @@ export declare function runTokenStep(options?: {
|
|
|
39
40
|
/** Skip the "replace existing?" prompt; always replace if existing is present. */
|
|
40
41
|
force?: boolean;
|
|
41
42
|
}): Promise<TokenStepResult>;
|
|
42
|
-
export {};
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { confirm, password } from "@inquirer/prompts";
|
|
9
9
|
import { GraphQLService } from "../../utils/graphql-service.js";
|
|
10
|
+
import { sanitizeForLog } from "../../utils/sanitize-for-log.js";
|
|
10
11
|
import { readToken, writeToken } from "./shared.js";
|
|
12
|
+
// Re-export for legacy import paths under `init/`. New code should import
|
|
13
|
+
// from `utils/sanitize-for-log.js` directly.
|
|
14
|
+
export { sanitizeForLog };
|
|
11
15
|
const TOKEN_GENERATION_URL = "https://linear.app/settings/account/security";
|
|
12
16
|
const VIEWER_QUERY = /* GraphQL */ `
|
|
13
17
|
query {
|
|
@@ -23,29 +27,6 @@ const VIEWER_QUERY = /* GraphQL */ `
|
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
`;
|
|
26
|
-
/**
|
|
27
|
-
* Strip anything that looks like a Linear API token from a string. Defense in
|
|
28
|
-
* depth: today the @linear/sdk error message embeds {query, variables} but not
|
|
29
|
-
* the Authorization header. A future SDK upgrade that includes headers (which
|
|
30
|
-
* upstream graphql-request has done historically) would otherwise silently
|
|
31
|
-
* write `Bearer lin_api_…` into stdout / shell history / CI logs. The regex
|
|
32
|
-
* also catches token shapes that may show up in custom error wrappers.
|
|
33
|
-
*/
|
|
34
|
-
// Personal-API tokens (`lin_api_…`) and OAuth access/refresh tokens
|
|
35
|
-
// (`lin_oauth_…`). Pre-fix the regex only matched personal tokens.
|
|
36
|
-
const TOKEN_PREFIX_RE = /lin_(api|oauth)_[A-Za-z0-9_-]{16,}/g;
|
|
37
|
-
// High-entropy bearer payload fallback: catches generic Bearer-style
|
|
38
|
-
// strings adjacent to Authorization / Bearer keywords. Useful for
|
|
39
|
-
// future SDK error wrappers that might leak headers without the
|
|
40
|
-
// `lin_` prefix.
|
|
41
|
-
const BEARER_PAYLOAD_RE = /(\b(?:Authorization|Bearer)\b[:\s]*)([A-Za-z0-9_\-/+=]{40,})/gi;
|
|
42
|
-
export function sanitizeForLog(text) {
|
|
43
|
-
return text
|
|
44
|
-
.replace(TOKEN_PREFIX_RE, (m) => m.startsWith("lin_oauth_")
|
|
45
|
-
? "lin_oauth_***REDACTED***"
|
|
46
|
-
: "lin_api_***REDACTED***")
|
|
47
|
-
.replace(BEARER_PAYLOAD_RE, "$1***REDACTED***");
|
|
48
|
-
}
|
|
49
30
|
/**
|
|
50
31
|
* Strict shape check on the viewer response. Treats whitespace-only fields as
|
|
51
32
|
* "validated to nothing" — easy to forge with a malformed but truthy stub
|
|
@@ -73,7 +54,7 @@ function viewerIsValid(viewer) {
|
|
|
73
54
|
* is redacted before it hits stdout.
|
|
74
55
|
*/
|
|
75
56
|
export async function validateToken(token) {
|
|
76
|
-
const service = new GraphQLService(token);
|
|
57
|
+
const service = new GraphQLService({ apiKey: token });
|
|
77
58
|
let data;
|
|
78
59
|
try {
|
|
79
60
|
data = await service.rawRequest(VIEWER_QUERY);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Lets the user pick a default team, optional. Skip-by-default.
|
|
6
6
|
*/
|
|
7
7
|
import type { WizardConfig } from "./shared.js";
|
|
8
|
-
|
|
8
|
+
interface WorkspaceStepResult {
|
|
9
9
|
workspaceUrlKey: string;
|
|
10
10
|
defaultTeam: string | undefined;
|
|
11
11
|
teams: Record<string, string>;
|
|
@@ -18,3 +18,4 @@ export interface WorkspaceStepResult {
|
|
|
18
18
|
* the existing config exactly.
|
|
19
19
|
*/
|
|
20
20
|
export declare function runWorkspaceStep(token: string, workspaceUrlKey: string, existing: WizardConfig): Promise<WorkspaceStepResult>;
|
|
21
|
+
export {};
|
|
@@ -25,7 +25,7 @@ const TEAMS_QUERY = /* GraphQL */ `
|
|
|
25
25
|
* the existing config exactly.
|
|
26
26
|
*/
|
|
27
27
|
export async function runWorkspaceStep(token, workspaceUrlKey, existing) {
|
|
28
|
-
const service = new GraphQLService(token);
|
|
28
|
+
const service = new GraphQLService({ apiKey: token });
|
|
29
29
|
// Fetch teams once so we can populate both the picker and the cached id map.
|
|
30
30
|
const data = await service.rawRequest(TEAMS_QUERY);
|
|
31
31
|
const teams = data?.teams?.nodes ?? [];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `el-linear introspect` and `el-linear validate-flag` — expose the
|
|
3
|
+
* commander command tree so external linters (e.g. CI steps that scan
|
|
4
|
+
* SKILL.md files for `el-linear <cmd> --<flag>` references) can verify
|
|
5
|
+
* that hardcoded flag names actually exist on the current binary.
|
|
6
|
+
*
|
|
7
|
+
* Without this, a flag rename inside `el-linear` (say `--parent-ticket`
|
|
8
|
+
* → `--parent`) silently breaks every skill that prose-references the
|
|
9
|
+
* old name. Skills then fail at runtime, far from the rename commit.
|
|
10
|
+
*
|
|
11
|
+
* `introspect` dumps the full tree — name, description, version,
|
|
12
|
+
* options, aliases, and recursively nested subcommands.
|
|
13
|
+
*
|
|
14
|
+
* `validate-flag` is a thin ergonomic wrapper for the CI-lint use case:
|
|
15
|
+
*
|
|
16
|
+
* $ el-linear validate-flag issues create --parent-ticket
|
|
17
|
+
* { "ok": true, "command": ["issues", "create"], "flag": "--parent-ticket" }
|
|
18
|
+
* $ el-linear validate-flag issues create --does-not-exist
|
|
19
|
+
* { "ok": false, ... "error": "..." } # exit code 1
|
|
20
|
+
*
|
|
21
|
+
* Both commands ignore root-level flags (`--api-token`, `--json`, etc.)
|
|
22
|
+
* when walking subcommand options; those are dumped at the root, not
|
|
23
|
+
* under every command. `validate-flag` walks ancestors so a global flag
|
|
24
|
+
* (e.g. `--format`) validates on any subcommand.
|
|
25
|
+
*/
|
|
26
|
+
import type { Command } from "commander";
|
|
27
|
+
export declare function setupIntrospectCommand(program: Command): void;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `el-linear introspect` and `el-linear validate-flag` — expose the
|
|
3
|
+
* commander command tree so external linters (e.g. CI steps that scan
|
|
4
|
+
* SKILL.md files for `el-linear <cmd> --<flag>` references) can verify
|
|
5
|
+
* that hardcoded flag names actually exist on the current binary.
|
|
6
|
+
*
|
|
7
|
+
* Without this, a flag rename inside `el-linear` (say `--parent-ticket`
|
|
8
|
+
* → `--parent`) silently breaks every skill that prose-references the
|
|
9
|
+
* old name. Skills then fail at runtime, far from the rename commit.
|
|
10
|
+
*
|
|
11
|
+
* `introspect` dumps the full tree — name, description, version,
|
|
12
|
+
* options, aliases, and recursively nested subcommands.
|
|
13
|
+
*
|
|
14
|
+
* `validate-flag` is a thin ergonomic wrapper for the CI-lint use case:
|
|
15
|
+
*
|
|
16
|
+
* $ el-linear validate-flag issues create --parent-ticket
|
|
17
|
+
* { "ok": true, "command": ["issues", "create"], "flag": "--parent-ticket" }
|
|
18
|
+
* $ el-linear validate-flag issues create --does-not-exist
|
|
19
|
+
* { "ok": false, ... "error": "..." } # exit code 1
|
|
20
|
+
*
|
|
21
|
+
* Both commands ignore root-level flags (`--api-token`, `--json`, etc.)
|
|
22
|
+
* when walking subcommand options; those are dumped at the root, not
|
|
23
|
+
* under every command. `validate-flag` walks ancestors so a global flag
|
|
24
|
+
* (e.g. `--format`) validates on any subcommand.
|
|
25
|
+
*/
|
|
26
|
+
import { outputSuccess } from "../utils/output.js";
|
|
27
|
+
function describeOption(opt) {
|
|
28
|
+
return {
|
|
29
|
+
flags: opt.flags,
|
|
30
|
+
long: opt.long ?? null,
|
|
31
|
+
short: opt.short ?? null,
|
|
32
|
+
description: opt.description,
|
|
33
|
+
required: opt.required,
|
|
34
|
+
optional: opt.optional,
|
|
35
|
+
variadic: opt.variadic,
|
|
36
|
+
defaultValue: opt.defaultValue,
|
|
37
|
+
negated: opt.negate,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function describeCommand(cmd) {
|
|
41
|
+
return {
|
|
42
|
+
name: cmd.name(),
|
|
43
|
+
aliases: cmd.aliases(),
|
|
44
|
+
description: cmd.description(),
|
|
45
|
+
usage: cmd.usage(),
|
|
46
|
+
options: cmd.options.map(describeOption),
|
|
47
|
+
commands: cmd.commands.map(describeCommand),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Walk the command tree to find the command at `path`. Returns null if
|
|
52
|
+
* any segment doesn't resolve. Matches both canonical name and aliases
|
|
53
|
+
* so `el-linear validate-flag issue get --foo` works the same as
|
|
54
|
+
* `el-linear validate-flag issues read --foo`.
|
|
55
|
+
*/
|
|
56
|
+
function findCommand(root, path) {
|
|
57
|
+
let current = root;
|
|
58
|
+
for (const segment of path) {
|
|
59
|
+
// Match by canonical name or alias. Commander throws on
|
|
60
|
+
// duplicate aliases at registration time, so first-match-wins is
|
|
61
|
+
// only meaningful for the name/alias choice within a single
|
|
62
|
+
// command, not for ambiguity across siblings.
|
|
63
|
+
const next = current.commands.find((c) => c.name() === segment || c.aliases().includes(segment));
|
|
64
|
+
if (!next)
|
|
65
|
+
return null;
|
|
66
|
+
current = next;
|
|
67
|
+
}
|
|
68
|
+
return current;
|
|
69
|
+
}
|
|
70
|
+
export function setupIntrospectCommand(program) {
|
|
71
|
+
program
|
|
72
|
+
.command("cli-introspect [path...]")
|
|
73
|
+
.description("Dump the el-linear CLI command tree as JSON so external linters can verify flag references in prose (e.g. SKILL.md). Pass a path (`cli-introspect issues create`) to dump a specific subtree. Distinct from `introspect` (which queries the Linear GraphQL schema).")
|
|
74
|
+
.action((path) => {
|
|
75
|
+
const target = path.length === 0 ? program : findCommand(program, path);
|
|
76
|
+
if (!target) {
|
|
77
|
+
outputSuccess({
|
|
78
|
+
error: `Command not found: ${path.join(" ")}`,
|
|
79
|
+
path,
|
|
80
|
+
});
|
|
81
|
+
process.exit(1);
|
|
82
|
+
return; // Defense: explicit so test correctness doesn't
|
|
83
|
+
// depend on the vi.spyOn(process.exit) mock throwing.
|
|
84
|
+
}
|
|
85
|
+
if (path.length === 0) {
|
|
86
|
+
outputSuccess({
|
|
87
|
+
name: program.name(),
|
|
88
|
+
description: program.description(),
|
|
89
|
+
version: program.version() ?? null,
|
|
90
|
+
options: program.options.map(describeOption),
|
|
91
|
+
commands: program.commands.map(describeCommand),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
outputSuccess(describeCommand(target));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
program
|
|
99
|
+
.command("validate-flag [args...]")
|
|
100
|
+
.description("Verify a flag exists on a command. Two equivalent forms:\n" +
|
|
101
|
+
" $ el-linear validate-flag --check-flag --parent-ticket issues create\n" +
|
|
102
|
+
" $ el-linear validate-flag issues create --parent-ticket\n" +
|
|
103
|
+
"The first form is unambiguous regardless of whether the flag-to-check " +
|
|
104
|
+
"takes an argument; the positional form needs `--` before flags that " +
|
|
105
|
+
"take args (e.g. `validate-flag -- issues create --jq`) so commander " +
|
|
106
|
+
"doesn't try to consume the next positional as the flag's value. " +
|
|
107
|
+
"Exits 0 if the flag is defined on the target command (or any " +
|
|
108
|
+
"ancestor for inherited globals), 1 otherwise.")
|
|
109
|
+
// Without these, commander tries to parse `--parent-ticket` (the
|
|
110
|
+
// flag we want to *check*) as if it were an option OF validate-flag
|
|
111
|
+
// itself. We need it to pass through as a positional arg.
|
|
112
|
+
.allowUnknownOption(true)
|
|
113
|
+
.option("--check-flag <flag>", "the flag to look up (unambiguous alternative to passing it positionally — use this when the flag-being-checked is one that takes an argument)")
|
|
114
|
+
.action((args, options) => {
|
|
115
|
+
// Resolve the flag-to-check from either `--check-flag <flag>` or
|
|
116
|
+
// the first positional arg that starts with `-`. The explicit
|
|
117
|
+
// option form sidesteps commander's positional ambiguity for
|
|
118
|
+
// flags that take arguments (e.g. `--jq <filter>`).
|
|
119
|
+
let flag;
|
|
120
|
+
let path;
|
|
121
|
+
if (options.checkFlag) {
|
|
122
|
+
flag = options.checkFlag;
|
|
123
|
+
path = args; // all positionals are the command path
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const flagIdx = args.findIndex((a) => a.startsWith("-"));
|
|
127
|
+
if (flagIdx === -1) {
|
|
128
|
+
outputSuccess({
|
|
129
|
+
ok: false,
|
|
130
|
+
error: "No flag argument found. Pass `--check-flag <flag>` or include `--<flag>` in the positional args.",
|
|
131
|
+
args,
|
|
132
|
+
});
|
|
133
|
+
process.exit(1);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
path = args.slice(0, flagIdx);
|
|
137
|
+
flag = args[flagIdx];
|
|
138
|
+
}
|
|
139
|
+
const target = path.length === 0 ? program : findCommand(program, path);
|
|
140
|
+
if (!target) {
|
|
141
|
+
outputSuccess({
|
|
142
|
+
ok: false,
|
|
143
|
+
error: `Command not found: ${path.join(" ") || "(root)"}`,
|
|
144
|
+
command: path,
|
|
145
|
+
flag,
|
|
146
|
+
});
|
|
147
|
+
process.exit(1);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Walk the target + ancestors so root-level globals
|
|
151
|
+
// (--api-token, --format, etc.) validate when a skill writes
|
|
152
|
+
// `el-linear issues create --format summary`.
|
|
153
|
+
const ancestors = [];
|
|
154
|
+
for (let c = target; c; c = c.parent) {
|
|
155
|
+
ancestors.push(c);
|
|
156
|
+
}
|
|
157
|
+
const allOptions = ancestors.flatMap((c) => c.options);
|
|
158
|
+
const found = allOptions.find((opt) => opt.long === flag || opt.short === flag);
|
|
159
|
+
if (!found) {
|
|
160
|
+
outputSuccess({
|
|
161
|
+
ok: false,
|
|
162
|
+
error: `Flag not found: ${flag}`,
|
|
163
|
+
command: path,
|
|
164
|
+
flag,
|
|
165
|
+
availableOptions: target.options.map((o) => o.long ?? o.short),
|
|
166
|
+
});
|
|
167
|
+
process.exit(1);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
outputSuccess({
|
|
171
|
+
ok: true,
|
|
172
|
+
command: path,
|
|
173
|
+
flag,
|
|
174
|
+
flags: found.flags,
|
|
175
|
+
description: found.description,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -36,5 +36,13 @@ export function gitCheckoutBranch(branchName) {
|
|
|
36
36
|
outputWarning("Not inside a git repository — skipping branch checkout.");
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
// `--` separates the new-branch name from any ref. Without it, a
|
|
40
|
+
// branch name starting with `-` (server bug, malicious team member
|
|
41
|
+
// crafting an issue title that survives slugification, or a forked
|
|
42
|
+
// Linear-compatible API) would be parsed as a git flag rather than
|
|
43
|
+
// a ref. The default `feature/` prefix from `toBranchName` already
|
|
44
|
+
// blocks the common case, but the prefix is configurable — empty-
|
|
45
|
+
// prefix callers would lose the defense without this terminator.
|
|
46
|
+
// Defense-in-depth (DEV-4064).
|
|
47
|
+
execFileSync("git", ["checkout", "-b", branchName, "--"], { stdio: "pipe" });
|
|
40
48
|
}
|
|
@@ -6,12 +6,10 @@
|
|
|
6
6
|
* Extracted from `commands/issues.ts` (ALL-938) so that file can
|
|
7
7
|
* focus on commander wiring + handlers.
|
|
8
8
|
*/
|
|
9
|
-
import type {
|
|
10
|
-
import type { LinearIssueRelation } from "../../types/linear.js";
|
|
9
|
+
import type { RelationIncomingNode, RelationOutgoingNode } from "../../queries/issues-types.js";
|
|
10
|
+
import type { LinearIssueRelation, LinearPriority } from "../../types/linear.js";
|
|
11
11
|
import type { GraphQLService } from "../../utils/graphql-service.js";
|
|
12
12
|
import type { LinearService } from "../../utils/linear-service.js";
|
|
13
|
-
type CreatedIssueRelation = NonNullable<IssueRelationCreateResponse["issueRelationCreate"]["issueRelation"]>;
|
|
14
|
-
export declare function transformIssueRelation(rel: CreatedIssueRelation): LinearIssueRelation;
|
|
15
13
|
/**
|
|
16
14
|
* Walk the `--related-to` / `--blocks` / `--blocked-by` /
|
|
17
15
|
* `--duplicate-of` flags and create the corresponding
|
|
@@ -29,7 +27,7 @@ export interface RelatedIssueEntry {
|
|
|
29
27
|
id: string;
|
|
30
28
|
name: string;
|
|
31
29
|
};
|
|
32
|
-
priority?:
|
|
30
|
+
priority?: LinearPriority;
|
|
33
31
|
assignee?: {
|
|
34
32
|
id: string;
|
|
35
33
|
name: string;
|
|
@@ -42,14 +40,5 @@ export interface RelatedIssueEntry {
|
|
|
42
40
|
};
|
|
43
41
|
type: string;
|
|
44
42
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Invert relation type for incoming (inverse) relations so the output
|
|
47
|
-
* reads naturally from the perspective of the queried issue. E.g. if
|
|
48
|
-
* DEV-100 "blocks" DEV-200, and we query DEV-200, the inverse relation
|
|
49
|
-
* type is "blocks" but direction is incoming → "blockedBy".
|
|
50
|
-
*/
|
|
51
|
-
export declare function normalizeInverseType(type: string): string;
|
|
52
|
-
export declare function buildRelatedIssueSummary(peer: RelationPeerNode): RelatedIssueEntry["issue"];
|
|
53
43
|
export declare function buildOutgoingRelationEntries(nodes: RelationOutgoingNode[] | undefined): RelatedIssueEntry[];
|
|
54
44
|
export declare function buildIncomingRelationEntries(nodes: RelationIncomingNode[] | undefined): RelatedIssueEntry[];
|
|
55
|
-
export {};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ISSUE_RELATION_CREATE_MUTATION } from "../../queries/issues.js";
|
|
10
10
|
import { splitList } from "../../utils/validators.js";
|
|
11
|
-
|
|
11
|
+
function transformIssueRelation(rel) {
|
|
12
12
|
return {
|
|
13
13
|
id: rel.id,
|
|
14
14
|
type: rel.type,
|
|
@@ -66,13 +66,13 @@ export async function createRelations(sourceId, options, graphQLService, linearS
|
|
|
66
66
|
* DEV-100 "blocks" DEV-200, and we query DEV-200, the inverse relation
|
|
67
67
|
* type is "blocks" but direction is incoming → "blockedBy".
|
|
68
68
|
*/
|
|
69
|
-
|
|
69
|
+
function normalizeInverseType(type) {
|
|
70
70
|
if (type === "blocks") {
|
|
71
71
|
return "blockedBy";
|
|
72
72
|
}
|
|
73
73
|
return type;
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
function buildRelatedIssueSummary(peer) {
|
|
76
76
|
return {
|
|
77
77
|
id: peer.id,
|
|
78
78
|
identifier: peer.identifier,
|