nexarch 0.13.0 → 0.13.1
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/dist/commands/ingest-infra.js +194 -61
- package/dist/commands/init-agent.js +3 -3
- package/dist/commands/init-project-infra.js +22 -20
- package/dist/commands/init-project.js +124 -5
- package/dist/index.js +1 -0
- package/dist/lib/ansible-detect.js +109 -0
- package/dist/lib/ansible-projection.js +150 -0
- package/dist/lib/iac-detect.js +414 -0
- package/dist/lib/iac-projection.js +231 -0
- package/dist/lib/iac-tool.js +138 -0
- package/dist/lib/skills.js +32 -10
- package/package.json +2 -2
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { detectAnsibleProject } from "./ansible-detect.js";
|
|
4
|
+
const HASHICORP_SHAPED = {
|
|
5
|
+
estateArgs: ["show", "-json"],
|
|
6
|
+
unitNoun: "root module",
|
|
7
|
+
hasWorkspaces: true,
|
|
8
|
+
provisions: true,
|
|
9
|
+
};
|
|
10
|
+
export const IAC_TOOLS = [
|
|
11
|
+
{ id: "terraform", displayName: "Terraform", executable: "terraform", applyExecutable: "terraform", aliases: ["terraform"], ...HASHICORP_SHAPED },
|
|
12
|
+
{ id: "opentofu", displayName: "OpenTofu", executable: "tofu", applyExecutable: "tofu", aliases: ["opentofu", "tofu"], ...HASHICORP_SHAPED },
|
|
13
|
+
{
|
|
14
|
+
id: "ansible",
|
|
15
|
+
displayName: "Ansible",
|
|
16
|
+
executable: "ansible-inventory",
|
|
17
|
+
// --export resolves group and host variables the way a playbook run would
|
|
18
|
+
// see them, rather than printing the raw file.
|
|
19
|
+
estateArgs: ["--list", "--export"],
|
|
20
|
+
applyExecutable: "ansible-playbook",
|
|
21
|
+
unitNoun: "inventory",
|
|
22
|
+
hasWorkspaces: false,
|
|
23
|
+
provisions: false,
|
|
24
|
+
aliases: ["ansible"],
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
export function getIacTool(value) {
|
|
28
|
+
if (!value)
|
|
29
|
+
return null;
|
|
30
|
+
const normalised = value.trim().toLowerCase();
|
|
31
|
+
return IAC_TOOLS.find((tool) => tool.aliases.includes(normalised)) ?? null;
|
|
32
|
+
}
|
|
33
|
+
const IGNORED = new Set(["node_modules", ".git", ".terraform", "dist", "build", ".next"]);
|
|
34
|
+
/**
|
|
35
|
+
* Files that say how the estate is *run*. Deliberately not `.tf` or `.hcl`:
|
|
36
|
+
* OpenTofu reads the same HCL and kept the `terraform { ... }` block name, so
|
|
37
|
+
* every OpenTofu repository on earth contains the word "terraform" in its
|
|
38
|
+
* configuration. Treating that as evidence of the Terraform CLI made a real
|
|
39
|
+
* OpenTofu estate -- `terraform {}` block, `tofu apply` in CI -- come back
|
|
40
|
+
* ambiguous, which then threw, so the one case this feature exists for could
|
|
41
|
+
* not be ingested without passing --tool by hand.
|
|
42
|
+
*/
|
|
43
|
+
const AUTOMATION_FILE = /\.(ya?ml|json|sh|bash|ps1|tfvars)$|^(Makefile|Taskfile\.ya?ml|justfile)$/i;
|
|
44
|
+
/** `.tofu` is OpenTofu-only: it is the extension Terraform will not read. */
|
|
45
|
+
const TOFU_SOURCE_FILE = /\.tofu$/i;
|
|
46
|
+
/**
|
|
47
|
+
* A CLI invocation, not a mention. `terraform apply`, `tofu init`, `tofu -chdir`.
|
|
48
|
+
* The subcommand list is what automation actually runs; "terraform" alone in a
|
|
49
|
+
* comment, a module source or an HCL block is not evidence of either binary.
|
|
50
|
+
*/
|
|
51
|
+
// Built with String.raw, not a plain template literal: in a plain one the
|
|
52
|
+
// backslash escapes are consumed by the string before the regex ever sees
|
|
53
|
+
// them, which is how the first version of these came to match nothing at all
|
|
54
|
+
// and report every repository as "defaulted".
|
|
55
|
+
const SUBCOMMANDS = "init|plan|apply|show|destroy|validate|state|workspace|output|fmt|refresh|import|providers|-chdir";
|
|
56
|
+
const TERRAFORM_COMMAND = new RegExp(String.raw `(?:^|[^\w./-])terraform\s+(?:${SUBCOMMANDS})\b`, "im");
|
|
57
|
+
const TOFU_COMMAND = new RegExp(String.raw `(?:^|[^\w./-])tofu\s+(?:${SUBCOMMANDS})\b`, "im");
|
|
58
|
+
function collectEvidence(dir, depth = 0, found = { terraform: false, opentofu: false }) {
|
|
59
|
+
if (depth > 4 || (found.terraform && found.opentofu))
|
|
60
|
+
return found;
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = readdirSync(dir);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (IGNORED.has(entry))
|
|
70
|
+
continue;
|
|
71
|
+
const full = join(dir, entry);
|
|
72
|
+
try {
|
|
73
|
+
if (statSync(full).isDirectory()) {
|
|
74
|
+
collectEvidence(full, depth + 1, found);
|
|
75
|
+
}
|
|
76
|
+
else if (TOFU_SOURCE_FILE.test(entry)) {
|
|
77
|
+
found.opentofu = true;
|
|
78
|
+
}
|
|
79
|
+
else if (AUTOMATION_FILE.test(entry)) {
|
|
80
|
+
// One file at a time: the first version pushed every file's full
|
|
81
|
+
// contents into an array before matching, which on a large estate is
|
|
82
|
+
// the whole repository held in memory to answer a yes/no.
|
|
83
|
+
const text = readFileSync(full, "utf8");
|
|
84
|
+
if (!found.opentofu && TOFU_COMMAND.test(text))
|
|
85
|
+
found.opentofu = true;
|
|
86
|
+
if (!found.terraform && TERRAFORM_COMMAND.test(text))
|
|
87
|
+
found.terraform = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch { /* unreadable configuration is not evidence */ }
|
|
91
|
+
if (found.terraform && found.opentofu)
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Detects the IaC tool from how the repository is actually driven — the
|
|
98
|
+
* commands its automation runs, `.tofu` files, which only OpenTofu reads, and
|
|
99
|
+
* Ansible's own repository shape.
|
|
100
|
+
*
|
|
101
|
+
* Terraform and OpenTofu together is genuinely ambiguous: they are the same
|
|
102
|
+
* tool twice, so one estate cannot be read by guessing which. **Terraform and
|
|
103
|
+
* Ansible together is not** — it is the most ordinary infrastructure repository
|
|
104
|
+
* there is, one tool provisioning and the other configuring, and they describe
|
|
105
|
+
* different things. So the provisioning tool wins and the signals say the
|
|
106
|
+
* inventory is there to be ingested too, rather than refusing a layout that is
|
|
107
|
+
* completely normal.
|
|
108
|
+
*/
|
|
109
|
+
export function detectIacTool(dir, requestedTool) {
|
|
110
|
+
if (requestedTool) {
|
|
111
|
+
const tool = getIacTool(requestedTool);
|
|
112
|
+
if (!tool)
|
|
113
|
+
throw new Error(`Unknown IaC tool "${requestedTool}". Use terraform, opentofu or ansible.`);
|
|
114
|
+
return { status: "detected", tool, signals: [`--tool ${tool.id}`] };
|
|
115
|
+
}
|
|
116
|
+
const { terraform, opentofu } = collectEvidence(dir);
|
|
117
|
+
const ansible = detectAnsibleProject(dir);
|
|
118
|
+
const alsoAnsible = ansible.isAnsible ? ["Ansible is here too — ingest its inventory with --tool ansible"] : [];
|
|
119
|
+
if (terraform && opentofu) {
|
|
120
|
+
return { status: "ambiguous", tool: null, signals: ["both `terraform` and `tofu` commands are run by this repository", ...alsoAnsible] };
|
|
121
|
+
}
|
|
122
|
+
if (opentofu)
|
|
123
|
+
return { status: "detected", tool: IAC_TOOLS[1], signals: ["`tofu` commands or .tofu files", ...alsoAnsible] };
|
|
124
|
+
if (terraform)
|
|
125
|
+
return { status: "detected", tool: IAC_TOOLS[0], signals: ["`terraform` commands", ...alsoAnsible] };
|
|
126
|
+
if (ansible.isAnsible) {
|
|
127
|
+
const tool = IAC_TOOLS.find((candidate) => candidate.id === "ansible");
|
|
128
|
+
return { status: "detected", tool, signals: ansible.signals };
|
|
129
|
+
}
|
|
130
|
+
return { status: "defaulted", tool: IAC_TOOLS[0], signals: ["no IaC CLI named in automation; defaulting to Terraform"] };
|
|
131
|
+
}
|
|
132
|
+
export function iacToolOrThrow(dir, requestedTool) {
|
|
133
|
+
const detection = detectIacTool(dir, requestedTool);
|
|
134
|
+
if (detection.status === "ambiguous") {
|
|
135
|
+
throw new Error("Both Terraform and OpenTofu were detected. Select the state CLI explicitly with --tool terraform or --tool opentofu.");
|
|
136
|
+
}
|
|
137
|
+
return detection.tool;
|
|
138
|
+
}
|
package/dist/lib/skills.js
CHANGED
|
@@ -317,22 +317,44 @@ say so rather than skipping it or inventing a target.
|
|
|
317
317
|
`;
|
|
318
318
|
const FEEDBACK_SKILL_BODY = `---
|
|
319
319
|
name: nexarch-feedback
|
|
320
|
-
description:
|
|
320
|
+
description: Offer to report a Nexarch problem the moment you notice it, rather than waiting to be asked. Use when a Nexarch tool error blocked work and needed a workaround, when a graph answer was wrong or misleading, or when a repository agent instruction contradicts what the tools actually do. Ask the human before sending. Requires the Nexarch MCP tools (nexarch_*).
|
|
321
321
|
---
|
|
322
322
|
|
|
323
323
|
# Nexarch Feedback
|
|
324
324
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
mislead;
|
|
328
|
-
|
|
325
|
+
These are worth reporting to Nexarch: a Nexarch tool error that blocked the task
|
|
326
|
+
and made you work around it; a graph answer that was wrong or unhelpful enough
|
|
327
|
+
to mislead; an instruction in the repository's agent files that was ambiguous or
|
|
328
|
+
contradicted what the tools actually do.
|
|
329
329
|
|
|
330
|
-
|
|
331
|
-
involved, and say what you expected instead. Include reproduction detail when
|
|
332
|
-
it clarifies the problem.
|
|
330
|
+
## Offer to report it when it happens
|
|
333
331
|
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
When you notice one, raise it with the human in the next thing you say to them
|
|
333
|
+
— not in a closing summary or a list of open items at the end of the task. Say
|
|
334
|
+
in a sentence or two what looked wrong, and ask whether they would like you to
|
|
335
|
+
report it to Nexarch.
|
|
336
|
+
|
|
337
|
+
- If they say yes, call \`nexarch_submit_feedback\` and tell them the feedback id
|
|
338
|
+
you got back.
|
|
339
|
+
- If they say no, drop it and do not offer again for the same problem.
|
|
340
|
+
- Do not send a report without their yes, unless they have already told you in
|
|
341
|
+
this session to report problems without asking.
|
|
342
|
+
|
|
343
|
+
If you find yourself writing "this may be a Nexarch bug", that is the moment to
|
|
344
|
+
offer.
|
|
345
|
+
|
|
346
|
+
## Make each report useful
|
|
347
|
+
|
|
348
|
+
Write one concrete sentence, name the tool or surface involved, and say what
|
|
349
|
+
you expected instead. Include reproduction detail when it clarifies the
|
|
350
|
+
problem. Never include secrets, credentials or customer data.
|
|
351
|
+
|
|
352
|
+
## Do not offer to report
|
|
353
|
+
|
|
354
|
+
A refused write when governance was working as designed, or your own malformed
|
|
355
|
+
call. Fix the call or follow the governance response instead. Work that is
|
|
356
|
+
waiting on a human decision, such as a proposed application awaiting
|
|
357
|
+
activation, is the process working, not a fault.
|
|
336
358
|
`;
|
|
337
359
|
export const SKILLS = [
|
|
338
360
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexarch",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Your architecture workspace for AI delivery.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nexarch",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"prepublishOnly": "tsc",
|
|
27
27
|
"dev": "tsx src/index.ts",
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
|
-
"test": "tsx scripts/test-mcp-proxy-response.ts && tsx scripts/test-trust-reattest.ts && tsx scripts/test-similar-applications.ts"
|
|
29
|
+
"test": "tsx scripts/test-mcp-proxy-response.ts && tsx scripts/test-trust-reattest.ts && tsx scripts/test-similar-applications.ts && tsx scripts/test-iac-tool.ts && tsx scripts/test-ansible.ts && tsx scripts/test-reference-sightings.ts"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22",
|