mdkg 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/dist/cli.js +667 -11
- package/dist/commands/checkpoint.js +133 -0
- package/dist/commands/format.js +297 -0
- package/dist/commands/guide.js +22 -0
- package/dist/commands/index.js +17 -0
- package/dist/commands/init.js +111 -0
- package/dist/commands/list.js +52 -0
- package/dist/commands/new.js +279 -0
- package/dist/commands/next.js +75 -0
- package/dist/commands/node_card.js +17 -0
- package/dist/commands/pack.js +105 -0
- package/dist/commands/search.js +70 -0
- package/dist/commands/show.js +95 -0
- package/dist/commands/validate.js +229 -0
- package/dist/commands/workspace.js +101 -0
- package/dist/core/config.js +162 -0
- package/dist/core/migrate.js +30 -0
- package/dist/core/paths.js +14 -0
- package/dist/graph/edges.js +64 -0
- package/dist/graph/frontmatter.js +132 -0
- package/dist/graph/index_cache.js +50 -0
- package/dist/graph/indexer.js +144 -0
- package/dist/graph/node.js +225 -0
- package/dist/graph/staleness.js +31 -0
- package/dist/graph/template_schema.js +86 -0
- package/dist/graph/validate_graph.js +115 -0
- package/dist/graph/workspace_files.js +64 -0
- package/dist/init/AGENTS.md +43 -0
- package/dist/init/CLAUDE.md +37 -0
- package/dist/init/config.json +67 -0
- package/dist/init/core/core.md +12 -0
- package/dist/init/core/guide.md +99 -0
- package/dist/init/core/rule-1-mdkg-conventions.md +232 -0
- package/dist/init/core/rule-2-context-pack-rules.md +186 -0
- package/dist/init/core/rule-3-cli-contract.md +177 -0
- package/dist/init/core/rule-4-repo-safety-and-ignores.md +97 -0
- package/dist/init/core/rule-5-release-and-versioning.md +82 -0
- package/dist/init/core/rule-6-templates-and-schemas.md +186 -0
- package/dist/init/templates/default/bug.md +54 -0
- package/dist/init/templates/default/chk.md +55 -0
- package/dist/init/templates/default/dec.md +38 -0
- package/dist/init/templates/default/edd.md +50 -0
- package/dist/init/templates/default/epic.md +46 -0
- package/dist/init/templates/default/feat.md +35 -0
- package/dist/init/templates/default/prd.md +59 -0
- package/dist/init/templates/default/prop.md +45 -0
- package/dist/init/templates/default/rule.md +33 -0
- package/dist/init/templates/default/task.md +53 -0
- package/dist/init/templates/default/test.md +49 -0
- package/dist/pack/export_json.js +38 -0
- package/dist/pack/export_md.js +93 -0
- package/dist/pack/export_toon.js +7 -0
- package/dist/pack/export_xml.js +73 -0
- package/dist/pack/order.js +162 -0
- package/dist/pack/pack.js +181 -0
- package/dist/pack/types.js +2 -0
- package/dist/pack/verbose_core.js +23 -0
- package/dist/templates/loader.js +82 -0
- package/dist/util/argparse.js +154 -0
- package/dist/util/date.js +9 -0
- package/dist/util/errors.js +12 -0
- package/dist/util/filter.js +26 -0
- package/dist/util/output.js +50 -0
- package/dist/util/qid.js +54 -0
- package/dist/util/sort.js +40 -0
- package/package.json +18 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runCheckpointNewCommand = runCheckpointNewCommand;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const config_1 = require("../core/config");
|
|
10
|
+
const index_cache_1 = require("../graph/index_cache");
|
|
11
|
+
const loader_1 = require("../templates/loader");
|
|
12
|
+
const date_1 = require("../util/date");
|
|
13
|
+
const errors_1 = require("../util/errors");
|
|
14
|
+
const ID_RE = /^[a-z]+-[0-9]+$/;
|
|
15
|
+
const ID_REF_RE = /^([a-z][a-z0-9_]*:)?[a-z]+-[0-9]+$/;
|
|
16
|
+
function parseCsvList(raw) {
|
|
17
|
+
if (!raw) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
return raw
|
|
21
|
+
.split(",")
|
|
22
|
+
.map((value) => value.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
function normalizeId(value, key) {
|
|
26
|
+
const normalized = value.toLowerCase();
|
|
27
|
+
if (!ID_RE.test(normalized) && normalized !== "rule-guide") {
|
|
28
|
+
throw new errors_1.UsageError(`${key} entries must match <prefix>-<number>: ${value}`);
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
function normalizeIdRef(value, key) {
|
|
33
|
+
const normalized = value.toLowerCase();
|
|
34
|
+
if (!ID_REF_RE.test(normalized)) {
|
|
35
|
+
throw new errors_1.UsageError(`${key} entries must match <id> or <ws>:<id>: ${value}`);
|
|
36
|
+
}
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
function nextCheckpointId(index, ws) {
|
|
40
|
+
let max = 0;
|
|
41
|
+
for (const node of Object.values(index.nodes)) {
|
|
42
|
+
if (node.ws !== ws) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const match = /^chk-(\d+)$/.exec(node.id);
|
|
46
|
+
if (!match) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const parsed = Number.parseInt(match[1] ?? "", 10);
|
|
50
|
+
if (Number.isInteger(parsed) && parsed > max) {
|
|
51
|
+
max = parsed;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return `chk-${max + 1}`;
|
|
55
|
+
}
|
|
56
|
+
function slugifyTitle(title) {
|
|
57
|
+
const slug = title
|
|
58
|
+
.trim()
|
|
59
|
+
.toLowerCase()
|
|
60
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
61
|
+
.replace(/^-+|-+$/g, "")
|
|
62
|
+
.replace(/-+/g, "-");
|
|
63
|
+
if (!slug) {
|
|
64
|
+
return "checkpoint";
|
|
65
|
+
}
|
|
66
|
+
const maxLen = 80;
|
|
67
|
+
return slug.length > maxLen ? slug.slice(0, maxLen).replace(/-+$/g, "") : slug;
|
|
68
|
+
}
|
|
69
|
+
function normalizeWorkspace(value) {
|
|
70
|
+
if (!value) {
|
|
71
|
+
return "root";
|
|
72
|
+
}
|
|
73
|
+
const normalized = value.toLowerCase();
|
|
74
|
+
if (normalized === "all") {
|
|
75
|
+
throw new errors_1.UsageError("--ws all is not valid for checkpoint creation");
|
|
76
|
+
}
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
function runCheckpointNewCommand(options) {
|
|
80
|
+
const title = options.title.trim();
|
|
81
|
+
if (!title) {
|
|
82
|
+
throw new errors_1.UsageError("checkpoint title cannot be empty");
|
|
83
|
+
}
|
|
84
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
85
|
+
const ws = normalizeWorkspace(options.ws);
|
|
86
|
+
if (!config.workspaces[ws]) {
|
|
87
|
+
throw new errors_1.NotFoundError(`workspace not found: ${ws}`);
|
|
88
|
+
}
|
|
89
|
+
const status = (options.status ?? "backlog").toLowerCase();
|
|
90
|
+
const allowedStatuses = new Set(config.work.status_enum.map((value) => value.toLowerCase()));
|
|
91
|
+
if (!allowedStatuses.has(status)) {
|
|
92
|
+
throw new errors_1.UsageError(`--status must be one of ${Array.from(allowedStatuses).join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
const priorityMin = config.work.priority_min;
|
|
95
|
+
const priorityMax = config.work.priority_max;
|
|
96
|
+
const priority = options.priority ?? priorityMax;
|
|
97
|
+
if (!Number.isInteger(priority) || priority < priorityMin || priority > priorityMax) {
|
|
98
|
+
throw new errors_1.UsageError(`--priority must be between ${priorityMin} and ${priorityMax}`);
|
|
99
|
+
}
|
|
100
|
+
const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
|
|
101
|
+
const id = nextCheckpointId(index, ws);
|
|
102
|
+
const slug = slugifyTitle(title);
|
|
103
|
+
const fileName = `${id}-${slug}.md`;
|
|
104
|
+
const wsEntry = config.workspaces[ws];
|
|
105
|
+
const workDir = path_1.default.resolve(options.root, wsEntry.path, wsEntry.mdkg_dir, "work");
|
|
106
|
+
const filePath = path_1.default.join(workDir, fileName);
|
|
107
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
108
|
+
throw new errors_1.UsageError(`checkpoint file already exists: ${path_1.default.relative(options.root, filePath)}`);
|
|
109
|
+
}
|
|
110
|
+
const relates = parseCsvList(options.relates).map((value) => normalizeIdRef(value, "--relates"));
|
|
111
|
+
for (const target of relates) {
|
|
112
|
+
const qid = target.includes(":") ? target : `${ws}:${target}`;
|
|
113
|
+
if (!index.nodes[qid]) {
|
|
114
|
+
throw new errors_1.NotFoundError(`related node not found: ${target}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const scope = parseCsvList(options.scope).map((value) => normalizeId(value, "--scope"));
|
|
118
|
+
const today = (0, date_1.formatDate)(new Date());
|
|
119
|
+
const template = (0, loader_1.loadTemplate)(options.root, config, "checkpoint", options.template);
|
|
120
|
+
const content = (0, loader_1.renderTemplate)(template, {
|
|
121
|
+
id,
|
|
122
|
+
title,
|
|
123
|
+
status,
|
|
124
|
+
priority,
|
|
125
|
+
created: today,
|
|
126
|
+
updated: today,
|
|
127
|
+
relates,
|
|
128
|
+
scope,
|
|
129
|
+
});
|
|
130
|
+
fs_1.default.mkdirSync(workDir, { recursive: true });
|
|
131
|
+
fs_1.default.writeFileSync(filePath, content, "utf8");
|
|
132
|
+
console.log(`checkpoint created: ${ws}:${id} (${path_1.default.relative(options.root, filePath)})`);
|
|
133
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runFormatCommand = runFormatCommand;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const config_1 = require("../core/config");
|
|
10
|
+
const frontmatter_1 = require("../graph/frontmatter");
|
|
11
|
+
const template_schema_1 = require("../graph/template_schema");
|
|
12
|
+
const node_1 = require("../graph/node");
|
|
13
|
+
const workspace_files_1 = require("../graph/workspace_files");
|
|
14
|
+
const errors_1 = require("../util/errors");
|
|
15
|
+
const date_1 = require("../util/date");
|
|
16
|
+
const ID_RE = /^[a-z]+-[0-9]+$/;
|
|
17
|
+
const ID_REF_RE = /^([a-z][a-z0-9_]*:)?[a-z]+-[0-9]+$/;
|
|
18
|
+
const DEC_ID_RE = /^dec-[0-9]+$/;
|
|
19
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
20
|
+
const ID_LIST_KEYS = new Set(["refs", "scope"]);
|
|
21
|
+
const ID_REF_LIST_KEYS = new Set(["relates", "blocked_by", "blocks"]);
|
|
22
|
+
const ID_REF_SCALAR_KEYS = new Set(["epic", "parent", "prev", "next"]);
|
|
23
|
+
const PRESERVE_CASE_LIST_KEYS = new Set(["links", "artifacts"]);
|
|
24
|
+
function isValidId(value) {
|
|
25
|
+
return ID_RE.test(value) || value === "rule-guide";
|
|
26
|
+
}
|
|
27
|
+
function isCoreListFile(filePath) {
|
|
28
|
+
return path_1.default.basename(filePath) === "core.md" && path_1.default.basename(path_1.default.dirname(filePath)) === "core";
|
|
29
|
+
}
|
|
30
|
+
function normalizeScalar(value) {
|
|
31
|
+
return value.trim();
|
|
32
|
+
}
|
|
33
|
+
function sortList(values) {
|
|
34
|
+
return values.slice().sort((a, b) => {
|
|
35
|
+
const aLower = a.toLowerCase();
|
|
36
|
+
const bLower = b.toLowerCase();
|
|
37
|
+
if (aLower < bLower) {
|
|
38
|
+
return -1;
|
|
39
|
+
}
|
|
40
|
+
if (aLower > bLower) {
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
if (a < b) {
|
|
44
|
+
return -1;
|
|
45
|
+
}
|
|
46
|
+
if (a > b) {
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function normalizeList(values, key, errors, filePath) {
|
|
53
|
+
const trimmed = values.map((value) => normalizeScalar(value));
|
|
54
|
+
const shouldLowercase = !PRESERVE_CASE_LIST_KEYS.has(key);
|
|
55
|
+
const normalized = shouldLowercase ? trimmed.map((value) => value.toLowerCase()) : trimmed;
|
|
56
|
+
for (const entry of normalized) {
|
|
57
|
+
if (!entry) {
|
|
58
|
+
errors.push(`${filePath}: ${key} entries must be non-empty`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ID_LIST_KEYS.has(key) && !isValidId(entry)) {
|
|
62
|
+
errors.push(`${filePath}: ${key} entries must match <prefix>-<number>`);
|
|
63
|
+
}
|
|
64
|
+
if (ID_REF_LIST_KEYS.has(key) && !ID_REF_RE.test(entry)) {
|
|
65
|
+
errors.push(`${filePath}: ${key} entries must be valid id references`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return sortList(normalized);
|
|
69
|
+
}
|
|
70
|
+
function normalizeIdRef(value, key, errors, filePath) {
|
|
71
|
+
const normalized = normalizeScalar(value).toLowerCase();
|
|
72
|
+
if (!ID_REF_RE.test(normalized)) {
|
|
73
|
+
errors.push(`${filePath}: ${key} must be a valid id reference`);
|
|
74
|
+
}
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
77
|
+
function normalizeFrontmatterValue(key, value, schema, errors, filePath) {
|
|
78
|
+
const expected = schema.keyKinds[key];
|
|
79
|
+
if (expected === "list") {
|
|
80
|
+
if (!Array.isArray(value)) {
|
|
81
|
+
errors.push(`${filePath}: ${key} must be a list`);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
return normalizeList(value, key, errors, filePath);
|
|
85
|
+
}
|
|
86
|
+
if (expected === "boolean") {
|
|
87
|
+
if (typeof value !== "boolean") {
|
|
88
|
+
errors.push(`${filePath}: ${key} must be a boolean`);
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(value) || typeof value === "boolean") {
|
|
94
|
+
errors.push(`${filePath}: ${key} must be a string`);
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
return normalizeScalar(value);
|
|
98
|
+
}
|
|
99
|
+
function normalizeFrontmatter(frontmatter, schema, type, workStatusEnum, priorityMin, priorityMax, filePath) {
|
|
100
|
+
const errors = [];
|
|
101
|
+
const normalized = {};
|
|
102
|
+
for (const key of Object.keys(frontmatter)) {
|
|
103
|
+
if (!schema.allowedKeys.has(key)) {
|
|
104
|
+
errors.push(`${filePath}: unknown key: ${key}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const key of schema.allowedKeys) {
|
|
108
|
+
const value = frontmatter[key];
|
|
109
|
+
if (value === undefined) {
|
|
110
|
+
if (schema.listKeys.has(key)) {
|
|
111
|
+
normalized[key] = [];
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const normalizedValue = normalizeFrontmatterValue(key, value, schema, errors, filePath);
|
|
116
|
+
if (normalizedValue !== undefined) {
|
|
117
|
+
normalized[key] = normalizedValue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const idValue = normalized.id;
|
|
121
|
+
if (typeof idValue !== "string") {
|
|
122
|
+
errors.push(`${filePath}: id is required`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const normalizedId = idValue.toLowerCase();
|
|
126
|
+
if (!isValidId(normalizedId)) {
|
|
127
|
+
errors.push(`${filePath}: id must match <prefix>-<number>`);
|
|
128
|
+
}
|
|
129
|
+
normalized.id = normalizedId;
|
|
130
|
+
}
|
|
131
|
+
normalized.type = type;
|
|
132
|
+
if (typeof normalized.title !== "string" || normalized.title.trim().length === 0) {
|
|
133
|
+
errors.push(`${filePath}: title is required`);
|
|
134
|
+
}
|
|
135
|
+
const createdValue = normalized.created;
|
|
136
|
+
if (typeof createdValue !== "string") {
|
|
137
|
+
errors.push(`${filePath}: created is required`);
|
|
138
|
+
}
|
|
139
|
+
else if (!DATE_RE.test(createdValue)) {
|
|
140
|
+
errors.push(`${filePath}: created must be YYYY-MM-DD`);
|
|
141
|
+
}
|
|
142
|
+
const updatedValue = normalized.updated;
|
|
143
|
+
if (typeof updatedValue !== "string") {
|
|
144
|
+
errors.push(`${filePath}: updated is required`);
|
|
145
|
+
}
|
|
146
|
+
else if (!DATE_RE.test(updatedValue)) {
|
|
147
|
+
errors.push(`${filePath}: updated must be YYYY-MM-DD`);
|
|
148
|
+
}
|
|
149
|
+
if (node_1.WORK_TYPES.has(type)) {
|
|
150
|
+
const statusValue = normalized.status;
|
|
151
|
+
if (typeof statusValue !== "string") {
|
|
152
|
+
errors.push(`${filePath}: status is required for work items`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const normalizedStatus = statusValue.toLowerCase();
|
|
156
|
+
const allowed = new Set(workStatusEnum.map((value) => value.toLowerCase()));
|
|
157
|
+
if (!allowed.has(normalizedStatus)) {
|
|
158
|
+
errors.push(`${filePath}: status must be one of ${Array.from(allowed).join(", ")}`);
|
|
159
|
+
}
|
|
160
|
+
normalized.status = normalizedStatus;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (node_1.DEC_TYPES.has(type)) {
|
|
164
|
+
const statusValue = normalized.status;
|
|
165
|
+
if (typeof statusValue !== "string") {
|
|
166
|
+
errors.push(`${filePath}: status is required for decision records`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const normalizedStatus = statusValue.toLowerCase();
|
|
170
|
+
if (!["proposed", "accepted", "rejected", "superseded"].includes(normalizedStatus)) {
|
|
171
|
+
errors.push(`${filePath}: status must be one of proposed, accepted, rejected, superseded`);
|
|
172
|
+
}
|
|
173
|
+
normalized.status = normalizedStatus;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (normalized.priority !== undefined) {
|
|
177
|
+
const priorityValue = normalized.priority;
|
|
178
|
+
if (typeof priorityValue !== "string") {
|
|
179
|
+
errors.push(`${filePath}: priority must be an integer`);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const parsed = Number.parseInt(priorityValue, 10);
|
|
183
|
+
if (!Number.isInteger(parsed)) {
|
|
184
|
+
errors.push(`${filePath}: priority must be an integer`);
|
|
185
|
+
}
|
|
186
|
+
else if (parsed < priorityMin || parsed > priorityMax) {
|
|
187
|
+
errors.push(`${filePath}: priority must be between ${priorityMin} and ${priorityMax}`);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
normalized.priority = String(parsed);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (normalized.priority !== undefined && !node_1.WORK_TYPES.has(type)) {
|
|
195
|
+
errors.push(`${filePath}: priority is only allowed for work items`);
|
|
196
|
+
}
|
|
197
|
+
for (const key of ID_REF_SCALAR_KEYS) {
|
|
198
|
+
const value = normalized[key];
|
|
199
|
+
if (typeof value === "string") {
|
|
200
|
+
normalized[key] = normalizeIdRef(value, key, errors, filePath);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (typeof normalized.supersedes === "string") {
|
|
204
|
+
const normalizedValue = normalizeScalar(normalized.supersedes).toLowerCase();
|
|
205
|
+
if (!DEC_ID_RE.test(normalizedValue)) {
|
|
206
|
+
errors.push(`${filePath}: supersedes must be a dec-# id`);
|
|
207
|
+
}
|
|
208
|
+
normalized.supersedes = normalizedValue;
|
|
209
|
+
}
|
|
210
|
+
if (normalized.supersedes !== undefined && !node_1.DEC_TYPES.has(type)) {
|
|
211
|
+
errors.push(`${filePath}: supersedes is only allowed for decision records`);
|
|
212
|
+
}
|
|
213
|
+
if (!node_1.WORK_TYPES.has(type) && !node_1.DEC_TYPES.has(type) && normalized.status !== undefined) {
|
|
214
|
+
errors.push(`${filePath}: status is not allowed for this type`);
|
|
215
|
+
}
|
|
216
|
+
return { normalized, errors };
|
|
217
|
+
}
|
|
218
|
+
function runFormatCommand(options) {
|
|
219
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
220
|
+
const templateSchemas = (0, template_schema_1.loadTemplateSchemas)(options.root, config, node_1.ALLOWED_TYPES);
|
|
221
|
+
const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(options.root, config);
|
|
222
|
+
const today = (0, date_1.formatDate)(options.now ?? new Date());
|
|
223
|
+
const errors = [];
|
|
224
|
+
const updates = [];
|
|
225
|
+
for (const files of Object.values(filesByAlias)) {
|
|
226
|
+
for (const filePath of files) {
|
|
227
|
+
if (isCoreListFile(filePath)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
let content = "";
|
|
231
|
+
try {
|
|
232
|
+
content = fs_1.default.readFileSync(filePath, "utf8");
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
236
|
+
errors.push(`${filePath}: failed to read file: ${message}`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
let parsed;
|
|
240
|
+
try {
|
|
241
|
+
parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
245
|
+
errors.push(message);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const typeValue = parsed.frontmatter.type;
|
|
249
|
+
if (typeof typeValue !== "string") {
|
|
250
|
+
errors.push(`${filePath}: type is required`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const type = typeValue.toLowerCase();
|
|
254
|
+
if (!node_1.ALLOWED_TYPES.has(type)) {
|
|
255
|
+
errors.push(`${filePath}: type must be one of ${Array.from(node_1.ALLOWED_TYPES).join(", ")}`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const schema = templateSchemas[type];
|
|
259
|
+
if (!schema) {
|
|
260
|
+
errors.push(`${filePath}: template schema missing for type ${type}`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const { normalized, errors: fmErrors } = normalizeFrontmatter(parsed.frontmatter, schema, type, config.work.status_enum, config.work.priority_min, config.work.priority_max, filePath);
|
|
264
|
+
if (fmErrors.length > 0) {
|
|
265
|
+
errors.push(...fmErrors);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const keepTrailingNewline = content.endsWith("\n");
|
|
269
|
+
const frontmatterLines = (0, frontmatter_1.formatFrontmatter)(normalized, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER);
|
|
270
|
+
const frontmatterBlock = ["---", ...frontmatterLines, "---"].join("\n");
|
|
271
|
+
let formattedContent = parsed.body.length > 0 ? `${frontmatterBlock}\n${parsed.body}` : frontmatterBlock;
|
|
272
|
+
if (keepTrailingNewline && !formattedContent.endsWith("\n")) {
|
|
273
|
+
formattedContent = `${formattedContent}\n`;
|
|
274
|
+
}
|
|
275
|
+
if (formattedContent !== content) {
|
|
276
|
+
normalized.updated = today;
|
|
277
|
+
const updatedLines = (0, frontmatter_1.formatFrontmatter)(normalized, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER);
|
|
278
|
+
const updatedBlock = ["---", ...updatedLines, "---"].join("\n");
|
|
279
|
+
let updatedContent = parsed.body.length > 0 ? `${updatedBlock}\n${parsed.body}` : updatedBlock;
|
|
280
|
+
if (keepTrailingNewline && !updatedContent.endsWith("\n")) {
|
|
281
|
+
updatedContent = `${updatedContent}\n`;
|
|
282
|
+
}
|
|
283
|
+
updates.push({ filePath, content: updatedContent });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (errors.length > 0) {
|
|
288
|
+
for (const error of errors) {
|
|
289
|
+
console.error(error);
|
|
290
|
+
}
|
|
291
|
+
throw new errors_1.ValidationError(`format failed with ${errors.length} error(s)`);
|
|
292
|
+
}
|
|
293
|
+
for (const update of updates) {
|
|
294
|
+
fs_1.default.writeFileSync(update.filePath, update.content, "utf8");
|
|
295
|
+
}
|
|
296
|
+
console.log(`format updated ${updates.length} file(s)`);
|
|
297
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runGuideCommand = runGuideCommand;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const errors_1 = require("../util/errors");
|
|
10
|
+
function runGuideCommand(options) {
|
|
11
|
+
const guidePath = path_1.default.join(options.root, ".mdkg", "core", "guide.md");
|
|
12
|
+
if (!fs_1.default.existsSync(guidePath)) {
|
|
13
|
+
throw new errors_1.NotFoundError(`guide not found: ${guidePath}`);
|
|
14
|
+
}
|
|
15
|
+
const content = fs_1.default.readFileSync(guidePath, "utf8");
|
|
16
|
+
const trimmed = content.trimEnd();
|
|
17
|
+
if (trimmed.length === 0) {
|
|
18
|
+
console.log("");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(trimmed);
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runIndexCommand = runIndexCommand;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const config_1 = require("../core/config");
|
|
9
|
+
const indexer_1 = require("../graph/indexer");
|
|
10
|
+
const index_cache_1 = require("../graph/index_cache");
|
|
11
|
+
function runIndexCommand(options) {
|
|
12
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
13
|
+
const index = (0, indexer_1.buildIndex)(options.root, config, { tolerant: options.tolerant });
|
|
14
|
+
const outputPath = path_1.default.resolve(options.root, config.index.global_index_path);
|
|
15
|
+
(0, index_cache_1.writeIndex)(outputPath, index);
|
|
16
|
+
console.log(`index written: ${path_1.default.relative(options.root, outputPath)}`);
|
|
17
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runInitCommand = runInitCommand;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const errors_1 = require("../util/errors");
|
|
10
|
+
const DEFAULT_SEED_SUBDIR = path_1.default.resolve(__dirname, "..", "init");
|
|
11
|
+
function listFiles(dir) {
|
|
12
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
16
|
+
const files = [];
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
files.push(...listFiles(fullPath));
|
|
21
|
+
}
|
|
22
|
+
else if (entry.isFile()) {
|
|
23
|
+
files.push(fullPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
function copySeedFile(src, dest, force, stats) {
|
|
29
|
+
if (fs_1.default.existsSync(dest) && !force) {
|
|
30
|
+
stats.skipped += 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
|
|
34
|
+
fs_1.default.copyFileSync(src, dest);
|
|
35
|
+
stats.created += 1;
|
|
36
|
+
}
|
|
37
|
+
function copySeedDir(srcDir, destDir, force, stats) {
|
|
38
|
+
if (!fs_1.default.existsSync(srcDir)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const files = listFiles(srcDir);
|
|
42
|
+
for (const filePath of files) {
|
|
43
|
+
const relPath = path_1.default.relative(srcDir, filePath);
|
|
44
|
+
const destPath = path_1.default.join(destDir, relPath);
|
|
45
|
+
copySeedFile(filePath, destPath, force, stats);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function appendIgnoreEntries(filePath, entries) {
|
|
49
|
+
const raw = fs_1.default.existsSync(filePath) ? fs_1.default.readFileSync(filePath, "utf8") : "";
|
|
50
|
+
const lines = raw.split(/\r?\n/);
|
|
51
|
+
const existing = new Set(lines.map((line) => line.trim()).filter(Boolean));
|
|
52
|
+
const additions = entries.filter((entry) => !existing.has(entry));
|
|
53
|
+
if (additions.length === 0) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const suffix = raw.length === 0 || raw.endsWith("\n") ? "" : "\n";
|
|
57
|
+
const updated = `${raw}${suffix}${additions.join("\n")}\n`;
|
|
58
|
+
fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
|
|
59
|
+
fs_1.default.writeFileSync(filePath, updated, "utf8");
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
function runInitCommand(options) {
|
|
63
|
+
const root = path_1.default.resolve(options.root);
|
|
64
|
+
const seedRoot = options.seedRoot ? path_1.default.resolve(options.seedRoot) : DEFAULT_SEED_SUBDIR;
|
|
65
|
+
const createAgents = Boolean(options.createAgents || options.createLlm);
|
|
66
|
+
const createClaude = Boolean(options.createClaude || options.createLlm);
|
|
67
|
+
const seedConfig = path_1.default.join(seedRoot, "config.json");
|
|
68
|
+
const seedCore = path_1.default.join(seedRoot, "core");
|
|
69
|
+
const seedTemplates = path_1.default.join(seedRoot, "templates");
|
|
70
|
+
const seedAgents = path_1.default.join(seedRoot, "AGENTS.md");
|
|
71
|
+
const seedClaude = path_1.default.join(seedRoot, "CLAUDE.md");
|
|
72
|
+
if (!fs_1.default.existsSync(seedConfig) || !fs_1.default.existsSync(seedCore) || !fs_1.default.existsSync(seedTemplates)) {
|
|
73
|
+
throw new errors_1.NotFoundError(`init assets not found at ${seedRoot} (try reinstalling mdkg)`);
|
|
74
|
+
}
|
|
75
|
+
if (createAgents && !fs_1.default.existsSync(seedAgents)) {
|
|
76
|
+
throw new errors_1.NotFoundError(`init assets missing AGENTS.md at ${seedRoot}`);
|
|
77
|
+
}
|
|
78
|
+
if (createClaude && !fs_1.default.existsSync(seedClaude)) {
|
|
79
|
+
throw new errors_1.NotFoundError(`init assets missing CLAUDE.md at ${seedRoot}`);
|
|
80
|
+
}
|
|
81
|
+
const mdkgDir = path_1.default.join(root, ".mdkg");
|
|
82
|
+
fs_1.default.mkdirSync(mdkgDir, { recursive: true });
|
|
83
|
+
fs_1.default.mkdirSync(path_1.default.join(mdkgDir, "work"), { recursive: true });
|
|
84
|
+
fs_1.default.mkdirSync(path_1.default.join(mdkgDir, "design"), { recursive: true });
|
|
85
|
+
const stats = { created: 0, skipped: 0 };
|
|
86
|
+
copySeedFile(seedConfig, path_1.default.join(mdkgDir, "config.json"), Boolean(options.force), stats);
|
|
87
|
+
copySeedDir(seedCore, path_1.default.join(mdkgDir, "core"), Boolean(options.force), stats);
|
|
88
|
+
copySeedDir(seedTemplates, path_1.default.join(mdkgDir, "templates"), Boolean(options.force), stats);
|
|
89
|
+
if (createAgents) {
|
|
90
|
+
copySeedFile(seedAgents, path_1.default.join(root, "AGENTS.md"), Boolean(options.force), stats);
|
|
91
|
+
}
|
|
92
|
+
if (createClaude) {
|
|
93
|
+
copySeedFile(seedClaude, path_1.default.join(root, "CLAUDE.md"), Boolean(options.force), stats);
|
|
94
|
+
}
|
|
95
|
+
if (options.updateGitignore) {
|
|
96
|
+
appendIgnoreEntries(path_1.default.join(root, ".gitignore"), [".mdkg/index/", ".mdkg/pack/"]);
|
|
97
|
+
}
|
|
98
|
+
if (options.updateNpmignore) {
|
|
99
|
+
appendIgnoreEntries(path_1.default.join(root, ".npmignore"), [".mdkg/", ".mdkg/index/", ".mdkg/pack/"]);
|
|
100
|
+
}
|
|
101
|
+
if (options.updateDockerignore) {
|
|
102
|
+
appendIgnoreEntries(path_1.default.join(root, ".dockerignore"), [".mdkg/"]);
|
|
103
|
+
}
|
|
104
|
+
console.log(`mdkg init complete: ${stats.created} file(s) created, ${stats.skipped} skipped`);
|
|
105
|
+
console.log("next:");
|
|
106
|
+
console.log(" mdkg index");
|
|
107
|
+
console.log(' mdkg new task "..." --status todo --priority 1');
|
|
108
|
+
console.log(" mdkg list --status todo");
|
|
109
|
+
console.log(" mdkg pack <id> --verbose");
|
|
110
|
+
console.log(" mdkg validate");
|
|
111
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runListCommand = runListCommand;
|
|
4
|
+
const config_1 = require("../core/config");
|
|
5
|
+
const index_cache_1 = require("../graph/index_cache");
|
|
6
|
+
const filter_1 = require("../util/filter");
|
|
7
|
+
const errors_1 = require("../util/errors");
|
|
8
|
+
const qid_1 = require("../util/qid");
|
|
9
|
+
const sort_1 = require("../util/sort");
|
|
10
|
+
const node_card_1 = require("./node_card");
|
|
11
|
+
function normalizeWorkspace(value) {
|
|
12
|
+
if (!value || value === "all") {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function runListCommand(options) {
|
|
18
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
19
|
+
const ws = normalizeWorkspace(options.ws);
|
|
20
|
+
if (ws && !config.workspaces[ws]) {
|
|
21
|
+
throw new errors_1.NotFoundError(`workspace not found: ${ws}`);
|
|
22
|
+
}
|
|
23
|
+
const { index, rebuilt, stale } = (0, index_cache_1.loadIndex)({
|
|
24
|
+
root: options.root,
|
|
25
|
+
config,
|
|
26
|
+
useCache: !options.noCache,
|
|
27
|
+
allowReindex: !options.noReindex,
|
|
28
|
+
});
|
|
29
|
+
if (stale && !rebuilt && !options.noCache) {
|
|
30
|
+
console.error("warning: index is stale; run mdkg index to refresh");
|
|
31
|
+
}
|
|
32
|
+
let epicQid;
|
|
33
|
+
if (options.epic) {
|
|
34
|
+
const resolved = (0, qid_1.resolveQid)(index, options.epic, ws);
|
|
35
|
+
if (resolved.status !== "ok") {
|
|
36
|
+
throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("epic", options.epic, resolved, ws));
|
|
37
|
+
}
|
|
38
|
+
epicQid = resolved.qid;
|
|
39
|
+
}
|
|
40
|
+
const filtered = (0, filter_1.filterNodes)(Object.values(index.nodes), {
|
|
41
|
+
ws,
|
|
42
|
+
type: options.type,
|
|
43
|
+
status: options.status,
|
|
44
|
+
epic: epicQid,
|
|
45
|
+
priority: options.priority,
|
|
46
|
+
blocked: options.blocked,
|
|
47
|
+
});
|
|
48
|
+
const sorted = (0, sort_1.sortNodesByQid)(filtered);
|
|
49
|
+
for (const node of sorted) {
|
|
50
|
+
console.log((0, node_card_1.formatNodeCard)(node));
|
|
51
|
+
}
|
|
52
|
+
}
|