@unbrained/pm-cli 2026.3.12 → 2026.5.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.
- package/.agents/pm/extensions/.managed-extensions.json +42 -0
- package/.agents/pm/extensions/beads/index.js +109 -0
- package/.agents/pm/extensions/beads/manifest.json +7 -0
- package/{dist/cli/commands/beads.js → .agents/pm/extensions/beads/runtime.js} +31 -21
- package/.agents/pm/extensions/beads/runtime.ts +702 -0
- package/.agents/pm/extensions/todos/index.js +126 -0
- package/.agents/pm/extensions/todos/manifest.json +7 -0
- package/{dist/extensions/builtins/todos/import-export.js → .agents/pm/extensions/todos/runtime.js} +39 -29
- package/.agents/pm/extensions/todos/runtime.ts +568 -0
- package/AGENTS.md +196 -92
- package/CHANGELOG.md +399 -0
- package/CODE_OF_CONDUCT.md +42 -0
- package/CONTRIBUTING.md +144 -0
- package/PRD.md +512 -164
- package/README.md +1053 -2
- package/SECURITY.md +51 -0
- package/dist/cli/commands/activity.d.ts +5 -0
- package/dist/cli/commands/activity.js +66 -3
- package/dist/cli/commands/activity.js.map +1 -1
- package/dist/cli/commands/aggregate.d.ts +54 -0
- package/dist/cli/commands/aggregate.js +181 -0
- package/dist/cli/commands/aggregate.js.map +1 -0
- package/dist/cli/commands/append.js +4 -1
- package/dist/cli/commands/append.js.map +1 -1
- package/dist/cli/commands/calendar.d.ts +109 -0
- package/dist/cli/commands/calendar.js +797 -0
- package/dist/cli/commands/calendar.js.map +1 -0
- package/dist/cli/commands/claim.d.ts +5 -1
- package/dist/cli/commands/claim.js +42 -21
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/close.d.ts +1 -0
- package/dist/cli/commands/close.js +54 -5
- package/dist/cli/commands/close.js.map +1 -1
- package/dist/cli/commands/comments-audit.d.ts +91 -0
- package/dist/cli/commands/comments-audit.js +195 -0
- package/dist/cli/commands/comments-audit.js.map +1 -0
- package/dist/cli/commands/comments.d.ts +1 -0
- package/dist/cli/commands/comments.js +70 -21
- package/dist/cli/commands/comments.js.map +1 -1
- package/dist/cli/commands/completion.d.ts +10 -4
- package/dist/cli/commands/completion.js +1184 -137
- package/dist/cli/commands/completion.js.map +1 -1
- package/dist/cli/commands/config.d.ts +35 -3
- package/dist/cli/commands/config.js +968 -13
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/context.d.ts +86 -0
- package/dist/cli/commands/context.js +299 -0
- package/dist/cli/commands/context.js.map +1 -0
- package/dist/cli/commands/contracts.d.ts +78 -0
- package/dist/cli/commands/contracts.js +920 -0
- package/dist/cli/commands/contracts.js.map +1 -0
- package/dist/cli/commands/create.d.ts +48 -14
- package/dist/cli/commands/create.js +1331 -160
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/commands/dedupe-audit.d.ts +81 -0
- package/dist/cli/commands/dedupe-audit.js +330 -0
- package/dist/cli/commands/dedupe-audit.js.map +1 -0
- package/dist/cli/commands/deps.d.ts +52 -0
- package/dist/cli/commands/deps.js +204 -0
- package/dist/cli/commands/deps.js.map +1 -0
- package/dist/cli/commands/docs.d.ts +19 -0
- package/dist/cli/commands/docs.js +212 -13
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/extension.d.ts +122 -0
- package/dist/cli/commands/extension.js +1850 -0
- package/dist/cli/commands/extension.js.map +1 -0
- package/dist/cli/commands/files.d.ts +52 -1
- package/dist/cli/commands/files.js +443 -13
- package/dist/cli/commands/files.js.map +1 -1
- package/dist/cli/commands/gc.d.ts +11 -1
- package/dist/cli/commands/gc.js +89 -11
- package/dist/cli/commands/gc.js.map +1 -1
- package/dist/cli/commands/get.d.ts +13 -0
- package/dist/cli/commands/get.js +35 -3
- package/dist/cli/commands/get.js.map +1 -1
- package/dist/cli/commands/health.d.ts +10 -2
- package/dist/cli/commands/health.js +774 -23
- package/dist/cli/commands/health.js.map +1 -1
- package/dist/cli/commands/history.d.ts +20 -0
- package/dist/cli/commands/history.js +152 -6
- package/dist/cli/commands/history.js.map +1 -1
- package/dist/cli/commands/index.d.ts +16 -3
- package/dist/cli/commands/index.js +16 -3
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +7 -2
- package/dist/cli/commands/init.js +137 -5
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/learnings.d.ts +17 -0
- package/dist/cli/commands/learnings.js +129 -0
- package/dist/cli/commands/learnings.js.map +1 -0
- package/dist/cli/commands/list.d.ts +29 -1
- package/dist/cli/commands/list.js +289 -53
- package/dist/cli/commands/list.js.map +1 -1
- package/dist/cli/commands/normalize.d.ts +51 -0
- package/dist/cli/commands/normalize.js +298 -0
- package/dist/cli/commands/normalize.js.map +1 -0
- package/dist/cli/commands/notes.d.ts +17 -0
- package/dist/cli/commands/notes.js +129 -0
- package/dist/cli/commands/notes.js.map +1 -0
- package/dist/cli/commands/reindex.d.ts +1 -0
- package/dist/cli/commands/reindex.js +208 -32
- package/dist/cli/commands/reindex.js.map +1 -1
- package/dist/cli/commands/restore.js +164 -30
- package/dist/cli/commands/restore.js.map +1 -1
- package/dist/cli/commands/search.d.ts +14 -1
- package/dist/cli/commands/search.js +475 -81
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/stats.js +26 -10
- package/dist/cli/commands/stats.js.map +1 -1
- package/dist/cli/commands/templates.d.ts +26 -0
- package/dist/cli/commands/templates.js +179 -0
- package/dist/cli/commands/templates.js.map +1 -0
- package/dist/cli/commands/test-all.d.ts +19 -1
- package/dist/cli/commands/test-all.js +161 -13
- package/dist/cli/commands/test-all.js.map +1 -1
- package/dist/cli/commands/test-runs.d.ts +63 -0
- package/dist/cli/commands/test-runs.js +179 -0
- package/dist/cli/commands/test-runs.js.map +1 -0
- package/dist/cli/commands/test.d.ts +75 -1
- package/dist/cli/commands/test.js +1360 -41
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/commands/update-many.d.ts +57 -0
- package/dist/cli/commands/update-many.js +631 -0
- package/dist/cli/commands/update-many.js.map +1 -0
- package/dist/cli/commands/update.d.ts +30 -0
- package/dist/cli/commands/update.js +1393 -84
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/commands/validate.d.ts +30 -0
- package/dist/cli/commands/validate.js +1140 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/error-guidance.d.ts +33 -0
- package/dist/cli/error-guidance.js +337 -0
- package/dist/cli/error-guidance.js.map +1 -0
- package/dist/cli/extension-command-options.d.ts +1 -0
- package/dist/cli/extension-command-options.js +92 -0
- package/dist/cli/extension-command-options.js.map +1 -1
- package/dist/cli/help-content.d.ts +20 -0
- package/dist/cli/help-content.js +543 -0
- package/dist/cli/help-content.js.map +1 -0
- package/dist/cli/main.js +3625 -445
- package/dist/cli/main.js.map +1 -1
- package/dist/core/extensions/index.d.ts +13 -1
- package/dist/core/extensions/index.js +108 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/item-fields.d.ts +2 -0
- package/dist/core/extensions/item-fields.js +79 -0
- package/dist/core/extensions/item-fields.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +322 -9
- package/dist/core/extensions/loader.js +911 -20
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runtime-registrations.d.ts +5 -0
- package/dist/core/extensions/runtime-registrations.js +51 -0
- package/dist/core/extensions/runtime-registrations.js.map +1 -0
- package/dist/core/history/history-stream-policy.d.ts +20 -0
- package/dist/core/history/history-stream-policy.js +53 -0
- package/dist/core/history/history-stream-policy.js.map +1 -0
- package/dist/core/history/history.js +90 -1
- package/dist/core/history/history.js.map +1 -1
- package/dist/core/item/id.js +4 -1
- package/dist/core/item/id.js.map +1 -1
- package/dist/core/item/index.d.ts +1 -0
- package/dist/core/item/index.js +1 -0
- package/dist/core/item/index.js.map +1 -1
- package/dist/core/item/item-format.d.ts +11 -5
- package/dist/core/item/item-format.js +507 -24
- package/dist/core/item/item-format.js.map +1 -1
- package/dist/core/item/parent-reference-policy.d.ts +6 -0
- package/dist/core/item/parent-reference-policy.js +32 -0
- package/dist/core/item/parent-reference-policy.js.map +1 -0
- package/dist/core/item/parse.d.ts +5 -0
- package/dist/core/item/parse.js +216 -19
- package/dist/core/item/parse.js.map +1 -1
- package/dist/core/item/sprint-release-format.d.ts +6 -0
- package/dist/core/item/sprint-release-format.js +33 -0
- package/dist/core/item/sprint-release-format.js.map +1 -0
- package/dist/core/item/status.d.ts +3 -0
- package/dist/core/item/status.js +24 -0
- package/dist/core/item/status.js.map +1 -0
- package/dist/core/item/type-registry.d.ts +37 -0
- package/dist/core/item/type-registry.js +706 -0
- package/dist/core/item/type-registry.js.map +1 -0
- package/dist/core/lock/lock.d.ts +1 -1
- package/dist/core/lock/lock.js +101 -12
- package/dist/core/lock/lock.js.map +1 -1
- package/dist/core/output/command-aware.d.ts +1 -0
- package/dist/core/output/command-aware.js +394 -0
- package/dist/core/output/command-aware.js.map +1 -0
- package/dist/core/output/output.d.ts +3 -0
- package/dist/core/output/output.js +124 -6
- package/dist/core/output/output.js.map +1 -1
- package/dist/core/schema/runtime-field-filters.d.ts +3 -0
- package/dist/core/schema/runtime-field-filters.js +39 -0
- package/dist/core/schema/runtime-field-filters.js.map +1 -0
- package/dist/core/schema/runtime-field-values.d.ts +8 -0
- package/dist/core/schema/runtime-field-values.js +154 -0
- package/dist/core/schema/runtime-field-values.js.map +1 -0
- package/dist/core/schema/runtime-schema.d.ts +68 -0
- package/dist/core/schema/runtime-schema.js +554 -0
- package/dist/core/schema/runtime-schema.js.map +1 -0
- package/dist/core/search/cache.d.ts +13 -1
- package/dist/core/search/cache.js +123 -14
- package/dist/core/search/cache.js.map +1 -1
- package/dist/core/search/semantic-defaults.d.ts +6 -0
- package/dist/core/search/semantic-defaults.js +120 -0
- package/dist/core/search/semantic-defaults.js.map +1 -0
- package/dist/core/search/vector-stores.js +3 -1
- package/dist/core/search/vector-stores.js.map +1 -1
- package/dist/core/shared/command-types.d.ts +2 -0
- package/dist/core/shared/conflict-markers.d.ts +7 -0
- package/dist/core/shared/conflict-markers.js +27 -0
- package/dist/core/shared/conflict-markers.js.map +1 -0
- package/dist/core/shared/constants.d.ts +15 -4
- package/dist/core/shared/constants.js +141 -1
- package/dist/core/shared/constants.js.map +1 -1
- package/dist/core/shared/errors.d.ts +10 -1
- package/dist/core/shared/errors.js +3 -1
- package/dist/core/shared/errors.js.map +1 -1
- package/dist/core/shared/text-normalization.d.ts +4 -0
- package/dist/core/shared/text-normalization.js +33 -0
- package/dist/core/shared/text-normalization.js.map +1 -0
- package/dist/core/shared/time.d.ts +1 -2
- package/dist/core/shared/time.js +98 -11
- package/dist/core/shared/time.js.map +1 -1
- package/dist/core/store/index.d.ts +1 -0
- package/dist/core/store/index.js +1 -0
- package/dist/core/store/index.js.map +1 -1
- package/dist/core/store/item-format-migration.d.ts +9 -0
- package/dist/core/store/item-format-migration.js +87 -0
- package/dist/core/store/item-format-migration.js.map +1 -0
- package/dist/core/store/item-store.d.ts +13 -4
- package/dist/core/store/item-store.js +238 -51
- package/dist/core/store/item-store.js.map +1 -1
- package/dist/core/store/paths.d.ts +21 -3
- package/dist/core/store/paths.js +59 -4
- package/dist/core/store/paths.js.map +1 -1
- package/dist/core/store/settings.d.ts +14 -1
- package/dist/core/store/settings.js +463 -7
- package/dist/core/store/settings.js.map +1 -1
- package/dist/core/telemetry/consent.d.ts +2 -0
- package/dist/core/telemetry/consent.js +79 -0
- package/dist/core/telemetry/consent.js.map +1 -0
- package/dist/core/telemetry/runtime.d.ts +38 -0
- package/dist/core/telemetry/runtime.js +733 -0
- package/dist/core/telemetry/runtime.js.map +1 -0
- package/dist/core/test/background-runs.d.ts +117 -0
- package/dist/core/test/background-runs.js +760 -0
- package/dist/core/test/background-runs.js.map +1 -0
- package/dist/core/test/item-test-run-tracking.d.ts +9 -0
- package/dist/core/test/item-test-run-tracking.js +50 -0
- package/dist/core/test/item-test-run-tracking.js.map +1 -0
- package/dist/sdk/cli-contracts.d.ts +92 -0
- package/dist/sdk/cli-contracts.js +2357 -0
- package/dist/sdk/cli-contracts.js.map +1 -0
- package/dist/sdk/index.d.ts +34 -0
- package/dist/sdk/index.js +23 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/types.d.ts +197 -3
- package/dist/types.js +48 -1
- package/dist/types.js.map +1 -1
- package/docs/ARCHITECTURE.md +368 -39
- package/docs/EXTENSIONS.md +454 -49
- package/docs/RELEASING.md +68 -19
- package/docs/SDK.md +123 -0
- package/docs/examples/starter-extension/README.md +48 -0
- package/docs/examples/starter-extension/index.js +191 -0
- package/docs/examples/starter-extension/manifest.json +17 -0
- package/docs/examples/starter-extension/package.json +10 -0
- package/package.json +33 -6
- package/.pi/extensions/pm-cli/index.ts +0 -778
- package/dist/cli/commands/beads.d.ts +0 -16
- package/dist/cli/commands/beads.js.map +0 -1
- package/dist/cli/commands/install.d.ts +0 -18
- package/dist/cli/commands/install.js +0 -87
- package/dist/cli/commands/install.js.map +0 -1
- package/dist/core/extensions/builtins.d.ts +0 -3
- package/dist/core/extensions/builtins.js +0 -47
- package/dist/core/extensions/builtins.js.map +0 -1
- package/dist/extensions/builtins/beads/index.d.ts +0 -8
- package/dist/extensions/builtins/beads/index.js +0 -33
- package/dist/extensions/builtins/beads/index.js.map +0 -1
- package/dist/extensions/builtins/todos/import-export.d.ts +0 -26
- package/dist/extensions/builtins/todos/import-export.js.map +0 -1
- package/dist/extensions/builtins/todos/index.d.ts +0 -8
- package/dist/extensions/builtins/todos/index.js +0 -38
- package/dist/extensions/builtins/todos/index.js.map +0 -1
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import type { Dirent } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getActiveExtensionRegistrations, runActiveOnReadHooks, runActiveOnWriteHooks } from "../../../../src/core/extensions/index.js";
|
|
5
|
+
import { pathExists, removeFileIfExists, writeFileAtomic } from "../../../../src/core/fs/fs-utils.js";
|
|
6
|
+
import { appendHistoryEntry, createHistoryEntry } from "../../../../src/core/history/history.js";
|
|
7
|
+
import { generateItemId, normalizeItemId } from "../../../../src/core/item/id.js";
|
|
8
|
+
import { canonicalDocument, normalizeFrontMatter, serializeItemDocument, splitFrontMatter } from "../../../../src/core/item/item-format.js";
|
|
9
|
+
import { normalizeStatusInput } from "../../../../src/core/item/status.js";
|
|
10
|
+
import { resolveItemTypeRegistry } from "../../../../src/core/item/type-registry.js";
|
|
11
|
+
import { parseTags } from "../../../../src/core/item/parse.js";
|
|
12
|
+
import { acquireLock } from "../../../../src/core/lock/lock.js";
|
|
13
|
+
import { EXIT_CODE } from "../../../../src/core/shared/constants.js";
|
|
14
|
+
import type { GlobalOptions } from "../../../../src/core/shared/command-types.js";
|
|
15
|
+
import { PmCliError } from "../../../../src/core/shared/errors.js";
|
|
16
|
+
import { nowIso } from "../../../../src/core/shared/time.js";
|
|
17
|
+
import { listAllFrontMatter, locateItem, readLocatedItem } from "../../../../src/core/store/item-store.js";
|
|
18
|
+
import { getHistoryPath, getItemPath, getSettingsPath, resolvePmRoot } from "../../../../src/core/store/paths.js";
|
|
19
|
+
import { readSettings } from "../../../../src/core/store/settings.js";
|
|
20
|
+
import { CONFIDENCE_TEXT_VALUES, ISSUE_SEVERITY_VALUES, RISK_VALUES } from "../../../../src/types/index.js";
|
|
21
|
+
import type { ItemDocument, ItemFrontMatter, ItemStatus, ItemType, PmSettings } from "../../../../src/types/index.js";
|
|
22
|
+
|
|
23
|
+
const DEFAULT_TODOS_FOLDER = ".pi/todos";
|
|
24
|
+
|
|
25
|
+
export interface TodosImportOptions {
|
|
26
|
+
folder?: string;
|
|
27
|
+
author?: string;
|
|
28
|
+
message?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TodosExportOptions {
|
|
32
|
+
folder?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TodosImportResult {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
folder: string;
|
|
38
|
+
imported: number;
|
|
39
|
+
skipped: number;
|
|
40
|
+
ids: string[];
|
|
41
|
+
warnings: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TodosExportResult {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
folder: string;
|
|
47
|
+
exported: number;
|
|
48
|
+
ids: string[];
|
|
49
|
+
warnings: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type PriorityValue = 0 | 1 | 2 | 3 | 4;
|
|
53
|
+
|
|
54
|
+
interface ParsedTodoCandidate {
|
|
55
|
+
entryName: string;
|
|
56
|
+
frontMatter: Record<string, unknown>;
|
|
57
|
+
body: string;
|
|
58
|
+
readWarnings: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface TodosImportRuntime {
|
|
62
|
+
pmRoot: string;
|
|
63
|
+
sourceFolder: string;
|
|
64
|
+
settings: PmSettings;
|
|
65
|
+
typeNames: string[];
|
|
66
|
+
typeToFolder: Record<string, string>;
|
|
67
|
+
author: string;
|
|
68
|
+
message: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type ImportCandidateResult = { id: string; writeWarnings: string[] } | { warning: string };
|
|
72
|
+
|
|
73
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
74
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toNonEmptyString(value: unknown): string | undefined {
|
|
78
|
+
if (typeof value !== "string") {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toIsoString(value: unknown): string | undefined {
|
|
86
|
+
const raw = toNonEmptyString(value);
|
|
87
|
+
if (!raw) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const timestamp = Date.parse(raw);
|
|
91
|
+
if (!Number.isFinite(timestamp)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return new Date(timestamp).toISOString();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toEstimatedMinutes(value: unknown): number | undefined {
|
|
98
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
102
|
+
const parsed = Number(value);
|
|
103
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
104
|
+
return parsed;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function toInteger(value: unknown): number | undefined {
|
|
111
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
115
|
+
const parsed = Number(value);
|
|
116
|
+
if (Number.isInteger(parsed)) {
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toPriority(value: unknown): PriorityValue {
|
|
124
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4) {
|
|
125
|
+
return value as PriorityValue;
|
|
126
|
+
}
|
|
127
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
128
|
+
const parsed = Number(value);
|
|
129
|
+
if (Number.isInteger(parsed) && parsed >= 0 && parsed <= 4) {
|
|
130
|
+
return parsed as PriorityValue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return 2;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function toConfidence(value: unknown): ItemFrontMatter["confidence"] | undefined {
|
|
137
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 100) {
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
if (typeof value !== "string") {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const normalized = value.trim().toLowerCase();
|
|
144
|
+
if (normalized.length === 0) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
if (normalized === "med") {
|
|
148
|
+
return "medium";
|
|
149
|
+
}
|
|
150
|
+
if (CONFIDENCE_TEXT_VALUES.includes(normalized as (typeof CONFIDENCE_TEXT_VALUES)[number])) {
|
|
151
|
+
return normalized as (typeof CONFIDENCE_TEXT_VALUES)[number];
|
|
152
|
+
}
|
|
153
|
+
const parsed = Number(normalized);
|
|
154
|
+
if (Number.isInteger(parsed) && parsed >= 0 && parsed <= 100) {
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toNormalizedEnum<T extends readonly string[]>(
|
|
161
|
+
value: unknown,
|
|
162
|
+
allowed: T,
|
|
163
|
+
): T[number] | undefined {
|
|
164
|
+
if (typeof value !== "string") {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const normalized = value.trim().toLowerCase();
|
|
168
|
+
if (normalized.length === 0) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const candidate = normalized === "med" ? "medium" : normalized;
|
|
172
|
+
if (allowed.includes(candidate as T[number])) {
|
|
173
|
+
return candidate as T[number];
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function toBoolean(value: unknown): boolean | undefined {
|
|
179
|
+
if (typeof value === "boolean") {
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
if (typeof value === "string") {
|
|
183
|
+
const normalized = value.trim().toLowerCase();
|
|
184
|
+
if (normalized === "true" || normalized === "1") {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (normalized === "false" || normalized === "0") {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (typeof value === "number") {
|
|
192
|
+
if (value === 1) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
if (value === 0) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function toTags(value: unknown): string[] {
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
const tags = value
|
|
205
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
206
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
207
|
+
.filter((entry) => entry.length > 0);
|
|
208
|
+
return Array.from(new Set(tags)).sort((left, right) => left.localeCompare(right));
|
|
209
|
+
}
|
|
210
|
+
if (typeof value === "string") {
|
|
211
|
+
return parseTags(value);
|
|
212
|
+
}
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function toItemType(value: unknown, typeNames: string[]): ItemType {
|
|
217
|
+
const normalized = toNonEmptyString(value)?.toLowerCase();
|
|
218
|
+
const fallbackType = typeNames.find((entry) => entry.toLowerCase() === "task") ?? typeNames[0] ?? "Task";
|
|
219
|
+
if (!normalized) {
|
|
220
|
+
return fallbackType;
|
|
221
|
+
}
|
|
222
|
+
for (const candidate of typeNames) {
|
|
223
|
+
if (candidate.toLowerCase() === normalized) {
|
|
224
|
+
return candidate;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return fallbackType;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function toStatus(value: unknown): ItemStatus {
|
|
231
|
+
const normalized = toNonEmptyString(value);
|
|
232
|
+
if (normalized) {
|
|
233
|
+
const canonical = normalizeStatusInput(normalized);
|
|
234
|
+
if (canonical) {
|
|
235
|
+
return canonical;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return "open";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function selectAuthor(explicitAuthor: string | undefined, settingsAuthor: string): string {
|
|
242
|
+
const candidate = explicitAuthor ?? process.env.PM_AUTHOR ?? settingsAuthor;
|
|
243
|
+
const trimmed = candidate.trim();
|
|
244
|
+
return trimmed.length > 0 ? trimmed : "unknown";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function ensureInitHasRun(pmRoot: string): Promise<void> {
|
|
248
|
+
return pathExists(getSettingsPath(pmRoot)).then((exists) => {
|
|
249
|
+
if (!exists) {
|
|
250
|
+
throw new PmCliError(`Tracker is not initialized at ${pmRoot}. Run pm init first.`, EXIT_CODE.NOT_FOUND);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function normalizeBody(body: string): string {
|
|
256
|
+
return body.replace(/^\n+/, "").replace(/\s+$/, "");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function emptyDocument(): ItemDocument {
|
|
260
|
+
return {
|
|
261
|
+
front_matter: {} as ItemFrontMatter,
|
|
262
|
+
body: "",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function resolveFolderPath(rawPath: string): string {
|
|
267
|
+
return path.isAbsolute(rawPath) ? rawPath : path.resolve(process.cwd(), rawPath);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function parseTodoMarkdown(content: string): { frontMatter: Record<string, unknown>; body: string } {
|
|
271
|
+
const split = splitFrontMatter(content);
|
|
272
|
+
if (split.frontMatter.length === 0) {
|
|
273
|
+
throw new TypeError("Missing JSON front matter");
|
|
274
|
+
}
|
|
275
|
+
const parsed = JSON.parse(split.frontMatter) as unknown;
|
|
276
|
+
if (!isRecord(parsed)) {
|
|
277
|
+
throw new TypeError("Front matter must be a JSON object");
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
frontMatter: parsed,
|
|
281
|
+
body: normalizeBody(split.body),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function readTodoCandidate(sourceFolder: string, entry: Dirent): Promise<ParsedTodoCandidate | { warning: string }> {
|
|
286
|
+
const sourcePath = path.join(sourceFolder, entry.name);
|
|
287
|
+
let raw: string;
|
|
288
|
+
try {
|
|
289
|
+
raw = await fs.readFile(sourcePath, "utf8");
|
|
290
|
+
} catch {
|
|
291
|
+
return { warning: `todos_import_read_failed:${entry.name}` };
|
|
292
|
+
}
|
|
293
|
+
const readWarnings = await runActiveOnReadHooks({
|
|
294
|
+
path: sourcePath,
|
|
295
|
+
scope: "project",
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
let parsed: { frontMatter: Record<string, unknown>; body: string };
|
|
299
|
+
try {
|
|
300
|
+
parsed = parseTodoMarkdown(raw);
|
|
301
|
+
} catch {
|
|
302
|
+
return { warning: `todos_import_invalid_front_matter:${entry.name}` };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
entryName: entry.name,
|
|
307
|
+
frontMatter: parsed.frontMatter,
|
|
308
|
+
body: parsed.body,
|
|
309
|
+
readWarnings,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function importTodoCandidate(candidate: ParsedTodoCandidate, runtime: TodosImportRuntime): Promise<ImportCandidateResult> {
|
|
314
|
+
const title = toNonEmptyString(candidate.frontMatter.title);
|
|
315
|
+
if (!title) {
|
|
316
|
+
return { warning: `todos_import_missing_title:${candidate.entryName}` };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const explicitId = toNonEmptyString(candidate.frontMatter.id);
|
|
320
|
+
const derivedId = path.basename(candidate.entryName, path.extname(candidate.entryName));
|
|
321
|
+
// Hidden filenames (for example `.md`) do not provide stable human ids.
|
|
322
|
+
const idSource = explicitId ?? (derivedId.startsWith(".") ? undefined : derivedId);
|
|
323
|
+
const id = idSource
|
|
324
|
+
? normalizeItemId(idSource, runtime.settings.id_prefix)
|
|
325
|
+
: await generateItemId(runtime.pmRoot, runtime.settings.id_prefix);
|
|
326
|
+
const createdAt = toIsoString(candidate.frontMatter.created_at) ?? nowIso();
|
|
327
|
+
const updatedAt = toIsoString(candidate.frontMatter.updated_at) ?? createdAt;
|
|
328
|
+
const type = toItemType(candidate.frontMatter.type, runtime.typeNames);
|
|
329
|
+
const located = await locateItem(
|
|
330
|
+
runtime.pmRoot,
|
|
331
|
+
id,
|
|
332
|
+
runtime.settings.id_prefix,
|
|
333
|
+
runtime.settings.item_format,
|
|
334
|
+
runtime.typeToFolder,
|
|
335
|
+
);
|
|
336
|
+
if (located) {
|
|
337
|
+
return { warning: `todos_import_item_exists:${id}` };
|
|
338
|
+
}
|
|
339
|
+
const itemPath = getItemPath(runtime.pmRoot, type, id, runtime.settings.item_format, runtime.typeToFolder);
|
|
340
|
+
|
|
341
|
+
const afterDocument = canonicalDocument({
|
|
342
|
+
front_matter: normalizeFrontMatter({
|
|
343
|
+
id,
|
|
344
|
+
title,
|
|
345
|
+
description: toNonEmptyString(candidate.frontMatter.description) ?? "",
|
|
346
|
+
type,
|
|
347
|
+
status: toStatus(candidate.frontMatter.status),
|
|
348
|
+
priority: toPriority(candidate.frontMatter.priority),
|
|
349
|
+
confidence: toConfidence(candidate.frontMatter.confidence),
|
|
350
|
+
tags: toTags(candidate.frontMatter.tags),
|
|
351
|
+
created_at: createdAt,
|
|
352
|
+
updated_at: updatedAt,
|
|
353
|
+
deadline: toIsoString(candidate.frontMatter.deadline),
|
|
354
|
+
assignee: toNonEmptyString(candidate.frontMatter.assignee),
|
|
355
|
+
author: toNonEmptyString(candidate.frontMatter.author) ?? runtime.author,
|
|
356
|
+
estimated_minutes: toEstimatedMinutes(candidate.frontMatter.estimated_minutes),
|
|
357
|
+
acceptance_criteria: toNonEmptyString(candidate.frontMatter.acceptance_criteria),
|
|
358
|
+
definition_of_ready: toNonEmptyString(candidate.frontMatter.definition_of_ready),
|
|
359
|
+
order: toInteger(candidate.frontMatter.order),
|
|
360
|
+
goal: toNonEmptyString(candidate.frontMatter.goal),
|
|
361
|
+
objective: toNonEmptyString(candidate.frontMatter.objective),
|
|
362
|
+
value: toNonEmptyString(candidate.frontMatter.value),
|
|
363
|
+
impact: toNonEmptyString(candidate.frontMatter.impact),
|
|
364
|
+
outcome: toNonEmptyString(candidate.frontMatter.outcome),
|
|
365
|
+
why_now: toNonEmptyString(candidate.frontMatter.why_now),
|
|
366
|
+
parent: toNonEmptyString(candidate.frontMatter.parent),
|
|
367
|
+
reviewer: toNonEmptyString(candidate.frontMatter.reviewer),
|
|
368
|
+
risk: toNormalizedEnum(candidate.frontMatter.risk, RISK_VALUES),
|
|
369
|
+
sprint: toNonEmptyString(candidate.frontMatter.sprint),
|
|
370
|
+
release: toNonEmptyString(candidate.frontMatter.release),
|
|
371
|
+
blocked_by: toNonEmptyString(candidate.frontMatter.blocked_by),
|
|
372
|
+
blocked_reason: toNonEmptyString(candidate.frontMatter.blocked_reason),
|
|
373
|
+
unblock_note: toNonEmptyString(candidate.frontMatter.unblock_note),
|
|
374
|
+
reporter: toNonEmptyString(candidate.frontMatter.reporter),
|
|
375
|
+
severity: toNormalizedEnum(candidate.frontMatter.severity, ISSUE_SEVERITY_VALUES),
|
|
376
|
+
environment: toNonEmptyString(candidate.frontMatter.environment),
|
|
377
|
+
repro_steps: toNonEmptyString(candidate.frontMatter.repro_steps),
|
|
378
|
+
resolution: toNonEmptyString(candidate.frontMatter.resolution),
|
|
379
|
+
expected_result: toNonEmptyString(candidate.frontMatter.expected_result),
|
|
380
|
+
actual_result: toNonEmptyString(candidate.frontMatter.actual_result),
|
|
381
|
+
affected_version: toNonEmptyString(candidate.frontMatter.affected_version),
|
|
382
|
+
fixed_version: toNonEmptyString(candidate.frontMatter.fixed_version),
|
|
383
|
+
component: toNonEmptyString(candidate.frontMatter.component),
|
|
384
|
+
regression: toBoolean(candidate.frontMatter.regression),
|
|
385
|
+
customer_impact: toNonEmptyString(candidate.frontMatter.customer_impact),
|
|
386
|
+
close_reason: toNonEmptyString(candidate.frontMatter.close_reason),
|
|
387
|
+
dependencies: Array.isArray(candidate.frontMatter.dependencies) ? (candidate.frontMatter.dependencies as any[]) : undefined,
|
|
388
|
+
comments: Array.isArray(candidate.frontMatter.comments) ? (candidate.frontMatter.comments as any[]) : undefined,
|
|
389
|
+
notes: Array.isArray(candidate.frontMatter.notes) ? (candidate.frontMatter.notes as any[]) : undefined,
|
|
390
|
+
learnings: Array.isArray(candidate.frontMatter.learnings) ? (candidate.frontMatter.learnings as any[]) : undefined,
|
|
391
|
+
files: Array.isArray(candidate.frontMatter.files) ? (candidate.frontMatter.files as any[]) : undefined,
|
|
392
|
+
docs: Array.isArray(candidate.frontMatter.docs) ? (candidate.frontMatter.docs as any[]) : undefined,
|
|
393
|
+
tests: Array.isArray(candidate.frontMatter.tests) ? (candidate.frontMatter.tests as any[]) : undefined,
|
|
394
|
+
} as ItemFrontMatter),
|
|
395
|
+
body: candidate.body,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const historyPath = getHistoryPath(runtime.pmRoot, id);
|
|
399
|
+
let writeWarnings: string[] = [];
|
|
400
|
+
try {
|
|
401
|
+
const releaseLock = await acquireLock(runtime.pmRoot, id, runtime.settings.locks.ttl_seconds, runtime.author);
|
|
402
|
+
try {
|
|
403
|
+
await writeFileAtomic(itemPath, serializeItemDocument(afterDocument, { format: runtime.settings.item_format }));
|
|
404
|
+
try {
|
|
405
|
+
const historyEntry = createHistoryEntry({
|
|
406
|
+
nowIso: nowIso(),
|
|
407
|
+
author: runtime.author,
|
|
408
|
+
op: "import",
|
|
409
|
+
before: emptyDocument(),
|
|
410
|
+
after: afterDocument,
|
|
411
|
+
message: runtime.message,
|
|
412
|
+
});
|
|
413
|
+
await appendHistoryEntry(historyPath, historyEntry);
|
|
414
|
+
writeWarnings = [
|
|
415
|
+
...(await runActiveOnWriteHooks({
|
|
416
|
+
path: itemPath,
|
|
417
|
+
scope: "project",
|
|
418
|
+
op: "import",
|
|
419
|
+
})),
|
|
420
|
+
...(await runActiveOnWriteHooks({
|
|
421
|
+
path: historyPath,
|
|
422
|
+
scope: "project",
|
|
423
|
+
op: "import:history",
|
|
424
|
+
})),
|
|
425
|
+
];
|
|
426
|
+
} catch (error: unknown) {
|
|
427
|
+
await removeFileIfExists(itemPath);
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
} finally {
|
|
431
|
+
await releaseLock();
|
|
432
|
+
}
|
|
433
|
+
} catch (error: unknown) {
|
|
434
|
+
if (error instanceof PmCliError && error.exitCode === EXIT_CODE.CONFLICT) {
|
|
435
|
+
return { warning: `todos_import_lock_conflict:${id}` };
|
|
436
|
+
}
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return { id, writeWarnings };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function runTodosImport(options: TodosImportOptions, global: GlobalOptions): Promise<TodosImportResult> {
|
|
444
|
+
const pmRoot = resolvePmRoot(process.cwd(), global.path);
|
|
445
|
+
await ensureInitHasRun(pmRoot);
|
|
446
|
+
|
|
447
|
+
const settings = await readSettings(pmRoot);
|
|
448
|
+
const typeRegistry = resolveItemTypeRegistry(settings, getActiveExtensionRegistrations());
|
|
449
|
+
const folder = toNonEmptyString(options.folder) ?? DEFAULT_TODOS_FOLDER;
|
|
450
|
+
const sourceFolder = resolveFolderPath(folder);
|
|
451
|
+
|
|
452
|
+
let entries: Dirent[];
|
|
453
|
+
try {
|
|
454
|
+
entries = await fs.readdir(sourceFolder, { withFileTypes: true });
|
|
455
|
+
} catch {
|
|
456
|
+
throw new PmCliError(`Todos source folder not found at ${sourceFolder}`, EXIT_CODE.NOT_FOUND);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const author = selectAuthor(toNonEmptyString(options.author), settings.author_default);
|
|
460
|
+
const message = toNonEmptyString(options.message) ?? "Import from todos markdown";
|
|
461
|
+
const warnings: string[] = [
|
|
462
|
+
...(await runActiveOnReadHooks({
|
|
463
|
+
path: sourceFolder,
|
|
464
|
+
scope: "project",
|
|
465
|
+
})),
|
|
466
|
+
];
|
|
467
|
+
const ids: string[] = [];
|
|
468
|
+
let imported = 0;
|
|
469
|
+
let skipped = 0;
|
|
470
|
+
|
|
471
|
+
const markdownFiles = entries
|
|
472
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
|
|
473
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
474
|
+
|
|
475
|
+
const runtime: TodosImportRuntime = {
|
|
476
|
+
pmRoot,
|
|
477
|
+
sourceFolder,
|
|
478
|
+
settings,
|
|
479
|
+
typeNames: typeRegistry.types,
|
|
480
|
+
typeToFolder: typeRegistry.type_to_folder,
|
|
481
|
+
author,
|
|
482
|
+
message,
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
for (const entry of markdownFiles) {
|
|
486
|
+
const candidate = await readTodoCandidate(sourceFolder, entry);
|
|
487
|
+
if ("warning" in candidate) {
|
|
488
|
+
warnings.push(candidate.warning);
|
|
489
|
+
skipped += 1;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
warnings.push(...candidate.readWarnings);
|
|
493
|
+
|
|
494
|
+
const importedCandidate = await importTodoCandidate(candidate, runtime);
|
|
495
|
+
if ("warning" in importedCandidate) {
|
|
496
|
+
warnings.push(importedCandidate.warning);
|
|
497
|
+
skipped += 1;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
warnings.push(...importedCandidate.writeWarnings);
|
|
501
|
+
|
|
502
|
+
ids.push(importedCandidate.id);
|
|
503
|
+
imported += 1;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
ok: true,
|
|
508
|
+
folder,
|
|
509
|
+
imported,
|
|
510
|
+
skipped,
|
|
511
|
+
ids,
|
|
512
|
+
warnings,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export async function runTodosExport(options: TodosExportOptions, global: GlobalOptions): Promise<TodosExportResult> {
|
|
517
|
+
const pmRoot = resolvePmRoot(process.cwd(), global.path);
|
|
518
|
+
await ensureInitHasRun(pmRoot);
|
|
519
|
+
|
|
520
|
+
const settings = await readSettings(pmRoot);
|
|
521
|
+
const typeRegistry = resolveItemTypeRegistry(settings, getActiveExtensionRegistrations());
|
|
522
|
+
const folder = toNonEmptyString(options.folder) ?? DEFAULT_TODOS_FOLDER;
|
|
523
|
+
const destinationFolder = resolveFolderPath(folder);
|
|
524
|
+
await fs.mkdir(destinationFolder, { recursive: true });
|
|
525
|
+
|
|
526
|
+
const warnings: string[] = [];
|
|
527
|
+
const ids: string[] = [];
|
|
528
|
+
let exported = 0;
|
|
529
|
+
const items = await listAllFrontMatter(pmRoot, settings.item_format, typeRegistry.type_to_folder);
|
|
530
|
+
const sorted = [...items].sort((left, right) => left.id.localeCompare(right.id));
|
|
531
|
+
|
|
532
|
+
for (const item of sorted) {
|
|
533
|
+
const located = await locateItem(pmRoot, item.id, settings.id_prefix, settings.item_format, typeRegistry.type_to_folder);
|
|
534
|
+
if (!located) {
|
|
535
|
+
warnings.push(`todos_export_missing_item:${item.id}`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const { document } = await readLocatedItem(located);
|
|
541
|
+
const todoFrontMatter: Record<string, unknown> = { ...document.front_matter };
|
|
542
|
+
const frontMatter = JSON.stringify(todoFrontMatter, null, 2);
|
|
543
|
+
const body = normalizeBody(document.body);
|
|
544
|
+
const serialized = body.length > 0 ? `${frontMatter}\n\n${body}\n` : `${frontMatter}\n`;
|
|
545
|
+
const exportPath = path.join(destinationFolder, `${document.front_matter.id}.md`);
|
|
546
|
+
await writeFileAtomic(exportPath, serialized);
|
|
547
|
+
warnings.push(
|
|
548
|
+
...(await runActiveOnWriteHooks({
|
|
549
|
+
path: exportPath,
|
|
550
|
+
scope: "project",
|
|
551
|
+
op: "todos:export",
|
|
552
|
+
})),
|
|
553
|
+
);
|
|
554
|
+
ids.push(document.front_matter.id);
|
|
555
|
+
exported += 1;
|
|
556
|
+
} catch {
|
|
557
|
+
warnings.push(`todos_export_read_failed:${item.id}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
ok: true,
|
|
563
|
+
folder,
|
|
564
|
+
exported,
|
|
565
|
+
ids,
|
|
566
|
+
warnings,
|
|
567
|
+
};
|
|
568
|
+
}
|