arkgate 2.11.0 → 2.13.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 +147 -0
- package/README.md +70 -41
- package/bin/ark-check.mjs +95 -36
- package/bin/ark-mcp.mjs +11 -5
- package/bin/ark-shared.mjs +88 -56
- package/bin/ark.mjs +97 -29
- package/bin/lib/agent-gates.mjs +79 -2093
- package/bin/lib/architecture-scan.mjs +8 -0
- package/bin/lib/ci-and-commands.mjs +392 -0
- package/bin/lib/codex-home.mjs +7 -0
- package/bin/lib/config-contract.mjs +331 -0
- package/bin/lib/deploy-path.mjs +205 -0
- package/bin/lib/doctor-plan.mjs +43 -16
- package/bin/lib/enforcement-profiles.mjs +97 -0
- package/bin/lib/gate-files.mjs +223 -0
- package/bin/lib/hook-templates.mjs +99 -0
- package/bin/lib/host-support-matrix.mjs +77 -0
- package/bin/lib/install-migrate.mjs +473 -0
- package/bin/lib/mcp-adoption.mjs +455 -0
- package/bin/lib/open-html.mjs +75 -0
- package/bin/lib/presets.mjs +6 -2
- package/bin/lib/safety-diagnostics.mjs +31 -11
- package/bin/lib/skill-install.mjs +323 -0
- package/bin/lib/ts-resolve.mjs +2 -1
- package/bin/lib/typescript-host.mjs +88 -0
- package/bin/lib/weakest-link.mjs +417 -0
- package/bin/lib/write-path-capabilities.mjs +182 -0
- package/bin/lib/write-path-detect.mjs +101 -0
- package/dist/configContract-iBLxx5Tz.d.cts +53 -0
- package/dist/configContract-iBLxx5Tz.d.ts +53 -0
- package/dist/eslint/index.cjs +375 -13
- package/dist/eslint/index.cjs.map +1 -1
- package/dist/eslint/index.d.cts +30 -20
- package/dist/eslint/index.d.ts +30 -20
- package/dist/eslint/index.js +375 -13
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.cjs +723 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -5
- package/dist/index.d.ts +95 -5
- package/dist/index.js +716 -61
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +150 -42
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +2 -1
- package/dist/nestjs/index.d.ts +2 -1
- package/dist/nestjs/index.js +150 -42
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +723 -61
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +3 -2
- package/dist/runtime/index.d.ts +3 -2
- package/dist/runtime/index.js +716 -61
- package/dist/runtime/index.js.map +1 -1
- package/dist/{types-BZ17b9i5.d.cts → types-BxBwnBpC.d.cts} +9 -36
- package/dist/{types-BZ17b9i5.d.ts → types-Wcs_l1_J.d.ts} +9 -36
- package/docs/agent-guide.md +43 -21
- package/docs/ai-gates.md +53 -18
- package/docs/configuration.md +97 -0
- package/docs/enthusiast/README.md +3 -3
- package/docs/enthusiast/how-to-agent-gates.md +7 -3
- package/docs/migrate-from-ark-runtime-kernel.md +3 -0
- package/docs/package-surface.md +22 -10
- package/docs/production-hardening.md +15 -2
- package/docs/threat-model.md +65 -0
- package/docs/typescript-support.md +3 -3
- package/package.json +15 -2
- package/schemas/ark.config.schema.json +750 -0
- package/server.json +2 -2
- package/templates/hooks/pre-commit-ark +37 -0
- package/templates/skills/ark-autopilot.md +77 -45
- package/templates/skills/ark-coverage.md +2 -2
- package/templates/skills/ark-explain.md +2 -1
- package/templates/skills/ark-explore.md +135 -34
- package/templates/skills/ark-runtime.md +8 -5
- package/templates/skills/ark-upgrade.md +36 -16
- package/tests/fixtures/ts-consumer/ark.config.json +2 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool detection, skill templates, stamping, and skill freshness gaps.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { codexPromptsDir } from './codex-home.mjs';
|
|
7
|
+
import { __packageRoot, readJson } from './gate-files.mjs';
|
|
8
|
+
|
|
9
|
+
export function normalizeToolsList(tools) {
|
|
10
|
+
if (tools == null) return [];
|
|
11
|
+
if (Array.isArray(tools)) {
|
|
12
|
+
return tools
|
|
13
|
+
.flatMap((t) => String(t).split(','))
|
|
14
|
+
.map((t) => t.trim().toLowerCase())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
if (typeof tools === 'string') {
|
|
18
|
+
return tools
|
|
19
|
+
.split(',')
|
|
20
|
+
.map((t) => t.trim().toLowerCase())
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function envTruthy(v) {
|
|
27
|
+
if (v == null || v === '') return false;
|
|
28
|
+
const s = String(v).trim().toLowerCase();
|
|
29
|
+
return s !== '0' && s !== 'false' && s !== 'no' && s !== 'off';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Best-effort active agent host for this process (session host).
|
|
34
|
+
* Prefer ARK_ACTIVE_HOST when set. Do NOT treat CODEX_HOME alone as Codex —
|
|
35
|
+
* that dir exists for anyone who installed Codex, even when running Grok/Claude.
|
|
36
|
+
*
|
|
37
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
38
|
+
* @returns {string|null} tool id (claude|cursor|codex|grok|…) or null if unknown
|
|
39
|
+
*/
|
|
40
|
+
export function detectActiveAgentHost(env = process.env) {
|
|
41
|
+
const explicit = String(env.ARK_ACTIVE_HOST || '')
|
|
42
|
+
.trim()
|
|
43
|
+
.toLowerCase();
|
|
44
|
+
if (explicit) return explicit;
|
|
45
|
+
|
|
46
|
+
// Grok / xAI Build
|
|
47
|
+
if (
|
|
48
|
+
envTruthy(env.GROK_BUILD) ||
|
|
49
|
+
envTruthy(env.XAI_GROK) ||
|
|
50
|
+
env.GROK_WORKSPACE_ROOT ||
|
|
51
|
+
env.GROK_SESSION_ID
|
|
52
|
+
) {
|
|
53
|
+
return 'grok';
|
|
54
|
+
}
|
|
55
|
+
// Claude Code
|
|
56
|
+
if (
|
|
57
|
+
env.CLAUDE_PROJECT_DIR ||
|
|
58
|
+
envTruthy(env.CLAUDE_CODE) ||
|
|
59
|
+
envTruthy(env.CLAUDECODE) ||
|
|
60
|
+
env.CLAUDE_CODE_ENTRYPOINT
|
|
61
|
+
) {
|
|
62
|
+
return 'claude';
|
|
63
|
+
}
|
|
64
|
+
// Cursor agent
|
|
65
|
+
if (env.CURSOR_TRACE_ID || env.CURSOR_AGENT || envTruthy(env.CURSOR_AGENT_CLI)) {
|
|
66
|
+
return 'cursor';
|
|
67
|
+
}
|
|
68
|
+
// Codex session — never CODEX_HOME alone (see above)
|
|
69
|
+
if (
|
|
70
|
+
envTruthy(env.CODEX_SANDBOX) ||
|
|
71
|
+
env.CODEX_THREAD_ID ||
|
|
72
|
+
envTruthy(env.CODEX_CI) ||
|
|
73
|
+
env.CODEX_SESSION_ID
|
|
74
|
+
) {
|
|
75
|
+
return 'codex';
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* True when Codex home / MCP / prompts should be treated as an urgent concern
|
|
82
|
+
* for this process. Non-Codex hosts (Grok, Claude, Cursor, …) defer Codex debt.
|
|
83
|
+
*
|
|
84
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
85
|
+
*/
|
|
86
|
+
export function codexConcernIsActive(env = process.env) {
|
|
87
|
+
return detectActiveAgentHost(env) === 'codex';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveTools(args) {
|
|
91
|
+
const explicit = normalizeToolsList(args.tools);
|
|
92
|
+
if (explicit.length > 0) {
|
|
93
|
+
return { tools: new Set(explicit), source: 'explicit' };
|
|
94
|
+
}
|
|
95
|
+
const root = args.root;
|
|
96
|
+
const detected = new Set();
|
|
97
|
+
if (fs.existsSync(path.join(root, '.claude'))) detected.add('claude');
|
|
98
|
+
if (fs.existsSync(path.join(root, '.cursor'))) detected.add('cursor');
|
|
99
|
+
if (fs.existsSync(path.join(root, '.codex'))) detected.add('codex');
|
|
100
|
+
if (fs.existsSync(path.join(root, '.grok'))) detected.add('grok');
|
|
101
|
+
if (fs.existsSync(path.join(root, '.windsurf'))) detected.add('windsurf');
|
|
102
|
+
// .clinerules can also be a single FILE (older Cline convention); only a directory
|
|
103
|
+
// can receive .clinerules/ark.md, so a file must not trigger detection.
|
|
104
|
+
if (fs.statSync(path.join(root, '.clinerules'), { throwIfNoEntry: false })?.isDirectory()) {
|
|
105
|
+
detected.add('cline');
|
|
106
|
+
}
|
|
107
|
+
if (fs.existsSync(path.join(root, '.kiro'))) detected.add('kiro');
|
|
108
|
+
if (fs.existsSync(path.join(root, '.roo'))) detected.add('roo');
|
|
109
|
+
if (fs.existsSync(path.join(root, '.continue'))) detected.add('continue');
|
|
110
|
+
if (fs.existsSync(path.join(root, '.gemini'))) detected.add('gemini');
|
|
111
|
+
// copilot has no reliable directory signal (.github exists in most repos),
|
|
112
|
+
// so it is explicit-only via --tools.
|
|
113
|
+
// Host signals: Grok Build / xAI agents often have no project `.grok/` yet but
|
|
114
|
+
// set an env marker (or run with GROK_*). Include Grok so skills install there.
|
|
115
|
+
if (
|
|
116
|
+
process.env.GROK_BUILD === '1' ||
|
|
117
|
+
process.env.GROK_BUILD === 'true' ||
|
|
118
|
+
process.env.XAI_GROK === '1' ||
|
|
119
|
+
process.env.XAI_GROK === 'true'
|
|
120
|
+
) {
|
|
121
|
+
detected.add('grok');
|
|
122
|
+
}
|
|
123
|
+
// No signal at all: fall back to a complete starter set including Grok (field
|
|
124
|
+
// log: default claude+cursor+codex silently omitted Grok skills for Grok hosts).
|
|
125
|
+
if (detected.size === 0) {
|
|
126
|
+
return { tools: new Set(['claude', 'cursor', 'codex', 'grok']), source: 'default' };
|
|
127
|
+
}
|
|
128
|
+
return { tools: detected, source: 'detected' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const KNOWN_TOOLS = [
|
|
132
|
+
'claude',
|
|
133
|
+
'cursor',
|
|
134
|
+
'codex',
|
|
135
|
+
'grok',
|
|
136
|
+
'windsurf',
|
|
137
|
+
'cline',
|
|
138
|
+
'copilot',
|
|
139
|
+
'kiro',
|
|
140
|
+
'roo',
|
|
141
|
+
'continue',
|
|
142
|
+
'gemini',
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
// One canonical markdown per skill (templates/skills/*.md, shipped in the npm
|
|
146
|
+
// package); installed into each tool's slash-command location. The YAML
|
|
147
|
+
// frontmatter (name/description) is understood or harmlessly ignored by every
|
|
148
|
+
// host. Kiro has no command mechanism — its steering rule file is the only gate.
|
|
149
|
+
export const SKILL_TOOL_TARGETS = {
|
|
150
|
+
claude: (name) => `.claude/skills/${name}/SKILL.md`,
|
|
151
|
+
cursor: (name) => `.cursor/commands/${name}.md`,
|
|
152
|
+
codex: (name) => `.codex/prompts/${name}.md`,
|
|
153
|
+
// Grok Build: project skills at .grok/skills/<name>/SKILL.md (slash-invocable).
|
|
154
|
+
grok: (name) => `.grok/skills/${name}/SKILL.md`,
|
|
155
|
+
windsurf: (name) => `.windsurf/workflows/${name}.md`,
|
|
156
|
+
cline: (name) => `.clinerules/workflows/${name}.md`,
|
|
157
|
+
copilot: (name) => `.github/prompts/${name}.prompt.md`,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// The version of the arkgate package these bins ship with. Used to
|
|
161
|
+
// stamp installed skills so a normal ark-check can tell "outdated skill from an
|
|
162
|
+
// older Ark" apart from "user-customized skill" — the stamp moves with the
|
|
163
|
+
// package, editing the body doesn't.
|
|
164
|
+
export function arkPackageVersion() {
|
|
165
|
+
try {
|
|
166
|
+
const pkg = readJson(path.join(__packageRoot, 'package.json'));
|
|
167
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Insert `arkVersion: <v>` into a skill's YAML frontmatter (before its closing
|
|
174
|
+
// `---`). No frontmatter → returned unchanged. Idempotent for a given version.
|
|
175
|
+
export function stampSkill(content, version) {
|
|
176
|
+
if (!version) return content;
|
|
177
|
+
const lines = content.split('\n');
|
|
178
|
+
if (lines[0] !== '---') return content;
|
|
179
|
+
const closeIdx = lines.indexOf('---', 1);
|
|
180
|
+
if (closeIdx === -1) return content;
|
|
181
|
+
const existing = lines.findIndex(
|
|
182
|
+
(line, i) => i > 0 && i < closeIdx && /^arkVersion:/.test(line)
|
|
183
|
+
);
|
|
184
|
+
if (existing !== -1) {
|
|
185
|
+
lines[existing] = `arkVersion: ${version}`;
|
|
186
|
+
} else {
|
|
187
|
+
lines.splice(closeIdx, 0, `arkVersion: ${version}`);
|
|
188
|
+
}
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Read the `arkVersion:` stamp from an installed skill file. Returns null when
|
|
193
|
+
// the file is absent or has no stamp (installed by a pre-stamp Ark, or hand-authored).
|
|
194
|
+
export function installedSkillVersion(filePath) {
|
|
195
|
+
let content;
|
|
196
|
+
try {
|
|
197
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const match = content.match(/^arkVersion:\s*(.+)$/m);
|
|
202
|
+
return match ? match[1].trim() : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Numeric-tuple compare of dotted versions; true when `a` is strictly older than
|
|
206
|
+
// `b`. Non-numeric/absent segments compare as 0, so "1.7" < "1.7.5".
|
|
207
|
+
export function isVersionOlder(a, b) {
|
|
208
|
+
const parse = (v) => String(v).split('.').map((n) => Number.parseInt(n, 10) || 0);
|
|
209
|
+
const av = parse(a);
|
|
210
|
+
const bv = parse(b);
|
|
211
|
+
const len = Math.max(av.length, bv.length);
|
|
212
|
+
for (let i = 0; i < len; i += 1) {
|
|
213
|
+
const x = av[i] ?? 0;
|
|
214
|
+
const y = bv[i] ?? 0;
|
|
215
|
+
if (x !== y) return x < y;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function skillTemplates() {
|
|
221
|
+
const dir = path.join(__packageRoot, 'templates', 'skills');
|
|
222
|
+
// A missing/mispackaged templates dir would otherwise install zero skills with
|
|
223
|
+
// exit 0 — warn so a packaging regression (e.g. "templates" dropped from the
|
|
224
|
+
// package.json files array) is visible instead of a silent no-op.
|
|
225
|
+
let entries;
|
|
226
|
+
try {
|
|
227
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
228
|
+
} catch {
|
|
229
|
+
console.error(
|
|
230
|
+
`Warning: skill templates directory not found (${dir}); no /ark-* skills installed.`
|
|
231
|
+
);
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
return entries
|
|
235
|
+
.filter((entry) => entry.isFile() && /^[a-z0-9-]+\.md$/.test(entry.name))
|
|
236
|
+
.map((entry) => entry.name)
|
|
237
|
+
.sort()
|
|
238
|
+
.map((name) => [path.basename(name, '.md'), fs.readFileSync(path.join(dir, name), 'utf8')]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Skill names only, silent on a missing templates dir — for the freshness
|
|
242
|
+
// advisory below, which must not print packaging warnings on every check run.
|
|
243
|
+
export function skillTemplateNames() {
|
|
244
|
+
const dir = path.join(__packageRoot, 'templates', 'skills');
|
|
245
|
+
let entries;
|
|
246
|
+
try {
|
|
247
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
248
|
+
} catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
return entries
|
|
252
|
+
.filter((entry) => entry.isFile() && /^[a-z0-9-]+\.md$/.test(entry.name))
|
|
253
|
+
.map((entry) => path.basename(entry.name, '.md'));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// A normal ark-check run is the reliable discovery point for new /ark-* skills.
|
|
257
|
+
// Ark ships no install lifecycle script (a postinstall banner would be blocked by
|
|
258
|
+
// modern package managers' script-approval policy anyway, so careful users never
|
|
259
|
+
// saw it — and it broke hardened installs). When a project has adopted Ark agent
|
|
260
|
+
// gates (AGENTS.md present) but a detected tool is missing
|
|
261
|
+
// skills this version ships, surface it here so agents and CI actually notice.
|
|
262
|
+
// Advisory only — never affects the exit code. Copilot has no reliable directory
|
|
263
|
+
// signal, so it is not auto-detected (explicit --tools only), matching resolveTools.
|
|
264
|
+
export function detectCodexHomeGap(root) {
|
|
265
|
+
if (!fs.existsSync(path.join(root, 'AGENTS.md'))) return null;
|
|
266
|
+
if (fs.existsSync(path.join(root, 'templates', 'skills'))) return null;
|
|
267
|
+
const skillNames = skillTemplateNames();
|
|
268
|
+
if (skillNames.length === 0) return null;
|
|
269
|
+
const dir = codexPromptsDir();
|
|
270
|
+
if (!fs.existsSync(dir)) return null;
|
|
271
|
+
const present = skillNames.filter((name) => fs.existsSync(path.join(dir, `${name}.md`)));
|
|
272
|
+
if (present.length === 0) return null; // Codex home never set up for Ark — don't nag.
|
|
273
|
+
const version = arkPackageVersion();
|
|
274
|
+
const missing = skillNames.length - present.length;
|
|
275
|
+
let stale = 0;
|
|
276
|
+
if (version) {
|
|
277
|
+
for (const name of present) {
|
|
278
|
+
const installed = installedSkillVersion(path.join(dir, `${name}.md`));
|
|
279
|
+
if (installed === null || isVersionOlder(installed, version)) stale += 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return missing > 0 || stale > 0 ? { missing, stale } : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function detectSkillGaps(root) {
|
|
286
|
+
if (!fs.existsSync(path.join(root, 'AGENTS.md'))) return [];
|
|
287
|
+
// The Ark source tree keeps the skill templates at templates/skills/ — it's the
|
|
288
|
+
// producer, not a consumer, so it must not nag itself to "install" its own skills.
|
|
289
|
+
if (fs.existsSync(path.join(root, 'templates', 'skills'))) return [];
|
|
290
|
+
const skillNames = skillTemplateNames();
|
|
291
|
+
if (skillNames.length === 0) return [];
|
|
292
|
+
const detected = [];
|
|
293
|
+
if (fs.existsSync(path.join(root, '.claude'))) detected.push('claude');
|
|
294
|
+
if (fs.existsSync(path.join(root, '.cursor'))) detected.push('cursor');
|
|
295
|
+
if (fs.existsSync(path.join(root, '.codex'))) detected.push('codex');
|
|
296
|
+
if (fs.existsSync(path.join(root, '.grok'))) detected.push('grok');
|
|
297
|
+
if (fs.existsSync(path.join(root, '.windsurf'))) detected.push('windsurf');
|
|
298
|
+
if (fs.statSync(path.join(root, '.clinerules'), { throwIfNoEntry: false })?.isDirectory()) {
|
|
299
|
+
detected.push('cline');
|
|
300
|
+
}
|
|
301
|
+
const version = arkPackageVersion();
|
|
302
|
+
const gaps = [];
|
|
303
|
+
for (const tool of detected) {
|
|
304
|
+
const target = SKILL_TOOL_TARGETS[tool];
|
|
305
|
+
if (!target) continue;
|
|
306
|
+
let missing = 0;
|
|
307
|
+
let stale = 0;
|
|
308
|
+
for (const name of skillNames) {
|
|
309
|
+
const file = path.join(root, target(name));
|
|
310
|
+
if (!fs.existsSync(file)) {
|
|
311
|
+
missing += 1;
|
|
312
|
+
} else if (version) {
|
|
313
|
+
// An installed skill with no stamp predates stamping (older Ark), or one
|
|
314
|
+
// stamped behind the current version is left over from an older install.
|
|
315
|
+
// Either way the shipped skill has moved on — offer a --force refresh.
|
|
316
|
+
const installed = installedSkillVersion(file);
|
|
317
|
+
if (installed === null || isVersionOlder(installed, version)) stale += 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (missing > 0 || stale > 0) gaps.push({ tool, missing, stale });
|
|
321
|
+
}
|
|
322
|
+
return gaps;
|
|
323
|
+
}
|
package/bin/lib/ts-resolve.mjs
CHANGED
|
@@ -128,9 +128,10 @@ export function scanCacheKey(root, args) {
|
|
|
128
128
|
// warm cache from an older Ark can't feed stale entries to new logic. v2: typeOnly on edges.
|
|
129
129
|
// v3: per-file exportsOnlyTypes. v4: typeOnlyExportNames + namedBindings.
|
|
130
130
|
// v5: hasTopLevelSideEffects. v6: non-exported impure inits + non-export class statics.
|
|
131
|
+
// v7: scope-aware forbidden globals + import-equals dependency edges.
|
|
131
132
|
return crypto
|
|
132
133
|
.createHash('sha1')
|
|
133
|
-
.update(`ark-check-cache-
|
|
134
|
+
.update(`ark-check-cache-v7\0${read(configPath)}\0${manifestPath ? read(manifestPath) : ''}`)
|
|
134
135
|
.digest('hex');
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript host resolution for architecture scan (API-compatible loader).
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { usableTypescript, typescriptUsabilityHint } from '../ark-shared.mjs';
|
|
6
|
+
import { __arkCheckCli } from './gate-files.mjs';
|
|
7
|
+
|
|
8
|
+
export async function loadTypeScript(root) {
|
|
9
|
+
const { createRequire } = await import('node:module');
|
|
10
|
+
const loaders = [];
|
|
11
|
+
try {
|
|
12
|
+
const req = createRequire(path.join(root, 'package.json'));
|
|
13
|
+
loaders.push({
|
|
14
|
+
label: 'project',
|
|
15
|
+
load: () => req('typescript'),
|
|
16
|
+
resolvePath: () => {
|
|
17
|
+
try {
|
|
18
|
+
return req.resolve('typescript');
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
} catch {
|
|
25
|
+
/* project has no package.json resolvable tree */
|
|
26
|
+
}
|
|
27
|
+
// Nested under arkgate (production dependency) — must work when project has only TS7.
|
|
28
|
+
try {
|
|
29
|
+
const req = createRequire(__arkCheckCli);
|
|
30
|
+
loaders.push({
|
|
31
|
+
label: 'arkgate',
|
|
32
|
+
load: () => req('typescript'),
|
|
33
|
+
resolvePath: () => {
|
|
34
|
+
try {
|
|
35
|
+
return req.resolve('typescript');
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
/* ark install tree unavailable */
|
|
43
|
+
}
|
|
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
|
+
let projectRejected = null;
|
|
54
|
+
const triedPaths = new Set();
|
|
55
|
+
for (const { label, load, resolvePath } of loaders) {
|
|
56
|
+
try {
|
|
57
|
+
const resolved = typeof resolvePath === 'function' ? resolvePath() : null;
|
|
58
|
+
if (resolved && triedPaths.has(resolved)) {
|
|
59
|
+
// Same physical package already rejected (e.g. project === hoisted arkgate path).
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (resolved) triedPaths.add(resolved);
|
|
63
|
+
|
|
64
|
+
const mod = await load();
|
|
65
|
+
const ts = usableTypescript(mod);
|
|
66
|
+
if (ts) {
|
|
67
|
+
const version =
|
|
68
|
+
typeof ts.version === 'string'
|
|
69
|
+
? ts.version
|
|
70
|
+
: typeof mod?.version === 'string'
|
|
71
|
+
? mod.version
|
|
72
|
+
: undefined;
|
|
73
|
+
return {
|
|
74
|
+
ts,
|
|
75
|
+
source: label,
|
|
76
|
+
version,
|
|
77
|
+
...(projectRejected ? { fallbackReason: projectRejected } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (label === 'project' && mod) {
|
|
81
|
+
projectRejected = `project typescript is not API-compatible (${typescriptUsabilityHint(mod)}); using ArkGate's JS-API TypeScript fallback (TypeScript 7.0 main export is version-only). See docs/typescript-support.md.`;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
/* try next loader */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|