@kb-labs/devlink-core 1.5.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 +120 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +664 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @kb-labs/plugin-template-cli
|
|
2
|
+
|
|
3
|
+
Reference CLI/REST/Studio plugin package for KB Labs Plugin Template.
|
|
4
|
+
|
|
5
|
+
## Vision & Purpose
|
|
6
|
+
|
|
7
|
+
**@kb-labs/plugin-template-cli** is the canonical example plugin package used by `@kb-labs/plugin-template`.
|
|
8
|
+
It shows how to implement a plugin that exposes:
|
|
9
|
+
|
|
10
|
+
- a **CLI command** (Hello),
|
|
11
|
+
- a **REST handler**, and
|
|
12
|
+
- a **Studio widget**,
|
|
13
|
+
|
|
14
|
+
all driven by a single manifest and contracts package.
|
|
15
|
+
|
|
16
|
+
## Package Status
|
|
17
|
+
|
|
18
|
+
- **Version**: 0.1.0
|
|
19
|
+
- **Stage**: Stable (template)
|
|
20
|
+
- **Status**: Reference Implementation ✅
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
### High-Level Overview
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
plugin-cli
|
|
28
|
+
│
|
|
29
|
+
├──► contracts (from @kb-labs/plugin-template-contracts)
|
|
30
|
+
├──► shared (constants/helpers)
|
|
31
|
+
├──► domain (Greeting entity and invariants)
|
|
32
|
+
├──► application (use-cases: create greeting, etc.)
|
|
33
|
+
├──► cli (Hello command wiring)
|
|
34
|
+
├──► rest (Hello REST handler + schema)
|
|
35
|
+
└──► studio (Hello Studio widget)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Key Components
|
|
39
|
+
|
|
40
|
+
- `src/domain/`: `Greeting` entity and domain rules
|
|
41
|
+
- `src/application/`: use-cases that orchestrate domain logic
|
|
42
|
+
- `src/cli/commands/hello/*`: CLI command implementation
|
|
43
|
+
- `src/rest/handlers/hello-handler.ts`: REST handler bound to manifest
|
|
44
|
+
- `src/studio/widgets/hello-widget.tsx`: Studio widget implementation
|
|
45
|
+
- `src/manifest.v2.ts`: Plugin manifest v2 (CLI/REST/Studio wiring)
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- **Single-source manifest** for CLI/REST/Studio surfaces
|
|
50
|
+
- **Layered architecture** (shared → domain → application → interface)
|
|
51
|
+
- **Type-safe contracts** via `@kb-labs/plugin-template-contracts`
|
|
52
|
+
- **Hello-world flow** demonstrating end-to-end plugin wiring
|
|
53
|
+
|
|
54
|
+
## Exports
|
|
55
|
+
|
|
56
|
+
From `src/index.ts`:
|
|
57
|
+
|
|
58
|
+
- `manifest`: Plugin Manifest V2
|
|
59
|
+
- All public surfaces:
|
|
60
|
+
- CLI command exports
|
|
61
|
+
- domain/application/shared re-exports
|
|
62
|
+
|
|
63
|
+
## Dependencies
|
|
64
|
+
|
|
65
|
+
### Runtime
|
|
66
|
+
|
|
67
|
+
- `@kb-labs/setup-operations`: reusable setup operations
|
|
68
|
+
- `@kb-labs/plugin-manifest`: manifest types and helpers
|
|
69
|
+
- `@kb-labs/plugin-template-contracts`: public contracts for this template plugin
|
|
70
|
+
- `@kb-labs/shared-cli-ui`: shared CLI UI helpers
|
|
71
|
+
- `react`, `react-dom`, `zod`
|
|
72
|
+
|
|
73
|
+
### Development
|
|
74
|
+
|
|
75
|
+
- `@kb-labs/devkit`: shared TS/ESLint/Vitest/TSUP presets
|
|
76
|
+
- `typescript`, `tsup`, `vitest`, `rimraf`
|
|
77
|
+
|
|
78
|
+
## Scripts
|
|
79
|
+
|
|
80
|
+
From `kb-labs-plugin-template` repo root:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pnpm install
|
|
84
|
+
pnpm --filter @kb-labs/plugin-template-cli build
|
|
85
|
+
pnpm --filter @kb-labs/plugin-template-cli test
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
To run sandboxes, see the root `README.md` (`pnpm sandbox:cli`, `sandbox:rest`, `sandbox:studio`).
|
|
89
|
+
|
|
90
|
+
## Command Implementation
|
|
91
|
+
|
|
92
|
+
This template demonstrates **three different approaches** to implementing CLI commands:
|
|
93
|
+
|
|
94
|
+
1. **High-level wrapper (`defineCommand`)** - Recommended for most cases
|
|
95
|
+
2. **Low-level atomic tools** - For maximum control
|
|
96
|
+
3. **Hybrid approach** - Combining both
|
|
97
|
+
|
|
98
|
+
See [`COMMAND_IMPLEMENTATION_GUIDE.md`](./COMMAND_IMPLEMENTATION_GUIDE.md) for detailed explanations and examples.
|
|
99
|
+
|
|
100
|
+
### Quick Start
|
|
101
|
+
|
|
102
|
+
The `template:hello` command in `src/cli/commands/hello/run.ts` shows all three approaches with working code examples. The default implementation uses Approach 1 (`defineCommand`), which provides:
|
|
103
|
+
|
|
104
|
+
- ✅ Zero-boilerplate flag validation
|
|
105
|
+
- ✅ Automatic analytics integration
|
|
106
|
+
- ✅ Structured logging
|
|
107
|
+
- ✅ Error handling
|
|
108
|
+
- ✅ Timing tracking
|
|
109
|
+
- ✅ JSON output mode
|
|
110
|
+
|
|
111
|
+
## Customising for Your Plugin
|
|
112
|
+
|
|
113
|
+
When using this as a starting point:
|
|
114
|
+
|
|
115
|
+
- Rename the package in `package.json` (e.g. `@kb-labs/my-plugin-cli`)
|
|
116
|
+
- Update manifest IDs and the contracts package
|
|
117
|
+
- Replace the Hello flow with your own domain, use-cases, and surfaces
|
|
118
|
+
- Choose the command implementation approach that fits your needs (see `COMMAND_IMPLEMENTATION_GUIDE.md`)
|
|
119
|
+
|
|
120
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { PackageMap, DevlinkMode, DevlinkPlan, DevlinkPlanItem, DevlinkState, DevlinkBackup, DiagnosticIssue } from '@kb-labs/devlink-contracts';
|
|
2
|
+
|
|
3
|
+
/** Monorepo info discovered on disk */
|
|
4
|
+
interface MonorepoInfo {
|
|
5
|
+
/** Dir name e.g. kb-labs-core */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Absolute path to the monorepo root */
|
|
8
|
+
rootPath: string;
|
|
9
|
+
/** All package.json paths within the monorepo */
|
|
10
|
+
packagePaths: string[];
|
|
11
|
+
/** pnpm-workspace.yaml content */
|
|
12
|
+
workspacePackages: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Discovers all submodule repos via .gitmodules (layout-agnostic).
|
|
16
|
+
* Includes both monorepos (with pnpm-workspace.yaml) and standalone packages.
|
|
17
|
+
*/
|
|
18
|
+
declare function discoverMonorepos(rootDir: string): MonorepoInfo[];
|
|
19
|
+
/**
|
|
20
|
+
* Builds a PackageMap: packageName → { linkPath, npmVersion, monorepo }.
|
|
21
|
+
* Scans all packages in all monorepos and collects their name/version.
|
|
22
|
+
*
|
|
23
|
+
* linkPath is relative to the root kb-labs/ dir (so it can be used as link:../path).
|
|
24
|
+
*/
|
|
25
|
+
declare function buildPackageMap(monorepos: MonorepoInfo[], rootDir: string): PackageMap;
|
|
26
|
+
/**
|
|
27
|
+
* Async version of buildPackageMap that verifies each package exists on npm.
|
|
28
|
+
* For 'local' mode skips the npm check and returns all packages found on disk.
|
|
29
|
+
* For 'npm'/'auto'/undefined filters out packages not published to the registry.
|
|
30
|
+
*/
|
|
31
|
+
declare function buildPackageMapFiltered(monorepos: MonorepoInfo[], rootDir: string, ttlMs?: number, mode?: DevlinkMode): Promise<PackageMap>;
|
|
32
|
+
/**
|
|
33
|
+
* Determines the current linking mode of cross-repo dependencies in a package.json.
|
|
34
|
+
* Returns counts of link:, npm, workspace: references for @kb-labs/* deps.
|
|
35
|
+
*/
|
|
36
|
+
declare function analyzePackageDeps(pkgPath: string, packageMap: PackageMap): {
|
|
37
|
+
linkCount: number;
|
|
38
|
+
npmCount: number;
|
|
39
|
+
workspaceCount: number;
|
|
40
|
+
unknownCount: number;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Determines which MonorepoInfo a package.json belongs to.
|
|
44
|
+
* Uses rootPath prefix matching.
|
|
45
|
+
*/
|
|
46
|
+
declare function resolvePackageMonorepo(pkgPath: string, monorepos: MonorepoInfo[]): MonorepoInfo | null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Checks if a package exists on the npm registry.
|
|
50
|
+
* Results are cached via useCache() with the given TTL.
|
|
51
|
+
*/
|
|
52
|
+
declare function isPublishedOnNpm(packageName: string, ttlMs?: number): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Filters a list of package names to only those published on npm.
|
|
55
|
+
* Runs checks concurrently.
|
|
56
|
+
*/
|
|
57
|
+
declare function filterPublishedPackages(packageNames: string[], ttlMs?: number): Promise<Set<string>>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Builds a DevlinkPlan — all package.json changes needed for the target mode.
|
|
61
|
+
*/
|
|
62
|
+
declare function buildPlan(mode: DevlinkMode, packageMap: PackageMap, monorepos: MonorepoInfo[], rootDir: string, options?: {
|
|
63
|
+
scopedRepos?: string[];
|
|
64
|
+
}): DevlinkPlan;
|
|
65
|
+
/**
|
|
66
|
+
* Returns a human-readable description of a plan item change.
|
|
67
|
+
*/
|
|
68
|
+
declare function describeChange(item: DevlinkPlanItem): string;
|
|
69
|
+
/**
|
|
70
|
+
* Groups plan items by monorepo for display purposes.
|
|
71
|
+
*/
|
|
72
|
+
declare function groupByMonorepo(items: DevlinkPlanItem[]): Map<string, DevlinkPlanItem[]>;
|
|
73
|
+
|
|
74
|
+
interface ApplyOptions {
|
|
75
|
+
dryRun?: boolean;
|
|
76
|
+
}
|
|
77
|
+
interface ApplyResult {
|
|
78
|
+
applied: number;
|
|
79
|
+
skipped: number;
|
|
80
|
+
errors: Array<{
|
|
81
|
+
file: string;
|
|
82
|
+
error: string;
|
|
83
|
+
}>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Applies a DevlinkPlan to package.json files on disk.
|
|
87
|
+
* Groups changes by file to minimize I/O operations.
|
|
88
|
+
*/
|
|
89
|
+
declare function applyPlan(plan: DevlinkPlan, options?: ApplyOptions): Promise<ApplyResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Checks if the given directory has uncommitted git changes.
|
|
92
|
+
* Returns a list of modified files, or empty array if clean.
|
|
93
|
+
*/
|
|
94
|
+
declare function checkGitDirty(repoPath: string): string[];
|
|
95
|
+
|
|
96
|
+
declare function loadState(rootDir: string): DevlinkState;
|
|
97
|
+
declare function saveState(rootDir: string, state: DevlinkState): void;
|
|
98
|
+
interface LockFile {
|
|
99
|
+
frozenAt: string;
|
|
100
|
+
plan: DevlinkPlan;
|
|
101
|
+
}
|
|
102
|
+
declare function freeze(rootDir: string, currentPlan: DevlinkPlan): LockFile;
|
|
103
|
+
declare function loadLock(rootDir: string): LockFile | null;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Creates a backup of given package.json files before a mutation.
|
|
107
|
+
* Returns the backup metadata.
|
|
108
|
+
*/
|
|
109
|
+
declare function createBackup(rootDir: string, filePaths: string[], description: string, currentMode: DevlinkMode | null): DevlinkBackup;
|
|
110
|
+
/**
|
|
111
|
+
* Lists all backups, sorted newest first.
|
|
112
|
+
*/
|
|
113
|
+
declare function listBackups(rootDir: string): DevlinkBackup[];
|
|
114
|
+
/**
|
|
115
|
+
* Returns the most recent backup, or null if none exist.
|
|
116
|
+
*/
|
|
117
|
+
declare function getLastBackup(rootDir: string): DevlinkBackup | null;
|
|
118
|
+
/**
|
|
119
|
+
* Restores package.json files from a specific backup.
|
|
120
|
+
*/
|
|
121
|
+
declare function restoreBackup(rootDir: string, backupId: string): {
|
|
122
|
+
restored: number;
|
|
123
|
+
errors: string[];
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Remove oldest backups beyond the retention limit.
|
|
127
|
+
*/
|
|
128
|
+
declare function pruneBackups(rootDir: string, maxBackups?: number): number;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Workspace YAML Manager
|
|
132
|
+
*
|
|
133
|
+
* Generates/updates pnpm-workspace.yaml in sub-repos to include
|
|
134
|
+
* cross-repo paths needed for autonomous `cd sub-repo && pnpm install`.
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
interface WorkspaceYamlUpdate {
|
|
138
|
+
repoName: string;
|
|
139
|
+
repoPath: string;
|
|
140
|
+
added: string[];
|
|
141
|
+
removed: string[];
|
|
142
|
+
kept: string[];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Update pnpm-workspace.yaml in all sub-repos to include correct cross-repo paths.
|
|
146
|
+
*
|
|
147
|
+
* For each sub-repo:
|
|
148
|
+
* 1. Keep intra-repo patterns (packages/*, apps/*, etc.)
|
|
149
|
+
* 2. Analyze which cross-repo packages are needed (from deps in all package.json)
|
|
150
|
+
* 3. Compute correct relative paths to those packages
|
|
151
|
+
* 4. Write updated workspace.yaml
|
|
152
|
+
*/
|
|
153
|
+
declare function updateWorkspaceYamls(monorepos: MonorepoInfo[], packageMap: PackageMap, rootDir: string, options?: {
|
|
154
|
+
dryRun?: boolean;
|
|
155
|
+
}): WorkspaceYamlUpdate[];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Diagnostics — detect broken deps, stale lockfiles, cross-repo workspace:* issues
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Run all diagnostic checks across the monorepo.
|
|
163
|
+
*/
|
|
164
|
+
declare function diagnose(monorepos: MonorepoInfo[], packageMap: PackageMap, rootDir: string): DiagnosticIssue[];
|
|
165
|
+
|
|
166
|
+
export { type ApplyOptions, type ApplyResult, type LockFile, type MonorepoInfo, type WorkspaceYamlUpdate, analyzePackageDeps, applyPlan, buildPackageMap, buildPackageMapFiltered, buildPlan, checkGitDirty, createBackup, describeChange, diagnose, discoverMonorepos, filterPublishedPackages, freeze, getLastBackup, groupByMonorepo, isPublishedOnNpm, listBackups, loadLock, loadState, pruneBackups, resolvePackageMonorepo, restoreBackup, saveState, updateWorkspaceYamls };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, rmSync, statSync } from 'fs';
|
|
2
|
+
import { join, dirname, relative, resolve } from 'path';
|
|
3
|
+
import yaml2 from 'js-yaml';
|
|
4
|
+
import { useCache, discoverSubRepoPaths } from '@kb-labs/sdk';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
|
|
7
|
+
// src/discovery/index.ts
|
|
8
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
9
|
+
async function isPublishedOnNpm(packageName, ttlMs = DEFAULT_TTL_MS) {
|
|
10
|
+
const cacheKey = `devlink:npm-exists:${packageName}`;
|
|
11
|
+
const cache = useCache();
|
|
12
|
+
if (cache) {
|
|
13
|
+
const cached = await cache.get(cacheKey);
|
|
14
|
+
if (cached !== void 0 && cached !== null) {
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
let exists;
|
|
19
|
+
try {
|
|
20
|
+
execSync(`npm view ${packageName} version --json`, {
|
|
21
|
+
stdio: "pipe",
|
|
22
|
+
timeout: 1e4
|
|
23
|
+
});
|
|
24
|
+
exists = true;
|
|
25
|
+
} catch {
|
|
26
|
+
exists = false;
|
|
27
|
+
}
|
|
28
|
+
if (cache) {
|
|
29
|
+
await cache.set(cacheKey, exists, ttlMs);
|
|
30
|
+
}
|
|
31
|
+
return exists;
|
|
32
|
+
}
|
|
33
|
+
async function filterPublishedPackages(packageNames, ttlMs = DEFAULT_TTL_MS) {
|
|
34
|
+
const results = await Promise.all(
|
|
35
|
+
packageNames.map(async (name) => ({
|
|
36
|
+
name,
|
|
37
|
+
published: await isPublishedOnNpm(name, ttlMs)
|
|
38
|
+
}))
|
|
39
|
+
);
|
|
40
|
+
return new Set(results.filter((r) => r.published).map((r) => r.name));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/discovery/index.ts
|
|
44
|
+
function discoverMonorepos(rootDir) {
|
|
45
|
+
const subRepoPaths = discoverSubRepoPaths(rootDir);
|
|
46
|
+
const monorepos = [];
|
|
47
|
+
for (const repoPath of subRepoPaths) {
|
|
48
|
+
const repoName = repoPath.split("/").pop() ?? repoPath;
|
|
49
|
+
const hasWorkspace = existsSync(join(repoPath, "pnpm-workspace.yaml"));
|
|
50
|
+
if (hasWorkspace) {
|
|
51
|
+
const packagePaths = findPackageJsonFiles(repoPath);
|
|
52
|
+
const repoWorkspace = yaml2.load(
|
|
53
|
+
readFileSync(join(repoPath, "pnpm-workspace.yaml"), "utf-8")
|
|
54
|
+
);
|
|
55
|
+
monorepos.push({
|
|
56
|
+
name: repoName,
|
|
57
|
+
rootPath: repoPath,
|
|
58
|
+
packagePaths,
|
|
59
|
+
workspacePackages: repoWorkspace.packages ?? []
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
const rootPkgPath = join(repoPath, "package.json");
|
|
63
|
+
if (existsSync(rootPkgPath)) {
|
|
64
|
+
monorepos.push({
|
|
65
|
+
name: repoName,
|
|
66
|
+
rootPath: repoPath,
|
|
67
|
+
packagePaths: [rootPkgPath],
|
|
68
|
+
workspacePackages: []
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return monorepos;
|
|
74
|
+
}
|
|
75
|
+
function buildPackageMap(monorepos, rootDir) {
|
|
76
|
+
const map = {};
|
|
77
|
+
for (const monorepo of monorepos) {
|
|
78
|
+
for (const pkgPath of monorepo.packagePaths) {
|
|
79
|
+
if (!existsSync(pkgPath)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
let pkg;
|
|
83
|
+
try {
|
|
84
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!pkg.name || !pkg.version) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!pkg.name.startsWith("@kb-labs/") && !pkg.name.startsWith("kb-labs-")) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const pkgDir = dirname(pkgPath);
|
|
95
|
+
const linkPath = relative(rootDir, pkgDir);
|
|
96
|
+
const entry = {
|
|
97
|
+
name: pkg.name,
|
|
98
|
+
linkPath,
|
|
99
|
+
npmVersion: `^${pkg.version}`,
|
|
100
|
+
monorepo: monorepo.name,
|
|
101
|
+
private: pkg.private ?? false
|
|
102
|
+
};
|
|
103
|
+
map[pkg.name] = entry;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return map;
|
|
107
|
+
}
|
|
108
|
+
async function buildPackageMapFiltered(monorepos, rootDir, ttlMs, mode) {
|
|
109
|
+
const rawMap = buildPackageMap(monorepos, rootDir);
|
|
110
|
+
if (mode === "local") {
|
|
111
|
+
return rawMap;
|
|
112
|
+
}
|
|
113
|
+
const packageNames = Object.keys(rawMap);
|
|
114
|
+
const published = await filterPublishedPackages(packageNames, ttlMs);
|
|
115
|
+
const filtered = {};
|
|
116
|
+
for (const name of packageNames) {
|
|
117
|
+
if (published.has(name)) {
|
|
118
|
+
filtered[name] = rawMap[name];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return filtered;
|
|
122
|
+
}
|
|
123
|
+
function analyzePackageDeps(pkgPath, packageMap) {
|
|
124
|
+
let linkCount = 0;
|
|
125
|
+
let npmCount = 0;
|
|
126
|
+
let workspaceCount = 0;
|
|
127
|
+
let unknownCount = 0;
|
|
128
|
+
if (!existsSync(pkgPath)) {
|
|
129
|
+
return { linkCount, npmCount, workspaceCount, unknownCount };
|
|
130
|
+
}
|
|
131
|
+
let pkg;
|
|
132
|
+
try {
|
|
133
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
134
|
+
} catch {
|
|
135
|
+
return { linkCount, npmCount, workspaceCount, unknownCount };
|
|
136
|
+
}
|
|
137
|
+
const sections = [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies];
|
|
138
|
+
for (const section of sections) {
|
|
139
|
+
if (!section) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
for (const [depName, depValue] of Object.entries(section)) {
|
|
143
|
+
if (!packageMap[depName]) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (depValue.startsWith("link:")) {
|
|
147
|
+
linkCount++;
|
|
148
|
+
} else if (depValue.startsWith("workspace:")) {
|
|
149
|
+
workspaceCount++;
|
|
150
|
+
} else if (depValue.startsWith("^") || depValue.startsWith("~") || /^\d/.test(depValue)) {
|
|
151
|
+
npmCount++;
|
|
152
|
+
} else {
|
|
153
|
+
unknownCount++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { linkCount, npmCount, workspaceCount, unknownCount };
|
|
158
|
+
}
|
|
159
|
+
function findPackageJsonFiles(repoRoot) {
|
|
160
|
+
const results = [];
|
|
161
|
+
function walk(dir, depth = 0) {
|
|
162
|
+
if (depth > 4) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
let entries;
|
|
166
|
+
try {
|
|
167
|
+
entries = readdirSync(dir, { withFileTypes: true, encoding: "utf-8" });
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const full = join(dir, entry.name);
|
|
176
|
+
if (entry.isDirectory()) {
|
|
177
|
+
walk(full, depth + 1);
|
|
178
|
+
} else if (entry.name === "package.json") {
|
|
179
|
+
results.push(full);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
walk(repoRoot);
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
function resolvePackageMonorepo(pkgPath, monorepos) {
|
|
187
|
+
for (const mono of monorepos) {
|
|
188
|
+
if (pkgPath.startsWith(mono.rootPath + "/") || pkgPath === join(mono.rootPath, "package.json")) {
|
|
189
|
+
return mono;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
function buildPlan(mode, packageMap, monorepos, rootDir, options = {}) {
|
|
195
|
+
const items = [];
|
|
196
|
+
const filteredMonorepos = options.scopedRepos?.length ? monorepos.filter((m) => options.scopedRepos.includes(m.name)) : monorepos;
|
|
197
|
+
for (const monorepo of filteredMonorepos) {
|
|
198
|
+
for (const pkgPath of monorepo.packagePaths) {
|
|
199
|
+
if (!existsSync(pkgPath)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
let pkg;
|
|
203
|
+
try {
|
|
204
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
205
|
+
} catch {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const sections = ["dependencies", "devDependencies", "peerDependencies"];
|
|
209
|
+
for (const section of sections) {
|
|
210
|
+
const deps = pkg[section];
|
|
211
|
+
if (!deps) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
for (const [depName, currentValue] of Object.entries(deps)) {
|
|
215
|
+
const entry = packageMap[depName];
|
|
216
|
+
if (!entry) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (currentValue === "*") {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (currentValue.startsWith("workspace:")) {
|
|
223
|
+
const consumerMono = resolvePackageMonorepo(pkgPath, monorepos);
|
|
224
|
+
if (consumerMono && consumerMono.name === entry.monorepo) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const targetValue = getTargetValue(mode, entry, pkgPath, rootDir);
|
|
229
|
+
if (targetValue === currentValue) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
items.push({
|
|
233
|
+
packageJsonPath: pkgPath,
|
|
234
|
+
packageJsonRel: relative(rootDir, pkgPath),
|
|
235
|
+
monorepo: monorepo.name,
|
|
236
|
+
depName,
|
|
237
|
+
from: currentValue,
|
|
238
|
+
to: targetValue,
|
|
239
|
+
section
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
mode,
|
|
247
|
+
items,
|
|
248
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
249
|
+
...options.scopedRepos?.length ? { scopedRepos: options.scopedRepos } : {}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function getTargetValue(mode, entry, fromPackageJson, rootDir) {
|
|
253
|
+
if (mode === "npm" && !entry.private) {
|
|
254
|
+
return entry.npmVersion;
|
|
255
|
+
}
|
|
256
|
+
const fromDir = dirname(fromPackageJson);
|
|
257
|
+
const targetDir = resolve(rootDir, entry.linkPath);
|
|
258
|
+
const relPath = relative(fromDir, targetDir);
|
|
259
|
+
const normalized = relPath.startsWith(".") ? relPath : `./${relPath}`;
|
|
260
|
+
return `link:${normalized}`;
|
|
261
|
+
}
|
|
262
|
+
function describeChange(item) {
|
|
263
|
+
return `${item.depName}: ${item.from} \u2192 ${item.to}`;
|
|
264
|
+
}
|
|
265
|
+
function groupByMonorepo(items) {
|
|
266
|
+
const groups = /* @__PURE__ */ new Map();
|
|
267
|
+
for (const item of items) {
|
|
268
|
+
if (!groups.has(item.monorepo)) {
|
|
269
|
+
groups.set(item.monorepo, []);
|
|
270
|
+
}
|
|
271
|
+
groups.get(item.monorepo).push(item);
|
|
272
|
+
}
|
|
273
|
+
return groups;
|
|
274
|
+
}
|
|
275
|
+
async function applyPlan(plan, options = {}) {
|
|
276
|
+
const { dryRun = false } = options;
|
|
277
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
278
|
+
for (const item of plan.items) {
|
|
279
|
+
if (!byFile.has(item.packageJsonPath)) {
|
|
280
|
+
byFile.set(item.packageJsonPath, []);
|
|
281
|
+
}
|
|
282
|
+
byFile.get(item.packageJsonPath).push(item);
|
|
283
|
+
}
|
|
284
|
+
let applied = 0;
|
|
285
|
+
let skipped = 0;
|
|
286
|
+
const errors = [];
|
|
287
|
+
for (const [filePath, items] of byFile.entries()) {
|
|
288
|
+
if (!existsSync(filePath)) {
|
|
289
|
+
skipped++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (dryRun) {
|
|
293
|
+
applied += items.length;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
298
|
+
const pkg = JSON.parse(raw);
|
|
299
|
+
for (const item of items) {
|
|
300
|
+
const section = pkg[item.section];
|
|
301
|
+
if (section && item.depName in section) {
|
|
302
|
+
section[item.depName] = item.to;
|
|
303
|
+
applied++;
|
|
304
|
+
} else {
|
|
305
|
+
skipped++;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const trailingNewline = raw.endsWith("\n") ? "\n" : "";
|
|
309
|
+
writeFileSync(filePath, JSON.stringify(pkg, null, 2) + trailingNewline, "utf-8");
|
|
310
|
+
} catch (err) {
|
|
311
|
+
errors.push({ file: filePath, error: String(err) });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { applied, skipped, errors };
|
|
315
|
+
}
|
|
316
|
+
function checkGitDirty(repoPath) {
|
|
317
|
+
try {
|
|
318
|
+
const output = execSync("git status --porcelain", {
|
|
319
|
+
cwd: repoPath,
|
|
320
|
+
encoding: "utf-8",
|
|
321
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
322
|
+
});
|
|
323
|
+
return output.split("\n").filter(Boolean).map((line) => line.slice(3).trim());
|
|
324
|
+
} catch {
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
var DEFAULT_STATE = {
|
|
329
|
+
currentMode: null,
|
|
330
|
+
lastApplied: null,
|
|
331
|
+
frozenAt: null
|
|
332
|
+
};
|
|
333
|
+
function getStatePath(rootDir) {
|
|
334
|
+
return join(rootDir, ".kb", "devlink", "state.json");
|
|
335
|
+
}
|
|
336
|
+
function getLockPath(rootDir) {
|
|
337
|
+
return join(rootDir, ".kb", "devlink", "lock.json");
|
|
338
|
+
}
|
|
339
|
+
function ensureDir(filePath) {
|
|
340
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
341
|
+
}
|
|
342
|
+
function loadState(rootDir) {
|
|
343
|
+
const statePath = getStatePath(rootDir);
|
|
344
|
+
if (!existsSync(statePath)) {
|
|
345
|
+
return { ...DEFAULT_STATE };
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
return JSON.parse(readFileSync(statePath, "utf-8"));
|
|
349
|
+
} catch {
|
|
350
|
+
return { ...DEFAULT_STATE };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function saveState(rootDir, state) {
|
|
354
|
+
const statePath = getStatePath(rootDir);
|
|
355
|
+
ensureDir(statePath);
|
|
356
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
357
|
+
}
|
|
358
|
+
function freeze(rootDir, currentPlan) {
|
|
359
|
+
const lock = {
|
|
360
|
+
frozenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
361
|
+
plan: currentPlan
|
|
362
|
+
};
|
|
363
|
+
const lockPath = getLockPath(rootDir);
|
|
364
|
+
ensureDir(lockPath);
|
|
365
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
366
|
+
const state = loadState(rootDir);
|
|
367
|
+
saveState(rootDir, { ...state, frozenAt: lock.frozenAt });
|
|
368
|
+
return lock;
|
|
369
|
+
}
|
|
370
|
+
function loadLock(rootDir) {
|
|
371
|
+
const lockPath = getLockPath(rootDir);
|
|
372
|
+
if (!existsSync(lockPath)) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
return JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
377
|
+
} catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function getBackupsDir(rootDir) {
|
|
382
|
+
return join(rootDir, ".kb", "devlink", "backups");
|
|
383
|
+
}
|
|
384
|
+
function getMetaPath(backupDir) {
|
|
385
|
+
return join(backupDir, "meta.json");
|
|
386
|
+
}
|
|
387
|
+
function createBackup(rootDir, filePaths, description, currentMode) {
|
|
388
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
389
|
+
const backupDir = join(getBackupsDir(rootDir), id);
|
|
390
|
+
mkdirSync(backupDir, { recursive: true });
|
|
391
|
+
const backedUpFiles = [];
|
|
392
|
+
for (const filePath of filePaths) {
|
|
393
|
+
if (!existsSync(filePath)) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const safeName = filePath.replace(/\//g, "__").replace(/:/g, "_");
|
|
397
|
+
const destPath = join(backupDir, safeName);
|
|
398
|
+
copyFileSync(filePath, destPath);
|
|
399
|
+
backedUpFiles.push(filePath);
|
|
400
|
+
}
|
|
401
|
+
const meta = {
|
|
402
|
+
id,
|
|
403
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
404
|
+
description,
|
|
405
|
+
files: backedUpFiles,
|
|
406
|
+
modeAtBackup: currentMode
|
|
407
|
+
};
|
|
408
|
+
writeFileSync(getMetaPath(backupDir), JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
409
|
+
pruneBackups(rootDir, 10);
|
|
410
|
+
return meta;
|
|
411
|
+
}
|
|
412
|
+
function listBackups(rootDir) {
|
|
413
|
+
const backupsDir = getBackupsDir(rootDir);
|
|
414
|
+
if (!existsSync(backupsDir)) {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
const entries = readdirSync(backupsDir, { withFileTypes: true });
|
|
418
|
+
const backups = [];
|
|
419
|
+
for (const entry of entries) {
|
|
420
|
+
if (!entry.isDirectory()) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const metaPath = getMetaPath(join(backupsDir, entry.name));
|
|
424
|
+
if (!existsSync(metaPath)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
429
|
+
backups.push(meta);
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return backups.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
434
|
+
}
|
|
435
|
+
function getLastBackup(rootDir) {
|
|
436
|
+
const backups = listBackups(rootDir);
|
|
437
|
+
return backups[0] ?? null;
|
|
438
|
+
}
|
|
439
|
+
function restoreBackup(rootDir, backupId) {
|
|
440
|
+
const backupDir = join(getBackupsDir(rootDir), backupId);
|
|
441
|
+
const metaPath = getMetaPath(backupDir);
|
|
442
|
+
if (!existsSync(metaPath)) {
|
|
443
|
+
throw new Error(`Backup ${backupId} not found`);
|
|
444
|
+
}
|
|
445
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
446
|
+
let restored = 0;
|
|
447
|
+
const errors = [];
|
|
448
|
+
for (const originalPath of meta.files) {
|
|
449
|
+
const safeName = originalPath.replace(/\//g, "__").replace(/:/g, "_");
|
|
450
|
+
const srcPath = join(backupDir, safeName);
|
|
451
|
+
if (!existsSync(srcPath)) {
|
|
452
|
+
errors.push(`Backup file missing: ${safeName}`);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
mkdirSync(dirname(originalPath), { recursive: true });
|
|
457
|
+
copyFileSync(srcPath, originalPath);
|
|
458
|
+
restored++;
|
|
459
|
+
} catch (err) {
|
|
460
|
+
errors.push(`Failed to restore ${originalPath}: ${String(err)}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return { restored, errors };
|
|
464
|
+
}
|
|
465
|
+
function pruneBackups(rootDir, maxBackups = 10) {
|
|
466
|
+
const backups = listBackups(rootDir);
|
|
467
|
+
if (backups.length <= maxBackups) {
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
const backupsDir = getBackupsDir(rootDir);
|
|
471
|
+
let pruned = 0;
|
|
472
|
+
for (const old of backups.slice(maxBackups)) {
|
|
473
|
+
const dir = join(backupsDir, old.id);
|
|
474
|
+
try {
|
|
475
|
+
rmSync(dir, { recursive: true, force: true });
|
|
476
|
+
pruned++;
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return pruned;
|
|
481
|
+
}
|
|
482
|
+
function updateWorkspaceYamls(monorepos, packageMap, rootDir, options = {}) {
|
|
483
|
+
const updates = [];
|
|
484
|
+
for (const mono of monorepos) {
|
|
485
|
+
const wsPath = join(mono.rootPath, "pnpm-workspace.yaml");
|
|
486
|
+
if (!existsSync(wsPath)) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const update = updateOneWorkspaceYaml(mono, monorepos, packageMap, rootDir, options);
|
|
490
|
+
if (update) {
|
|
491
|
+
updates.push(update);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return updates;
|
|
495
|
+
}
|
|
496
|
+
function updateOneWorkspaceYaml(mono, allMonorepos, packageMap, rootDir, options) {
|
|
497
|
+
const wsPath = join(mono.rootPath, "pnpm-workspace.yaml");
|
|
498
|
+
let workspace;
|
|
499
|
+
try {
|
|
500
|
+
workspace = yaml2.load(readFileSync(wsPath, "utf-8"));
|
|
501
|
+
} catch {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const currentPatterns = workspace.packages ?? [];
|
|
505
|
+
const intraPatterns = [];
|
|
506
|
+
for (const pattern of currentPatterns) {
|
|
507
|
+
if (!pattern.startsWith("../") && !pattern.startsWith("..\\")) {
|
|
508
|
+
intraPatterns.push(pattern);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const neededRepos = /* @__PURE__ */ new Set();
|
|
512
|
+
for (const pkgPath of mono.packagePaths) {
|
|
513
|
+
if (!existsSync(pkgPath)) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
let pkg;
|
|
517
|
+
try {
|
|
518
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
519
|
+
} catch {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const allDeps = {
|
|
523
|
+
...pkg.dependencies,
|
|
524
|
+
...pkg.devDependencies,
|
|
525
|
+
...pkg.peerDependencies
|
|
526
|
+
};
|
|
527
|
+
for (const depName of Object.keys(allDeps)) {
|
|
528
|
+
const entry = packageMap[depName];
|
|
529
|
+
if (!entry) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (entry.monorepo !== mono.name) {
|
|
533
|
+
neededRepos.add(entry.monorepo);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const crossPatterns = [];
|
|
538
|
+
for (const repoName of neededRepos) {
|
|
539
|
+
const targetMono = allMonorepos.find((m) => m.name === repoName);
|
|
540
|
+
if (!targetMono) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const relPath = relative(mono.rootPath, targetMono.rootPath);
|
|
544
|
+
if (targetMono.workspacePackages.length > 0) {
|
|
545
|
+
for (const pattern of targetMono.workspacePackages) {
|
|
546
|
+
crossPatterns.push(`${relPath}/${pattern}`);
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
crossPatterns.push(relPath);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
crossPatterns.sort();
|
|
553
|
+
const newPatterns = [...intraPatterns, ...crossPatterns];
|
|
554
|
+
const oldSet = new Set(currentPatterns);
|
|
555
|
+
const newSet = new Set(newPatterns);
|
|
556
|
+
const added = crossPatterns.filter((p) => !oldSet.has(p));
|
|
557
|
+
const removed = currentPatterns.filter((p) => p.startsWith("../") && !newSet.has(p));
|
|
558
|
+
const kept = intraPatterns;
|
|
559
|
+
if (added.length === 0 && removed.length === 0) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
if (!options.dryRun) {
|
|
563
|
+
const output = { packages: newPatterns };
|
|
564
|
+
writeFileSync(wsPath, yaml2.dump(output, { lineWidth: -1, quotingType: '"' }), "utf-8");
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
repoName: mono.name,
|
|
568
|
+
repoPath: mono.rootPath,
|
|
569
|
+
added,
|
|
570
|
+
removed,
|
|
571
|
+
kept
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function diagnose(monorepos, packageMap, rootDir) {
|
|
575
|
+
const issues = [];
|
|
576
|
+
for (const mono of monorepos) {
|
|
577
|
+
for (const pkgPath of mono.packagePaths) {
|
|
578
|
+
if (!existsSync(pkgPath)) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
let pkg;
|
|
582
|
+
try {
|
|
583
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
584
|
+
} catch {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const sections = [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies];
|
|
588
|
+
for (const section of sections) {
|
|
589
|
+
if (!section) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
for (const [depName, depValue] of Object.entries(section)) {
|
|
593
|
+
if (depValue.startsWith("link:")) {
|
|
594
|
+
const targetPath = resolve(dirname(pkgPath), depValue.slice(5));
|
|
595
|
+
if (!existsSync(targetPath)) {
|
|
596
|
+
issues.push({
|
|
597
|
+
type: "broken-link",
|
|
598
|
+
severity: "error",
|
|
599
|
+
file: pkgPath,
|
|
600
|
+
dep: depName,
|
|
601
|
+
message: `${depName}: link:${depValue.slice(5)} \u2192 target does not exist`,
|
|
602
|
+
fix: "Run devlink switch --mode=local to recalculate paths"
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (depValue.startsWith("workspace:") && packageMap[depName]) {
|
|
607
|
+
const consumerMono = resolvePackageMonorepo(pkgPath, monorepos);
|
|
608
|
+
const depMonorepo = packageMap[depName].monorepo;
|
|
609
|
+
if (consumerMono && consumerMono.name !== depMonorepo) {
|
|
610
|
+
issues.push({
|
|
611
|
+
type: "cross-repo-workspace",
|
|
612
|
+
severity: "warning",
|
|
613
|
+
file: pkgPath,
|
|
614
|
+
dep: depName,
|
|
615
|
+
message: `${depName}: workspace:* crosses sub-repo boundary (${consumerMono.name} \u2192 ${depMonorepo})`,
|
|
616
|
+
fix: "Run devlink switch --mode=local to convert to link:"
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
checkStaleLockfile(mono, issues);
|
|
624
|
+
}
|
|
625
|
+
issues.sort((a, b) => {
|
|
626
|
+
if (a.severity !== b.severity) {
|
|
627
|
+
return a.severity === "error" ? -1 : 1;
|
|
628
|
+
}
|
|
629
|
+
return a.type.localeCompare(b.type);
|
|
630
|
+
});
|
|
631
|
+
return issues;
|
|
632
|
+
}
|
|
633
|
+
function checkStaleLockfile(mono, issues) {
|
|
634
|
+
const lockPath = join(mono.rootPath, "pnpm-lock.yaml");
|
|
635
|
+
if (!existsSync(lockPath)) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
let lockMtime;
|
|
639
|
+
try {
|
|
640
|
+
lockMtime = statSync(lockPath).mtimeMs;
|
|
641
|
+
} catch {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
for (const pkgPath of mono.packagePaths) {
|
|
645
|
+
try {
|
|
646
|
+
const pkgMtime = statSync(pkgPath).mtimeMs;
|
|
647
|
+
if (pkgMtime > lockMtime) {
|
|
648
|
+
issues.push({
|
|
649
|
+
type: "stale-lockfile",
|
|
650
|
+
severity: "warning",
|
|
651
|
+
file: lockPath,
|
|
652
|
+
message: `${mono.name}: pnpm-lock.yaml is older than ${pkgPath}`,
|
|
653
|
+
fix: "Delete lockfile and run pnpm install, or use devlink switch --install"
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
} catch {
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export { analyzePackageDeps, applyPlan, buildPackageMap, buildPackageMapFiltered, buildPlan, checkGitDirty, createBackup, describeChange, diagnose, discoverMonorepos, filterPublishedPackages, freeze, getLastBackup, groupByMonorepo, isPublishedOnNpm, listBackups, loadLock, loadState, pruneBackups, resolvePackageMonorepo, restoreBackup, saveState, updateWorkspaceYamls };
|
|
663
|
+
//# sourceMappingURL=index.js.map
|
|
664
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/npm/index.ts","../src/discovery/index.ts","../src/plan/index.ts","../src/apply/index.ts","../src/state/index.ts","../src/backup/index.ts","../src/workspace-yaml/index.ts","../src/diagnostics/index.ts"],"names":["yaml","existsSync","readFileSync","relative","dirname","execSync","join","writeFileSync","mkdirSync","readdirSync","resolve"],"mappings":"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","file":"index.js","sourcesContent":["import { execSync } from 'child_process';\nimport { useCache } from '@kb-labs/sdk';\n\n/** Default TTL: 24 hours */\nconst DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Checks if a package exists on the npm registry.\n * Results are cached via useCache() with the given TTL.\n */\nexport async function isPublishedOnNpm(\n packageName: string,\n ttlMs = DEFAULT_TTL_MS\n): Promise<boolean> {\n const cacheKey = `devlink:npm-exists:${packageName}`;\n const cache = useCache();\n\n if (cache) {\n const cached = await cache.get<boolean>(cacheKey);\n if (cached !== undefined && cached !== null) {return cached;}\n }\n\n let exists: boolean;\n try {\n execSync(`npm view ${packageName} version --json`, {\n stdio: 'pipe',\n timeout: 10_000,\n });\n exists = true;\n } catch {\n exists = false;\n }\n\n if (cache) {\n await cache.set(cacheKey, exists, ttlMs);\n }\n\n return exists;\n}\n\n/**\n * Filters a list of package names to only those published on npm.\n * Runs checks concurrently.\n */\nexport async function filterPublishedPackages(\n packageNames: string[],\n ttlMs = DEFAULT_TTL_MS\n): Promise<Set<string>> {\n const results = await Promise.all(\n packageNames.map(async name => ({\n name,\n published: await isPublishedOnNpm(name, ttlMs),\n }))\n );\n\n return new Set(results.filter(r => r.published).map(r => r.name));\n}\n","import { readFileSync, existsSync, readdirSync } from 'fs';\nimport { join, relative, dirname } from 'path';\nimport yaml from 'js-yaml';\nimport { discoverSubRepoPaths } from '@kb-labs/sdk';\nimport type { PackageMap, PackageEntry, DevlinkMode } from '@kb-labs/devlink-contracts';\nimport { filterPublishedPackages } from '../npm/index.js';\n\ninterface PnpmWorkspace {\n packages?: string[];\n}\n\ninterface PackageJson {\n name?: string;\n version?: string;\n private?: boolean;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n}\n\n/** Monorepo info discovered on disk */\nexport interface MonorepoInfo {\n /** Dir name e.g. kb-labs-core */\n name: string;\n /** Absolute path to the monorepo root */\n rootPath: string;\n /** All package.json paths within the monorepo */\n packagePaths: string[];\n /** pnpm-workspace.yaml content */\n workspacePackages: string[];\n}\n\n/**\n * Discovers all submodule repos via .gitmodules (layout-agnostic).\n * Includes both monorepos (with pnpm-workspace.yaml) and standalone packages.\n */\nexport function discoverMonorepos(rootDir: string): MonorepoInfo[] {\n const subRepoPaths = discoverSubRepoPaths(rootDir);\n const monorepos: MonorepoInfo[] = [];\n\n for (const repoPath of subRepoPaths) {\n const repoName = repoPath.split('/').pop() ?? repoPath;\n const hasWorkspace = existsSync(join(repoPath, 'pnpm-workspace.yaml'));\n\n if (hasWorkspace) {\n // Monorepo: scan all package.json files\n const packagePaths = findPackageJsonFiles(repoPath);\n const repoWorkspace = yaml.load(\n readFileSync(join(repoPath, 'pnpm-workspace.yaml'), 'utf-8')\n ) as PnpmWorkspace;\n\n monorepos.push({\n name: repoName,\n rootPath: repoPath,\n packagePaths,\n workspacePackages: repoWorkspace.packages ?? [],\n });\n } else {\n // Standalone package (e.g. devkit): treat root package.json as sole package\n const rootPkgPath = join(repoPath, 'package.json');\n if (existsSync(rootPkgPath)) {\n monorepos.push({\n name: repoName,\n rootPath: repoPath,\n packagePaths: [rootPkgPath],\n workspacePackages: [],\n });\n }\n }\n }\n\n return monorepos;\n}\n\n/**\n * Builds a PackageMap: packageName → { linkPath, npmVersion, monorepo }.\n * Scans all packages in all monorepos and collects their name/version.\n *\n * linkPath is relative to the root kb-labs/ dir (so it can be used as link:../path).\n */\nexport function buildPackageMap(monorepos: MonorepoInfo[], rootDir: string): PackageMap {\n const map: PackageMap = {};\n\n for (const monorepo of monorepos) {\n for (const pkgPath of monorepo.packagePaths) {\n if (!existsSync(pkgPath)) {continue;}\n\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as PackageJson;\n } catch {\n continue;\n }\n\n if (!pkg.name || !pkg.version) {continue;}\n // Only include @kb-labs/* packages (or non-scoped kb-labs-* packages)\n if (!pkg.name.startsWith('@kb-labs/') && !pkg.name.startsWith('kb-labs-')) {continue;}\n\n const pkgDir = dirname(pkgPath);\n const linkPath = relative(rootDir, pkgDir);\n\n const entry: PackageEntry = {\n name: pkg.name,\n linkPath,\n npmVersion: `^${pkg.version}`,\n monorepo: monorepo.name,\n private: pkg.private ?? false,\n };\n\n map[pkg.name] = entry;\n }\n }\n\n return map;\n}\n\n/**\n * Async version of buildPackageMap that verifies each package exists on npm.\n * For 'local' mode skips the npm check and returns all packages found on disk.\n * For 'npm'/'auto'/undefined filters out packages not published to the registry.\n */\nexport async function buildPackageMapFiltered(\n monorepos: MonorepoInfo[],\n rootDir: string,\n ttlMs?: number,\n mode?: DevlinkMode\n): Promise<PackageMap> {\n const rawMap = buildPackageMap(monorepos, rootDir);\n // local mode: disk has priority, no npm check needed\n if (mode === 'local') {return rawMap;}\n const packageNames = Object.keys(rawMap);\n const published = await filterPublishedPackages(packageNames, ttlMs);\n const filtered: PackageMap = {};\n for (const name of packageNames) {\n if (published.has(name)) {filtered[name] = rawMap[name]!;}\n }\n return filtered;\n}\n\n/**\n * Determines the current linking mode of cross-repo dependencies in a package.json.\n * Returns counts of link:, npm, workspace: references for @kb-labs/* deps.\n */\nexport function analyzePackageDeps(\n pkgPath: string,\n packageMap: PackageMap\n): { linkCount: number; npmCount: number; workspaceCount: number; unknownCount: number } {\n let linkCount = 0;\n let npmCount = 0;\n let workspaceCount = 0;\n let unknownCount = 0;\n\n if (!existsSync(pkgPath)) {return { linkCount, npmCount, workspaceCount, unknownCount };}\n\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as PackageJson;\n } catch {\n return { linkCount, npmCount, workspaceCount, unknownCount };\n }\n\n const sections = [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies];\n for (const section of sections) {\n if (!section) {continue;}\n for (const [depName, depValue] of Object.entries(section)) {\n if (!packageMap[depName]) {continue;} // Not a cross-repo dep\n if (depValue.startsWith('link:')) {\n linkCount++;\n } else if (depValue.startsWith('workspace:')) {\n workspaceCount++;\n } else if (depValue.startsWith('^') || depValue.startsWith('~') || /^\\d/.test(depValue)) {\n npmCount++;\n } else {\n unknownCount++;\n }\n }\n }\n\n return { linkCount, npmCount, workspaceCount, unknownCount };\n}\n\n/**\n * Finds all package.json files within a monorepo (excludes node_modules, dist).\n */\nfunction findPackageJsonFiles(repoRoot: string): string[] {\n const results: string[] = [];\n\n function walk(dir: string, depth = 0) {\n if (depth > 4) {return;}\n\n let entries: import('fs').Dirent<string>[];\n try {\n entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf-8' });\n } catch {\n return;\n }\n\n for (const entry of entries) {\n if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') {continue;}\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n walk(full, depth + 1);\n } else if (entry.name === 'package.json') {\n results.push(full);\n }\n }\n }\n\n walk(repoRoot);\n return results;\n}\n\n/**\n * Determines which MonorepoInfo a package.json belongs to.\n * Uses rootPath prefix matching.\n */\nexport function resolvePackageMonorepo(\n pkgPath: string,\n monorepos: MonorepoInfo[]\n): MonorepoInfo | null {\n for (const mono of monorepos) {\n if (pkgPath.startsWith(mono.rootPath + '/') || pkgPath === join(mono.rootPath, 'package.json')) {\n return mono;\n }\n }\n return null;\n}\n","import { readFileSync, existsSync } from 'fs';\nimport { relative, dirname, resolve } from 'path';\nimport type {\n DevlinkMode,\n DevlinkPlan,\n DevlinkPlanItem,\n PackageEntry,\n PackageMap,\n} from '@kb-labs/devlink-contracts';\nimport type { MonorepoInfo } from '../discovery/index.js';\nimport { resolvePackageMonorepo } from '../discovery/index.js';\n\ntype DepSection = 'dependencies' | 'devDependencies' | 'peerDependencies';\n\ninterface PackageJson {\n name?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n}\n\n/**\n * Builds a DevlinkPlan — all package.json changes needed for the target mode.\n */\nexport function buildPlan(\n mode: DevlinkMode,\n packageMap: PackageMap,\n monorepos: MonorepoInfo[],\n rootDir: string,\n options: { scopedRepos?: string[] } = {}\n): DevlinkPlan {\n const items: DevlinkPlanItem[] = [];\n const filteredMonorepos = options.scopedRepos?.length\n ? monorepos.filter(m => options.scopedRepos!.includes(m.name))\n : monorepos;\n\n for (const monorepo of filteredMonorepos) {\n for (const pkgPath of monorepo.packagePaths) {\n if (!existsSync(pkgPath)) {continue;}\n\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as PackageJson;\n } catch {\n continue;\n }\n\n const sections: DepSection[] = ['dependencies', 'devDependencies', 'peerDependencies'];\n for (const section of sections) {\n const deps = pkg[section];\n if (!deps) {continue;}\n\n for (const [depName, currentValue] of Object.entries(deps)) {\n const entry = packageMap[depName];\n if (!entry) {continue;} // Not a cross-repo dep\n\n // Skip bare * — ambiguous wildcard, not a versioned dep we manage\n if (currentValue === '*') {continue;}\n\n // workspace:* — only skip if intra-repo (same monorepo handles it via pnpm)\n // Cross-repo workspace:* must be converted to link: or ^version\n if (currentValue.startsWith('workspace:')) {\n const consumerMono = resolvePackageMonorepo(pkgPath, monorepos);\n if (consumerMono && consumerMono.name === entry.monorepo) {continue;}\n }\n\n const targetValue = getTargetValue(mode, entry, pkgPath, rootDir);\n if (targetValue === currentValue) {continue;} // Already correct\n\n items.push({\n packageJsonPath: pkgPath,\n packageJsonRel: relative(rootDir, pkgPath),\n monorepo: monorepo.name,\n depName,\n from: currentValue,\n to: targetValue,\n section,\n });\n }\n }\n }\n }\n\n return {\n mode,\n items,\n timestamp: new Date().toISOString(),\n ...(options.scopedRepos?.length ? { scopedRepos: options.scopedRepos } : {}),\n };\n}\n\n/**\n * Computes the target value for a dependency based on mode.\n * Private packages always stay as link: (not published to npm).\n */\nfunction getTargetValue(\n mode: DevlinkMode,\n entry: PackageEntry,\n fromPackageJson: string,\n rootDir: string\n): string {\n // npm mode: use ^version, but private packages stay as link: (not on npm)\n if (mode === 'npm' && !entry.private) {\n return entry.npmVersion;\n }\n\n // local, auto, or private in npm mode: use link: path\n const fromDir = dirname(fromPackageJson);\n const targetDir = resolve(rootDir, entry.linkPath);\n const relPath = relative(fromDir, targetDir);\n const normalized = relPath.startsWith('.') ? relPath : `./${relPath}`;\n return `link:${normalized}`;\n}\n\n/**\n * Returns a human-readable description of a plan item change.\n */\nexport function describeChange(item: DevlinkPlanItem): string {\n return `${item.depName}: ${item.from} → ${item.to}`;\n}\n\n/**\n * Groups plan items by monorepo for display purposes.\n */\nexport function groupByMonorepo(items: DevlinkPlanItem[]): Map<string, DevlinkPlanItem[]> {\n const groups = new Map<string, DevlinkPlanItem[]>();\n for (const item of items) {\n if (!groups.has(item.monorepo)) {groups.set(item.monorepo, []);}\n groups.get(item.monorepo)!.push(item);\n }\n return groups;\n}\n","import { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { execSync } from 'child_process';\nimport type { DevlinkPlan } from '@kb-labs/devlink-contracts';\n\ntype DepSection = 'dependencies' | 'devDependencies' | 'peerDependencies';\n\ninterface PackageJson {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n [key: string]: unknown;\n}\n\nexport interface ApplyOptions {\n dryRun?: boolean;\n}\n\nexport interface ApplyResult {\n applied: number;\n skipped: number;\n errors: Array<{ file: string; error: string }>;\n}\n\n/**\n * Applies a DevlinkPlan to package.json files on disk.\n * Groups changes by file to minimize I/O operations.\n */\nexport async function applyPlan(plan: DevlinkPlan, options: ApplyOptions = {}): Promise<ApplyResult> {\n const { dryRun = false } = options;\n\n // Group items by packageJsonPath\n const byFile = new Map<string, typeof plan.items>();\n for (const item of plan.items) {\n if (!byFile.has(item.packageJsonPath)) {byFile.set(item.packageJsonPath, []);}\n byFile.get(item.packageJsonPath)!.push(item);\n }\n\n let applied = 0;\n let skipped = 0;\n const errors: ApplyResult['errors'] = [];\n\n for (const [filePath, items] of byFile.entries()) {\n if (!existsSync(filePath)) {\n skipped++;\n continue;\n }\n\n if (dryRun) {\n applied += items.length;\n continue;\n }\n\n try {\n const raw = readFileSync(filePath, 'utf-8');\n const pkg = JSON.parse(raw) as PackageJson;\n\n for (const item of items) {\n const section = pkg[item.section as DepSection];\n if (section && item.depName in section) {\n section[item.depName] = item.to;\n applied++;\n } else {\n skipped++;\n }\n }\n\n // Write back with same trailing newline and 2-space indent\n const trailingNewline = raw.endsWith('\\n') ? '\\n' : '';\n writeFileSync(filePath, JSON.stringify(pkg, null, 2) + trailingNewline, 'utf-8');\n } catch (err) {\n errors.push({ file: filePath, error: String(err) });\n }\n }\n\n return { applied, skipped, errors };\n}\n\n/**\n * Checks if the given directory has uncommitted git changes.\n * Returns a list of modified files, or empty array if clean.\n */\nexport function checkGitDirty(repoPath: string): string[] {\n try {\n const output = execSync('git status --porcelain', {\n cwd: repoPath,\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return output\n .split('\\n')\n .filter(Boolean)\n .map(line => line.slice(3).trim());\n } catch {\n return []; // Not a git repo or git not available\n }\n}\n","import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';\nimport { join, dirname } from 'path';\nimport type { DevlinkState, DevlinkPlan } from '@kb-labs/devlink-contracts';\n\nconst DEFAULT_STATE: DevlinkState = {\n currentMode: null,\n lastApplied: null,\n frozenAt: null,\n};\n\nfunction getStatePath(rootDir: string): string {\n return join(rootDir, '.kb', 'devlink', 'state.json');\n}\n\nfunction getLockPath(rootDir: string): string {\n return join(rootDir, '.kb', 'devlink', 'lock.json');\n}\n\nfunction ensureDir(filePath: string): void {\n mkdirSync(dirname(filePath), { recursive: true });\n}\n\n// ─── State ────────────────────────────────────────────────────────────────────\n\nexport function loadState(rootDir: string): DevlinkState {\n const statePath = getStatePath(rootDir);\n if (!existsSync(statePath)) {return { ...DEFAULT_STATE };}\n\n try {\n return JSON.parse(readFileSync(statePath, 'utf-8')) as DevlinkState;\n } catch {\n return { ...DEFAULT_STATE };\n }\n}\n\nexport function saveState(rootDir: string, state: DevlinkState): void {\n const statePath = getStatePath(rootDir);\n ensureDir(statePath);\n writeFileSync(statePath, JSON.stringify(state, null, 2) + '\\n', 'utf-8');\n}\n\n// ─── Lock / Freeze ────────────────────────────────────────────────────────────\n\nexport interface LockFile {\n frozenAt: string;\n plan: DevlinkPlan;\n}\n\nexport function freeze(rootDir: string, currentPlan: DevlinkPlan): LockFile {\n const lock: LockFile = {\n frozenAt: new Date().toISOString(),\n plan: currentPlan,\n };\n\n const lockPath = getLockPath(rootDir);\n ensureDir(lockPath);\n writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\\n', 'utf-8');\n\n // Update state.frozenAt\n const state = loadState(rootDir);\n saveState(rootDir, { ...state, frozenAt: lock.frozenAt });\n\n return lock;\n}\n\nexport function loadLock(rootDir: string): LockFile | null {\n const lockPath = getLockPath(rootDir);\n if (!existsSync(lockPath)) {return null;}\n\n try {\n return JSON.parse(readFileSync(lockPath, 'utf-8')) as LockFile;\n } catch {\n return null;\n }\n}\n","import {\n readFileSync,\n writeFileSync,\n mkdirSync,\n existsSync,\n readdirSync,\n copyFileSync,\n rmSync,\n} from 'fs';\nimport { join, dirname } from 'path';\nimport type { DevlinkBackup, DevlinkMode } from '@kb-labs/devlink-contracts';\n\nfunction getBackupsDir(rootDir: string): string {\n return join(rootDir, '.kb', 'devlink', 'backups');\n}\n\nfunction getMetaPath(backupDir: string): string {\n return join(backupDir, 'meta.json');\n}\n\n/**\n * Creates a backup of given package.json files before a mutation.\n * Returns the backup metadata.\n */\nexport function createBackup(\n rootDir: string,\n filePaths: string[],\n description: string,\n currentMode: DevlinkMode | null\n): DevlinkBackup {\n const id = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;\n const backupDir = join(getBackupsDir(rootDir), id);\n mkdirSync(backupDir, { recursive: true });\n\n const backedUpFiles: string[] = [];\n\n for (const filePath of filePaths) {\n if (!existsSync(filePath)) {continue;}\n\n // Flatten the path for storage: replace / with __ to keep it flat\n const safeName = filePath.replace(/\\//g, '__').replace(/:/g, '_');\n const destPath = join(backupDir, safeName);\n copyFileSync(filePath, destPath);\n backedUpFiles.push(filePath);\n }\n\n const meta: DevlinkBackup = {\n id,\n timestamp: new Date().toISOString(),\n description,\n files: backedUpFiles,\n modeAtBackup: currentMode,\n };\n\n writeFileSync(getMetaPath(backupDir), JSON.stringify(meta, null, 2) + '\\n', 'utf-8');\n\n // Auto-prune: keep only the 10 most recent backups\n pruneBackups(rootDir, 10);\n\n return meta;\n}\n\n/**\n * Lists all backups, sorted newest first.\n */\nexport function listBackups(rootDir: string): DevlinkBackup[] {\n const backupsDir = getBackupsDir(rootDir);\n if (!existsSync(backupsDir)) {return [];}\n\n const entries = readdirSync(backupsDir, { withFileTypes: true });\n const backups: DevlinkBackup[] = [];\n\n for (const entry of entries) {\n if (!entry.isDirectory()) {continue;}\n const metaPath = getMetaPath(join(backupsDir, entry.name));\n if (!existsSync(metaPath)) {continue;}\n\n try {\n const meta = JSON.parse(readFileSync(metaPath, 'utf-8')) as DevlinkBackup;\n backups.push(meta);\n } catch {\n // Skip corrupted backups\n }\n }\n\n return backups.sort((a, b) => b.timestamp.localeCompare(a.timestamp));\n}\n\n/**\n * Returns the most recent backup, or null if none exist.\n */\nexport function getLastBackup(rootDir: string): DevlinkBackup | null {\n const backups = listBackups(rootDir);\n return backups[0] ?? null;\n}\n\n/**\n * Restores package.json files from a specific backup.\n */\nexport function restoreBackup(rootDir: string, backupId: string): { restored: number; errors: string[] } {\n const backupDir = join(getBackupsDir(rootDir), backupId);\n const metaPath = getMetaPath(backupDir);\n\n if (!existsSync(metaPath)) {\n throw new Error(`Backup ${backupId} not found`);\n }\n\n const meta = JSON.parse(readFileSync(metaPath, 'utf-8')) as DevlinkBackup;\n let restored = 0;\n const errors: string[] = [];\n\n for (const originalPath of meta.files) {\n const safeName = originalPath.replace(/\\//g, '__').replace(/:/g, '_');\n const srcPath = join(backupDir, safeName);\n\n if (!existsSync(srcPath)) {\n errors.push(`Backup file missing: ${safeName}`);\n continue;\n }\n\n try {\n mkdirSync(dirname(originalPath), { recursive: true });\n copyFileSync(srcPath, originalPath);\n restored++;\n } catch (err) {\n errors.push(`Failed to restore ${originalPath}: ${String(err)}`);\n }\n }\n\n return { restored, errors };\n}\n\n/**\n * Remove oldest backups beyond the retention limit.\n */\nexport function pruneBackups(rootDir: string, maxBackups = 10): number {\n const backups = listBackups(rootDir); // sorted newest first\n if (backups.length <= maxBackups) {return 0;}\n\n const backupsDir = getBackupsDir(rootDir);\n let pruned = 0;\n\n for (const old of backups.slice(maxBackups)) {\n const dir = join(backupsDir, old.id);\n try {\n rmSync(dir, { recursive: true, force: true });\n pruned++;\n } catch { /* skip */ }\n }\n\n return pruned;\n}\n","/**\n * Workspace YAML Manager\n *\n * Generates/updates pnpm-workspace.yaml in sub-repos to include\n * cross-repo paths needed for autonomous `cd sub-repo && pnpm install`.\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { join, relative } from 'path';\nimport yaml from 'js-yaml';\nimport type { PackageMap } from '@kb-labs/devlink-contracts';\nimport type { MonorepoInfo } from '../discovery/index.js';\n\ninterface PnpmWorkspace {\n packages?: string[];\n}\n\ninterface PackageJson {\n name?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n}\n\nexport interface WorkspaceYamlUpdate {\n repoName: string;\n repoPath: string;\n added: string[];\n removed: string[];\n kept: string[];\n}\n\n/**\n * Update pnpm-workspace.yaml in all sub-repos to include correct cross-repo paths.\n *\n * For each sub-repo:\n * 1. Keep intra-repo patterns (packages/*, apps/*, etc.)\n * 2. Analyze which cross-repo packages are needed (from deps in all package.json)\n * 3. Compute correct relative paths to those packages\n * 4. Write updated workspace.yaml\n */\nexport function updateWorkspaceYamls(\n monorepos: MonorepoInfo[],\n packageMap: PackageMap,\n rootDir: string,\n options: { dryRun?: boolean } = {},\n): WorkspaceYamlUpdate[] {\n const updates: WorkspaceYamlUpdate[] = [];\n\n for (const mono of monorepos) {\n const wsPath = join(mono.rootPath, 'pnpm-workspace.yaml');\n\n // Skip repos without workspace yaml (standalone packages)\n if (!existsSync(wsPath)) {continue;}\n\n const update = updateOneWorkspaceYaml(mono, monorepos, packageMap, rootDir, options);\n if (update) {\n updates.push(update);\n }\n }\n\n return updates;\n}\n\nfunction updateOneWorkspaceYaml(\n mono: MonorepoInfo,\n allMonorepos: MonorepoInfo[],\n packageMap: PackageMap,\n rootDir: string,\n options: { dryRun?: boolean },\n): WorkspaceYamlUpdate | null {\n const wsPath = join(mono.rootPath, 'pnpm-workspace.yaml');\n\n // Read current workspace.yaml\n let workspace: PnpmWorkspace;\n try {\n workspace = yaml.load(readFileSync(wsPath, 'utf-8')) as PnpmWorkspace;\n } catch {\n return null;\n }\n\n const currentPatterns = workspace.packages ?? [];\n\n // Separate intra-repo patterns from cross-repo paths\n const intraPatterns: string[] = [];\n for (const pattern of currentPatterns) {\n // Intra-repo: doesn't start with ../ (relative to own root)\n if (!pattern.startsWith('../') && !pattern.startsWith('..\\\\')) {\n intraPatterns.push(pattern);\n }\n }\n\n // Find all cross-repo packages needed by this sub-repo\n const neededRepos = new Set<string>(); // monorepo names\n for (const pkgPath of mono.packagePaths) {\n if (!existsSync(pkgPath)) {continue;}\n\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as PackageJson;\n } catch {continue;}\n\n const allDeps = {\n ...pkg.dependencies,\n ...pkg.devDependencies,\n ...pkg.peerDependencies,\n };\n\n for (const depName of Object.keys(allDeps)) {\n const entry = packageMap[depName];\n if (!entry) {continue;}\n // Cross-repo dep: target is in a different monorepo\n if (entry.monorepo !== mono.name) {\n neededRepos.add(entry.monorepo);\n }\n }\n }\n\n // Compute cross-repo patterns\n const crossPatterns: string[] = [];\n for (const repoName of neededRepos) {\n const targetMono = allMonorepos.find(m => m.name === repoName);\n if (!targetMono) {continue;}\n\n const relPath = relative(mono.rootPath, targetMono.rootPath);\n\n if (targetMono.workspacePackages.length > 0) {\n // Monorepo: add patterns for its internal packages\n // e.g. \"../../infra/kb-labs-devkit\" for standalone\n // e.g. \"../../platform/kb-labs-core/packages/*\" for monorepos with packages/*\n for (const pattern of targetMono.workspacePackages) {\n crossPatterns.push(`${relPath}/${pattern}`);\n }\n } else {\n // Standalone: add direct path\n crossPatterns.push(relPath);\n }\n }\n\n // Sort and deduplicate\n crossPatterns.sort();\n const newPatterns = [...intraPatterns, ...crossPatterns];\n\n // Check if anything changed\n const oldSet = new Set(currentPatterns);\n const newSet = new Set(newPatterns);\n const added = crossPatterns.filter(p => !oldSet.has(p));\n const removed = currentPatterns.filter(p => p.startsWith('../') && !newSet.has(p));\n const kept = intraPatterns;\n\n if (added.length === 0 && removed.length === 0) {\n return null; // No changes needed\n }\n\n // Write updated workspace.yaml\n if (!options.dryRun) {\n const output: PnpmWorkspace = { packages: newPatterns };\n writeFileSync(wsPath, yaml.dump(output, { lineWidth: -1, quotingType: '\"' }), 'utf-8');\n }\n\n return {\n repoName: mono.name,\n repoPath: mono.rootPath,\n added,\n removed,\n kept,\n };\n}\n","/**\n * Diagnostics — detect broken deps, stale lockfiles, cross-repo workspace:* issues\n */\n\nimport { readFileSync, existsSync, statSync } from 'fs';\nimport { join, dirname, resolve } from 'path';\nimport type { DiagnosticIssue, PackageMap } from '@kb-labs/devlink-contracts';\nimport type { MonorepoInfo } from '../discovery/index.js';\nimport { resolvePackageMonorepo } from '../discovery/index.js';\n\ninterface PackageJson {\n name?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n}\n\n/**\n * Run all diagnostic checks across the monorepo.\n */\nexport function diagnose(\n monorepos: MonorepoInfo[],\n packageMap: PackageMap,\n rootDir: string,\n): DiagnosticIssue[] {\n const issues: DiagnosticIssue[] = [];\n\n for (const mono of monorepos) {\n for (const pkgPath of mono.packagePaths) {\n if (!existsSync(pkgPath)) {continue;}\n\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as PackageJson;\n } catch {continue;}\n\n const sections = [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies];\n for (const section of sections) {\n if (!section) {continue;}\n for (const [depName, depValue] of Object.entries(section)) {\n // Check broken link: paths\n if (depValue.startsWith('link:')) {\n const targetPath = resolve(dirname(pkgPath), depValue.slice(5));\n if (!existsSync(targetPath)) {\n issues.push({\n type: 'broken-link',\n severity: 'error',\n file: pkgPath,\n dep: depName,\n message: `${depName}: link:${depValue.slice(5)} → target does not exist`,\n fix: 'Run devlink switch --mode=local to recalculate paths',\n });\n }\n }\n\n // Check cross-repo workspace:*\n if (depValue.startsWith('workspace:') && packageMap[depName]) {\n const consumerMono = resolvePackageMonorepo(pkgPath, monorepos);\n const depMonorepo = packageMap[depName]!.monorepo;\n if (consumerMono && consumerMono.name !== depMonorepo) {\n issues.push({\n type: 'cross-repo-workspace',\n severity: 'warning',\n file: pkgPath,\n dep: depName,\n message: `${depName}: workspace:* crosses sub-repo boundary (${consumerMono.name} → ${depMonorepo})`,\n fix: 'Run devlink switch --mode=local to convert to link:',\n });\n }\n }\n }\n }\n }\n\n // Check stale lockfile\n checkStaleLockfile(mono, issues);\n }\n\n // Sort: errors first, then warnings\n issues.sort((a, b) => {\n if (a.severity !== b.severity) {return a.severity === 'error' ? -1 : 1;}\n return a.type.localeCompare(b.type);\n });\n\n return issues;\n}\n\n/**\n * Check if a sub-repo's lockfile is stale (older than any package.json).\n */\nfunction checkStaleLockfile(mono: MonorepoInfo, issues: DiagnosticIssue[]): void {\n const lockPath = join(mono.rootPath, 'pnpm-lock.yaml');\n if (!existsSync(lockPath)) {return;}\n\n let lockMtime: number;\n try {\n lockMtime = statSync(lockPath).mtimeMs;\n } catch {return;}\n\n for (const pkgPath of mono.packagePaths) {\n try {\n const pkgMtime = statSync(pkgPath).mtimeMs;\n if (pkgMtime > lockMtime) {\n issues.push({\n type: 'stale-lockfile',\n severity: 'warning',\n file: lockPath,\n message: `${mono.name}: pnpm-lock.yaml is older than ${pkgPath}`,\n fix: 'Delete lockfile and run pnpm install, or use devlink switch --install',\n });\n return; // One warning per repo is enough\n }\n } catch { /* skip */ }\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kb-labs/devlink-core",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Core business logic for DevLink plugin — discovery, plan, apply, state, backup.",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./discovery": {
|
|
14
|
+
"import": "./dist/discovery/index.js",
|
|
15
|
+
"types": "./dist/discovery/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./plan": {
|
|
18
|
+
"import": "./dist/plan/index.js",
|
|
19
|
+
"types": "./dist/plan/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./apply": {
|
|
22
|
+
"import": "./dist/apply/index.js",
|
|
23
|
+
"types": "./dist/apply/index.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./state": {
|
|
26
|
+
"import": "./dist/state/index.js",
|
|
27
|
+
"types": "./dist/state/index.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./backup": {
|
|
30
|
+
"import": "./dist/backup/index.js",
|
|
31
|
+
"types": "./dist/backup/index.d.ts"
|
|
32
|
+
},
|
|
33
|
+
"./dist/*": "./dist/*"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"scripts": {
|
|
41
|
+
"clean": "rimraf dist",
|
|
42
|
+
"build": "tsup --config tsup.config.ts",
|
|
43
|
+
"dev": "tsup --config tsup.config.ts --watch",
|
|
44
|
+
"lint": "eslint src --ext .ts",
|
|
45
|
+
"lint:fix": "eslint . --fix",
|
|
46
|
+
"type-check": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run --passWithNoTests",
|
|
48
|
+
"test:watch": "vitest"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@kb-labs/devlink-contracts": "^1.5.0",
|
|
52
|
+
"@kb-labs/sdk": "^1.5.0",
|
|
53
|
+
"js-yaml": "^4.1.0",
|
|
54
|
+
"zod": "^3.23.8"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@kb-labs/devkit": "link:../../../../infra/kb-labs-devkit",
|
|
58
|
+
"@types/js-yaml": "^4.0.9",
|
|
59
|
+
"@types/node": "^24.3.3",
|
|
60
|
+
"eslint": "^9",
|
|
61
|
+
"rimraf": "^6.0.1",
|
|
62
|
+
"tsup": "^8.5.0",
|
|
63
|
+
"typescript": "^5.6.3",
|
|
64
|
+
"vitest": "^3.2.4"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=20.0.0",
|
|
68
|
+
"pnpm": ">=9.0.0"
|
|
69
|
+
}
|
|
70
|
+
}
|