@floh-solutions/pharos-cli 0.25.0 → 0.26.1

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.
@@ -0,0 +1,58 @@
1
+ import type { GitHubIssue } from "@floh-solutions/gh-core";
2
+ import { type CategorySource } from "./bridge.js";
3
+ import type { Session } from "./session.js";
4
+ /**
5
+ * Which work item type an adopted issue becomes — **resolved, never named**
6
+ * (#872).
7
+ *
8
+ * ## The bug this replaces
9
+ *
10
+ * `issue.ts` carried `const DEFAULT_WORK_ITEM_TYPE = "Issue"` and used it in two
11
+ * places. That is correct on *this* project by accident: Basic's requirement
12
+ * type happens to be called `Issue`. On **Agile** the word means an
13
+ * *impediment* — a blocker, tracked off the backlog — and the requirement type
14
+ * is `User Story`. So every GitHub issue adopted onto an Agile board was filed
15
+ * as a blocker, silently, and the CLI shipped that way in 0.25.0.
16
+ *
17
+ * A type name is a property of the project's **process template**. It can never
18
+ * be written down here.
19
+ *
20
+ * ## How it is decided
21
+ *
22
+ * 1. `--type` wins outright. Somebody naming a type has looked at their own
23
+ * board, and this must not argue with them.
24
+ * 2. Otherwise the issue's **labels** choose a category — a saved mapping by
25
+ * label id first, then the name defaults for a repository nobody has
26
+ * configured. See `bridge.ts`.
27
+ * 3. The category is resolved against **this project's** own answer, from
28
+ * `wit/workitemtypecategories`. The lookups are ordered because they
29
+ * degrade: a feature request asks for `FeatureCategory` and settles for
30
+ * `RequirementCategory`, which is all Basic has.
31
+ * 4. If none of that resolves — an unreachable board, a project with no
32
+ * requirement category at all — it falls back to the old constant **and says
33
+ * so**, because refusing to adopt over a naming question would be worse than
34
+ * adopting into a type somebody can change.
35
+ *
36
+ * Every step is reported, because "this became a Bug because you mapped it" and
37
+ * "this became a Bug because nothing matched" are different facts and only one
38
+ * of them is a decision.
39
+ */
40
+ export interface ResolvedType {
41
+ /** What to create. */
42
+ type: string;
43
+ /** How it was arrived at, for the report. */
44
+ source: CategorySource | "flag" | "fallback";
45
+ /** The category that was asked for, when one was. */
46
+ category?: string;
47
+ /** The label that decided it, when one did. */
48
+ label?: string;
49
+ /** Set when the project could not answer and the constant was used. */
50
+ note?: string;
51
+ }
52
+ /** The last-resort name. Basic's requirement type, and the historical default. */
53
+ export declare const FALLBACK_WORK_ITEM_TYPE = "Issue";
54
+ export declare function resolveWorkItemType(session: Session, issue: Pick<GitHubIssue, "repo" | "labels">, options?: {
55
+ type?: string | undefined;
56
+ env?: NodeJS.ProcessEnv;
57
+ }): Promise<ResolvedType>;
58
+ //# sourceMappingURL=adopt-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adopt-type.d.ts","sourceRoot":"","sources":["../src/adopt-type.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,WAAW,YAAY;IAC3B,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,MAAM,EAAE,cAAc,GAAG,MAAM,GAAG,UAAU,CAAC;IAC7C,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,UAAU,CAAC;AAE/C,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,QAAQ,CAAC,EAC3C,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GACnE,OAAO,CAAC,YAAY,CAAC,CAkDvB"}
@@ -0,0 +1,61 @@
1
+ import { WorkItemTypesApi, typeForCategory } from "@floh-solutions/ado-core";
2
+ import { categoryFor, defaultBridgePath, parseBridgeConfig, repoConfig, } from "./bridge.js";
3
+ /** The last-resort name. Basic's requirement type, and the historical default. */
4
+ export const FALLBACK_WORK_ITEM_TYPE = "Issue";
5
+ export async function resolveWorkItemType(session, issue, options = {}) {
6
+ if (options.type !== undefined && options.type !== "") {
7
+ return { type: options.type, source: "flag" };
8
+ }
9
+ const env = options.env ?? process.env;
10
+ const organization = session.client.organization;
11
+ const project = session.client.project ?? "";
12
+ const config = await readBridge(env);
13
+ const repo = repoConfig(config, organization, project, issue.repo);
14
+ const choice = categoryFor(issue.labels.map((label) => ({ id: label.id ?? -1, name: label.name })), repo);
15
+ let catalog;
16
+ try {
17
+ catalog = await new WorkItemTypesApi(session.client.http).categories();
18
+ }
19
+ catch (error) {
20
+ // The board could not be asked. Adopting into the historical default is
21
+ // better than refusing, and naming the reason is what stops it reading as a
22
+ // deliberate choice.
23
+ return {
24
+ type: FALLBACK_WORK_ITEM_TYPE,
25
+ source: "fallback",
26
+ note: `the project's work item types could not be read (${error.message}), so the `
27
+ + `default "${FALLBACK_WORK_ITEM_TYPE}" was used — pass --type to be explicit`,
28
+ };
29
+ }
30
+ const resolved = typeForCategory(catalog, ...choice.categories);
31
+ if (resolved === undefined) {
32
+ return {
33
+ type: FALLBACK_WORK_ITEM_TYPE,
34
+ source: "fallback",
35
+ ...(choice.label === undefined ? {} : { label: choice.label }),
36
+ note: `this project has none of ${choice.categories.join(", ")}, so the default `
37
+ + `"${FALLBACK_WORK_ITEM_TYPE}" was used — pass --type to be explicit`,
38
+ };
39
+ }
40
+ return {
41
+ type: resolved,
42
+ source: choice.source,
43
+ ...(choice.categories[0] === undefined ? {} : { category: choice.categories[0] }),
44
+ ...(choice.label === undefined ? {} : { label: choice.label }),
45
+ };
46
+ }
47
+ /** The mapping, or an empty one. A missing or malformed file is not an error. */
48
+ async function readBridge(env) {
49
+ const path = defaultBridgePath(env);
50
+ try {
51
+ const { readFile } = await import("node:fs/promises");
52
+ return parseBridgeConfig(await readFile(path, "utf8"), path);
53
+ }
54
+ catch {
55
+ // No file yet is the ordinary state, and a malformed one must not take down
56
+ // `adopt` — it degrades to the name defaults, which is what an
57
+ // unconfigured repository gets anyway.
58
+ return { version: 1, scopes: {} };
59
+ }
60
+ }
61
+ //# sourceMappingURL=adopt-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adopt-type.js","sourceRoot":"","sources":["../src/adopt-type.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * `~/.config/pharos/bridge.json` — **what an issue becomes when somebody adopts
3
+ * it**, and the one file the CLI and the app both read (#861).
4
+ *
5
+ * ## Why it is a file rather than app state
6
+ *
7
+ * The same rule `pharos-convert` and `doctor` follow, and the one AGENTS.md
8
+ * keeps restating: if the app owns the mapping and the CLI guesses, then
9
+ * `pharos issue adopt` in a headless session files to a different work item type
10
+ * than the app does — and **the one that drifts is the one nobody is watching**.
11
+ * So the CLI owns the file, the CLI has verbs over it, and the app's settings
12
+ * page is a GUI over those verbs rather than a second implementation.
13
+ *
14
+ * ## Why it is keyed by scope on day one
15
+ *
16
+ * The UI only ever shows one organisation and project today. The key is there
17
+ * anyway because this is the single artefact multi-scope would otherwise force a
18
+ * rewrite of — see `docs/pharos-multi-scope-plan.md`. It costs one nesting level
19
+ * now and saves throwing the file away later.
20
+ *
21
+ * ## Why labels are keyed by ID
22
+ *
23
+ * **Probed on a live repository:** creating a label, renaming it and reading it
24
+ * back leaves `id` and `node_id` unchanged. So an id-keyed mapping survives a
25
+ * rename, which is the thing people actually do to labels.
26
+ *
27
+ * Two consequences worth stating rather than discovering:
28
+ *
29
+ * - **Ids are per repository.** `bug` is `11710323621` on one repo and
30
+ * `1593484620` on another. A mapping is therefore per repo by construction,
31
+ * not a global preference.
32
+ * - **Delete-and-recreate yields a NEW id**, so a mapping whose id no longer
33
+ * resolves goes visibly *broken* rather than silently re-binding to a
34
+ * different label that happens to share a name. That is the correct
35
+ * behaviour: the old label is gone, and what replaced it is somebody else's
36
+ * decision to confirm.
37
+ *
38
+ * `name` is stored beside the id for display and for diagnosis. It is never what
39
+ * a lookup matches on.
40
+ */
41
+ export interface BridgeConfig {
42
+ version: 1;
43
+ scopes: Record<string, ScopeConfig>;
44
+ }
45
+ export interface ScopeConfig {
46
+ repos: Record<string, RepoBridgeConfig>;
47
+ digest: DigestPolicy;
48
+ }
49
+ export interface RepoBridgeConfig {
50
+ /** Label **id** (as a string key) → what adopting an issue with it produces. */
51
+ labels: Record<string, LabelMapping>;
52
+ /** Where an issue with no mapped label goes. */
53
+ defaultCategory: string;
54
+ }
55
+ export interface LabelMapping {
56
+ /** Display only, and deliberately not what anything matches on. */
57
+ name: string;
58
+ /** An Azure DevOps work item type CATEGORY, never a type name. */
59
+ category: string;
60
+ }
61
+ /**
62
+ * When a digest is written back to the work item.
63
+ *
64
+ * **Two events, not every comment.** Adopt and close are decisions; a comment is
65
+ * chatter. Mirroring chatter puts noise on a client's board and re-opens the
66
+ * echo problem the hub already paid for — while never writing turns the work
67
+ * item into a dead link, which is worse. See the wave spec.
68
+ */
69
+ export interface DigestPolicy {
70
+ onAdopt: boolean;
71
+ onClose: boolean;
72
+ }
73
+ export declare const DEFAULT_DIGEST: DigestPolicy;
74
+ /** Azure DevOps' category reference names, as used here. */
75
+ export declare const CATEGORY: {
76
+ readonly bug: "Microsoft.BugCategory";
77
+ readonly requirement: "Microsoft.RequirementCategory";
78
+ readonly feature: "Microsoft.FeatureCategory";
79
+ readonly epic: "Microsoft.EpicCategory";
80
+ readonly task: "Microsoft.TaskCategory";
81
+ };
82
+ /**
83
+ * What a repository nobody has configured does.
84
+ *
85
+ * **Matched by NAME, and that is the one place a name is allowed to matter.** A
86
+ * repo you have never opened the settings for should still adopt sensibly, and
87
+ * the only thing available before somebody maps anything is what the labels are
88
+ * called. The moment a mapping is saved it pins to the id and renames stop
89
+ * mattering — *id for bindings you made, name for defaults you did not*.
90
+ *
91
+ * Ordered, because the lookups degrade: a feature request wants
92
+ * `FeatureCategory` and settles for `RequirementCategory`, which is all Basic
93
+ * has.
94
+ */
95
+ export declare const NAME_DEFAULTS: readonly {
96
+ readonly match: readonly string[];
97
+ readonly categories: readonly string[];
98
+ }[];
99
+ /** Where the file lives. `PHAROS_BRIDGE_FILE` overrides it, for tests. */
100
+ export declare function defaultBridgePath(env?: NodeJS.ProcessEnv): string;
101
+ /** `org/project`, lowercased — the scope key. */
102
+ export declare function scopeKey(org: string, project: string): string;
103
+ export declare function emptyBridgeConfig(): BridgeConfig;
104
+ /**
105
+ * Read a config, or refuse with a message naming the file.
106
+ *
107
+ * **A file it cannot parse is an answer, not a crash** — the same rule
108
+ * `doctor`'s repos.json handling follows. A malformed mapping means adoption
109
+ * falls back to the name defaults and says so, rather than taking the CLI down
110
+ * on an unrelated verb.
111
+ */
112
+ export declare function parseBridgeConfig(text: string, source?: string): BridgeConfig;
113
+ export declare function formatBridgeConfig(config: BridgeConfig): string;
114
+ /** This repository's mapping in this scope, or the empty one. */
115
+ export declare function repoConfig(config: BridgeConfig, org: string, project: string, repo: string): RepoBridgeConfig;
116
+ export declare function digestPolicy(config: BridgeConfig, org: string, project: string): DigestPolicy;
117
+ /** One label as GitHub reports it. */
118
+ export interface IssueLabel {
119
+ id: number;
120
+ name: string;
121
+ }
122
+ /** How a category was arrived at — reported, because a silent default misleads. */
123
+ export type CategorySource = "mapping" | "name-default" | "repo-default";
124
+ export interface CategoryChoice {
125
+ /** Ordered: try each in turn, because a project may not have the first. */
126
+ categories: readonly string[];
127
+ source: CategorySource;
128
+ /** Which label decided it, when one did. */
129
+ label?: string;
130
+ }
131
+ /**
132
+ * Which category an issue's labels ask for.
133
+ *
134
+ * **Saved mappings win, by id.** Then the name defaults, for a repository nobody
135
+ * has configured. Then the repository's own default.
136
+ *
137
+ * The `source` comes back with it on purpose: "this went to a requirement
138
+ * because you mapped `bug`" and "this went to a requirement because nothing
139
+ * matched" are different facts, and a picker that showed the same preselection
140
+ * for both would be hiding the second one.
141
+ */
142
+ export declare function categoryFor(labels: readonly IssueLabel[], repo: RepoBridgeConfig): CategoryChoice;
143
+ /**
144
+ * A saved mapping whose label no longer exists in the repository.
145
+ *
146
+ * Reported rather than pruned. The mapping is a decision somebody made, and a
147
+ * label that has gone is usually a rename they did not realise breaks it — or a
148
+ * delete-and-recreate, which is a genuinely different label. Either way the
149
+ * answer is to show it, not to guess.
150
+ */
151
+ export declare function brokenMappings(repo: RepoBridgeConfig, live: readonly IssueLabel[]): {
152
+ id: string;
153
+ name: string;
154
+ category: string;
155
+ }[];
156
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACxC,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACrC,gDAAgD;IAChD,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,cAAc,EAAE,YAA+C,CAAC;AAE7E,4DAA4D;AAC5D,eAAO,MAAM,QAAQ;;;;;;CAMX,CAAC;AAEX;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAMjH,CAAC;AAEF,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAK9E;AAED,iDAAiD;AACjD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,iBAAiB,IAAI,YAAY,CAEhD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAgB,GAAG,YAAY,CA+CpF;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAE/D;AAED,iEAAiE;AACjE,wBAAgB,UAAU,CACxB,MAAM,EAAE,YAAY,EACpB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,gBAAgB,CAQlB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CAE7F;AAED,sCAAsC;AACtC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,mFAAmF;AACnF,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,cAAc,GAAG,cAAc,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,MAAM,EAAE,cAAc,CAAC;IACvB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,IAAI,EAAE,gBAAgB,GACrB,cAAc,CAiBhB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,SAAS,UAAU,EAAE,GAC1B;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAAE,CAKlD"}
package/dist/bridge.js ADDED
@@ -0,0 +1,157 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ export const DEFAULT_DIGEST = { onAdopt: true, onClose: true };
4
+ /** Azure DevOps' category reference names, as used here. */
5
+ export const CATEGORY = {
6
+ bug: "Microsoft.BugCategory",
7
+ requirement: "Microsoft.RequirementCategory",
8
+ feature: "Microsoft.FeatureCategory",
9
+ epic: "Microsoft.EpicCategory",
10
+ task: "Microsoft.TaskCategory",
11
+ };
12
+ /**
13
+ * What a repository nobody has configured does.
14
+ *
15
+ * **Matched by NAME, and that is the one place a name is allowed to matter.** A
16
+ * repo you have never opened the settings for should still adopt sensibly, and
17
+ * the only thing available before somebody maps anything is what the labels are
18
+ * called. The moment a mapping is saved it pins to the id and renames stop
19
+ * mattering — *id for bindings you made, name for defaults you did not*.
20
+ *
21
+ * Ordered, because the lookups degrade: a feature request wants
22
+ * `FeatureCategory` and settles for `RequirementCategory`, which is all Basic
23
+ * has.
24
+ */
25
+ export const NAME_DEFAULTS = [
26
+ { match: ["bug", "defect", "regression"], categories: [CATEGORY.bug] },
27
+ {
28
+ match: ["enhancement", "feature", "feature request"],
29
+ categories: [CATEGORY.feature, CATEGORY.requirement],
30
+ },
31
+ ];
32
+ /** Where the file lives. `PHAROS_BRIDGE_FILE` overrides it, for tests. */
33
+ export function defaultBridgePath(env = process.env) {
34
+ const override = env.PHAROS_BRIDGE_FILE;
35
+ if (override !== undefined && override !== "")
36
+ return override;
37
+ const base = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
38
+ return join(base, "pharos", "bridge.json");
39
+ }
40
+ /** `org/project`, lowercased — the scope key. */
41
+ export function scopeKey(org, project) {
42
+ return `${org.trim().toLowerCase()}/${project.trim().toLowerCase()}`;
43
+ }
44
+ export function emptyBridgeConfig() {
45
+ return { version: 1, scopes: {} };
46
+ }
47
+ /**
48
+ * Read a config, or refuse with a message naming the file.
49
+ *
50
+ * **A file it cannot parse is an answer, not a crash** — the same rule
51
+ * `doctor`'s repos.json handling follows. A malformed mapping means adoption
52
+ * falls back to the name defaults and says so, rather than taking the CLI down
53
+ * on an unrelated verb.
54
+ */
55
+ export function parseBridgeConfig(text, source = "bridge.json") {
56
+ let raw;
57
+ try {
58
+ raw = JSON.parse(text);
59
+ }
60
+ catch (error) {
61
+ throw new Error(`${source} is not valid JSON: ${error.message}`);
62
+ }
63
+ if (typeof raw !== "object" || raw === null)
64
+ throw new Error(`${source} is not an object.`);
65
+ const record = raw;
66
+ if (record.version !== 1) {
67
+ throw new Error(`${source} has version ${String(record.version)}; this build understands 1.`);
68
+ }
69
+ const scopes = {};
70
+ const rawScopes = (record.scopes ?? {});
71
+ for (const [key, value] of Object.entries(rawScopes)) {
72
+ const scope = (value ?? {});
73
+ const repos = {};
74
+ for (const [repo, rawRepo] of Object.entries((scope.repos ?? {}))) {
75
+ const entry = (rawRepo ?? {});
76
+ const labels = {};
77
+ for (const [id, rawLabel] of Object.entries((entry.labels ?? {}))) {
78
+ const label = (rawLabel ?? {});
79
+ if (typeof label.category !== "string")
80
+ continue;
81
+ labels[id] = {
82
+ name: typeof label.name === "string" ? label.name : "",
83
+ category: label.category,
84
+ };
85
+ }
86
+ repos[repo.toLowerCase()] = {
87
+ labels,
88
+ defaultCategory: typeof entry.defaultCategory === "string" ? entry.defaultCategory : CATEGORY.requirement,
89
+ };
90
+ }
91
+ const digest = (scope.digest ?? {});
92
+ scopes[key.toLowerCase()] = {
93
+ repos,
94
+ digest: {
95
+ onAdopt: digest.onAdopt !== false,
96
+ onClose: digest.onClose !== false,
97
+ },
98
+ };
99
+ }
100
+ return { version: 1, scopes };
101
+ }
102
+ export function formatBridgeConfig(config) {
103
+ return `${JSON.stringify(config, null, 2)}\n`;
104
+ }
105
+ /** This repository's mapping in this scope, or the empty one. */
106
+ export function repoConfig(config, org, project, repo) {
107
+ const scope = config.scopes[scopeKey(org, project)];
108
+ return (scope?.repos[repo.trim().toLowerCase()] ?? {
109
+ labels: {},
110
+ defaultCategory: CATEGORY.requirement,
111
+ });
112
+ }
113
+ export function digestPolicy(config, org, project) {
114
+ return config.scopes[scopeKey(org, project)]?.digest ?? DEFAULT_DIGEST;
115
+ }
116
+ /**
117
+ * Which category an issue's labels ask for.
118
+ *
119
+ * **Saved mappings win, by id.** Then the name defaults, for a repository nobody
120
+ * has configured. Then the repository's own default.
121
+ *
122
+ * The `source` comes back with it on purpose: "this went to a requirement
123
+ * because you mapped `bug`" and "this went to a requirement because nothing
124
+ * matched" are different facts, and a picker that showed the same preselection
125
+ * for both would be hiding the second one.
126
+ */
127
+ export function categoryFor(labels, repo) {
128
+ for (const label of labels) {
129
+ const mapped = repo.labels[String(label.id)];
130
+ if (mapped !== undefined) {
131
+ return { categories: [mapped.category], source: "mapping", label: label.name };
132
+ }
133
+ }
134
+ for (const label of labels) {
135
+ const lowered = label.name.trim().toLowerCase();
136
+ const rule = NAME_DEFAULTS.find((candidate) => candidate.match.includes(lowered));
137
+ if (rule !== undefined) {
138
+ return { categories: rule.categories, source: "name-default", label: label.name };
139
+ }
140
+ }
141
+ return { categories: [repo.defaultCategory], source: "repo-default" };
142
+ }
143
+ /**
144
+ * A saved mapping whose label no longer exists in the repository.
145
+ *
146
+ * Reported rather than pruned. The mapping is a decision somebody made, and a
147
+ * label that has gone is usually a rename they did not realise breaks it — or a
148
+ * delete-and-recreate, which is a genuinely different label. Either way the
149
+ * answer is to show it, not to guess.
150
+ */
151
+ export function brokenMappings(repo, live) {
152
+ const known = new Set(live.map((label) => String(label.id)));
153
+ return Object.entries(repo.labels)
154
+ .filter(([id]) => !known.has(id))
155
+ .map(([id, mapping]) => ({ id, name: mapping.name, category: mapping.category }));
156
+ }
157
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts CHANGED
@@ -12,7 +12,7 @@ import { type ExitCode } from "./output.js";
12
12
  * expensive kind of wrong. Anything added to the switch belongs here, and the
13
13
  * test below fails if it does not.
14
14
  */
15
- export declare const COMMANDS: readonly ["setup", "whoami", "query", "task", "types", "history", "iterations", "areas", "links", "fields", "create", "update", "link", "unlink", "delete", "restore", "deleted", "attach", "detach", "download", "image", "people", "wiki", "comment", "hooks", "plan", "doctor", "issue"];
15
+ export declare const COMMANDS: readonly ["setup", "whoami", "query", "task", "types", "history", "iterations", "areas", "links", "fields", "create", "update", "link", "unlink", "delete", "restore", "deleted", "attach", "detach", "download", "image", "people", "wiki", "comment", "hooks", "plan", "doctor", "issue", "bridge"];
16
16
  export interface RunOptions {
17
17
  argv: string[];
18
18
  cwd: string;
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AA8B5D,OAAO,EAKL,KAAK,QAAQ,EAEd,MAAM,aAAa,CAAC;AAyBrB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,6RAMX,CAAC;AA8gBX,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,gDAAgD;IAChD,SAAS,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAChD,+EAA+E;IAC/E,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,uDAAuD;IACvD,MAAM,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;CACnC;AAED,wBAAsB,GAAG,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAOhE"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AA+B5D,OAAO,EAKL,KAAK,QAAQ,EAEd,MAAM,aAAa,CAAC;AAyBrB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,uSAMX,CAAC;AA8gBX,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,gDAAgD;IAChD,SAAS,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAChD,+EAA+E;IAC/E,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,uDAAuD;IACvD,MAAM,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;CACnC;AAED,wBAAsB,GAAG,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAOhE"}
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { runClassification, runFields, runHistory, runLinkTypes, } from "./comma
9
9
  import { runComment } from "./commands/comment.js";
10
10
  import { runCreate } from "./commands/create.js";
11
11
  import { runDetach } from "./commands/detach.js";
12
+ import { runBridge } from "./commands/bridge.js";
12
13
  import { runDoctor } from "./commands/doctor.js";
13
14
  import { runDownload, runTypes, runWhoami } from "./commands/discover.js";
14
15
  import { runImage, runWikiImage } from "./commands/image.js";
@@ -61,7 +62,7 @@ export const COMMANDS = [
61
62
  "history", "iterations", "areas", "links", "fields",
62
63
  "create", "update", "link", "unlink", "delete", "restore", "deleted",
63
64
  "attach", "detach", "download", "image", "people",
64
- "wiki", "comment", "hooks", "plan", "doctor", "issue",
65
+ "wiki", "comment", "hooks", "plan", "doctor", "issue", "bridge",
65
66
  ];
66
67
  /** Where long text comes from, for the verbs that take some. */
67
68
  const CONTENT_OPTIONS = {
@@ -735,31 +736,44 @@ async function dispatch(io, options) {
735
736
  "if-body": { type: "string" },
736
737
  "if-body-file": { type: "string" },
737
738
  }
738
- : command === "deleted"
739
- ? { top: { type: "string" } }
740
- : command === "history"
741
- ? { top: { type: "string" }, all: { type: "boolean" } }
742
- : command === "iterations" || command === "areas"
743
- ? { depth: { type: "string" } }
744
- : command === "fields"
745
- ? { type: { type: "string" }, constrained: { type: "boolean" } }
746
- : command === "hooks"
747
- ? {
748
- hub: { type: "string" },
749
- user: { type: "string" },
750
- password: { type: "string" },
751
- }
752
- : command === "setup"
739
+ : command === "bridge"
740
+ ? {
741
+ repo: { type: "string" },
742
+ // Singular and NOT repeatable, unlike `issue`'s `--label`: this one
743
+ // names exactly one mapping to write or remove.
744
+ label: { type: "string" },
745
+ category: { type: "string" },
746
+ "on-adopt": { type: "boolean" },
747
+ "on-close": { type: "boolean" },
748
+ // The only way to say "no". A boolean flag cannot express false, so
749
+ // without this the digest policy was write-only.
750
+ manual: { type: "boolean" },
751
+ }
752
+ : command === "deleted"
753
+ ? { top: { type: "string" } }
754
+ : command === "history"
755
+ ? { top: { type: "string" }, all: { type: "boolean" } }
756
+ : command === "iterations" || command === "areas"
757
+ ? { depth: { type: "string" } }
758
+ : command === "fields"
759
+ ? { type: { type: "string" }, constrained: { type: "boolean" } }
760
+ : command === "hooks"
753
761
  ? {
754
- print: { type: "boolean" },
755
- stdin: { type: "boolean" },
756
- skill: { type: "boolean" },
757
- convert: { type: "boolean" },
758
- install: { type: "string" },
759
- repo: { type: "string" },
760
- "gh-account": { type: "string" },
762
+ hub: { type: "string" },
763
+ user: { type: "string" },
764
+ password: { type: "string" },
761
765
  }
762
- : CONTENT_OPTIONS;
766
+ : command === "setup"
767
+ ? {
768
+ print: { type: "boolean" },
769
+ stdin: { type: "boolean" },
770
+ skill: { type: "boolean" },
771
+ convert: { type: "boolean" },
772
+ install: { type: "string" },
773
+ repo: { type: "string" },
774
+ "gh-account": { type: "string" },
775
+ }
776
+ : CONTENT_OPTIONS;
763
777
  const { values, positionals } = parse(options.argv, { ...GLOBAL_OPTIONS, ...extras });
764
778
  if (values["version"] === true)
765
779
  return emitText(io, await version(options.cwd));
@@ -956,6 +970,15 @@ async function dispatch(io, options) {
956
970
  return runComment(io, session, rest, {
957
971
  text: await content(options, values),
958
972
  });
973
+ case "bridge":
974
+ return runBridge(io, session, rest, {
975
+ repo: asString(values["repo"]),
976
+ label: asString(values["label"]),
977
+ category: asString(values["category"]),
978
+ onAdopt: values["on-adopt"] === undefined ? undefined : values["on-adopt"] === true,
979
+ onClose: values["on-close"] === undefined ? undefined : values["on-close"] === true,
980
+ manual: values["manual"] === true ? true : undefined,
981
+ });
959
982
  case "issue":
960
983
  return runIssue(io, session, rest, {
961
984
  type: asString(values["type"]),