felenova-backlog 0.1.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 (147) hide show
  1. package/README.md +58 -0
  2. package/bin/backlog.js +2 -0
  3. package/dist/_core/criteria.d.ts +13 -0
  4. package/dist/_core/criteria.js +44 -0
  5. package/dist/_core/criteria.js.map +1 -0
  6. package/dist/_core/errors.d.ts +53 -0
  7. package/dist/_core/errors.js +91 -0
  8. package/dist/_core/errors.js.map +1 -0
  9. package/dist/_core/index.d.ts +24 -0
  10. package/dist/_core/index.js +27 -0
  11. package/dist/_core/index.js.map +1 -0
  12. package/dist/_core/loop.d.ts +73 -0
  13. package/dist/_core/loop.js +46 -0
  14. package/dist/_core/loop.js.map +1 -0
  15. package/dist/_core/markdown/items.d.ts +35 -0
  16. package/dist/_core/markdown/items.js +256 -0
  17. package/dist/_core/markdown/items.js.map +1 -0
  18. package/dist/_core/markdown/matrix.d.ts +20 -0
  19. package/dist/_core/markdown/matrix.js +147 -0
  20. package/dist/_core/markdown/matrix.js.map +1 -0
  21. package/dist/_core/markdown/track.d.ts +71 -0
  22. package/dist/_core/markdown/track.js +79 -0
  23. package/dist/_core/markdown/track.js.map +1 -0
  24. package/dist/_core/matrix-status.d.ts +9 -0
  25. package/dist/_core/matrix-status.js +31 -0
  26. package/dist/_core/matrix-status.js.map +1 -0
  27. package/dist/_core/matrix.d.ts +46 -0
  28. package/dist/_core/matrix.js +0 -0
  29. package/dist/_core/matrix.js.map +1 -0
  30. package/dist/_core/permissions.d.ts +14 -0
  31. package/dist/_core/permissions.js +87 -0
  32. package/dist/_core/permissions.js.map +1 -0
  33. package/dist/_core/promote.d.ts +20 -0
  34. package/dist/_core/promote.js +47 -0
  35. package/dist/_core/promote.js.map +1 -0
  36. package/dist/_core/ready.d.ts +8 -0
  37. package/dist/_core/ready.js +22 -0
  38. package/dist/_core/ready.js.map +1 -0
  39. package/dist/_core/review.d.ts +49 -0
  40. package/dist/_core/review.js +137 -0
  41. package/dist/_core/review.js.map +1 -0
  42. package/dist/_core/schema.d.ts +636 -0
  43. package/dist/_core/schema.js +304 -0
  44. package/dist/_core/schema.js.map +1 -0
  45. package/dist/_core/ship.d.ts +23 -0
  46. package/dist/_core/ship.js +31 -0
  47. package/dist/_core/ship.js.map +1 -0
  48. package/dist/_core/spec-diff.d.ts +19 -0
  49. package/dist/_core/spec-diff.js +25 -0
  50. package/dist/_core/spec-diff.js.map +1 -0
  51. package/dist/_core/status.d.ts +7 -0
  52. package/dist/_core/status.js +25 -0
  53. package/dist/_core/status.js.map +1 -0
  54. package/dist/_core/util/deep-equal.d.ts +8 -0
  55. package/dist/_core/util/deep-equal.js +31 -0
  56. package/dist/_core/util/deep-equal.js.map +1 -0
  57. package/dist/api.d.ts +46 -0
  58. package/dist/api.js +104 -0
  59. package/dist/api.js.map +1 -0
  60. package/dist/cache.d.ts +53 -0
  61. package/dist/cache.js +114 -0
  62. package/dist/cache.js.map +1 -0
  63. package/dist/commands/common.d.ts +59 -0
  64. package/dist/commands/common.js +94 -0
  65. package/dist/commands/common.js.map +1 -0
  66. package/dist/commands/export.d.ts +20 -0
  67. package/dist/commands/export.js +56 -0
  68. package/dist/commands/export.js.map +1 -0
  69. package/dist/commands/matrix.d.ts +16 -0
  70. package/dist/commands/matrix.js +48 -0
  71. package/dist/commands/matrix.js.map +1 -0
  72. package/dist/commands/next.d.ts +14 -0
  73. package/dist/commands/next.js +30 -0
  74. package/dist/commands/next.js.map +1 -0
  75. package/dist/commands/promote.d.ts +15 -0
  76. package/dist/commands/promote.js +172 -0
  77. package/dist/commands/promote.js.map +1 -0
  78. package/dist/commands/report.d.ts +18 -0
  79. package/dist/commands/report.js +45 -0
  80. package/dist/commands/report.js.map +1 -0
  81. package/dist/commands/ship.d.ts +15 -0
  82. package/dist/commands/ship.js +231 -0
  83. package/dist/commands/ship.js.map +1 -0
  84. package/dist/commands/status.d.ts +13 -0
  85. package/dist/commands/status.js +141 -0
  86. package/dist/commands/status.js.map +1 -0
  87. package/dist/commands/sync.d.ts +17 -0
  88. package/dist/commands/sync.js +90 -0
  89. package/dist/commands/sync.js.map +1 -0
  90. package/dist/conductor/registry.d.ts +45 -0
  91. package/dist/conductor/registry.js +164 -0
  92. package/dist/conductor/registry.js.map +1 -0
  93. package/dist/config.d.ts +47 -0
  94. package/dist/config.js +137 -0
  95. package/dist/config.js.map +1 -0
  96. package/dist/doctor.d.ts +28 -0
  97. package/dist/doctor.js +377 -0
  98. package/dist/doctor.js.map +1 -0
  99. package/dist/errors.d.ts +23 -0
  100. package/dist/errors.js +114 -0
  101. package/dist/errors.js.map +1 -0
  102. package/dist/export.d.ts +1 -0
  103. package/dist/export.js +9 -0
  104. package/dist/export.js.map +1 -0
  105. package/dist/import.d.ts +3 -0
  106. package/dist/import.js +89 -0
  107. package/dist/import.js.map +1 -0
  108. package/dist/index.d.ts +1 -0
  109. package/dist/index.js +183 -0
  110. package/dist/index.js.map +1 -0
  111. package/dist/init/auth.d.ts +85 -0
  112. package/dist/init/auth.js +194 -0
  113. package/dist/init/auth.js.map +1 -0
  114. package/dist/init/claudemd.d.ts +16 -0
  115. package/dist/init/claudemd.js +62 -0
  116. package/dist/init/claudemd.js.map +1 -0
  117. package/dist/init/index.d.ts +47 -0
  118. package/dist/init/index.js +159 -0
  119. package/dist/init/index.js.map +1 -0
  120. package/dist/init/mcp.d.ts +18 -0
  121. package/dist/init/mcp.js +97 -0
  122. package/dist/init/mcp.js.map +1 -0
  123. package/dist/init/plugin.d.ts +60 -0
  124. package/dist/init/plugin.js +120 -0
  125. package/dist/init/plugin.js.map +1 -0
  126. package/dist/init/scaffold.d.ts +19 -0
  127. package/dist/init/scaffold.js +46 -0
  128. package/dist/init/scaffold.js.map +1 -0
  129. package/dist/init/templates.d.ts +48 -0
  130. package/dist/init/templates.js +147 -0
  131. package/dist/init/templates.js.map +1 -0
  132. package/dist/plugin/.claude-plugin/plugin.json +11 -0
  133. package/dist/plugin/README.md +117 -0
  134. package/dist/plugin/commands/next.md +28 -0
  135. package/dist/plugin/commands/promote.md +30 -0
  136. package/dist/plugin/commands/report.md +26 -0
  137. package/dist/plugin/commands/ship.md +30 -0
  138. package/dist/plugin/skills/backlog-management/SKILL.md +63 -0
  139. package/dist/plugin/skills/backlog-query/SKILL.md +57 -0
  140. package/dist/plugin/skills/feature-kickoff/SKILL.md +112 -0
  141. package/dist/snapshot.d.ts +24 -0
  142. package/dist/snapshot.js +2 -0
  143. package/dist/snapshot.js.map +1 -0
  144. package/dist/token.d.ts +29 -0
  145. package/dist/token.js +99 -0
  146. package/dist/token.js.map +1 -0
  147. package/package.json +52 -0
