nexarch 0.10.1 → 0.11.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/README.md +20 -0
- package/dist/commands/applied-policies.js +57 -0
- package/dist/commands/check-in.js +61 -28
- package/dist/commands/governance-summary.js +56 -0
- package/dist/commands/init-project.js +68 -9
- package/dist/commands/policy-audit-results.js +103 -0
- package/dist/commands/proposals-start.js +589 -0
- package/dist/index.js +44 -1
- package/dist/lib/mcp.js +16 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,26 @@ npx nexarch setup
|
|
|
25
25
|
| `nexarch init-agent [--json] [--strict] [--agent-id <id>] [--redact-hostname]` | Run onboarding handshake and mandatory `agent` entity registration in graph |
|
|
26
26
|
| `nexarch init-agent --bind-to-external-key <key> [--bind-relationship-type <code>]` | Optionally bind the agent node to an existing graph external key |
|
|
27
27
|
| `nexarch init-agent` (default behavior) | Also upserts technology component entities (host, OS, Node.js runtime) and links them to the agent |
|
|
28
|
+
| `nexarch agent identify ...` | Submit provider/model/client identity metadata to complete agent profile enrichment |
|
|
29
|
+
|
|
30
|
+
## Bootstrap flow after `init-agent`
|
|
31
|
+
|
|
32
|
+
`nexarch init-agent` writes a bootstrap file at:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
~/.nexarch/agent-bootstrap.md
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That file contains a pre-filled `npx nexarch agent identify ...` command template.
|
|
39
|
+
The current workflow is:
|
|
40
|
+
|
|
41
|
+
1. run `npx nexarch init-agent`
|
|
42
|
+
2. open `~/.nexarch/agent-bootstrap.md`
|
|
43
|
+
3. copy the generated `npx nexarch agent identify ...` template
|
|
44
|
+
4. fill in the real provider/model/client details
|
|
45
|
+
5. run the completed command
|
|
46
|
+
|
|
47
|
+
Do not assume an `npx nexarch agent prompt` command exists unless/until it is explicitly implemented and documented.
|
|
28
48
|
|
|
29
49
|
## Company context behavior
|
|
30
50
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import { requireCredentials } from "../lib/credentials.js";
|
|
3
|
+
import { callMcpTool } from "../lib/mcp.js";
|
|
4
|
+
function parseFlag(args, flag) {
|
|
5
|
+
return args.includes(flag);
|
|
6
|
+
}
|
|
7
|
+
function parseOptionValue(args, option) {
|
|
8
|
+
const idx = args.indexOf(option);
|
|
9
|
+
if (idx === -1)
|
|
10
|
+
return null;
|
|
11
|
+
const v = args[idx + 1];
|
|
12
|
+
if (!v || v.startsWith("--"))
|
|
13
|
+
return null;
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
function parseToolText(result) {
|
|
17
|
+
const text = result.content?.[0]?.text ?? "{}";
|
|
18
|
+
return JSON.parse(text);
|
|
19
|
+
}
|
|
20
|
+
export async function appliedPolicies(args) {
|
|
21
|
+
const asJson = parseFlag(args, "--json");
|
|
22
|
+
const showMarkdown = parseFlag(args, "--markdown");
|
|
23
|
+
if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
|
|
24
|
+
console.log(`
|
|
25
|
+
Usage:
|
|
26
|
+
nexarch applied-policies [--pack <packCode>] [--markdown] [--json]
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--pack <code> Filter to a specific policy pack code
|
|
30
|
+
--markdown Include full document markdown in human output
|
|
31
|
+
--json Print JSON response (always includes markdown)
|
|
32
|
+
`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const packCode = parseOptionValue(args, "--pack") ?? parseOptionValue(args, "--pack-code");
|
|
36
|
+
const creds = requireCredentials();
|
|
37
|
+
const raw = await callMcpTool("nexarch_get_applied_policies", { ...(packCode ? { packCode } : {}) }, { companyId: creds.companyId });
|
|
38
|
+
const result = parseToolText(raw);
|
|
39
|
+
if (asJson) {
|
|
40
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log(`Applied policies${packCode ? ` (pack: ${packCode})` : ""}`);
|
|
44
|
+
if (result.policyBundleHash)
|
|
45
|
+
console.log(`Bundle hash: ${result.policyBundleHash}`);
|
|
46
|
+
console.log(`Documents: ${result.policyCount ?? 0}`);
|
|
47
|
+
for (const pack of result.policies ?? []) {
|
|
48
|
+
console.log(`\n[${pack.packCode}] ${pack.packName} — v${pack.installedVersion}`);
|
|
49
|
+
for (const doc of pack.documents ?? []) {
|
|
50
|
+
console.log(` - ${doc.title} (${doc.code})`);
|
|
51
|
+
if (showMarkdown && doc.contentMarkdown) {
|
|
52
|
+
const indented = doc.contentMarkdown.split("\n").map((l) => ` ${l}`).join("\n");
|
|
53
|
+
console.log(indented);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -40,49 +40,82 @@ export async function checkIn(args) {
|
|
|
40
40
|
const agentKey = agentKeyArg ?? identity.agentKey;
|
|
41
41
|
if (!agentKey) {
|
|
42
42
|
if (asJson) {
|
|
43
|
-
process.stdout.write(JSON.stringify({ commands: [], reason: "no-agent-key" }) + "\n");
|
|
43
|
+
process.stdout.write(JSON.stringify({ commands: [], draftApplications: [], reason: "no-agent-key" }) + "\n");
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
46
|
console.log("No agent key found. Pass --agent-key or run nexarch init-agent first.");
|
|
47
47
|
}
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
const raw = await
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
const [raw, draftRaw] = await Promise.all([
|
|
51
|
+
callMcpTool("nexarch_claim_command", {
|
|
52
|
+
agentRef: agentKey,
|
|
53
|
+
agentKey,
|
|
54
|
+
companyId: creds.companyId,
|
|
55
|
+
}, { companyId: creds.companyId }),
|
|
56
|
+
callMcpTool("nexarch_list_entities", {
|
|
57
|
+
companyId: creds.companyId,
|
|
58
|
+
entityTypeCode: "application",
|
|
59
|
+
status: "draft",
|
|
60
|
+
}, { companyId: creds.companyId }),
|
|
61
|
+
]);
|
|
55
62
|
const result = parseToolText(raw);
|
|
63
|
+
const draftResult = parseToolText(draftRaw);
|
|
64
|
+
const draftApplications = draftResult.entities ?? [];
|
|
56
65
|
if (asJson) {
|
|
57
|
-
process.stdout.write(JSON.stringify(result) + "\n");
|
|
66
|
+
process.stdout.write(JSON.stringify({ ...result, draftApplications }) + "\n");
|
|
58
67
|
return;
|
|
59
68
|
}
|
|
60
69
|
const commands = result.commands ?? [];
|
|
61
|
-
if (commands.length === 0) {
|
|
70
|
+
if (commands.length === 0 && draftApplications.length === 0) {
|
|
62
71
|
console.log("No pending application-target commands found.");
|
|
72
|
+
console.log("No applications in draft state.");
|
|
63
73
|
return;
|
|
64
74
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
if (commands.length > 0) {
|
|
76
|
+
console.log("\nPending application-target commands (preview only; nothing claimed):");
|
|
77
|
+
console.log(`Company scope is resolved server-side from companyId=${creds.companyId}.`);
|
|
78
|
+
if (identity.companyId && identity.companyId !== creds.companyId) {
|
|
79
|
+
console.log(`Note: local identity companyId ${identity.companyId} differs from current credentials companyId ${creds.companyId}.`);
|
|
80
|
+
}
|
|
81
|
+
for (const cmd of commands) {
|
|
82
|
+
const claimable = cmd.claimable ? "yes" : "no";
|
|
83
|
+
const reason = cmd.claimReason ?? (cmd.claimable ? "application_access_confirmed" : "application_not_found_for_company");
|
|
84
|
+
console.log(`\n- ID : ${cmd.id}`);
|
|
85
|
+
console.log(` Type : ${cmd.command_type}`);
|
|
86
|
+
console.log(` Target : ${cmd.target_entity_key ?? "(none)"}`);
|
|
87
|
+
console.log(` Priority : ${cmd.priority}`);
|
|
88
|
+
console.log(` Claimable : ${claimable} (${reason})`);
|
|
89
|
+
if (cmd.command_type_version != null)
|
|
90
|
+
console.log(` Type ver : ${cmd.command_type_version}`);
|
|
91
|
+
if (cmd.resolved_runtime_handler)
|
|
92
|
+
console.log(` Runtime : ${cmd.resolved_runtime_handler}`);
|
|
93
|
+
if (cmd.claimable) {
|
|
94
|
+
console.log(` Claim cmd : npx nexarch command-claim --id "${cmd.id}"`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
console.log("\nTo validate an application target is accessible in Nexarch before claiming:");
|
|
98
|
+
console.log(" npx nexarch policy-controls --entity <application:key> --json");
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log("No pending application-target commands found.");
|
|
69
102
|
}
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
console.log(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
console.log(`
|
|
82
|
-
|
|
83
|
-
console.log(` Claim cmd : npx nexarch command-claim --id "${cmd.id}"`);
|
|
103
|
+
if (draftApplications.length > 0) {
|
|
104
|
+
const count = draftApplications.length;
|
|
105
|
+
console.log(`\n--- Draft applications (${count}) ---`);
|
|
106
|
+
console.log(`${count === 1 ? "There is 1 application" : `There are ${count} applications`} in draft state.` +
|
|
107
|
+
" Ask the user if they would like to review and instantiate any of them.");
|
|
108
|
+
for (const app of draftApplications) {
|
|
109
|
+
const subtype = app.entitySubtypeCode ? ` [${app.entitySubtypeCode}]` : "";
|
|
110
|
+
const key = app.externalKey ? ` (${app.externalKey})` : "";
|
|
111
|
+
console.log(`\n Name : ${app.name}${subtype}${key}`);
|
|
112
|
+
if (app.description)
|
|
113
|
+
console.log(` Desc : ${app.description}`);
|
|
114
|
+
console.log(` ID : ${app.id}`);
|
|
115
|
+
console.log(` Instantiate: npx nexarch proposals start --id "${app.id}"`);
|
|
84
116
|
}
|
|
117
|
+
console.log("\nTo scaffold a draft application into a local project:");
|
|
118
|
+
console.log(" npx nexarch proposals start --id <applicationId>");
|
|
119
|
+
console.log(" npx nexarch proposals start --id <applicationId> --dir <path> --non-interactive");
|
|
85
120
|
}
|
|
86
|
-
console.log("\nTo validate an application target is accessible in Nexarch before claiming:");
|
|
87
|
-
console.log(" npx nexarch policy-controls --entity <application:key> --json");
|
|
88
121
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import { requireCredentials } from "../lib/credentials.js";
|
|
3
|
+
import { callMcpTool } from "../lib/mcp.js";
|
|
4
|
+
function parseFlag(args, flag) {
|
|
5
|
+
return args.includes(flag);
|
|
6
|
+
}
|
|
7
|
+
function parseToolText(result) {
|
|
8
|
+
const text = result.content?.[0]?.text ?? "{}";
|
|
9
|
+
return JSON.parse(text);
|
|
10
|
+
}
|
|
11
|
+
export async function governanceSummary(args) {
|
|
12
|
+
const asJson = parseFlag(args, "--json");
|
|
13
|
+
if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
|
|
14
|
+
console.log(`
|
|
15
|
+
Usage:
|
|
16
|
+
nexarch governance-summary [--json]
|
|
17
|
+
|
|
18
|
+
Returns review queue counts, graph stats, and a per-application policy
|
|
19
|
+
audit rollup (latest run status, pass/partial/fail counts).
|
|
20
|
+
`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const creds = requireCredentials();
|
|
24
|
+
const raw = await callMcpTool("nexarch_get_governance_summary", { companyId: creds.companyId }, { companyId: creds.companyId });
|
|
25
|
+
const result = parseToolText(raw);
|
|
26
|
+
if (asJson) {
|
|
27
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log("Governance summary");
|
|
31
|
+
const review = result.reviewQueue ?? {};
|
|
32
|
+
console.log(` Review queue: ${review.pending_entities ?? 0} entities, ${review.pending_relationships ?? 0} relationships`);
|
|
33
|
+
console.log(` Graph: ${result.graphEntityCount ?? 0} entities, ${result.graphRelationshipCount ?? 0} relationships (${result.architectureFactCount ?? 0} facts)`);
|
|
34
|
+
console.log(` Canonical coverage: ${result.canonicalCoverageOverallPct ?? 0}% overall ` +
|
|
35
|
+
`(entities ${result.canonicalCoverageEntityPct ?? 0}%, relationships ${result.canonicalCoverageRelationshipPct ?? 0}%)`);
|
|
36
|
+
console.log(` Canonical registry size: ${result.canonicalRegistryCount ?? 0}`);
|
|
37
|
+
const audit = result.policyAudit;
|
|
38
|
+
if (audit) {
|
|
39
|
+
console.log("\nPolicy audit rollup");
|
|
40
|
+
console.log(` Applications: ${audit.totalApplications} (audited ${audit.appsAudited}, missing audit ${audit.appsWithoutAudit})`);
|
|
41
|
+
console.log(` With failures: ${audit.appsWithFailures}, with partials only: ${audit.appsWithPartials}, clean: ${audit.appsClean}`);
|
|
42
|
+
if (audit.applications.length > 0) {
|
|
43
|
+
console.log("\n Per application:");
|
|
44
|
+
for (const app of audit.applications) {
|
|
45
|
+
if (!app.latestRunId) {
|
|
46
|
+
console.log(` - ${app.applicationName} (${app.applicationEntityRef}): no audit run`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
console.log(` - ${app.applicationName} (${app.applicationEntityRef}): ${app.latestRunStatus} — ` +
|
|
50
|
+
`${app.passCount ?? 0} pass, ${app.partialCount ?? 0} partial, ${app.failCount ?? 0} fail` +
|
|
51
|
+
(app.totalRules ? ` of ${app.totalRules}` : "") +
|
|
52
|
+
(app.completedAt ? ` (completed ${app.completedAt})` : ""));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -168,13 +168,65 @@ function readSvnRepositoryUrl(dir) {
|
|
|
168
168
|
return null;
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
+
function isLocalOnlyUrl(value) {
|
|
172
|
+
if (!value)
|
|
173
|
+
return true;
|
|
174
|
+
try {
|
|
175
|
+
const u = new URL(value);
|
|
176
|
+
const host = u.hostname.toLowerCase();
|
|
177
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1")
|
|
178
|
+
return true;
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function readGitRemoteCandidates(dir) {
|
|
186
|
+
const remotes = (safeExec("git", ["remote"], dir) ?? "")
|
|
187
|
+
.split("\n")
|
|
188
|
+
.map((value) => value.trim())
|
|
189
|
+
.filter(Boolean);
|
|
190
|
+
const names = remotes.length > 0 ? remotes : ["origin"];
|
|
191
|
+
const candidates = [];
|
|
192
|
+
for (const name of names) {
|
|
193
|
+
const value = safeExec("git", ["remote", "get-url", name], dir)
|
|
194
|
+
?? safeExec("git", ["config", "--get", `remote.${name}.url`], dir);
|
|
195
|
+
if (value)
|
|
196
|
+
candidates.push(value);
|
|
197
|
+
}
|
|
198
|
+
return Array.from(new Set(candidates));
|
|
199
|
+
}
|
|
200
|
+
function choosePreferredRepositoryRef(candidates) {
|
|
201
|
+
if (candidates.length === 0)
|
|
202
|
+
return null;
|
|
203
|
+
const scored = candidates.map((rawRef, index) => {
|
|
204
|
+
const url = normalizeRepoUrl(rawRef);
|
|
205
|
+
const provider = inferProvider(url ?? rawRef);
|
|
206
|
+
const hostedProvider = provider !== "unknown";
|
|
207
|
+
const localOnly = isLocalOnlyUrl(url);
|
|
208
|
+
const score = [
|
|
209
|
+
hostedProvider ? 1 : 0,
|
|
210
|
+
!localOnly && Boolean(url) ? 1 : 0,
|
|
211
|
+
index === 0 ? 1 : 0,
|
|
212
|
+
];
|
|
213
|
+
return { rawRef, score };
|
|
214
|
+
});
|
|
215
|
+
scored.sort((a, b) => {
|
|
216
|
+
for (let i = 0; i < a.score.length; i += 1) {
|
|
217
|
+
if (a.score[i] !== b.score[i])
|
|
218
|
+
return b.score[i] - a.score[i];
|
|
219
|
+
}
|
|
220
|
+
return 0;
|
|
221
|
+
});
|
|
222
|
+
return scored[0]?.rawRef ?? null;
|
|
223
|
+
}
|
|
171
224
|
function detectSourceRepository(dir) {
|
|
172
225
|
let rawRef = null;
|
|
173
226
|
let vcsType = "unknown";
|
|
174
|
-
// Prefer git when present.
|
|
227
|
+
// Prefer git when present, and prefer hosted remotes over local-only URLs.
|
|
175
228
|
if (existsSync(join(dir, ".git"))) {
|
|
176
|
-
rawRef =
|
|
177
|
-
?? safeExec("git", ["config", "--get", "remote.origin.url"], dir);
|
|
229
|
+
rawRef = choosePreferredRepositoryRef(readGitRemoteCandidates(dir));
|
|
178
230
|
if (rawRef)
|
|
179
231
|
vcsType = "git";
|
|
180
232
|
}
|
|
@@ -1625,11 +1677,17 @@ export async function initProject(args) {
|
|
|
1625
1677
|
},
|
|
1626
1678
|
});
|
|
1627
1679
|
const preservedEntities = entitiesResult.preserved ?? [];
|
|
1680
|
+
const nothingWritten = (entitiesResult.summary?.succeeded ?? 0) === 0;
|
|
1681
|
+
const status = refreshMode && nothingWritten && preservedEntities.length > 0
|
|
1682
|
+
? "review_enrichment"
|
|
1683
|
+
: "enrichment_required";
|
|
1628
1684
|
return {
|
|
1629
|
-
status
|
|
1630
|
-
note:
|
|
1631
|
-
? "
|
|
1632
|
-
:
|
|
1685
|
+
status,
|
|
1686
|
+
note: status === "review_enrichment"
|
|
1687
|
+
? "No new skeletons were written — all entities were already enriched and preserved. Review the preservedEntities list and confirm scan values match. Update with nexarch update-entity only if something has genuinely changed."
|
|
1688
|
+
: refreshMode
|
|
1689
|
+
? "Execute ALL pendingSteps. In refresh mode, compare graphState vs scanState to identify gaps — do not blindly remove relationships the scanner missed; the scanner cannot detect platforms, integrations, data assets, or application_functions."
|
|
1690
|
+
: "Execute ALL pendingSteps before reporting the project as registered. Do not pause or ask for confirmation between steps.",
|
|
1633
1691
|
...(refreshMode && preservedEntities.length > 0
|
|
1634
1692
|
? {
|
|
1635
1693
|
preservedEntities: {
|
|
@@ -1842,9 +1900,10 @@ export async function initProject(args) {
|
|
|
1842
1900
|
return lines.join("\n");
|
|
1843
1901
|
}
|
|
1844
1902
|
const enrichmentRequired = buildEnrichmentPayload();
|
|
1903
|
+
const outputStatus = enrichmentRequired.status;
|
|
1845
1904
|
const output = {
|
|
1846
1905
|
ok: Number(entitiesResult.summary?.failed ?? 0) === 0,
|
|
1847
|
-
status:
|
|
1906
|
+
status: outputStatus,
|
|
1848
1907
|
mode: refreshMode ? "refresh" : "init",
|
|
1849
1908
|
project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride, detectedEcosystems },
|
|
1850
1909
|
entities: entitiesResult.summary ?? {},
|
|
@@ -1885,7 +1944,7 @@ export async function initProject(args) {
|
|
|
1885
1944
|
const preservedCount = output.entities.preserved ?? 0;
|
|
1886
1945
|
console.log(` Entities : ${output.entities.succeeded ?? 0} written, ${preservedCount > 0 ? `${preservedCount} preserved (enriched), ` : ""}${output.entities.failed ?? 0} failed`);
|
|
1887
1946
|
console.log(` Relationships: ${output.relationships.succeeded ?? 0} written`);
|
|
1888
|
-
console.log(
|
|
1947
|
+
console.log(` Status : ${output.status === "review_enrichment" ? "all enriched; review preserved entities" : "skeleton created; enrichment pending"}`);
|
|
1889
1948
|
if (output.metrics.relationshipsSkippedAsDuplicate > 0) {
|
|
1890
1949
|
console.log(` Deduped rels : ${output.metrics.relationshipsSkippedAsDuplicate} skipped as duplicates before upsert`);
|
|
1891
1950
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import { requireCredentials } from "../lib/credentials.js";
|
|
3
|
+
import { callMcpTool } from "../lib/mcp.js";
|
|
4
|
+
function parseFlag(args, flag) {
|
|
5
|
+
return args.includes(flag);
|
|
6
|
+
}
|
|
7
|
+
function parseOptionValue(args, option) {
|
|
8
|
+
const idx = args.indexOf(option);
|
|
9
|
+
if (idx === -1)
|
|
10
|
+
return null;
|
|
11
|
+
const v = args[idx + 1];
|
|
12
|
+
if (!v || v.startsWith("--"))
|
|
13
|
+
return null;
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
function parseToolText(result) {
|
|
17
|
+
const text = result.content?.[0]?.text ?? "{}";
|
|
18
|
+
return JSON.parse(text);
|
|
19
|
+
}
|
|
20
|
+
export async function policyAuditResults(args) {
|
|
21
|
+
const asJson = parseFlag(args, "--json");
|
|
22
|
+
if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
|
|
23
|
+
console.log(`
|
|
24
|
+
Usage:
|
|
25
|
+
nexarch policy-audit-results --entity <applicationEntityRef> [--limit <1-10>] [--json]
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--entity <key> Required application reference (e.g. application:my-service).
|
|
29
|
+
Aliases: --entity-ref, --application-ref, --application-key
|
|
30
|
+
--limit <n> Number of most recent runs to return (default 1, max 10)
|
|
31
|
+
--json Print JSON response
|
|
32
|
+
`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const entity = parseOptionValue(args, "--entity") ??
|
|
36
|
+
parseOptionValue(args, "--entity-ref") ??
|
|
37
|
+
parseOptionValue(args, "--application-ref") ??
|
|
38
|
+
parseOptionValue(args, "--application-key");
|
|
39
|
+
if (!entity) {
|
|
40
|
+
console.error("error: --entity <applicationEntityRef> is required (e.g. application:my-service)");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const limitRaw = parseOptionValue(args, "--limit");
|
|
44
|
+
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : null;
|
|
45
|
+
if (limitRaw && (!Number.isFinite(limit) || (limit ?? 0) < 1)) {
|
|
46
|
+
console.error("error: --limit must be a positive integer");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const creds = requireCredentials();
|
|
50
|
+
const raw = await callMcpTool("nexarch_get_policy_audit_results", {
|
|
51
|
+
applicationEntityRef: entity,
|
|
52
|
+
...(limit ? { limit } : {}),
|
|
53
|
+
companyId: creds.companyId,
|
|
54
|
+
}, { companyId: creds.companyId });
|
|
55
|
+
const result = parseToolText(raw);
|
|
56
|
+
if (asJson) {
|
|
57
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (result.ok === false) {
|
|
61
|
+
console.error(`Policy audit results failed${result.error ? `: ${result.error}` : ""}`);
|
|
62
|
+
if (result.hint)
|
|
63
|
+
console.error(result.hint);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const label = result.applicationName
|
|
67
|
+
? `${result.applicationName} (${result.applicationEntityRef ?? entity})`
|
|
68
|
+
: (result.applicationEntityRef ?? entity);
|
|
69
|
+
console.log(`Policy audit results for ${label}`);
|
|
70
|
+
const runs = result.runs ?? [];
|
|
71
|
+
if (runs.length === 0) {
|
|
72
|
+
console.log("No audit runs found.");
|
|
73
|
+
if (result.hint)
|
|
74
|
+
console.log(result.hint);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const run of runs) {
|
|
78
|
+
const summary = run.summary ?? {};
|
|
79
|
+
console.log(`\nRun ${run.runId} — ${run.status}`);
|
|
80
|
+
if (run.startedAt)
|
|
81
|
+
console.log(` Started: ${run.startedAt}`);
|
|
82
|
+
if (run.completedAt)
|
|
83
|
+
console.log(` Completed: ${run.completedAt}`);
|
|
84
|
+
if (summary && (summary.passCount !== undefined || summary.failCount !== undefined)) {
|
|
85
|
+
console.log(` Summary: ${summary.passCount ?? 0} pass, ${summary.partialCount ?? 0} partial, ${summary.failCount ?? 0} fail` +
|
|
86
|
+
(summary.totalRules !== undefined ? ` (of ${summary.totalRules})` : ""));
|
|
87
|
+
}
|
|
88
|
+
for (const control of run.controls ?? []) {
|
|
89
|
+
console.log(`\n - ${control.controlName} (${control.controlId})`);
|
|
90
|
+
for (const rule of control.rules ?? []) {
|
|
91
|
+
const level = rule.requirementLevel ? ` [${rule.requirementLevel}]` : "";
|
|
92
|
+
console.log(` • [${rule.result}] ${rule.ruleName}${level} (${rule.ruleId})`);
|
|
93
|
+
if (rule.rationale)
|
|
94
|
+
console.log(` ${rule.rationale}`);
|
|
95
|
+
if (Array.isArray(rule.missingRequirements) && rule.missingRequirements.length > 0) {
|
|
96
|
+
for (const m of rule.missingRequirements) {
|
|
97
|
+
console.log(` - missing: ${String(m)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import * as readline from "node:readline/promises";
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
5
|
+
import { requireCredentials } from "../lib/credentials.js";
|
|
6
|
+
import { callMcpTool } from "../lib/mcp.js";
|
|
7
|
+
function parseFlag(args, flag) {
|
|
8
|
+
return args.includes(flag);
|
|
9
|
+
}
|
|
10
|
+
function parseOptionValue(args, option) {
|
|
11
|
+
const idx = args.indexOf(option);
|
|
12
|
+
if (idx === -1)
|
|
13
|
+
return null;
|
|
14
|
+
const value = args[idx + 1];
|
|
15
|
+
if (!value || value.startsWith("--"))
|
|
16
|
+
return null;
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function parseToolText(result) {
|
|
20
|
+
const text = result.content?.[0]?.text ?? "{}";
|
|
21
|
+
return JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
function slugify(value) {
|
|
24
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "application";
|
|
25
|
+
}
|
|
26
|
+
function ensureDir(path) {
|
|
27
|
+
mkdirSync(path, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
function isDirectoryEmpty(path) {
|
|
30
|
+
if (!existsSync(path))
|
|
31
|
+
return true;
|
|
32
|
+
const stat = statSync(path);
|
|
33
|
+
if (!stat.isDirectory())
|
|
34
|
+
return false;
|
|
35
|
+
return readdirSync(path).length === 0;
|
|
36
|
+
}
|
|
37
|
+
function isDirectoryPath(path) {
|
|
38
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
39
|
+
}
|
|
40
|
+
function trimText(value) {
|
|
41
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
42
|
+
}
|
|
43
|
+
function formatBulletList(values) {
|
|
44
|
+
if (values.length === 0)
|
|
45
|
+
return "- None recorded";
|
|
46
|
+
return values.map((value) => `- ${value}`).join("\n");
|
|
47
|
+
}
|
|
48
|
+
function safeJson(value) {
|
|
49
|
+
return JSON.stringify(value, null, 2);
|
|
50
|
+
}
|
|
51
|
+
async function promptChoice(proposals) {
|
|
52
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
53
|
+
try {
|
|
54
|
+
console.log("\nProposed applications:");
|
|
55
|
+
proposals.forEach((proposal, index) => {
|
|
56
|
+
console.log(` ${index + 1}) ${proposal.name}${proposal.entitySubtypeCode ? ` [${proposal.entitySubtypeCode}]` : ""}`);
|
|
57
|
+
if (proposal.description)
|
|
58
|
+
console.log(` ${proposal.description}`);
|
|
59
|
+
});
|
|
60
|
+
// eslint-disable-next-line no-constant-condition
|
|
61
|
+
while (true) {
|
|
62
|
+
const answer = (await rl.question("\nChoose a proposal to start: ")).trim();
|
|
63
|
+
const pick = Number(answer);
|
|
64
|
+
if (Number.isFinite(pick) && pick >= 1 && pick <= proposals.length)
|
|
65
|
+
return proposals[pick - 1];
|
|
66
|
+
console.log(`Please enter a number between 1 and ${proposals.length}.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
rl.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function confirm(promptText, defaultYes = true) {
|
|
74
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
75
|
+
return defaultYes;
|
|
76
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
77
|
+
try {
|
|
78
|
+
const answer = (await rl.question(promptText)).trim().toLowerCase();
|
|
79
|
+
if (!answer)
|
|
80
|
+
return defaultYes;
|
|
81
|
+
return answer === "y" || answer === "yes";
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
rl.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function promptValue(promptText, fallback) {
|
|
88
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
89
|
+
return fallback;
|
|
90
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
91
|
+
try {
|
|
92
|
+
const answer = (await rl.question(promptText)).trim();
|
|
93
|
+
return answer || fallback;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
rl.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function normalizeForMatch(value) {
|
|
100
|
+
return (value ?? "").trim().toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
function inferStackTemplate(proposal) {
|
|
103
|
+
const subtype = normalizeForMatch(proposal.application.entitySubtypeCode);
|
|
104
|
+
const haystack = [
|
|
105
|
+
proposal.application.name,
|
|
106
|
+
proposal.application.description,
|
|
107
|
+
proposal.application.entitySubtypeCode,
|
|
108
|
+
...proposal.technologyChoices.flatMap((tech) => [tech.name, tech.description, tech.entitySubtypeCode, tech.entityTypeCode]),
|
|
109
|
+
].map(normalizeForMatch).join(" ");
|
|
110
|
+
if (haystack.includes("next") || haystack.includes("next.js") || subtype.includes("web") || subtype.includes("portal") || subtype.includes("frontend")) {
|
|
111
|
+
return {
|
|
112
|
+
key: "next-web",
|
|
113
|
+
label: "Next.js web application",
|
|
114
|
+
runtime: "Node.js + Next.js",
|
|
115
|
+
devCommand: "npm run dev",
|
|
116
|
+
buildCommand: "npm run build",
|
|
117
|
+
entrySummary: "App Router web app with a simple landing page and architecture notes route.",
|
|
118
|
+
nextSteps: [
|
|
119
|
+
"Replace the placeholder app route with the real user journeys.",
|
|
120
|
+
"Connect the recommended services and data sources from the proposal.",
|
|
121
|
+
"Add policy control checks into CI before first release.",
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (haystack.includes("react")) {
|
|
126
|
+
return {
|
|
127
|
+
key: "react-web",
|
|
128
|
+
label: "React front-end application",
|
|
129
|
+
runtime: "Node.js + React",
|
|
130
|
+
devCommand: "npm run dev",
|
|
131
|
+
buildCommand: "npm run build",
|
|
132
|
+
entrySummary: "React single-page app starter with proposal notes embedded in the UI.",
|
|
133
|
+
nextSteps: [
|
|
134
|
+
"Replace the placeholder components with the real application screens.",
|
|
135
|
+
"Wire in API/data integrations from the selected technology stack.",
|
|
136
|
+
"Add UI and accessibility tests before first handover.",
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (haystack.includes("python") || haystack.includes("fastapi") || haystack.includes("django") || haystack.includes("flask")) {
|
|
141
|
+
return {
|
|
142
|
+
key: "python-api",
|
|
143
|
+
label: "Python API service",
|
|
144
|
+
runtime: "Python + FastAPI-style service",
|
|
145
|
+
devCommand: "python -m app.main",
|
|
146
|
+
buildCommand: "python -m compileall app",
|
|
147
|
+
entrySummary: "Python service scaffold with health endpoint and proposal docs.",
|
|
148
|
+
nextSteps: [
|
|
149
|
+
"Replace the placeholder endpoint with the application API surface.",
|
|
150
|
+
"Add dependency management and environment configuration for the target platform.",
|
|
151
|
+
"Implement the policy controls as startup checks, logging, and tests.",
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (haystack.includes("worker") || haystack.includes("queue") || haystack.includes("job") || subtype.includes("service")) {
|
|
156
|
+
return {
|
|
157
|
+
key: "worker-service",
|
|
158
|
+
label: "Background worker service",
|
|
159
|
+
runtime: "Node.js worker/service",
|
|
160
|
+
devCommand: "npm run dev",
|
|
161
|
+
buildCommand: "npm run build",
|
|
162
|
+
entrySummary: "Worker starter with job processor placeholder and operational runbook notes.",
|
|
163
|
+
nextSteps: [
|
|
164
|
+
"Replace the sample processor with the real workload handler.",
|
|
165
|
+
"Connect queue, scheduler, or event infrastructure from the chosen stack.",
|
|
166
|
+
"Add observability and retry controls before production use.",
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (haystack.includes("api") || haystack.includes("express") || haystack.includes("nest") || haystack.includes("service")) {
|
|
171
|
+
return {
|
|
172
|
+
key: "node-api",
|
|
173
|
+
label: "Node.js API service",
|
|
174
|
+
runtime: "Node.js + TypeScript API",
|
|
175
|
+
devCommand: "npm run dev",
|
|
176
|
+
buildCommand: "npm run build",
|
|
177
|
+
entrySummary: "TypeScript API starter with health route and proposal-backed service notes.",
|
|
178
|
+
nextSteps: [
|
|
179
|
+
"Replace the sample route with the real API contract.",
|
|
180
|
+
"Add persistence and integration adapters based on the selected technologies.",
|
|
181
|
+
"Implement automated tests and policy evidence capture in CI.",
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
key: "generic-ts",
|
|
187
|
+
label: "Generic TypeScript application",
|
|
188
|
+
runtime: "TypeScript starter",
|
|
189
|
+
devCommand: "npm run dev",
|
|
190
|
+
buildCommand: "npm run build",
|
|
191
|
+
entrySummary: "Lightweight TypeScript starter with proposal context and implementation placeholders.",
|
|
192
|
+
nextSteps: [
|
|
193
|
+
"Replace the starter files with the real application structure.",
|
|
194
|
+
"Bring in the frameworks and services recommended in the proposal.",
|
|
195
|
+
"Add delivery tests and policy evidence as implementation starts.",
|
|
196
|
+
],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function buildReadme(proposal, template) {
|
|
200
|
+
const technologies = proposal.technologyChoices.map((item) => item.name);
|
|
201
|
+
const policies = proposal.policyControls.map((item) => item.name);
|
|
202
|
+
return `# ${proposal.application.name}
|
|
203
|
+
|
|
204
|
+
${proposal.application.description ?? "Application scaffold generated from a NexArch proposal."}
|
|
205
|
+
|
|
206
|
+
## Starter template
|
|
207
|
+
|
|
208
|
+
- Template: ${template.label}
|
|
209
|
+
- Runtime: ${template.runtime}
|
|
210
|
+
- Suggested dev command: \`${template.devCommand}\`
|
|
211
|
+
- Suggested build command: \`${template.buildCommand}\`
|
|
212
|
+
- Starter shape: ${template.entrySummary}
|
|
213
|
+
|
|
214
|
+
## Proposal summary
|
|
215
|
+
|
|
216
|
+
- Workflow state: ${proposal.application.workflowState}
|
|
217
|
+
- Application subtype: ${proposal.application.entitySubtypeCode ?? "Not specified"}
|
|
218
|
+
- Recommendation basis: ${proposal.application.recommendationBasis ?? "Not specified"}
|
|
219
|
+
- NexArch reference: ${proposal.application.entityRef ?? proposal.application.id}
|
|
220
|
+
|
|
221
|
+
${proposal.application.recommendationSummary ? `## Recommendation notes\n\n${proposal.application.recommendationSummary}\n\n` : ""}## Suggested technology choices
|
|
222
|
+
|
|
223
|
+
${formatBulletList(technologies)}
|
|
224
|
+
|
|
225
|
+
## Policy controls to satisfy
|
|
226
|
+
|
|
227
|
+
${formatBulletList(policies)}
|
|
228
|
+
|
|
229
|
+
## Getting started
|
|
230
|
+
|
|
231
|
+
${template.nextSteps.map((step, index) => `${index + 1}. ${step}`).join("\n")}
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
function buildProposalMarkdown(proposal) {
|
|
235
|
+
const techLines = proposal.technologyChoices.map((tech) => `${tech.name}${tech.entitySubtypeCode ? ` (${tech.entitySubtypeCode})` : ""}${tech.description ? ` — ${tech.description}` : ""}`);
|
|
236
|
+
const policyLines = proposal.policyControls.map((control) => `${control.name}${control.description ? ` — ${control.description}` : ""}`);
|
|
237
|
+
return `# NexArch proposal context\n\n## Application\n\n- Name: ${proposal.application.name}\n- ID: ${proposal.application.id}\n- Reference: ${proposal.application.entityRef ?? "n/a"}\n- Subtype: ${proposal.application.entitySubtypeCode ?? "n/a"}\n- Description: ${proposal.application.description ?? "n/a"}\n- Recommendation basis: ${proposal.application.recommendationBasis ?? "n/a"}\n- Proposal source: ${proposal.proposal.proposalSource ?? "n/a"}\n\n${proposal.application.recommendationSummary ? `## Recommendation summary\n\n${proposal.application.recommendationSummary}\n\n` : ""}## Technology choices\n\n${formatBulletList(techLines)}\n\n## Policy controls\n\n${formatBulletList(policyLines)}\n`;
|
|
238
|
+
}
|
|
239
|
+
function buildPolicyMarkdown(proposal) {
|
|
240
|
+
if (proposal.policyControls.length === 0) {
|
|
241
|
+
return "# Policy controls\n\nNo explicit policy controls were linked to this proposal.\n";
|
|
242
|
+
}
|
|
243
|
+
return `# Policy controls\n\n${proposal.policyControls.map((control) => {
|
|
244
|
+
const guidance = trimText(typeof control.controlAttributes.summary === "string" ? control.controlAttributes.summary : null);
|
|
245
|
+
const rules = Array.isArray(control.controlAttributes.rules)
|
|
246
|
+
? control.controlAttributes.rules
|
|
247
|
+
.map((rule) => trimText(typeof rule.title === "string" ? rule.title : typeof rule.code === "string" ? rule.code : null))
|
|
248
|
+
.filter((value) => Boolean(value))
|
|
249
|
+
: [];
|
|
250
|
+
return `## ${control.name}\n\n${control.description ?? "No description provided."}\n\n${guidance ? `Guidance: ${guidance}\n\n` : ""}${rules.length > 0 ? `Rules\n\n${formatBulletList(rules)}\n` : ""}`;
|
|
251
|
+
}).join("\n")}`;
|
|
252
|
+
}
|
|
253
|
+
function buildTsConfig() {
|
|
254
|
+
return `{
|
|
255
|
+
"compilerOptions": {
|
|
256
|
+
"target": "ES2022",
|
|
257
|
+
"module": "NodeNext",
|
|
258
|
+
"moduleResolution": "NodeNext",
|
|
259
|
+
"strict": true,
|
|
260
|
+
"esModuleInterop": true,
|
|
261
|
+
"forceConsistentCasingInFileNames": true,
|
|
262
|
+
"skipLibCheck": true,
|
|
263
|
+
"outDir": "dist"
|
|
264
|
+
},
|
|
265
|
+
"include": ["src/**/*.ts"]
|
|
266
|
+
}
|
|
267
|
+
`;
|
|
268
|
+
}
|
|
269
|
+
function buildPackageJson(proposal, template) {
|
|
270
|
+
const base = {
|
|
271
|
+
name: slugify(proposal.application.name),
|
|
272
|
+
version: "0.1.0",
|
|
273
|
+
private: true,
|
|
274
|
+
description: proposal.application.description ?? `Scaffold for ${proposal.application.name}`,
|
|
275
|
+
};
|
|
276
|
+
if (template.key === "next-web") {
|
|
277
|
+
return `${safeJson({
|
|
278
|
+
...base,
|
|
279
|
+
scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint" },
|
|
280
|
+
dependencies: { next: "latest", react: "latest", "react-dom": "latest" },
|
|
281
|
+
})}\n`;
|
|
282
|
+
}
|
|
283
|
+
if (template.key === "react-web") {
|
|
284
|
+
return `${safeJson({
|
|
285
|
+
...base,
|
|
286
|
+
scripts: { dev: "vite", build: "vite build", preview: "vite preview" },
|
|
287
|
+
dependencies: { react: "latest", "react-dom": "latest" },
|
|
288
|
+
devDependencies: { typescript: "latest", vite: "latest" },
|
|
289
|
+
})}\n`;
|
|
290
|
+
}
|
|
291
|
+
if (template.key === "python-api") {
|
|
292
|
+
return `${safeJson({
|
|
293
|
+
...base,
|
|
294
|
+
note: "Python scaffold - see pyproject.toml for runtime dependencies.",
|
|
295
|
+
})}\n`;
|
|
296
|
+
}
|
|
297
|
+
return `${safeJson({
|
|
298
|
+
...base,
|
|
299
|
+
type: "module",
|
|
300
|
+
scripts: { dev: "node --watch src/index.ts", build: "tsc --noEmit false", start: "node dist/index.js" },
|
|
301
|
+
devDependencies: { typescript: "latest", "@types/node": "latest" },
|
|
302
|
+
})}\n`;
|
|
303
|
+
}
|
|
304
|
+
function buildStarterSource(proposal, template) {
|
|
305
|
+
const suffix = proposal.application.name.replace(/[^a-zA-Z0-9]+/g, "") || "Application";
|
|
306
|
+
switch (template.key) {
|
|
307
|
+
case "next-web":
|
|
308
|
+
return [
|
|
309
|
+
{
|
|
310
|
+
relativePath: "src/app/page.tsx",
|
|
311
|
+
content: `export default function Home() {
|
|
312
|
+
return (
|
|
313
|
+
<main style={{ padding: 32, fontFamily: "Arial, sans-serif" }}>
|
|
314
|
+
<h1>${proposal.application.name}</h1>
|
|
315
|
+
<p>${proposal.application.description ?? "Application scaffold generated from a NexArch proposal."}</p>
|
|
316
|
+
<p>Start here by replacing this page with the real user journey.</p>
|
|
317
|
+
</main>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
`,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
relativePath: "src/app/layout.tsx",
|
|
324
|
+
content: `export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
325
|
+
return (
|
|
326
|
+
<html lang="en">
|
|
327
|
+
<body>{children}</body>
|
|
328
|
+
</html>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
`,
|
|
332
|
+
},
|
|
333
|
+
];
|
|
334
|
+
case "react-web":
|
|
335
|
+
return [
|
|
336
|
+
{
|
|
337
|
+
relativePath: "src/main.tsx",
|
|
338
|
+
content: `import React from "react";
|
|
339
|
+
import ReactDOM from "react-dom/client";
|
|
340
|
+
import { App } from "./App";
|
|
341
|
+
|
|
342
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
343
|
+
<React.StrictMode>
|
|
344
|
+
<App />
|
|
345
|
+
</React.StrictMode>,
|
|
346
|
+
);
|
|
347
|
+
`,
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
relativePath: "src/App.tsx",
|
|
351
|
+
content: `export function App() {
|
|
352
|
+
return (
|
|
353
|
+
<main style={{ padding: 32, fontFamily: "Arial, sans-serif" }}>
|
|
354
|
+
<h1>${proposal.application.name}</h1>
|
|
355
|
+
<p>${proposal.application.description ?? "Application scaffold generated from a NexArch proposal."}</p>
|
|
356
|
+
</main>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
`,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
relativePath: "index.html",
|
|
363
|
+
content: `<!doctype html>
|
|
364
|
+
<html lang="en">
|
|
365
|
+
<head>
|
|
366
|
+
<meta charset="UTF-8" />
|
|
367
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
368
|
+
<title>${proposal.application.name}</title>
|
|
369
|
+
</head>
|
|
370
|
+
<body>
|
|
371
|
+
<div id="root"></div>
|
|
372
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
373
|
+
</body>
|
|
374
|
+
</html>
|
|
375
|
+
`,
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
case "python-api":
|
|
379
|
+
return [
|
|
380
|
+
{
|
|
381
|
+
relativePath: "app/main.py",
|
|
382
|
+
content: `def healthcheck() -> dict[str, str]:
|
|
383
|
+
return {"status": "ok", "application": ${JSON.stringify(proposal.application.name)}}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
print(healthcheck())
|
|
388
|
+
`,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
relativePath: "pyproject.toml",
|
|
392
|
+
content: `[project]
|
|
393
|
+
name = "${slugify(proposal.application.name)}"
|
|
394
|
+
version = "0.1.0"
|
|
395
|
+
description = ${JSON.stringify(proposal.application.description ?? `Scaffold for ${proposal.application.name}`)}
|
|
396
|
+
requires-python = ">=3.11"
|
|
397
|
+
dependencies = []
|
|
398
|
+
`,
|
|
399
|
+
},
|
|
400
|
+
];
|
|
401
|
+
case "worker-service":
|
|
402
|
+
return [
|
|
403
|
+
{
|
|
404
|
+
relativePath: "src/index.ts",
|
|
405
|
+
content: `export async function start${suffix}Worker(): Promise<void> {
|
|
406
|
+
console.log(${JSON.stringify(`Worker scaffold ready for ${proposal.application.name}`)});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
start${suffix}Worker().catch((error) => {
|
|
410
|
+
console.error(error);
|
|
411
|
+
process.exitCode = 1;
|
|
412
|
+
});
|
|
413
|
+
`,
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
relativePath: "src/jobs/example-job.ts",
|
|
417
|
+
content: `export async function processExampleJob(): Promise<void> {
|
|
418
|
+
console.log("Replace this with the real job handler.");
|
|
419
|
+
}
|
|
420
|
+
`,
|
|
421
|
+
},
|
|
422
|
+
];
|
|
423
|
+
case "node-api":
|
|
424
|
+
return [
|
|
425
|
+
{
|
|
426
|
+
relativePath: "src/index.ts",
|
|
427
|
+
content: `import { createServer } from "node:http";
|
|
428
|
+
|
|
429
|
+
const server = createServer((_req, res) => {
|
|
430
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
431
|
+
res.end(JSON.stringify({ status: "ok", application: ${JSON.stringify(proposal.application.name)} }));
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
server.listen(3000, () => {
|
|
435
|
+
console.log("API starter listening on http://localhost:3000");
|
|
436
|
+
});
|
|
437
|
+
`,
|
|
438
|
+
},
|
|
439
|
+
];
|
|
440
|
+
default:
|
|
441
|
+
return [
|
|
442
|
+
{
|
|
443
|
+
relativePath: "src/index.ts",
|
|
444
|
+
content: `export function start${suffix}App(): void {
|
|
445
|
+
console.log(${JSON.stringify(`Start building ${proposal.application.name}`)});
|
|
446
|
+
}
|
|
447
|
+
`,
|
|
448
|
+
},
|
|
449
|
+
];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function buildTemplateFiles(proposal, template) {
|
|
453
|
+
const shared = [
|
|
454
|
+
{ relativePath: ".gitignore", content: "node_modules/\ndist/\n.env\n.DS_Store\n.venv/\n" },
|
|
455
|
+
{ relativePath: ".editorconfig", content: "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n" },
|
|
456
|
+
{ relativePath: "README.md", content: buildReadme(proposal, template) },
|
|
457
|
+
{ relativePath: "docs/architecture/nexarch-proposal.md", content: buildProposalMarkdown(proposal) },
|
|
458
|
+
{ relativePath: "docs/architecture/policy-controls.md", content: buildPolicyMarkdown(proposal) },
|
|
459
|
+
{ relativePath: ".nexarch/proposal.json", content: `${safeJson({ ...proposal, scaffoldTemplate: template })}\n` },
|
|
460
|
+
{ relativePath: "tests/README.md", content: "Add delivery tests for the new application here.\n" },
|
|
461
|
+
{ relativePath: ".env.example", content: "# Add environment variables for this application here\n" },
|
|
462
|
+
];
|
|
463
|
+
shared.push({ relativePath: "package.json", content: buildPackageJson(proposal, template) });
|
|
464
|
+
if (["node-api", "worker-service", "generic-ts"].includes(template.key)) {
|
|
465
|
+
shared.push({ relativePath: "tsconfig.json", content: buildTsConfig() });
|
|
466
|
+
}
|
|
467
|
+
return [...shared, ...buildStarterSource(proposal, template)];
|
|
468
|
+
}
|
|
469
|
+
function scaffoldProposal(targetDir, proposal) {
|
|
470
|
+
const template = inferStackTemplate(proposal);
|
|
471
|
+
const files = buildTemplateFiles(proposal, template);
|
|
472
|
+
for (const file of files) {
|
|
473
|
+
const outputPath = join(targetDir, file.relativePath);
|
|
474
|
+
ensureDir(dirname(outputPath));
|
|
475
|
+
writeFileSync(outputPath, file.content, "utf8");
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
createdFiles: files.map((file) => join(targetDir, file.relativePath)),
|
|
479
|
+
template,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
export async function proposalsStart(args) {
|
|
483
|
+
const asJson = parseFlag(args, "--json");
|
|
484
|
+
const nonInteractive = parseFlag(args, "--non-interactive");
|
|
485
|
+
const force = parseFlag(args, "--force");
|
|
486
|
+
const skipActivate = parseFlag(args, "--skip-activate");
|
|
487
|
+
const activateFlag = parseFlag(args, "--activate");
|
|
488
|
+
const applicationId = parseOptionValue(args, "--id") ?? parseOptionValue(args, "--application-id");
|
|
489
|
+
const targetDirArg = parseOptionValue(args, "--dir");
|
|
490
|
+
const reason = parseOptionValue(args, "--reason") ?? "Application scaffold started from approved proposal workflow";
|
|
491
|
+
const repoUrl = parseOptionValue(args, "--repo");
|
|
492
|
+
const creds = requireCredentials();
|
|
493
|
+
const listRaw = await callMcpTool("nexarch_list_proposed_applications", { companyId: creds.companyId }, { companyId: creds.companyId });
|
|
494
|
+
const list = parseToolText(listRaw);
|
|
495
|
+
const proposals = list.proposals ?? [];
|
|
496
|
+
if (proposals.length === 0) {
|
|
497
|
+
const payload = { ok: true, proposals: [], message: "No proposed applications found." };
|
|
498
|
+
if (asJson)
|
|
499
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
500
|
+
else
|
|
501
|
+
console.log(payload.message);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
let chosen = applicationId
|
|
505
|
+
? proposals.find((proposal) => proposal.id === applicationId)
|
|
506
|
+
: undefined;
|
|
507
|
+
if (!chosen && applicationId) {
|
|
508
|
+
throw new Error(`Proposal not found: ${applicationId}`);
|
|
509
|
+
}
|
|
510
|
+
if (!chosen) {
|
|
511
|
+
if (nonInteractive || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
512
|
+
if (proposals.length === 1) {
|
|
513
|
+
chosen = proposals[0];
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
throw new Error("Multiple proposed applications found. Re-run with --id <applicationId>.");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
chosen = await promptChoice(proposals);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const getRaw = await callMcpTool("nexarch_get_proposed_application", { applicationId: chosen.id, companyId: creds.companyId }, { companyId: creds.companyId });
|
|
524
|
+
const getResult = parseToolText(getRaw);
|
|
525
|
+
const proposal = getResult.proposal;
|
|
526
|
+
if (!proposal)
|
|
527
|
+
throw new Error("Proposal context was not returned by MCP gateway.");
|
|
528
|
+
const suggestedDir = resolvePath(targetDirArg ?? slugify(proposal.application.name));
|
|
529
|
+
const targetDir = nonInteractive || targetDirArg
|
|
530
|
+
? suggestedDir
|
|
531
|
+
: resolvePath(await promptValue(`Target directory [${suggestedDir}]: `, suggestedDir));
|
|
532
|
+
if (existsSync(targetDir) && !isDirectoryPath(targetDir)) {
|
|
533
|
+
throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
534
|
+
}
|
|
535
|
+
if (existsSync(targetDir) && !isDirectoryEmpty(targetDir) && !force) {
|
|
536
|
+
throw new Error(`Target directory is not empty: ${targetDir}. Re-run with --force to allow writing into it.`);
|
|
537
|
+
}
|
|
538
|
+
const scaffold = scaffoldProposal(targetDir, proposal);
|
|
539
|
+
let activated = null;
|
|
540
|
+
const shouldActivate = skipActivate
|
|
541
|
+
? false
|
|
542
|
+
: activateFlag || nonInteractive || !process.stdin.isTTY || !process.stdout.isTTY
|
|
543
|
+
? true
|
|
544
|
+
: await confirm("Activate this proposal in NexArch now? [Y/n]: ", true);
|
|
545
|
+
if (shouldActivate) {
|
|
546
|
+
const activateRaw = await callMcpTool("nexarch_activate_proposed_application", {
|
|
547
|
+
applicationId: proposal.application.id,
|
|
548
|
+
companyId: creds.companyId,
|
|
549
|
+
reason,
|
|
550
|
+
scaffold: {
|
|
551
|
+
command: "nexarch proposals start",
|
|
552
|
+
template: scaffold.template,
|
|
553
|
+
targetDir,
|
|
554
|
+
repoUrl: trimText(repoUrl),
|
|
555
|
+
createdAt: new Date().toISOString(),
|
|
556
|
+
createdFiles: scaffold.createdFiles,
|
|
557
|
+
},
|
|
558
|
+
agentContext: {
|
|
559
|
+
initiatedBy: "nexarch-cli",
|
|
560
|
+
command: "proposals start",
|
|
561
|
+
operatorEmail: creds.email,
|
|
562
|
+
},
|
|
563
|
+
}, { companyId: creds.companyId });
|
|
564
|
+
activated = parseToolText(activateRaw);
|
|
565
|
+
}
|
|
566
|
+
const payload = {
|
|
567
|
+
ok: true,
|
|
568
|
+
proposalId: proposal.application.id,
|
|
569
|
+
proposalName: proposal.application.name,
|
|
570
|
+
targetDir,
|
|
571
|
+
activated,
|
|
572
|
+
scaffold,
|
|
573
|
+
};
|
|
574
|
+
if (asJson) {
|
|
575
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
console.log(`\nStarted ${proposal.application.name}`);
|
|
579
|
+
console.log(`- Proposal: ${proposal.application.id}`);
|
|
580
|
+
console.log(`- Folder : ${targetDir}`);
|
|
581
|
+
console.log(`- Template: ${scaffold.template.label}`);
|
|
582
|
+
console.log(`- Runtime : ${scaffold.template.runtime}`);
|
|
583
|
+
console.log(`- Files : ${scaffold.createdFiles.length} created`);
|
|
584
|
+
console.log(`- State : ${activated ? `${activated.status}${activated.alreadyActive ? " (already active)" : ""}` : "left in proposed state"}`);
|
|
585
|
+
console.log("\nNext steps:");
|
|
586
|
+
scaffold.template.nextSteps.forEach((step, index) => console.log(`${index + 1}. ${step}`));
|
|
587
|
+
console.log(`${scaffold.template.nextSteps.length + 1}. Use docs/architecture/nexarch-proposal.md and policy-controls.md while building.`);
|
|
588
|
+
console.log(`${scaffold.template.nextSteps.length + 2}. Check the delivery back into NexArch when implementation is underway.`);
|
|
589
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,10 @@ import { commandClaim } from "./commands/command-claim.js";
|
|
|
21
21
|
import { policyControls } from "./commands/policy-controls.js";
|
|
22
22
|
import { policyAuditTemplate } from "./commands/policy-audit-template.js";
|
|
23
23
|
import { policyAuditSubmit } from "./commands/policy-audit-submit.js";
|
|
24
|
+
import { policyAuditResults } from "./commands/policy-audit-results.js";
|
|
25
|
+
import { appliedPolicies } from "./commands/applied-policies.js";
|
|
26
|
+
import { governanceSummary } from "./commands/governance-summary.js";
|
|
27
|
+
import { proposalsStart } from "./commands/proposals-start.js";
|
|
24
28
|
const [, , command, ...args] = process.argv;
|
|
25
29
|
const commands = {
|
|
26
30
|
login,
|
|
@@ -46,6 +50,9 @@ const commands = {
|
|
|
46
50
|
"policy-controls": policyControls,
|
|
47
51
|
"policy-audit-template": policyAuditTemplate,
|
|
48
52
|
"policy-audit-submit": policyAuditSubmit,
|
|
53
|
+
"policy-audit-results": policyAuditResults,
|
|
54
|
+
"applied-policies": appliedPolicies,
|
|
55
|
+
"governance-summary": governanceSummary,
|
|
49
56
|
};
|
|
50
57
|
async function main() {
|
|
51
58
|
if (command === "agent") {
|
|
@@ -55,6 +62,13 @@ async function main() {
|
|
|
55
62
|
return;
|
|
56
63
|
}
|
|
57
64
|
}
|
|
65
|
+
if (command === "proposals") {
|
|
66
|
+
const [subcommand, ...subArgs] = args;
|
|
67
|
+
if (subcommand === "start") {
|
|
68
|
+
await proposalsStart(subArgs);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
58
72
|
const handler = commands[command ?? ""];
|
|
59
73
|
if (!handler) {
|
|
60
74
|
console.log(`
|
|
@@ -166,9 +180,24 @@ Usage:
|
|
|
166
180
|
--to <toExternalKey>
|
|
167
181
|
--limit <1-500>
|
|
168
182
|
--json
|
|
169
|
-
nexarch check-in Preview pending application-target commands (no auto-claim)
|
|
183
|
+
nexarch check-in Preview pending application-target commands (no auto-claim)
|
|
184
|
+
and report any applications currently in draft state so the
|
|
185
|
+
agent can prompt the user to explore and instantiate them.
|
|
170
186
|
Scope is resolved server-side from active company context.
|
|
171
187
|
Options: --agent-key <key> override stored agent key
|
|
188
|
+
--json JSON output includes draftApplications[]
|
|
189
|
+
nexarch proposals start
|
|
190
|
+
Start a new application workspace from a proposed NexArch app.
|
|
191
|
+
Lists proposed apps, lets you choose one, writes a starter
|
|
192
|
+
project scaffold, and activates the proposal to active.
|
|
193
|
+
Options: --id <applicationId>
|
|
194
|
+
--dir <path>
|
|
195
|
+
--reason <text>
|
|
196
|
+
--repo <url>
|
|
197
|
+
--skip-activate
|
|
198
|
+
--activate
|
|
199
|
+
--force
|
|
200
|
+
--non-interactive
|
|
172
201
|
--json
|
|
173
202
|
nexarch command-claim
|
|
174
203
|
Explicitly claim a pending command by ID.
|
|
@@ -206,6 +235,20 @@ Usage:
|
|
|
206
235
|
--findings-json <json-array>
|
|
207
236
|
--findings-file <path.json>
|
|
208
237
|
--json
|
|
238
|
+
nexarch policy-audit-results
|
|
239
|
+
Retrieve stored results of previous policy audits for an application.
|
|
240
|
+
Options: --entity <applicationEntityRef> (required)
|
|
241
|
+
--limit <1-10> (default 1)
|
|
242
|
+
--json
|
|
243
|
+
nexarch applied-policies
|
|
244
|
+
List policy documents applied to this company account.
|
|
245
|
+
Options: --pack <packCode> filter to a specific pack
|
|
246
|
+
--markdown include full document markdown
|
|
247
|
+
--json
|
|
248
|
+
nexarch governance-summary
|
|
249
|
+
Print review queue, graph stats, and per-application policy
|
|
250
|
+
audit rollup (latest run status, pass/partial/fail counts).
|
|
251
|
+
Options: --json
|
|
209
252
|
`);
|
|
210
253
|
process.exit(command ? 1 : 0);
|
|
211
254
|
}
|
package/dist/lib/mcp.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import https from "https";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { dirname, join } from "path";
|
|
2
5
|
import { requireCredentials } from "./credentials.js";
|
|
3
6
|
const MCP_GATEWAY_URL = "https://mcp.nexarch.ai";
|
|
7
|
+
function readCliVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkgPath = join(here, "..", "..", "package.json");
|
|
11
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
12
|
+
return pkg.version ?? "0.0.0";
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return "0.0.0";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const CLI_VERSION = readCliVersion();
|
|
4
19
|
const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.NEXARCH_MCP_TIMEOUT_MS ?? "90000", 10) || 90_000;
|
|
5
20
|
const REQUEST_RETRIES = Math.max(0, Number.parseInt(process.env.NEXARCH_MCP_RETRIES ?? "2", 10) || 2);
|
|
6
21
|
function sleep(ms) {
|
|
@@ -97,7 +112,7 @@ export async function mcpInitialize(options = {}) {
|
|
|
97
112
|
return callMcpRpc("initialize", {
|
|
98
113
|
protocolVersion: "2024-11-05",
|
|
99
114
|
capabilities: {},
|
|
100
|
-
clientInfo: { name: "nexarch-cli", version:
|
|
115
|
+
clientInfo: { name: "nexarch-cli", version: CLI_VERSION },
|
|
101
116
|
}, options);
|
|
102
117
|
}
|
|
103
118
|
export async function mcpListTools(options = {}) {
|