@clearlydev/nx-archweave 0.1.1 → 0.2.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/README.md +1 -1
- package/dist/eslint/fsd-boundaries.d.ts +25 -8
- package/dist/eslint/fsd-policy.d.ts +38 -0
- package/dist/eslint/index.d.ts +10 -9
- package/dist/eslint/index.js +227 -34
- package/dist/eslint/slice-groups.d.ts +12 -0
- package/dist/generators/add-agent-rules/generator.js +1175 -23
- package/dist/generators/add-backend/generator.js +1127 -75
- package/dist/generators/add-fsd-lite/generator.js +1274 -96
- package/dist/generators/add-review/files/dependabot.yml +7 -0
- package/dist/generators/add-review/generator.js +1100 -46
- package/dist/generators/doctor/generator.js +780 -96
- package/dist/generators/entity/generator.js +251 -32
- package/dist/generators/feature/generator.js +251 -32
- package/dist/generators/init/generator.js +1955 -549
- package/dist/generators/page/generator.js +251 -32
- package/dist/generators/shared/generator.js +251 -32
- package/dist/generators/widget/generator.js +251 -32
- package/package.json +4 -1
- package/src/generators/add-agent-rules/schema.json +5 -0
- package/src/generators/add-fsd-lite/schema.json +2 -2
- package/src/generators/entity/schema.json +11 -1
- package/src/generators/feature/schema.json +11 -1
- package/src/generators/init/schema.json +13 -2
- package/src/generators/page/schema.json +15 -3
- package/src/generators/shared/schema.json +7 -1
- package/src/generators/widget/schema.json +11 -1
- package/dist/eslint/fsd-boundaries.js +0 -144
- package/dist/eslint/tsconfig-aliases.js +0 -114
- package/dist/generators/add-agent-rules/render.js +0 -46
- package/dist/generators/add-backend/render.js +0 -102
- package/dist/generators/add-fsd-lite/render.js +0 -72
- package/dist/generators/add-github-templates/render.js +0 -26
- package/dist/generators/add-integration/render.js +0 -34
- package/dist/generators/add-review/render.js +0 -96
- package/dist/generators/allow-builds/render.js +0 -82
- package/dist/generators/doctor/integration-report.js +0 -70
- package/dist/generators/doctor/render.js +0 -10
- package/dist/generators/init/render.js +0 -125
- package/dist/generators/init/validation.js +0 -74
- package/dist/generators/install-integration/render.js +0 -128
- package/dist/generators/record-integration/render.js +0 -31
- package/dist/generators/shared/dry-run.js +0 -12
- package/dist/generators/shared/frontend-app.js +0 -57
- package/dist/generators/shared/project-type.js +0 -39
- package/dist/generators/shared/record-pipeline.js +0 -34
- package/dist/generators/shared/run-nx.js +0 -24
- package/dist/generators/shared/template-files.js +0 -38
- package/dist/generators/shared/toolkit-config-file.js +0 -110
- package/dist/generators/slice/core.js +0 -185
- package/dist/generators/slice/render.js +0 -70
- package/dist/lib/nx-plugin.js +0 -7
- package/dist/runtime/integrations-composition-root.js +0 -20
- package/dist/runtime/node-command-runner.js +0 -81
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { yamlPackageKey } from '@nx-archweave/template-engine';
|
|
2
|
-
const ALLOW_BUILDS_LABEL = {
|
|
3
|
-
allowed: 'already true',
|
|
4
|
-
denied: 'false → true',
|
|
5
|
-
pending: 'awaiting a decision → true',
|
|
6
|
-
other: 'another value → true',
|
|
7
|
-
absent: 'added (true)',
|
|
8
|
-
};
|
|
9
|
-
export function renderAllowBuildsReport(report) {
|
|
10
|
-
const rebuild = `pnpm rebuild ${report.packages.join(' ')}`;
|
|
11
|
-
if (report.refused === undefined) {
|
|
12
|
-
const lines = [
|
|
13
|
-
`allow-builds — ${report.applied ? 'applied' : 'preview (nothing written)'}`,
|
|
14
|
-
'',
|
|
15
|
-
`pnpm-workspace.yaml › ${report.target}`,
|
|
16
|
-
...report.changes.map((change) => ` ${change.name}: ${label(report.target, change.previous)}`),
|
|
17
|
-
'',
|
|
18
|
-
];
|
|
19
|
-
if (report.namesUnchecked)
|
|
20
|
-
lines.push('The names were not checked: there is no pnpm-lock.yaml.', '');
|
|
21
|
-
if (report.applied) {
|
|
22
|
-
lines.push(`Running: ${rebuild}`);
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
lines.push(`Then: ${rebuild}`, "Allowing a build lets that package's install script run with your user's permissions.", '', `To apply: pnpm nx g @clearlydev/nx-archweave:allow-builds --packages=${report.packages.join(',')} --apply`);
|
|
26
|
-
}
|
|
27
|
-
return lines.join('\n');
|
|
28
|
-
}
|
|
29
|
-
const head = 'allow-builds — refused (nothing written)';
|
|
30
|
-
switch (report.refused) {
|
|
31
|
-
case 'npm':
|
|
32
|
-
return [
|
|
33
|
-
head,
|
|
34
|
-
'This is an npm workspace: allow-builds edits pnpm settings only. npm runs dependency install scripts',
|
|
35
|
-
'itself unless ignore-scripts is set, so there is nothing to re-allow; npm 11 records an approval with',
|
|
36
|
-
` npm install-scripts approve ${report.packages.join(' ')}`,
|
|
37
|
-
].join('\n');
|
|
38
|
-
case 'yarn':
|
|
39
|
-
return [
|
|
40
|
-
head,
|
|
41
|
-
'This is a yarn workspace: allow-builds edits pnpm settings only. yarn classic runs dependency',
|
|
42
|
-
'install scripts itself; yarn berry runs them unless .yarnrc.yml sets enableScripts: false —',
|
|
43
|
-
'remove that line (or set it to true) and run `yarn install` to let them run.',
|
|
44
|
-
].join('\n');
|
|
45
|
-
case 'bun':
|
|
46
|
-
return [
|
|
47
|
-
head,
|
|
48
|
-
'This is a bun workspace: allow-builds edits pnpm settings only. bun blocks the install scripts of',
|
|
49
|
-
'dependencies outside its trusted list; `bun pm untrusted` lists the blocked ones, and',
|
|
50
|
-
` bun pm trust ${report.packages.join(' ')}`,
|
|
51
|
-
'runs their scripts and records them in package.json trustedDependencies.',
|
|
52
|
-
].join('\n');
|
|
53
|
-
case 'no-package-manager':
|
|
54
|
-
return [head, 'There is no pnpm-lock.yaml or pnpm-workspace.yaml here — allow-builds edits pnpm settings only.'].join('\n');
|
|
55
|
-
case 'no-workspace-yaml':
|
|
56
|
-
return [
|
|
57
|
-
head,
|
|
58
|
-
'This pnpm project has no pnpm-workspace.yaml, where pnpm keeps its build decisions. Add one with',
|
|
59
|
-
' allowBuilds:',
|
|
60
|
-
...report.packages.map((name) => ` ${yamlPackageKey(name)}: true`),
|
|
61
|
-
`then run "${rebuild}".`,
|
|
62
|
-
].join('\n');
|
|
63
|
-
case 'unknown-packages':
|
|
64
|
-
return [
|
|
65
|
-
head,
|
|
66
|
-
`These packages are not in pnpm-lock.yaml: ${report.unknown.join(', ')} — check the names; only an installed package has a build to run.`,
|
|
67
|
-
].join('\n');
|
|
68
|
-
case 'unrecognized':
|
|
69
|
-
return [
|
|
70
|
-
head,
|
|
71
|
-
`pnpm-workspace.yaml is in a shape allow-builds does not edit: ${report.reason}.`,
|
|
72
|
-
`Instead, allow ${report.packages.join(', ')} in pnpm-workspace.yaml yourself — "<package>: true" under allowBuilds`,
|
|
73
|
-
`(pnpm 11; pnpm 10 from 10.28), or a "- <package>" item under onlyBuiltDependencies on older pnpm 10 — then run "${rebuild}".`,
|
|
74
|
-
].join('\n');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
function label(target, previous) {
|
|
78
|
-
if (target === 'allowBuilds')
|
|
79
|
-
return ALLOW_BUILDS_LABEL[previous];
|
|
80
|
-
// a list item is either there or not — denied / pending / other only exist in allowBuilds
|
|
81
|
-
return previous === 'allowed' ? 'already listed' : 'added to the list';
|
|
82
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { computeOverallStatus } from '@nx-archweave/shared';
|
|
2
|
-
import { runDoctorChecks } from '@nx-archweave/template-engine';
|
|
3
|
-
import { detectIntegrationsDrift, readEnabledIntegrationIds, buildIntegrationDoctorChecks, classifyIntegrationDrift, loadManifest, detectWorkspacePackage, verifyClaudeMarketplacePlugin, PLUGIN_CATALOG, } from '@nx-archweave/integrations';
|
|
4
|
-
import { createIntegrationsCompositionRoot, } from "../../runtime/integrations-composition-root.js";
|
|
5
|
-
import { diskRunNx } from "../shared/run-nx.js";
|
|
6
|
-
// Only claude-code is a supported target for now — doctor
|
|
7
|
-
// does not yet accept a --target option, so this is the one hardcoded value.
|
|
8
|
-
const DOCTOR_TARGET_ENVIRONMENT = 'claude-code';
|
|
9
|
-
export async function buildDoctorReportWithIntegrations(root, options = {}, deps = {}) {
|
|
10
|
-
const integrationsRoot = deps.integrationsRoot ?? createIntegrationsCompositionRoot();
|
|
11
|
-
const baseReport = runDoctorChecks(root);
|
|
12
|
-
const declaredIds = readEnabledIntegrationIds(root);
|
|
13
|
-
const runNx = diskRunNx(root);
|
|
14
|
-
const driftResults = await detectIntegrationsDrift({
|
|
15
|
-
workspaceRoot: root,
|
|
16
|
-
targetEnvironment: DOCTOR_TARGET_ENVIRONMENT,
|
|
17
|
-
declaredIds,
|
|
18
|
-
integrations: integrationsRoot.listIntegrations(),
|
|
19
|
-
runNx,
|
|
20
|
-
});
|
|
21
|
-
// Without --live-integrations: no ReadOnlyCommandRunner call happens at
|
|
22
|
-
// all — doctor stays fast and available even where the claude CLI is not
|
|
23
|
-
// installed, trusting the recorded manifest state exactly as before.
|
|
24
|
-
const finalDriftResults = options.liveIntegrations
|
|
25
|
-
? await applyLiveVerification(driftResults, {
|
|
26
|
-
root,
|
|
27
|
-
declaredIds,
|
|
28
|
-
runner: integrationsRoot.runner,
|
|
29
|
-
runNx,
|
|
30
|
-
})
|
|
31
|
-
: driftResults;
|
|
32
|
-
const integrationChecks = buildIntegrationDoctorChecks(finalDriftResults);
|
|
33
|
-
const checks = [...baseReport.checks, ...integrationChecks];
|
|
34
|
-
return { checks, overallStatus: computeOverallStatus(checks) };
|
|
35
|
-
}
|
|
36
|
-
// Re-derives each catalog plugin's drift result using a live verification signal in
|
|
37
|
-
// place of the manifest's frozen `verification` field — every other input to
|
|
38
|
-
// classifyIntegrationDrift (declared, detected) is unchanged, so this never
|
|
39
|
-
// duplicates the classification logic itself, only substitutes one input to
|
|
40
|
-
// it. This is still fully read-only: no manifest or config write, no repair.
|
|
41
|
-
async function applyLiveVerification(driftResults, ctx) {
|
|
42
|
-
const context = { workspaceRoot: ctx.root, targetEnvironment: DOCTOR_TARGET_ENVIRONMENT };
|
|
43
|
-
const manifest = loadManifest(ctx.root);
|
|
44
|
-
const live = new Map();
|
|
45
|
-
// Only the plugins doctor reports on, and only those this target can verify — each live
|
|
46
|
-
// check is one `claude plugin list` call.
|
|
47
|
-
const reported = new Set(driftResults.map((result) => result.id));
|
|
48
|
-
for (const entry of PLUGIN_CATALOG) {
|
|
49
|
-
if (!reported.has(entry.id) || entry.hosts[DOCTOR_TARGET_ENVIRONMENT] === undefined)
|
|
50
|
-
continue;
|
|
51
|
-
const existingEntry = manifest.integrations[entry.id];
|
|
52
|
-
const detected = await detectWorkspacePackage(entry, context);
|
|
53
|
-
const { result: liveVerification } = await verifyClaudeMarketplacePlugin(entry, { runner: ctx.runner }, context);
|
|
54
|
-
// A stale recorded "passed" must never survive a current live failure —
|
|
55
|
-
// the live result always wins over the recorded one
|
|
56
|
-
// when --live-integrations is set, never the other way around.
|
|
57
|
-
const liveEntry = existingEntry
|
|
58
|
-
? { ...existingEntry, verification: liveVerification.verified ? 'passed' : 'failed' }
|
|
59
|
-
: existingEntry;
|
|
60
|
-
live.set(entry.id, classifyIntegrationDrift({
|
|
61
|
-
id: entry.id,
|
|
62
|
-
installable: true,
|
|
63
|
-
declared: ctx.declaredIds.includes(entry.id),
|
|
64
|
-
manifestEntry: liveEntry,
|
|
65
|
-
detected,
|
|
66
|
-
runNx: ctx.runNx,
|
|
67
|
-
}));
|
|
68
|
-
}
|
|
69
|
-
return driftResults.map((result) => live.get(result.id) ?? result);
|
|
70
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export function renderDoctorReport(report) {
|
|
2
|
-
const lines = [`Overall status: ${report.overallStatus}`, ''];
|
|
3
|
-
for (const check of report.checks) {
|
|
4
|
-
lines.push(`[${check.status.toUpperCase()}] ${check.label}: ${check.message}`);
|
|
5
|
-
if (check.suggestedAction) {
|
|
6
|
-
lines.push(` → ${check.suggestedAction}`);
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
return lines.join('\n');
|
|
10
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { packageManagerCommands } from '@nx-archweave/template-engine';
|
|
2
|
-
import { renderAddFsdLiteReport } from "../add-fsd-lite/render.js";
|
|
3
|
-
import { renderAddAgentRulesReport } from "../add-agent-rules/render.js";
|
|
4
|
-
import { renderAddGithubTemplatesReport } from "../add-github-templates/render.js";
|
|
5
|
-
import { renderAddReviewReport } from "../add-review/render.js";
|
|
6
|
-
import { renderAddBackendBody } from "../add-backend/render.js";
|
|
7
|
-
const SECTION_TITLES = {
|
|
8
|
-
fsd: 'add-fsd-lite',
|
|
9
|
-
agentRules: 'add-agent-rules',
|
|
10
|
-
githubTemplates: 'add-github-templates',
|
|
11
|
-
review: 'add-review',
|
|
12
|
-
backend: 'add-backend',
|
|
13
|
-
};
|
|
14
|
-
function renderSectionReport(report) {
|
|
15
|
-
switch (report.kind) {
|
|
16
|
-
case 'fsd':
|
|
17
|
-
return renderAddFsdLiteReport(report.report);
|
|
18
|
-
case 'agentRules':
|
|
19
|
-
return renderAddAgentRulesReport(report.report);
|
|
20
|
-
case 'githubTemplates':
|
|
21
|
-
return renderAddGithubTemplatesReport(report.report);
|
|
22
|
-
case 'review':
|
|
23
|
-
return renderAddReviewReport(report.report);
|
|
24
|
-
case 'backend':
|
|
25
|
-
return renderAddBackendBody(report.report);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
/** Whether a section left the run in a state that only a `git restore .` can cleanly
|
|
29
|
-
* undo — i.e. it aborted outright. Sections that merely proposed/skipped a file (no
|
|
30
|
-
* write happened) do not count: there is nothing of theirs to roll back. */
|
|
31
|
-
function sectionAborted(section) {
|
|
32
|
-
if (section.thrownError !== undefined)
|
|
33
|
-
return true;
|
|
34
|
-
if (section.report === null)
|
|
35
|
-
return false;
|
|
36
|
-
if (section.report.kind === 'fsd')
|
|
37
|
-
return section.report.report.aborted;
|
|
38
|
-
if (section.report.kind === 'review')
|
|
39
|
-
return section.report.report.workflowAction === 'aborted';
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
export function renderInitReport(report) {
|
|
43
|
-
const lines = [];
|
|
44
|
-
const backendSuffix = report.backend === 'none' ? '' : `, backend: ${report.backend}`;
|
|
45
|
-
const header = `init (preset: ${report.preset}, frontend: ${report.frontend}${backendSuffix})`;
|
|
46
|
-
lines.push(report.dryRun ? `${header} — dry run` : header);
|
|
47
|
-
if (report.refused) {
|
|
48
|
-
lines.push('');
|
|
49
|
-
lines.push('Refused: the working tree has uncommitted changes.');
|
|
50
|
-
lines.push(' init only runs against a clean working tree, so a partial run can always be undone');
|
|
51
|
-
lines.push(' with a single `git restore .` — commit or stash your changes, then rerun.');
|
|
52
|
-
return lines.join('\n');
|
|
53
|
-
}
|
|
54
|
-
for (const section of report.sections) {
|
|
55
|
-
// backend 'none' must leave the output byte-identical to the frontend-only
|
|
56
|
-
// era (silence guarantee) — a disabled backend section is omitted
|
|
57
|
-
// entirely, not shown as skipped.
|
|
58
|
-
if (section.id === 'backend' && !section.enabled)
|
|
59
|
-
continue;
|
|
60
|
-
lines.push('');
|
|
61
|
-
lines.push(`## ${SECTION_TITLES[section.id]}`);
|
|
62
|
-
if (!section.enabled) {
|
|
63
|
-
lines.push(section.id === 'fsd' && report.frontend === 'none'
|
|
64
|
-
? ' skipped (backend-only workspace — no frontend to structure)'
|
|
65
|
-
: ' skipped (disabled for this run)');
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (section.thrownError !== undefined) {
|
|
69
|
-
lines.push(` aborted — unexpected error: ${section.thrownError}`);
|
|
70
|
-
lines.push(' Nothing was written for this section. Rerun add-fsd-lite directly once resolved.');
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (section.report !== null) {
|
|
74
|
-
lines.push(renderSectionReport(section.report));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (report.config !== undefined && (report.config.action !== 'skipped' || report.config.notRecorded.length > 0)) {
|
|
78
|
-
lines.push('');
|
|
79
|
-
lines.push('## toolkit.config.json');
|
|
80
|
-
const fieldList = report.config.fields.join(', ');
|
|
81
|
-
switch (report.config.action) {
|
|
82
|
-
case 'created':
|
|
83
|
-
case 'updated':
|
|
84
|
-
lines.push(` ${report.dryRun ? 'would record' : 'recorded'} ${fieldList} (${report.config.action})`);
|
|
85
|
-
break;
|
|
86
|
-
case 'unchanged':
|
|
87
|
-
lines.push(` already records ${fieldList} (unchanged)`);
|
|
88
|
-
break;
|
|
89
|
-
case 'unrecognized':
|
|
90
|
-
lines.push(` not updated — the file is not a JSON object, so init left it alone (it never rewrites a shape it`);
|
|
91
|
-
lines.push(` does not recognize). Add ${fieldList} by hand so standalone reruns resolve the same values.`);
|
|
92
|
-
break;
|
|
93
|
-
case 'skipped':
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
if (report.config.notRecorded.length > 0) {
|
|
97
|
-
lines.push(` not recorded: ${report.config.notRecorded.map((n) => n.field).join(', ')} — that section did not apply cleanly`);
|
|
98
|
-
lines.push(' this run (aborted, or written as an alternate file). Once resolved, record it with:');
|
|
99
|
-
const runNx = packageManagerCommands(report.packageManager ?? 'pnpm').runNx;
|
|
100
|
-
for (const entry of report.config.notRecorded)
|
|
101
|
-
lines.push(` ${runNx} g @clearlydev/nx-archweave:${entry.rerun}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const ranDirty = report.allowDirty === true && !report.cleanTree && !report.dryRun;
|
|
105
|
-
if (ranDirty) {
|
|
106
|
-
lines.push('');
|
|
107
|
-
lines.push('Note: --allowDirty was set and the working tree already had uncommitted changes —');
|
|
108
|
-
lines.push('`git restore .` also reverts those pre-existing changes, not only what this run wrote.');
|
|
109
|
-
}
|
|
110
|
-
const hadAbort = report.sections.some(sectionAborted);
|
|
111
|
-
if (!report.dryRun && hadAbort) {
|
|
112
|
-
lines.push('');
|
|
113
|
-
if (ranDirty) {
|
|
114
|
-
lines.push('One or more sections aborted. Everything this run wrote can be undone with:');
|
|
115
|
-
lines.push(' git restore .');
|
|
116
|
-
lines.push(' (--allowDirty: this also reverts the pre-existing uncommitted changes noted above.)');
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
lines.push('One or more sections aborted. init never runs against a dirty tree (see above), so everything');
|
|
120
|
-
lines.push('this run wrote can be undone in one step:');
|
|
121
|
-
lines.push(' git restore .');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return lines.join('\n');
|
|
125
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { spawnableCommand } from '@nx-archweave/shared';
|
|
3
|
-
export function createProcessExec() {
|
|
4
|
-
return (command, args, options) => {
|
|
5
|
-
// nx / actionlint may be .cmd shims on Windows, which Node cannot start without cmd.exe.
|
|
6
|
-
const spawnable = spawnableCommand(command, args);
|
|
7
|
-
const result = spawnSync(spawnable.command, spawnable.args, {
|
|
8
|
-
cwd: options?.cwd,
|
|
9
|
-
encoding: 'utf-8',
|
|
10
|
-
windowsVerbatimArguments: spawnable.windowsVerbatimArguments,
|
|
11
|
-
});
|
|
12
|
-
return { status: result.status ?? null, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
/** Runs `exec`, catching anything it throws (e.g. a stub or an unforeseen environment
|
|
16
|
-
* failure) so a single check can never take down the rest of validation — the "never
|
|
17
|
-
* throws" half of the best-effort contract applies even to the injected dependency, not
|
|
18
|
-
* just to non-zero exit codes. */
|
|
19
|
-
function safeExec(exec, command, args, options) {
|
|
20
|
-
try {
|
|
21
|
-
return exec(command, args, options);
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
return { status: null, stdout: '', stderr: error instanceof Error ? error.message : String(error) };
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/** Post-flush, best-effort validation: never throws, only
|
|
28
|
-
* reports via console. Runs after the generator's `GeneratorCallback` fires, once every
|
|
29
|
-
* file this run wrote is actually on disk. Neither actionlint nor nx are added as
|
|
30
|
-
* dependencies — both are invoked as external binaries, detected at call time. */
|
|
31
|
-
export function runPostInitValidation(input, exec) {
|
|
32
|
-
console.log('');
|
|
33
|
-
console.log('Post-run validation (best-effort):');
|
|
34
|
-
if (input.workflowPaths.length === 0) {
|
|
35
|
-
console.log(' actionlint: skipped (no GitHub Actions workflow files were written this run)');
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
const versionCheck = safeExec(exec, 'actionlint', ['--version'], { cwd: input.root });
|
|
39
|
-
if (versionCheck.status !== 0) {
|
|
40
|
-
console.log(' actionlint: not found on PATH — skipped.');
|
|
41
|
-
console.log(' Install it from https://github.com/rhysd/actionlint to validate workflow files locally.');
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
const lintResult = safeExec(exec, 'actionlint', input.workflowPaths, { cwd: input.root });
|
|
45
|
-
if (lintResult.status === 0) {
|
|
46
|
-
console.log(` actionlint: passed (${input.workflowPaths.join(', ')})`);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
console.log(' actionlint: reported issues —');
|
|
50
|
-
for (const line of (lintResult.stdout || lintResult.stderr).split('\n')) {
|
|
51
|
-
if (line.length > 0)
|
|
52
|
-
console.log(` ${line}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const nxVersionCheck = safeExec(exec, 'nx', ['--version'], { cwd: input.root });
|
|
58
|
-
if (nxVersionCheck.status !== 0) {
|
|
59
|
-
console.log(' nx graph: nx not found on PATH — skipped.');
|
|
60
|
-
console.log(' Run it via your package manager instead, e.g. `pnpm nx graph`, `yarn nx graph`, `bun nx graph` or `npm exec nx graph`.');
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const graphResult = safeExec(exec, 'nx', ['graph', `--file=${input.graphOutputFile}`], { cwd: input.root });
|
|
64
|
-
if (graphResult.status === 0) {
|
|
65
|
-
console.log(` nx graph: valid (written to ${input.graphOutputFile})`);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
console.log(' nx graph: failed —');
|
|
69
|
-
for (const line of (graphResult.stdout || graphResult.stderr).split('\n')) {
|
|
70
|
-
if (line.length > 0)
|
|
71
|
-
console.log(` ${line}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
function describeState(state) {
|
|
2
|
-
switch (state.kind) {
|
|
3
|
-
case 'absent':
|
|
4
|
-
return 'not installed';
|
|
5
|
-
case 'already-installed':
|
|
6
|
-
return `already installed and enabled at ${state.scope} scope${state.version ? ` (version ${state.version})` : ''}`;
|
|
7
|
-
case 'installed-other-scope':
|
|
8
|
-
return `installed and enabled, but at ${state.installedScope} scope`;
|
|
9
|
-
case 'disabled':
|
|
10
|
-
return 'installed but disabled';
|
|
11
|
-
case 'ambiguous':
|
|
12
|
-
return 'more than one installed plugin matches exactly';
|
|
13
|
-
case 'cli-not-found':
|
|
14
|
-
return 'unknown — the claude CLI was not found';
|
|
15
|
-
case 'cli-error':
|
|
16
|
-
return `unknown — the claude CLI could not list plugins (${state.reason})`;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function describeInstall(detail) {
|
|
20
|
-
switch (detail.kind) {
|
|
21
|
-
case 'executed':
|
|
22
|
-
return 'the official command exited successfully';
|
|
23
|
-
case 'manual-only':
|
|
24
|
-
return 'no scriptable command exists for this target — nothing was executed';
|
|
25
|
-
case 'cli-not-found':
|
|
26
|
-
return 'not executed — the claude CLI was not found';
|
|
27
|
-
case 'failed-to-start':
|
|
28
|
-
return `not executed — the claude CLI could not be started (${detail.code})`;
|
|
29
|
-
case 'cli-unsupported':
|
|
30
|
-
return 'not executed — this claude CLI does not support scriptable plugin install';
|
|
31
|
-
case 'confirmation-required':
|
|
32
|
-
return 'stopped — the marketplace declares a command that a person must confirm';
|
|
33
|
-
case 'failed':
|
|
34
|
-
return `failed${detail.failureCode ? ` (${detail.failureCode})` : ''}`;
|
|
35
|
-
case 'timeout':
|
|
36
|
-
return 'stopped — the command timed out';
|
|
37
|
-
case 'output-too-large':
|
|
38
|
-
return 'stopped — the command produced too much output';
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function describeDeclaration(declaration, tool, preview) {
|
|
42
|
-
switch (declaration.action) {
|
|
43
|
-
case 'updated':
|
|
44
|
-
return `toolkit.config.json: "${tool}" ${preview ? 'would be added' : 'added'} to integrations.enabled`;
|
|
45
|
-
case 'unchanged':
|
|
46
|
-
return `toolkit.config.json: "${tool}" is already declared in integrations.enabled (unchanged)`;
|
|
47
|
-
case 'no-config':
|
|
48
|
-
return (`toolkit.config.json: not declared — this workspace has no toolkit.config.json (it was not set up with init). ` +
|
|
49
|
-
`Add "${tool}" to integrations.enabled there if you want doctor to track the declaration.`);
|
|
50
|
-
case 'unrecognized':
|
|
51
|
-
return ('toolkit.config.json: not updated — the file is not in a shape nx-archweave recognizes, so it was left alone. ' +
|
|
52
|
-
`Add "${tool}" to integrations.enabled by hand.`);
|
|
53
|
-
case 'write-failed':
|
|
54
|
-
return `toolkit.config.json: not updated — writing it failed (${declaration.message}). Add "${tool}" to integrations.enabled by hand.`;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
// Every value here is a tool id, a URL, a command, a classification, a manifest
|
|
58
|
-
// entry (installPath excluded by construction) or a
|
|
59
|
-
// CLI message that was home-path-redacted where it was produced (install.ts).
|
|
60
|
-
export function renderInstallIntegrationView(view) {
|
|
61
|
-
const lines = [
|
|
62
|
-
`Integration: ${view.tool} (a separate project — nx-archweave only runs its official installer)`,
|
|
63
|
-
`Source repository: ${view.sourceRepository}`,
|
|
64
|
-
`Official homepage: ${view.officialHomepage}`,
|
|
65
|
-
`License: ${view.license}`,
|
|
66
|
-
view.requestedVersion === null
|
|
67
|
-
? 'Version: latest (decided by the marketplace) — the version actually installed is the one recorded'
|
|
68
|
-
: `Requested version: ${view.requestedVersion} (not enforced — the marketplace decides; the version actually installed is the one recorded)`,
|
|
69
|
-
`Target: ${view.target}`,
|
|
70
|
-
`Scope: ${view.scope}`,
|
|
71
|
-
`Official command: ${view.command}`,
|
|
72
|
-
...(view.teamMarketplace
|
|
73
|
-
? [`For teammates (project scope), the marketplace is declared in .claude/settings.json first: ${view.teamMarketplace.command}`]
|
|
74
|
-
: []),
|
|
75
|
-
...(view.workspaceFiles.length > 0 ? [`Claude Code will update in this workspace: ${view.workspaceFiles.join(', ')}`] : []),
|
|
76
|
-
`Current state: ${describeState(view.currentState)}`,
|
|
77
|
-
];
|
|
78
|
-
if (view.applyIgnoredForNxDryRun) {
|
|
79
|
-
lines.push("Note: --apply was ignored because Nx's --dry-run is set.");
|
|
80
|
-
}
|
|
81
|
-
if (view.preview && view.currentState.kind === 'absent') {
|
|
82
|
-
lines.push('', ...(view.teamMarketplace ? [`Would run: ${view.teamMarketplace.command}`] : []), `Would run: ${view.command}`, `To run it: ${view.rerunCommand}`);
|
|
83
|
-
}
|
|
84
|
-
else if (view.preview && view.teamMarketplace) {
|
|
85
|
-
lines.push('', `Would run: ${view.teamMarketplace.command}`, `To run it: ${view.rerunCommand}`);
|
|
86
|
-
}
|
|
87
|
-
if (view.teamMarketplace?.detail) {
|
|
88
|
-
const { detail } = view.teamMarketplace;
|
|
89
|
-
lines.push('', detail.kind === 'executed'
|
|
90
|
-
? 'Marketplace: declared in .claude/settings.json (extraKnownMarketplaces) — teammates get its source from the committed file'
|
|
91
|
-
: `Marketplace declaration: ${describeInstall(detail)}${detail.kind === 'failed' ? ` — ${detail.message}` : ''}`);
|
|
92
|
-
}
|
|
93
|
-
if (view.install) {
|
|
94
|
-
lines.push('', `Install: ${describeInstall(view.install.detail)}`);
|
|
95
|
-
}
|
|
96
|
-
if (view.record) {
|
|
97
|
-
const { record } = view;
|
|
98
|
-
lines.push('', `Verification: ${record.verification.verified ? 'passed' : 'not verified'}`, ...record.verification.problems.map((problem) => ` ! ${problem}`));
|
|
99
|
-
if (record.hasConflict && record.existingEntry) {
|
|
100
|
-
lines.push('Conflict: an existing manifest entry differs from what was verified now — the fresh result replaces it.', ` Existing: ${JSON.stringify(record.existingEntry)}`, ` Verified: ${JSON.stringify(record.proposedEntry)}`);
|
|
101
|
-
}
|
|
102
|
-
const installKind = view.install?.detail.kind;
|
|
103
|
-
if (record.proposedEntry) {
|
|
104
|
-
if (installKind !== undefined && installKind !== 'executed') {
|
|
105
|
-
lines.push(`The install command reported ${installKind}, but a fresh check shows the plugin installed and enabled.`);
|
|
106
|
-
}
|
|
107
|
-
lines.push(record.wasWritten
|
|
108
|
-
? 'Recorded — the following manifest entry was written to archweave-integrations.json:'
|
|
109
|
-
: 'archweave-integrations.json would get this entry:', ` ${JSON.stringify(record.proposedEntry)}`);
|
|
110
|
-
}
|
|
111
|
-
else if (installKind === 'executed') {
|
|
112
|
-
lines.push('The install command exited 0, but the fresh verification did not confirm the plugin — nothing was written.');
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
lines.push('Nothing was written — verification did not succeed via the authoritative claude-cli signal.');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (view.declaration) {
|
|
119
|
-
lines.push(describeDeclaration(view.declaration, view.tool, view.preview));
|
|
120
|
-
}
|
|
121
|
-
if (view.guidance.length > 0) {
|
|
122
|
-
lines.push('', 'Next steps:', ...view.guidance.map((step) => ` - ${step}`));
|
|
123
|
-
}
|
|
124
|
-
lines.push('', view.preview
|
|
125
|
-
? '(preview — only read-only checks ran; nothing was installed and no file was written)'
|
|
126
|
-
: '(applied — the lines above state exactly what was executed and written)');
|
|
127
|
-
return lines.join('\n');
|
|
128
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// Never renders installPath or any absolute/home-directory path — the
|
|
2
|
-
// manifest entry types used here already exclude installPath by
|
|
3
|
-
// construction, and this function only ever reads from
|
|
4
|
-
// those already-narrowed values.
|
|
5
|
-
export function renderRecordIntegrationView(view) {
|
|
6
|
-
const lines = [
|
|
7
|
-
`Integration: ${view.tool}`,
|
|
8
|
-
`Target: ${view.target}`,
|
|
9
|
-
`Source repository: ${view.sourceRepository}`,
|
|
10
|
-
`License: ${view.license}`,
|
|
11
|
-
`Verification source: ${view.verificationSource}`,
|
|
12
|
-
`Current verification: ${view.verification.verified ? 'passed' : 'not verified'}`,
|
|
13
|
-
...view.verification.problems.map((problem) => ` ! ${problem}`),
|
|
14
|
-
'',
|
|
15
|
-
];
|
|
16
|
-
if (view.hasConflict && view.existingEntry) {
|
|
17
|
-
lines.push('Conflict: an existing manifest entry differs from what would be recorded.', ` Existing: ${JSON.stringify(view.existingEntry)}`, ` Proposed: ${JSON.stringify(view.proposedEntry)}`, '');
|
|
18
|
-
}
|
|
19
|
-
if (view.proposedEntry) {
|
|
20
|
-
lines.push(view.wasWritten
|
|
21
|
-
? 'Recorded — the following manifest entry was written:'
|
|
22
|
-
: 'The following manifest entry would be recorded:', ` ${JSON.stringify(view.proposedEntry)}`);
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
lines.push('Nothing would be recorded — verification did not succeed via the authoritative claude-cli signal.');
|
|
26
|
-
}
|
|
27
|
-
lines.push('', view.wasWritten
|
|
28
|
-
? '(recorded — the manifest was written)'
|
|
29
|
-
: '(dry-run — no files were changed, no installer was executed, manifest was not written)');
|
|
30
|
-
return lines.join('\n');
|
|
31
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `nx g` owns the dry-run flag: Nx 23.1's generate command deletes `dryRun` /
|
|
3
|
-
* `dry-run` from the options it hands a generator and then refills the schema
|
|
4
|
-
* default, and signals its own `--dry-run` only through `NX_DRY_RUN=true`
|
|
5
|
-
* (`nx/src/command-line/generate/generate.js`, `command-object.js`). So an Nx
|
|
6
|
-
* entry point that read only `options.dryRun` would always see `false` and print
|
|
7
|
-
* "created" for a run Nx never flushes. Programmatic callers (init → the `run*`
|
|
8
|
-
* cores, tests) keep passing `dryRun` explicitly; Nx's own flag always wins.
|
|
9
|
-
*/
|
|
10
|
-
export function resolveDryRun(options) {
|
|
11
|
-
return process.env['NX_DRY_RUN'] === 'true' || (options.dryRun ?? false);
|
|
12
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { getProjects } from '@nx/devkit';
|
|
2
|
-
import { resolveProjectType } from "./project-type.js";
|
|
3
|
-
const baseNameOf = (root) => root.split('/').pop() ?? root;
|
|
4
|
-
function isEndToEnd(root) {
|
|
5
|
-
const dir = baseNameOf(root);
|
|
6
|
-
return dir === 'e2e' || dir.endsWith('-e2e');
|
|
7
|
-
}
|
|
8
|
-
/** add-backend's api app (default name `api`) and anything tagged as the backend scope are
|
|
9
|
-
* applications, but not frontends. */
|
|
10
|
-
function isBackend(root, config) {
|
|
11
|
-
return baseNameOf(root) === 'api' || (config.tags ?? []).includes('scope:backend');
|
|
12
|
-
}
|
|
13
|
-
function sourceRootOf(config) {
|
|
14
|
-
if (config.sourceRoot)
|
|
15
|
-
return config.sourceRoot;
|
|
16
|
-
return config.root === '' || config.root === '.' ? 'src' : `${config.root}/src`;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Same two tiers as add-backend's frontend lookup: declared applications first; only when
|
|
20
|
-
* there are none do undeclared projects count, typed the way Nx infers them (Nx's Next.js
|
|
21
|
-
* and classic-preset apps are bare package.json projects). A declared root application
|
|
22
|
-
* (a standalone workspace, root `.`) counts; an undeclared root package.json never does.
|
|
23
|
-
*/
|
|
24
|
-
export function resolveFrontendApp(tree, explicitProject) {
|
|
25
|
-
let projects;
|
|
26
|
-
try {
|
|
27
|
-
projects = getProjects(tree);
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
-
const pathMatch = /Cannot parse ([^:]+):/.exec(message);
|
|
32
|
-
return { kind: 'invalid-project-json', detail: pathMatch ? pathMatch[1] : message };
|
|
33
|
-
}
|
|
34
|
-
const found = (project, config) => ({
|
|
35
|
-
kind: 'found',
|
|
36
|
-
project,
|
|
37
|
-
root: config.root,
|
|
38
|
-
sourceRoot: sourceRootOf(config),
|
|
39
|
-
});
|
|
40
|
-
if (explicitProject !== undefined) {
|
|
41
|
-
const config = projects.get(explicitProject);
|
|
42
|
-
return config ? found(explicitProject, config) : { kind: 'not-found', project: explicitProject };
|
|
43
|
-
}
|
|
44
|
-
const eligible = [...projects.entries()].filter(([, config]) => !isEndToEnd(config.root) && !isBackend(config.root, config));
|
|
45
|
-
const declared = eligible.filter(([, config]) => config.projectType === 'application');
|
|
46
|
-
const inferred = eligible.filter(([, config]) => config.projectType === undefined &&
|
|
47
|
-
config.root !== '' &&
|
|
48
|
-
config.root !== '.' &&
|
|
49
|
-
resolveProjectType(tree, config.root, undefined) === 'application');
|
|
50
|
-
const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
|
|
51
|
-
if (candidates.length === 0)
|
|
52
|
-
return { kind: 'none' };
|
|
53
|
-
if (candidates.length > 1)
|
|
54
|
-
return { kind: 'ambiguous', candidates: candidates.map(([name]) => name) };
|
|
55
|
-
const [project, config] = candidates[0];
|
|
56
|
-
return found(project, config);
|
|
57
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { parseJson } from '@nx/devkit';
|
|
2
|
-
/**
|
|
3
|
-
* The project type Nx itself would use when a project declares none — the
|
|
4
|
-
* classic-preset Nx 23 layout configures apps through a bare package.json, so a
|
|
5
|
-
* filter on the declared `projectType` alone misses every one of them.
|
|
6
|
-
*
|
|
7
|
-
* Mirrors the project graph's rule (nx 23 core `normalize-project-nodes`
|
|
8
|
-
* `getProjectType`): a declared type wins (anything but `library` counts as an
|
|
9
|
-
* application, as in Nx); then `tsconfig.lib.json` → library; `tsconfig.app.json`
|
|
10
|
-
* → application; a readable package.json with none of exports/main/module/bin
|
|
11
|
-
* set → application; anything else — including a missing or unparseable
|
|
12
|
-
* package.json — → library. Mirrored, not imported: the plugin's only runtime
|
|
13
|
-
* dependency stays @nx/devkit. Not mirrored: the package-json plugin's
|
|
14
|
-
* `workspaceLayout.appsDir`/`libsDir` override, which devkit's tree-based
|
|
15
|
-
* `getProjects` never applies either.
|
|
16
|
-
*/
|
|
17
|
-
export function resolveProjectType(tree, root, declared) {
|
|
18
|
-
if (declared)
|
|
19
|
-
return declared === 'library' ? 'library' : 'application';
|
|
20
|
-
const prefix = root === '' || root === '.' ? '' : `${root}/`;
|
|
21
|
-
if (tree.exists(`${prefix}tsconfig.lib.json`))
|
|
22
|
-
return 'library';
|
|
23
|
-
if (tree.exists(`${prefix}tsconfig.app.json`))
|
|
24
|
-
return 'application';
|
|
25
|
-
const raw = tree.read(`${prefix}package.json`, 'utf-8');
|
|
26
|
-
if (raw === null)
|
|
27
|
-
return 'library';
|
|
28
|
-
try {
|
|
29
|
-
// parseJson accepts comments and trailing commas, like Nx's readJsonFile
|
|
30
|
-
const manifest = parseJson(raw);
|
|
31
|
-
// truthiness, like Nx: an empty "main": "" is no entry point
|
|
32
|
-
if (manifest && !['exports', 'main', 'module', 'bin'].some((key) => Boolean(manifest[key])))
|
|
33
|
-
return 'application';
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
// unparseable — Nx's graph falls through to library as well
|
|
37
|
-
}
|
|
38
|
-
return 'library';
|
|
39
|
-
}
|