@deksden-com/dd-flow-cli 0.1.0 → 0.2.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.
@@ -0,0 +1,118 @@
1
+ import { getCanonStatus } from "./canon.js";
2
+ import { getCliBuildInfo } from "./build-info.js";
3
+ import { getProjectVersionStatus, resolveStatusProjectRoot } from "./version-status.js";
4
+ import { findProjectByRoot } from "./projects.js";
5
+ export function getRuntimeStatus(context, input = {}) {
6
+ const cwd = process.cwd();
7
+ const projectRootResolution = resolveStatusProjectRoot({ requestedRoot: input.projectRoot, cwd });
8
+ const projectRoot = projectRootResolution.root;
9
+ const project = projectRoot ? findProjectByRoot(context, projectRoot) : undefined;
10
+ const canonStatus = getCanonStatus(context);
11
+ const canon = asRecord(canonStatus)?.canon;
12
+ const resolvedCanonForProject = canonForProjectStatus(canon);
13
+ const projectVersionStatus = getProjectVersionStatus({
14
+ projectRoot,
15
+ rootSource: projectRootResolution.root_source,
16
+ canon: resolvedCanonForProject
17
+ });
18
+ const cli = getCliBuildInfo();
19
+ return {
20
+ ok: true,
21
+ schema_id: "dd-flow/status-report@1",
22
+ dd_flow_home: context.ddFlowHome,
23
+ cwd,
24
+ cli,
25
+ project: {
26
+ requested_root: projectRootResolution.requested_root,
27
+ root: projectRoot,
28
+ root_source: projectRootResolution.root_source,
29
+ registered: Boolean(project),
30
+ id: project?.id ?? null,
31
+ status: project?.status ?? null,
32
+ memory_bank: projectVersionStatus?.memory_bank ?? null,
33
+ flow_pack: projectVersionStatus?.flow_pack ?? null,
34
+ drift: projectVersionStatus?.drift ?? null
35
+ },
36
+ canon: {
37
+ ...(asRecord(canonStatus) ?? {}),
38
+ resolved: canon ?? null,
39
+ compatibility: cliCanonCompatibility(cli, resolvedCanonForProject?.metadata.version ?? null)
40
+ }
41
+ };
42
+ }
43
+ function asRecord(value) {
44
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
45
+ }
46
+ function stringValue(value) {
47
+ return typeof value === "string" && value.length > 0 ? value : null;
48
+ }
49
+ function canonForProjectStatus(value) {
50
+ const record = asRecord(value);
51
+ if (!record || typeof record.root !== "string")
52
+ return null;
53
+ const metadata = asRecord(record.metadata);
54
+ const status = metadata?.status;
55
+ if (metadata && (status === "present" || status === "degraded" || status === "unknown")) {
56
+ return {
57
+ root: record.root,
58
+ metadata: {
59
+ version: stringValue(metadata.version),
60
+ commit: stringValue(metadata.commit),
61
+ flow_contract: stringValue(metadata.flow_contract),
62
+ status,
63
+ diagnostics: Array.isArray(metadata.diagnostics) ? metadata.diagnostics.filter((item) => typeof item === "string") : []
64
+ }
65
+ };
66
+ }
67
+ const version = stringValue(record.version);
68
+ const commit = stringValue(record.commit);
69
+ const flowContract = stringValue(record.flow_contract);
70
+ const diagnostics = [
71
+ ...(version ? [] : ["canon_version_unknown"]),
72
+ ...(commit ? [] : ["canon_commit_unknown"]),
73
+ ...(flowContract ? [] : ["flow_contract_unknown"])
74
+ ];
75
+ return {
76
+ root: record.root,
77
+ metadata: {
78
+ version,
79
+ commit,
80
+ flow_contract: flowContract,
81
+ status: diagnostics.length === 0 ? "present" : "degraded",
82
+ diagnostics
83
+ }
84
+ };
85
+ }
86
+ function cliCanonCompatibility(cli, resolvedCanonVersion) {
87
+ const builtWithVersion = cli.build.built_with_canon.version;
88
+ if (!builtWithVersion || !resolvedCanonVersion) {
89
+ return { cli_built_with_resolved_canon: "unknown", reason: "version_missing" };
90
+ }
91
+ const built = parseSemver(builtWithVersion);
92
+ const resolved = parseSemver(resolvedCanonVersion);
93
+ if (!built || !resolved)
94
+ return { cli_built_with_resolved_canon: "unknown", reason: "invalid_semver" };
95
+ const comparison = compareSemver(built, resolved);
96
+ if (comparison === 0)
97
+ return { cli_built_with_resolved_canon: "same", reason: "semver_equal" };
98
+ return {
99
+ cli_built_with_resolved_canon: comparison < 0 ? "behind" : "ahead",
100
+ reason: comparison < 0 ? "cli_built_with_older_canon" : "cli_built_with_newer_canon"
101
+ };
102
+ }
103
+ function parseSemver(version) {
104
+ const match = version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:[-+].*)?$/);
105
+ if (!match?.[1] || !match[2] || !match[3])
106
+ return null;
107
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
108
+ }
109
+ function compareSemver(left, right) {
110
+ for (let index = 0; index < 3; index += 1) {
111
+ const leftPart = left[index] ?? 0;
112
+ const rightPart = right[index] ?? 0;
113
+ const diff = leftPart - rightPart;
114
+ if (diff !== 0)
115
+ return diff;
116
+ }
117
+ return 0;
118
+ }
@@ -0,0 +1,258 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { parse as parseYaml } from "yaml";
5
+ const canonicalOnlyFlowNames = ["mb-init.md", "mb-upgrade.md", "mb-upgrade-review.md", "mb-distill.md"];
6
+ export function resolveStatusProjectRoot(input) {
7
+ if (input.requestedRoot) {
8
+ const absolute = path.resolve(input.requestedRoot);
9
+ return { requested_root: absolute, root: existingDirectoryRealpath(absolute), root_source: "explicit" };
10
+ }
11
+ const gitRoot = nearestGitRoot(input.cwd);
12
+ if (gitRoot) {
13
+ return { requested_root: path.resolve(input.cwd), root: gitRoot, root_source: "git" };
14
+ }
15
+ const cwdRoot = existingDirectoryRealpath(input.cwd);
16
+ return { requested_root: path.resolve(input.cwd), root: cwdRoot, root_source: "cwd" };
17
+ }
18
+ export function getProjectVersionStatus(input) {
19
+ if (!input.projectRoot)
20
+ return null;
21
+ const memoryBank = readMemoryBankMetadata(input.projectRoot);
22
+ const flowPack = readFlowPackMetadata(input.projectRoot, memoryBank.root);
23
+ const memoryBankDrift = compareMemoryBankVersion(memoryBank.version, input.canon?.metadata.version ?? null, memoryBank.status);
24
+ const flowPackDrift = compareFlowPackCommit(flowPack.source_commit, input.canon?.metadata.commit ?? null, input.canon?.root ?? null, flowPack.status);
25
+ const overall = overallDrift(memoryBankDrift, flowPackDrift, memoryBank.status, flowPack.status);
26
+ return {
27
+ root: input.projectRoot,
28
+ root_source: input.rootSource,
29
+ memory_bank: memoryBank,
30
+ flow_pack: flowPack,
31
+ drift: {
32
+ memory_bank_version: memoryBankDrift,
33
+ flow_pack_commit: flowPackDrift,
34
+ overall,
35
+ next_action: nextAction(overall, memoryBank.status, flowPack.status)
36
+ }
37
+ };
38
+ }
39
+ function readMemoryBankMetadata(projectRoot) {
40
+ const dotRoot = path.join(projectRoot, ".memory-bank");
41
+ const legacyRoot = path.join(projectRoot, "memory-bank");
42
+ const root = fs.existsSync(dotRoot) && fs.statSync(dotRoot).isDirectory() ? dotRoot : fs.existsSync(legacyRoot) && fs.statSync(legacyRoot).isDirectory() ? legacyRoot : null;
43
+ const folderName = root ? path.basename(root) : null;
44
+ if (!root) {
45
+ return { root: null, folder_name: null, version: null, source: null, status: "missing", reason: "no_memory_bank" };
46
+ }
47
+ const indexPath = path.join(root, "index.md");
48
+ const indexVersion = fs.existsSync(indexPath) ? readMemoryBankVersionFromIndex(indexPath) : null;
49
+ if (indexVersion) {
50
+ return { root, folder_name: folderName, version: indexVersion, source: path.relative(projectRoot, indexPath), status: "present" };
51
+ }
52
+ const mbbPath = path.join(root, "mbb", "index.md");
53
+ const mbbVersion = fs.existsSync(mbbPath) ? readMemoryBankCanonTextVersion(mbbPath) : null;
54
+ if (mbbVersion) {
55
+ return { root, folder_name: folderName, version: mbbVersion, source: path.relative(projectRoot, mbbPath), status: "present" };
56
+ }
57
+ return { root, folder_name: folderName, version: null, source: null, status: "unknown", reason: "memory_bank_version_missing" };
58
+ }
59
+ function readFlowPackMetadata(projectRoot, memoryBankRoot) {
60
+ if (!memoryBankRoot) {
61
+ return {
62
+ manifest_path: null,
63
+ schema_id: null,
64
+ pack_version: null,
65
+ source_commit: null,
66
+ canon_version_at_source_commit: null,
67
+ status: "missing",
68
+ reason: "no_memory_bank",
69
+ canonical_only_files: []
70
+ };
71
+ }
72
+ const flowRoot = path.join(memoryBankRoot, "dd-flow");
73
+ const manifestPath = path.join(flowRoot, "manifest.json");
74
+ const canonicalOnlyFiles = canonicalOnlyFlowNames.filter((file) => fs.existsSync(path.join(flowRoot, file)));
75
+ if (!fs.existsSync(manifestPath)) {
76
+ return {
77
+ manifest_path: path.relative(projectRoot, manifestPath),
78
+ schema_id: null,
79
+ pack_version: null,
80
+ source_commit: null,
81
+ canon_version_at_source_commit: null,
82
+ status: "missing",
83
+ reason: "flow_pack_manifest_missing",
84
+ canonical_only_files: canonicalOnlyFiles
85
+ };
86
+ }
87
+ let manifest;
88
+ try {
89
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
90
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
91
+ throw new Error("manifest is not an object");
92
+ manifest = parsed;
93
+ }
94
+ catch {
95
+ return {
96
+ manifest_path: path.relative(projectRoot, manifestPath),
97
+ schema_id: null,
98
+ pack_version: null,
99
+ source_commit: null,
100
+ canon_version_at_source_commit: null,
101
+ status: "invalid",
102
+ reason: "flow_pack_manifest_invalid_json",
103
+ canonical_only_files: canonicalOnlyFiles
104
+ };
105
+ }
106
+ const schemaId = stringOrNull(manifest.schema_id);
107
+ const canonVersion = stringOrNull(manifest.canon_version_at_source_commit) ?? stringOrNull(manifest.canon_version);
108
+ const sourceCommit = stringOrNull(manifest.source_commit);
109
+ const status = schemaId === "dd-flow/project-flow-pack-manifest@2" && canonVersion ? "present" : schemaId === "dd-flow/project-flow-pack-manifest@1" ? "degraded" : "invalid";
110
+ const reason = status === "degraded" ? "legacy_flow_pack_manifest" : status === "invalid" ? "flow_pack_manifest_schema_unknown" : undefined;
111
+ return {
112
+ manifest_path: path.relative(projectRoot, manifestPath),
113
+ schema_id: schemaId,
114
+ pack_version: stringOrNull(manifest.pack_version),
115
+ source_commit: sourceCommit,
116
+ canon_version_at_source_commit: canonVersion,
117
+ status,
118
+ ...(reason ? { reason } : {}),
119
+ canonical_only_files: canonicalOnlyFiles
120
+ };
121
+ }
122
+ function readMemoryBankVersionFromIndex(file) {
123
+ const text = fs.readFileSync(file, "utf8");
124
+ const match = text.match(/^---\n([\s\S]*?)\n---/);
125
+ if (!match?.[1])
126
+ return null;
127
+ try {
128
+ const parsed = parseYaml(match[1]);
129
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
130
+ const value = parsed.memory_bank_version;
131
+ return typeof value === "string" && value.length > 0 ? value : null;
132
+ }
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ return null;
138
+ }
139
+ function readMemoryBankCanonTextVersion(file) {
140
+ const text = fs.readFileSync(file, "utf8");
141
+ const ru = text.match(/Текущая версия канона Memory Bank:\s*`([^`]+)`/);
142
+ if (ru?.[1])
143
+ return ru[1];
144
+ const en = text.match(/Memory Bank canon version:\s*`?([0-9]+\.[0-9]+\.[0-9][^`\s]*)`?/i);
145
+ return en?.[1] ?? null;
146
+ }
147
+ function compareMemoryBankVersion(current, target, artifactStatus) {
148
+ if (artifactStatus === "missing")
149
+ return { status: "missing", confidence: "none", reason: "memory_bank_missing", current, target };
150
+ if (!current)
151
+ return { status: "unknown", confidence: "none", reason: "memory_bank_version_missing", current, target };
152
+ if (!target)
153
+ return { status: "unknown", confidence: "none", reason: "canon_version_unknown", current, target };
154
+ const currentSemver = parseSemver(current);
155
+ const targetSemver = parseSemver(target);
156
+ if (!currentSemver || !targetSemver)
157
+ return { status: "invalid", confidence: "none", reason: "invalid_semver", current, target };
158
+ const comparison = compareSemver(currentSemver, targetSemver);
159
+ if (comparison === 0)
160
+ return { status: "same", confidence: "semver", reason: "semver_equal", current, target };
161
+ return { status: comparison < 0 ? "behind" : "ahead", confidence: "semver", reason: comparison < 0 ? "semver_older" : "semver_newer", current, target };
162
+ }
163
+ function compareFlowPackCommit(current, target, canonRoot, artifactStatus) {
164
+ if (artifactStatus === "missing")
165
+ return { status: "missing", confidence: "none", reason: "flow_pack_manifest_missing", current, target };
166
+ if (artifactStatus === "invalid")
167
+ return { status: "invalid", confidence: "none", reason: "flow_pack_manifest_invalid", current, target };
168
+ if (!current)
169
+ return { status: "unknown", confidence: "none", reason: "flow_pack_source_commit_missing", current, target };
170
+ if (!target)
171
+ return { status: "unknown", confidence: "none", reason: "canon_commit_unknown", current, target };
172
+ if (current === target)
173
+ return { status: "same", confidence: "exact", reason: "commit_equal", current, target };
174
+ if (!canonRoot || !fs.existsSync(path.join(canonRoot, ".git"))) {
175
+ return { status: "unknown", confidence: "none", reason: "git_history_unavailable", current, target };
176
+ }
177
+ const projectAncestor = gitSuccess(canonRoot, ["merge-base", "--is-ancestor", current, target]);
178
+ if (projectAncestor)
179
+ return { status: "behind", confidence: "git_ancestry", reason: "source_commit_ancestor_of_canon", current, target };
180
+ const canonAncestor = gitSuccess(canonRoot, ["merge-base", "--is-ancestor", target, current]);
181
+ if (canonAncestor)
182
+ return { status: "ahead", confidence: "git_ancestry", reason: "source_commit_descendant_of_canon", current, target };
183
+ const currentExists = gitSuccess(canonRoot, ["cat-file", "-e", `${current}^{commit}`]);
184
+ const targetExists = gitSuccess(canonRoot, ["cat-file", "-e", `${target}^{commit}`]);
185
+ return {
186
+ status: currentExists && targetExists ? "diverged" : "unknown",
187
+ confidence: currentExists && targetExists ? "git_ancestry" : "none",
188
+ reason: currentExists && targetExists ? "git_histories_diverged" : "source_commit_not_in_resolved_canon",
189
+ current,
190
+ target
191
+ };
192
+ }
193
+ function overallDrift(memoryBank, flowPack, memoryStatus, flowStatus) {
194
+ if (memoryStatus === "missing")
195
+ return "not_applicable";
196
+ if (memoryBank.status === "invalid" || flowPack.status === "invalid")
197
+ return "invalid";
198
+ if (memoryBank.status === "missing" || flowPack.status === "missing")
199
+ return "missing";
200
+ if (memoryBank.status === "ahead" || flowPack.status === "ahead")
201
+ return "ahead";
202
+ if (memoryBank.status === "behind" || flowPack.status === "behind")
203
+ return "behind";
204
+ if (memoryBank.status === "diverged" || flowPack.status === "diverged")
205
+ return "diverged";
206
+ if (memoryBank.status === "same" && (flowPack.status === "same" || flowStatus === "degraded"))
207
+ return flowStatus === "degraded" ? "unknown" : "same";
208
+ return "unknown";
209
+ }
210
+ function nextAction(overall, memoryStatus, flowStatus) {
211
+ if (overall === "same")
212
+ return "no_action";
213
+ if (overall === "not_applicable" && memoryStatus === "missing")
214
+ return "run_mb_init_if_project_needs_memory_bank";
215
+ if (overall === "behind" || flowStatus === "degraded")
216
+ return "run_canonical_mb_upgrade";
217
+ if (overall === "missing")
218
+ return "run_canonical_mb_upgrade_or_repair_manifest";
219
+ if (overall === "invalid")
220
+ return "repair_memory_bank_markers_or_manifest";
221
+ return "inspect_status_reasons";
222
+ }
223
+ function nearestGitRoot(cwd) {
224
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
225
+ if (result.status !== 0)
226
+ return null;
227
+ const stdout = result.stdout.trim();
228
+ return stdout ? existingDirectoryRealpath(stdout) : null;
229
+ }
230
+ function existingDirectoryRealpath(root) {
231
+ const absolute = path.resolve(root);
232
+ if (!fs.existsSync(absolute) || !fs.statSync(absolute).isDirectory())
233
+ return null;
234
+ return fs.realpathSync(absolute);
235
+ }
236
+ function stringOrNull(value) {
237
+ return typeof value === "string" && value.length > 0 ? value : null;
238
+ }
239
+ function parseSemver(version) {
240
+ const match = version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:[-+].*)?$/);
241
+ if (!match?.[1] || !match[2] || !match[3])
242
+ return null;
243
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
244
+ }
245
+ function compareSemver(left, right) {
246
+ for (let index = 0; index < 3; index += 1) {
247
+ const leftPart = left[index] ?? 0;
248
+ const rightPart = right[index] ?? 0;
249
+ const diff = leftPart - rightPart;
250
+ if (diff !== 0)
251
+ return diff;
252
+ }
253
+ return 0;
254
+ }
255
+ function gitSuccess(cwd, args) {
256
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" });
257
+ return result.status === 0;
258
+ }
@@ -194,6 +194,13 @@ function migrate(db) {
194
194
  FOREIGN KEY(project_id) REFERENCES projects(id)
195
195
  );
196
196
 
197
+ CREATE TABLE IF NOT EXISTS runtime_config (
198
+ key TEXT PRIMARY KEY,
199
+ value_json TEXT NOT NULL,
200
+ created_at TEXT NOT NULL,
201
+ updated_at TEXT NOT NULL
202
+ );
203
+
197
204
  CREATE TABLE IF NOT EXISTS flow_sessions (
198
205
  session_id TEXT NOT NULL,
199
206
  project_id TEXT NOT NULL,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "license": "MIT",
35
35
  "scripts": {
36
- "build": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
36
+ "build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
37
37
  "typecheck": "tsc --noEmit",
38
38
  "lint": "eslint . --max-warnings=0",
39
39
  "test": "vitest run",