kibi-cli 0.2.8 → 0.4.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.
- package/dist/cli.d.ts +4 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -14
- package/dist/commands/check.d.ts +5 -8
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +41 -62
- package/dist/commands/coverage.d.ts +12 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +24 -0
- package/dist/commands/discovery-shared.d.ts +11 -0
- package/dist/commands/discovery-shared.d.ts.map +1 -0
- package/dist/commands/discovery-shared.js +281 -0
- package/dist/commands/doctor.d.ts +3 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +12 -13
- package/dist/commands/gaps.d.ts +12 -0
- package/dist/commands/gaps.d.ts.map +1 -0
- package/dist/commands/gaps.js +28 -0
- package/dist/commands/graph.d.ts +13 -0
- package/dist/commands/graph.d.ts.map +1 -0
- package/dist/commands/graph.js +35 -0
- package/dist/commands/init.d.ts +3 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +4 -3
- package/dist/commands/query.d.ts +3 -1
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +9 -20
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +38 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +9 -0
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +79 -12
- package/dist/commands/sync.d.ts +4 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +79 -31
- package/dist/extractors/markdown.d.ts +17 -0
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +104 -14
- package/dist/prolog/codec.d.ts +32 -5
- package/dist/prolog/codec.d.ts.map +1 -1
- package/dist/prolog/codec.js +95 -58
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +12 -2
- package/dist/public/check-types.d.ts +7 -0
- package/dist/public/check-types.d.ts.map +1 -0
- package/dist/public/check-types.js +18 -0
- package/dist/public/schemas/entity.d.ts +68 -0
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +52 -0
- package/dist/relationships/shards.d.ts.map +1 -1
- package/dist/relationships/shards.js +6 -3
- package/dist/schemas/entity.schema.json +120 -0
- package/dist/search-ranking.d.ts +9 -0
- package/dist/search-ranking.d.ts.map +1 -0
- package/dist/search-ranking.js +149 -0
- package/dist/traceability/symbol-extract.d.ts.map +1 -1
- package/dist/traceability/symbol-extract.js +16 -8
- package/dist/types/entities.d.ts +19 -1
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/utils/prolog-cleanup.d.ts +10 -0
- package/dist/utils/prolog-cleanup.d.ts.map +1 -0
- package/dist/utils/prolog-cleanup.js +39 -0
- package/dist/utils/rule-registry.d.ts +8 -0
- package/dist/utils/rule-registry.d.ts.map +1 -1
- package/dist/utils/rule-registry.js +14 -12
- package/package.json +10 -2
- package/schema/config.json +7 -1
- package/schema/entities.pl +18 -0
- package/schema/validation.pl +115 -4
- package/src/public/check-types.ts +37 -0
- package/src/public/schemas/entity.ts +58 -0
- package/src/schemas/entity.schema.json +56 -1
- package/dist/kb/target-resolver.d.ts +0 -80
- package/dist/kb/target-resolver.d.ts.map +0 -1
- package/dist/kb/target-resolver.js +0 -313
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
// Typed fact field enums per proposal
|
|
20
|
+
type FactKind = "subject" | "property_value" | "observation" | "meta";
|
|
21
|
+
type Operator = "eq" | "neq" | "lt" | "lte" | "gt" | "gte";
|
|
22
|
+
type ValueType = "string" | "int" | "number" | "bool";
|
|
23
|
+
type Polarity = "require" | "forbid";
|
|
24
|
+
|
|
19
25
|
const entitySchema = {
|
|
20
26
|
$id: "entity.schema.json",
|
|
21
27
|
title: "Entity",
|
|
@@ -69,6 +75,26 @@ const entitySchema = {
|
|
|
69
75
|
"fact",
|
|
70
76
|
],
|
|
71
77
|
},
|
|
78
|
+
// Typed fact fields - only valid when type === "fact"
|
|
79
|
+
fact_kind: {
|
|
80
|
+
type: "string",
|
|
81
|
+
enum: ["subject", "property_value", "observation", "meta"],
|
|
82
|
+
},
|
|
83
|
+
subject_key: { type: "string" },
|
|
84
|
+
property_key: { type: "string" },
|
|
85
|
+
operator: { type: "string", enum: ["eq", "neq", "lt", "lte", "gt", "gte"] },
|
|
86
|
+
value_type: { type: "string", enum: ["string", "int", "number", "bool"] },
|
|
87
|
+
value_string: { type: "string" },
|
|
88
|
+
value_int: { type: "integer" },
|
|
89
|
+
value_number: { type: "number" },
|
|
90
|
+
value_bool: { type: "boolean" },
|
|
91
|
+
unit: { type: "string" },
|
|
92
|
+
scope: { type: "string" },
|
|
93
|
+
polarity: { type: "string", enum: ["require", "forbid"] },
|
|
94
|
+
closed_world: { type: "boolean" },
|
|
95
|
+
valid_from: { type: "string" },
|
|
96
|
+
valid_to: { type: "string" },
|
|
97
|
+
canonical_key: { type: "string" },
|
|
72
98
|
},
|
|
73
99
|
required: [
|
|
74
100
|
"id",
|
|
@@ -79,6 +105,38 @@ const entitySchema = {
|
|
|
79
105
|
"source",
|
|
80
106
|
"type",
|
|
81
107
|
],
|
|
108
|
+
allOf: [
|
|
109
|
+
// Forbid fact-only fields on non-fact entities
|
|
110
|
+
{
|
|
111
|
+
if: {
|
|
112
|
+
properties: { type: { const: "fact" } },
|
|
113
|
+
},
|
|
114
|
+
// Fact entities can have fact fields (no restriction)
|
|
115
|
+
// Non-fact entities cannot have fact fields
|
|
116
|
+
else: {
|
|
117
|
+
not: {
|
|
118
|
+
anyOf: [
|
|
119
|
+
{ required: ["fact_kind"] },
|
|
120
|
+
{ required: ["subject_key"] },
|
|
121
|
+
{ required: ["property_key"] },
|
|
122
|
+
{ required: ["operator"] },
|
|
123
|
+
{ required: ["value_type"] },
|
|
124
|
+
{ required: ["value_string"] },
|
|
125
|
+
{ required: ["value_int"] },
|
|
126
|
+
{ required: ["value_number"] },
|
|
127
|
+
{ required: ["value_bool"] },
|
|
128
|
+
{ required: ["unit"] },
|
|
129
|
+
{ required: ["scope"] },
|
|
130
|
+
{ required: ["polarity"] },
|
|
131
|
+
{ required: ["closed_world"] },
|
|
132
|
+
{ required: ["valid_from"] },
|
|
133
|
+
{ required: ["valid_to"] },
|
|
134
|
+
{ required: ["canonical_key"] },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
82
140
|
additionalProperties: false,
|
|
83
141
|
};
|
|
84
142
|
|
|
@@ -50,7 +50,32 @@
|
|
|
50
50
|
"symbol",
|
|
51
51
|
"fact"
|
|
52
52
|
]
|
|
53
|
-
}
|
|
53
|
+
},
|
|
54
|
+
"fact_kind": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"enum": ["subject", "property_value", "observation", "meta"]
|
|
57
|
+
},
|
|
58
|
+
"subject_key": { "type": "string" },
|
|
59
|
+
"property_key": { "type": "string" },
|
|
60
|
+
"operator": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"enum": ["eq", "neq", "lt", "lte", "gt", "gte"]
|
|
63
|
+
},
|
|
64
|
+
"value_type": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"enum": ["string", "int", "number", "bool"]
|
|
67
|
+
},
|
|
68
|
+
"value_string": { "type": "string" },
|
|
69
|
+
"value_int": { "type": "integer" },
|
|
70
|
+
"value_number": { "type": "number" },
|
|
71
|
+
"value_bool": { "type": "boolean" },
|
|
72
|
+
"unit": { "type": "string" },
|
|
73
|
+
"scope": { "type": "string" },
|
|
74
|
+
"polarity": { "type": "string", "enum": ["require", "forbid"] },
|
|
75
|
+
"closed_world": { "type": "boolean" },
|
|
76
|
+
"valid_from": { "type": "string" },
|
|
77
|
+
"valid_to": { "type": "string" },
|
|
78
|
+
"canonical_key": { "type": "string" }
|
|
54
79
|
},
|
|
55
80
|
"required": [
|
|
56
81
|
"id",
|
|
@@ -61,5 +86,35 @@
|
|
|
61
86
|
"source",
|
|
62
87
|
"type"
|
|
63
88
|
],
|
|
89
|
+
"allOf": [
|
|
90
|
+
{
|
|
91
|
+
"if": {
|
|
92
|
+
"properties": { "type": { "const": "fact" } }
|
|
93
|
+
},
|
|
94
|
+
"then": {},
|
|
95
|
+
"else": {
|
|
96
|
+
"not": {
|
|
97
|
+
"anyOf": [
|
|
98
|
+
{ "required": ["fact_kind"] },
|
|
99
|
+
{ "required": ["subject_key"] },
|
|
100
|
+
{ "required": ["property_key"] },
|
|
101
|
+
{ "required": ["operator"] },
|
|
102
|
+
{ "required": ["value_type"] },
|
|
103
|
+
{ "required": ["value_string"] },
|
|
104
|
+
{ "required": ["value_int"] },
|
|
105
|
+
{ "required": ["value_number"] },
|
|
106
|
+
{ "required": ["value_bool"] },
|
|
107
|
+
{ "required": ["unit"] },
|
|
108
|
+
{ "required": ["scope"] },
|
|
109
|
+
{ "required": ["polarity"] },
|
|
110
|
+
{ "required": ["closed_world"] },
|
|
111
|
+
{ "required": ["valid_from"] },
|
|
112
|
+
{ "required": ["valid_to"] },
|
|
113
|
+
{ "required": ["canonical_key"] }
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
],
|
|
64
119
|
"additionalProperties": false
|
|
65
120
|
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
export type ResolutionReason = "env" | "kb" | "git" | "cwd";
|
|
2
|
-
export interface WorkspaceInfo {
|
|
3
|
-
root: string;
|
|
4
|
-
reason: ResolutionReason;
|
|
5
|
-
}
|
|
6
|
-
export interface KbTarget {
|
|
7
|
-
workspaceRoot: string;
|
|
8
|
-
branch: string;
|
|
9
|
-
kbPath: string;
|
|
10
|
-
reason: ResolutionReason;
|
|
11
|
-
}
|
|
12
|
-
export type BranchErrorCode = "ENV_OVERRIDE" | "DETACHED_HEAD" | "UNBORN_BRANCH" | "GIT_NOT_AVAILABLE" | "NOT_A_GIT_REPO" | "UNKNOWN_ERROR";
|
|
13
|
-
export interface BranchResolutionSuccess {
|
|
14
|
-
branch: string;
|
|
15
|
-
}
|
|
16
|
-
export interface BranchResolutionError {
|
|
17
|
-
error: string;
|
|
18
|
-
code: BranchErrorCode;
|
|
19
|
-
}
|
|
20
|
-
export type BranchResolutionResult = BranchResolutionSuccess | BranchResolutionError;
|
|
21
|
-
/**
|
|
22
|
-
* Resolve workspace root directory.
|
|
23
|
-
* Priority: env vars > .kb directory > .git directory > cwd
|
|
24
|
-
*/
|
|
25
|
-
export declare function resolveWorkspaceRoot(startDir?: string): string;
|
|
26
|
-
/**
|
|
27
|
-
* Resolve workspace root with reason for resolution.
|
|
28
|
-
*/
|
|
29
|
-
export declare function resolveWorkspaceRootInfo(startDir?: string): WorkspaceInfo;
|
|
30
|
-
/**
|
|
31
|
-
* Resolve KB path for a given workspace and branch.
|
|
32
|
-
*/
|
|
33
|
-
export declare function resolveKbPath(workspaceRoot: string, branch: string): string;
|
|
34
|
-
/**
|
|
35
|
-
* Resolve the active branch according to precedence:
|
|
36
|
-
* 1. KIBI_BRANCH env var (if set)
|
|
37
|
-
* 2. Git active branch (from git branch --show-current)
|
|
38
|
-
* 3. Diagnostic failure (no silent fallback)
|
|
39
|
-
*/
|
|
40
|
-
export declare function resolveActiveBranch(workspaceRoot?: string): BranchResolutionResult;
|
|
41
|
-
/**
|
|
42
|
-
* @deprecated defaultBranch is deprecated. Branch lifecycle now follows git naturally.
|
|
43
|
-
* This function is kept for backward compatibility but should not be used for new code.
|
|
44
|
-
*
|
|
45
|
-
* Resolve the default branch using precedence:
|
|
46
|
-
* 1. Configured defaultBranch from config (if set and valid)
|
|
47
|
-
* 2. Git remote HEAD (refs/remotes/origin/HEAD)
|
|
48
|
-
* 3. Fallback to "main"
|
|
49
|
-
*/
|
|
50
|
-
export declare function resolveDefaultBranch(cwd?: string, config?: {
|
|
51
|
-
defaultBranch?: string;
|
|
52
|
-
}): {
|
|
53
|
-
branch: string;
|
|
54
|
-
} | {
|
|
55
|
-
error: string;
|
|
56
|
-
code: string;
|
|
57
|
-
};
|
|
58
|
-
/**
|
|
59
|
-
* Resolve complete KB target with all components.
|
|
60
|
-
*/
|
|
61
|
-
export declare function resolveKbTarget(options?: {
|
|
62
|
-
workspaceRoot?: string;
|
|
63
|
-
branch?: string;
|
|
64
|
-
config?: {
|
|
65
|
-
defaultBranch?: string;
|
|
66
|
-
};
|
|
67
|
-
}): KbTarget;
|
|
68
|
-
/**
|
|
69
|
-
* Check if repository is in detached HEAD state.
|
|
70
|
-
*/
|
|
71
|
-
export declare function isDetachedHead(workspaceRoot?: string): boolean;
|
|
72
|
-
/**
|
|
73
|
-
* Validate a branch name for safety.
|
|
74
|
-
*/
|
|
75
|
-
export declare function isValidBranchName(name: string): boolean;
|
|
76
|
-
/**
|
|
77
|
-
* Get a detailed diagnostic message for branch resolution failures.
|
|
78
|
-
*/
|
|
79
|
-
export declare function getBranchDiagnostic(branch: string | undefined, error: string): string;
|
|
80
|
-
//# sourceMappingURL=target-resolver.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"target-resolver.d.ts","sourceRoot":"","sources":["../../src/kb/target-resolver.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,WAAW,QAAQ;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,eAAe,GACf,eAAe,GACf,mBAAmB,GACnB,gBAAgB,GAChB,eAAe,CAAC;AAEpB,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,eAAe,CAAC;CACvB;AAED,MAAM,MAAM,sBAAsB,GAC9B,uBAAuB,GACvB,qBAAqB,CAAC;AAE1B;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAiB7E;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,GAAE,MAAsB,GAC/B,aAAa,CAiBf;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAW3E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,aAAa,GAAE,MAAsB,GACpC,sBAAsB,CA4GxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,GAAE,MAAsB,EAC3B,MAAM,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAmCtD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE;IACxC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC,GAAG,QAAQ,CAwBX;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,aAAa,GAAE,MAAsB,GAAG,OAAO,CAa7E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAuBvD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,KAAK,EAAE,MAAM,GACZ,MAAM,CAuBR"}
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
-
* Copyright (C) 2026 Piotr Franczyk
|
|
4
|
-
*
|
|
5
|
-
* This program is free software: you can redistribute it and/or modify
|
|
6
|
-
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
-
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
-
* (at your option) any later version.
|
|
9
|
-
*/
|
|
10
|
-
import { execSync } from "node:child_process";
|
|
11
|
-
import * as path from "node:path";
|
|
12
|
-
const WORKSPACE_ENV_KEYS = [
|
|
13
|
-
"KIBI_WORKSPACE",
|
|
14
|
-
"KIBI_PROJECT_ROOT",
|
|
15
|
-
"KIBI_ROOT",
|
|
16
|
-
];
|
|
17
|
-
const KB_PATH_ENV_KEYS = ["KIBI_KB_PATH", "KB_PATH"];
|
|
18
|
-
/**
|
|
19
|
-
* Resolve workspace root directory.
|
|
20
|
-
* Priority: env vars > .kb directory > .git directory > cwd
|
|
21
|
-
*/
|
|
22
|
-
export function resolveWorkspaceRoot(startDir = process.cwd()) {
|
|
23
|
-
const envRoot = readFirstEnv(WORKSPACE_ENV_KEYS);
|
|
24
|
-
if (envRoot) {
|
|
25
|
-
return path.resolve(envRoot);
|
|
26
|
-
}
|
|
27
|
-
const kbRoot = findUpwards(startDir, ".kb");
|
|
28
|
-
if (kbRoot) {
|
|
29
|
-
return kbRoot;
|
|
30
|
-
}
|
|
31
|
-
const gitRoot = findUpwards(startDir, ".git");
|
|
32
|
-
if (gitRoot) {
|
|
33
|
-
return gitRoot;
|
|
34
|
-
}
|
|
35
|
-
return path.resolve(startDir);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Resolve workspace root with reason for resolution.
|
|
39
|
-
*/
|
|
40
|
-
export function resolveWorkspaceRootInfo(startDir = process.cwd()) {
|
|
41
|
-
const envRoot = readFirstEnv(WORKSPACE_ENV_KEYS);
|
|
42
|
-
if (envRoot) {
|
|
43
|
-
return { root: path.resolve(envRoot), reason: "env" };
|
|
44
|
-
}
|
|
45
|
-
const kbRoot = findUpwards(startDir, ".kb");
|
|
46
|
-
if (kbRoot) {
|
|
47
|
-
return { root: kbRoot, reason: "kb" };
|
|
48
|
-
}
|
|
49
|
-
const gitRoot = findUpwards(startDir, ".git");
|
|
50
|
-
if (gitRoot) {
|
|
51
|
-
return { root: gitRoot, reason: "git" };
|
|
52
|
-
}
|
|
53
|
-
return { root: path.resolve(startDir), reason: "cwd" };
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Resolve KB path for a given workspace and branch.
|
|
57
|
-
*/
|
|
58
|
-
export function resolveKbPath(workspaceRoot, branch) {
|
|
59
|
-
const envPath = readFirstEnv(KB_PATH_ENV_KEYS);
|
|
60
|
-
if (envPath) {
|
|
61
|
-
const resolved = path.resolve(envPath);
|
|
62
|
-
if (isBranchPath(resolved)) {
|
|
63
|
-
return resolved;
|
|
64
|
-
}
|
|
65
|
-
return path.join(resolved, "branches", branch);
|
|
66
|
-
}
|
|
67
|
-
return path.join(workspaceRoot, ".kb", "branches", branch);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Resolve the active branch according to precedence:
|
|
71
|
-
* 1. KIBI_BRANCH env var (if set)
|
|
72
|
-
* 2. Git active branch (from git branch --show-current)
|
|
73
|
-
* 3. Diagnostic failure (no silent fallback)
|
|
74
|
-
*/
|
|
75
|
-
export function resolveActiveBranch(workspaceRoot = process.cwd()) {
|
|
76
|
-
// 1. Check KIBI_BRANCH env var first (highest precedence)
|
|
77
|
-
const envBranch = process.env.KIBI_BRANCH?.trim();
|
|
78
|
-
if (envBranch) {
|
|
79
|
-
if (!isValidBranchName(envBranch)) {
|
|
80
|
-
return {
|
|
81
|
-
error: `Invalid branch name from KIBI_BRANCH environment variable: '${envBranch}'`,
|
|
82
|
-
code: "ENV_OVERRIDE",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
return { branch: envBranch };
|
|
86
|
-
}
|
|
87
|
-
// 2. Try to get the current git branch
|
|
88
|
-
try {
|
|
89
|
-
const branch = execSync("git branch --show-current", {
|
|
90
|
-
cwd: workspaceRoot,
|
|
91
|
-
encoding: "utf8",
|
|
92
|
-
timeout: 5000,
|
|
93
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
94
|
-
}).trim();
|
|
95
|
-
if (!branch) {
|
|
96
|
-
return {
|
|
97
|
-
error: getBranchDiagnostic(undefined, "Git is in detached HEAD state"),
|
|
98
|
-
code: "DETACHED_HEAD",
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
if (!isValidBranchName(branch)) {
|
|
102
|
-
return {
|
|
103
|
-
error: `Invalid branch name detected: '${branch}'`,
|
|
104
|
-
code: "UNKNOWN_ERROR",
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
// Normalize 'master' to 'main' for consistency
|
|
108
|
-
const normalizedBranch = branch === "master" ? "main" : branch;
|
|
109
|
-
return { branch: normalizedBranch };
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
// Try alternative: git rev-parse --abbrev-ref HEAD
|
|
113
|
-
try {
|
|
114
|
-
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
115
|
-
cwd: workspaceRoot,
|
|
116
|
-
encoding: "utf8",
|
|
117
|
-
timeout: 5000,
|
|
118
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
119
|
-
}).trim();
|
|
120
|
-
if (branch === "HEAD") {
|
|
121
|
-
return {
|
|
122
|
-
error: getBranchDiagnostic(undefined, "Git is in detached HEAD state"),
|
|
123
|
-
code: "DETACHED_HEAD",
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
if (!branch || branch === "") {
|
|
127
|
-
return {
|
|
128
|
-
error: getBranchDiagnostic(undefined, "Unable to determine git branch"),
|
|
129
|
-
code: "UNBORN_BRANCH",
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
if (!isValidBranchName(branch)) {
|
|
133
|
-
return {
|
|
134
|
-
error: `Invalid branch name detected: '${branch}'`,
|
|
135
|
-
code: "UNKNOWN_ERROR",
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
const normalizedBranch = branch === "master" ? "main" : branch;
|
|
139
|
-
return { branch: normalizedBranch };
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
143
|
-
if (errorMessage.includes("not a git repository")) {
|
|
144
|
-
return {
|
|
145
|
-
error: getBranchDiagnostic(undefined, "Not a git repository"),
|
|
146
|
-
code: "NOT_A_GIT_REPO",
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
if (errorMessage.includes("command not found") ||
|
|
150
|
-
errorMessage.includes("ENOENT")) {
|
|
151
|
-
return {
|
|
152
|
-
error: getBranchDiagnostic(undefined, "Git is not installed or not available in PATH"),
|
|
153
|
-
code: "GIT_NOT_AVAILABLE",
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
error: getBranchDiagnostic(undefined, errorMessage),
|
|
158
|
-
code: "UNKNOWN_ERROR",
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* @deprecated defaultBranch is deprecated. Branch lifecycle now follows git naturally.
|
|
165
|
-
* This function is kept for backward compatibility but should not be used for new code.
|
|
166
|
-
*
|
|
167
|
-
* Resolve the default branch using precedence:
|
|
168
|
-
* 1. Configured defaultBranch from config (if set and valid)
|
|
169
|
-
* 2. Git remote HEAD (refs/remotes/origin/HEAD)
|
|
170
|
-
* 3. Fallback to "main"
|
|
171
|
-
*/
|
|
172
|
-
export function resolveDefaultBranch(cwd = process.cwd(), config) {
|
|
173
|
-
// 1. Check config.defaultBranch first
|
|
174
|
-
const configuredBranch = config?.defaultBranch?.trim();
|
|
175
|
-
if (configuredBranch) {
|
|
176
|
-
if (!isValidBranchName(configuredBranch)) {
|
|
177
|
-
return {
|
|
178
|
-
error: `Invalid defaultBranch configured in .kb/config.json: '${configuredBranch}'`,
|
|
179
|
-
code: "INVALID_CONFIG",
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
return { branch: configuredBranch };
|
|
183
|
-
}
|
|
184
|
-
// 2. Try to get the remote default branch from origin/HEAD
|
|
185
|
-
try {
|
|
186
|
-
const remoteHead = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
187
|
-
cwd,
|
|
188
|
-
encoding: "utf8",
|
|
189
|
-
timeout: 5000,
|
|
190
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
191
|
-
}).trim();
|
|
192
|
-
const match = remoteHead.match(/^refs\/remotes\/origin\/(.+)$/);
|
|
193
|
-
if (match) {
|
|
194
|
-
const branch = match[1];
|
|
195
|
-
if (isValidBranchName(branch)) {
|
|
196
|
-
return { branch };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
// origin/HEAD doesn't exist or command failed
|
|
202
|
-
}
|
|
203
|
-
// 3. Final fallback to "main"
|
|
204
|
-
return { branch: "main" };
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Resolve complete KB target with all components.
|
|
208
|
-
*/
|
|
209
|
-
export function resolveKbTarget(options) {
|
|
210
|
-
const workspaceRoot = options?.workspaceRoot ?? resolveWorkspaceRoot();
|
|
211
|
-
let branch;
|
|
212
|
-
let reason;
|
|
213
|
-
if (options?.branch) {
|
|
214
|
-
branch = options.branch;
|
|
215
|
-
reason = "env";
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
const branchResult = resolveActiveBranch(workspaceRoot);
|
|
219
|
-
if ("error" in branchResult) {
|
|
220
|
-
throw new Error(`Failed to resolve active branch: ${branchResult.error}. ` +
|
|
221
|
-
`Ensure you are in a git repository with a valid branch checked out.`);
|
|
222
|
-
}
|
|
223
|
-
branch = branchResult.branch;
|
|
224
|
-
reason = "git";
|
|
225
|
-
}
|
|
226
|
-
const kbPath = resolveKbPath(workspaceRoot, branch);
|
|
227
|
-
return { workspaceRoot, branch, kbPath, reason };
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Check if repository is in detached HEAD state.
|
|
231
|
-
*/
|
|
232
|
-
export function isDetachedHead(workspaceRoot = process.cwd()) {
|
|
233
|
-
try {
|
|
234
|
-
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
235
|
-
cwd: workspaceRoot,
|
|
236
|
-
encoding: "utf8",
|
|
237
|
-
timeout: 5000,
|
|
238
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
239
|
-
}).trim();
|
|
240
|
-
return branch === "HEAD";
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
return true;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Validate a branch name for safety.
|
|
248
|
-
*/
|
|
249
|
-
export function isValidBranchName(name) {
|
|
250
|
-
if (!name || name.length === 0 || name.length > 255)
|
|
251
|
-
return false;
|
|
252
|
-
// Reject path traversal attempts
|
|
253
|
-
if (name.includes("..") || path.isAbsolute(name) || name.startsWith("/")) {
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
// Only allow safe characters
|
|
257
|
-
if (!/^[a-zA-Z0-9._\-/+]+$/.test(name))
|
|
258
|
-
return false;
|
|
259
|
-
// Reject problematic patterns
|
|
260
|
-
if (name.includes("//") ||
|
|
261
|
-
name.endsWith("/") ||
|
|
262
|
-
name.endsWith(".") ||
|
|
263
|
-
name.includes("\\") ||
|
|
264
|
-
name.startsWith("-")) {
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Get a detailed diagnostic message for branch resolution failures.
|
|
271
|
-
*/
|
|
272
|
-
export function getBranchDiagnostic(branch, error) {
|
|
273
|
-
const lines = ["Branch Resolution Failed", "", `Reason: ${error}`];
|
|
274
|
-
if (branch) {
|
|
275
|
-
lines.push(`Detected branch: ${branch}`);
|
|
276
|
-
}
|
|
277
|
-
lines.push("", "Resolution options:", "1. Set KIBI_BRANCH environment variable to explicitly specify the branch:", " export KIBI_BRANCH=main", "", "2. Ensure you are in a git repository with a valid checked-out branch", "", "3. If in detached HEAD state, create or checkout a branch:", " git checkout -b my-branch", "", "4. For non-git workspaces, always use KIBI_BRANCH:", " KIBI_BRANCH=feature-branch kibi sync");
|
|
278
|
-
return lines.join("\n");
|
|
279
|
-
}
|
|
280
|
-
function readFirstEnv(keys) {
|
|
281
|
-
for (const key of keys) {
|
|
282
|
-
const value = process.env[key]?.trim();
|
|
283
|
-
if (value) {
|
|
284
|
-
return value;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
function findUpwards(startDir, marker) {
|
|
290
|
-
let current = path.resolve(startDir);
|
|
291
|
-
while (true) {
|
|
292
|
-
const candidate = path.join(current, marker);
|
|
293
|
-
try {
|
|
294
|
-
// Check if path exists (works for both files and directories)
|
|
295
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
296
|
-
import("node:fs").then((fs) => {
|
|
297
|
-
fs.accessSync(candidate);
|
|
298
|
-
});
|
|
299
|
-
return current;
|
|
300
|
-
}
|
|
301
|
-
catch {
|
|
302
|
-
const parent = path.dirname(current);
|
|
303
|
-
if (parent === current) {
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
current = parent;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
function isBranchPath(p) {
|
|
311
|
-
const parent = path.basename(path.dirname(p));
|
|
312
|
-
return parent === "branches";
|
|
313
|
-
}
|