mdkg 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/CONTRIBUTING.md +124 -0
  3. package/README.md +49 -14
  4. package/dist/cli.js +113 -32
  5. package/dist/commands/checkpoint.js +19 -2
  6. package/dist/commands/event.js +12 -0
  7. package/dist/commands/init.js +4 -0
  8. package/dist/commands/init_manifest.js +131 -0
  9. package/dist/commands/new.js +57 -21
  10. package/dist/commands/pack.js +14 -0
  11. package/dist/commands/query_output.js +2 -0
  12. package/dist/commands/search.js +8 -0
  13. package/dist/commands/show.js +7 -0
  14. package/dist/commands/skill.js +80 -12
  15. package/dist/commands/task.js +42 -12
  16. package/dist/commands/upgrade.js +286 -0
  17. package/dist/commands/validate.js +31 -3
  18. package/dist/commands/workspace.js +105 -13
  19. package/dist/core/config.js +217 -22
  20. package/dist/core/migrate.js +39 -5
  21. package/dist/core/version.js +31 -0
  22. package/dist/core/workspace_path.js +41 -0
  23. package/dist/graph/agent_file_types.js +392 -0
  24. package/dist/graph/edges.js +13 -10
  25. package/dist/graph/frontmatter.js +33 -0
  26. package/dist/graph/indexer.js +1 -0
  27. package/dist/graph/node.js +43 -16
  28. package/dist/graph/skills_indexer.js +14 -1
  29. package/dist/graph/template_schema.js +13 -126
  30. package/dist/graph/validate_graph.js +302 -2
  31. package/dist/init/AGENT_START.md +14 -2
  32. package/dist/init/CLI_COMMAND_MATRIX.md +49 -1
  33. package/dist/init/README.md +14 -0
  34. package/dist/init/core/rule-6-templates-and-schemas.md +1 -3
  35. package/dist/init/init-manifest.json +197 -0
  36. package/dist/init/legacy/v0.0.9-init-manifest.json +197 -0
  37. package/dist/init/skills/default/verify-close-and-checkpoint/SKILL.md +12 -11
  38. package/dist/init/templates/default/dispute.md +31 -0
  39. package/dist/init/templates/default/feedback.md +27 -0
  40. package/dist/init/templates/default/proposal.md +35 -0
  41. package/dist/init/templates/default/receipt.md +31 -0
  42. package/dist/init/templates/default/spec.md +43 -0
  43. package/dist/init/templates/default/work.md +44 -0
  44. package/dist/init/templates/default/work_order.md +32 -0
  45. package/dist/pack/export_json.js +3 -0
  46. package/dist/pack/export_md.js +9 -0
  47. package/dist/pack/export_xml.js +9 -0
  48. package/dist/pack/order.js +7 -0
  49. package/dist/pack/pack.js +1 -0
  50. package/dist/templates/loader.js +2 -2
  51. package/dist/util/argparse.js +2 -0
  52. package/dist/util/id.js +19 -0
  53. package/package.json +10 -2
  54. package/scripts/postinstall.js +89 -0
