@unbrained/pm-cli 2026.8.25 → 2026.8.27
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/.agents/skills/HARNESS_COMPATIBILITY.md +32 -0
- package/.agents/skills/README.md +47 -0
- package/.agents/skills/pm-developer/SKILL.md +117 -0
- package/.agents/skills/pm-developer/references/COMMAND_PLAYBOOK.md +49 -0
- package/.agents/skills/pm-developer/references/GRAPH_AND_RELATIONSHIPS.md +91 -0
- package/.agents/skills/pm-developer/references/MULTI_AGENT_MERGE.md +72 -0
- package/.agents/skills/pm-developer/references/PROMPTS.md +17 -0
- package/.agents/skills/pm-developer/references/SCRIPTING_COMPOSITION.md +82 -0
- package/.agents/skills/pm-developer/references/TOKEN_BUDGETS.md +85 -0
- package/.agents/skills/pm-extensions/SKILL.md +106 -0
- package/.agents/skills/pm-extensions/references/AUTHORING.md +95 -0
- package/.agents/skills/pm-extensions/references/LIFECYCLE.md +40 -0
- package/.agents/skills/pm-extensions/references/TROUBLESHOOTING.md +25 -0
- package/.agents/skills/pm-sdk/SKILL.md +107 -0
- package/.agents/skills/pm-sdk/references/DOMAIN_MODELING.md +78 -0
- package/.agents/skills/pm-sdk/references/INTEGRATION_CHECKLIST.md +31 -0
- package/.agents/skills/pm-sdk/references/PROMPTS.md +13 -0
- package/.agents/skills/pm-sdk/references/SURFACE_MAP.md +82 -0
- package/.agents/skills/pm-user/SKILL.md +111 -0
- package/.agents/skills/pm-user/references/BACKLOG_SHAPING.md +105 -0
- package/.agents/skills/pm-user/references/PROMPTS.md +17 -0
- package/.agents/skills/pm-user/references/WORKFLOWS.md +35 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +58 -3
- package/README.md +8 -5
- package/dist/cli/commander-usage.js +11 -7
- package/dist/cli/error-guidance.js +3 -3
- package/dist/cli/help-content.d.ts +2 -0
- package/dist/cli/help-content.js +53 -17
- package/dist/cli/help-json-payload.d.ts +8 -2
- package/dist/cli/help-json-payload.js +46 -12
- package/dist/cli/main.js +52 -74
- package/dist/cli/register-annotations.js +27 -21
- package/dist/cli/register-setup.js +98 -57
- package/dist/cli-bundle/bundle-manifest.json +156 -156
- package/dist/cli-bundle/chunks/chunk-3OO3W6FW.js +202 -0
- package/dist/cli-bundle/chunks/chunk-52EKTW6V.js +3 -0
- package/dist/cli-bundle/chunks/{chunk-QLUORNIB.js → chunk-CVBBGWW5.js} +62 -44
- package/dist/cli-bundle/chunks/chunk-MFNTKMTI.js +13 -0
- package/dist/cli-bundle/chunks/chunk-OS27HHBN.js +35 -0
- package/dist/cli-bundle/chunks/chunk-QTO7USTH.js +2 -0
- package/dist/cli-bundle/chunks/{chunk-5I5RWIJC.js → chunk-R4ETAOJC.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-OWHNAR2B.js → chunk-SH6P7FXI.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-SHMDY36D.js +8 -0
- package/dist/cli-bundle/chunks/{chunk-244MI4GS.js → chunk-SKXLJIEK.js} +60 -60
- package/dist/cli-bundle/chunks/chunk-TNX6HC54.js +3 -0
- package/dist/cli-bundle/chunks/{register-list-query-XVN2ZLI7.js → register-list-query-EUWM6VII.js} +2 -2
- package/dist/cli-bundle/chunks/{register-mutation-QCKAEGIJ.js → register-mutation-FD4HSAVU.js} +4 -4
- package/dist/cli-bundle/chunks/{register-operations-SDEAXE7E.js → register-operations-HRMNFEC3.js} +2 -2
- package/dist/cli-bundle/chunks/register-setup-33GNICLX.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-IBZZZGK3.js → chunk-2AGZ5BRT.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-4BR5UU52.js +50 -0
- package/dist/cli-bundle/focused-chunks/{chunk-OHIHZ7HS.js → chunk-6GCRSLPG.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-OVJL6NZE.js → chunk-AD6ULRAF.js} +4 -4
- package/dist/cli-bundle/focused-chunks/{chunk-7YCDTCBC.js → chunk-AQ5IYEZZ.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-YO3ZF3FI.js → chunk-EKX37ZHA.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-N7W67YIG.js → chunk-FC2AXLB5.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-H5JZEIQV.js → chunk-HC7ODMH3.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-NOOZGIXP.js → chunk-HVQ22RC4.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-A644DUFQ.js → chunk-MEASX544.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-RAFKLNZX.js → chunk-MXTYGECH.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-2UIWOP3O.js → chunk-SUBSWYW3.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-VXWATRFL.js → chunk-XDPYBQCF.js} +9 -9
- package/dist/cli-bundle/focused-chunks/{chunk-XUQPEKRN.js → chunk-Y3JJXRVK.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-GQR3WH3F.js → chunk-Y5A7SJJ7.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-YHWHX6YY.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-ONYQCALA.js → chunk-YVVZ3LQ6.js} +6 -6
- package/dist/cli-bundle/focused-chunks/chunk-Z2USIBR2.js +5 -0
- package/dist/cli-bundle/main.js +15 -14
- package/dist/cli-bundle/sdk-authoring.js +1 -1
- package/dist/cli-bundle/sdk-contracts.js +2 -2
- package/dist/cli-bundle/sdk-core.js +31 -31
- package/dist/cli-bundle/sdk-governance.js +1 -1
- package/dist/cli-bundle/sdk-graph.js +1 -1
- package/dist/cli-bundle/sdk-merge.js +31 -31
- package/dist/cli-bundle/sdk-query.js +1 -1
- package/dist/cli-bundle/sdk-runtime.js +1 -1
- package/dist/cli-bundle/sdk-testing.js +1 -1
- package/dist/cli-bundle/sdk.js +32 -6
- package/dist/core/extensions/manifest-schema.d.ts +20 -0
- package/dist/core/extensions/manifest-schema.js +28 -10
- package/dist/core/governance/issue-codes.d.ts +11 -2
- package/dist/core/governance/issue-codes.js +29 -10
- package/dist/core/item/item-format.js +3 -3
- package/dist/core/store/item-store.js +12 -5
- package/dist/mcp/http-server.d.ts +60 -0
- package/dist/mcp/http-server.js +451 -0
- package/dist/mcp/legacy-adapter.d.ts +50 -0
- package/dist/mcp/legacy-adapter.js +64 -0
- package/dist/mcp/server.d.ts +42 -9
- package/dist/mcp/server.js +572 -59
- package/dist/mcp/tool-definitions.d.ts +2 -0
- package/dist/mcp/tool-definitions.js +2 -2
- package/dist/sdk/agent/closed-domain-contracts.d.ts +1 -1
- package/dist/sdk/agent/closed-domain-contracts.js +24 -2
- package/dist/sdk/agent/refusal-closure-census.d.ts +6 -2
- package/dist/sdk/agent/refusal-closure-census.js +16 -8
- package/dist/sdk/agent-capability-contracts.js +6 -2
- package/dist/sdk/cli-bootstrap.js +3 -2
- package/dist/sdk/cli-contracts/command-aliases.js +15 -2
- package/dist/sdk/cli-contracts/enum-contracts.d.ts +4 -1
- package/dist/sdk/cli-contracts/enum-contracts.js +7 -2
- package/dist/sdk/cli-contracts/flag-contracts.js +9 -5
- package/dist/sdk/cli-contracts/grammar-contracts.d.ts +3 -3
- package/dist/sdk/cli-contracts/grammar-contracts.js +24 -17
- package/dist/sdk/cli-contracts/runtime-contracts.js +13 -11
- package/dist/sdk/cli-contracts/tool-schema.js +18 -15
- package/dist/sdk/cli-contracts.d.ts +1 -1
- package/dist/sdk/cli-contracts.js +3 -3
- package/dist/sdk/cli-program.js +3 -2
- package/dist/sdk/completion.js +40 -13
- package/dist/sdk/compose.d.ts +4 -1
- package/dist/sdk/compose.js +23 -36
- package/dist/sdk/extension/author-manifest.d.ts +22 -0
- package/dist/sdk/extension/author-manifest.js +93 -0
- package/dist/sdk/extension.js +6 -3
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +20 -5
- package/dist/sdk/generated/generated-error-code-catalog-part-2.js +18 -3
- package/dist/sdk/governance/health.js +7 -2
- package/dist/sdk/governance/upgrade.d.ts +2 -0
- package/dist/sdk/governance/upgrade.js +30 -8
- package/dist/sdk/governance/validate.js +8 -6
- package/dist/sdk/guide-topics.js +6 -6
- package/dist/sdk/index.d.ts +10 -2
- package/dist/sdk/index.js +11 -3
- package/dist/sdk/mcp/apps.d.ts +70 -0
- package/dist/sdk/mcp/apps.js +154 -0
- package/dist/sdk/mcp/authorization.d.ts +134 -0
- package/dist/sdk/mcp/authorization.js +405 -0
- package/dist/sdk/mcp/interactions.d.ts +118 -0
- package/dist/sdk/mcp/interactions.js +337 -0
- package/dist/sdk/mcp/protocol.d.ts +142 -0
- package/dist/sdk/mcp/protocol.js +174 -0
- package/dist/sdk/mcp/skills.d.ts +127 -0
- package/dist/sdk/mcp/skills.js +390 -0
- package/dist/sdk/mcp/subscriptions.d.ts +65 -0
- package/dist/sdk/mcp/subscriptions.js +212 -0
- package/dist/sdk/mcp/tasks.d.ts +107 -0
- package/dist/sdk/mcp/tasks.js +431 -0
- package/dist/sdk/mcp/transport.d.ts +30 -0
- package/dist/sdk/mcp/transport.js +261 -0
- package/dist/sdk/merge/receipts.d.ts +16 -0
- package/dist/sdk/merge/receipts.js +9 -8
- package/dist/sdk/read-output-contracts.js +16 -3
- package/dist/sdk/runtime-action-aliases.js +7 -3
- package/dist/sdk/runtime-input.js +12 -4
- package/dist/sdk/runtime-primitives.d.ts +2 -2
- package/dist/sdk/runtime-primitives.js +4 -4
- package/dist/sdk/test/execution.d.ts +6 -0
- package/dist/sdk/test/execution.js +32 -3
- package/docs/AGENT_PROVENANCE_ADR.md +6 -4
- package/docs/AGENT_RUNTIME_PRIMITIVES.md +6 -5
- package/docs/CLAUDE_CODE_PLUGIN.md +12 -5
- package/docs/CLI_GRAMMAR.md +7 -1
- package/docs/COMMANDS.md +2 -2
- package/docs/DIAGNOSTIC_OUTPUT_CONTRACTS.md +8 -0
- package/docs/EXTENSIONS.md +36 -36
- package/docs/MCP_2026_07_28.md +160 -0
- package/docs/MCP_2026_07_28_CONFORMANCE.md +30 -0
- package/docs/MCP_REMOTE_TRANSPORT_SECURITY.md +180 -0
- package/docs/MCP_SKILLS_AND_APPS.md +107 -0
- package/docs/QUICKSTART.md +15 -15
- package/docs/README.md +5 -0
- package/docs/RELEASING.md +12 -3
- package/docs/SDK.md +22 -1
- package/docs/SDK_AGENT_SESSION_CONTEXT.md +18 -13
- package/docs/SDK_CONTEXT_INTEGRITY.md +6 -0
- package/docs/SDK_EVIDENCE_TRACEABILITY.md +9 -1
- package/docs/SDK_MCP_INTERACTIONS.md +227 -0
- package/docs/TESTING.md +4 -0
- package/docs/generated/AGENT_CAPABILITY_ROUTING.md +1 -1
- package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +13 -11
- package/marketplace.json +2 -2
- package/package.json +15 -11
- package/packages/pm-beads/README.md +12 -6
- package/packages/pm-beads/docs/MIGRATION.md +53 -0
- package/packages/pm-beads/extensions/beads/index.ts +8 -0
- package/packages/pm-beads/extensions/beads/runtime.ts +671 -112
- package/packages/pm-beads/package.json +1 -1
- package/packages/pm-calendar/package.json +1 -1
- package/packages/pm-command-kit/package.json +1 -1
- package/packages/pm-digital-twin/package.json +1 -1
- package/packages/pm-governance-audit/package.json +1 -1
- package/packages/pm-guide-shell/package.json +1 -1
- package/packages/pm-kanban/package.json +1 -1
- package/packages/pm-lifecycle-hooks/package.json +1 -1
- package/packages/pm-linked-test-adapters/package.json +1 -1
- package/packages/pm-search-advanced/package.json +1 -1
- package/packages/pm-templates/package.json +1 -1
- package/packages/pm-todos/package.json +1 -1
- package/packages/pm-vcs/package.json +1 -1
- package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
- package/scripts/finalize-build.mjs +1 -0
- package/sdk/public-surface.json +902 -54
- package/dist/cli-bundle/chunks/chunk-2F3LUFMW.js +0 -8
- package/dist/cli-bundle/chunks/chunk-65MHLHAA.js +0 -2
- package/dist/cli-bundle/chunks/chunk-6C7GIMIL.js +0 -13
- package/dist/cli-bundle/chunks/chunk-QKGMHGEI.js +0 -202
- package/dist/cli-bundle/chunks/chunk-T2ENPRXF.js +0 -3
- package/dist/cli-bundle/chunks/chunk-TPQIBSL2.js +0 -3
- package/dist/cli-bundle/chunks/chunk-YQMYF3YD.js +0 -35
- package/dist/cli-bundle/chunks/register-setup-LXVBRCJ3.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-42S3GGZ7.js +0 -50
- package/dist/cli-bundle/focused-chunks/chunk-7I23XGWO.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-LMKG3DFE.js +0 -5
|
@@ -11,7 +11,9 @@ import type {
|
|
|
11
11
|
GlobalOptions,
|
|
12
12
|
ItemStatus,
|
|
13
13
|
ItemType,
|
|
14
|
+
LogNote,
|
|
14
15
|
PmSettings,
|
|
16
|
+
StructuredJsonValue,
|
|
15
17
|
ToImportLinkedArtifactsOptions,
|
|
16
18
|
ToImportLinkedTestsOptions,
|
|
17
19
|
ToImportLogEntriesOptions,
|
|
@@ -27,10 +29,16 @@ const UNSAFE_AUTO_DISCOVERY_FILES = [
|
|
|
27
29
|
"sync_base.jsonl",
|
|
28
30
|
] as const;
|
|
29
31
|
|
|
32
|
+
const AUTO_DISCOVERY_WARNINGS = new Map<string, string[]>([
|
|
33
|
+
["issues.jsonl", ["beads_import_source_autodiscovered:issues.jsonl"]],
|
|
34
|
+
]);
|
|
35
|
+
|
|
30
36
|
/** Inputs that customize the beads import operation. */
|
|
31
37
|
export interface BeadsImportOptions {
|
|
32
38
|
/** Value that configures or reports file for this contract. */
|
|
33
39
|
file?: string;
|
|
40
|
+
/** Path to a complete `bd backup` JSONL directory. */
|
|
41
|
+
backupDir?: string;
|
|
34
42
|
/** Value that configures or reports author for this contract. */
|
|
35
43
|
author?: string;
|
|
36
44
|
/** Human-readable explanation suitable for logs and agent-facing output. */
|
|
@@ -53,6 +61,36 @@ export interface BeadsImportResult {
|
|
|
53
61
|
ids: string[];
|
|
54
62
|
/** Value that configures or reports warnings for this contract. */
|
|
55
63
|
warnings: string[];
|
|
64
|
+
/** Whether source and imported relational counts prove lossless parity. */
|
|
65
|
+
complete?: boolean;
|
|
66
|
+
/** Source rows counted across a portable backup. */
|
|
67
|
+
source_counts?: BeadsPortableCounts;
|
|
68
|
+
/** Rows attached to imported pm items from a portable backup. */
|
|
69
|
+
imported_counts?: BeadsPortableCounts;
|
|
70
|
+
/** Complete source-to-imported identity receipt when source IDs are preserved. */
|
|
71
|
+
id_mapping?: BeadsIdMapping[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Relational row counts carried by a Beads portable-backup receipt. */
|
|
75
|
+
export interface BeadsPortableCounts {
|
|
76
|
+
/** Issue rows in `issues.jsonl`. */
|
|
77
|
+
issues: number;
|
|
78
|
+
/** Event rows in `events.jsonl`. */
|
|
79
|
+
events: number;
|
|
80
|
+
/** Comment rows in `comments.jsonl`. */
|
|
81
|
+
comments: number;
|
|
82
|
+
/** Dependency rows in `dependencies.jsonl`. */
|
|
83
|
+
dependencies: number;
|
|
84
|
+
/** Label rows in `labels.jsonl`. */
|
|
85
|
+
labels: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One exact source-to-imported ID mapping. */
|
|
89
|
+
export interface BeadsIdMapping {
|
|
90
|
+
/** Authoritative source identifier. */
|
|
91
|
+
source_id: string;
|
|
92
|
+
/** Identifier committed to pm storage. */
|
|
93
|
+
imported_id: string;
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
interface BeadsRecord extends Record<string, unknown> {
|
|
@@ -80,6 +118,12 @@ interface BeadsRecord extends Record<string, unknown> {
|
|
|
80
118
|
design?: unknown;
|
|
81
119
|
external_ref?: unknown;
|
|
82
120
|
close_reason?: unknown;
|
|
121
|
+
resolution?: unknown;
|
|
122
|
+
expected_result?: unknown;
|
|
123
|
+
actual_result?: unknown;
|
|
124
|
+
parent?: unknown;
|
|
125
|
+
comment_count?: unknown;
|
|
126
|
+
source_events?: unknown;
|
|
83
127
|
dependencies?: unknown;
|
|
84
128
|
comments?: unknown;
|
|
85
129
|
notes?: unknown;
|
|
@@ -96,6 +140,7 @@ interface BeadsImportRuntime {
|
|
|
96
140
|
| "commitImportedItem"
|
|
97
141
|
| "generateItemId"
|
|
98
142
|
| "getItemPath"
|
|
143
|
+
| "listAllItemMetadataLight"
|
|
99
144
|
| "locateItem"
|
|
100
145
|
| "normalizeItemMetadata"
|
|
101
146
|
>;
|
|
@@ -107,6 +152,19 @@ interface BeadsImportRuntime {
|
|
|
107
152
|
message: string;
|
|
108
153
|
}
|
|
109
154
|
|
|
155
|
+
interface ResolvedBeadsSource {
|
|
156
|
+
source: string;
|
|
157
|
+
sourcePaths: string[];
|
|
158
|
+
raw: string;
|
|
159
|
+
warnings: string[];
|
|
160
|
+
records?: BeadsRecord[];
|
|
161
|
+
counts?: BeadsPortableCounts;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface BeadsPortableBackupState {
|
|
165
|
+
counts?: Partial<BeadsPortableCounts> & { config?: number };
|
|
166
|
+
}
|
|
167
|
+
|
|
110
168
|
type BeadsImportLineResult =
|
|
111
169
|
| { id: string; writeWarnings: string[] }
|
|
112
170
|
| { warning: string };
|
|
@@ -120,7 +178,6 @@ const {
|
|
|
120
178
|
getActiveExtensionRegistrations,
|
|
121
179
|
isTimestampLiteral,
|
|
122
180
|
normalizeItemId,
|
|
123
|
-
normalizeRawItemId,
|
|
124
181
|
nowIso,
|
|
125
182
|
pathExists,
|
|
126
183
|
readSettings,
|
|
@@ -256,10 +313,24 @@ function normalizeImportedId(
|
|
|
256
313
|
preserveSourceIds: boolean,
|
|
257
314
|
): string {
|
|
258
315
|
return preserveSourceIds
|
|
259
|
-
?
|
|
316
|
+
? preserveBeadsSourceId(id)
|
|
260
317
|
: normalizeItemId(id, prefix);
|
|
261
318
|
}
|
|
262
319
|
|
|
320
|
+
function preserveBeadsSourceId(id: string): string {
|
|
321
|
+
if (
|
|
322
|
+
id.length === 0 ||
|
|
323
|
+
id !== id.trim() ||
|
|
324
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(id)
|
|
325
|
+
) {
|
|
326
|
+
throw new PmCliError(
|
|
327
|
+
`Beads source ID ${JSON.stringify(id)} cannot be preserved safely; IDs must contain only letters, digits, dots, underscores, and hyphens with no surrounding whitespace.`,
|
|
328
|
+
EXIT_CODE.USAGE,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return id;
|
|
332
|
+
}
|
|
333
|
+
|
|
263
334
|
function toDependencies(
|
|
264
335
|
value: unknown,
|
|
265
336
|
fallbackCreatedAt: string,
|
|
@@ -284,31 +355,39 @@ function toDependencies(
|
|
|
284
355
|
return dependencies.length > 0 ? dependencies : undefined;
|
|
285
356
|
}
|
|
286
357
|
|
|
287
|
-
function
|
|
288
|
-
value: unknown,
|
|
289
|
-
fallbackCreatedAt: string,
|
|
290
|
-
prefix: string,
|
|
291
|
-
preserveSourceIds: boolean,
|
|
292
|
-
): Dependency | undefined {
|
|
358
|
+
function toBeadsDependencyId(value: unknown): string | undefined {
|
|
293
359
|
if (typeof value === "string") {
|
|
294
|
-
return
|
|
295
|
-
value,
|
|
296
|
-
fallbackCreatedAt,
|
|
297
|
-
prefix,
|
|
298
|
-
preserveSourceIds,
|
|
299
|
-
);
|
|
360
|
+
return toNonEmptyString(value);
|
|
300
361
|
}
|
|
301
362
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
302
363
|
return undefined;
|
|
303
364
|
}
|
|
304
365
|
const candidate = value as Record<string, unknown>;
|
|
305
|
-
|
|
366
|
+
return (
|
|
367
|
+
toNonEmptyString(candidate.depends_on_id) ??
|
|
306
368
|
toNonEmptyString(candidate.id) ??
|
|
307
|
-
toNonEmptyString(candidate.item_id)
|
|
308
|
-
|
|
369
|
+
toNonEmptyString(candidate.item_id)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function toDependency(
|
|
374
|
+
value: unknown,
|
|
375
|
+
fallbackCreatedAt: string,
|
|
376
|
+
prefix: string,
|
|
377
|
+
preserveSourceIds: boolean,
|
|
378
|
+
): Dependency | undefined {
|
|
379
|
+
const id = toBeadsDependencyId(value);
|
|
309
380
|
if (!id) {
|
|
310
381
|
return undefined;
|
|
311
382
|
}
|
|
383
|
+
if (typeof value === "string") {
|
|
384
|
+
return {
|
|
385
|
+
id: normalizeImportedId(id, prefix, preserveSourceIds),
|
|
386
|
+
kind: "related",
|
|
387
|
+
created_at: fallbackCreatedAt,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const candidate = value as Record<string, unknown>;
|
|
312
391
|
const dependencyKind = toDependencyKind(candidate.type ?? candidate.kind);
|
|
313
392
|
return {
|
|
314
393
|
id: normalizeImportedId(id, prefix, preserveSourceIds),
|
|
@@ -321,23 +400,6 @@ function toDependency(
|
|
|
321
400
|
};
|
|
322
401
|
}
|
|
323
402
|
|
|
324
|
-
function toDependencyFromString(
|
|
325
|
-
value: string,
|
|
326
|
-
fallbackCreatedAt: string,
|
|
327
|
-
prefix: string,
|
|
328
|
-
preserveSourceIds: boolean,
|
|
329
|
-
): Dependency | undefined {
|
|
330
|
-
const id = toNonEmptyString(value);
|
|
331
|
-
if (!id) {
|
|
332
|
-
return undefined;
|
|
333
|
-
}
|
|
334
|
-
return {
|
|
335
|
-
id: normalizeImportedId(id, prefix, preserveSourceIds),
|
|
336
|
-
kind: "related",
|
|
337
|
-
created_at: fallbackCreatedAt,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
403
|
const selectAuthor = selectImportAuthor;
|
|
342
404
|
const ensureInitHasRun = ensureTrackerInitialized;
|
|
343
405
|
|
|
@@ -365,17 +427,26 @@ async function readStdin(): Promise<string> {
|
|
|
365
427
|
});
|
|
366
428
|
}
|
|
367
429
|
|
|
368
|
-
async function resolveBeadsSource(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}> {
|
|
430
|
+
async function resolveBeadsSource(
|
|
431
|
+
rawPath: string | undefined,
|
|
432
|
+
rawBackupDir: string | undefined,
|
|
433
|
+
): Promise<ResolvedBeadsSource> {
|
|
434
|
+
const backupDir = toNonEmptyString(rawBackupDir);
|
|
374
435
|
const explicitSource = toNonEmptyString(rawPath);
|
|
436
|
+
if (backupDir && explicitSource) {
|
|
437
|
+
throw new PmCliError(
|
|
438
|
+
'Options "--file" and "--backup-dir" are mutually exclusive.',
|
|
439
|
+
EXIT_CODE.USAGE,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
if (backupDir) {
|
|
443
|
+
return resolvePortableBackup(backupDir);
|
|
444
|
+
}
|
|
375
445
|
if (explicitSource) {
|
|
376
446
|
if (explicitSource === "-") {
|
|
377
447
|
return {
|
|
378
448
|
source: "-",
|
|
449
|
+
sourcePaths: [],
|
|
379
450
|
raw: await readStdin(),
|
|
380
451
|
warnings: [],
|
|
381
452
|
};
|
|
@@ -390,7 +461,7 @@ async function resolveBeadsSource(rawPath: string | undefined): Promise<{
|
|
|
390
461
|
}
|
|
391
462
|
return {
|
|
392
463
|
source: explicitSource,
|
|
393
|
-
|
|
464
|
+
sourcePaths: [explicitPath],
|
|
394
465
|
raw: await fs.readFile(explicitPath, "utf8"),
|
|
395
466
|
warnings: [],
|
|
396
467
|
};
|
|
@@ -401,12 +472,9 @@ async function resolveBeadsSource(rawPath: string | undefined): Promise<{
|
|
|
401
472
|
if (await pathExists(candidatePath)) {
|
|
402
473
|
return {
|
|
403
474
|
source: candidate,
|
|
404
|
-
|
|
475
|
+
sourcePaths: [candidatePath],
|
|
405
476
|
raw: await fs.readFile(candidatePath, "utf8"),
|
|
406
|
-
warnings:
|
|
407
|
-
candidate === PRIMARY_AUTO_DISCOVERY_FILES[0]
|
|
408
|
-
? []
|
|
409
|
-
: [`beads_import_source_autodiscovered:${candidate}`],
|
|
477
|
+
warnings: AUTO_DISCOVERY_WARNINGS.get(candidate) ?? [],
|
|
410
478
|
};
|
|
411
479
|
}
|
|
412
480
|
}
|
|
@@ -427,6 +495,275 @@ async function resolveBeadsSource(rawPath: string | undefined): Promise<{
|
|
|
427
495
|
);
|
|
428
496
|
}
|
|
429
497
|
|
|
498
|
+
async function readPortableJsonl(
|
|
499
|
+
backupPath: string,
|
|
500
|
+
filename: string,
|
|
501
|
+
): Promise<{ path: string; records: Record<string, unknown>[] }> {
|
|
502
|
+
const filePath = path.join(backupPath, filename);
|
|
503
|
+
if (!(await pathExists(filePath))) {
|
|
504
|
+
throw new PmCliError(
|
|
505
|
+
`Beads portable backup is incomplete: missing ${filePath}. Run bd backup --force and pass its backup directory with --backup-dir.`,
|
|
506
|
+
EXIT_CODE.NOT_FOUND,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const records: Record<string, unknown>[] = [];
|
|
510
|
+
const lines = (await fs.readFile(filePath, "utf8")).split(/\r?\n/u);
|
|
511
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
512
|
+
const line = lines[index]!.trim();
|
|
513
|
+
if (line.length === 0) continue;
|
|
514
|
+
let parsed: unknown;
|
|
515
|
+
try {
|
|
516
|
+
parsed = JSON.parse(line) as unknown;
|
|
517
|
+
} catch {
|
|
518
|
+
throw new PmCliError(
|
|
519
|
+
`Beads portable backup contains invalid JSON in ${filename}:${index + 1}.`,
|
|
520
|
+
EXIT_CODE.USAGE,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
if (
|
|
524
|
+
typeof parsed !== "object" ||
|
|
525
|
+
parsed === null ||
|
|
526
|
+
Array.isArray(parsed)
|
|
527
|
+
) {
|
|
528
|
+
throw new PmCliError(
|
|
529
|
+
`Beads portable backup contains a non-record row in ${filename}:${index + 1}.`,
|
|
530
|
+
EXIT_CODE.USAGE,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
records.push(parsed as Record<string, unknown>);
|
|
534
|
+
}
|
|
535
|
+
return { path: filePath, records };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function portableRelationIssueId(
|
|
539
|
+
record: Record<string, unknown>,
|
|
540
|
+
filename: string,
|
|
541
|
+
): string {
|
|
542
|
+
const issueId = toNonEmptyString(record.issue_id);
|
|
543
|
+
if (!issueId) {
|
|
544
|
+
throw new PmCliError(
|
|
545
|
+
`Beads portable backup relation in ${filename} is missing issue_id.`,
|
|
546
|
+
EXIT_CODE.USAGE,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
return issueId;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function attachPortableRelations(
|
|
553
|
+
issues: BeadsRecord[],
|
|
554
|
+
comments: Record<string, unknown>[],
|
|
555
|
+
events: Record<string, unknown>[],
|
|
556
|
+
dependencies: Record<string, unknown>[],
|
|
557
|
+
labels: Record<string, unknown>[],
|
|
558
|
+
): void {
|
|
559
|
+
const byId = new Map<string, BeadsRecord>();
|
|
560
|
+
for (const issue of issues) {
|
|
561
|
+
const id = toNonEmptyString(issue.id);
|
|
562
|
+
if (!id) {
|
|
563
|
+
throw new PmCliError(
|
|
564
|
+
"Beads portable backup issue is missing id.",
|
|
565
|
+
EXIT_CODE.USAGE,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
if (byId.has(id)) {
|
|
569
|
+
throw new PmCliError(
|
|
570
|
+
`Beads portable backup contains duplicate issue ID ${id}.`,
|
|
571
|
+
EXIT_CODE.CONFLICT,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (!toNonEmptyString(issue.title)) {
|
|
575
|
+
throw new PmCliError(
|
|
576
|
+
`Beads portable backup issue ${id} is missing title.`,
|
|
577
|
+
EXIT_CODE.USAGE,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
byId.set(id, issue);
|
|
581
|
+
}
|
|
582
|
+
attachPortableRelationRows(
|
|
583
|
+
byId,
|
|
584
|
+
comments,
|
|
585
|
+
"comments.jsonl",
|
|
586
|
+
"comments",
|
|
587
|
+
(row, issueId) => {
|
|
588
|
+
if (!toNonEmptyString(row.text)) {
|
|
589
|
+
throw new PmCliError(
|
|
590
|
+
`Beads portable backup comment for ${issueId} is missing text.`,
|
|
591
|
+
EXIT_CODE.USAGE,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
return row;
|
|
595
|
+
},
|
|
596
|
+
);
|
|
597
|
+
attachPortableRelationRows(
|
|
598
|
+
byId,
|
|
599
|
+
events,
|
|
600
|
+
"events.jsonl",
|
|
601
|
+
"source_events",
|
|
602
|
+
(row, issueId) => {
|
|
603
|
+
if (!toNonEmptyString(row.event_type)) {
|
|
604
|
+
throw new PmCliError(
|
|
605
|
+
`Beads portable backup event for ${issueId} is missing event_type.`,
|
|
606
|
+
EXIT_CODE.USAGE,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
return row;
|
|
610
|
+
},
|
|
611
|
+
);
|
|
612
|
+
attachPortableRelationRows(
|
|
613
|
+
byId,
|
|
614
|
+
dependencies,
|
|
615
|
+
"dependencies.jsonl",
|
|
616
|
+
"dependencies",
|
|
617
|
+
(row, issueId) => {
|
|
618
|
+
const targetId = toNonEmptyString(row.depends_on_id);
|
|
619
|
+
if (!targetId) {
|
|
620
|
+
throw new PmCliError(
|
|
621
|
+
`Beads portable backup dependency for ${issueId} is missing depends_on_id.`,
|
|
622
|
+
EXIT_CODE.USAGE,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if (!byId.has(targetId)) {
|
|
626
|
+
throw new PmCliError(
|
|
627
|
+
`Beads portable backup dependency for ${issueId} references missing target ${targetId}.`,
|
|
628
|
+
EXIT_CODE.CONFLICT,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
...row,
|
|
633
|
+
id: undefined,
|
|
634
|
+
item_id: undefined,
|
|
635
|
+
depends_on_id: targetId,
|
|
636
|
+
};
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
attachPortableRelationRows(
|
|
640
|
+
byId,
|
|
641
|
+
labels,
|
|
642
|
+
"labels.jsonl",
|
|
643
|
+
"labels",
|
|
644
|
+
(row, issueId) => {
|
|
645
|
+
const label = toNonEmptyString(row.label);
|
|
646
|
+
if (!label) {
|
|
647
|
+
throw new PmCliError(
|
|
648
|
+
`Beads portable backup label for ${issueId} is missing label.`,
|
|
649
|
+
EXIT_CODE.USAGE,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
return label;
|
|
653
|
+
},
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function attachPortableRelationRows(
|
|
658
|
+
byId: ReadonlyMap<string, BeadsRecord>,
|
|
659
|
+
rows: Record<string, unknown>[],
|
|
660
|
+
filename: string,
|
|
661
|
+
field: "comments" | "dependencies" | "labels" | "source_events",
|
|
662
|
+
normalize: (row: Record<string, unknown>, issueId: string) => unknown,
|
|
663
|
+
): void {
|
|
664
|
+
for (const row of rows) {
|
|
665
|
+
const issueId = portableRelationIssueId(row, filename);
|
|
666
|
+
const issue = byId.get(issueId);
|
|
667
|
+
if (!issue) {
|
|
668
|
+
throw new PmCliError(
|
|
669
|
+
`Beads portable backup ${filename} references missing issue ${issueId}.`,
|
|
670
|
+
EXIT_CODE.CONFLICT,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const current = Array.isArray(issue[field]) ? issue[field] : [];
|
|
674
|
+
issue[field] = [...current, normalize(row, issueId)];
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function verifyPortableCounts(
|
|
679
|
+
expected: BeadsPortableBackupState["counts"],
|
|
680
|
+
actual: BeadsPortableCounts,
|
|
681
|
+
): void {
|
|
682
|
+
if (!expected) {
|
|
683
|
+
throw new PmCliError(
|
|
684
|
+
"Beads portable backup_state.json is missing counts.",
|
|
685
|
+
EXIT_CODE.USAGE,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
for (const key of Object.keys(actual) as Array<keyof BeadsPortableCounts>) {
|
|
689
|
+
if (expected[key] !== actual[key]) {
|
|
690
|
+
throw new PmCliError(
|
|
691
|
+
`Beads portable backup count mismatch for ${key}: backup_state=${String(expected[key])}, observed=${actual[key]}.`,
|
|
692
|
+
EXIT_CODE.CONFLICT,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function resolvePortableBackup(
|
|
699
|
+
rawBackupDir: string,
|
|
700
|
+
): Promise<ResolvedBeadsSource> {
|
|
701
|
+
const backupPath = resolveInputPath(rawBackupDir);
|
|
702
|
+
if (!(await pathExists(backupPath))) {
|
|
703
|
+
throw new PmCliError(
|
|
704
|
+
`Beads portable backup directory not found at ${backupPath}`,
|
|
705
|
+
EXIT_CODE.NOT_FOUND,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const [issuesFile, eventsFile, commentsFile, dependenciesFile, labelsFile] =
|
|
709
|
+
await Promise.all([
|
|
710
|
+
readPortableJsonl(backupPath, "issues.jsonl"),
|
|
711
|
+
readPortableJsonl(backupPath, "events.jsonl"),
|
|
712
|
+
readPortableJsonl(backupPath, "comments.jsonl"),
|
|
713
|
+
readPortableJsonl(backupPath, "dependencies.jsonl"),
|
|
714
|
+
readPortableJsonl(backupPath, "labels.jsonl"),
|
|
715
|
+
]);
|
|
716
|
+
const statePath = path.join(backupPath, "backup_state.json");
|
|
717
|
+
if (!(await pathExists(statePath))) {
|
|
718
|
+
throw new PmCliError(
|
|
719
|
+
`Beads portable backup is incomplete: missing ${statePath}.`,
|
|
720
|
+
EXIT_CODE.NOT_FOUND,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
let state: BeadsPortableBackupState;
|
|
724
|
+
try {
|
|
725
|
+
state = JSON.parse(
|
|
726
|
+
await fs.readFile(statePath, "utf8"),
|
|
727
|
+
) as BeadsPortableBackupState;
|
|
728
|
+
} catch {
|
|
729
|
+
throw new PmCliError(
|
|
730
|
+
`Beads portable backup contains invalid JSON in ${statePath}.`,
|
|
731
|
+
EXIT_CODE.USAGE,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
const records = issuesFile.records.map((record) => ({ ...record }));
|
|
735
|
+
attachPortableRelations(
|
|
736
|
+
records,
|
|
737
|
+
commentsFile.records,
|
|
738
|
+
eventsFile.records,
|
|
739
|
+
dependenciesFile.records,
|
|
740
|
+
labelsFile.records,
|
|
741
|
+
);
|
|
742
|
+
const counts: BeadsPortableCounts = {
|
|
743
|
+
issues: records.length,
|
|
744
|
+
events: eventsFile.records.length,
|
|
745
|
+
comments: commentsFile.records.length,
|
|
746
|
+
dependencies: dependenciesFile.records.length,
|
|
747
|
+
labels: labelsFile.records.length,
|
|
748
|
+
};
|
|
749
|
+
verifyPortableCounts(state.counts, counts);
|
|
750
|
+
return {
|
|
751
|
+
source: backupPath,
|
|
752
|
+
sourcePaths: [
|
|
753
|
+
issuesFile.path,
|
|
754
|
+
eventsFile.path,
|
|
755
|
+
commentsFile.path,
|
|
756
|
+
dependenciesFile.path,
|
|
757
|
+
labelsFile.path,
|
|
758
|
+
statePath,
|
|
759
|
+
],
|
|
760
|
+
raw: "",
|
|
761
|
+
warnings: [],
|
|
762
|
+
records,
|
|
763
|
+
counts,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
430
767
|
function parseBeadsLine(line: string, lineNumber: number): ParsedBeadsLine {
|
|
431
768
|
const trimmed = line.trim();
|
|
432
769
|
if (trimmed.length === 0) {
|
|
@@ -444,11 +781,144 @@ function parseBeadsLine(line: string, lineNumber: number): ParsedBeadsLine {
|
|
|
444
781
|
return { record: parsed as BeadsRecord };
|
|
445
782
|
}
|
|
446
783
|
|
|
784
|
+
function parseBeadsRecords(source: ResolvedBeadsSource): {
|
|
785
|
+
records: Array<{ record: BeadsRecord; lineNumber: number }>;
|
|
786
|
+
warnings: string[];
|
|
787
|
+
skipped: number;
|
|
788
|
+
} {
|
|
789
|
+
if (source.records) {
|
|
790
|
+
return {
|
|
791
|
+
records: source.records.map((record, index) => ({
|
|
792
|
+
record,
|
|
793
|
+
lineNumber: index + 1,
|
|
794
|
+
})),
|
|
795
|
+
warnings: [],
|
|
796
|
+
skipped: 0,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const records: Array<{ record: BeadsRecord; lineNumber: number }> = [];
|
|
800
|
+
const warnings: string[] = [];
|
|
801
|
+
let skipped = 0;
|
|
802
|
+
const lines = source.raw.split(/\r?\n/u);
|
|
803
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
804
|
+
const lineNumber = index + 1;
|
|
805
|
+
const parsed = parseBeadsLine(lines[index]!, lineNumber);
|
|
806
|
+
if (!parsed) continue;
|
|
807
|
+
if ("warning" in parsed) {
|
|
808
|
+
warnings.push(parsed.warning);
|
|
809
|
+
skipped += 1;
|
|
810
|
+
} else {
|
|
811
|
+
records.push({ record: parsed.record, lineNumber });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return { records, warnings, skipped };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function assertLosslessSourceFormat(
|
|
818
|
+
source: ResolvedBeadsSource,
|
|
819
|
+
records: Array<{ record: BeadsRecord }>,
|
|
820
|
+
): void {
|
|
821
|
+
if (
|
|
822
|
+
source.counts === undefined &&
|
|
823
|
+
records.some(
|
|
824
|
+
({ record }) =>
|
|
825
|
+
typeof record.comment_count === "number" && record.comment_count > 0,
|
|
826
|
+
)
|
|
827
|
+
) {
|
|
828
|
+
throw new PmCliError(
|
|
829
|
+
"This Beads issue export advertises comment counts but omits the relational comment/event tables. Run `bd backup --force` and import the resulting directory with `pm beads import --backup-dir <path>`; no pm items were written.",
|
|
830
|
+
EXIT_CODE.USAGE,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function assertPreservedRelationshipIdSafety(record: BeadsRecord): void {
|
|
836
|
+
const parent = toNonEmptyString(record.parent);
|
|
837
|
+
if (parent) preserveBeadsSourceId(parent);
|
|
838
|
+
if (!Array.isArray(record.dependencies)) return;
|
|
839
|
+
for (const dependency of record.dependencies) {
|
|
840
|
+
const dependencyId = toBeadsDependencyId(dependency);
|
|
841
|
+
if (dependencyId) preserveBeadsSourceId(dependencyId);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function assertPreservedIdSafety(
|
|
846
|
+
records: Array<{ record: BeadsRecord; lineNumber: number }>,
|
|
847
|
+
runtime: BeadsImportRuntime,
|
|
848
|
+
): Promise<void> {
|
|
849
|
+
if (!runtime.preserveSourceIds) return;
|
|
850
|
+
const sourceByFoldedId = new Map<
|
|
851
|
+
string,
|
|
852
|
+
{ id: string; lineNumber: number }
|
|
853
|
+
>();
|
|
854
|
+
for (const { record, lineNumber } of records) {
|
|
855
|
+
assertPreservedRelationshipIdSafety(record);
|
|
856
|
+
if (typeof record.id !== "string" || record.id.trim().length === 0) {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const id = preserveBeadsSourceId(record.id);
|
|
860
|
+
const folded = id.toLocaleLowerCase("en-US");
|
|
861
|
+
const previous = sourceByFoldedId.get(folded);
|
|
862
|
+
if (previous) {
|
|
863
|
+
throw new PmCliError(
|
|
864
|
+
`Beads preserve-source-ids case-insensitive ID collision: ${previous.id} (line ${previous.lineNumber}) conflicts with ${id} (line ${lineNumber}); no pm items were written.`,
|
|
865
|
+
EXIT_CODE.CONFLICT,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
sourceByFoldedId.set(folded, { id, lineNumber });
|
|
869
|
+
}
|
|
870
|
+
const existingItems = await runtime.sdk.listAllItemMetadataLight(
|
|
871
|
+
runtime.pmRoot,
|
|
872
|
+
runtime.settings.item_format,
|
|
873
|
+
runtime.typeRegistry.type_to_folder,
|
|
874
|
+
);
|
|
875
|
+
const existingByFoldedId = new Map(
|
|
876
|
+
existingItems.map(({ id }) => [id.toLocaleLowerCase("en-US"), id]),
|
|
877
|
+
);
|
|
878
|
+
for (const { id } of sourceByFoldedId.values()) {
|
|
879
|
+
const existingId = existingByFoldedId.get(id.toLocaleLowerCase("en-US"));
|
|
880
|
+
if (existingId) {
|
|
881
|
+
throw new PmCliError(
|
|
882
|
+
`Beads preserve-source-ids target collision: source ${id} conflicts case-insensitively with existing pm item ${existingId}; no pm items were written.`,
|
|
883
|
+
EXIT_CODE.CONFLICT,
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function countImportedPortableRelations(
|
|
890
|
+
records: Array<{ record: BeadsRecord }>,
|
|
891
|
+
importedSourceIds: ReadonlySet<string>,
|
|
892
|
+
): BeadsPortableCounts {
|
|
893
|
+
const importedRecords = records
|
|
894
|
+
.map(({ record }) => record)
|
|
895
|
+
.filter(
|
|
896
|
+
(record) =>
|
|
897
|
+
typeof record.id === "string" && importedSourceIds.has(record.id),
|
|
898
|
+
);
|
|
899
|
+
const count = (field: keyof BeadsRecord): number =>
|
|
900
|
+
importedRecords.reduce(
|
|
901
|
+
(total, record) =>
|
|
902
|
+
total + (Array.isArray(record[field]) ? record[field].length : 0),
|
|
903
|
+
0,
|
|
904
|
+
);
|
|
905
|
+
return {
|
|
906
|
+
issues: importedRecords.length,
|
|
907
|
+
events: count("source_events"),
|
|
908
|
+
comments: count("comments"),
|
|
909
|
+
dependencies: count("dependencies"),
|
|
910
|
+
labels: count("labels"),
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
447
914
|
async function resolveBeadsImportId(
|
|
448
915
|
record: BeadsRecord,
|
|
449
916
|
runtime: BeadsImportRuntime,
|
|
450
917
|
): Promise<string> {
|
|
451
|
-
const rawId =
|
|
918
|
+
const rawId =
|
|
919
|
+
typeof record.id === "string" && record.id.trim().length > 0
|
|
920
|
+
? record.id
|
|
921
|
+
: undefined;
|
|
452
922
|
return rawId
|
|
453
923
|
? normalizeImportedId(
|
|
454
924
|
rawId,
|
|
@@ -461,6 +931,53 @@ async function resolveBeadsImportId(
|
|
|
461
931
|
);
|
|
462
932
|
}
|
|
463
933
|
|
|
934
|
+
function toBeadsEventNotes(
|
|
935
|
+
value: unknown,
|
|
936
|
+
fallbackCreatedAt: string,
|
|
937
|
+
fallbackAuthor: string,
|
|
938
|
+
): LogNote[] {
|
|
939
|
+
if (!Array.isArray(value)) return [];
|
|
940
|
+
return value.flatMap((entry): LogNote[] => {
|
|
941
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
942
|
+
return [];
|
|
943
|
+
}
|
|
944
|
+
const record = entry as Record<string, unknown>;
|
|
945
|
+
const eventType = toNonEmptyString(record.event_type);
|
|
946
|
+
if (!eventType) {
|
|
947
|
+
return [];
|
|
948
|
+
}
|
|
949
|
+
const data = {
|
|
950
|
+
source: "beads-portable-backup",
|
|
951
|
+
...record,
|
|
952
|
+
} as Record<string, StructuredJsonValue>;
|
|
953
|
+
return [
|
|
954
|
+
{
|
|
955
|
+
created_at: toIsoString(record.created_at) ?? fallbackCreatedAt,
|
|
956
|
+
author: toNonEmptyString(record.actor) ?? fallbackAuthor,
|
|
957
|
+
text: toNonEmptyString(record.comment) ?? eventType,
|
|
958
|
+
format: "json",
|
|
959
|
+
event_type: `beads:${eventType}`,
|
|
960
|
+
data,
|
|
961
|
+
},
|
|
962
|
+
];
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function collectBeadsNotes(
|
|
967
|
+
record: BeadsRecord,
|
|
968
|
+
createdAt: string,
|
|
969
|
+
author: string,
|
|
970
|
+
): LogNote[] | undefined {
|
|
971
|
+
const notes =
|
|
972
|
+
toImportLogEntries(record.notes, {
|
|
973
|
+
...BEADS_LOG_ENTRY_OPTIONS,
|
|
974
|
+
fallbackCreatedAt: createdAt,
|
|
975
|
+
fallbackAuthor: author,
|
|
976
|
+
}) ?? [];
|
|
977
|
+
notes.push(...toBeadsEventNotes(record.source_events, createdAt, author));
|
|
978
|
+
return notes.length > 0 ? notes : undefined;
|
|
979
|
+
}
|
|
980
|
+
|
|
464
981
|
function buildBeadsImportedBody(record: BeadsRecord): string {
|
|
465
982
|
const rawBody = toNonEmptyString(record.body) ?? "";
|
|
466
983
|
const design = toNonEmptyString(record.design);
|
|
@@ -486,21 +1003,66 @@ async function importBeadsRecord(
|
|
|
486
1003
|
return { warning: `beads_import_missing_title:${lineNumber}` };
|
|
487
1004
|
}
|
|
488
1005
|
|
|
1006
|
+
const id = await resolveBeadsImportId(record, runtime);
|
|
1007
|
+
const itemMetadata = buildBeadsItemMetadata(record, id, title, runtime);
|
|
1008
|
+
const afterDocument = runtime.sdk.canonicalDocument({
|
|
1009
|
+
metadata: itemMetadata,
|
|
1010
|
+
body: buildBeadsImportedBody(record),
|
|
1011
|
+
});
|
|
1012
|
+
const existing = await runtime.sdk.locateItem(
|
|
1013
|
+
runtime.pmRoot,
|
|
1014
|
+
id,
|
|
1015
|
+
runtime.settings.id_prefix,
|
|
1016
|
+
runtime.settings.item_format,
|
|
1017
|
+
runtime.typeRegistry.type_to_folder,
|
|
1018
|
+
);
|
|
1019
|
+
if (existing) {
|
|
1020
|
+
return { warning: `beads_import_item_exists:${id}` };
|
|
1021
|
+
}
|
|
1022
|
+
const itemPath = runtime.sdk.getItemPath(
|
|
1023
|
+
runtime.pmRoot,
|
|
1024
|
+
itemMetadata.type,
|
|
1025
|
+
id,
|
|
1026
|
+
"toon",
|
|
1027
|
+
runtime.typeRegistry.type_to_folder,
|
|
1028
|
+
);
|
|
1029
|
+
const commit = await runtime.sdk.commitImportedItem({
|
|
1030
|
+
pmRoot: runtime.pmRoot,
|
|
1031
|
+
id,
|
|
1032
|
+
itemPath,
|
|
1033
|
+
document: afterDocument,
|
|
1034
|
+
author: runtime.author,
|
|
1035
|
+
message: runtime.message,
|
|
1036
|
+
settings: runtime.settings,
|
|
1037
|
+
conflictWarningPrefix: "beads_import_lock_conflict",
|
|
1038
|
+
});
|
|
1039
|
+
return commit.committed
|
|
1040
|
+
? { id, writeWarnings: commit.writeWarnings }
|
|
1041
|
+
: { warning: commit.conflictWarning };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function buildBeadsItemMetadata(
|
|
1045
|
+
record: BeadsRecord,
|
|
1046
|
+
id: string,
|
|
1047
|
+
title: string,
|
|
1048
|
+
runtime: BeadsImportRuntime,
|
|
1049
|
+
): ReturnType<typeof pmSdk.normalizeItemMetadata> {
|
|
489
1050
|
const createdAt = toIsoString(record.created_at) ?? nowIso();
|
|
490
1051
|
const updatedAt = toIsoString(record.updated_at) ?? createdAt;
|
|
491
|
-
const id = await resolveBeadsImportId(record, runtime);
|
|
492
1052
|
const typeMapping = toItemType(record.issue_type ?? record.type);
|
|
493
1053
|
const type = typeMapping.type;
|
|
1054
|
+
const status = toStatus(record.status);
|
|
494
1055
|
const closedAt = toIsoString(record.closed_at);
|
|
1056
|
+
const closeReason = toNonEmptyString(record.close_reason);
|
|
495
1057
|
const assignee =
|
|
496
1058
|
toNonEmptyString(record.assignee) ?? toNonEmptyString(record.owner);
|
|
497
|
-
|
|
1059
|
+
return runtime.sdk.normalizeItemMetadata({
|
|
498
1060
|
id,
|
|
499
1061
|
title,
|
|
500
1062
|
description: toNonEmptyString(record.description) ?? "",
|
|
501
1063
|
type,
|
|
502
1064
|
source_type: typeMapping.sourceType,
|
|
503
|
-
status
|
|
1065
|
+
status,
|
|
504
1066
|
priority: toPriority(record.priority),
|
|
505
1067
|
tags: toTags(record.tags ?? record.labels),
|
|
506
1068
|
created_at: createdAt,
|
|
@@ -517,7 +1079,20 @@ async function importBeadsRecord(
|
|
|
517
1079
|
acceptance_criteria: toNonEmptyString(record.acceptance_criteria),
|
|
518
1080
|
design: toNonEmptyString(record.design),
|
|
519
1081
|
external_ref: toNonEmptyString(record.external_ref),
|
|
520
|
-
|
|
1082
|
+
parent:
|
|
1083
|
+
typeof record.parent === "string" && record.parent.trim().length > 0
|
|
1084
|
+
? normalizeImportedId(
|
|
1085
|
+
record.parent,
|
|
1086
|
+
runtime.settings.id_prefix,
|
|
1087
|
+
runtime.preserveSourceIds,
|
|
1088
|
+
)
|
|
1089
|
+
: undefined,
|
|
1090
|
+
close_reason: closeReason,
|
|
1091
|
+
resolution:
|
|
1092
|
+
toNonEmptyString(record.resolution) ??
|
|
1093
|
+
(status === "closed" || status === "canceled" ? closeReason : undefined),
|
|
1094
|
+
expected_result: toNonEmptyString(record.expected_result),
|
|
1095
|
+
actual_result: toNonEmptyString(record.actual_result),
|
|
521
1096
|
dependencies: toDependencies(
|
|
522
1097
|
record.dependencies,
|
|
523
1098
|
createdAt,
|
|
@@ -529,11 +1104,7 @@ async function importBeadsRecord(
|
|
|
529
1104
|
fallbackCreatedAt: createdAt,
|
|
530
1105
|
fallbackAuthor: runtime.author,
|
|
531
1106
|
}),
|
|
532
|
-
notes:
|
|
533
|
-
...BEADS_LOG_ENTRY_OPTIONS,
|
|
534
|
-
fallbackCreatedAt: createdAt,
|
|
535
|
-
fallbackAuthor: runtime.author,
|
|
536
|
-
}),
|
|
1107
|
+
notes: collectBeadsNotes(record, createdAt, runtime.author),
|
|
537
1108
|
learnings: toImportLogEntries(record.learnings, {
|
|
538
1109
|
...BEADS_LOG_ENTRY_OPTIONS,
|
|
539
1110
|
fallbackCreatedAt: createdAt,
|
|
@@ -543,40 +1114,6 @@ async function importBeadsRecord(
|
|
|
543
1114
|
tests: toImportLinkedTests(record.tests, BEADS_TEST_OPTIONS),
|
|
544
1115
|
docs: toImportLinkedDocs(record.docs, BEADS_DOC_OPTIONS),
|
|
545
1116
|
});
|
|
546
|
-
const afterDocument = runtime.sdk.canonicalDocument({
|
|
547
|
-
metadata: itemMetadata,
|
|
548
|
-
body: buildBeadsImportedBody(record),
|
|
549
|
-
});
|
|
550
|
-
const existing = await runtime.sdk.locateItem(
|
|
551
|
-
runtime.pmRoot,
|
|
552
|
-
id,
|
|
553
|
-
runtime.settings.id_prefix,
|
|
554
|
-
runtime.settings.item_format,
|
|
555
|
-
runtime.typeRegistry.type_to_folder,
|
|
556
|
-
);
|
|
557
|
-
if (existing) {
|
|
558
|
-
return { warning: `beads_import_item_exists:${id}` };
|
|
559
|
-
}
|
|
560
|
-
const itemPath = runtime.sdk.getItemPath(
|
|
561
|
-
runtime.pmRoot,
|
|
562
|
-
type,
|
|
563
|
-
id,
|
|
564
|
-
"toon",
|
|
565
|
-
runtime.typeRegistry.type_to_folder,
|
|
566
|
-
);
|
|
567
|
-
const commit = await runtime.sdk.commitImportedItem({
|
|
568
|
-
pmRoot: runtime.pmRoot,
|
|
569
|
-
id,
|
|
570
|
-
itemPath,
|
|
571
|
-
document: afterDocument,
|
|
572
|
-
author: runtime.author,
|
|
573
|
-
message: runtime.message,
|
|
574
|
-
settings: runtime.settings,
|
|
575
|
-
conflictWarningPrefix: "beads_import_lock_conflict",
|
|
576
|
-
});
|
|
577
|
-
return commit.committed
|
|
578
|
-
? { id, writeWarnings: commit.writeWarnings }
|
|
579
|
-
: { warning: commit.conflictWarning };
|
|
580
1117
|
}
|
|
581
1118
|
|
|
582
1119
|
/** Executes the beads import operation through the package runtime. */
|
|
@@ -593,14 +1130,18 @@ export async function runBeadsImport(
|
|
|
593
1130
|
getActiveExtensionRegistrations(),
|
|
594
1131
|
);
|
|
595
1132
|
const preserveSourceIds = options.preserveSourceIds === true;
|
|
1133
|
+
const resolvedSource = await resolveBeadsSource(
|
|
1134
|
+
options.file,
|
|
1135
|
+
options.backupDir,
|
|
1136
|
+
);
|
|
596
1137
|
const {
|
|
597
1138
|
source,
|
|
598
|
-
|
|
599
|
-
raw,
|
|
1139
|
+
sourcePaths,
|
|
600
1140
|
warnings: sourceWarnings,
|
|
601
|
-
|
|
1141
|
+
counts: sourceCounts,
|
|
1142
|
+
} = resolvedSource;
|
|
602
1143
|
const warnings: string[] = [...sourceWarnings];
|
|
603
|
-
|
|
1144
|
+
for (const sourcePath of sourcePaths) {
|
|
604
1145
|
warnings.push(
|
|
605
1146
|
...(await runActiveOnReadHooks({
|
|
606
1147
|
path: sourcePath,
|
|
@@ -608,7 +1149,9 @@ export async function runBeadsImport(
|
|
|
608
1149
|
})),
|
|
609
1150
|
);
|
|
610
1151
|
}
|
|
611
|
-
const
|
|
1152
|
+
const parsed = parseBeadsRecords(resolvedSource);
|
|
1153
|
+
warnings.push(...parsed.warnings);
|
|
1154
|
+
assertLosslessSourceFormat(resolvedSource, parsed.records);
|
|
612
1155
|
const author = selectAuthor(
|
|
613
1156
|
toNonEmptyString(options.author),
|
|
614
1157
|
settings.author_default,
|
|
@@ -616,8 +1159,10 @@ export async function runBeadsImport(
|
|
|
616
1159
|
const message =
|
|
617
1160
|
toNonEmptyString(options.message) ?? "Import from Beads JSONL";
|
|
618
1161
|
const ids: string[] = [];
|
|
1162
|
+
const idMapping: BeadsIdMapping[] = [];
|
|
1163
|
+
const importedSourceIds = new Set<string>();
|
|
619
1164
|
let imported = 0;
|
|
620
|
-
let skipped =
|
|
1165
|
+
let skipped = parsed.skipped;
|
|
621
1166
|
const runtime: BeadsImportRuntime = {
|
|
622
1167
|
sdk: pmSdk,
|
|
623
1168
|
pmRoot,
|
|
@@ -627,23 +1172,10 @@ export async function runBeadsImport(
|
|
|
627
1172
|
author,
|
|
628
1173
|
message,
|
|
629
1174
|
};
|
|
1175
|
+
await assertPreservedIdSafety(parsed.records, runtime);
|
|
630
1176
|
|
|
631
|
-
for (
|
|
632
|
-
const
|
|
633
|
-
const parsed = parseBeadsLine(lines[index], lineNumber);
|
|
634
|
-
if (!parsed) {
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
if ("warning" in parsed) {
|
|
638
|
-
warnings.push(parsed.warning);
|
|
639
|
-
skipped += 1;
|
|
640
|
-
continue;
|
|
641
|
-
}
|
|
642
|
-
const importedLine = await importBeadsRecord(
|
|
643
|
-
parsed.record,
|
|
644
|
-
lineNumber,
|
|
645
|
-
runtime,
|
|
646
|
-
);
|
|
1177
|
+
for (const { record, lineNumber } of parsed.records) {
|
|
1178
|
+
const importedLine = await importBeadsRecord(record, lineNumber, runtime);
|
|
647
1179
|
if ("warning" in importedLine) {
|
|
648
1180
|
warnings.push(importedLine.warning);
|
|
649
1181
|
skipped += 1;
|
|
@@ -651,9 +1183,22 @@ export async function runBeadsImport(
|
|
|
651
1183
|
}
|
|
652
1184
|
warnings.push(...importedLine.writeWarnings);
|
|
653
1185
|
ids.push(importedLine.id);
|
|
1186
|
+
if (typeof record.id === "string" && record.id.trim().length > 0) {
|
|
1187
|
+
importedSourceIds.add(record.id);
|
|
1188
|
+
if (preserveSourceIds) {
|
|
1189
|
+
idMapping.push({
|
|
1190
|
+
source_id: record.id,
|
|
1191
|
+
imported_id: importedLine.id,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
654
1195
|
imported += 1;
|
|
655
1196
|
}
|
|
656
1197
|
|
|
1198
|
+
const importedCounts = sourceCounts
|
|
1199
|
+
? countImportedPortableRelations(parsed.records, importedSourceIds)
|
|
1200
|
+
: undefined;
|
|
1201
|
+
|
|
657
1202
|
return {
|
|
658
1203
|
ok: true,
|
|
659
1204
|
source,
|
|
@@ -661,5 +1206,19 @@ export async function runBeadsImport(
|
|
|
661
1206
|
skipped,
|
|
662
1207
|
ids,
|
|
663
1208
|
warnings,
|
|
1209
|
+
...(sourceCounts
|
|
1210
|
+
? {
|
|
1211
|
+
complete:
|
|
1212
|
+
skipped === 0 &&
|
|
1213
|
+
Object.keys(sourceCounts).every(
|
|
1214
|
+
(key) =>
|
|
1215
|
+
sourceCounts[key as keyof BeadsPortableCounts] ===
|
|
1216
|
+
importedCounts?.[key as keyof BeadsPortableCounts],
|
|
1217
|
+
),
|
|
1218
|
+
source_counts: sourceCounts,
|
|
1219
|
+
imported_counts: importedCounts,
|
|
1220
|
+
}
|
|
1221
|
+
: {}),
|
|
1222
|
+
...(preserveSourceIds ? { id_mapping: idMapping } : {}),
|
|
664
1223
|
};
|
|
665
1224
|
}
|