package/dist/api.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The single HTTP call site in the CLI (DECISION-025). The CLI speaks REST `/v1` with a PAT:
3
+ * one call per command, the same `{ error, message }` refusal contract as `/mcp`, and no
4
+ * retries — every write in the register is non-idempotent.
5
+ *
6
+ * `src/no-stray-fetch.test.ts` fails the build if any other module calls `fetch`.
7
+ */
8
+ /** A refusal the CLI can render. `status: 0` means the request never reached the service. */
9
+ export class ApiError extends Error {
10
+ status;
11
+ code;
12
+ constructor(status, code, message) {
13
+ super(message);
14
+ this.name = 'ApiError';
15
+ this.status = status;
16
+ this.code = code;
17
+ }
18
+ }
19
+ const DEFAULT_TIMEOUT_MS = 15_000;
20
+ function joinUrl(base, path) {
21
+ const root = base.replace(/\/+$/, '');
22
+ return path.startsWith('/') ? `${root}${path}` : `${root}/${path}`;
23
+ }
24
+ /**
25
+ * One request. A 2xx JSON body is parsed and returned (`204` and an empty body are `null`);
26
+ * anything else throws `ApiError` built from the service's `{ error, message }`, and a body
27
+ * that is not that shape is `internal`. A transport failure is `status: 0`/`unreachable`.
28
+ */
29
+ export async function request(cfg, token, method, path, body) {
30
+ if (!token) {
31
+ throw new ApiError(0, 'token_required', 'no PAT available — set BACKLOG_TOKEN or run: backlog init');
32
+ }
33
+ const target = joinUrl(cfg.url, path);
34
+ const headers = {
35
+ Authorization: `Bearer ${token}`,
36
+ accept: 'application/json',
37
+ 'X-Backlog-Client': 'cli',
38
+ 'X-Backlog-Actor': 'human',
39
+ };
40
+ const init = { method, headers };
41
+ if (body !== undefined) {
42
+ headers['content-type'] = 'application/json';
43
+ init.body = JSON.stringify(body);
44
+ }
45
+ init.signal = AbortSignal.timeout(cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS);
46
+ const fetchImpl = cfg.fetchImpl ?? fetch;
47
+ let response;
48
+ try {
49
+ response = await fetchImpl(target, init);
50
+ }
51
+ catch {
52
+ // Deliberately swallows the cause: a transport error can carry the request, and the
53
+ // request carries the Authorization header.
54
+ throw new ApiError(0, 'unreachable', cfg.url);
55
+ }
56
+ const text = await response.text().catch(() => '');
57
+ if (!response.ok) {
58
+ let code = 'internal';
59
+ let message = '';
60
+ try {
61
+ const parsed = JSON.parse(text);
62
+ if (typeof parsed === 'object' && parsed !== null) {
63
+ const record = parsed;
64
+ if (typeof record.error === 'string' && record.error !== '')
65
+ code = record.error;
66
+ if (typeof record.message === 'string')
67
+ message = record.message;
68
+ }
69
+ }
70
+ catch {
71
+ // a non-JSON body (a proxy's HTML 502, say) is `internal`
72
+ }
73
+ throw new ApiError(response.status, code, message);
74
+ }
75
+ if (response.status === 204 || text.trim() === '')
76
+ return null;
77
+ try {
78
+ return JSON.parse(text);
79
+ }
80
+ catch {
81
+ throw new ApiError(response.status, 'internal', 'the service returned a malformed response');
82
+ }
83
+ }
84
+ export const get = (cfg, token, path) => request(cfg, token, 'GET', path);
85
+ export const post = (cfg, token, path, body) => request(cfg, token, 'POST', path, body ?? {});
86
+ export const patch = (cfg, token, path, body) => request(cfg, token, 'PATCH', path, body ?? {});
87
+ export const del = (cfg, token, path) => request(cfg, token, 'DELETE', path);
88
+ /** A config + token bound once, so a command never handles the token itself. */
89
+ export function createClient(opts) {
90
+ const cfg = {
91
+ url: opts.url,
92
+ project: opts.project,
93
+ fetchImpl: opts.fetchImpl,
94
+ timeoutMs: opts.timeoutMs,
95
+ };
96
+ return {
97
+ url: opts.url,
98
+ get: (path) => get(cfg, opts.token, path),
99
+ post: (path, body) => post(cfg, opts.token, path, body),
100
+ patch: (path, body) => patch(cfg, opts.token, path, body),
101
+ del: (path) => del(cfg, opts.token, path),
102
+ };
103
+ }
104
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The offline half of `backlog status` (PRODUCT-SPEC §12). Nothing here is authoritative:
3
+ * the cache is a rendering of one successful read, kept so a plane, a train or a dead Fly
4
+ * machine still leaves `status` with something to print. It is never consulted for a write,
5
+ * never merged, and never read by any other command.
6
+ *
7
+ * It is gitignored (`init` adds `.backlog/`, and `status` adds it too when the repo has a
8
+ * `.gitignore`), and it holds **no token** — only what `/v1` already answered.
9
+ */
10
+ export declare const CACHE_DIR = ".backlog";
11
+ export declare const CACHE_FILE = ".backlog/cache.json";
12
+ /** What `status` renders, plus the two fields that say which view this is. */
13
+ export interface StatusCachePayload {
14
+ project: string;
15
+ url: string;
16
+ role: string | null;
17
+ tracks: unknown[];
18
+ items: unknown[];
19
+ }
20
+ export interface StatusCache extends StatusCachePayload {
21
+ /** When the service last answered — printed verbatim in the `cached from …` line. */
22
+ fetched_at: string;
23
+ }
24
+ export declare function cachePath(root: string): string;
25
+ /**
26
+ * Writes the cache (mode 644 in a 755 directory) and returns its path.
27
+ *
28
+ * Write-then-rename, not write-in-place: the moment this function is *most* likely to run
29
+ * is the moment the service is flaky, and a process killed halfway through a truncating
30
+ * write would leave `status --offline` with a half-JSON file — which `readStatusCache`
31
+ * correctly treats as no cache at all, destroying a perfectly good earlier view. The bytes
32
+ * therefore land in a sibling temp file that nobody reads, and `rename(2)` — atomic within
33
+ * a directory — swaps it in. A kill before the rename leaves the previous cache intact.
34
+ *
35
+ * The five payload fields are copied one by one rather than spread: the cache is a
36
+ * committed-adjacent file, and an extra field appearing on a caller's object (a credential
37
+ * among them) must not be able to ride along into it.
38
+ */
39
+ export declare function writeStatusCache(root: string, payload: StatusCachePayload, now?: Date): string;
40
+ /**
41
+ * Reads the cache for exactly this `(url, project)`. Anything else — absent, unreadable,
42
+ * malformed, or written against another service or project — is `null`: a stale view of a
43
+ * different register is worse than no view at all.
44
+ */
45
+ export declare function readStatusCache(root: string, target: {
46
+ url: string;
47
+ project: string;
48
+ }): StatusCache | null;
49
+ /**
50
+ * Adds `.backlog/` to an existing `.gitignore`. Returns whether it wrote. A repo with no
51
+ * `.gitignore` has made a choice, so this never creates one — the same rule `init` follows.
52
+ */
53
+ export declare function gitignoreCache(root: string): boolean;
package/dist/cache.js ADDED
@@ -0,0 +1,114 @@
1
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * The offline half of `backlog status` (PRODUCT-SPEC §12). Nothing here is authoritative:
5
+ * the cache is a rendering of one successful read, kept so a plane, a train or a dead Fly
6
+ * machine still leaves `status` with something to print. It is never consulted for a write,
7
+ * never merged, and never read by any other command.
8
+ *
9
+ * It is gitignored (`init` adds `.backlog/`, and `status` adds it too when the repo has a
10
+ * `.gitignore`), and it holds **no token** — only what `/v1` already answered.
11
+ */
12
+ export const CACHE_DIR = '.backlog';
13
+ export const CACHE_FILE = `${CACHE_DIR}/cache.json`;
14
+ const GITIGNORE_ENTRY = `${CACHE_DIR}/`;
15
+ export function cachePath(root) {
16
+ return join(root, CACHE_FILE);
17
+ }
18
+ /**
19
+ * Writes the cache (mode 644 in a 755 directory) and returns its path.
20
+ *
21
+ * Write-then-rename, not write-in-place: the moment this function is *most* likely to run
22
+ * is the moment the service is flaky, and a process killed halfway through a truncating
23
+ * write would leave `status --offline` with a half-JSON file — which `readStatusCache`
24
+ * correctly treats as no cache at all, destroying a perfectly good earlier view. The bytes
25
+ * therefore land in a sibling temp file that nobody reads, and `rename(2)` — atomic within
26
+ * a directory — swaps it in. A kill before the rename leaves the previous cache intact.
27
+ *
28
+ * The five payload fields are copied one by one rather than spread: the cache is a
29
+ * committed-adjacent file, and an extra field appearing on a caller's object (a credential
30
+ * among them) must not be able to ride along into it.
31
+ */
32
+ export function writeStatusCache(root, payload, now = new Date()) {
33
+ const dir = join(root, CACHE_DIR);
34
+ mkdirSync(dir, { recursive: true, mode: 0o755 });
35
+ const path = cachePath(root);
36
+ const body = {
37
+ fetched_at: now.toISOString(),
38
+ project: payload.project,
39
+ url: payload.url,
40
+ role: payload.role,
41
+ tracks: payload.tracks,
42
+ items: payload.items,
43
+ };
44
+ const tmp = `${path}.tmp-${process.pid.toString(36)}${Date.now().toString(36)}`;
45
+ try {
46
+ writeFileSync(tmp, `${JSON.stringify(body, null, 2)}\n`, { mode: 0o644 });
47
+ // A permissive umask widens a create mode, and this file is committed-adjacent: it must
48
+ // be readable, and it must not be writable by anyone else.
49
+ if (process.platform !== 'win32')
50
+ chmodSync(tmp, 0o644);
51
+ renameSync(tmp, path);
52
+ }
53
+ catch (err) {
54
+ rmSync(tmp, { force: true });
55
+ throw err;
56
+ }
57
+ if (process.platform !== 'win32') {
58
+ chmodSync(dir, 0o755);
59
+ chmodSync(path, 0o644);
60
+ }
61
+ return path;
62
+ }
63
+ /**
64
+ * Reads the cache for exactly this `(url, project)`. Anything else — absent, unreadable,
65
+ * malformed, or written against another service or project — is `null`: a stale view of a
66
+ * different register is worse than no view at all.
67
+ */
68
+ export function readStatusCache(root, target) {
69
+ let raw;
70
+ try {
71
+ raw = JSON.parse(readFileSync(cachePath(root), 'utf8'));
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
77
+ return null;
78
+ const doc = raw;
79
+ if (typeof doc.fetched_at !== 'string' || doc.fetched_at === '')
80
+ return null;
81
+ if (doc.project !== target.project)
82
+ return null;
83
+ if (doc.url !== target.url)
84
+ return null;
85
+ return {
86
+ fetched_at: doc.fetched_at,
87
+ project: doc.project,
88
+ url: doc.url,
89
+ role: typeof doc.role === 'string' ? doc.role : null,
90
+ tracks: Array.isArray(doc.tracks) ? doc.tracks : [],
91
+ items: Array.isArray(doc.items) ? doc.items : [],
92
+ };
93
+ }
94
+ /**
95
+ * Adds `.backlog/` to an existing `.gitignore`. Returns whether it wrote. A repo with no
96
+ * `.gitignore` has made a choice, so this never creates one — the same rule `init` follows.
97
+ */
98
+ export function gitignoreCache(root) {
99
+ const path = join(root, '.gitignore');
100
+ let text;
101
+ try {
102
+ text = readFileSync(path, 'utf8');
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ const present = new Set(text.split('\n').map((line) => line.trim()));
108
+ if (present.has(GITIGNORE_ENTRY))
109
+ return false;
110
+ const prefix = text === '' || text.endsWith('\n') ? text : `${text}\n`;
111
+ writeFileSync(path, `${prefix}\n# backlog\n${GITIGNORE_ENTRY}\n`, 'utf8');
112
+ return true;
113
+ }
114
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import { type ApiClient } from '../api.js';
2
+ import { type TargetFlags } from '../config.js';
3
+ import { type ErrorContext } from '../errors.js';
4
+ /**
5
+ * What every command shares: resolve the config and the token, bind one client, and turn
6
+ * every refusal into exactly one sentence (Global Constraints — "one human sentence per
7
+ * refusal", and `explain` needs `ctx.url` or the `unreachable`/`internal` rows degrade).
8
+ *
9
+ * A command is a pure function of `{ cwd, home, env, fetchImpl }` plus its flags and returns
10
+ * `{ ok, lines }`; `src/index.ts` prints the lines and turns `ok` into the exit code. Nothing
11
+ * here reads `process`, so the tests run against a temp cwd and a temp HOME.
12
+ */
13
+ export interface CommandIo {
14
+ cwd: string;
15
+ home: string;
16
+ env: NodeJS.ProcessEnv;
17
+ fetchImpl?: typeof fetch | undefined;
18
+ }
19
+ export interface CommandResult {
20
+ ok: boolean;
21
+ lines: string[];
22
+ }
23
+ /** A refusal the CLI decided on its own (a missing config, a bad flag) — already a sentence. */
24
+ export declare class CommandError extends Error {
25
+ constructor(message: string);
26
+ }
27
+ export interface CommandContext {
28
+ url: string;
29
+ project: string;
30
+ conductorDir: string;
31
+ /** The directory the config lives in — the repo root, and where `.backlog/` goes. */
32
+ root: string;
33
+ client: ApiClient;
34
+ /** Passed to `explain` on every refusal. */
35
+ ctx: ErrorContext;
36
+ }
37
+ /**
38
+ * The config file is mandatory for every command: an env-only invocation would work, but a
39
+ * repo with no `backlog.config.json` has not been initialised and the only useful thing to
40
+ * say is which command fixes that.
41
+ */
42
+ export declare function resolveContext(command: string, io: CommandIo, flags: TargetFlags): CommandContext;
43
+ /** One line for anything a command can throw. `ApiError` goes through the shipped table. */
44
+ export declare function renderFailure(err: unknown, ctx: ErrorContext): string;
45
+ /**
46
+ * Resolve, run, and render. `body` may throw anything above; a non-`Error` throw is
47
+ * re-thrown, because a bug should not be dressed up as a refusal.
48
+ */
49
+ export declare function runCommand(command: string, io: CommandIo, flags: TargetFlags, body: (ctx: CommandContext) => Promise<CommandResult>): Promise<CommandResult>;
50
+ /** `--json` prints the payload and nothing else — the only escape hatch from the sentences. */
51
+ export declare function jsonResult(payload: unknown): CommandResult;
52
+ /** JSON from the service is unvalidated: read it tolerantly, never crash on a missing field. */
53
+ export declare function asRecord(value: unknown): Record<string, unknown>;
54
+ export declare function asArray(value: unknown): unknown[];
55
+ export declare function text(value: unknown, fallback?: string): string;
56
+ /** `1 item`, `2 items` — a count the reader does not have to forgive. */
57
+ export declare function plural(count: number, one: string, many?: string): string;
58
+ /** Pads a column without letting a long value push the rest of the row out of line. */
59
+ export declare function pad(value: string, width: number): string;
@@ -0,0 +1,94 @@
1
+ import { dirname } from 'node:path';
2
+ import { ApiError, createClient } from '../api.js';
3
+ import { CONFIG_FILENAME, ConfigError, loadConfig, resolveTarget, } from '../config.js';
4
+ import { explain } from '../errors.js';
5
+ import { resolveToken, TokenError } from '../token.js';
6
+ /** A refusal the CLI decided on its own (a missing config, a bad flag) — already a sentence. */
7
+ export class CommandError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'CommandError';
11
+ }
12
+ }
13
+ /**
14
+ * The config file is mandatory for every command: an env-only invocation would work, but a
15
+ * repo with no `backlog.config.json` has not been initialised and the only useful thing to
16
+ * say is which command fixes that.
17
+ */
18
+ export function resolveContext(command, io, flags) {
19
+ const loaded = loadConfig(io.cwd);
20
+ if (!loaded) {
21
+ throw new CommandError(`no ${CONFIG_FILENAME} in ${io.cwd} or any directory above it — run: backlog init`);
22
+ }
23
+ const target = resolveTarget(flags, io.env, io.cwd);
24
+ const token = resolveToken(io.env, io.home);
25
+ return {
26
+ url: target.url,
27
+ project: target.project,
28
+ conductorDir: target.conductor_dir,
29
+ root: dirname(loaded.path),
30
+ client: createClient({
31
+ url: target.url,
32
+ token,
33
+ project: target.project,
34
+ ...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}),
35
+ }),
36
+ ctx: { project: target.project, url: target.url, command },
37
+ };
38
+ }
39
+ /** One line for anything a command can throw. `ApiError` goes through the shipped table. */
40
+ export function renderFailure(err, ctx) {
41
+ if (err instanceof ApiError)
42
+ return explain(err, ctx);
43
+ if (err instanceof CommandError || err instanceof ConfigError || err instanceof TokenError) {
44
+ return err.message;
45
+ }
46
+ if (err instanceof Error && err.message !== '')
47
+ return err.message;
48
+ throw err;
49
+ }
50
+ /**
51
+ * Resolve, run, and render. `body` may throw anything above; a non-`Error` throw is
52
+ * re-thrown, because a bug should not be dressed up as a refusal.
53
+ */
54
+ export async function runCommand(command, io, flags, body) {
55
+ let ctx;
56
+ try {
57
+ ctx = resolveContext(command, io, flags);
58
+ }
59
+ catch (err) {
60
+ return { ok: false, lines: [renderFailure(err, { command })] };
61
+ }
62
+ try {
63
+ return await body(ctx);
64
+ }
65
+ catch (err) {
66
+ return { ok: false, lines: [renderFailure(err, ctx.ctx)] };
67
+ }
68
+ }
69
+ /** `--json` prints the payload and nothing else — the only escape hatch from the sentences. */
70
+ export function jsonResult(payload) {
71
+ return { ok: true, lines: [JSON.stringify(payload, null, 2)] };
72
+ }
73
+ /* ------------------------------------------------------- shared payload readers */
74
+ /** JSON from the service is unvalidated: read it tolerantly, never crash on a missing field. */
75
+ export function asRecord(value) {
76
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
77
+ ? value
78
+ : {};
79
+ }
80
+ export function asArray(value) {
81
+ return Array.isArray(value) ? value : [];
82
+ }
83
+ export function text(value, fallback = '-') {
84
+ return typeof value === 'string' && value !== '' ? value : fallback;
85
+ }
86
+ /** `1 item`, `2 items` — a count the reader does not have to forgive. */
87
+ export function plural(count, one, many = `${one}s`) {
88
+ return `${count} ${count === 1 ? one : many}`;
89
+ }
90
+ /** Pads a column without letting a long value push the rest of the row out of line. */
91
+ export function pad(value, width) {
92
+ return value.length >= width ? value : value.padEnd(width, ' ');
93
+ }
94
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/commands/common.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type CommandIo, type CommandResult } from './common.js';
2
+ /**
3
+ * `backlog export [outDir]` — the snapshot → markdown writer, wired to the config.
4
+ *
5
+ * It is deliberately **snapshot-only**. The service ships 36 MCP tools and none of them is an
6
+ * export group: there is no `export_markdown` to call (MIG-001 owns it), and re-implementing
7
+ * the serializer client-side would put a second renderer of the canonical format in the tree —
8
+ * exactly what `packages/core`'s markdown module exists to prevent. So with no snapshot the
9
+ * command says what to run and exits 1, and the exporter itself is untouched.
10
+ *
11
+ * The config supplies the project (for that sentence) and the default output directory (the
12
+ * repo the config lives in). Nothing here talks to the service, so `export` needs no token.
13
+ */
14
+ export declare const DEFAULT_SNAPSHOT = "backlog-snapshot.json";
15
+ export interface ExportFlags {
16
+ /** The positionals: `[]`, `[outDir]`, or the shipped `[snapshot, outDir]` form. */
17
+ positionals: string[];
18
+ snapshot?: string | undefined;
19
+ }
20
+ export declare function runExport(io: CommandIo, flags: ExportFlags): Promise<CommandResult>;
@@ -0,0 +1,56 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { isAbsolute, join, resolve } from 'node:path';
3
+ import { exportFromSnapshotFile } from '../export.js';
4
+ import { CommandError, renderFailure, resolveContext, } from './common.js';
5
+ /**
6
+ * `backlog export [outDir]` — the snapshot → markdown writer, wired to the config.
7
+ *
8
+ * It is deliberately **snapshot-only**. The service ships 36 MCP tools and none of them is an
9
+ * export group: there is no `export_markdown` to call (MIG-001 owns it), and re-implementing
10
+ * the serializer client-side would put a second renderer of the canonical format in the tree —
11
+ * exactly what `packages/core`'s markdown module exists to prevent. So with no snapshot the
12
+ * command says what to run and exits 1, and the exporter itself is untouched.
13
+ *
14
+ * The config supplies the project (for that sentence) and the default output directory (the
15
+ * repo the config lives in). Nothing here talks to the service, so `export` needs no token.
16
+ */
17
+ export const DEFAULT_SNAPSHOT = 'backlog-snapshot.json';
18
+ export async function runExport(io, flags) {
19
+ let ctx;
20
+ try {
21
+ // `export` needs the config (the project slug and the repo root), not a token.
22
+ ctx = resolveContext('export', io, {});
23
+ }
24
+ catch (err) {
25
+ return { ok: false, lines: [renderFailure(err, { command: 'export' })] };
26
+ }
27
+ const positionals = flags.positionals.filter((value) => value !== '');
28
+ const here = (value) => (isAbsolute(value) ? value : resolve(io.cwd, value));
29
+ // Two positionals is the shipped `backlog export <snapshot> <outDir>` invocation; one is the
30
+ // new `backlog export <outDir>`; none writes back into the repo the config was found in.
31
+ const outDir = positionals.length >= 2
32
+ ? here(positionals[1])
33
+ : positionals[0]
34
+ ? here(positionals[0])
35
+ : ctx.root;
36
+ const snapshot = flags.snapshot
37
+ ? here(flags.snapshot)
38
+ : positionals.length >= 2
39
+ ? here(positionals[0])
40
+ : join(ctx.root, DEFAULT_SNAPSHOT);
41
+ try {
42
+ if (!existsSync(snapshot)) {
43
+ throw new CommandError(`export needs a snapshot until export_markdown ships (MIG-001) — run: backlog import . --project ${ctx.project} --write`);
44
+ }
45
+ const written = await exportFromSnapshotFile(snapshot, outDir);
46
+ const items = written.filter((rel) => rel.includes('/items/')).length;
47
+ return {
48
+ ok: true,
49
+ lines: [`wrote ${written.length} files under ${outDir} (${items} items)`],
50
+ };
51
+ }
52
+ catch (err) {
53
+ return { ok: false, lines: [renderFailure(err, ctx.ctx)] };
54
+ }
55
+ }
56
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.js","sourceRoot":"","sources":["../../src/commands/export.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EACL,YAAY,EAGZ,aAAa,EACb,cAAc,GACf,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,uBAAuB,CAAC;AAQxD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,EAAa,EAAE,KAAkB;IAC/D,IAAI,GAAsC,CAAC;IAC3C,IAAI,CAAC;QACH,+EAA+E;QAC/E,GAAG,GAAG,cAAc,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC;IAC3E,CAAC;IAED,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;IACtE,MAAM,IAAI,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;IAC7F,6FAA6F;IAC7F,yFAAyF;IACzF,MAAM,MAAM,GACV,WAAW,CAAC,MAAM,IAAI,CAAC;QACrB,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACtB,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;YACd,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;YACtB,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;IACjB,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ;QAC7B,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QACtB,CAAC,CAAC,WAAW,CAAC,MAAM,IAAI,CAAC;YACvB,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;YACtB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAEvC,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,YAAY,CACpB,mGAAmG,GAAG,CAAC,OAAO,UAAU,CACzH,CAAC;QACJ,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;QACtE,OAAO;YACL,EAAE,EAAE,IAAI;YACR,KAAK,EAAE,CAAC,SAAS,OAAO,CAAC,MAAM,gBAAgB,MAAM,KAAK,KAAK,SAAS,CAAC;SAC1E,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;IAC7D,CAAC;AACH,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { TargetFlags } from '../config.js';
2
+ import { type CommandIo, type CommandResult } from './common.js';
3
+ /**
4
+ * `backlog matrix init <track> [--force]` — scaffolds one QA row per locked criterion (§5.6).
5
+ * The service owns the scaffold, the row ids and the refusal; the CLI prints what came back
6
+ * and, on a `conflict`, adds the one thing the service's sentence cannot say: what `--force`
7
+ * actually does, which is *add* rows for uncovered criteria and never rewrite an existing one.
8
+ */
9
+ export interface MatrixInitFlags extends TargetFlags {
10
+ track: string;
11
+ force?: boolean | undefined;
12
+ json?: boolean | undefined;
13
+ }
14
+ /** `--force` is a CLI flag, so the service's own sentence cannot name it. */
15
+ export declare const FORCE_HINT = "--force adds rows only for criteria that have none; it never rewrites an existing row";
16
+ export declare function runMatrixInit(io: CommandIo, flags: MatrixInitFlags): Promise<CommandResult>;
@@ -0,0 +1,48 @@
1
+ import { ApiError } from '../api.js';
2
+ import { explain } from '../errors.js';
3
+ import { asArray, asRecord, CommandError, jsonResult, pad, plural, runCommand, text, } from './common.js';
4
+ /** `--force` is a CLI flag, so the service's own sentence cannot name it. */
5
+ export const FORCE_HINT = '--force adds rows only for criteria that have none; it never rewrites an existing row';
6
+ /** `{ item: 'PLG-001', ac: 1 }` → `PLG-001 AC1`; a legacy row names its README rule. */
7
+ function criterionLabel(value) {
8
+ const ref = asRecord(value);
9
+ if (typeof ref.item === 'string' && typeof ref.ac === 'number')
10
+ return `${ref.item} AC${ref.ac}`;
11
+ if (typeof ref.legacy === 'string')
12
+ return `legacy ${ref.legacy}`;
13
+ return '(unlinked)';
14
+ }
15
+ export async function runMatrixInit(io, flags) {
16
+ return runCommand('matrix init', io, flags, async (ctx) => {
17
+ let payload;
18
+ try {
19
+ payload = await ctx.client.post(`/v1/tracks/${encodeURIComponent(ctx.project)}/${encodeURIComponent(flags.track)}/matrix`, flags.force ? { force: true } : {});
20
+ }
21
+ catch (err) {
22
+ if (err instanceof ApiError && err.code === 'conflict') {
23
+ throw new CommandError(`${explain(err, ctx.ctx)} — ${FORCE_HINT}`);
24
+ }
25
+ throw err;
26
+ }
27
+ if (flags.json)
28
+ return jsonResult(payload);
29
+ const view = asRecord(payload);
30
+ const added = new Set(asArray(view.added).map((id) => text(id, '')));
31
+ const rows = asArray(view.rows).map((row) => asRecord(row));
32
+ const lines = [];
33
+ for (const row of rows) {
34
+ const id = text(row.row_id, '');
35
+ if (!added.has(id))
36
+ continue;
37
+ lines.push(`${pad(id, 22)}${pad(criterionLabel(row.linked_criterion), 16)}${text(row.what_to_verify, '(no description)')}`);
38
+ }
39
+ if (added.size === 0) {
40
+ lines.push('no rows added — every criterion on this track already has one');
41
+ }
42
+ const gate = asRecord(view.gate);
43
+ const blocking = asArray(gate.blocking).length;
44
+ lines.push(`${plural(added.size, 'row')} added; ${plural(rows.length, 'row')} on ${flags.track}; gate: ${gate.ok === true ? 'ok' : `blocked (${blocking} blocking)`}`);
45
+ return { ok: true, lines };
46
+ });
47
+ }
48
+ //# sourceMappingURL=matrix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matrix.js","sourceRoot":"","sources":["../../src/commands/matrix.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { TargetFlags } from '../config.js';
2
+ import { type CommandIo, type CommandResult } from './common.js';
3
+ /**
4
+ * `backlog next` — what is ready to pick up, in the order core ranked it. No cache: "what
5
+ * should I do next" read from a stale file is how two people take the same item.
6
+ *
7
+ * The `warnings[]` the op returns are prerequisites naming refs the project does not hold;
8
+ * core cannot tell a typo from an unfiled item, so it stays silent and the caller is told.
9
+ */
10
+ export interface NextFlags extends TargetFlags {
11
+ limit?: number | undefined;
12
+ json?: boolean | undefined;
13
+ }
14
+ export declare function runNext(io: CommandIo, flags: NextFlags): Promise<CommandResult>;