@@ -0,0 +1,286 @@
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.runUpgradeCommand = runUpgradeCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const migrate_1 = require("../core/migrate");
10
+ const config_1 = require("../core/config");
11
+ const paths_1 = require("../core/paths");
12
+ const version_1 = require("../core/version");
13
+ const errors_1 = require("../util/errors");
14
+ const init_manifest_1 = require("./init_manifest");
15
+ const skill_support_1 = require("./skill_support");
16
+ const skill_mirror_1 = require("./skill_mirror");
17
+ const DEFAULT_SEED_SUBDIR = path_1.default.resolve(__dirname, "..", "init");
18
+ const PROTECTED_CORE_DOCS = new Set([".mdkg/core/SOUL.md", ".mdkg/core/HUMAN.md"]);
19
+ const CREATE_ONLY_PRESERVED = new Set([".mdkg/core/core.md"]);
20
+ function seededInitEvent(nowIso) {
21
+ const event = {
22
+ ts: nowIso,
23
+ run_id: `upgrade-${nowIso.replace(/[^0-9]/g, "").slice(0, 14)}`,
24
+ workspace: "root",
25
+ agent: "mdkg",
26
+ kind: "RUN_STARTED",
27
+ status: "ok",
28
+ refs: ["edd-4"],
29
+ artifacts: [],
30
+ notes: "upgrade ensured agent event log",
31
+ redacted: true,
32
+ };
33
+ return `${JSON.stringify(event)}\n`;
34
+ }
35
+ function requireSeedAssets(seedRoot) {
36
+ for (const required of ["config.json", "README.md", "core", "templates"]) {
37
+ if (!fs_1.default.existsSync(path_1.default.join(seedRoot, required))) {
38
+ throw new errors_1.NotFoundError(`upgrade assets missing ${required} at ${seedRoot} (try reinstalling mdkg)`);
39
+ }
40
+ }
41
+ }
42
+ function isAgentWorkspace(root) {
43
+ return [
44
+ path_1.default.join(root, ".mdkg", "skills"),
45
+ path_1.default.join(root, ".agents", "skills"),
46
+ path_1.default.join(root, ".claude", "skills"),
47
+ path_1.default.join(root, ".mdkg", "work", "events", "events.jsonl"),
48
+ ].some((candidate) => fs_1.default.existsSync(candidate));
49
+ }
50
+ function copyFile(src, dest) {
51
+ fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
52
+ fs_1.default.copyFileSync(src, dest);
53
+ }
54
+ function writeFile(filePath, content) {
55
+ fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
56
+ fs_1.default.writeFileSync(filePath, content, "utf8");
57
+ }
58
+ function createSummary() {
59
+ return {
60
+ created: 0,
61
+ updated: 0,
62
+ migrated: 0,
63
+ synced: 0,
64
+ skipped: 0,
65
+ conflicted: 0,
66
+ unchanged: 0,
67
+ };
68
+ }
69
+ function record(summary, changes, change) {
70
+ changes.push(change);
71
+ switch (change.action) {
72
+ case "create":
73
+ summary.created += 1;
74
+ break;
75
+ case "update":
76
+ summary.updated += 1;
77
+ break;
78
+ case "migrate":
79
+ summary.migrated += 1;
80
+ break;
81
+ case "sync":
82
+ summary.synced += 1;
83
+ break;
84
+ case "conflict":
85
+ summary.skipped += 1;
86
+ summary.conflicted += 1;
87
+ break;
88
+ }
89
+ }
90
+ function buildKnownHashes(manifests) {
91
+ const hashes = new Map();
92
+ for (const manifest of manifests) {
93
+ if (!manifest) {
94
+ continue;
95
+ }
96
+ for (const file of manifest.files) {
97
+ if (!hashes.has(file.path)) {
98
+ hashes.set(file.path, new Set());
99
+ }
100
+ hashes.get(file.path)?.add(file.sha256);
101
+ }
102
+ }
103
+ return hashes;
104
+ }
105
+ function shouldIncludeFile(file, agentWorkspace) {
106
+ if (file.category === "default_skill") {
107
+ return agentWorkspace;
108
+ }
109
+ return file.category !== "config";
110
+ }
111
+ function planSeedFile(options) {
112
+ const destPath = path_1.default.join(options.root, options.file.path);
113
+ const srcPath = (0, init_manifest_1.seedSourcePath)(options.seedRoot, options.file);
114
+ const currentHash = fs_1.default.existsSync(destPath) && fs_1.default.statSync(destPath).isFile() ? (0, init_manifest_1.sha256File)(destPath) : undefined;
115
+ const nextHash = options.file.sha256;
116
+ if (!currentHash) {
117
+ record(options.summary, options.changes, {
118
+ path: options.file.path,
119
+ category: options.file.category,
120
+ action: "create",
121
+ reason: "missing managed init asset",
122
+ });
123
+ if (!options.dryRun) {
124
+ copyFile(srcPath, destPath);
125
+ }
126
+ options.managedCurrentFiles.push(options.file);
127
+ return true;
128
+ }
129
+ if (currentHash === nextHash) {
130
+ options.summary.unchanged += 1;
131
+ options.managedCurrentFiles.push(options.file);
132
+ return false;
133
+ }
134
+ if (CREATE_ONLY_PRESERVED.has(options.file.path)) {
135
+ options.summary.unchanged += 1;
136
+ return false;
137
+ }
138
+ if (PROTECTED_CORE_DOCS.has(options.file.path)) {
139
+ record(options.summary, options.changes, {
140
+ path: options.file.path,
141
+ category: options.file.category,
142
+ action: "conflict",
143
+ reason: "protected core document exists; local content preserved",
144
+ });
145
+ return false;
146
+ }
147
+ const known = options.knownHashes.get(options.file.path);
148
+ if (known?.has(currentHash)) {
149
+ record(options.summary, options.changes, {
150
+ path: options.file.path,
151
+ category: options.file.category,
152
+ action: "update",
153
+ reason: "matches a managed seed hash",
154
+ });
155
+ if (!options.dryRun) {
156
+ copyFile(srcPath, destPath);
157
+ }
158
+ options.managedCurrentFiles.push(options.file);
159
+ return true;
160
+ }
161
+ record(options.summary, options.changes, {
162
+ path: options.file.path,
163
+ category: options.file.category,
164
+ action: "conflict",
165
+ reason: "local changes detected; content preserved",
166
+ });
167
+ return false;
168
+ }
169
+ function migrateConfigIfNeeded(root, dryRun, summary, changes) {
170
+ const cfgPath = (0, paths_1.configPath)(root);
171
+ const raw = JSON.parse(fs_1.default.readFileSync(cfgPath, "utf8"));
172
+ const migrated = (0, migrate_1.migrateConfig)(raw);
173
+ (0, config_1.validateConfigSchema)(migrated.config);
174
+ if (migrated.from === migrated.to) {
175
+ summary.unchanged += 1;
176
+ return;
177
+ }
178
+ record(summary, changes, {
179
+ path: ".mdkg/config.json",
180
+ category: "config",
181
+ action: "migrate",
182
+ reason: `config schema_version ${migrated.from} -> ${migrated.to}`,
183
+ });
184
+ if (!dryRun) {
185
+ writeFile(cfgPath, `${JSON.stringify(migrated.config, null, 2)}\n`);
186
+ }
187
+ }
188
+ function ensureAgentRuntimeFiles(root, dryRun, summary, changes) {
189
+ const eventsPath = path_1.default.join(root, ".mdkg", "work", "events", "events.jsonl");
190
+ if (!fs_1.default.existsSync(eventsPath)) {
191
+ record(summary, changes, {
192
+ path: ".mdkg/work/events/events.jsonl",
193
+ category: "event_log",
194
+ action: "create",
195
+ reason: "agent workspace is missing event log",
196
+ });
197
+ if (!dryRun) {
198
+ writeFile(eventsPath, seededInitEvent(new Date().toISOString()));
199
+ }
200
+ }
201
+ else {
202
+ summary.unchanged += 1;
203
+ }
204
+ }
205
+ function emitHumanReceipt(receipt) {
206
+ const mode = receipt.dry_run ? "dry-run" : "apply";
207
+ console.log(`mdkg upgrade ${mode}: ${receipt.summary.created} create, ${receipt.summary.updated} update, ${receipt.summary.migrated} migrate, ${receipt.summary.synced} sync, ${receipt.summary.conflicted} conflict`);
208
+ if (receipt.changes.length === 0) {
209
+ console.log("no upgrade changes pending");
210
+ }
211
+ else {
212
+ for (const change of receipt.changes) {
213
+ console.log(` ${change.action}: ${change.path} (${change.reason})`);
214
+ }
215
+ }
216
+ if (receipt.dry_run && receipt.changes.some((change) => change.action !== "conflict")) {
217
+ console.log("next: mdkg upgrade --apply");
218
+ }
219
+ }
220
+ function runUpgradeCommand(options) {
221
+ const root = path_1.default.resolve(options.root);
222
+ const seedRoot = options.seedRoot ? path_1.default.resolve(options.seedRoot) : DEFAULT_SEED_SUBDIR;
223
+ const dryRun = !options.apply;
224
+ const version = (0, version_1.readPackageVersion)();
225
+ requireSeedAssets(seedRoot);
226
+ const currentManifest = (0, init_manifest_1.createInitManifest)(seedRoot, version);
227
+ const existingManifest = (0, init_manifest_1.readInitManifest)(path_1.default.join(root, ".mdkg", init_manifest_1.INIT_MANIFEST_FILE));
228
+ const legacyManifests = (0, init_manifest_1.loadLegacyInitManifests)(seedRoot);
229
+ const knownHashes = buildKnownHashes([existingManifest, ...legacyManifests]);
230
+ const summary = createSummary();
231
+ const changes = [];
232
+ const agentWorkspace = isAgentWorkspace(root);
233
+ const managedCurrentFiles = [];
234
+ migrateConfigIfNeeded(root, dryRun, summary, changes);
235
+ for (const file of currentManifest.files) {
236
+ if (!shouldIncludeFile(file, agentWorkspace)) {
237
+ continue;
238
+ }
239
+ planSeedFile({
240
+ root,
241
+ seedRoot,
242
+ file,
243
+ knownHashes,
244
+ dryRun,
245
+ summary,
246
+ changes,
247
+ managedCurrentFiles,
248
+ });
249
+ }
250
+ if (agentWorkspace) {
251
+ ensureAgentRuntimeFiles(root, dryRun, summary, changes);
252
+ }
253
+ if (!dryRun) {
254
+ const appliedManifest = {
255
+ ...currentManifest,
256
+ files: managedCurrentFiles.sort((a, b) => a.path.localeCompare(b.path)),
257
+ };
258
+ (0, init_manifest_1.writeInitManifest)(path_1.default.join(root, ".mdkg", init_manifest_1.INIT_MANIFEST_FILE), appliedManifest);
259
+ if (agentWorkspace) {
260
+ const config = (0, config_1.validateConfigSchema)((0, migrate_1.migrateConfig)(JSON.parse(fs_1.default.readFileSync((0, paths_1.configPath)(root), "utf8"))).config);
261
+ (0, skill_support_1.refreshSkillsRegistry)(root, config);
262
+ (0, skill_mirror_1.scaffoldMirrorRoots)(root);
263
+ const result = (0, skill_mirror_1.syncSkillMirrors)({ root, config, createRoots: true });
264
+ record(summary, changes, {
265
+ path: ".agents/skills,.claude/skills",
266
+ category: "skill_mirror",
267
+ action: "sync",
268
+ reason: `${result.synced} mirrored skill(s), ${result.pruned} stale mirror(s) pruned`,
269
+ });
270
+ }
271
+ }
272
+ const receipt = {
273
+ action: "upgrade",
274
+ dry_run: dryRun,
275
+ version,
276
+ summary,
277
+ changes,
278
+ };
279
+ if (options.json) {
280
+ console.log(`${JSON.stringify(receipt, null, 2)}`);
281
+ }
282
+ else {
283
+ emitHumanReceipt(receipt);
284
+ }
285
+ return receipt;
286
+ }
@@ -65,6 +65,13 @@ const RECOMMENDED_HEADINGS = {
65
65
  "Rollout plan",
66
66
  ],
