arkgate 3.7.0 → 3.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 +76 -1145
- package/README.md +59 -19
- package/bin/ark-check-runtime.mjs +1598 -0
- package/bin/ark-check.mjs +32 -1565
- package/bin/ark-mcp-runtime.mjs +1976 -0
- package/bin/ark-mcp.mjs +84 -1495
- package/bin/ark-shared.mjs +34 -38
- package/bin/ark.mjs +33 -66
- package/bin/lib/adapter-contract.mjs +161 -9
- package/bin/lib/agent-gates.mjs +1 -0
- package/bin/lib/analysis-completeness.mjs +28 -0
- package/bin/lib/analysis-engine.mjs +8 -8
- package/bin/lib/analysis-policy.mjs +27 -0
- package/bin/lib/architecture-scan.mjs +70 -357
- package/bin/lib/auto-patch.mjs +76 -8
- package/bin/lib/ci-and-commands.mjs +1 -1
- package/bin/lib/codex-home.mjs +43 -16
- package/bin/lib/design-delta.mjs +4 -0
- package/bin/lib/doctor-advisories.mjs +4 -3
- package/bin/lib/doctor-plan.mjs +40 -41
- package/bin/lib/enforcement-state.mjs +2 -0
- package/bin/lib/github-enforcement.mjs +443 -0
- package/bin/lib/hook-templates.mjs +12 -148
- package/bin/lib/html-report-advisories.mjs +1 -1
- package/bin/lib/html-report-depth.mjs +9 -0
- package/bin/lib/html-report.mjs +5 -5
- package/bin/lib/install-migrate.mjs +83 -79
- package/bin/lib/managed-upgrade.mjs +622 -0
- package/bin/lib/mcp-adoption.mjs +3 -1
- package/bin/lib/parse-health.mjs +6 -5
- package/bin/lib/port-proof.mjs +2 -2
- package/bin/lib/prepare-change.mjs +68 -38
- package/bin/lib/prepare-write.mjs +7 -1
- package/bin/lib/resident-doctor-client.mjs +55 -0
- package/bin/lib/resident-hook.mjs +247 -0
- package/bin/lib/resolved-candidate-facts.mjs +1160 -0
- package/bin/lib/scan-files.mjs +19 -6
- package/bin/lib/snippet-analysis.mjs +119 -0
- package/bin/lib/source-policy.mjs +24 -0
- package/bin/lib/typescript-host.mjs +15 -18
- package/bin/lib/unavailable-analysis.mjs +76 -0
- package/bin/lib/upgrade-command.mjs +115 -0
- package/bin/lib/weakest-link.mjs +21 -179
- package/bin/lib/write-path-capabilities.mjs +167 -16
- package/bin/lib/write-path-detect.mjs +3 -2
- package/dist/eslint/index.cjs +3 -3
- package/dist/eslint/index.d.ts +3 -0
- package/dist/eslint/index.js +3 -3
- package/dist/index.cjs +7 -7
- package/dist/index.d.ts +1073 -141
- package/dist/index.js +7 -7
- package/docs/agent-guide.md +103 -59
- package/docs/ai-gates.md +97 -16
- package/docs/demos/01-write-gate-self-correction.md +2 -2
- package/docs/enthusiast/README.md +10 -10
- package/docs/enthusiast/how-to-gallery-starter.md +2 -2
- package/docs/enthusiast/reference-commands.md +18 -1
- package/docs/enthusiast/tutorial-first-project.md +2 -2
- package/docs/package-surface.md +98 -12
- package/docs/typescript-support.md +109 -37
- package/package.json +32 -4
- package/schemas/ark.analysis-result.schema.json +159 -2
- package/schemas/ark.design-delta.schema.json +1 -0
- package/schemas/ark.enforcement-state.schema.json +84 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
- package/server.json +2 -2
- package/templates/skills/ark-explore.md +5 -5
- package/templates/skills/ark-fix.md +1 -1
- package/templates/skills/ark-runtime.md +15 -8
- package/templates/skills/ark-upgrade.md +122 -182
- package/bin/lib/ai-velocity.mjs +0 -293
- package/bin/lib/graph-cycles.mjs +0 -6
- package/bin/lib/safety-diagnostics.mjs +0 -284
- package/bin/lib/ts-resolve.mjs +0 -228
- package/dist/configTypes-DAPvBqK6.d.cts +0 -61
- package/dist/eslint/index.d.cts +0 -146
- package/dist/index.d.cts +0 -986
package/bin/lib/scan-files.mjs
CHANGED
|
@@ -56,19 +56,27 @@ function isInsideRoot(root, target) {
|
|
|
56
56
|
* while escaping links fail closed instead of reading arbitrary filesystem paths.
|
|
57
57
|
*/
|
|
58
58
|
export function walk(dir, files = [], options = {}) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
let state = options.state;
|
|
60
|
+
if (!state) {
|
|
61
|
+
if (options.root) options.observeInput?.(path.resolve(options.root), 'realpath');
|
|
62
|
+
state = {
|
|
63
|
+
root: options.root ? fs.realpathSync(options.root) : undefined,
|
|
64
|
+
visitedDirectories: new Set(),
|
|
65
|
+
visitedFiles: new Set(),
|
|
66
|
+
observeInput: options.observeInput,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
state.observeInput?.(path.resolve(dir), 'lstat');
|
|
64
70
|
const lstat = fs.lstatSync(dir, { throwIfNoEntry: false });
|
|
65
71
|
if (!lstat) return files;
|
|
72
|
+
state.observeInput?.(path.resolve(dir), 'realpath');
|
|
66
73
|
const resolved = fs.realpathSync(dir);
|
|
67
74
|
if (state.root && !isInsideRoot(state.root, resolved)) {
|
|
68
75
|
throw new Error(
|
|
69
76
|
`Refusing to scan symlink outside project root: ${dir} -> ${resolved}`
|
|
70
77
|
);
|
|
71
78
|
}
|
|
79
|
+
if (lstat.isSymbolicLink()) state.observeInput?.(path.resolve(dir), 'stat');
|
|
72
80
|
const stat = lstat.isSymbolicLink()
|
|
73
81
|
? fs.statSync(dir, { throwIfNoEntry: false })
|
|
74
82
|
: lstat;
|
|
@@ -87,8 +95,10 @@ export function walk(dir, files = [], options = {}) {
|
|
|
87
95
|
return files;
|
|
88
96
|
}
|
|
89
97
|
if (!stat.isDirectory()) return files;
|
|
98
|
+
state.onDirectory?.(dir, resolved);
|
|
90
99
|
if (state.visitedDirectories.has(resolved)) return files;
|
|
91
100
|
state.visitedDirectories.add(resolved);
|
|
101
|
+
state.observeInput?.(path.resolve(dir), 'directory');
|
|
92
102
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
93
103
|
const full = path.join(dir, entry.name);
|
|
94
104
|
if (entry.isDirectory()) {
|
|
@@ -105,11 +115,14 @@ export function walk(dir, files = [], options = {}) {
|
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
/** Walk include roots then drop codegen / config.exclude (universal scan filter). */
|
|
108
|
-
export function collectGovernedFiles(root, config) {
|
|
118
|
+
export function collectGovernedFiles(root, config, options = {}) {
|
|
119
|
+
options.observeInput?.(path.resolve(root), 'realpath');
|
|
109
120
|
const state = {
|
|
110
121
|
root: fs.realpathSync(root),
|
|
111
122
|
visitedDirectories: new Set(),
|
|
112
123
|
visitedFiles: new Set(),
|
|
124
|
+
onDirectory: options.onDirectory,
|
|
125
|
+
observeInput: options.observeInput,
|
|
113
126
|
};
|
|
114
127
|
const raw = (config.include ?? []).flatMap((entry) =>
|
|
115
128
|
walk(path.join(root, entry), [], { state })
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Fail-closed completeness evidence for one proposed source snippet. */
|
|
2
|
+
import { ANALYSIS_COMPLETENESS } from './analysis-completeness.mjs';
|
|
3
|
+
|
|
4
|
+
function finding(ruleId, message, file, nextAction) {
|
|
5
|
+
return {
|
|
6
|
+
ruleId,
|
|
7
|
+
code: ruleId,
|
|
8
|
+
message,
|
|
9
|
+
...(file ? { file, filePath: file } : {}),
|
|
10
|
+
nextAction,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateSnippetAnalysis({ gate, ts, source, context = {} }) {
|
|
15
|
+
const observed = gate.validate(source, context);
|
|
16
|
+
const base = {
|
|
17
|
+
valid: Boolean(observed.lexicalValid ?? observed.valid),
|
|
18
|
+
violations: Array.isArray(observed.violations) ? observed.violations : [],
|
|
19
|
+
};
|
|
20
|
+
const file = context.filePath;
|
|
21
|
+
|
|
22
|
+
if (!ts || typeof ts.createSourceFile !== 'function') {
|
|
23
|
+
return {
|
|
24
|
+
mode: 'lexical-compatibility',
|
|
25
|
+
valid: false,
|
|
26
|
+
lexicalValid: false,
|
|
27
|
+
completeness: ANALYSIS_COMPLETENESS.unavailable,
|
|
28
|
+
completenessReasons: [
|
|
29
|
+
{
|
|
30
|
+
code: 'ANALYSIS_HOST_UNAVAILABLE',
|
|
31
|
+
message: 'No API-compatible TypeScript host parsed the proposed source.',
|
|
32
|
+
...(file ? { file } : {}),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
violations: [
|
|
36
|
+
...base.violations,
|
|
37
|
+
finding(
|
|
38
|
+
'ANALYSIS_HOST_UNAVAILABLE',
|
|
39
|
+
'Analysis unavailable: no API-compatible TypeScript host parsed the proposed source.',
|
|
40
|
+
file,
|
|
41
|
+
'Restore ArkGate\'s TypeScript analysis host, then validate the complete source again.'
|
|
42
|
+
),
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const parsed = ts.createSourceFile(
|
|
49
|
+
file || 'generated.ts',
|
|
50
|
+
source,
|
|
51
|
+
ts.ScriptTarget.Latest,
|
|
52
|
+
true
|
|
53
|
+
);
|
|
54
|
+
if (!Array.isArray(parsed.parseDiagnostics)) throw new Error('parse diagnostics unavailable');
|
|
55
|
+
const diagnosticCount = parsed.parseDiagnostics.length;
|
|
56
|
+
if (diagnosticCount > 0) {
|
|
57
|
+
return {
|
|
58
|
+
mode: 'lexical-compatibility',
|
|
59
|
+
valid: false,
|
|
60
|
+
lexicalValid: false,
|
|
61
|
+
completeness: ANALYSIS_COMPLETENESS.partial,
|
|
62
|
+
completenessReasons: [
|
|
63
|
+
{
|
|
64
|
+
code: 'ANALYSIS_PARSE_INCOMPLETE',
|
|
65
|
+
message: `The proposed source has ${diagnosticCount} parse diagnostic(s).`,
|
|
66
|
+
...(file ? { file } : {}),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
violations: [
|
|
70
|
+
...base.violations,
|
|
71
|
+
finding(
|
|
72
|
+
'ANALYSIS_PARSE_INCOMPLETE',
|
|
73
|
+
`Analysis partial: proposed source has ${diagnosticCount} parse diagnostic(s).`,
|
|
74
|
+
file,
|
|
75
|
+
'Fix the syntax until the TypeScript parser reports zero diagnostics, then validate again.'
|
|
76
|
+
),
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
...base,
|
|
82
|
+
mode: 'lexical-compatibility',
|
|
83
|
+
valid: false,
|
|
84
|
+
lexicalValid: base.valid,
|
|
85
|
+
completeness: ANALYSIS_COMPLETENESS.partial,
|
|
86
|
+
completenessReasons: [
|
|
87
|
+
{
|
|
88
|
+
code: 'LEXICAL_EVIDENCE_INCOMPLETE',
|
|
89
|
+
message:
|
|
90
|
+
'Single-file validation cannot prove project module resolution or complete candidate evidence; use ark_prepare_change for a parity-capable verdict.',
|
|
91
|
+
...(file ? { file } : {}),
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
} catch {
|
|
96
|
+
return {
|
|
97
|
+
mode: 'lexical-compatibility',
|
|
98
|
+
valid: false,
|
|
99
|
+
lexicalValid: false,
|
|
100
|
+
completeness: ANALYSIS_COMPLETENESS.unavailable,
|
|
101
|
+
completenessReasons: [
|
|
102
|
+
{
|
|
103
|
+
code: 'ANALYSIS_HOST_UNAVAILABLE',
|
|
104
|
+
message: 'The TypeScript host could not parse the proposed source.',
|
|
105
|
+
...(file ? { file } : {}),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
violations: [
|
|
109
|
+
...base.violations,
|
|
110
|
+
finding(
|
|
111
|
+
'ANALYSIS_HOST_UNAVAILABLE',
|
|
112
|
+
'Analysis unavailable: the TypeScript host could not parse the proposed source.',
|
|
113
|
+
file,
|
|
114
|
+
'Restore ArkGate\'s TypeScript analysis host, then validate the complete source again.'
|
|
115
|
+
),
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -12,6 +12,30 @@ export const SOURCE_POLICY_MESSAGES = {
|
|
|
12
12
|
RAW_EVENT_PUBLISH: 'Publish through a registered intent creator; raw event objects or intent strings bypass Ark contracts and tooling.',
|
|
13
13
|
PUBLISH_MISSING_SOURCE: 'Strict Ark publish calls must include metadata.source.',
|
|
14
14
|
};
|
|
15
|
+
export const DEFAULT_INTENT_PREFIXES = Object.freeze([
|
|
16
|
+
{ layer: 'DomainModel', prefixes: ['Domain.'] },
|
|
17
|
+
{ layer: 'ApplicationOrchestration', prefixes: ['Application.'] },
|
|
18
|
+
{ layer: 'PersistenceAdapters', prefixes: ['Adapter.Persistence.', 'Adapter.Repository.'] },
|
|
19
|
+
{ layer: 'IntegrationAdapters', prefixes: ['Adapter.Integration.', 'Adapter.External.'] },
|
|
20
|
+
{ layer: 'WorkflowSagaEngine', prefixes: ['Workflow.'] },
|
|
21
|
+
{ layer: 'BackgroundJobsScheduling', prefixes: ['Job.'] },
|
|
22
|
+
{ layer: 'PresentationAdapters', prefixes: ['Presentation.', 'Adapter.Presentation.', 'Adapter.Api.'] },
|
|
23
|
+
{ layer: 'ReportingReadModels', prefixes: ['Reporting.'] },
|
|
24
|
+
{ layer: 'ExtensibilityMetadata', prefixes: ['Metadata.'] },
|
|
25
|
+
{ layer: 'SecurityAuditObservability', prefixes: ['Security.', 'Audit.', 'Observability.'] },
|
|
26
|
+
{ layer: 'Kernel', prefixes: ['Kernel.'] },
|
|
27
|
+
]);
|
|
28
|
+
/** Longest matching prefix wins; declaration order resolves an identical-prefix tie. */
|
|
29
|
+
export function resolveIntentLayer(intent, layers) {
|
|
30
|
+
const candidates = layers.flatMap((layer, layerIndex) => (layer.prefixes ?? layer.intentPrefixes ?? []).map((prefix) => ({
|
|
31
|
+
layer: layer.name,
|
|
32
|
+
layerIndex,
|
|
33
|
+
prefix: prefix.endsWith('.') ? prefix : `${prefix}.`,
|
|
34
|
+
})));
|
|
35
|
+
return candidates
|
|
36
|
+
.filter(({ prefix }) => intent.startsWith(prefix))
|
|
37
|
+
.sort((left, right) => right.prefix.length - left.prefix.length || left.layerIndex - right.layerIndex)[0]?.layer;
|
|
38
|
+
}
|
|
15
39
|
export function looksLikeArkIntent(value) {
|
|
16
40
|
return /^(Domain|Application|Adapter|Workflow|Job|Presentation|Reporting|Metadata|Security|Audit|Observability|Kernel)\.[A-Za-z0-9_.]+$/.test(value);
|
|
17
41
|
}
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* TypeScript host resolution for architecture scan (API-compatible loader).
|
|
3
3
|
*/
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
import { usableTypescript, typescriptUsabilityHint } from '../ark-shared.mjs';
|
|
6
|
-
import { __arkCheckCli } from './gate-files.mjs';
|
|
7
7
|
|
|
8
8
|
export async function loadTypeScript(root) {
|
|
9
|
-
const { createRequire } = await import('node:module');
|
|
10
9
|
const loaders = [];
|
|
11
10
|
try {
|
|
12
11
|
const req = createRequire(path.join(root, 'package.json'));
|
|
@@ -24,15 +23,15 @@ export async function loadTypeScript(root) {
|
|
|
24
23
|
} catch {
|
|
25
24
|
/* project has no package.json resolvable tree */
|
|
26
25
|
}
|
|
27
|
-
//
|
|
26
|
+
// Dedicated production fallback: a different package identity cannot dedupe to project TS7.
|
|
28
27
|
try {
|
|
29
|
-
const req = createRequire(
|
|
28
|
+
const req = createRequire(import.meta.url);
|
|
30
29
|
loaders.push({
|
|
31
|
-
label: 'arkgate',
|
|
32
|
-
load: () => req('typescript'),
|
|
30
|
+
label: 'arkgate-fallback',
|
|
31
|
+
load: () => req('typescript-ark-host'),
|
|
33
32
|
resolvePath: () => {
|
|
34
33
|
try {
|
|
35
|
-
return req.resolve('typescript');
|
|
34
|
+
return req.resolve('typescript-ark-host');
|
|
36
35
|
} catch {
|
|
37
36
|
return null;
|
|
38
37
|
}
|
|
@@ -41,15 +40,6 @@ export async function loadTypeScript(root) {
|
|
|
41
40
|
} catch {
|
|
42
41
|
/* ark install tree unavailable */
|
|
43
42
|
}
|
|
44
|
-
loaders.push({
|
|
45
|
-
label: 'import',
|
|
46
|
-
load: async () => {
|
|
47
|
-
const m = await import('typescript');
|
|
48
|
-
return m;
|
|
49
|
-
},
|
|
50
|
-
resolvePath: () => null,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
43
|
let projectRejected = null;
|
|
54
44
|
const triedPaths = new Set();
|
|
55
45
|
for (const { label, load, resolvePath } of loaders) {
|
|
@@ -74,15 +64,22 @@ export async function loadTypeScript(root) {
|
|
|
74
64
|
ts,
|
|
75
65
|
source: label,
|
|
76
66
|
version,
|
|
67
|
+
...(resolved ? { resolvedPath: resolved } : {}),
|
|
77
68
|
...(projectRejected ? { fallbackReason: projectRejected } : {}),
|
|
78
69
|
};
|
|
79
70
|
}
|
|
80
71
|
if (label === 'project' && mod) {
|
|
81
|
-
projectRejected = `project
|
|
72
|
+
projectRejected = `project TypeScript is not API-compatible (${typescriptUsabilityHint(mod)}); using ArkGate's physically independent TypeScript 6 JS-API host. See docs/typescript-support.md.`;
|
|
82
73
|
}
|
|
83
74
|
} catch {
|
|
84
75
|
/* try next loader */
|
|
85
76
|
}
|
|
86
77
|
}
|
|
87
|
-
return
|
|
78
|
+
return {
|
|
79
|
+
ts: null,
|
|
80
|
+
source: 'unavailable',
|
|
81
|
+
reason:
|
|
82
|
+
'ArkGate could not load either the project TypeScript API or its independent TypeScript 6 fallback.',
|
|
83
|
+
...(projectRejected ? { fallbackReason: projectRejected } : {}),
|
|
84
|
+
};
|
|
88
85
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Report TypeScript-host unavailability without a false-green architecture result. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { computeCoverage, runDoctor, runPlan } from './doctor-plan.mjs';
|
|
5
|
+
import { ANALYSIS_COMPLETENESS } from './analysis-completeness.mjs';
|
|
6
|
+
import { summarizeParseHealth } from './parse-health.mjs';
|
|
7
|
+
|
|
8
|
+
export function reportUnavailableAnalysis({
|
|
9
|
+
root,
|
|
10
|
+
config,
|
|
11
|
+
rules,
|
|
12
|
+
files,
|
|
13
|
+
args,
|
|
14
|
+
message,
|
|
15
|
+
nextAction,
|
|
16
|
+
createResult,
|
|
17
|
+
}) {
|
|
18
|
+
const completeness = ANALYSIS_COMPLETENESS.unavailable;
|
|
19
|
+
const parseHealth = summarizeParseHealth();
|
|
20
|
+
const finding = {
|
|
21
|
+
ruleId: 'ANALYSIS_HOST_UNAVAILABLE',
|
|
22
|
+
message,
|
|
23
|
+
nextAction,
|
|
24
|
+
file: '<analysis-host>',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (args.plan) {
|
|
28
|
+
const coverage = computeCoverage(root, config, files, rules);
|
|
29
|
+
runPlan(root, [], args.json, coverage.governed.percent, coverage.governed.totalFiles, {
|
|
30
|
+
config,
|
|
31
|
+
files,
|
|
32
|
+
coverage,
|
|
33
|
+
completeness,
|
|
34
|
+
});
|
|
35
|
+
process.exitCode = 2;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (args.doctor) {
|
|
40
|
+
const configPath = path.isAbsolute(args.config) ? args.config : path.join(root, args.config);
|
|
41
|
+
runDoctor(root, config, files, rules, [], args.json, {
|
|
42
|
+
configPath,
|
|
43
|
+
configMissing: !fs.existsSync(configPath),
|
|
44
|
+
parseHealth,
|
|
45
|
+
completeness,
|
|
46
|
+
});
|
|
47
|
+
process.exitCode = 2;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const adapterResult = createResult({
|
|
52
|
+
valid: false,
|
|
53
|
+
completeness,
|
|
54
|
+
completenessReasons: [
|
|
55
|
+
{
|
|
56
|
+
code: finding.ruleId,
|
|
57
|
+
message: finding.message,
|
|
58
|
+
file: finding.file,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
mode: 'resolved-candidate-facts',
|
|
62
|
+
violations: [finding],
|
|
63
|
+
});
|
|
64
|
+
if (args.json) {
|
|
65
|
+
console.log(JSON.stringify({
|
|
66
|
+
...adapterResult,
|
|
67
|
+
ok: false,
|
|
68
|
+
violations: [finding],
|
|
69
|
+
warnings: [],
|
|
70
|
+
}, null, 2));
|
|
71
|
+
} else {
|
|
72
|
+
console.error(message);
|
|
73
|
+
console.error(`Next action: ${nextAction}`);
|
|
74
|
+
}
|
|
75
|
+
process.exitCode = 2;
|
|
76
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** Read-only-first `ark upgrade` orchestration. Managed identity logic lives separately. */
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
applyManagedUpgrade,
|
|
8
|
+
managedUpgradeJson,
|
|
9
|
+
planManagedUpgrade,
|
|
10
|
+
renderManagedUpgrade,
|
|
11
|
+
} from './managed-upgrade.mjs';
|
|
12
|
+
|
|
13
|
+
function installedCli(root) {
|
|
14
|
+
const requireFromProject = createRequire(path.join(root, 'package.json'));
|
|
15
|
+
const packageJson = requireFromProject.resolve('arkgate/package.json');
|
|
16
|
+
return path.join(path.dirname(packageJson), 'bin', 'ark.mjs');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function previewArgs(args) {
|
|
20
|
+
const next = ['upgrade', '--root', args.root, '--no-install'];
|
|
21
|
+
if (args.tools) next.push('--tools', args.tools);
|
|
22
|
+
if (args.acceptConflicts) next.push('--accept-conflicts');
|
|
23
|
+
if (!args.strict) next.push('--no-strict');
|
|
24
|
+
if (args.json) next.push('--json');
|
|
25
|
+
return next;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function quote(value) {
|
|
29
|
+
const text = String(value);
|
|
30
|
+
return /^[A-Za-z0-9_./:@=-]+$/.test(text) ? text : `'${text.replace(/'/g, `'"'"'`)}'`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function nextCommand(args, planDigest) {
|
|
34
|
+
const parts = ['ark', 'upgrade', '--apply', '--root', args.root];
|
|
35
|
+
if (!args.install) parts.push('--no-install');
|
|
36
|
+
if (!args.install && planDigest) parts.push('--plan-digest', planDigest);
|
|
37
|
+
if (args.tools) parts.push('--tools', args.tools);
|
|
38
|
+
if (args.acceptConflicts) parts.push('--accept-conflicts');
|
|
39
|
+
if (!args.strict) parts.push('--no-strict');
|
|
40
|
+
if (args.json) parts.push('--json');
|
|
41
|
+
return parts.map(quote).join(' ');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function verify(root, json, arkCheck, runArkCheck) {
|
|
45
|
+
const args = ['--root', root, '--config', 'ark.config.json', '--strict-merge'];
|
|
46
|
+
if (!json) return { exitCode: runArkCheck(args, { cwd: root }) };
|
|
47
|
+
const result = spawnSync(process.execPath, [arkCheck, ...args, '--json'], {
|
|
48
|
+
cwd: root,
|
|
49
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
});
|
|
52
|
+
return { exitCode: result.status ?? 1, stderr: result.stderr?.trim() || undefined };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function runUpgradeCommand(args, dependencies) {
|
|
56
|
+
const root = args.root;
|
|
57
|
+
if (args.apply && args.install) {
|
|
58
|
+
const [command, commandArgs] = dependencies.packageInstallArgv(root);
|
|
59
|
+
if (!args.json) console.log(`Updating ArkGate: ${command} ${commandArgs.join(' ')}`);
|
|
60
|
+
const install = spawnSync(command, commandArgs, {
|
|
61
|
+
cwd: root,
|
|
62
|
+
stdio: args.json ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
});
|
|
65
|
+
const exitCode = install.status ?? 1;
|
|
66
|
+
if (exitCode !== 0) {
|
|
67
|
+
if (args.json && install.stderr) console.error(install.stderr.trim());
|
|
68
|
+
console.error(
|
|
69
|
+
`Package update failed (exit ${exitCode}). Fix the install and re-run, or use ` +
|
|
70
|
+
'`ark upgrade --no-install` against the installed version.'
|
|
71
|
+
);
|
|
72
|
+
return exitCode;
|
|
73
|
+
}
|
|
74
|
+
return spawnSync(process.execPath, [installedCli(root), ...previewArgs(args)], {
|
|
75
|
+
cwd: root,
|
|
76
|
+
stdio: 'inherit',
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
}).status ?? 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const plan = planManagedUpgrade(root, {
|
|
82
|
+
tools: args.tools,
|
|
83
|
+
acceptConflicts: args.acceptConflicts,
|
|
84
|
+
});
|
|
85
|
+
if (!args.apply) {
|
|
86
|
+
const command = nextCommand(args, plan.planDigest);
|
|
87
|
+
if (args.json) console.log(managedUpgradeJson(plan, { nextCommand: command }));
|
|
88
|
+
else {
|
|
89
|
+
renderManagedUpgrade(plan, {
|
|
90
|
+
next: args.install
|
|
91
|
+
? `Update the package and recompute this preview with: ${command}`
|
|
92
|
+
: `Apply the exact preview with: ${command}`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const applied = applyManagedUpgrade(root, plan, args.planDigest);
|
|
99
|
+
if (applied.blocked) {
|
|
100
|
+
if (args.json) console.log(JSON.stringify(applied, null, 2));
|
|
101
|
+
else renderManagedUpgrade(applied, {
|
|
102
|
+
next: 'Preview again with --accept-conflicts, then use that preview\'s exact next command.',
|
|
103
|
+
});
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
const verification = args.strict
|
|
107
|
+
? { mode: 'strict-merge', ...verify(root, args.json, dependencies.arkCheck, dependencies.runArkCheck) }
|
|
108
|
+
: { mode: 'skipped', exitCode: 0 };
|
|
109
|
+
if (args.json) console.log(JSON.stringify({ ...applied, verification }, null, 2));
|
|
110
|
+
else {
|
|
111
|
+
renderManagedUpgrade(applied);
|
|
112
|
+
if (!args.strict) console.log('Architecture verification skipped (--no-strict).');
|
|
113
|
+
}
|
|
114
|
+
return verification.exitCode;
|
|
115
|
+
}
|