@ankhorage/doctor 0.7.2 → 0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f260d59: Validate the exact Devtools-owned Bun runtime policy across package metadata and managed CI/release workflows, with actionable `ankh devtools sync` repair diagnostics.
8
+
9
+ ## 0.7.3
10
+
11
+ ### Patch Changes
12
+
13
+ - a6a33a3: Accept `@ankhorage/devtools` as a runtime dependency for `@ankhorage/ankh` while keeping the usual devDependency requirement for other public packages.
14
+
3
15
  ## 0.7.2
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # DOCTOR
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v0.7.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v0.7.2](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
7
7
 
8
8
  Executable doctor provider and standalone CLI for lightweight Ankhorage repo and package compliance diagnostics.
9
9
 
@@ -0,0 +1,3 @@
1
+ import type { DoctorAnalysisResult } from './analysis.js';
2
+ export declare function applyBunRuntimePolicy(result: DoctorAnalysisResult): Promise<DoctorAnalysisResult>;
3
+ //# sourceMappingURL=bunPolicyAnalysis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bunPolicyAnalysis.d.ts","sourceRoot":"","sources":["../src/bunPolicyAnalysis.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAuB1D,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC,CAmB/B"}
@@ -0,0 +1,190 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ const DEVTOOLS_POLICY_SPECIFIER = '@ankhorage/devtools/policy';
6
+ const REPAIR_HINT = 'Run "ankh devtools sync" to repair the managed Bun state.';
7
+ const BUN_VERSION_PATTERN = /^\s*bun-version:\s*['"]?([^'"\s#]+)['"]?\s*(?:#.*)?$/mu;
8
+ const WORKFLOWS = [
9
+ {
10
+ relativePath: path.join('.github', 'workflows', 'ci.yml'),
11
+ ruleId: 'repo.workflows.ci.bun-policy',
12
+ },
13
+ {
14
+ relativePath: path.join('.github', 'workflows', 'release.yml'),
15
+ ruleId: 'repo.workflows.release.bun-policy',
16
+ },
17
+ ];
18
+ export async function applyBunRuntimePolicy(result) {
19
+ if (result.profile !== 'public-package' || !result.hasPackageJson) {
20
+ return result;
21
+ }
22
+ const policy = await loadTargetBunRuntimePolicy(result.targetPath);
23
+ const packageJsonPath = path.join(result.targetPath, 'package.json');
24
+ const packageJson = await readPackageJson(packageJsonPath);
25
+ if (policy === null || packageJson === null) {
26
+ return result;
27
+ }
28
+ const diagnostics = [
29
+ ...result.diagnostics,
30
+ ...analyzePackageBunPolicy(packageJson, packageJsonPath, result, policy),
31
+ ...(await analyzeWorkflowBunPolicy(result, policy)),
32
+ ];
33
+ return withDiagnostics(result, diagnostics);
34
+ }
35
+ async function loadTargetBunRuntimePolicy(targetPath) {
36
+ const requireFromTarget = createRequire(path.join(targetPath, 'package.json'));
37
+ try {
38
+ const modulePath = requireFromTarget.resolve(DEVTOOLS_POLICY_SPECIFIER);
39
+ const importedModule = (await import(pathToFileURL(modulePath).href));
40
+ return parseBunRuntimePolicy(importedModule);
41
+ }
42
+ catch (error) {
43
+ if (isUnavailablePolicyError(error)) {
44
+ return null;
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+ function analyzePackageBunPolicy(packageJson, packageJsonPath, result, policy) {
50
+ const diagnostics = [];
51
+ const { packageManager } = packageJson;
52
+ if (isNonEmptyString(packageManager) && packageManager.startsWith('bun@')) {
53
+ pushMismatch(diagnostics, {
54
+ actual: packageManager,
55
+ expected: policy.packageManager,
56
+ location: 'package.json#packageManager',
57
+ path: packageJsonPath,
58
+ profile: result.profile,
59
+ ruleId: 'package.json.package-manager.policy',
60
+ });
61
+ }
62
+ const devDependencies = isRecord(packageJson.devDependencies)
63
+ ? packageJson.devDependencies
64
+ : null;
65
+ const bunTypes = devDependencies?.['@types/bun'];
66
+ if (isNonEmptyString(bunTypes)) {
67
+ pushMismatch(diagnostics, {
68
+ actual: bunTypes,
69
+ expected: policy.typesRange,
70
+ location: 'package.json#devDependencies.@types/bun',
71
+ path: packageJsonPath,
72
+ profile: result.profile,
73
+ ruleId: 'package.dependencies.types-bun.policy',
74
+ });
75
+ }
76
+ return diagnostics;
77
+ }
78
+ async function analyzeWorkflowBunPolicy(result, policy) {
79
+ const diagnostics = await Promise.all(WORKFLOWS.map(async (workflow) => await analyzeWorkflow(result, workflow, policy)));
80
+ return diagnostics.flat();
81
+ }
82
+ async function analyzeWorkflow(result, workflow, policy) {
83
+ const workflowPath = path.join(result.targetPath, workflow.relativePath);
84
+ const contents = await readTextOrNull(workflowPath);
85
+ if (contents === null) {
86
+ return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'missing-path')];
87
+ }
88
+ const match = BUN_VERSION_PATTERN.exec(contents);
89
+ const actual = match?.[1];
90
+ if (!isNonEmptyString(actual)) {
91
+ return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'field-missing')];
92
+ }
93
+ const diagnostics = [];
94
+ pushMismatch(diagnostics, {
95
+ actual,
96
+ expected: policy.version,
97
+ location: `${workflow.relativePath}#bun-version`,
98
+ path: workflowPath,
99
+ profile: result.profile,
100
+ ruleId: workflow.ruleId,
101
+ });
102
+ return diagnostics;
103
+ }
104
+ function createWorkflowDiagnostic(result, workflow, policy, workflowPath, code) {
105
+ const detail = code === 'missing-path'
106
+ ? `Managed workflow is missing: ${workflow.relativePath}.`
107
+ : `${workflow.relativePath} does not define bun-version.`;
108
+ return {
109
+ code,
110
+ message: `${detail} Expected Bun ${policy.version}. ${REPAIR_HINT}`,
111
+ path: workflowPath,
112
+ profile: result.profile,
113
+ ruleId: workflow.ruleId,
114
+ severity: 'error',
115
+ };
116
+ }
117
+ function pushMismatch(diagnostics, input) {
118
+ if (input.actual === input.expected) {
119
+ return;
120
+ }
121
+ diagnostics.push({
122
+ code: 'field-invalid',
123
+ message: `Bun policy drift at ${input.location}: expected "${input.expected}", found "${input.actual}". ${REPAIR_HINT}`,
124
+ path: input.path,
125
+ profile: input.profile,
126
+ ruleId: input.ruleId,
127
+ severity: 'error',
128
+ });
129
+ }
130
+ function parseBunRuntimePolicy(importedModule) {
131
+ if (!isRecord(importedModule) || !isRecord(importedModule.bunRuntimePolicy)) {
132
+ return null;
133
+ }
134
+ const { packageManager, typesRange, version } = importedModule.bunRuntimePolicy;
135
+ return isNonEmptyString(packageManager) &&
136
+ isNonEmptyString(typesRange) &&
137
+ isNonEmptyString(version)
138
+ ? { packageManager, typesRange, version }
139
+ : null;
140
+ }
141
+ function withDiagnostics(result, diagnostics) {
142
+ return {
143
+ ...result,
144
+ diagnostics,
145
+ fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },
146
+ };
147
+ }
148
+ async function readPackageJson(packageJsonPath) {
149
+ try {
150
+ const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
151
+ return isRecord(parsed) ? parsed : null;
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ async function readTextOrNull(filePath) {
158
+ try {
159
+ return await fs.readFile(filePath, 'utf8');
160
+ }
161
+ catch (error) {
162
+ if (isNodeError(error) && error.code === 'ENOENT') {
163
+ return null;
164
+ }
165
+ throw error;
166
+ }
167
+ }
168
+ function isUnavailablePolicyError(error) {
169
+ if (isNodeError(error) &&
170
+ (error.code === 'MODULE_NOT_FOUND' ||
171
+ error.code === 'ERR_MODULE_NOT_FOUND' ||
172
+ error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED')) {
173
+ return true;
174
+ }
175
+ const message = error instanceof Error ? error.message : String(error);
176
+ return (message.includes(DEVTOOLS_POLICY_SPECIFIER) &&
177
+ (message.includes('Cannot find module') ||
178
+ message.includes('not exported') ||
179
+ message.includes('Package subpath')));
180
+ }
181
+ function isNonEmptyString(value) {
182
+ return typeof value === 'string' && value.trim() !== '';
183
+ }
184
+ function isRecord(value) {
185
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
186
+ }
187
+ function isNodeError(error) {
188
+ return error instanceof Error && 'code' in error;
189
+ }
190
+ //# sourceMappingURL=bunPolicyAnalysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bunPolicyAnalysis.js","sourceRoot":"","sources":["../src/bunPolicyAnalysis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAWzC,MAAM,yBAAyB,GAAG,4BAA4B,CAAC;AAC/D,MAAM,WAAW,GAAG,2DAA2D,CAAC;AAChF,MAAM,mBAAmB,GAAG,wDAAwD,CAAC;AACrF,MAAM,SAAS,GAAG;IAChB;QACE,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,QAAQ,CAAC;QACzD,MAAM,EAAE,8BAA8B;KACvC;IACD;QACE,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,aAAa,CAAC;QAC9D,MAAM,EAAE,mCAAmC;KAC5C;CAC6F,CAAC;AAEjG,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAA4B;IAE5B,IAAI,MAAM,CAAC,OAAO,KAAK,gBAAgB,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QAClE,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,0BAA0B,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACnE,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;IACrE,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,eAAe,CAAC,CAAC;IAC3D,IAAI,MAAM,KAAK,IAAI,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QAC5C,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,WAAW,GAAG;QAClB,GAAG,MAAM,CAAC,WAAW;QACrB,GAAG,uBAAuB,CAAC,WAAW,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC;QACxE,GAAG,CAAC,MAAM,wBAAwB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACpD,CAAC;IAEF,OAAO,eAAe,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAC9C,CAAC;AAED,KAAK,UAAU,0BAA0B,CACvC,UAAkB;IAElB,MAAM,iBAAiB,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC;IAC/E,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;QACxE,MAAM,cAAc,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAY,CAAC;QACjF,OAAO,qBAAqB,CAAC,cAAc,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,wBAAwB,CAAC,KAAK,CAAC,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,uBAAuB,CAC9B,WAAoC,EACpC,eAAuB,EACvB,MAA4B,EAC5B,MAAgC;IAEhC,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,MAAM,EAAE,cAAc,EAAE,GAAG,WAAW,CAAC;IACvC,IAAI,gBAAgB,CAAC,cAAc,CAAC,IAAI,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1E,YAAY,CAAC,WAAW,EAAE;YACxB,MAAM,EAAE,cAAc;YACtB,QAAQ,EAAE,MAAM,CAAC,cAAc;YAC/B,QAAQ,EAAE,6BAA6B;YACvC,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,MAAM,EAAE,qCAAqC;SAC9C,CAAC,CAAC;IACL,CAAC;IAED,MAAM,eAAe,GAAG,QAAQ,CAAC,WAAW,CAAC,eAAe,CAAC;QAC3D,CAAC,CAAC,WAAW,CAAC,eAAe;QAC7B,CAAC,CAAC,IAAI,CAAC;IACT,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC,YAAY,CAAC,CAAC;IACjD,IAAI,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/B,YAAY,CAAC,WAAW,EAAE;YACxB,MAAM,EAAE,QAAQ;YAChB,QAAQ,EAAE,MAAM,CAAC,UAAU;YAC3B,QAAQ,EAAE,yCAAyC;YACnD,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,MAAM,EAAE,uCAAuC;SAChD,CAAC,CAAC;IACL,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,KAAK,UAAU,wBAAwB,CACrC,MAA4B,EAC5B,MAAgC;IAEhC,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,GAAG,CACnC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,MAAM,eAAe,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CACnF,CAAC;IACF,OAAO,WAAW,CAAC,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED,KAAK,UAAU,eAAe,CAC5B,MAA4B,EAC5B,QAAoC,EACpC,MAAgC;IAEhC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,YAAY,CAAC,CAAC;IACpD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,CAAC,wBAAwB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC,CAAC;IAC5F,CAAC;IAED,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1B,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,wBAAwB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe,CAAC,CAAC,CAAC;IAC7F,CAAC;IAED,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,YAAY,CAAC,WAAW,EAAE;QACxB,MAAM;QACN,QAAQ,EAAE,MAAM,CAAC,OAAO;QACxB,QAAQ,EAAE,GAAG,QAAQ,CAAC,YAAY,cAAc;QAChD,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,QAAQ,CAAC,MAAM;KACxB,CAAC,CAAC;IACH,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,SAAS,wBAAwB,CAC/B,MAA4B,EAC5B,QAAoC,EACpC,MAAgC,EAChC,YAAoB,EACpB,IAA8B;IAE9B,MAAM,MAAM,GACV,IAAI,KAAK,cAAc;QACrB,CAAC,CAAC,gCAAgC,QAAQ,CAAC,YAAY,GAAG;QAC1D,CAAC,CAAC,GAAG,QAAQ,CAAC,YAAY,+BAA+B,CAAC;IAC9D,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,MAAM,iBAAiB,MAAM,CAAC,OAAO,KAAK,WAAW,EAAE;QACnE,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,QAAQ,EAAE,OAAO;KAClB,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CACnB,WAA+B,EAC/B,KAOC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,QAAQ,EAAE,CAAC;QACpC,OAAO;IACT,CAAC;IACD,WAAW,CAAC,IAAI,CAAC;QACf,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,uBAAuB,KAAK,CAAC,QAAQ,eAAe,KAAK,CAAC,QAAQ,aAAa,KAAK,CAAC,MAAM,MAAM,WAAW,EAAE;QACvH,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,QAAQ,EAAE,OAAO;KAClB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,qBAAqB,CAAC,cAAuB;IACpD,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAC5E,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC,gBAAgB,CAAC;IAChF,OAAO,gBAAgB,CAAC,cAAc,CAAC;QACrC,gBAAgB,CAAC,UAAU,CAAC;QAC5B,gBAAgB,CAAC,OAAO,CAAC;QACzB,CAAC,CAAC,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE;QACzC,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,SAAS,eAAe,CACtB,MAA4B,EAC5B,WAAwC;IAExC,OAAO;QACL,GAAG,MAAM;QACT,WAAW;QACX,OAAO,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE;KAC7E,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,eAAuB;IACpD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,CAAY,CAAC;QACjF,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,QAAgB;IAC5C,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,WAAW,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAClD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,wBAAwB,CAAC,KAAc;IAC9C,IACE,WAAW,CAAC,KAAK,CAAC;QAClB,CAAC,KAAK,CAAC,IAAI,KAAK,kBAAkB;YAChC,KAAK,CAAC,IAAI,KAAK,sBAAsB;YACrC,KAAK,CAAC,IAAI,KAAK,+BAA+B,CAAC,EACjD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,CACL,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAC;QAC3C,CAAC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC;YACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;YAChC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC,CACvC,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;AAC1D,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,CAAC;AACnD,CAAC","sourcesContent":["import { promises as fs } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport type { DoctorAnalysisResult } from './analysis.js';\nimport type { DoctorDiagnostic, DoctorRuleId } from './diagnostics.js';\n\ninterface BunRuntimePolicySnapshot {\n readonly packageManager: string;\n readonly typesRange: string;\n readonly version: string;\n}\n\nconst DEVTOOLS_POLICY_SPECIFIER = '@ankhorage/devtools/policy';\nconst REPAIR_HINT = 'Run \"ankh devtools sync\" to repair the managed Bun state.';\nconst BUN_VERSION_PATTERN = /^\\s*bun-version:\\s*['\"]?([^'\"\\s#]+)['\"]?\\s*(?:#.*)?$/mu;\nconst WORKFLOWS = [\n {\n relativePath: path.join('.github', 'workflows', 'ci.yml'),\n ruleId: 'repo.workflows.ci.bun-policy',\n },\n {\n relativePath: path.join('.github', 'workflows', 'release.yml'),\n ruleId: 'repo.workflows.release.bun-policy',\n },\n] as const satisfies readonly { readonly relativePath: string; readonly ruleId: DoctorRuleId }[];\n\nexport async function applyBunRuntimePolicy(\n result: DoctorAnalysisResult,\n): Promise<DoctorAnalysisResult> {\n if (result.profile !== 'public-package' || !result.hasPackageJson) {\n return result;\n }\n\n const policy = await loadTargetBunRuntimePolicy(result.targetPath);\n const packageJsonPath = path.join(result.targetPath, 'package.json');\n const packageJson = await readPackageJson(packageJsonPath);\n if (policy === null || packageJson === null) {\n return result;\n }\n\n const diagnostics = [\n ...result.diagnostics,\n ...analyzePackageBunPolicy(packageJson, packageJsonPath, result, policy),\n ...(await analyzeWorkflowBunPolicy(result, policy)),\n ];\n\n return withDiagnostics(result, diagnostics);\n}\n\nasync function loadTargetBunRuntimePolicy(\n targetPath: string,\n): Promise<BunRuntimePolicySnapshot | null> {\n const requireFromTarget = createRequire(path.join(targetPath, 'package.json'));\n try {\n const modulePath = requireFromTarget.resolve(DEVTOOLS_POLICY_SPECIFIER);\n const importedModule = (await import(pathToFileURL(modulePath).href)) as unknown;\n return parseBunRuntimePolicy(importedModule);\n } catch (error) {\n if (isUnavailablePolicyError(error)) {\n return null;\n }\n throw error;\n }\n}\n\nfunction analyzePackageBunPolicy(\n packageJson: Record<string, unknown>,\n packageJsonPath: string,\n result: DoctorAnalysisResult,\n policy: BunRuntimePolicySnapshot,\n): DoctorDiagnostic[] {\n const diagnostics: DoctorDiagnostic[] = [];\n const { packageManager } = packageJson;\n if (isNonEmptyString(packageManager) && packageManager.startsWith('bun@')) {\n pushMismatch(diagnostics, {\n actual: packageManager,\n expected: policy.packageManager,\n location: 'package.json#packageManager',\n path: packageJsonPath,\n profile: result.profile,\n ruleId: 'package.json.package-manager.policy',\n });\n }\n\n const devDependencies = isRecord(packageJson.devDependencies)\n ? packageJson.devDependencies\n : null;\n const bunTypes = devDependencies?.['@types/bun'];\n if (isNonEmptyString(bunTypes)) {\n pushMismatch(diagnostics, {\n actual: bunTypes,\n expected: policy.typesRange,\n location: 'package.json#devDependencies.@types/bun',\n path: packageJsonPath,\n profile: result.profile,\n ruleId: 'package.dependencies.types-bun.policy',\n });\n }\n\n return diagnostics;\n}\n\nasync function analyzeWorkflowBunPolicy(\n result: DoctorAnalysisResult,\n policy: BunRuntimePolicySnapshot,\n): Promise<DoctorDiagnostic[]> {\n const diagnostics = await Promise.all(\n WORKFLOWS.map(async (workflow) => await analyzeWorkflow(result, workflow, policy)),\n );\n return diagnostics.flat();\n}\n\nasync function analyzeWorkflow(\n result: DoctorAnalysisResult,\n workflow: (typeof WORKFLOWS)[number],\n policy: BunRuntimePolicySnapshot,\n): Promise<DoctorDiagnostic[]> {\n const workflowPath = path.join(result.targetPath, workflow.relativePath);\n const contents = await readTextOrNull(workflowPath);\n if (contents === null) {\n return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'missing-path')];\n }\n\n const match = BUN_VERSION_PATTERN.exec(contents);\n const actual = match?.[1];\n if (!isNonEmptyString(actual)) {\n return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'field-missing')];\n }\n\n const diagnostics: DoctorDiagnostic[] = [];\n pushMismatch(diagnostics, {\n actual,\n expected: policy.version,\n location: `${workflow.relativePath}#bun-version`,\n path: workflowPath,\n profile: result.profile,\n ruleId: workflow.ruleId,\n });\n return diagnostics;\n}\n\nfunction createWorkflowDiagnostic(\n result: DoctorAnalysisResult,\n workflow: (typeof WORKFLOWS)[number],\n policy: BunRuntimePolicySnapshot,\n workflowPath: string,\n code: DoctorDiagnostic['code'],\n): DoctorDiagnostic {\n const detail =\n code === 'missing-path'\n ? `Managed workflow is missing: ${workflow.relativePath}.`\n : `${workflow.relativePath} does not define bun-version.`;\n return {\n code,\n message: `${detail} Expected Bun ${policy.version}. ${REPAIR_HINT}`,\n path: workflowPath,\n profile: result.profile,\n ruleId: workflow.ruleId,\n severity: 'error',\n };\n}\n\nfunction pushMismatch(\n diagnostics: DoctorDiagnostic[],\n input: {\n readonly actual: string;\n readonly expected: string;\n readonly location: string;\n readonly path: string;\n readonly profile: DoctorAnalysisResult['profile'];\n readonly ruleId: DoctorRuleId;\n },\n): void {\n if (input.actual === input.expected) {\n return;\n }\n diagnostics.push({\n code: 'field-invalid',\n message: `Bun policy drift at ${input.location}: expected \"${input.expected}\", found \"${input.actual}\". ${REPAIR_HINT}`,\n path: input.path,\n profile: input.profile,\n ruleId: input.ruleId,\n severity: 'error',\n });\n}\n\nfunction parseBunRuntimePolicy(importedModule: unknown): BunRuntimePolicySnapshot | null {\n if (!isRecord(importedModule) || !isRecord(importedModule.bunRuntimePolicy)) {\n return null;\n }\n const { packageManager, typesRange, version } = importedModule.bunRuntimePolicy;\n return isNonEmptyString(packageManager) &&\n isNonEmptyString(typesRange) &&\n isNonEmptyString(version)\n ? { packageManager, typesRange, version }\n : null;\n}\n\nfunction withDiagnostics(\n result: DoctorAnalysisResult,\n diagnostics: readonly DoctorDiagnostic[],\n): DoctorAnalysisResult {\n return {\n ...result,\n diagnostics,\n fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },\n };\n}\n\nasync function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {\n try {\n const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as unknown;\n return isRecord(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\nasync function readTextOrNull(filePath: string): Promise<string | null> {\n try {\n return await fs.readFile(filePath, 'utf8');\n } catch (error) {\n if (isNodeError(error) && error.code === 'ENOENT') {\n return null;\n }\n throw error;\n }\n}\n\nfunction isUnavailablePolicyError(error: unknown): boolean {\n if (\n isNodeError(error) &&\n (error.code === 'MODULE_NOT_FOUND' ||\n error.code === 'ERR_MODULE_NOT_FOUND' ||\n error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED')\n ) {\n return true;\n }\n\n const message = error instanceof Error ? error.message : String(error);\n return (\n message.includes(DEVTOOLS_POLICY_SPECIFIER) &&\n (message.includes('Cannot find module') ||\n message.includes('not exported') ||\n message.includes('Package subpath'))\n );\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim() !== '';\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isNodeError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && 'code' in error;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAGtF,OAAO,EAGL,KAAK,qBAAqB,EAE1B,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AAQxB,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACpD;AAED,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,kCAAuC,GAC/C,0BAA0B,CAgC5B;AAED,QAAA,MAAM,QAAQ,4BAAgC,CAAC;AAE/C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAEtF,OAAO,EAGL,KAAK,qBAAqB,EAE1B,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AASxB,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACpD;AAED,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,kCAAuC,GAC/C,0BAA0B,CAgC5B;AAED,QAAA,MAAM,QAAQ,4BAAgC,CAAC;AAE/C,eAAe,QAAQ,CAAC"}
package/dist/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';
2
1
  import { createProviderCommandDescriptors, DOCTOR_COMMANDS, runDoctorCommand, } from '../commands.js';
2
+ import { analyzeDoctorTargetWithCliLayout } from '../dependencyPolicyAnalysis.js';
3
3
  import { DOCTOR_CAPABILITIES, DOCTOR_COMMAND_CATEGORY, DOCTOR_PACKAGE_NAME, DOCTOR_PACKAGE_VERSION, } from '../packageMetadata.js';
4
4
  export function createDoctorRuntimeProvider(options = {}) {
5
5
  const runCommandImpl = options.runCommandImpl ?? runDoctorCommand;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gCAAgC,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EACL,gCAAgC,EAChC,eAAe,EAEf,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAO/B,MAAM,UAAU,2BAA2B,CACzC,UAA8C,EAAE;IAEhD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAClE,MAAM,QAAQ,GAAG;QACf,aAAa,EAAE,gCAAgC;QAC/C,GAAG,OAAO,CAAC,QAAQ;KACY,CAAC;IAElC,OAAO;QACL,EAAE,EAAE,mBAAmB;QACvB,QAAQ,EAAE,uBAAuB;QACjC,OAAO,EAAE,sBAAsB;QAC/B,YAAY,EAAE,CAAC,GAAG,mBAAmB,CAAC;QACtC,QAAQ,EAAE,gCAAgC,EAAE;QAC5C,QAAQ,EAAE,eAAe,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;YACxC,MAAM,OAAO,GAAuB,CAAC,OAAO,EAAE,EAAE,CAC9C,cAAc,CACZ;gBACE,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,OAAO;gBACP,OAAO,EAAE,OAAO,CAAC,OAAO;aACzB,EACD;gBACE,QAAQ;aACT,CACF,CAAC;YAEJ,OAAO;gBACL,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,OAAO;aACR,CAAC;QACJ,CAAC,CAAC;KACkC,CAAC;AACzC,CAAC;AAED,MAAM,QAAQ,GAAG,2BAA2B,EAAE,CAAC;AAE/C,eAAe,QAAQ,CAAC","sourcesContent":["import type { AnkhCommandHandler, AnkhRuntimeCommandProvider } from '@ankhorage/ankh';\n\nimport { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';\nimport {\n createProviderCommandDescriptors,\n DOCTOR_COMMANDS,\n type DoctorCommandServices,\n runDoctorCommand,\n type RunDoctorCommandImpl,\n} from '../commands.js';\nimport {\n DOCTOR_CAPABILITIES,\n DOCTOR_COMMAND_CATEGORY,\n DOCTOR_PACKAGE_NAME,\n DOCTOR_PACKAGE_VERSION,\n} from '../packageMetadata.js';\n\nexport interface CreateDoctorRuntimeProviderOptions {\n readonly runCommandImpl?: RunDoctorCommandImpl;\n readonly services?: Partial<DoctorCommandServices>;\n}\n\nexport function createDoctorRuntimeProvider(\n options: CreateDoctorRuntimeProviderOptions = {},\n): AnkhRuntimeCommandProvider {\n const runCommandImpl = options.runCommandImpl ?? runDoctorCommand;\n const services = {\n analyzeTarget: analyzeDoctorTargetWithCliLayout,\n ...options.services,\n } satisfies DoctorCommandServices;\n\n return {\n id: DOCTOR_PACKAGE_NAME,\n category: DOCTOR_COMMAND_CATEGORY,\n version: DOCTOR_PACKAGE_VERSION,\n capabilities: [...DOCTOR_CAPABILITIES],\n commands: createProviderCommandDescriptors(),\n handlers: DOCTOR_COMMANDS.map((command) => {\n const handler: AnkhCommandHandler = (request) =>\n runCommandImpl(\n {\n argv: request.argv,\n command,\n context: request.context,\n },\n {\n services,\n },\n );\n\n return {\n path: command.path,\n handler,\n };\n }),\n } satisfies AnkhRuntimeCommandProvider;\n}\n\nconst provider = createDoctorRuntimeProvider();\n\nexport default provider;\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,gCAAgC,EAChC,eAAe,EAEf,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gCAAgC,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAO/B,MAAM,UAAU,2BAA2B,CACzC,UAA8C,EAAE;IAEhD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAClE,MAAM,QAAQ,GAAG;QACf,aAAa,EAAE,gCAAgC;QAC/C,GAAG,OAAO,CAAC,QAAQ;KACY,CAAC;IAElC,OAAO;QACL,EAAE,EAAE,mBAAmB;QACvB,QAAQ,EAAE,uBAAuB;QACjC,OAAO,EAAE,sBAAsB;QAC/B,YAAY,EAAE,CAAC,GAAG,mBAAmB,CAAC;QACtC,QAAQ,EAAE,gCAAgC,EAAE;QAC5C,QAAQ,EAAE,eAAe,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;YACxC,MAAM,OAAO,GAAuB,CAAC,OAAO,EAAE,EAAE,CAC9C,cAAc,CACZ;gBACE,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,OAAO;gBACP,OAAO,EAAE,OAAO,CAAC,OAAO;aACzB,EACD;gBACE,QAAQ;aACT,CACF,CAAC;YAEJ,OAAO;gBACL,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,OAAO;aACR,CAAC;QACJ,CAAC,CAAC;KACkC,CAAC;AACzC,CAAC;AAED,MAAM,QAAQ,GAAG,2BAA2B,EAAE,CAAC;AAE/C,eAAe,QAAQ,CAAC","sourcesContent":["import type { AnkhCommandHandler, AnkhRuntimeCommandProvider } from '@ankhorage/ankh';\n\nimport {\n createProviderCommandDescriptors,\n DOCTOR_COMMANDS,\n type DoctorCommandServices,\n runDoctorCommand,\n type RunDoctorCommandImpl,\n} from '../commands.js';\nimport { analyzeDoctorTargetWithCliLayout } from '../dependencyPolicyAnalysis.js';\nimport {\n DOCTOR_CAPABILITIES,\n DOCTOR_COMMAND_CATEGORY,\n DOCTOR_PACKAGE_NAME,\n DOCTOR_PACKAGE_VERSION,\n} from '../packageMetadata.js';\n\nexport interface CreateDoctorRuntimeProviderOptions {\n readonly runCommandImpl?: RunDoctorCommandImpl;\n readonly services?: Partial<DoctorCommandServices>;\n}\n\nexport function createDoctorRuntimeProvider(\n options: CreateDoctorRuntimeProviderOptions = {},\n): AnkhRuntimeCommandProvider {\n const runCommandImpl = options.runCommandImpl ?? runDoctorCommand;\n const services = {\n analyzeTarget: analyzeDoctorTargetWithCliLayout,\n ...options.services,\n } satisfies DoctorCommandServices;\n\n return {\n id: DOCTOR_PACKAGE_NAME,\n category: DOCTOR_COMMAND_CATEGORY,\n version: DOCTOR_PACKAGE_VERSION,\n capabilities: [...DOCTOR_CAPABILITIES],\n commands: createProviderCommandDescriptors(),\n handlers: DOCTOR_COMMANDS.map((command) => {\n const handler: AnkhCommandHandler = (request) =>\n runCommandImpl(\n {\n argv: request.argv,\n command,\n context: request.context,\n },\n {\n services,\n },\n );\n\n return {\n path: command.path,\n handler,\n };\n }),\n } satisfies AnkhRuntimeCommandProvider;\n}\n\nconst provider = createDoctorRuntimeProvider();\n\nexport default provider;\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"standalone.d.ts","sourceRoot":"","sources":["../../src/cli/standalone.ts"],"names":[],"mappings":";AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEzF,OAAO,EACL,KAAK,qBAAqB,EAK1B,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IACxC,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACpD;AAED,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,sBAAsB,CAAC,CAkCjC"}
1
+ {"version":3,"file":"standalone.d.ts","sourceRoot":"","sources":["../../src/cli/standalone.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEzF,OAAO,EACL,KAAK,qBAAqB,EAK1B,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AAGxB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IACxC,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACpD;AAED,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,sBAAsB,CAAC,CAkCjC"}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- import { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';
3
2
  import { createDefaultCommandContext } from '../commandContext.js';
4
3
  import { findDoctorCommandByStandaloneName, renderRootHelp, renderUnknownCommand, runDoctorCommand, } from '../commands.js';
4
+ import { analyzeDoctorTargetWithCliLayout } from '../dependencyPolicyAnalysis.js';
5
5
  export async function runCli(argv, options = {}) {
6
6
  const context = options.context ?? createDefaultCommandContext();
7
7
  const runCommand = options.runCommandImpl ?? runDoctorCommand;
@@ -1 +1 @@
1
- {"version":3,"file":"standalone.js","sourceRoot":"","sources":["../../src/cli/standalone.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,gCAAgC,EAAE,MAAM,yBAAyB,CAAC;AAE3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAEL,iCAAiC,EACjC,cAAc,EACd,oBAAoB,EACpB,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC;AAQxB,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,IAAuB,EACvB,UAA4B,EAAE;IAE9B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,2BAA2B,EAAE,CAAC;IACjE,MAAM,UAAU,GAAG,OAAO,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAC9D,MAAM,CAAC,UAAU,EAAE,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC;IAEzC,IAAI,UAAU,KAAK,SAAS,IAAI,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC;QACxD,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;QACrD,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,IAAI,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;QAC5C,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,MAAM,OAAO,GAAG,iCAAiC,CAAC,UAAU,CAAC,CAAC;IAC9D,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,CAAC,WAAW,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC,CAAC;QACtD,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,OAAO,UAAU,CACf;QACE,IAAI,EAAE,UAAU;QAChB,OAAO;QACP,OAAO;KACR,EACD;QACE,QAAQ,EAAE;YACR,aAAa,EAAE,gCAAgC;YAC/C,GAAG,OAAO,CAAC,QAAQ;SACpB;KACF,CACF,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC;AAClE,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC;AACxE,CAAC;AAED,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IACrB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC","sourcesContent":["#!/usr/bin/env bun\n\nimport { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';\nimport type { DoctorCommandContext, DoctorCommandRunResult } from '../commandContext.js';\nimport { createDefaultCommandContext } from '../commandContext.js';\nimport {\n type DoctorCommandServices,\n findDoctorCommandByStandaloneName,\n renderRootHelp,\n renderUnknownCommand,\n runDoctorCommand,\n type RunDoctorCommandImpl,\n} from '../commands.js';\n\nexport interface DoctorCliOptions {\n readonly context?: DoctorCommandContext;\n readonly runCommandImpl?: RunDoctorCommandImpl;\n readonly services?: Partial<DoctorCommandServices>;\n}\n\nexport async function runCli(\n argv: readonly string[],\n options: DoctorCliOptions = {},\n): Promise<DoctorCommandRunResult> {\n const context = options.context ?? createDefaultCommandContext();\n const runCommand = options.runCommandImpl ?? runDoctorCommand;\n const [firstToken, ...restTokens] = argv;\n\n if (firstToken === undefined || isHelpToken(firstToken)) {\n context.writeStdout(renderRootHelp(context.version));\n return { exitCode: 0 };\n }\n\n if (isVersionToken(firstToken)) {\n context.writeStdout(`${context.version}\\n`);\n return { exitCode: 0 };\n }\n\n const command = findDoctorCommandByStandaloneName(firstToken);\n if (command === null) {\n context.writeStderr(renderUnknownCommand(firstToken));\n return { exitCode: 1 };\n }\n\n return runCommand(\n {\n argv: restTokens,\n command,\n context,\n },\n {\n services: {\n analyzeTarget: analyzeDoctorTargetWithCliLayout,\n ...options.services,\n },\n },\n );\n}\n\nfunction isHelpToken(value: string): boolean {\n return value === '--help' || value === '-h' || value === 'help';\n}\n\nfunction isVersionToken(value: string): boolean {\n return value === '--version' || value === '-v' || value === 'version';\n}\n\nif (import.meta.main) {\n const result = await runCli(process.argv.slice(2));\n process.exit(result.exitCode);\n}\n"]}
1
+ {"version":3,"file":"standalone.js","sourceRoot":"","sources":["../../src/cli/standalone.ts"],"names":[],"mappings":";AAGA,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAEL,iCAAiC,EACjC,cAAc,EACd,oBAAoB,EACpB,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gCAAgC,EAAE,MAAM,gCAAgC,CAAC;AAQlF,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,IAAuB,EACvB,UAA4B,EAAE;IAE9B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,2BAA2B,EAAE,CAAC;IACjE,MAAM,UAAU,GAAG,OAAO,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAC9D,MAAM,CAAC,UAAU,EAAE,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC;IAEzC,IAAI,UAAU,KAAK,SAAS,IAAI,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC;QACxD,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;QACrD,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,IAAI,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;QAC5C,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,MAAM,OAAO,GAAG,iCAAiC,CAAC,UAAU,CAAC,CAAC;IAC9D,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,CAAC,WAAW,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC,CAAC;QACtD,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,OAAO,UAAU,CACf;QACE,IAAI,EAAE,UAAU;QAChB,OAAO;QACP,OAAO;KACR,EACD;QACE,QAAQ,EAAE;YACR,aAAa,EAAE,gCAAgC;YAC/C,GAAG,OAAO,CAAC,QAAQ;SACpB;KACF,CACF,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC;AAClE,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC;AACxE,CAAC;AAED,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IACrB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC","sourcesContent":["#!/usr/bin/env bun\n\nimport type { DoctorCommandContext, DoctorCommandRunResult } from '../commandContext.js';\nimport { createDefaultCommandContext } from '../commandContext.js';\nimport {\n type DoctorCommandServices,\n findDoctorCommandByStandaloneName,\n renderRootHelp,\n renderUnknownCommand,\n runDoctorCommand,\n type RunDoctorCommandImpl,\n} from '../commands.js';\nimport { analyzeDoctorTargetWithCliLayout } from '../dependencyPolicyAnalysis.js';\n\nexport interface DoctorCliOptions {\n readonly context?: DoctorCommandContext;\n readonly runCommandImpl?: RunDoctorCommandImpl;\n readonly services?: Partial<DoctorCommandServices>;\n}\n\nexport async function runCli(\n argv: readonly string[],\n options: DoctorCliOptions = {},\n): Promise<DoctorCommandRunResult> {\n const context = options.context ?? createDefaultCommandContext();\n const runCommand = options.runCommandImpl ?? runDoctorCommand;\n const [firstToken, ...restTokens] = argv;\n\n if (firstToken === undefined || isHelpToken(firstToken)) {\n context.writeStdout(renderRootHelp(context.version));\n return { exitCode: 0 };\n }\n\n if (isVersionToken(firstToken)) {\n context.writeStdout(`${context.version}\\n`);\n return { exitCode: 0 };\n }\n\n const command = findDoctorCommandByStandaloneName(firstToken);\n if (command === null) {\n context.writeStderr(renderUnknownCommand(firstToken));\n return { exitCode: 1 };\n }\n\n return runCommand(\n {\n argv: restTokens,\n command,\n context,\n },\n {\n services: {\n analyzeTarget: analyzeDoctorTargetWithCliLayout,\n ...options.services,\n },\n },\n );\n}\n\nfunction isHelpToken(value: string): boolean {\n return value === '--help' || value === '-h' || value === 'help';\n}\n\nfunction isVersionToken(value: string): boolean {\n return value === '--version' || value === '-v' || value === 'version';\n}\n\nif (import.meta.main) {\n const result = await runCli(process.argv.slice(2));\n process.exit(result.exitCode);\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import { type DoctorAnalysisRequest, type DoctorAnalysisResult } from './analysis.js';
2
+ export declare function analyzeDoctorTarget(request: DoctorAnalysisRequest): Promise<DoctorAnalysisResult>;
3
+ export declare function analyzeDoctorTargetWithCliLayout(request: DoctorAnalysisRequest): Promise<DoctorAnalysisResult>;
4
+ //# sourceMappingURL=dependencyPolicyAnalysis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependencyPolicyAnalysis.d.ts","sourceRoot":"","sources":["../src/dependencyPolicyAnalysis.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAC1B,MAAM,eAAe,CAAC;AASvB,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED,wBAAsB,gCAAgC,CACpD,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAE/B"}
@@ -0,0 +1,80 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { analyzeDoctorTarget as analyzeBaseDoctorTarget, } from './analysis.js';
4
+ import { applyBunRuntimePolicy } from './bunPolicyAnalysis.js';
5
+ import { analyzeDoctorTargetWithCliLayout as analyzeBaseDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
6
+ const ANKH_PACKAGE_NAME = '@ankhorage/ankh';
7
+ const DEVTOOLS_PACKAGE_NAME = '@ankhorage/devtools';
8
+ const DEVTOOLS_REQUIRED_RULE_ID = 'package.dependencies.devtools.required';
9
+ export async function analyzeDoctorTarget(request) {
10
+ return applyRepositoryPolicies(await analyzeBaseDoctorTarget(request));
11
+ }
12
+ export async function analyzeDoctorTargetWithCliLayout(request) {
13
+ return applyRepositoryPolicies(await analyzeBaseDoctorTargetWithCliLayout(request));
14
+ }
15
+ async function applyRepositoryPolicies(result) {
16
+ return applyBunRuntimePolicy(await applyDevtoolsDependencyPlacementPolicy(result));
17
+ }
18
+ async function applyDevtoolsDependencyPlacementPolicy(result) {
19
+ if (!result.hasPackageJson) {
20
+ return result;
21
+ }
22
+ const packageJsonPath = path.join(result.targetPath, 'package.json');
23
+ const packageJson = await readPackageJson(packageJsonPath);
24
+ if (packageJson?.name !== ANKH_PACKAGE_NAME || !expectsDevtools(packageJson)) {
25
+ return result;
26
+ }
27
+ const dependencies = isRecord(packageJson.dependencies) ? packageJson.dependencies : null;
28
+ const hasRuntimeDevtools = hasDependency(dependencies, DEVTOOLS_PACKAGE_NAME);
29
+ const diagnosticsWithoutPlacementRule = result.diagnostics.filter((diagnostic) => diagnostic.ruleId !== DEVTOOLS_REQUIRED_RULE_ID);
30
+ const diagnostics = hasRuntimeDevtools
31
+ ? diagnosticsWithoutPlacementRule
32
+ : [
33
+ ...diagnosticsWithoutPlacementRule,
34
+ createMissingRuntimeDevtoolsDiagnostic(packageJsonPath, result),
35
+ ];
36
+ return {
37
+ ...result,
38
+ diagnostics,
39
+ fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },
40
+ };
41
+ }
42
+ async function readPackageJson(packageJsonPath) {
43
+ try {
44
+ const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
45
+ return isRecord(parsed) ? parsed : null;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ function createMissingRuntimeDevtoolsDiagnostic(packageJsonPath, result) {
52
+ return {
53
+ code: 'missing-dependency',
54
+ message: '@ankhorage/ankh bundles @ankhorage/devtools as a runtime core provider and must declare it in dependencies.',
55
+ path: packageJsonPath,
56
+ profile: result.profile,
57
+ ruleId: DEVTOOLS_REQUIRED_RULE_ID,
58
+ severity: 'error',
59
+ };
60
+ }
61
+ function expectsDevtools(packageJson) {
62
+ const scripts = isRecord(packageJson.scripts) ? packageJson.scripts : null;
63
+ if (scripts === null) {
64
+ return false;
65
+ }
66
+ return Object.values(scripts).some((scriptValue) => isNonEmptyString(scriptValue) &&
67
+ (scriptValue.includes('ankhorage-eslint') ||
68
+ scriptValue.includes('ankhorage-prettier') ||
69
+ scriptValue.includes('ankhorage-knip')));
70
+ }
71
+ function hasDependency(dependencyMap, dependencyName) {
72
+ return dependencyMap !== null && isNonEmptyString(dependencyMap[dependencyName]);
73
+ }
74
+ function isNonEmptyString(value) {
75
+ return typeof value === 'string' && value.trim() !== '';
76
+ }
77
+ function isRecord(value) {
78
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
79
+ }
80
+ //# sourceMappingURL=dependencyPolicyAnalysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependencyPolicyAnalysis.js","sourceRoot":"","sources":["../src/dependencyPolicyAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nimport {\n analyzeDoctorTarget as analyzeBaseDoctorTarget,\n type DoctorAnalysisRequest,\n type DoctorAnalysisResult,\n} from './analysis.js';\nimport { applyBunRuntimePolicy } from './bunPolicyAnalysis.js';\nimport { analyzeDoctorTargetWithCliLayout as analyzeBaseDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';\nimport type { DoctorDiagnostic } from './diagnostics.js';\n\nconst ANKH_PACKAGE_NAME = '@ankhorage/ankh';\nconst DEVTOOLS_PACKAGE_NAME = '@ankhorage/devtools';\nconst DEVTOOLS_REQUIRED_RULE_ID = 'package.dependencies.devtools.required';\n\nexport async function analyzeDoctorTarget(\n request: DoctorAnalysisRequest,\n): Promise<DoctorAnalysisResult> {\n return applyRepositoryPolicies(await analyzeBaseDoctorTarget(request));\n}\n\nexport async function analyzeDoctorTargetWithCliLayout(\n request: DoctorAnalysisRequest,\n): Promise<DoctorAnalysisResult> {\n return applyRepositoryPolicies(await analyzeBaseDoctorTargetWithCliLayout(request));\n}\n\nasync function applyRepositoryPolicies(\n result: DoctorAnalysisResult,\n): Promise<DoctorAnalysisResult> {\n return applyBunRuntimePolicy(await applyDevtoolsDependencyPlacementPolicy(result));\n}\n\nasync function applyDevtoolsDependencyPlacementPolicy(\n result: DoctorAnalysisResult,\n): Promise<DoctorAnalysisResult> {\n if (!result.hasPackageJson) {\n return result;\n }\n\n const packageJsonPath = path.join(result.targetPath, 'package.json');\n const packageJson = await readPackageJson(packageJsonPath);\n if (packageJson?.name !== ANKH_PACKAGE_NAME || !expectsDevtools(packageJson)) {\n return result;\n }\n\n const dependencies = isRecord(packageJson.dependencies) ? packageJson.dependencies : null;\n const hasRuntimeDevtools = hasDependency(dependencies, DEVTOOLS_PACKAGE_NAME);\n const diagnosticsWithoutPlacementRule = result.diagnostics.filter(\n (diagnostic) => diagnostic.ruleId !== DEVTOOLS_REQUIRED_RULE_ID,\n );\n const diagnostics = hasRuntimeDevtools\n ? diagnosticsWithoutPlacementRule\n : [\n ...diagnosticsWithoutPlacementRule,\n createMissingRuntimeDevtoolsDiagnostic(packageJsonPath, result),\n ];\n\n return {\n ...result,\n diagnostics,\n fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },\n };\n}\n\nasync function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {\n try {\n const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as unknown;\n return isRecord(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\nfunction createMissingRuntimeDevtoolsDiagnostic(\n packageJsonPath: string,\n result: DoctorAnalysisResult,\n): DoctorDiagnostic {\n return {\n code: 'missing-dependency',\n message:\n '@ankhorage/ankh bundles @ankhorage/devtools as a runtime core provider and must declare it in dependencies.',\n path: packageJsonPath,\n profile: result.profile,\n ruleId: DEVTOOLS_REQUIRED_RULE_ID,\n severity: 'error',\n };\n}\n\nfunction expectsDevtools(packageJson: Record<string, unknown>): boolean {\n const scripts = isRecord(packageJson.scripts) ? packageJson.scripts : null;\n if (scripts === null) {\n return false;\n }\n\n return Object.values(scripts).some(\n (scriptValue) =>\n isNonEmptyString(scriptValue) &&\n (scriptValue.includes('ankhorage-eslint') ||\n scriptValue.includes('ankhorage-prettier') ||\n scriptValue.includes('ankhorage-knip')),\n );\n}\n\nfunction hasDependency(\n dependencyMap: Record<string, unknown> | null,\n dependencyName: string,\n): boolean {\n return dependencyMap !== null && isNonEmptyString(dependencyMap[dependencyName]);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim() !== '';\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  export type DoctorDiagnosticCode = 'field-invalid' | 'field-missing' | 'invalid-ankh-metadata' | 'invalid-app-manifest-json' | 'invalid-package-json' | 'missing-dependency' | 'missing-path' | 'missing-script' | 'provider-source-import-failed' | 'provider-source-missing' | 'target-not-directory' | 'target-not-found' | 'unsupported-target';
2
2
  export type DoctorDiagnosticSeverity = 'error' | 'warning';
3
3
  export type DoctorPolicyProfile = 'app-manifest' | 'integration-monorepo' | 'public-package' | 'unknown';
4
- export type DoctorRuleId = 'integration.package.ankh.disallowed' | 'integration.package.private.required' | 'integration.package.workspaces.required' | 'manifest.auth.authorization.engine.valid' | 'manifest.auth.authorization.kind.valid' | 'manifest.auth.authorization.valid-shape' | 'manifest.auth.flow.post-sign-in.distinct' | 'manifest.auth.flow.route.required' | 'manifest.auth.flow.route.unique' | 'manifest.auth.flow.route.valid' | 'manifest.auth.flow.unauthorized-route.valid' | 'manifest.auth.flow.valid-shape' | 'manifest.auth.oauth.callback-route.valid' | 'manifest.auth.oauth.provider.credentials-ref.required' | 'manifest.auth.oauth.provider.credentials-ref.valid' | 'manifest.auth.oauth.provider.duplicate' | 'manifest.auth.oauth.provider.inline-secret.disallowed' | 'manifest.auth.oauth.providers.valid' | 'manifest.auth.oauth.valid-shape' | 'manifest.auth.valid-shape' | 'manifest.infra.valid-shape' | 'manifest.json.readable' | 'manifest.json.valid' | 'manifest.root.valid-shape' | 'manifest.secret-store.provider.required' | 'manifest.secret-store.provider.valid' | 'manifest.secret-store.valid-shape' | 'manifest.settings.auth-flow.removed' | 'package.ankh.capabilities.match-provider' | 'package.ankh.present.valid-shape' | 'package.ankh.provider-path.required' | 'package.ankh.required-for-provider' | 'package.cli.export.required' | 'package.cli.index.required' | 'package.cli.root-file.disallowed' | 'package.dependencies.ankh-workspace-alias.disallowed' | 'package.dependencies.ankhorage4-source.disallowed' | 'package.dependencies.changesets.required' | 'package.dependencies.devtools.required' | 'package.dependencies.paradox.required' | 'package.dependencies.types-bun.required' | 'package.dependencies.types-node.required' | 'package.dependencies.typescript.required' | 'package.imports.ankh-workspace-alias.disallowed' | 'package.imports.ankhorage4-source.disallowed' | 'package.json.bugs.required' | 'package.json.description.required' | 'package.json.exports.required' | 'package.json.files.required' | 'package.json.homepage.required' | 'package.json.keywords.required' | 'package.json.license.required' | 'package.json.name.required' | 'package.json.package-manager.bun' | 'package.json.package-manager.required' | 'package.json.private.public-package-disallowed' | 'package.json.publish-config.public' | 'package.json.publish-config.required' | 'package.json.repository.required' | 'package.json.type.module' | 'package.json.type.required' | 'package.json.version.required' | 'package.scripts.build.required' | 'package.scripts.changeset-status.required' | 'package.scripts.changeset.required' | 'package.scripts.docs.required' | 'package.scripts.format-check.required' | 'package.scripts.format.required' | 'package.scripts.knip.required' | 'package.scripts.lint-fix.required' | 'package.scripts.lint.required' | 'package.scripts.test.required' | 'package.scripts.typecheck.required' | 'package.scripts.version-packages.required' | 'provider.commands.match-capabilities' | 'provider.commands.match-handlers' | 'provider.source.importable' | 'provider.source.required' | 'repo.changelog.required' | 'repo.changeset.required' | 'repo.license.required' | 'repo.readme.required' | 'repo.workflows.required' | 'studio.dependencies.dnd.required' | 'studio.dependencies.runtime.required' | 'studio.imports.dnd.required' | 'studio.imports.runtime.required' | 'target.package-json.required' | 'target.path.directory' | 'target.path.exists' | 'target.profile.supported' | 'target.repo-markers.required';
4
+ export type DoctorRuleId = 'integration.package.ankh.disallowed' | 'integration.package.private.required' | 'integration.package.workspaces.required' | 'manifest.auth.authorization.engine.valid' | 'manifest.auth.authorization.kind.valid' | 'manifest.auth.authorization.valid-shape' | 'manifest.auth.flow.post-sign-in.distinct' | 'manifest.auth.flow.route.required' | 'manifest.auth.flow.route.unique' | 'manifest.auth.flow.route.valid' | 'manifest.auth.flow.unauthorized-route.valid' | 'manifest.auth.flow.valid-shape' | 'manifest.auth.oauth.callback-route.valid' | 'manifest.auth.oauth.provider.credentials-ref.required' | 'manifest.auth.oauth.provider.credentials-ref.valid' | 'manifest.auth.oauth.provider.duplicate' | 'manifest.auth.oauth.provider.inline-secret.disallowed' | 'manifest.auth.oauth.providers.valid' | 'manifest.auth.oauth.valid-shape' | 'manifest.auth.valid-shape' | 'manifest.infra.valid-shape' | 'manifest.json.readable' | 'manifest.json.valid' | 'manifest.root.valid-shape' | 'manifest.secret-store.provider.required' | 'manifest.secret-store.provider.valid' | 'manifest.secret-store.valid-shape' | 'manifest.settings.auth-flow.removed' | 'package.ankh.capabilities.match-provider' | 'package.ankh.present.valid-shape' | 'package.ankh.provider-path.required' | 'package.ankh.required-for-provider' | 'package.cli.export.required' | 'package.cli.index.required' | 'package.cli.root-file.disallowed' | 'package.dependencies.ankh-workspace-alias.disallowed' | 'package.dependencies.ankhorage4-source.disallowed' | 'package.dependencies.changesets.required' | 'package.dependencies.devtools.required' | 'package.dependencies.paradox.required' | 'package.dependencies.types-bun.policy' | 'package.dependencies.types-bun.required' | 'package.dependencies.types-node.required' | 'package.dependencies.typescript.required' | 'package.imports.ankh-workspace-alias.disallowed' | 'package.imports.ankhorage4-source.disallowed' | 'package.json.bugs.required' | 'package.json.description.required' | 'package.json.exports.required' | 'package.json.files.required' | 'package.json.homepage.required' | 'package.json.keywords.required' | 'package.json.license.required' | 'package.json.name.required' | 'package.json.package-manager.bun' | 'package.json.package-manager.policy' | 'package.json.package-manager.required' | 'package.json.private.public-package-disallowed' | 'package.json.publish-config.public' | 'package.json.publish-config.required' | 'package.json.repository.required' | 'package.json.type.module' | 'package.json.type.required' | 'package.json.version.required' | 'package.scripts.build.required' | 'package.scripts.changeset-status.required' | 'package.scripts.changeset.required' | 'package.scripts.docs.required' | 'package.scripts.format-check.required' | 'package.scripts.format.required' | 'package.scripts.knip.required' | 'package.scripts.lint-fix.required' | 'package.scripts.lint.required' | 'package.scripts.test.required' | 'package.scripts.typecheck.required' | 'package.scripts.version-packages.required' | 'provider.commands.match-capabilities' | 'provider.commands.match-handlers' | 'provider.source.importable' | 'provider.source.required' | 'repo.changelog.required' | 'repo.changeset.required' | 'repo.license.required' | 'repo.readme.required' | 'repo.workflows.ci.bun-policy' | 'repo.workflows.release.bun-policy' | 'repo.workflows.required' | 'studio.dependencies.dnd.required' | 'studio.dependencies.runtime.required' | 'studio.imports.dnd.required' | 'studio.imports.runtime.required' | 'target.package-json.required' | 'target.path.directory' | 'target.path.exists' | 'target.profile.supported' | 'target.repo-markers.required';
5
5
  export interface DoctorDiagnostic {
6
6
  readonly code: DoctorDiagnosticCode;
7
7
  readonly message: string;
@@ -1 +1 @@
1
- {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC5B,eAAe,GACf,eAAe,GACf,uBAAuB,GACvB,2BAA2B,GAC3B,sBAAsB,GACtB,oBAAoB,GACpB,cAAc,GACd,gBAAgB,GAChB,+BAA+B,GAC/B,yBAAyB,GACzB,sBAAsB,GACtB,kBAAkB,GAClB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,SAAS,CAAC;AAC3D,MAAM,MAAM,mBAAmB,GAC7B,cAAc,GAAG,sBAAsB,GAAG,gBAAgB,GAAG,SAAS,CAAC;AACzE,MAAM,MAAM,YAAY,GACpB,qCAAqC,GACrC,sCAAsC,GACtC,yCAAyC,GACzC,0CAA0C,GAC1C,wCAAwC,GACxC,yCAAyC,GACzC,0CAA0C,GAC1C,mCAAmC,GACnC,iCAAiC,GACjC,gCAAgC,GAChC,6CAA6C,GAC7C,gCAAgC,GAChC,0CAA0C,GAC1C,uDAAuD,GACvD,oDAAoD,GACpD,wCAAwC,GACxC,uDAAuD,GACvD,qCAAqC,GACrC,iCAAiC,GACjC,2BAA2B,GAC3B,4BAA4B,GAC5B,wBAAwB,GACxB,qBAAqB,GACrB,2BAA2B,GAC3B,yCAAyC,GACzC,sCAAsC,GACtC,mCAAmC,GACnC,qCAAqC,GACrC,0CAA0C,GAC1C,kCAAkC,GAClC,qCAAqC,GACrC,oCAAoC,GACpC,6BAA6B,GAC7B,4BAA4B,GAC5B,kCAAkC,GAClC,sDAAsD,GACtD,mDAAmD,GACnD,0CAA0C,GAC1C,wCAAwC,GACxC,uCAAuC,GACvC,yCAAyC,GACzC,0CAA0C,GAC1C,0CAA0C,GAC1C,iDAAiD,GACjD,8CAA8C,GAC9C,4BAA4B,GAC5B,mCAAmC,GACnC,+BAA+B,GAC/B,6BAA6B,GAC7B,gCAAgC,GAChC,gCAAgC,GAChC,+BAA+B,GAC/B,4BAA4B,GAC5B,kCAAkC,GAClC,uCAAuC,GACvC,gDAAgD,GAChD,oCAAoC,GACpC,sCAAsC,GACtC,kCAAkC,GAClC,0BAA0B,GAC1B,4BAA4B,GAC5B,+BAA+B,GAC/B,gCAAgC,GAChC,2CAA2C,GAC3C,oCAAoC,GACpC,+BAA+B,GAC/B,uCAAuC,GACvC,iCAAiC,GACjC,+BAA+B,GAC/B,mCAAmC,GACnC,+BAA+B,GAC/B,+BAA+B,GAC/B,oCAAoC,GACpC,2CAA2C,GAC3C,sCAAsC,GACtC,kCAAkC,GAClC,4BAA4B,GAC5B,0BAA0B,GAC1B,yBAAyB,GACzB,yBAAyB,GACzB,uBAAuB,GACvB,sBAAsB,GACtB,yBAAyB,GACzB,kCAAkC,GAClC,sCAAsC,GACtC,6BAA6B,GAC7B,iCAAiC,GACjC,8BAA8B,GAC9B,uBAAuB,GACvB,oBAAoB,GACpB,0BAA0B,GAC1B,8BAA8B,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;CAC7C;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAEtF;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAExF"}
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC5B,eAAe,GACf,eAAe,GACf,uBAAuB,GACvB,2BAA2B,GAC3B,sBAAsB,GACtB,oBAAoB,GACpB,cAAc,GACd,gBAAgB,GAChB,+BAA+B,GAC/B,yBAAyB,GACzB,sBAAsB,GACtB,kBAAkB,GAClB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,SAAS,CAAC;AAC3D,MAAM,MAAM,mBAAmB,GAC7B,cAAc,GAAG,sBAAsB,GAAG,gBAAgB,GAAG,SAAS,CAAC;AACzE,MAAM,MAAM,YAAY,GACpB,qCAAqC,GACrC,sCAAsC,GACtC,yCAAyC,GACzC,0CAA0C,GAC1C,wCAAwC,GACxC,yCAAyC,GACzC,0CAA0C,GAC1C,mCAAmC,GACnC,iCAAiC,GACjC,gCAAgC,GAChC,6CAA6C,GAC7C,gCAAgC,GAChC,0CAA0C,GAC1C,uDAAuD,GACvD,oDAAoD,GACpD,wCAAwC,GACxC,uDAAuD,GACvD,qCAAqC,GACrC,iCAAiC,GACjC,2BAA2B,GAC3B,4BAA4B,GAC5B,wBAAwB,GACxB,qBAAqB,GACrB,2BAA2B,GAC3B,yCAAyC,GACzC,sCAAsC,GACtC,mCAAmC,GACnC,qCAAqC,GACrC,0CAA0C,GAC1C,kCAAkC,GAClC,qCAAqC,GACrC,oCAAoC,GACpC,6BAA6B,GAC7B,4BAA4B,GAC5B,kCAAkC,GAClC,sDAAsD,GACtD,mDAAmD,GACnD,0CAA0C,GAC1C,wCAAwC,GACxC,uCAAuC,GACvC,uCAAuC,GACvC,yCAAyC,GACzC,0CAA0C,GAC1C,0CAA0C,GAC1C,iDAAiD,GACjD,8CAA8C,GAC9C,4BAA4B,GAC5B,mCAAmC,GACnC,+BAA+B,GAC/B,6BAA6B,GAC7B,gCAAgC,GAChC,gCAAgC,GAChC,+BAA+B,GAC/B,4BAA4B,GAC5B,kCAAkC,GAClC,qCAAqC,GACrC,uCAAuC,GACvC,gDAAgD,GAChD,oCAAoC,GACpC,sCAAsC,GACtC,kCAAkC,GAClC,0BAA0B,GAC1B,4BAA4B,GAC5B,+BAA+B,GAC/B,gCAAgC,GAChC,2CAA2C,GAC3C,oCAAoC,GACpC,+BAA+B,GAC/B,uCAAuC,GACvC,iCAAiC,GACjC,+BAA+B,GAC/B,mCAAmC,GACnC,+BAA+B,GAC/B,+BAA+B,GAC/B,oCAAoC,GACpC,2CAA2C,GAC3C,sCAAsC,GACtC,kCAAkC,GAClC,4BAA4B,GAC5B,0BAA0B,GAC1B,yBAAyB,GACzB,yBAAyB,GACzB,uBAAuB,GACvB,sBAAsB,GACtB,8BAA8B,GAC9B,mCAAmC,GACnC,yBAAyB,GACzB,kCAAkC,GAClC,sCAAsC,GACtC,6BAA6B,GAC7B,iCAAiC,GACjC,8BAA8B,GAC9B,uBAAuB,GACvB,oBAAoB,GACpB,0BAA0B,GAC1B,8BAA8B,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;CAC7C;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAEtF;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAExF"}
@@ -1 +1 @@
1
- {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAyHA,MAAM,UAAU,qBAAqB,CAAC,WAAwC;IAC5E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;AACpF,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,WAAwC;IAC9E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;AACtF,CAAC","sourcesContent":["export type DoctorDiagnosticCode =\n | 'field-invalid'\n | 'field-missing'\n | 'invalid-ankh-metadata'\n | 'invalid-app-manifest-json'\n | 'invalid-package-json'\n | 'missing-dependency'\n | 'missing-path'\n | 'missing-script'\n | 'provider-source-import-failed'\n | 'provider-source-missing'\n | 'target-not-directory'\n | 'target-not-found'\n | 'unsupported-target';\n\nexport type DoctorDiagnosticSeverity = 'error' | 'warning';\nexport type DoctorPolicyProfile =\n 'app-manifest' | 'integration-monorepo' | 'public-package' | 'unknown';\nexport type DoctorRuleId =\n | 'integration.package.ankh.disallowed'\n | 'integration.package.private.required'\n | 'integration.package.workspaces.required'\n | 'manifest.auth.authorization.engine.valid'\n | 'manifest.auth.authorization.kind.valid'\n | 'manifest.auth.authorization.valid-shape'\n | 'manifest.auth.flow.post-sign-in.distinct'\n | 'manifest.auth.flow.route.required'\n | 'manifest.auth.flow.route.unique'\n | 'manifest.auth.flow.route.valid'\n | 'manifest.auth.flow.unauthorized-route.valid'\n | 'manifest.auth.flow.valid-shape'\n | 'manifest.auth.oauth.callback-route.valid'\n | 'manifest.auth.oauth.provider.credentials-ref.required'\n | 'manifest.auth.oauth.provider.credentials-ref.valid'\n | 'manifest.auth.oauth.provider.duplicate'\n | 'manifest.auth.oauth.provider.inline-secret.disallowed'\n | 'manifest.auth.oauth.providers.valid'\n | 'manifest.auth.oauth.valid-shape'\n | 'manifest.auth.valid-shape'\n | 'manifest.infra.valid-shape'\n | 'manifest.json.readable'\n | 'manifest.json.valid'\n | 'manifest.root.valid-shape'\n | 'manifest.secret-store.provider.required'\n | 'manifest.secret-store.provider.valid'\n | 'manifest.secret-store.valid-shape'\n | 'manifest.settings.auth-flow.removed'\n | 'package.ankh.capabilities.match-provider'\n | 'package.ankh.present.valid-shape'\n | 'package.ankh.provider-path.required'\n | 'package.ankh.required-for-provider'\n | 'package.cli.export.required'\n | 'package.cli.index.required'\n | 'package.cli.root-file.disallowed'\n | 'package.dependencies.ankh-workspace-alias.disallowed'\n | 'package.dependencies.ankhorage4-source.disallowed'\n | 'package.dependencies.changesets.required'\n | 'package.dependencies.devtools.required'\n | 'package.dependencies.paradox.required'\n | 'package.dependencies.types-bun.required'\n | 'package.dependencies.types-node.required'\n | 'package.dependencies.typescript.required'\n | 'package.imports.ankh-workspace-alias.disallowed'\n | 'package.imports.ankhorage4-source.disallowed'\n | 'package.json.bugs.required'\n | 'package.json.description.required'\n | 'package.json.exports.required'\n | 'package.json.files.required'\n | 'package.json.homepage.required'\n | 'package.json.keywords.required'\n | 'package.json.license.required'\n | 'package.json.name.required'\n | 'package.json.package-manager.bun'\n | 'package.json.package-manager.required'\n | 'package.json.private.public-package-disallowed'\n | 'package.json.publish-config.public'\n | 'package.json.publish-config.required'\n | 'package.json.repository.required'\n | 'package.json.type.module'\n | 'package.json.type.required'\n | 'package.json.version.required'\n | 'package.scripts.build.required'\n | 'package.scripts.changeset-status.required'\n | 'package.scripts.changeset.required'\n | 'package.scripts.docs.required'\n | 'package.scripts.format-check.required'\n | 'package.scripts.format.required'\n | 'package.scripts.knip.required'\n | 'package.scripts.lint-fix.required'\n | 'package.scripts.lint.required'\n | 'package.scripts.test.required'\n | 'package.scripts.typecheck.required'\n | 'package.scripts.version-packages.required'\n | 'provider.commands.match-capabilities'\n | 'provider.commands.match-handlers'\n | 'provider.source.importable'\n | 'provider.source.required'\n | 'repo.changelog.required'\n | 'repo.changeset.required'\n | 'repo.license.required'\n | 'repo.readme.required'\n | 'repo.workflows.required'\n | 'studio.dependencies.dnd.required'\n | 'studio.dependencies.runtime.required'\n | 'studio.imports.dnd.required'\n | 'studio.imports.runtime.required'\n | 'target.package-json.required'\n | 'target.path.directory'\n | 'target.path.exists'\n | 'target.profile.supported'\n | 'target.repo-markers.required';\n\nexport interface DoctorDiagnostic {\n readonly code: DoctorDiagnosticCode;\n readonly message: string;\n readonly path: string;\n readonly profile: DoctorPolicyProfile;\n readonly ruleId: DoctorRuleId;\n readonly severity: DoctorDiagnosticSeverity;\n}\n\nexport function countErrorDiagnostics(diagnostics: readonly DoctorDiagnostic[]): number {\n return diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length;\n}\n\nexport function countWarningDiagnostics(diagnostics: readonly DoctorDiagnostic[]): number {\n return diagnostics.filter((diagnostic) => diagnostic.severity === 'warning').length;\n}\n"]}
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AA6HA,MAAM,UAAU,qBAAqB,CAAC,WAAwC;IAC5E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;AACpF,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,WAAwC;IAC9E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;AACtF,CAAC","sourcesContent":["export type DoctorDiagnosticCode =\n | 'field-invalid'\n | 'field-missing'\n | 'invalid-ankh-metadata'\n | 'invalid-app-manifest-json'\n | 'invalid-package-json'\n | 'missing-dependency'\n | 'missing-path'\n | 'missing-script'\n | 'provider-source-import-failed'\n | 'provider-source-missing'\n | 'target-not-directory'\n | 'target-not-found'\n | 'unsupported-target';\n\nexport type DoctorDiagnosticSeverity = 'error' | 'warning';\nexport type DoctorPolicyProfile =\n 'app-manifest' | 'integration-monorepo' | 'public-package' | 'unknown';\nexport type DoctorRuleId =\n | 'integration.package.ankh.disallowed'\n | 'integration.package.private.required'\n | 'integration.package.workspaces.required'\n | 'manifest.auth.authorization.engine.valid'\n | 'manifest.auth.authorization.kind.valid'\n | 'manifest.auth.authorization.valid-shape'\n | 'manifest.auth.flow.post-sign-in.distinct'\n | 'manifest.auth.flow.route.required'\n | 'manifest.auth.flow.route.unique'\n | 'manifest.auth.flow.route.valid'\n | 'manifest.auth.flow.unauthorized-route.valid'\n | 'manifest.auth.flow.valid-shape'\n | 'manifest.auth.oauth.callback-route.valid'\n | 'manifest.auth.oauth.provider.credentials-ref.required'\n | 'manifest.auth.oauth.provider.credentials-ref.valid'\n | 'manifest.auth.oauth.provider.duplicate'\n | 'manifest.auth.oauth.provider.inline-secret.disallowed'\n | 'manifest.auth.oauth.providers.valid'\n | 'manifest.auth.oauth.valid-shape'\n | 'manifest.auth.valid-shape'\n | 'manifest.infra.valid-shape'\n | 'manifest.json.readable'\n | 'manifest.json.valid'\n | 'manifest.root.valid-shape'\n | 'manifest.secret-store.provider.required'\n | 'manifest.secret-store.provider.valid'\n | 'manifest.secret-store.valid-shape'\n | 'manifest.settings.auth-flow.removed'\n | 'package.ankh.capabilities.match-provider'\n | 'package.ankh.present.valid-shape'\n | 'package.ankh.provider-path.required'\n | 'package.ankh.required-for-provider'\n | 'package.cli.export.required'\n | 'package.cli.index.required'\n | 'package.cli.root-file.disallowed'\n | 'package.dependencies.ankh-workspace-alias.disallowed'\n | 'package.dependencies.ankhorage4-source.disallowed'\n | 'package.dependencies.changesets.required'\n | 'package.dependencies.devtools.required'\n | 'package.dependencies.paradox.required'\n | 'package.dependencies.types-bun.policy'\n | 'package.dependencies.types-bun.required'\n | 'package.dependencies.types-node.required'\n | 'package.dependencies.typescript.required'\n | 'package.imports.ankh-workspace-alias.disallowed'\n | 'package.imports.ankhorage4-source.disallowed'\n | 'package.json.bugs.required'\n | 'package.json.description.required'\n | 'package.json.exports.required'\n | 'package.json.files.required'\n | 'package.json.homepage.required'\n | 'package.json.keywords.required'\n | 'package.json.license.required'\n | 'package.json.name.required'\n | 'package.json.package-manager.bun'\n | 'package.json.package-manager.policy'\n | 'package.json.package-manager.required'\n | 'package.json.private.public-package-disallowed'\n | 'package.json.publish-config.public'\n | 'package.json.publish-config.required'\n | 'package.json.repository.required'\n | 'package.json.type.module'\n | 'package.json.type.required'\n | 'package.json.version.required'\n | 'package.scripts.build.required'\n | 'package.scripts.changeset-status.required'\n | 'package.scripts.changeset.required'\n | 'package.scripts.docs.required'\n | 'package.scripts.format-check.required'\n | 'package.scripts.format.required'\n | 'package.scripts.knip.required'\n | 'package.scripts.lint-fix.required'\n | 'package.scripts.lint.required'\n | 'package.scripts.test.required'\n | 'package.scripts.typecheck.required'\n | 'package.scripts.version-packages.required'\n | 'provider.commands.match-capabilities'\n | 'provider.commands.match-handlers'\n | 'provider.source.importable'\n | 'provider.source.required'\n | 'repo.changelog.required'\n | 'repo.changeset.required'\n | 'repo.license.required'\n | 'repo.readme.required'\n | 'repo.workflows.ci.bun-policy'\n | 'repo.workflows.release.bun-policy'\n | 'repo.workflows.required'\n | 'studio.dependencies.dnd.required'\n | 'studio.dependencies.runtime.required'\n | 'studio.imports.dnd.required'\n | 'studio.imports.runtime.required'\n | 'target.package-json.required'\n | 'target.path.directory'\n | 'target.path.exists'\n | 'target.profile.supported'\n | 'target.repo-markers.required';\n\nexport interface DoctorDiagnostic {\n readonly code: DoctorDiagnosticCode;\n readonly message: string;\n readonly path: string;\n readonly profile: DoctorPolicyProfile;\n readonly ruleId: DoctorRuleId;\n readonly severity: DoctorDiagnosticSeverity;\n}\n\nexport function countErrorDiagnostics(diagnostics: readonly DoctorDiagnostic[]): number {\n return diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length;\n}\n\nexport function countWarningDiagnostics(diagnostics: readonly DoctorDiagnostic[]): number {\n return diagnostics.filter((diagnostic) => diagnostic.severity === 'warning').length;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  export type { DoctorAnalysisRequest, DoctorAnalysisResult, DoctorFixPlan, DoctorPlannedChange, DoctorPlannedChangeKind, DoctorTargetCheck, DoctorTargetMode, } from './analysis.js';
2
- export { analyzeDoctorTarget } from './analysis.js';
3
2
  export { createDoctorRuntimeProvider, type CreateDoctorRuntimeProviderOptions, } from './cli/index.js';
4
3
  export { runCli } from './cli/standalone.js';
5
- export { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
6
4
  export type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';
7
5
  export { createDefaultCommandContext } from './commandContext.js';
6
+ export { analyzeDoctorTarget, analyzeDoctorTargetWithCliLayout, } from './dependencyPolicyAnalysis.js';
8
7
  export type { DoctorDiagnostic, DoctorDiagnosticCode, DoctorDiagnosticSeverity, DoctorPolicyProfile, DoctorRuleId, } from './diagnostics.js';
9
8
  export { analyzeAppManifest, analyzeAppManifestFile, analyzeAppManifestTarget, } from './manifestAnalysis.js';
10
9
  export { DOCTOR_CAPABILITIES, DOCTOR_COMMAND_CATEGORY, DOCTOR_PACKAGE_METADATA, DOCTOR_PACKAGE_NAME, DOCTOR_PACKAGE_VERSION, } from './packageMetadata.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EACL,2BAA2B,EAC3B,KAAK,kCAAkC,GACxC,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,gCAAgC,EAAE,MAAM,wBAAwB,CAAC;AAC1E,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAClE,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,EACnB,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,2BAA2B,EAC3B,KAAK,kCAAkC,GACxC,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EACL,mBAAmB,EACnB,gCAAgC,GACjC,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,EACnB,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,7 @@
1
- export { analyzeDoctorTarget } from './analysis.js';
2
1
  export { createDoctorRuntimeProvider, } from './cli/index.js';
3
2
  export { runCli } from './cli/standalone.js';
4
- export { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
5
3
  export { createDefaultCommandContext } from './commandContext.js';
4
+ export { analyzeDoctorTarget, analyzeDoctorTargetWithCliLayout, } from './dependencyPolicyAnalysis.js';
6
5
  export { analyzeAppManifest, analyzeAppManifestFile, analyzeAppManifestTarget, } from './manifestAnalysis.js';
7
6
  export { DOCTOR_CAPABILITIES, DOCTOR_COMMAND_CATEGORY, DOCTOR_PACKAGE_METADATA, DOCTOR_PACKAGE_NAME, DOCTOR_PACKAGE_VERSION, } from './packageMetadata.js';
8
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EACL,2BAA2B,GAE5B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,gCAAgC,EAAE,MAAM,wBAAwB,CAAC;AAE1E,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAQlE,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC","sourcesContent":["export type {\n DoctorAnalysisRequest,\n DoctorAnalysisResult,\n DoctorFixPlan,\n DoctorPlannedChange,\n DoctorPlannedChangeKind,\n DoctorTargetCheck,\n DoctorTargetMode,\n} from './analysis.js';\nexport { analyzeDoctorTarget } from './analysis.js';\nexport {\n createDoctorRuntimeProvider,\n type CreateDoctorRuntimeProviderOptions,\n} from './cli/index.js';\nexport { runCli } from './cli/standalone.js';\nexport { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';\nexport type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';\nexport { createDefaultCommandContext } from './commandContext.js';\nexport type {\n DoctorDiagnostic,\n DoctorDiagnosticCode,\n DoctorDiagnosticSeverity,\n DoctorPolicyProfile,\n DoctorRuleId,\n} from './diagnostics.js';\nexport {\n analyzeAppManifest,\n analyzeAppManifestFile,\n analyzeAppManifestTarget,\n} from './manifestAnalysis.js';\nexport {\n DOCTOR_CAPABILITIES,\n DOCTOR_COMMAND_CATEGORY,\n DOCTOR_PACKAGE_METADATA,\n DOCTOR_PACKAGE_NAME,\n DOCTOR_PACKAGE_VERSION,\n} from './packageMetadata.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EACL,2BAA2B,GAE5B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EACL,mBAAmB,EACnB,gCAAgC,GACjC,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC","sourcesContent":["export type {\n DoctorAnalysisRequest,\n DoctorAnalysisResult,\n DoctorFixPlan,\n DoctorPlannedChange,\n DoctorPlannedChangeKind,\n DoctorTargetCheck,\n DoctorTargetMode,\n} from './analysis.js';\nexport {\n createDoctorRuntimeProvider,\n type CreateDoctorRuntimeProviderOptions,\n} from './cli/index.js';\nexport { runCli } from './cli/standalone.js';\nexport type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';\nexport { createDefaultCommandContext } from './commandContext.js';\nexport {\n analyzeDoctorTarget,\n analyzeDoctorTargetWithCliLayout,\n} from './dependencyPolicyAnalysis.js';\nexport type {\n DoctorDiagnostic,\n DoctorDiagnosticCode,\n DoctorDiagnosticSeverity,\n DoctorPolicyProfile,\n DoctorRuleId,\n} from './diagnostics.js';\nexport {\n analyzeAppManifest,\n analyzeAppManifestFile,\n analyzeAppManifestTarget,\n} from './manifestAnalysis.js';\nexport {\n DOCTOR_CAPABILITIES,\n DOCTOR_COMMAND_CATEGORY,\n DOCTOR_PACKAGE_METADATA,\n DOCTOR_PACKAGE_NAME,\n DOCTOR_PACKAGE_VERSION,\n} from './packageMetadata.js';\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/doctor",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Executable Ankh provider and standalone CLI for lightweight repo and package compliance diagnostics.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -0,0 +1,261 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ import type { DoctorAnalysisResult } from './analysis.js';
7
+ import type { DoctorDiagnostic, DoctorRuleId } from './diagnostics.js';
8
+
9
+ interface BunRuntimePolicySnapshot {
10
+ readonly packageManager: string;
11
+ readonly typesRange: string;
12
+ readonly version: string;
13
+ }
14
+
15
+ const DEVTOOLS_POLICY_SPECIFIER = '@ankhorage/devtools/policy';
16
+ const REPAIR_HINT = 'Run "ankh devtools sync" to repair the managed Bun state.';
17
+ const BUN_VERSION_PATTERN = /^\s*bun-version:\s*['"]?([^'"\s#]+)['"]?\s*(?:#.*)?$/mu;
18
+ const WORKFLOWS = [
19
+ {
20
+ relativePath: path.join('.github', 'workflows', 'ci.yml'),
21
+ ruleId: 'repo.workflows.ci.bun-policy',
22
+ },
23
+ {
24
+ relativePath: path.join('.github', 'workflows', 'release.yml'),
25
+ ruleId: 'repo.workflows.release.bun-policy',
26
+ },
27
+ ] as const satisfies readonly { readonly relativePath: string; readonly ruleId: DoctorRuleId }[];
28
+
29
+ export async function applyBunRuntimePolicy(
30
+ result: DoctorAnalysisResult,
31
+ ): Promise<DoctorAnalysisResult> {
32
+ if (result.profile !== 'public-package' || !result.hasPackageJson) {
33
+ return result;
34
+ }
35
+
36
+ const policy = await loadTargetBunRuntimePolicy(result.targetPath);
37
+ const packageJsonPath = path.join(result.targetPath, 'package.json');
38
+ const packageJson = await readPackageJson(packageJsonPath);
39
+ if (policy === null || packageJson === null) {
40
+ return result;
41
+ }
42
+
43
+ const diagnostics = [
44
+ ...result.diagnostics,
45
+ ...analyzePackageBunPolicy(packageJson, packageJsonPath, result, policy),
46
+ ...(await analyzeWorkflowBunPolicy(result, policy)),
47
+ ];
48
+
49
+ return withDiagnostics(result, diagnostics);
50
+ }
51
+
52
+ async function loadTargetBunRuntimePolicy(
53
+ targetPath: string,
54
+ ): Promise<BunRuntimePolicySnapshot | null> {
55
+ const requireFromTarget = createRequire(path.join(targetPath, 'package.json'));
56
+ try {
57
+ const modulePath = requireFromTarget.resolve(DEVTOOLS_POLICY_SPECIFIER);
58
+ const importedModule = (await import(pathToFileURL(modulePath).href)) as unknown;
59
+ return parseBunRuntimePolicy(importedModule);
60
+ } catch (error) {
61
+ if (isUnavailablePolicyError(error)) {
62
+ return null;
63
+ }
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ function analyzePackageBunPolicy(
69
+ packageJson: Record<string, unknown>,
70
+ packageJsonPath: string,
71
+ result: DoctorAnalysisResult,
72
+ policy: BunRuntimePolicySnapshot,
73
+ ): DoctorDiagnostic[] {
74
+ const diagnostics: DoctorDiagnostic[] = [];
75
+ const { packageManager } = packageJson;
76
+ if (isNonEmptyString(packageManager) && packageManager.startsWith('bun@')) {
77
+ pushMismatch(diagnostics, {
78
+ actual: packageManager,
79
+ expected: policy.packageManager,
80
+ location: 'package.json#packageManager',
81
+ path: packageJsonPath,
82
+ profile: result.profile,
83
+ ruleId: 'package.json.package-manager.policy',
84
+ });
85
+ }
86
+
87
+ const devDependencies = isRecord(packageJson.devDependencies)
88
+ ? packageJson.devDependencies
89
+ : null;
90
+ const bunTypes = devDependencies?.['@types/bun'];
91
+ if (isNonEmptyString(bunTypes)) {
92
+ pushMismatch(diagnostics, {
93
+ actual: bunTypes,
94
+ expected: policy.typesRange,
95
+ location: 'package.json#devDependencies.@types/bun',
96
+ path: packageJsonPath,
97
+ profile: result.profile,
98
+ ruleId: 'package.dependencies.types-bun.policy',
99
+ });
100
+ }
101
+
102
+ return diagnostics;
103
+ }
104
+
105
+ async function analyzeWorkflowBunPolicy(
106
+ result: DoctorAnalysisResult,
107
+ policy: BunRuntimePolicySnapshot,
108
+ ): Promise<DoctorDiagnostic[]> {
109
+ const diagnostics = await Promise.all(
110
+ WORKFLOWS.map(async (workflow) => await analyzeWorkflow(result, workflow, policy)),
111
+ );
112
+ return diagnostics.flat();
113
+ }
114
+
115
+ async function analyzeWorkflow(
116
+ result: DoctorAnalysisResult,
117
+ workflow: (typeof WORKFLOWS)[number],
118
+ policy: BunRuntimePolicySnapshot,
119
+ ): Promise<DoctorDiagnostic[]> {
120
+ const workflowPath = path.join(result.targetPath, workflow.relativePath);
121
+ const contents = await readTextOrNull(workflowPath);
122
+ if (contents === null) {
123
+ return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'missing-path')];
124
+ }
125
+
126
+ const match = BUN_VERSION_PATTERN.exec(contents);
127
+ const actual = match?.[1];
128
+ if (!isNonEmptyString(actual)) {
129
+ return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'field-missing')];
130
+ }
131
+
132
+ const diagnostics: DoctorDiagnostic[] = [];
133
+ pushMismatch(diagnostics, {
134
+ actual,
135
+ expected: policy.version,
136
+ location: `${workflow.relativePath}#bun-version`,
137
+ path: workflowPath,
138
+ profile: result.profile,
139
+ ruleId: workflow.ruleId,
140
+ });
141
+ return diagnostics;
142
+ }
143
+
144
+ function createWorkflowDiagnostic(
145
+ result: DoctorAnalysisResult,
146
+ workflow: (typeof WORKFLOWS)[number],
147
+ policy: BunRuntimePolicySnapshot,
148
+ workflowPath: string,
149
+ code: DoctorDiagnostic['code'],
150
+ ): DoctorDiagnostic {
151
+ const detail =
152
+ code === 'missing-path'
153
+ ? `Managed workflow is missing: ${workflow.relativePath}.`
154
+ : `${workflow.relativePath} does not define bun-version.`;
155
+ return {
156
+ code,
157
+ message: `${detail} Expected Bun ${policy.version}. ${REPAIR_HINT}`,
158
+ path: workflowPath,
159
+ profile: result.profile,
160
+ ruleId: workflow.ruleId,
161
+ severity: 'error',
162
+ };
163
+ }
164
+
165
+ function pushMismatch(
166
+ diagnostics: DoctorDiagnostic[],
167
+ input: {
168
+ readonly actual: string;
169
+ readonly expected: string;
170
+ readonly location: string;
171
+ readonly path: string;
172
+ readonly profile: DoctorAnalysisResult['profile'];
173
+ readonly ruleId: DoctorRuleId;
174
+ },
175
+ ): void {
176
+ if (input.actual === input.expected) {
177
+ return;
178
+ }
179
+ diagnostics.push({
180
+ code: 'field-invalid',
181
+ message: `Bun policy drift at ${input.location}: expected "${input.expected}", found "${input.actual}". ${REPAIR_HINT}`,
182
+ path: input.path,
183
+ profile: input.profile,
184
+ ruleId: input.ruleId,
185
+ severity: 'error',
186
+ });
187
+ }
188
+
189
+ function parseBunRuntimePolicy(importedModule: unknown): BunRuntimePolicySnapshot | null {
190
+ if (!isRecord(importedModule) || !isRecord(importedModule.bunRuntimePolicy)) {
191
+ return null;
192
+ }
193
+ const { packageManager, typesRange, version } = importedModule.bunRuntimePolicy;
194
+ return isNonEmptyString(packageManager) &&
195
+ isNonEmptyString(typesRange) &&
196
+ isNonEmptyString(version)
197
+ ? { packageManager, typesRange, version }
198
+ : null;
199
+ }
200
+
201
+ function withDiagnostics(
202
+ result: DoctorAnalysisResult,
203
+ diagnostics: readonly DoctorDiagnostic[],
204
+ ): DoctorAnalysisResult {
205
+ return {
206
+ ...result,
207
+ diagnostics,
208
+ fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },
209
+ };
210
+ }
211
+
212
+ async function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {
213
+ try {
214
+ const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as unknown;
215
+ return isRecord(parsed) ? parsed : null;
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+
221
+ async function readTextOrNull(filePath: string): Promise<string | null> {
222
+ try {
223
+ return await fs.readFile(filePath, 'utf8');
224
+ } catch (error) {
225
+ if (isNodeError(error) && error.code === 'ENOENT') {
226
+ return null;
227
+ }
228
+ throw error;
229
+ }
230
+ }
231
+
232
+ function isUnavailablePolicyError(error: unknown): boolean {
233
+ if (
234
+ isNodeError(error) &&
235
+ (error.code === 'MODULE_NOT_FOUND' ||
236
+ error.code === 'ERR_MODULE_NOT_FOUND' ||
237
+ error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED')
238
+ ) {
239
+ return true;
240
+ }
241
+
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ return (
244
+ message.includes(DEVTOOLS_POLICY_SPECIFIER) &&
245
+ (message.includes('Cannot find module') ||
246
+ message.includes('not exported') ||
247
+ message.includes('Package subpath'))
248
+ );
249
+ }
250
+
251
+ function isNonEmptyString(value: unknown): value is string {
252
+ return typeof value === 'string' && value.trim() !== '';
253
+ }
254
+
255
+ function isRecord(value: unknown): value is Record<string, unknown> {
256
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
257
+ }
258
+
259
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
260
+ return error instanceof Error && 'code' in error;
261
+ }
package/src/cli/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { AnkhCommandHandler, AnkhRuntimeCommandProvider } from '@ankhorage/ankh';
2
2
 
3
- import { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';
4
3
  import {
5
4
  createProviderCommandDescriptors,
6
5
  DOCTOR_COMMANDS,
@@ -8,6 +7,7 @@ import {
8
7
  runDoctorCommand,
9
8
  type RunDoctorCommandImpl,
10
9
  } from '../commands.js';
10
+ import { analyzeDoctorTargetWithCliLayout } from '../dependencyPolicyAnalysis.js';
11
11
  import {
12
12
  DOCTOR_CAPABILITIES,
13
13
  DOCTOR_COMMAND_CATEGORY,
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';
4
3
  import type { DoctorCommandContext, DoctorCommandRunResult } from '../commandContext.js';
5
4
  import { createDefaultCommandContext } from '../commandContext.js';
6
5
  import {
@@ -11,6 +10,7 @@ import {
11
10
  runDoctorCommand,
12
11
  type RunDoctorCommandImpl,
13
12
  } from '../commands.js';
13
+ import { analyzeDoctorTargetWithCliLayout } from '../dependencyPolicyAnalysis.js';
14
14
 
15
15
  export interface DoctorCliOptions {
16
16
  readonly context?: DoctorCommandContext;
@@ -0,0 +1,119 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import {
5
+ analyzeDoctorTarget as analyzeBaseDoctorTarget,
6
+ type DoctorAnalysisRequest,
7
+ type DoctorAnalysisResult,
8
+ } from './analysis.js';
9
+ import { applyBunRuntimePolicy } from './bunPolicyAnalysis.js';
10
+ import { analyzeDoctorTargetWithCliLayout as analyzeBaseDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
11
+ import type { DoctorDiagnostic } from './diagnostics.js';
12
+
13
+ const ANKH_PACKAGE_NAME = '@ankhorage/ankh';
14
+ const DEVTOOLS_PACKAGE_NAME = '@ankhorage/devtools';
15
+ const DEVTOOLS_REQUIRED_RULE_ID = 'package.dependencies.devtools.required';
16
+
17
+ export async function analyzeDoctorTarget(
18
+ request: DoctorAnalysisRequest,
19
+ ): Promise<DoctorAnalysisResult> {
20
+ return applyRepositoryPolicies(await analyzeBaseDoctorTarget(request));
21
+ }
22
+
23
+ export async function analyzeDoctorTargetWithCliLayout(
24
+ request: DoctorAnalysisRequest,
25
+ ): Promise<DoctorAnalysisResult> {
26
+ return applyRepositoryPolicies(await analyzeBaseDoctorTargetWithCliLayout(request));
27
+ }
28
+
29
+ async function applyRepositoryPolicies(
30
+ result: DoctorAnalysisResult,
31
+ ): Promise<DoctorAnalysisResult> {
32
+ return applyBunRuntimePolicy(await applyDevtoolsDependencyPlacementPolicy(result));
33
+ }
34
+
35
+ async function applyDevtoolsDependencyPlacementPolicy(
36
+ result: DoctorAnalysisResult,
37
+ ): Promise<DoctorAnalysisResult> {
38
+ if (!result.hasPackageJson) {
39
+ return result;
40
+ }
41
+
42
+ const packageJsonPath = path.join(result.targetPath, 'package.json');
43
+ const packageJson = await readPackageJson(packageJsonPath);
44
+ if (packageJson?.name !== ANKH_PACKAGE_NAME || !expectsDevtools(packageJson)) {
45
+ return result;
46
+ }
47
+
48
+ const dependencies = isRecord(packageJson.dependencies) ? packageJson.dependencies : null;
49
+ const hasRuntimeDevtools = hasDependency(dependencies, DEVTOOLS_PACKAGE_NAME);
50
+ const diagnosticsWithoutPlacementRule = result.diagnostics.filter(
51
+ (diagnostic) => diagnostic.ruleId !== DEVTOOLS_REQUIRED_RULE_ID,
52
+ );
53
+ const diagnostics = hasRuntimeDevtools
54
+ ? diagnosticsWithoutPlacementRule
55
+ : [
56
+ ...diagnosticsWithoutPlacementRule,
57
+ createMissingRuntimeDevtoolsDiagnostic(packageJsonPath, result),
58
+ ];
59
+
60
+ return {
61
+ ...result,
62
+ diagnostics,
63
+ fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },
64
+ };
65
+ }
66
+
67
+ async function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {
68
+ try {
69
+ const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as unknown;
70
+ return isRecord(parsed) ? parsed : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function createMissingRuntimeDevtoolsDiagnostic(
77
+ packageJsonPath: string,
78
+ result: DoctorAnalysisResult,
79
+ ): DoctorDiagnostic {
80
+ return {
81
+ code: 'missing-dependency',
82
+ message:
83
+ '@ankhorage/ankh bundles @ankhorage/devtools as a runtime core provider and must declare it in dependencies.',
84
+ path: packageJsonPath,
85
+ profile: result.profile,
86
+ ruleId: DEVTOOLS_REQUIRED_RULE_ID,
87
+ severity: 'error',
88
+ };
89
+ }
90
+
91
+ function expectsDevtools(packageJson: Record<string, unknown>): boolean {
92
+ const scripts = isRecord(packageJson.scripts) ? packageJson.scripts : null;
93
+ if (scripts === null) {
94
+ return false;
95
+ }
96
+
97
+ return Object.values(scripts).some(
98
+ (scriptValue) =>
99
+ isNonEmptyString(scriptValue) &&
100
+ (scriptValue.includes('ankhorage-eslint') ||
101
+ scriptValue.includes('ankhorage-prettier') ||
102
+ scriptValue.includes('ankhorage-knip')),
103
+ );
104
+ }
105
+
106
+ function hasDependency(
107
+ dependencyMap: Record<string, unknown> | null,
108
+ dependencyName: string,
109
+ ): boolean {
110
+ return dependencyMap !== null && isNonEmptyString(dependencyMap[dependencyName]);
111
+ }
112
+
113
+ function isNonEmptyString(value: unknown): value is string {
114
+ return typeof value === 'string' && value.trim() !== '';
115
+ }
116
+
117
+ function isRecord(value: unknown): value is Record<string, unknown> {
118
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
119
+ }
@@ -57,6 +57,7 @@ export type DoctorRuleId =
57
57
  | 'package.dependencies.changesets.required'
58
58
  | 'package.dependencies.devtools.required'
59
59
  | 'package.dependencies.paradox.required'
60
+ | 'package.dependencies.types-bun.policy'
60
61
  | 'package.dependencies.types-bun.required'
61
62
  | 'package.dependencies.types-node.required'
62
63
  | 'package.dependencies.typescript.required'
@@ -71,6 +72,7 @@ export type DoctorRuleId =
71
72
  | 'package.json.license.required'
72
73
  | 'package.json.name.required'
73
74
  | 'package.json.package-manager.bun'
75
+ | 'package.json.package-manager.policy'
74
76
  | 'package.json.package-manager.required'
75
77
  | 'package.json.private.public-package-disallowed'
76
78
  | 'package.json.publish-config.public'
@@ -99,6 +101,8 @@ export type DoctorRuleId =
99
101
  | 'repo.changeset.required'
100
102
  | 'repo.license.required'
101
103
  | 'repo.readme.required'
104
+ | 'repo.workflows.ci.bun-policy'
105
+ | 'repo.workflows.release.bun-policy'
102
106
  | 'repo.workflows.required'
103
107
  | 'studio.dependencies.dnd.required'
104
108
  | 'studio.dependencies.runtime.required'
package/src/index.ts CHANGED
@@ -7,15 +7,17 @@ export type {
7
7
  DoctorTargetCheck,
8
8
  DoctorTargetMode,
9
9
  } from './analysis.js';
10
- export { analyzeDoctorTarget } from './analysis.js';
11
10
  export {
12
11
  createDoctorRuntimeProvider,
13
12
  type CreateDoctorRuntimeProviderOptions,
14
13
  } from './cli/index.js';
15
14
  export { runCli } from './cli/standalone.js';
16
- export { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
17
15
  export type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';
18
16
  export { createDefaultCommandContext } from './commandContext.js';
17
+ export {
18
+ analyzeDoctorTarget,
19
+ analyzeDoctorTargetWithCliLayout,
20
+ } from './dependencyPolicyAnalysis.js';
19
21
  export type {
20
22
  DoctorDiagnostic,
21
23
  DoctorDiagnosticCode,