67
67
  dec: ["Context", "Decision", "Alternatives considered", "Consequences", "Links / references"],
68
+ spec: ["Purpose", "Runtime", "Work Contracts", "Capabilities"],
69
+ work: ["Capability", "Inputs", "Outputs", "Receipt"],
70
+ work_order: ["Request", "Inputs", "Constraints"],
71
+ receipt: ["Outcome", "Artifacts", "Notes"],
72
+ feedback: ["Feedback", "Evidence"],
73
+ dispute: ["Dispute", "Evidence", "Resolution"],
74
+ proposal: ["Summary", "Evidence", "Proposed Change", "Review"],
68
75
  };
69
76
  function normalizeHeading(value) {
70
77
  return value.trim().toLowerCase();
@@ -119,6 +126,7 @@ function buildIndexNode(root, ws, filePath, node) {
119
126
  refs: node.refs,
120
127
  aliases: node.aliases,
121
128
  skills: node.skills,
129
+ attributes: node.attributes,
122
130
  path: path_1.default.relative(root, filePath),
123
131
  edges: normalizeEdges(node.edges, ws),
124
132
  };
@@ -256,11 +264,10 @@ function runValidateCommand(options) {
256
264
  nodes,
257
265
  reverse_edges: {},
258
266
  };
259
- const graphErrors = (0, validate_graph_1.collectGraphErrors)(index, { allowMissing: false });
260
- errors.push(...graphErrors);
267
+ let knownSkills = new Set();
261
268
  try {
262
269
  const skillsIndex = (0, skills_indexer_1.buildSkillsIndex)(options.root, config);
263
- const knownSkills = new Set(Object.keys(skillsIndex.skills));
270
+ knownSkills = new Set(Object.keys(skillsIndex.skills));
264
271
  for (const node of Object.values(nodes)) {
265
272
  for (const slug of node.skills) {
266
273
  if (!knownSkills.has(slug)) {
@@ -273,6 +280,11 @@ function runValidateCommand(options) {
273
280
  const message = err instanceof Error ? err.message : "unknown skill validation error";
274
281
  errors.push(message);
275
282
  }
283
+ const graphErrors = (0, validate_graph_1.collectGraphErrors)(index, {
284
+ allowMissing: false,
285
+ knownSkillSlugs: knownSkills,
286
+ });
287
+ errors.push(...graphErrors);
276
288
  const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(options.root, config);
277
289
  for (const dirPath of listDirectories(skillsRoot)) {
278
290
  const canonicalPath = path_1.default.join(dirPath, "SKILL.md");
@@ -305,6 +317,22 @@ function runValidateCommand(options) {
305
317
  fs_1.default.mkdirSync(path_1.default.dirname(outPath), { recursive: true });
306
318
  fs_1.default.writeFileSync(outPath, reportLines.join("\n"), "utf8");
307
319
  }
320
+ const receipt = {
321
+ action: "validated",
322
+ ok: uniqueErrors.length === 0,
323
+ warning_count: uniqueWarnings.length,
324
+ error_count: uniqueErrors.length,
325
+ warnings: uniqueWarnings,
326
+ errors: uniqueErrors,
327
+ ...(outPath ? { report_path: outPath } : {}),
328
+ };
329
+ if (options.json) {
330
+ console.log(JSON.stringify(receipt, null, 2));
331
+ if (uniqueErrors.length > 0) {
332
+ throw new errors_1.ValidationError(`validation failed with ${uniqueErrors.length} error(s)`);
333
+ }
334
+ return;
335
+ }
308
336
  if (!options.quiet) {
309
337
  for (const warning of uniqueWarnings) {
310
338
  console.error(`warning: ${warning}`);
@@ -6,11 +6,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runWorkspaceListCommand = runWorkspaceListCommand;
7
7
  exports.runWorkspaceAddCommand = runWorkspaceAddCommand;
8
8
  exports.runWorkspaceRemoveCommand = runWorkspaceRemoveCommand;
9
+ exports.runWorkspaceEnableCommand = runWorkspaceEnableCommand;
10
+ exports.runWorkspaceDisableCommand = runWorkspaceDisableCommand;
9
11
  const fs_1 = __importDefault(require("fs"));
10
12
  const path_1 = __importDefault(require("path"));
11
13
  const config_1 = require("../core/config");
14
+ const migrate_1 = require("../core/migrate");
15
+ const workspace_path_1 = require("../core/workspace_path");
12
16
  const errors_1 = require("../util/errors");
13
17
  const ALIAS_RE = /^[a-z][a-z0-9_]*$/;
18
+ function workspaceReceipt(alias, workspace) {
19
+ return {
20
+ alias,
21
+ path: workspace.path,
22
+ enabled: workspace.enabled,
23
+ mdkg_dir: workspace.mdkg_dir,
24
+ };
25
+ }
26
+ function printWorkspaceMutationReceipt(action, workspace, json) {
27
+ if (json) {
28
+ console.log(JSON.stringify({ action, workspace }, null, 2));
29
+ return;
30
+ }
31
+ if (action === "added") {
32
+ console.log(`workspace added: ${workspace.alias} (${workspace.path})`);
33
+ return;
34
+ }
35
+ console.log(`workspace ${action}: ${workspace.alias}`);
36
+ }
14
37
  function readRawConfig(root) {
15
38
  const configPath = path_1.default.join(root, ".mdkg", "config.json");
16
39
  if (!fs_1.default.existsSync(configPath)) {
@@ -27,24 +50,51 @@ function readRawConfig(root) {
27
50
  if (typeof raw !== "object" || raw === null) {
28
51
  throw new errors_1.UsageError("config must be a JSON object");
29
52
  }
30
- return { path: configPath, raw: raw };
53
+ const migrated = (0, migrate_1.migrateConfig)(raw).config;
54
+ (0, config_1.validateConfigSchema)(migrated);
55
+ if (typeof migrated !== "object" || migrated === null || Array.isArray(migrated)) {
56
+ throw new errors_1.UsageError("config must be a JSON object");
57
+ }
58
+ return { path: configPath, raw: migrated };
31
59
  }
32
60
  function writeRawConfig(configPath, raw) {
33
61
  fs_1.default.writeFileSync(configPath, JSON.stringify(raw, null, 2), "utf8");
34
62
  }
35
63
  function normalizeAlias(alias) {
36
- const normalized = alias.toLowerCase();
37
- if (normalized === "all") {
64
+ if (alias.toLowerCase() === "all") {
38
65
  throw new errors_1.UsageError("workspace alias cannot be 'all'");
39
66
  }
40
- if (!ALIAS_RE.test(normalized)) {
67
+ if (alias !== alias.toLowerCase() || !ALIAS_RE.test(alias)) {
41
68
  throw new errors_1.UsageError("workspace alias must be lowercase and use [a-z0-9_]");
42
69
  }
43
- return normalized;
70
+ return alias;
71
+ }
72
+ function normalizeCommandWorkspacePath(value, label) {
73
+ try {
74
+ return (0, workspace_path_1.normalizeContainedWorkspacePath)(value, label);
75
+ }
76
+ catch (err) {
77
+ const message = err instanceof Error ? err.message : String(err);
78
+ throw new errors_1.UsageError(message);
79
+ }
44
80
  }
45
81
  function runWorkspaceListCommand(options) {
46
82
  const config = (0, config_1.loadConfig)(options.root);
47
83
  const aliases = Object.keys(config.workspaces).sort();
84
+ if (options.json) {
85
+ console.log(JSON.stringify({
86
+ workspaces: aliases.map((alias) => {
87
+ const ws = config.workspaces[alias];
88
+ return {
89
+ alias,
90
+ path: ws.path,
91
+ enabled: ws.enabled,
92
+ mdkg_dir: ws.mdkg_dir,
93
+ };
94
+ }),
95
+ }, null, 2));
96
+ return;
97
+ }
48
98
  if (aliases.length === 0) {
49
99
  console.log("no workspaces registered");
50
100
  return;
@@ -57,11 +107,11 @@ function runWorkspaceListCommand(options) {
57
107
  }
58
108
  function runWorkspaceAddCommand(options) {
59
109
  const alias = normalizeAlias(options.alias);
60
- const workspacePath = options.workspacePath.trim();
61
- if (!workspacePath) {
62
- throw new errors_1.UsageError("workspace path cannot be empty");
110
+ const workspacePath = normalizeCommandWorkspacePath(options.workspacePath, "workspace path");
111
+ const mdkgDir = normalizeCommandWorkspacePath(options.mdkgDir ?? ".mdkg", "workspace mdkg dir");
112
+ if ((0, workspace_path_1.isRootWorkspacePath)(workspacePath)) {
113
+ throw new errors_1.UsageError('workspace path must not be "." for non-root workspaces');
63
114
  }
64
- const mdkgDir = options.mdkgDir?.trim() || ".mdkg";
65
115
  const { path: configPath, raw } = readRawConfig(options.root);
66
116
  const workspacesRaw = raw.workspaces;
67
117
  if (typeof workspacesRaw !== "object" || workspacesRaw === null) {
@@ -71,14 +121,27 @@ function runWorkspaceAddCommand(options) {
71
121
  if (workspaces[alias]) {
72
122
  throw new errors_1.UsageError(`workspace already exists: ${alias}`);
73
123
  }
74
- workspaces[alias] = { path: workspacePath, enabled: true, mdkg_dir: mdkgDir };
124
+ const docRootKey = (0, workspace_path_1.workspaceDocumentRootKey)(workspacePath, mdkgDir);
125
+ for (const [existingAlias, entry] of Object.entries(workspaces)) {
126
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
127
+ continue;
128
+ }
129
+ const existing = entry;
130
+ if (typeof existing.path === "string" &&
131
+ typeof existing.mdkg_dir === "string" &&
132
+ (0, workspace_path_1.workspaceDocumentRootKey)(existing.path, existing.mdkg_dir) === docRootKey) {
133
+ throw new errors_1.UsageError(`workspace document root already registered by ${existingAlias}`);
134
+ }
135
+ }
136
+ const workspace = { path: workspacePath, enabled: true, mdkg_dir: mdkgDir };
137
+ workspaces[alias] = workspace;
75
138
  raw.workspaces = workspaces;
76
139
  writeRawConfig(configPath, raw);
77
140
  const wsRoot = path_1.default.resolve(options.root, workspacePath, mdkgDir);
78
141
  fs_1.default.mkdirSync(path_1.default.join(wsRoot, "core"), { recursive: true });
79
142
  fs_1.default.mkdirSync(path_1.default.join(wsRoot, "design"), { recursive: true });
80
143
  fs_1.default.mkdirSync(path_1.default.join(wsRoot, "work"), { recursive: true });
81
- console.log(`workspace added: ${alias} (${workspacePath})`);
144
+ printWorkspaceMutationReceipt("added", workspaceReceipt(alias, workspace), options.json);
82
145
  }
83
146
  function runWorkspaceRemoveCommand(options) {
84
147
  const alias = normalizeAlias(options.alias);
@@ -91,11 +154,40 @@ function runWorkspaceRemoveCommand(options) {
91
154
  throw new errors_1.UsageError("config.workspaces must be an object");
92
155
  }
93
156
  const workspaces = workspacesRaw;
94
- if (!workspaces[alias]) {
157
+ const workspace = workspaces[alias];
158
+ if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
95
159
  throw new errors_1.NotFoundError(`workspace not found: ${alias}`);
96
160
  }
161
+ const removed = workspaceReceipt(alias, workspace);
97
162
  delete workspaces[alias];
98
163
  raw.workspaces = workspaces;
99
164
  writeRawConfig(configPath, raw);
100
- console.log(`workspace removed: ${alias}`);
165
+ printWorkspaceMutationReceipt("removed", removed, options.json);
166
+ }
167
+ function setWorkspaceEnabled(options, enabled) {
168
+ const alias = normalizeAlias(options.alias);
169
+ if (alias === "root" && !enabled) {
170
+ throw new errors_1.UsageError("cannot disable root workspace");
171
+ }
172
+ const { path: configPath, raw } = readRawConfig(options.root);
173
+ const workspacesRaw = raw.workspaces;
174
+ if (typeof workspacesRaw !== "object" || workspacesRaw === null) {
175
+ throw new errors_1.UsageError("config.workspaces must be an object");
176
+ }
177
+ const workspaces = workspacesRaw;
178
+ const workspace = workspaces[alias];
179
+ if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
180
+ throw new errors_1.NotFoundError(`workspace not found: ${alias}`);
181
+ }
182
+ const updated = { ...workspace, enabled };
183
+ workspaces[alias] = updated;
184
+ raw.workspaces = workspaces;
185
+ writeRawConfig(configPath, raw);
186
+ printWorkspaceMutationReceipt(enabled ? "enabled" : "disabled", workspaceReceipt(alias, updated), options.json);
187
+ }
188
+ function runWorkspaceEnableCommand(options) {
189
+ setWorkspaceEnabled(options, true);
190
+ }
191
+ function runWorkspaceDisableCommand(options) {
192
+ setWorkspaceEnabled(options, false);
101
193
  }