@ankhorage/doctor 0.5.0 → 0.7.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 +12 -0
- package/README.md +7 -2
- package/dist/analysis.d.ts +1 -1
- package/dist/analysis.d.ts.map +1 -1
- package/dist/analysis.js.map +1 -1
- package/dist/cliLayoutAnalysis.d.ts.map +1 -1
- package/dist/cliLayoutAnalysis.js +5 -0
- package/dist/cliLayoutAnalysis.js.map +1 -1
- package/dist/commands.d.ts +3 -3
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +5 -5
- package/dist/commands.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -3
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/manifestAnalysis.d.ts +6 -0
- package/dist/manifestAnalysis.d.ts.map +1 -0
- package/dist/manifestAnalysis.js +324 -0
- package/dist/manifestAnalysis.js.map +1 -0
- package/dist/readme-usage.js +6 -1
- package/dist/readme-usage.js.map +1 -1
- package/dist/secretManifestAnalysis.d.ts +3 -0
- package/dist/secretManifestAnalysis.d.ts.map +1 -0
- package/dist/secretManifestAnalysis.js +96 -0
- package/dist/secretManifestAnalysis.js.map +1 -0
- package/package.json +5 -2
- package/src/analysis.ts +1 -1
- package/src/cliLayoutAnalysis.ts +6 -0
- package/src/commands.ts +7 -6
- package/src/diagnostics.ts +28 -1
- package/src/index.ts +5 -0
- package/src/manifestAnalysis.ts +461 -0
- package/src/readme-usage.ts +6 -1
- package/src/secretManifestAnalysis.ts +241 -0
package/dist/readme-usage.js
CHANGED
|
@@ -20,8 +20,13 @@ import { runCli } from './cli/standalone.js';
|
|
|
20
20
|
*
|
|
21
21
|
* - strict `public-package` validation for extracted public package repos
|
|
22
22
|
* - light recognized `integration-monorepo` validation for `ankhorage4`
|
|
23
|
+
* - canonical app-manifest authentication validation for JSON file targets
|
|
23
24
|
* - non-mutating `fix` plans for deterministic mechanical changes only
|
|
24
25
|
*
|
|
26
|
+
* Manifest validation accepts `infra.auth.flow` as the only auth-flow location, rejects
|
|
27
|
+
* `settings.authFlow` with a manual-migration diagnostic, and permits authentication without
|
|
28
|
+
* an authorization block.
|
|
29
|
+
*
|
|
25
30
|
* Still deferred:
|
|
26
31
|
*
|
|
27
32
|
* - GitHub checks
|
|
@@ -31,7 +36,7 @@ import { runCli } from './cli/standalone.js';
|
|
|
31
36
|
*
|
|
32
37
|
* Path handling:
|
|
33
38
|
*
|
|
34
|
-
* - pass `[path]`, or
|
|
39
|
+
* - pass a repo/package directory or app-manifest JSON file as `[path]`, or
|
|
35
40
|
* - omit it to inspect the current working directory
|
|
36
41
|
*
|
|
37
42
|
* @usage
|
package/dist/readme-usage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readme-usage.js","sourceRoot":"","sources":["../src/readme-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C
|
|
1
|
+
{"version":3,"file":"readme-usage.js","sourceRoot":"","sources":["../src/readme-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,MAAM,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC","sourcesContent":["import { runCli } from './cli/standalone.js';\n\n/***\n * Provider and CLI surface\n *\n * `@ankhorage/doctor` is the repo/package compliance provider for Ankhorage.\n *\n * The same shared command implementation backs both:\n *\n * - `ankh doctor ...`\n * - `bunx @ankhorage/doctor ...`\n *\n * Current command surface:\n *\n * - `validate`\n * - `fix`\n * - `repo`\n * - `package`\n *\n * `doctor#2` adds a profile-based policy engine:\n *\n * - strict `public-package` validation for extracted public package repos\n * - light recognized `integration-monorepo` validation for `ankhorage4`\n * - canonical app-manifest authentication validation for JSON file targets\n * - non-mutating `fix` plans for deterministic mechanical changes only\n *\n * Manifest validation accepts `infra.auth.flow` as the only auth-flow location, rejects\n * `settings.authFlow` with a manual-migration diagnostic, and permits authentication without\n * an authorization block.\n *\n * Still deferred:\n *\n * - GitHub checks\n * - CI checks\n * - on-disk `fix --apply`\n * - deeper cross-repo policy enforcement\n *\n * Path handling:\n *\n * - pass a repo/package directory or app-manifest JSON file as `[path]`, or\n * - omit it to inspect the current working directory\n *\n * @usage\n */\nawait runCli(['--help']);\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretManifestAnalysis.d.ts","sourceRoot":"","sources":["../src/secretManifestAnalysis.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAgB,MAAM,kBAAkB,CAAC;AAIvE,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,YAAY,EAAE,MAAM,GACnB,gBAAgB,EAAE,CAiKpB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { findForbiddenInlineSecretFields, normalizeSecretRef, SECRET_STORE_PROVIDERS, } from '@ankhorage/contracts/secrets';
|
|
2
|
+
const PROFILE = 'app-manifest';
|
|
3
|
+
export function analyzeSecretStoreManifest(infra, manifestPath) {
|
|
4
|
+
const diagnostics = [];
|
|
5
|
+
const secretStoreProvider = validateSecretStore(infra.secretStore, manifestPath, diagnostics);
|
|
6
|
+
if (!isRecord(infra.auth))
|
|
7
|
+
return diagnostics;
|
|
8
|
+
const { oauth } = infra.auth;
|
|
9
|
+
if (oauth === undefined)
|
|
10
|
+
return diagnostics;
|
|
11
|
+
if (!isRecord(oauth)) {
|
|
12
|
+
diagnostics.push(diagnostic('field-invalid', 'manifest.infra.auth.oauth must be a JSON object when present.', manifestPath, 'manifest.auth.oauth.valid-shape'));
|
|
13
|
+
return diagnostics;
|
|
14
|
+
}
|
|
15
|
+
if (typeof oauth.enabled !== 'boolean') {
|
|
16
|
+
diagnostics.push(diagnostic(oauth.enabled === undefined ? 'field-missing' : 'field-invalid', 'manifest.infra.auth.oauth.enabled must be a boolean.', manifestPath, 'manifest.auth.oauth.valid-shape'));
|
|
17
|
+
}
|
|
18
|
+
if (typeof oauth.callbackRoute !== 'string' ||
|
|
19
|
+
oauth.callbackRoute.trim().length === 0 ||
|
|
20
|
+
!oauth.callbackRoute.trim().startsWith('/')) {
|
|
21
|
+
diagnostics.push(diagnostic(oauth.callbackRoute === undefined ? 'field-missing' : 'field-invalid', 'manifest.infra.auth.oauth.callbackRoute must be a non-empty absolute route.', manifestPath, 'manifest.auth.oauth.callback-route.valid'));
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(oauth.providers)) {
|
|
24
|
+
diagnostics.push(diagnostic(oauth.providers === undefined ? 'field-missing' : 'field-invalid', 'manifest.infra.auth.oauth.providers must be an array.', manifestPath, 'manifest.auth.oauth.providers.valid'));
|
|
25
|
+
return diagnostics;
|
|
26
|
+
}
|
|
27
|
+
const enabledProviders = oauth.providers.filter((provider) => isRecord(provider) && provider.enabled !== false);
|
|
28
|
+
if (oauth.enabled === true && enabledProviders.length === 0) {
|
|
29
|
+
diagnostics.push(diagnostic('field-invalid', 'OAuth is enabled, but no OAuth providers are enabled.', manifestPath, 'manifest.auth.oauth.providers.valid'));
|
|
30
|
+
}
|
|
31
|
+
if (enabledProviders.length > 0 && secretStoreProvider === null) {
|
|
32
|
+
diagnostics.push(diagnostic('field-missing', 'Enabled OAuth providers require manifest.infra.secretStore.provider.', manifestPath, 'manifest.secret-store.provider.required'));
|
|
33
|
+
}
|
|
34
|
+
const seenProviderIds = new Set();
|
|
35
|
+
for (const [index, provider] of oauth.providers.entries()) {
|
|
36
|
+
if (!isRecord(provider)) {
|
|
37
|
+
diagnostics.push(diagnostic('field-invalid', `manifest.infra.auth.oauth.providers[${index}] must be a JSON object.`, manifestPath, 'manifest.auth.oauth.providers.valid'));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const providerId = typeof provider.id === 'string' ? provider.id.trim() : '';
|
|
41
|
+
if (providerId.length === 0) {
|
|
42
|
+
diagnostics.push(diagnostic(provider.id === undefined ? 'field-missing' : 'field-invalid', `manifest.infra.auth.oauth.providers[${index}].id must be a non-empty string.`, manifestPath, 'manifest.auth.oauth.providers.valid'));
|
|
43
|
+
}
|
|
44
|
+
else if (seenProviderIds.has(providerId)) {
|
|
45
|
+
diagnostics.push(diagnostic('field-invalid', `OAuth provider id "${providerId}" is configured more than once.`, manifestPath, 'manifest.auth.oauth.provider.duplicate'));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
seenProviderIds.add(providerId);
|
|
49
|
+
}
|
|
50
|
+
for (const field of findForbiddenInlineSecretFields(provider)) {
|
|
51
|
+
diagnostics.push(diagnostic('field-invalid', `Inline OAuth field "${field}" is forbidden. Store the value in the configured secret store and reference it with credentialsRef.`, manifestPath, 'manifest.auth.oauth.provider.inline-secret.disallowed'));
|
|
52
|
+
}
|
|
53
|
+
if (provider.enabled === false)
|
|
54
|
+
continue;
|
|
55
|
+
if (typeof provider.credentialsRef !== 'string') {
|
|
56
|
+
diagnostics.push(diagnostic(provider.credentialsRef === undefined ? 'field-missing' : 'field-invalid', `Enabled OAuth provider "${providerId || index}" requires a credentialsRef.`, manifestPath, 'manifest.auth.oauth.provider.credentials-ref.required'));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const refResult = normalizeSecretRef(provider.credentialsRef);
|
|
60
|
+
if (!refResult.ok || refResult.data !== provider.credentialsRef) {
|
|
61
|
+
diagnostics.push(diagnostic('field-invalid', `OAuth provider "${providerId || index}" credentialsRef must be a canonical logical secret reference.`, manifestPath, 'manifest.auth.oauth.provider.credentials-ref.valid'));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return diagnostics;
|
|
65
|
+
}
|
|
66
|
+
function validateSecretStore(value, manifestPath, diagnostics) {
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
return null;
|
|
69
|
+
if (!isRecord(value)) {
|
|
70
|
+
diagnostics.push(diagnostic('field-invalid', 'manifest.infra.secretStore must be a JSON object when present.', manifestPath, 'manifest.secret-store.valid-shape'));
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (typeof value.provider !== 'string' || value.provider.trim().length === 0) {
|
|
74
|
+
diagnostics.push(diagnostic(value.provider === undefined ? 'field-missing' : 'field-invalid', 'manifest.infra.secretStore.provider must be a non-empty string.', manifestPath, 'manifest.secret-store.provider.required'));
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const provider = value.provider.trim();
|
|
78
|
+
if (!SECRET_STORE_PROVIDERS.includes(provider)) {
|
|
79
|
+
diagnostics.push(diagnostic('field-invalid', `Unsupported secret-store provider "${provider}". Current providers: ${SECRET_STORE_PROVIDERS.join(', ')}.`, manifestPath, 'manifest.secret-store.provider.valid'));
|
|
80
|
+
}
|
|
81
|
+
return provider;
|
|
82
|
+
}
|
|
83
|
+
function diagnostic(code, message, path, ruleId) {
|
|
84
|
+
return {
|
|
85
|
+
code,
|
|
86
|
+
message,
|
|
87
|
+
path,
|
|
88
|
+
profile: PROFILE,
|
|
89
|
+
ruleId,
|
|
90
|
+
severity: 'error',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function isRecord(value) {
|
|
94
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=secretManifestAnalysis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretManifestAnalysis.js","sourceRoot":"","sources":["../src/secretManifestAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n findForbiddenInlineSecretFields,\n normalizeSecretRef,\n SECRET_STORE_PROVIDERS,\n} from '@ankhorage/contracts/secrets';\n\nimport type { DoctorDiagnostic, DoctorRuleId } from './diagnostics.js';\n\nconst PROFILE = 'app-manifest' as const;\n\nexport function analyzeSecretStoreManifest(\n infra: Record<string, unknown>,\n manifestPath: string,\n): DoctorDiagnostic[] {\n const diagnostics: DoctorDiagnostic[] = [];\n const secretStoreProvider = validateSecretStore(infra.secretStore, manifestPath, diagnostics);\n\n if (!isRecord(infra.auth)) return diagnostics;\n const { oauth } = infra.auth;\n if (oauth === undefined) return diagnostics;\n\n if (!isRecord(oauth)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n 'manifest.infra.auth.oauth must be a JSON object when present.',\n manifestPath,\n 'manifest.auth.oauth.valid-shape',\n ),\n );\n return diagnostics;\n }\n\n if (typeof oauth.enabled !== 'boolean') {\n diagnostics.push(\n diagnostic(\n oauth.enabled === undefined ? 'field-missing' : 'field-invalid',\n 'manifest.infra.auth.oauth.enabled must be a boolean.',\n manifestPath,\n 'manifest.auth.oauth.valid-shape',\n ),\n );\n }\n\n if (\n typeof oauth.callbackRoute !== 'string' ||\n oauth.callbackRoute.trim().length === 0 ||\n !oauth.callbackRoute.trim().startsWith('/')\n ) {\n diagnostics.push(\n diagnostic(\n oauth.callbackRoute === undefined ? 'field-missing' : 'field-invalid',\n 'manifest.infra.auth.oauth.callbackRoute must be a non-empty absolute route.',\n manifestPath,\n 'manifest.auth.oauth.callback-route.valid',\n ),\n );\n }\n\n if (!Array.isArray(oauth.providers)) {\n diagnostics.push(\n diagnostic(\n oauth.providers === undefined ? 'field-missing' : 'field-invalid',\n 'manifest.infra.auth.oauth.providers must be an array.',\n manifestPath,\n 'manifest.auth.oauth.providers.valid',\n ),\n );\n return diagnostics;\n }\n\n const enabledProviders = oauth.providers.filter(\n (provider) => isRecord(provider) && provider.enabled !== false,\n );\n\n if (oauth.enabled === true && enabledProviders.length === 0) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n 'OAuth is enabled, but no OAuth providers are enabled.',\n manifestPath,\n 'manifest.auth.oauth.providers.valid',\n ),\n );\n }\n\n if (enabledProviders.length > 0 && secretStoreProvider === null) {\n diagnostics.push(\n diagnostic(\n 'field-missing',\n 'Enabled OAuth providers require manifest.infra.secretStore.provider.',\n manifestPath,\n 'manifest.secret-store.provider.required',\n ),\n );\n }\n\n const seenProviderIds = new Set<string>();\n\n for (const [index, provider] of oauth.providers.entries()) {\n if (!isRecord(provider)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n `manifest.infra.auth.oauth.providers[${index}] must be a JSON object.`,\n manifestPath,\n 'manifest.auth.oauth.providers.valid',\n ),\n );\n continue;\n }\n\n const providerId = typeof provider.id === 'string' ? provider.id.trim() : '';\n if (providerId.length === 0) {\n diagnostics.push(\n diagnostic(\n provider.id === undefined ? 'field-missing' : 'field-invalid',\n `manifest.infra.auth.oauth.providers[${index}].id must be a non-empty string.`,\n manifestPath,\n 'manifest.auth.oauth.providers.valid',\n ),\n );\n } else if (seenProviderIds.has(providerId)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n `OAuth provider id \"${providerId}\" is configured more than once.`,\n manifestPath,\n 'manifest.auth.oauth.provider.duplicate',\n ),\n );\n } else {\n seenProviderIds.add(providerId);\n }\n\n for (const field of findForbiddenInlineSecretFields(provider)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n `Inline OAuth field \"${field}\" is forbidden. Store the value in the configured secret store and reference it with credentialsRef.`,\n manifestPath,\n 'manifest.auth.oauth.provider.inline-secret.disallowed',\n ),\n );\n }\n\n if (provider.enabled === false) continue;\n\n if (typeof provider.credentialsRef !== 'string') {\n diagnostics.push(\n diagnostic(\n provider.credentialsRef === undefined ? 'field-missing' : 'field-invalid',\n `Enabled OAuth provider \"${providerId || index}\" requires a credentialsRef.`,\n manifestPath,\n 'manifest.auth.oauth.provider.credentials-ref.required',\n ),\n );\n continue;\n }\n\n const refResult = normalizeSecretRef(provider.credentialsRef);\n if (!refResult.ok || refResult.data !== provider.credentialsRef) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n `OAuth provider \"${providerId || index}\" credentialsRef must be a canonical logical secret reference.`,\n manifestPath,\n 'manifest.auth.oauth.provider.credentials-ref.valid',\n ),\n );\n }\n }\n\n return diagnostics;\n}\n\nfunction validateSecretStore(\n value: unknown,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): string | null {\n if (value === undefined) return null;\n\n if (!isRecord(value)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n 'manifest.infra.secretStore must be a JSON object when present.',\n manifestPath,\n 'manifest.secret-store.valid-shape',\n ),\n );\n return null;\n }\n\n if (typeof value.provider !== 'string' || value.provider.trim().length === 0) {\n diagnostics.push(\n diagnostic(\n value.provider === undefined ? 'field-missing' : 'field-invalid',\n 'manifest.infra.secretStore.provider must be a non-empty string.',\n manifestPath,\n 'manifest.secret-store.provider.required',\n ),\n );\n return null;\n }\n\n const provider = value.provider.trim();\n if (!(SECRET_STORE_PROVIDERS as readonly string[]).includes(provider)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n `Unsupported secret-store provider \"${provider}\". Current providers: ${SECRET_STORE_PROVIDERS.join(', ')}.`,\n manifestPath,\n 'manifest.secret-store.provider.valid',\n ),\n );\n }\n\n return provider;\n}\n\nfunction diagnostic(\n code: DoctorDiagnostic['code'],\n message: string,\n path: string,\n ruleId: DoctorRuleId,\n): DoctorDiagnostic {\n return {\n code,\n message,\n path,\n profile: PROFILE,\n ruleId,\n severity: 'error',\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Executable Ankh provider and standalone CLI for lightweight repo and package compliance diagnostics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"doctor",
|
|
20
20
|
"cli",
|
|
21
21
|
"compliance",
|
|
22
|
+
"manifest",
|
|
23
|
+
"secret-store",
|
|
24
|
+
"oauth",
|
|
22
25
|
"typescript",
|
|
23
26
|
"bun"
|
|
24
27
|
],
|
|
@@ -74,7 +77,7 @@
|
|
|
74
77
|
"version-packages": "changeset version"
|
|
75
78
|
},
|
|
76
79
|
"dependencies": {
|
|
77
|
-
"@ankhorage/contracts": "^1.
|
|
80
|
+
"@ankhorage/contracts": "^2.1.0"
|
|
78
81
|
},
|
|
79
82
|
"devDependencies": {
|
|
80
83
|
"@ankhorage/ankh": "^0.4.0",
|
package/src/analysis.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
4
4
|
|
|
5
5
|
import type { DoctorDiagnostic, DoctorPolicyProfile, DoctorRuleId } from './diagnostics.js';
|
|
6
6
|
|
|
7
|
-
export type DoctorTargetCheck = 'package' | 'repo';
|
|
7
|
+
export type DoctorTargetCheck = 'manifest' | 'package' | 'repo';
|
|
8
8
|
export type DoctorTargetMode = 'fix' | 'package' | 'repo' | 'validate';
|
|
9
9
|
export type DoctorPlannedChangeKind =
|
|
10
10
|
'create-directory' | 'create-file' | 'update-json' | 'update-text';
|
package/src/cliLayoutAnalysis.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type DoctorAnalysisResult,
|
|
8
8
|
} from './analysis.js';
|
|
9
9
|
import type { DoctorDiagnostic, DoctorPolicyProfile } from './diagnostics.js';
|
|
10
|
+
import { analyzeAppManifestTarget } from './manifestAnalysis.js';
|
|
10
11
|
|
|
11
12
|
const LEGACY_ROOT_CLI_SOURCE = path.join('src', 'cli.ts');
|
|
12
13
|
const CANONICAL_CLI_INDEX_SOURCE = path.join('src', 'cli', 'index.ts');
|
|
@@ -62,6 +63,11 @@ interface DependencyEntry {
|
|
|
62
63
|
export async function analyzeDoctorTargetWithCliLayout(
|
|
63
64
|
request: DoctorAnalysisRequest,
|
|
64
65
|
): Promise<DoctorAnalysisResult> {
|
|
66
|
+
const manifestResult = await analyzeAppManifestTarget(request);
|
|
67
|
+
if (manifestResult !== null) {
|
|
68
|
+
return manifestResult;
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
const result = await analyzeDoctorTarget(request);
|
|
66
72
|
if (!result.hasPackageJson) {
|
|
67
73
|
return result;
|
package/src/commands.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AnkhCapabilityId, AnkhCommandDescriptor } from '@ankhorage/contracts/cli';
|
|
2
2
|
|
|
3
3
|
import type { DoctorAnalysisResult, DoctorPlannedChange, DoctorTargetMode } from './analysis.js';
|
|
4
|
-
import {
|
|
4
|
+
import { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
|
|
5
5
|
import type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';
|
|
6
6
|
import {
|
|
7
7
|
countErrorDiagnostics,
|
|
@@ -38,7 +38,7 @@ export interface DoctorCommandInvocation {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export interface DoctorCommandServices {
|
|
41
|
-
readonly analyzeTarget: typeof
|
|
41
|
+
readonly analyzeTarget: typeof analyzeDoctorTargetWithCliLayout;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export interface RunDoctorCommandOptions {
|
|
@@ -64,7 +64,8 @@ export const DOCTOR_COMMANDS = [
|
|
|
64
64
|
path: ['validate'],
|
|
65
65
|
capability: COMMAND_CAPABILITIES.validate,
|
|
66
66
|
mode: 'validate',
|
|
67
|
-
summary:
|
|
67
|
+
summary:
|
|
68
|
+
'Validate a repo, package, or app-manifest JSON path with the applicable Doctor profile.',
|
|
68
69
|
run: runValidateCommand,
|
|
69
70
|
},
|
|
70
71
|
{
|
|
@@ -147,7 +148,7 @@ export function renderRootHelp(version: string): string {
|
|
|
147
148
|
commandLines,
|
|
148
149
|
'',
|
|
149
150
|
'Path resolution:',
|
|
150
|
-
' Pass
|
|
151
|
+
' Pass a repo/package directory or app-manifest JSON file, or omit [path] to inspect the current directory.',
|
|
151
152
|
'',
|
|
152
153
|
].join('\n');
|
|
153
154
|
}
|
|
@@ -165,7 +166,7 @@ function renderCommandHelp(command: DoctorCommandDefinition): string {
|
|
|
165
166
|
` ankh ${DOCTOR_COMMAND_CATEGORY} ${command.path.join(' ')} [path]`,
|
|
166
167
|
'',
|
|
167
168
|
'Path resolution:',
|
|
168
|
-
' Pass
|
|
169
|
+
' Pass a repo/package directory or app-manifest JSON file, or omit [path] to inspect the current directory.',
|
|
169
170
|
'',
|
|
170
171
|
].join('\n');
|
|
171
172
|
}
|
|
@@ -174,7 +175,7 @@ function createDoctorCommandServices(
|
|
|
174
175
|
overrides: Partial<DoctorCommandServices> = {},
|
|
175
176
|
): DoctorCommandServices {
|
|
176
177
|
return {
|
|
177
|
-
analyzeTarget: overrides.analyzeTarget ??
|
|
178
|
+
analyzeTarget: overrides.analyzeTarget ?? analyzeDoctorTargetWithCliLayout,
|
|
178
179
|
};
|
|
179
180
|
}
|
|
180
181
|
|
package/src/diagnostics.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type DoctorDiagnosticCode =
|
|
|
2
2
|
| 'field-invalid'
|
|
3
3
|
| 'field-missing'
|
|
4
4
|
| 'invalid-ankh-metadata'
|
|
5
|
+
| 'invalid-app-manifest-json'
|
|
5
6
|
| 'invalid-package-json'
|
|
6
7
|
| 'missing-dependency'
|
|
7
8
|
| 'missing-path'
|
|
@@ -13,11 +14,37 @@ export type DoctorDiagnosticCode =
|
|
|
13
14
|
| 'unsupported-target';
|
|
14
15
|
|
|
15
16
|
export type DoctorDiagnosticSeverity = 'error' | 'warning';
|
|
16
|
-
export type DoctorPolicyProfile =
|
|
17
|
+
export type DoctorPolicyProfile =
|
|
18
|
+
'app-manifest' | 'integration-monorepo' | 'public-package' | 'unknown';
|
|
17
19
|
export type DoctorRuleId =
|
|
18
20
|
| 'integration.package.ankh.disallowed'
|
|
19
21
|
| 'integration.package.private.required'
|
|
20
22
|
| 'integration.package.workspaces.required'
|
|
23
|
+
| 'manifest.auth.authorization.engine.valid'
|
|
24
|
+
| 'manifest.auth.authorization.kind.valid'
|
|
25
|
+
| 'manifest.auth.authorization.valid-shape'
|
|
26
|
+
| 'manifest.auth.flow.post-sign-in.distinct'
|
|
27
|
+
| 'manifest.auth.flow.route.required'
|
|
28
|
+
| 'manifest.auth.flow.route.unique'
|
|
29
|
+
| 'manifest.auth.flow.route.valid'
|
|
30
|
+
| 'manifest.auth.flow.unauthorized-route.valid'
|
|
31
|
+
| 'manifest.auth.flow.valid-shape'
|
|
32
|
+
| 'manifest.auth.oauth.callback-route.valid'
|
|
33
|
+
| 'manifest.auth.oauth.provider.credentials-ref.required'
|
|
34
|
+
| 'manifest.auth.oauth.provider.credentials-ref.valid'
|
|
35
|
+
| 'manifest.auth.oauth.provider.duplicate'
|
|
36
|
+
| 'manifest.auth.oauth.provider.inline-secret.disallowed'
|
|
37
|
+
| 'manifest.auth.oauth.providers.valid'
|
|
38
|
+
| 'manifest.auth.oauth.valid-shape'
|
|
39
|
+
| 'manifest.auth.valid-shape'
|
|
40
|
+
| 'manifest.infra.valid-shape'
|
|
41
|
+
| 'manifest.json.readable'
|
|
42
|
+
| 'manifest.json.valid'
|
|
43
|
+
| 'manifest.root.valid-shape'
|
|
44
|
+
| 'manifest.secret-store.provider.required'
|
|
45
|
+
| 'manifest.secret-store.provider.valid'
|
|
46
|
+
| 'manifest.secret-store.valid-shape'
|
|
47
|
+
| 'manifest.settings.auth-flow.removed'
|
|
21
48
|
| 'package.ankh.capabilities.match-provider'
|
|
22
49
|
| 'package.ankh.present.valid-shape'
|
|
23
50
|
| 'package.ankh.provider-path.required'
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,11 @@ export type {
|
|
|
23
23
|
DoctorPolicyProfile,
|
|
24
24
|
DoctorRuleId,
|
|
25
25
|
} from './diagnostics.js';
|
|
26
|
+
export {
|
|
27
|
+
analyzeAppManifest,
|
|
28
|
+
analyzeAppManifestFile,
|
|
29
|
+
analyzeAppManifestTarget,
|
|
30
|
+
} from './manifestAnalysis.js';
|
|
26
31
|
export {
|
|
27
32
|
DOCTOR_CAPABILITIES,
|
|
28
33
|
DOCTOR_COMMAND_CATEGORY,
|