@kb-labs/core-workspace 1.7.0 → 1.8.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @kb-labs/core-workspace
2
2
 
3
- > **Core workspace utilities for KB Labs, including root directory resolution and workspace detection.** Provides utilities for locating the KB Labs workspace umbrella directory for internal tooling consumption.
3
+ > **Core workspace utilities for KB Labs, including platform/project root resolution.** Provides utilities for locating the KB Labs platform installation and the user's project directory for internal tooling consumption (CLI, services, dev tools).
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
6
  [![Node.js](https://img.shields.io/badge/Node.js-18.18.0+-green.svg)](https://nodejs.org/)
@@ -8,32 +8,65 @@
8
8
 
9
9
  ## 🎯 Vision & Purpose
10
10
 
11
- **@kb-labs/core-workspace** provides workspace utilities for KB Labs internal tooling (CLI, REST, Studio). It resolves the workspace root directory by detecting workspace markers (`.git`, `pnpm-workspace.yaml`, `package.json`) and provides workspace metadata.
11
+ **@kb-labs/core-workspace** provides root-directory resolution utilities shared by CLI, services, and `kb-dev`.
12
12
 
13
- ## 🏗️ Architecture
13
+ KB Labs distinguishes **two** logical roots, which may coincide (dev mode) or differ (installed mode):
14
14
 
15
- ### Core Components
15
+ | Root | What it is | Where it lives |
16
+ | --------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
17
+ | **platformRoot** | Directory that contains the installed KB Labs platform code (parent of `node_modules/@kb-labs/*`) | `/opt/kb-labs` (installed), workspace root (dev) |
18
+ | **projectRoot** | Directory that contains the user's `.kb/kb.config.json` and all per-project plugin state | `~/work/my-app` (installed), workspace root (dev) |
16
19
 
17
- #### Workspace Root Resolver
20
+ **Why two roots?** Plugin discovery needs to scan the platform's `node_modules`, while platform state (`.kb/qa`, `.kb/mind`, `.kb/marketplace.lock`, adapter storage paths like `.kb/database/kb.sqlite`) belongs to the user's project. Until the platform moved out of the workspace, both assumptions resolved to the same `cwd` — and the codebase silently conflated them. This package makes the distinction explicit.
18
21
 
19
- - **Purpose**: Resolve workspace root directory
20
- - **Responsibilities**: Detect workspace markers, return root path
21
- - **Dependencies**: `core-sys` for repository utilities
22
+ ## 🏗️ Architecture
22
23
 
23
- ### Design Patterns
24
+ ### Core Functions
24
25
 
25
- - **Utility Pattern**: Pure utility functions
26
- - **Strategy Pattern**: Multiple detection strategies
26
+ #### `resolveRoots(options)` — main entry point
27
27
 
28
- ### Data Flow
28
+ Resolves both `platformRoot` and `projectRoot` in a single call. This is what the CLI bootstrap, service bootstrap, and `kb-dev` should use.
29
29
 
30
+ ```typescript
31
+ const { platformRoot, projectRoot, sameLocation, sources } = await resolveRoots({
32
+ moduleUrl: import.meta.url, // from the caller's binary/entry
33
+ startDir: process.cwd(),
34
+ })
30
35
  ```
31
- resolveWorkspaceRoot({ startDir, stopDir })
32
- │
33
- ├──► Walk up directory tree
34
- ├──► Check for markers (.git, pnpm-workspace.yaml, package.json)
35
- ├──► Return workspace root
36
- ```
36
+
37
+ - `sameLocation: true` ⇒ dev mode (both roots are the same directory).
38
+ - `sources.platform` / `sources.project` describe how each root was discovered (`'explicit' | 'env' | 'module' | 'marker' | 'config' | 'repo' | 'fallback'`).
39
+
40
+ #### `resolvePlatformRoot(options)`
41
+
42
+ Finds the directory that contains the installed KB Labs platform (parent of `node_modules/@kb-labs/*`).
43
+
44
+ Priority chain:
45
+
46
+ 1. Explicit `cwd` option.
47
+ 2. `KB_PLATFORM_ROOT` env var (set by the installer wrapper in installed mode).
48
+ 3. Walk up from `moduleUrl` (`import.meta.url` of the caller). This walks all the way up and:
49
+ - Prefers the **top-most** `pnpm-workspace.yaml` if one exists (handles KB Labs "workspace of workspaces" dev layout).
50
+ - Falls back to the first directory whose `node_modules` contains a known platform marker (`@kb-labs/cli-bin`, `@kb-labs/core-runtime`) — this is the normal installed-mode path.
51
+ 4. Walk up from `startDir` looking for the same markers or `pnpm-workspace.yaml`.
52
+ 5. Repository root via `findRepoRoot`.
53
+ 6. Fallback to `startDir`.
54
+
55
+ #### `resolveProjectRoot(options)`
56
+
57
+ Finds the user's project root — the directory containing `.kb/kb.config.json`.
58
+
59
+ Priority chain:
60
+
61
+ 1. Explicit `cwd` option.
62
+ 2. Env vars: `KB_PROJECT_ROOT` (preferred) or legacy `KB_LABS_WORKSPACE_ROOT` / `KB_LABS_REPO_ROOT`.
63
+ 3. Nearest `.kb/kb.config.json` ancestor walking up from `startDir`.
64
+ 4. Repository root via `findRepoRoot`.
65
+ 5. Fallback to `startDir`.
66
+
67
+ #### `resolveWorkspaceRoot(options)` — deprecated alias
68
+
69
+ Kept as an alias of `resolveProjectRoot` for backwards compatibility. Prefer the explicit name.
37
70
 
38
71
  ## 🚀 Quick Start
39
72
 
@@ -46,32 +79,51 @@ pnpm add @kb-labs/core-workspace
46
79
  ### Basic Usage
47
80
 
48
81
  ```typescript
49
- import { resolveWorkspaceRoot } from '@kb-labs/core-workspace';
82
+ import { resolveRoots } from '@kb-labs/core-workspace'
50
83
 
51
- const { root, found } = await resolveWorkspaceRoot({
52
- startDir: process.cwd()
53
- });
84
+ // In a CLI entrypoint (bin.ts):
85
+ const { platformRoot, projectRoot, sameLocation } = await resolveRoots({
86
+ moduleUrl: import.meta.url,
87
+ startDir: process.cwd(),
88
+ })
54
89
 
55
- if (found) {
56
- console.log('Workspace root:', root);
57
- }
90
+ console.log({ platformRoot, projectRoot, sameLocation })
58
91
  ```
59
92
 
60
- ## ✨ Features
93
+ ### In dev mode (monorepo workspace)
61
94
 
62
- - **Multiple Markers**: Detects workspace via `.git`, `pnpm-workspace.yaml`, `package.json`
63
- - **Flexible Resolution**: Supports start and stop directories
64
- - **Workspace Metadata**: Provides workspace filesystem information
95
+ ```text
96
+ platformRoot === projectRoot === <workspace-root>
97
+ sameLocation: true
98
+ sources: { platform: 'module', project: 'config' }
99
+ ```
65
100
 
66
- ## 🔧 Configuration
101
+ ### In installed mode
67
102
 
68
- ### Configuration Options
103
+ ```text
104
+ platformRoot: /opt/kb-labs (from KB_PLATFORM_ROOT or moduleUrl walk-up)
105
+ projectRoot: /home/user/work/my-project (from .kb/kb.config.json walk-up)
106
+ sameLocation: false
107
+ ```
108
+
109
+ ## ✨ Features
69
110
 
70
- No configuration needed (utilities only).
111
+ - **Two-root resolution**: clearly distinguishes platform code location from project state location.
112
+ - **Multiple discovery signals**: explicit options, env vars, `import.meta.url` walk-up, marker walk-up, repo root.
113
+ - **Dev/installed parity**: same API works in workspace dev mode and in deployed/installed mode; callers don't need to branch.
114
+ - **Nested-workspace aware**: handles the KB Labs "workspace of workspaces" layout where sub-repos each have their own `pnpm-workspace.yaml`.
115
+ - **Backwards compatible**: legacy `resolveWorkspaceRoot` and `KB_LABS_WORKSPACE_ROOT` / `KB_LABS_REPO_ROOT` env vars still work.
116
+
117
+ ## 🔧 Configuration
71
118
 
72
119
  ### Environment Variables
73
120
 
74
- None.
121
+ | Variable | Purpose |
122
+ | ------------------------- | ----------------------------------------------------------------------------- |
123
+ | `KB_PLATFORM_ROOT` | Override platform root (installer wrapper, CI). |
124
+ | `KB_PROJECT_ROOT` | Override project root. |
125
+ | `KB_LABS_WORKSPACE_ROOT` | Legacy alias for `KB_PROJECT_ROOT` (kept for backwards compatibility). |
126
+ | `KB_LABS_REPO_ROOT` | Legacy alias for `KB_PROJECT_ROOT` (kept for backwards compatibility). |
75
127
 
76
128
  ## 🤝 Contributing
77
129
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,38 @@
1
- interface WorkspaceRootResolution {
1
+ /**
2
+ * @module @kb-labs/core-workspace/types
3
+ *
4
+ * Types for workspace/platform/project root resolution.
5
+ *
6
+ * KB Labs distinguishes **two** logical roots:
7
+ *
8
+ * - **platformRoot** — the directory that contains the installed KB Labs
9
+ * platform code (i.e. the parent of `node_modules/@kb-labs/*`). This is the
10
+ * place from which plugin discovery should scan `node_modules`, and where
11
+ * cloud/CI deployments may place per-installation platform defaults.
12
+ *
13
+ * - **projectRoot** — the directory that contains the user's `.kb/` folder,
14
+ * including `.kb/kb.config.json` and any runtime state written by plugins
15
+ * (`.kb/qa`, `.kb/mind`, `.kb/release`, `.kb/marketplace.lock`, etc.).
16
+ * This is *where the user is working*.
17
+ *
18
+ * In development (monorepo workspace) mode these two roots typically resolve
19
+ * to the same directory. In installed mode they are different — the platform
20
+ * lives under e.g. `/opt/kb-labs` while the project lives under
21
+ * `~/work/my-app`.
22
+ */
23
+ /**
24
+ * Minimal filesystem façade used by the resolvers. Allows tests to inject a
25
+ * virtual filesystem without touching `node:fs` directly.
26
+ */
27
+ interface WorkspaceFs {
28
+ exists(path: string): Promise<boolean>;
29
+ }
30
+ type ProjectRootSource = 'explicit' | 'env' | 'config' | 'repo' | 'fallback';
31
+ interface ProjectRootResolution {
2
32
  rootDir: string;
3
- source: 'explicit' | 'env' | 'config' | 'repo' | 'fallback';
33
+ source: ProjectRootSource;
4
34
  }
5
- interface ResolveWorkspaceRootOptions {
35
+ interface ResolveProjectRootOptions {
6
36
  /**
7
37
  * Explicit root directory to use (e.g. from CLI flag). Highest priority.
8
38
  */
@@ -24,20 +54,135 @@ interface ResolveWorkspaceRootOptions {
24
54
  */
25
55
  verbose?: boolean;
26
56
  }
27
- interface WorkspaceFs {
28
- exists(path: string): Promise<boolean>;
57
+ /**
58
+ * @deprecated Use {@link ResolveProjectRootOptions}. Alias kept for backwards
59
+ * compatibility with `resolveWorkspaceRoot`.
60
+ */
61
+ type ResolveWorkspaceRootOptions = ResolveProjectRootOptions;
62
+ /**
63
+ * @deprecated Use {@link ProjectRootResolution}.
64
+ */
65
+ type WorkspaceRootResolution = ProjectRootResolution;
66
+ type PlatformRootSource = 'explicit' | 'env' | 'module' | 'marker' | 'repo' | 'fallback';
67
+ interface PlatformRootResolution {
68
+ rootDir: string;
69
+ source: PlatformRootSource;
70
+ }
71
+ interface ResolvePlatformRootOptions {
72
+ /**
73
+ * Explicit root directory to use (e.g. from CLI flag). Highest priority.
74
+ */
75
+ cwd?: string;
76
+ /**
77
+ * `import.meta.url` of the calling module (typically the CLI entrypoint or
78
+ * a service bootstrap file). Used to locate the installed
79
+ * `node_modules/@kb-labs/*` tree by walking up from the module's physical
80
+ * location. This is the **most reliable** signal in installed mode because
81
+ * it does not depend on process cwd or directory layout assumptions.
82
+ */
83
+ moduleUrl?: string;
84
+ /**
85
+ * Starting directory for marker-based discovery. Defaults to process.cwd().
86
+ */
87
+ startDir?: string;
88
+ /**
89
+ * Environment variables map. Defaults to process.env.
90
+ */
91
+ env?: Record<string, string | undefined>;
92
+ /**
93
+ * Optionally inject custom filesystem helpers for testing.
94
+ */
95
+ fs?: WorkspaceFs;
96
+ /**
97
+ * When true, includes additional metadata in errors/logs.
98
+ */
99
+ verbose?: boolean;
100
+ }
101
+ interface ResolveRootsOptions extends ResolvePlatformRootOptions, ResolveProjectRootOptions {
102
+ }
103
+ interface RootsResolution {
104
+ /** Where `node_modules/@kb-labs/*` lives. */
105
+ platformRoot: string;
106
+ /** Where `.kb/kb.config.json` and per-project state live. */
107
+ projectRoot: string;
108
+ /**
109
+ * `true` when both roots resolve to the same physical directory — this is
110
+ * the normal case in the KB Labs monorepo dev mode.
111
+ */
112
+ sameLocation: boolean;
113
+ /** How each root was resolved (for diagnostics). */
114
+ sources: {
115
+ platform: PlatformRootSource;
116
+ project: ProjectRootSource;
117
+ };
29
118
  }
30
119
 
31
120
  /**
32
- * Resolve the workspace root for kb-labs tooling.
121
+ * Resolve the project root — the directory that contains the user's
122
+ * `.kb/kb.config.json` and plugin state.
33
123
  *
34
124
  * Priority order:
35
- * 1. Explicit `cwd` option (e.g. CLI flag)
36
- * 2. Environment variables (`KB_LABS_WORKSPACE_ROOT`, `KB_LABS_REPO_ROOT`)
37
- * 3. Nearest `.kb/kb-labs.config.json` ancestor
38
- * 4. Repository root discovered via `findRepoRoot`
39
- * 5. Fallback to provided `startDir` / process cwd
125
+ *
126
+ * 1. Explicit `cwd` option (e.g. CLI flag).
127
+ * 2. Environment variables: `KB_PROJECT_ROOT` (preferred), then the legacy
128
+ * `KB_LABS_WORKSPACE_ROOT` / `KB_LABS_REPO_ROOT`.
129
+ * 3. Nearest `.kb/kb.config.json` ancestor walking up from `startDir`.
130
+ * 4. Repository root discovered via `findRepoRoot` (pnpm workspace, `.git`,
131
+ * `package.json`).
132
+ * 5. Fallback to `startDir` / `process.cwd()`.
133
+ *
134
+ * In monorepo dev mode this typically returns the workspace root. In installed
135
+ * mode it returns the user's project directory.
136
+ */
137
+ declare function resolveProjectRoot(options?: ResolveProjectRootOptions): Promise<ProjectRootResolution>;
138
+ /**
139
+ * @deprecated Use {@link resolveProjectRoot}. This alias is kept for
140
+ * backwards compatibility; semantics are identical.
141
+ *
142
+ * The name "workspace root" is ambiguous — in KB Labs terminology, what it
143
+ * actually refers to is the project root (where `.kb/kb.config.json` lives),
144
+ * not the KB Labs platform installation. Prefer the explicit name.
40
145
  */
41
146
  declare function resolveWorkspaceRoot(options?: ResolveWorkspaceRootOptions): Promise<WorkspaceRootResolution>;
147
+ /**
148
+ * Resolve the platform root — the directory that contains the installed
149
+ * KB Labs platform code (i.e. the parent of `node_modules/@kb-labs/*`).
150
+ *
151
+ * Priority order:
152
+ *
153
+ * 1. Explicit `cwd` option (e.g. CLI flag).
154
+ * 2. Environment variable `KB_PLATFORM_ROOT` (typically set by the installer
155
+ * wrapper script in installed mode).
156
+ * 3. Walk up from `moduleUrl` (e.g. `import.meta.url` of the CLI `bin.ts`),
157
+ * looking for a directory whose `node_modules` contains a known platform
158
+ * package. This is the most reliable signal in installed mode because it
159
+ * does not depend on `process.cwd()`.
160
+ * 4. Walk up from `startDir` looking for either a platform marker or
161
+ * `pnpm-workspace.yaml` (the latter covers monorepo dev mode).
162
+ * 5. Repository root discovered via `findRepoRoot`.
163
+ * 6. Fallback to `startDir` / `process.cwd()`.
164
+ *
165
+ * In monorepo dev mode steps 4 or 5 will typically match the workspace root,
166
+ * so `platformRoot` and `projectRoot` resolve to the same directory.
167
+ */
168
+ declare function resolvePlatformRoot(options?: ResolvePlatformRootOptions): Promise<PlatformRootResolution>;
169
+ /**
170
+ * Resolve both the platform root and the project root in a single call. This
171
+ * is the main entry point for the CLI, service bootstrap, and `kb-dev`.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * // In bin.ts (the CLI entrypoint):
176
+ * const { platformRoot, projectRoot } = await resolveRoots({
177
+ * moduleUrl: import.meta.url,
178
+ * startDir: process.cwd(),
179
+ * })
180
+ * ```
181
+ *
182
+ * In dev mode `platformRoot === projectRoot` and `sameLocation` is `true`.
183
+ * In installed mode they differ: `platformRoot` points at the platform
184
+ * installation while `projectRoot` points at the user's project directory.
185
+ */
186
+ declare function resolveRoots(options?: ResolveRootsOptions): Promise<RootsResolution>;
42
187
 
43
- export { type ResolveWorkspaceRootOptions, type WorkspaceRootResolution, resolveWorkspaceRoot };
188
+ export { type PlatformRootResolution, type PlatformRootSource, type ProjectRootResolution, type ProjectRootSource, type ResolvePlatformRootOptions, type ResolveProjectRootOptions, type ResolveRootsOptions, type ResolveWorkspaceRootOptions, type RootsResolution, type WorkspaceFs, type WorkspaceRootResolution, resolvePlatformRoot, resolveProjectRoot, resolveRoots, resolveWorkspaceRoot };
package/dist/index.js CHANGED
@@ -1,9 +1,14 @@
1
1
  import path from 'path';
2
2
  import { promises } from 'fs';
3
+ import { fileURLToPath } from 'url';
3
4
  import { findRepoRoot } from '@kb-labs/core-sys';
4
5
 
5
6
  // src/workspace/root-resolver.ts
6
7
  var WORKSPACE_CONFIG_RELATIVE = path.join(".kb", "kb.config.json");
8
+ var PLATFORM_PACKAGE_MARKERS = [
9
+ "@kb-labs/cli-bin",
10
+ "@kb-labs/core-runtime"
11
+ ];
7
12
  var defaultFs = {
8
13
  async exists(target) {
9
14
  try {
@@ -14,11 +19,11 @@ var defaultFs = {
14
19
  }
15
20
  }
16
21
  };
17
- function resolveEnvRoot(env) {
22
+ function resolveProjectEnvRoot(env) {
18
23
  if (!env) {
19
24
  return void 0;
20
25
  }
21
- return env.KB_LABS_WORKSPACE_ROOT ?? env.KB_LABS_REPO_ROOT;
26
+ return env.KB_PROJECT_ROOT ?? env.KB_LABS_WORKSPACE_ROOT ?? env.KB_LABS_REPO_ROOT;
22
27
  }
23
28
  async function findConfigRoot(startDir, fs) {
24
29
  let current = path.resolve(startDir);
@@ -34,7 +39,7 @@ async function findConfigRoot(startDir, fs) {
34
39
  current = parent;
35
40
  }
36
41
  }
37
- async function resolveWorkspaceRoot(options = {}) {
42
+ async function resolveProjectRoot(options = {}) {
38
43
  const {
39
44
  cwd,
40
45
  env = process.env,
@@ -47,7 +52,7 @@ async function resolveWorkspaceRoot(options = {}) {
47
52
  source: "explicit"
48
53
  };
49
54
  }
50
- const envRoot = resolveEnvRoot(env);
55
+ const envRoot = resolveProjectEnvRoot(env);
51
56
  if (envRoot) {
52
57
  return {
53
58
  rootDir: path.resolve(envRoot),
@@ -78,7 +83,128 @@ async function resolveWorkspaceRoot(options = {}) {
78
83
  source: "fallback"
79
84
  };
80
85
  }
86
+ async function resolveWorkspaceRoot(options = {}) {
87
+ return resolveProjectRoot(options);
88
+ }
89
+ async function hasPlatformMarker(candidate, fs) {
90
+ for (const marker of PLATFORM_PACKAGE_MARKERS) {
91
+ const markerPath = path.join(candidate, "node_modules", marker);
92
+ if (await fs.exists(markerPath)) {
93
+ return true;
94
+ }
95
+ }
96
+ return false;
97
+ }
98
+ async function walkUpForPlatformMarker(startDir, fs) {
99
+ let current = path.resolve(startDir);
100
+ while (true) {
101
+ if (await hasPlatformMarker(current, fs)) {
102
+ return current;
103
+ }
104
+ if (await fs.exists(path.join(current, "pnpm-workspace.yaml"))) {
105
+ return current;
106
+ }
107
+ const parent = path.dirname(current);
108
+ if (parent === current) {
109
+ return void 0;
110
+ }
111
+ current = parent;
112
+ }
113
+ }
114
+ async function resolvePlatformRootFromModuleUrl(moduleUrl, fs) {
115
+ let modulePath;
116
+ try {
117
+ modulePath = fileURLToPath(moduleUrl);
118
+ } catch {
119
+ return void 0;
120
+ }
121
+ let current = path.dirname(modulePath);
122
+ let firstMarkerHit;
123
+ let topMostWorkspace;
124
+ while (true) {
125
+ if (!firstMarkerHit && await hasPlatformMarker(current, fs)) {
126
+ firstMarkerHit = current;
127
+ }
128
+ if (await fs.exists(path.join(current, "pnpm-workspace.yaml"))) {
129
+ topMostWorkspace = current;
130
+ }
131
+ const parent = path.dirname(current);
132
+ if (parent === current) {
133
+ return topMostWorkspace ?? firstMarkerHit;
134
+ }
135
+ current = parent;
136
+ }
137
+ }
138
+ async function resolvePlatformRoot(options = {}) {
139
+ const {
140
+ cwd,
141
+ moduleUrl,
142
+ env = process.env,
143
+ fs = defaultFs,
144
+ startDir = process.cwd()
145
+ } = options;
146
+ if (cwd) {
147
+ return {
148
+ rootDir: path.resolve(cwd),
149
+ source: "explicit"
150
+ };
151
+ }
152
+ const envRoot = env?.KB_PLATFORM_ROOT;
153
+ if (envRoot) {
154
+ return {
155
+ rootDir: path.resolve(envRoot),
156
+ source: "env"
157
+ };
158
+ }
159
+ if (moduleUrl) {
160
+ const fromModule = await resolvePlatformRootFromModuleUrl(moduleUrl, fs);
161
+ if (fromModule) {
162
+ return {
163
+ rootDir: fromModule,
164
+ source: "module"
165
+ };
166
+ }
167
+ }
168
+ const fromMarker = await walkUpForPlatformMarker(startDir, fs);
169
+ if (fromMarker) {
170
+ return {
171
+ rootDir: fromMarker,
172
+ source: "marker"
173
+ };
174
+ }
175
+ try {
176
+ const repoRoot = await findRepoRoot(startDir);
177
+ const resolvedRepo = path.resolve(repoRoot);
178
+ const fsRoot = path.parse(resolvedRepo).root;
179
+ if (resolvedRepo !== fsRoot) {
180
+ return {
181
+ rootDir: resolvedRepo,
182
+ source: "repo"
183
+ };
184
+ }
185
+ } catch {
186
+ }
187
+ return {
188
+ rootDir: path.resolve(startDir),
189
+ source: "fallback"
190
+ };
191
+ }
192
+ async function resolveRoots(options = {}) {
193
+ const [platform, project] = await Promise.all([
194
+ resolvePlatformRoot(options),
195
+ resolveProjectRoot(options)
196
+ ]);
197
+ return {
198
+ platformRoot: platform.rootDir,
199
+ projectRoot: project.rootDir,
200
+ sameLocation: path.resolve(platform.rootDir) === path.resolve(project.rootDir),
201
+ sources: {
202
+ platform: platform.source,
203
+ project: project.source
204
+ }
205
+ };
206
+ }
81
207
 
82
- export { resolveWorkspaceRoot };
208
+ export { resolvePlatformRoot, resolveProjectRoot, resolveRoots, resolveWorkspaceRoot };
83
209
  //# sourceMappingURL=index.js.map
84
210
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/workspace/root-resolver.ts"],"names":["fsp"],"mappings":";;;;;AAWA,IAAM,yBAAA,GAA4B,IAAA,CAAK,IAAA,CAAK,KAAA,EAAO,gBAAgB,CAAA;AAEnE,IAAM,SAAA,GAAyB;AAAA,EAC7B,MAAM,OAAO,MAAA,EAAkC;AAC7C,IAAA,IAAI;AACF,MAAA,MAAMA,QAAA,CAAI,OAAO,MAAM,CAAA;AACvB,MAAA,OAAO,IAAA;AAAA,IACT,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AACF,CAAA;AAEA,SAAS,eACP,GAAA,EACoB;AACpB,EAAA,IAAI,CAAC,GAAA,EAAK;AAAC,IAAA,OAAO,MAAA;AAAA,EAAS;AAC3B,EAAA,OAAO,GAAA,CAAI,0BAA0B,GAAA,CAAI,iBAAA;AAC3C;AAEA,eAAe,cAAA,CACb,UACA,EAAA,EAC6B;AAC7B,EAAA,IAAI,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,QAAQ,CAAA;AAEnC,EAAA,OAAO,IAAA,EAAM;AACX,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,yBAAyB,CAAA;AAE/D,IAAA,IAAI,MAAM,EAAA,CAAG,MAAA,CAAO,UAAU,CAAA,EAAG;AAC/B,MAAA,OAAO,OAAA;AAAA,IACT;AAEA,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AACnC,IAAA,IAAI,WAAW,OAAA,EAAS;AACtB,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,OAAA,GAAU,MAAA;AAAA,EACZ;AACF;AAYA,eAAsB,oBAAA,CACpB,OAAA,GAAuC,EAAC,EACN;AAClC,EAAA,MAAM;AAAA,IACJ,GAAA;AAAA,IACA,MAAM,OAAA,CAAQ,GAAA;AAAA,IACd,EAAA,GAAK,SAAA;AAAA,IACL,QAAA,GAAW,QAAQ,GAAA;AAAI,GACzB,GAAI,OAAA;AAEJ,EAAA,IAAI,GAAA,EAAK;AACP,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,IAAA,CAAK,OAAA,CAAQ,GAAG,CAAA;AAAA,MACzB,MAAA,EAAQ;AAAA,KACV;AAAA,EACF;AAEA,EAAA,MAAM,OAAA,GAAU,eAAe,GAAG,CAAA;AAClC,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAAA,MAC7B,MAAA,EAAQ;AAAA,KACV;AAAA,EACF;AAEA,EAAA,MAAM,UAAA,GAAa,MAAM,cAAA,CAAe,QAAA,EAAU,EAAE,CAAA;AACpD,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,UAAA;AAAA,MACT,MAAA,EAAQ;AAAA,KACV;AAAA,EACF;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,QAAA,GAAW,MAAM,YAAA,CAAa,QAAQ,CAAA;AAC5C,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,OAAA,CAAQ,QAAQ,CAAA;AAC1C,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,YAAY,CAAA,CAAE,IAAA;AAExC,IAAA,IAAI,iBAAiB,MAAA,EAAQ;AAC3B,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,YAAA;AAAA,QACT,MAAA,EAAQ;AAAA,OACV;AAAA,IACF;AAAA,EACF,CAAA,CAAA,MAAQ;AAAA,EAER;AAEA,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,IAAA,CAAK,OAAA,CAAQ,QAAQ,CAAA;AAAA,IAC9B,MAAA,EAAQ;AAAA,GACV;AACF","file":"index.js","sourcesContent":["import path from 'node:path'\nimport { promises as fsp } from 'node:fs'\n\nimport { findRepoRoot } from '@kb-labs/core-sys'\n\nimport type {\n ResolveWorkspaceRootOptions,\n WorkspaceFs,\n WorkspaceRootResolution,\n} from './types'\n\nconst WORKSPACE_CONFIG_RELATIVE = path.join('.kb', 'kb.config.json')\n\nconst defaultFs: WorkspaceFs = {\n async exists(target: string): Promise<boolean> {\n try {\n await fsp.access(target)\n return true\n } catch {\n return false\n }\n },\n}\n\nfunction resolveEnvRoot(\n env: Record<string, string | undefined> | undefined,\n): string | undefined {\n if (!env) {return undefined}\n return env.KB_LABS_WORKSPACE_ROOT ?? env.KB_LABS_REPO_ROOT\n}\n\nasync function findConfigRoot(\n startDir: string,\n fs: WorkspaceFs,\n): Promise<string | undefined> {\n let current = path.resolve(startDir)\n\n while (true) {\n const configPath = path.join(current, WORKSPACE_CONFIG_RELATIVE)\n // eslint-disable-next-line no-await-in-loop -- Searching for workspace root: must check each directory sequentially\n if (await fs.exists(configPath)) {\n return current\n }\n\n const parent = path.dirname(current)\n if (parent === current) {\n return undefined\n }\n\n current = parent\n }\n}\n\n/**\n * Resolve the workspace root for kb-labs tooling.\n *\n * Priority order:\n * 1. Explicit `cwd` option (e.g. CLI flag)\n * 2. Environment variables (`KB_LABS_WORKSPACE_ROOT`, `KB_LABS_REPO_ROOT`)\n * 3. Nearest `.kb/kb-labs.config.json` ancestor\n * 4. Repository root discovered via `findRepoRoot`\n * 5. Fallback to provided `startDir` / process cwd\n */\nexport async function resolveWorkspaceRoot(\n options: ResolveWorkspaceRootOptions = {},\n): Promise<WorkspaceRootResolution> {\n const {\n cwd,\n env = process.env,\n fs = defaultFs,\n startDir = process.cwd(),\n } = options\n\n if (cwd) {\n return {\n rootDir: path.resolve(cwd),\n source: 'explicit',\n }\n }\n\n const envRoot = resolveEnvRoot(env)\n if (envRoot) {\n return {\n rootDir: path.resolve(envRoot),\n source: 'env',\n }\n }\n\n const configRoot = await findConfigRoot(startDir, fs)\n if (configRoot) {\n return {\n rootDir: configRoot,\n source: 'config',\n }\n }\n\n try {\n const repoRoot = await findRepoRoot(startDir)\n const resolvedRepo = path.resolve(repoRoot)\n const fsRoot = path.parse(resolvedRepo).root\n\n if (resolvedRepo !== fsRoot) {\n return {\n rootDir: resolvedRepo,\n source: 'repo',\n }\n }\n } catch {\n // swallow and fall through to fallback\n }\n\n return {\n rootDir: path.resolve(startDir),\n source: 'fallback',\n }\n}\n\n"]}
1
+ {"version":3,"sources":["../src/workspace/root-resolver.ts"],"names":["fsp"],"mappings":"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","file":"index.js","sourcesContent":["import path from 'node:path'\nimport { promises as fsp } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\n\nimport { findRepoRoot } from '@kb-labs/core-sys'\n\nimport type {\n PlatformRootResolution,\n ProjectRootResolution,\n ResolvePlatformRootOptions,\n ResolveProjectRootOptions,\n ResolveRootsOptions,\n ResolveWorkspaceRootOptions,\n RootsResolution,\n WorkspaceFs,\n WorkspaceRootResolution,\n} from './types'\n\nconst WORKSPACE_CONFIG_RELATIVE = path.join('.kb', 'kb.config.json')\n\n/**\n * Package markers used to recognise an installed KB Labs platform. Any one of\n * these, when present as `<root>/node_modules/<marker>`, indicates that\n * `<root>` is a platform installation.\n */\nconst PLATFORM_PACKAGE_MARKERS = [\n '@kb-labs/cli-bin',\n '@kb-labs/core-runtime',\n] as const\n\nconst defaultFs: WorkspaceFs = {\n async exists(target: string): Promise<boolean> {\n try {\n await fsp.access(target)\n return true\n } catch {\n return false\n }\n },\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Project root (previously \"workspace root\")\n// ──────────────────────────────────────────────────────────────────────────\n\nfunction resolveProjectEnvRoot(\n env: Record<string, string | undefined> | undefined,\n): string | undefined {\n if (!env) {\n return undefined\n }\n return (\n env.KB_PROJECT_ROOT ??\n env.KB_LABS_WORKSPACE_ROOT ??\n env.KB_LABS_REPO_ROOT\n )\n}\n\nasync function findConfigRoot(\n startDir: string,\n fs: WorkspaceFs,\n): Promise<string | undefined> {\n let current = path.resolve(startDir)\n\n while (true) {\n const configPath = path.join(current, WORKSPACE_CONFIG_RELATIVE)\n // eslint-disable-next-line no-await-in-loop -- Searching for project root: must check each directory sequentially\n if (await fs.exists(configPath)) {\n return current\n }\n\n const parent = path.dirname(current)\n if (parent === current) {\n return undefined\n }\n\n current = parent\n }\n}\n\n/**\n * Resolve the project root — the directory that contains the user's\n * `.kb/kb.config.json` and plugin state.\n *\n * Priority order:\n *\n * 1. Explicit `cwd` option (e.g. CLI flag).\n * 2. Environment variables: `KB_PROJECT_ROOT` (preferred), then the legacy\n * `KB_LABS_WORKSPACE_ROOT` / `KB_LABS_REPO_ROOT`.\n * 3. Nearest `.kb/kb.config.json` ancestor walking up from `startDir`.\n * 4. Repository root discovered via `findRepoRoot` (pnpm workspace, `.git`,\n * `package.json`).\n * 5. Fallback to `startDir` / `process.cwd()`.\n *\n * In monorepo dev mode this typically returns the workspace root. In installed\n * mode it returns the user's project directory.\n */\nexport async function resolveProjectRoot(\n options: ResolveProjectRootOptions = {},\n): Promise<ProjectRootResolution> {\n const {\n cwd,\n env = process.env,\n fs = defaultFs,\n startDir = process.cwd(),\n } = options\n\n if (cwd) {\n return {\n rootDir: path.resolve(cwd),\n source: 'explicit',\n }\n }\n\n const envRoot = resolveProjectEnvRoot(env)\n if (envRoot) {\n return {\n rootDir: path.resolve(envRoot),\n source: 'env',\n }\n }\n\n const configRoot = await findConfigRoot(startDir, fs)\n if (configRoot) {\n return {\n rootDir: configRoot,\n source: 'config',\n }\n }\n\n try {\n const repoRoot = await findRepoRoot(startDir)\n const resolvedRepo = path.resolve(repoRoot)\n const fsRoot = path.parse(resolvedRepo).root\n\n if (resolvedRepo !== fsRoot) {\n return {\n rootDir: resolvedRepo,\n source: 'repo',\n }\n }\n } catch {\n // swallow and fall through to fallback\n }\n\n return {\n rootDir: path.resolve(startDir),\n source: 'fallback',\n }\n}\n\n/**\n * @deprecated Use {@link resolveProjectRoot}. This alias is kept for\n * backwards compatibility; semantics are identical.\n *\n * The name \"workspace root\" is ambiguous — in KB Labs terminology, what it\n * actually refers to is the project root (where `.kb/kb.config.json` lives),\n * not the KB Labs platform installation. Prefer the explicit name.\n */\nexport async function resolveWorkspaceRoot(\n options: ResolveWorkspaceRootOptions = {},\n): Promise<WorkspaceRootResolution> {\n return resolveProjectRoot(options)\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Platform root\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Check whether `<candidate>/node_modules/<marker>` exists for any of the\n * known platform package markers.\n */\nasync function hasPlatformMarker(\n candidate: string,\n fs: WorkspaceFs,\n): Promise<boolean> {\n for (const marker of PLATFORM_PACKAGE_MARKERS) {\n const markerPath = path.join(candidate, 'node_modules', marker)\n // eslint-disable-next-line no-await-in-loop -- Sequential probe is intentional\n if (await fs.exists(markerPath)) {\n return true\n }\n }\n return false\n}\n\n/**\n * Walk up from `startDir` looking for either a platform marker\n * (`node_modules/@kb-labs/*`) or a pnpm workspace marker\n * (`pnpm-workspace.yaml`). Returns the first match.\n */\nasync function walkUpForPlatformMarker(\n startDir: string,\n fs: WorkspaceFs,\n): Promise<string | undefined> {\n let current = path.resolve(startDir)\n\n while (true) {\n // eslint-disable-next-line no-await-in-loop -- Sequential walk is intentional\n if (await hasPlatformMarker(current, fs)) {\n return current\n }\n\n // eslint-disable-next-line no-await-in-loop -- Sequential walk is intentional\n if (await fs.exists(path.join(current, 'pnpm-workspace.yaml'))) {\n return current\n }\n\n const parent = path.dirname(current)\n if (parent === current) {\n return undefined\n }\n current = parent\n }\n}\n\n/**\n * Given a `file://` URL (typically `import.meta.url` from a CLI entry or\n * service bootstrap), walk up through its directory ancestors to locate the\n * platform installation root.\n *\n * Two signals are collected on the way up:\n *\n * 1. **First platform-package marker hit** — a directory whose `node_modules`\n * contains `@kb-labs/cli-bin` (or another known platform marker). In\n * installed mode this is the platform installation directory.\n *\n * 2. **Top-most `pnpm-workspace.yaml`** — we keep walking past inner workspace\n * markers and record the *outermost* one. This is important in the\n * KB Labs \"workspace of workspaces\" dev layout, where individual sub-repos\n * (`platform/kb-labs-cli`, `plugins/kb-labs-*`, etc.) each have their own\n * `pnpm-workspace.yaml` while the true monorepo root lives at the very top.\n *\n * Precedence after walking: the outermost `pnpm-workspace.yaml` wins if one\n * exists (dev mode); otherwise we return the first marker hit (installed\n * mode). This way a naive walk-up can never stop prematurely at a nested\n * workspace or at a package with its own hoisting symlink.\n */\nasync function resolvePlatformRootFromModuleUrl(\n moduleUrl: string,\n fs: WorkspaceFs,\n): Promise<string | undefined> {\n let modulePath: string\n try {\n modulePath = fileURLToPath(moduleUrl)\n } catch {\n return undefined\n }\n\n let current = path.dirname(modulePath)\n let firstMarkerHit: string | undefined\n let topMostWorkspace: string | undefined\n\n while (true) {\n // eslint-disable-next-line no-await-in-loop -- Sequential walk is intentional\n if (!firstMarkerHit && (await hasPlatformMarker(current, fs))) {\n firstMarkerHit = current\n }\n\n // Keep overwriting: we want the *outermost* (highest-up) workspace root,\n // not the innermost. This handles the KB Labs nested-workspaces layout.\n // eslint-disable-next-line no-await-in-loop -- Sequential walk is intentional\n if (await fs.exists(path.join(current, 'pnpm-workspace.yaml'))) {\n topMostWorkspace = current\n }\n\n const parent = path.dirname(current)\n if (parent === current) {\n // Reached filesystem root. Prefer top-most workspace (dev mode);\n // otherwise return first marker hit (installed mode).\n return topMostWorkspace ?? firstMarkerHit\n }\n current = parent\n }\n}\n\n/**\n * Resolve the platform root — the directory that contains the installed\n * KB Labs platform code (i.e. the parent of `node_modules/@kb-labs/*`).\n *\n * Priority order:\n *\n * 1. Explicit `cwd` option (e.g. CLI flag).\n * 2. Environment variable `KB_PLATFORM_ROOT` (typically set by the installer\n * wrapper script in installed mode).\n * 3. Walk up from `moduleUrl` (e.g. `import.meta.url` of the CLI `bin.ts`),\n * looking for a directory whose `node_modules` contains a known platform\n * package. This is the most reliable signal in installed mode because it\n * does not depend on `process.cwd()`.\n * 4. Walk up from `startDir` looking for either a platform marker or\n * `pnpm-workspace.yaml` (the latter covers monorepo dev mode).\n * 5. Repository root discovered via `findRepoRoot`.\n * 6. Fallback to `startDir` / `process.cwd()`.\n *\n * In monorepo dev mode steps 4 or 5 will typically match the workspace root,\n * so `platformRoot` and `projectRoot` resolve to the same directory.\n */\nexport async function resolvePlatformRoot(\n options: ResolvePlatformRootOptions = {},\n): Promise<PlatformRootResolution> {\n const {\n cwd,\n moduleUrl,\n env = process.env,\n fs = defaultFs,\n startDir = process.cwd(),\n } = options\n\n if (cwd) {\n return {\n rootDir: path.resolve(cwd),\n source: 'explicit',\n }\n }\n\n const envRoot = env?.KB_PLATFORM_ROOT\n if (envRoot) {\n return {\n rootDir: path.resolve(envRoot),\n source: 'env',\n }\n }\n\n if (moduleUrl) {\n const fromModule = await resolvePlatformRootFromModuleUrl(moduleUrl, fs)\n if (fromModule) {\n return {\n rootDir: fromModule,\n source: 'module',\n }\n }\n }\n\n const fromMarker = await walkUpForPlatformMarker(startDir, fs)\n if (fromMarker) {\n return {\n rootDir: fromMarker,\n source: 'marker',\n }\n }\n\n try {\n const repoRoot = await findRepoRoot(startDir)\n const resolvedRepo = path.resolve(repoRoot)\n const fsRoot = path.parse(resolvedRepo).root\n\n if (resolvedRepo !== fsRoot) {\n return {\n rootDir: resolvedRepo,\n source: 'repo',\n }\n }\n } catch {\n // swallow and fall through to fallback\n }\n\n return {\n rootDir: path.resolve(startDir),\n source: 'fallback',\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Composite resolver\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Resolve both the platform root and the project root in a single call. This\n * is the main entry point for the CLI, service bootstrap, and `kb-dev`.\n *\n * @example\n * ```ts\n * // In bin.ts (the CLI entrypoint):\n * const { platformRoot, projectRoot } = await resolveRoots({\n * moduleUrl: import.meta.url,\n * startDir: process.cwd(),\n * })\n * ```\n *\n * In dev mode `platformRoot === projectRoot` and `sameLocation` is `true`.\n * In installed mode they differ: `platformRoot` points at the platform\n * installation while `projectRoot` points at the user's project directory.\n */\nexport async function resolveRoots(\n options: ResolveRootsOptions = {},\n): Promise<RootsResolution> {\n const [platform, project] = await Promise.all([\n resolvePlatformRoot(options),\n resolveProjectRoot(options),\n ])\n\n return {\n platformRoot: platform.rootDir,\n projectRoot: project.rootDir,\n sameLocation:\n path.resolve(platform.rootDir) === path.resolve(project.rootDir),\n sources: {\n platform: platform.source,\n project: project.source,\n },\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kb-labs/core-workspace",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Core workspace utilities for KB Labs, including root directory resolution and workspace detection",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "test:watch": "vitest"
30
30
  },
31
31
  "dependencies": {
32
- "@kb-labs/core-sys": "^1.7.0"
32
+ "@kb-labs/core-sys": "^1.8.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@kb-labs/devkit": "link:../../../../infra/kb-labs-devkit",