@nathapp/nax 0.38.1 → 0.39.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/dist/nax.js +48 -19
- package/package.json +1 -1
- package/src/cli/init-context.ts +348 -0
- package/src/cli/init-detect.ts +169 -0
- package/src/cli/init.ts +96 -31
- package/src/cli/plugins.ts +15 -4
- package/src/config/runtime-types.ts +3 -0
- package/src/config/schemas.ts +2 -0
- package/src/execution/lifecycle/run-setup.ts +7 -1
- package/src/plugins/loader.ts +38 -3
- package/src/plugins/types.ts +2 -0
package/dist/nax.js
CHANGED
|
@@ -18346,7 +18346,8 @@ var init_schemas3 = __esm(() => {
|
|
|
18346
18346
|
});
|
|
18347
18347
|
PluginConfigEntrySchema = exports_external.object({
|
|
18348
18348
|
module: exports_external.string().min(1, "plugin.module must be non-empty"),
|
|
18349
|
-
config: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
18349
|
+
config: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
|
|
18350
|
+
enabled: exports_external.boolean().default(true)
|
|
18350
18351
|
});
|
|
18351
18352
|
HooksConfigSchema = exports_external.object({
|
|
18352
18353
|
skipGlobal: exports_external.boolean().optional(),
|
|
@@ -18406,6 +18407,7 @@ var init_schemas3 = __esm(() => {
|
|
|
18406
18407
|
context: ContextConfigSchema,
|
|
18407
18408
|
optimizer: OptimizerConfigSchema.optional(),
|
|
18408
18409
|
plugins: exports_external.array(PluginConfigEntrySchema).optional(),
|
|
18410
|
+
disabledPlugins: exports_external.array(exports_external.string()).optional(),
|
|
18409
18411
|
hooks: HooksConfigSchema.optional(),
|
|
18410
18412
|
interaction: InteractionConfigSchema.optional(),
|
|
18411
18413
|
precheck: PrecheckConfigSchema.optional(),
|
|
@@ -20791,7 +20793,7 @@ var package_default;
|
|
|
20791
20793
|
var init_package = __esm(() => {
|
|
20792
20794
|
package_default = {
|
|
20793
20795
|
name: "@nathapp/nax",
|
|
20794
|
-
version: "0.
|
|
20796
|
+
version: "0.39.0",
|
|
20795
20797
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
20796
20798
|
type: "module",
|
|
20797
20799
|
bin: {
|
|
@@ -20855,8 +20857,8 @@ var init_version = __esm(() => {
|
|
|
20855
20857
|
NAX_VERSION = package_default.version;
|
|
20856
20858
|
NAX_COMMIT = (() => {
|
|
20857
20859
|
try {
|
|
20858
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
20859
|
-
return "
|
|
20860
|
+
if (/^[0-9a-f]{6,10}$/.test("e6f293e"))
|
|
20861
|
+
return "e6f293e";
|
|
20860
20862
|
} catch {}
|
|
20861
20863
|
try {
|
|
20862
20864
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -28144,16 +28146,29 @@ import * as path11 from "path";
|
|
|
28144
28146
|
function getSafeLogger6() {
|
|
28145
28147
|
return getSafeLogger();
|
|
28146
28148
|
}
|
|
28147
|
-
|
|
28149
|
+
function extractPluginName(pluginPath) {
|
|
28150
|
+
const basename2 = path11.basename(pluginPath);
|
|
28151
|
+
if (basename2 === "index.ts" || basename2 === "index.js" || basename2 === "index.mjs") {
|
|
28152
|
+
return path11.basename(path11.dirname(pluginPath));
|
|
28153
|
+
}
|
|
28154
|
+
return basename2.replace(/\.(ts|js|mjs)$/, "");
|
|
28155
|
+
}
|
|
28156
|
+
async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, disabledPlugins) {
|
|
28148
28157
|
const loadedPlugins = [];
|
|
28149
28158
|
const effectiveProjectRoot = projectRoot || projectDir;
|
|
28150
28159
|
const pluginNames = new Set;
|
|
28160
|
+
const disabledSet = new Set(disabledPlugins ?? []);
|
|
28161
|
+
const logger = getSafeLogger6();
|
|
28151
28162
|
const globalPlugins = await discoverPlugins(globalDir);
|
|
28152
28163
|
for (const plugin of globalPlugins) {
|
|
28164
|
+
const pluginName = extractPluginName(plugin.path);
|
|
28165
|
+
if (disabledSet.has(pluginName)) {
|
|
28166
|
+
logger?.info("plugins", `Skipping disabled plugin: '${pluginName}' (global directory)`);
|
|
28167
|
+
continue;
|
|
28168
|
+
}
|
|
28153
28169
|
const validated = await loadAndValidatePlugin(plugin.path, {}, [globalDir]);
|
|
28154
28170
|
if (validated) {
|
|
28155
28171
|
if (pluginNames.has(validated.name)) {
|
|
28156
|
-
const logger = getSafeLogger6();
|
|
28157
28172
|
logger?.warn("plugins", `Plugin name collision: '${validated.name}' (global directory)`);
|
|
28158
28173
|
}
|
|
28159
28174
|
loadedPlugins.push({
|
|
@@ -28165,10 +28180,14 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot) {
|
|
|
28165
28180
|
}
|
|
28166
28181
|
const projectPlugins = await discoverPlugins(projectDir);
|
|
28167
28182
|
for (const plugin of projectPlugins) {
|
|
28183
|
+
const pluginName = extractPluginName(plugin.path);
|
|
28184
|
+
if (disabledSet.has(pluginName)) {
|
|
28185
|
+
logger?.info("plugins", `Skipping disabled plugin: '${pluginName}' (project directory)`);
|
|
28186
|
+
continue;
|
|
28187
|
+
}
|
|
28168
28188
|
const validated = await loadAndValidatePlugin(plugin.path, {}, [projectDir]);
|
|
28169
28189
|
if (validated) {
|
|
28170
28190
|
if (pluginNames.has(validated.name)) {
|
|
28171
|
-
const logger = getSafeLogger6();
|
|
28172
28191
|
logger?.warn("plugins", `Plugin name collision: '${validated.name}' (project directory overrides global)`);
|
|
28173
28192
|
}
|
|
28174
28193
|
loadedPlugins.push({
|
|
@@ -28179,11 +28198,14 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot) {
|
|
|
28179
28198
|
}
|
|
28180
28199
|
}
|
|
28181
28200
|
for (const entry of configPlugins) {
|
|
28201
|
+
if (entry.enabled === false) {
|
|
28202
|
+
logger?.info("plugins", `Skipping disabled plugin: '${entry.module}'`);
|
|
28203
|
+
continue;
|
|
28204
|
+
}
|
|
28182
28205
|
const resolvedModule = resolveModulePath(entry.module, effectiveProjectRoot);
|
|
28183
28206
|
const validated = await loadAndValidatePlugin(resolvedModule, entry.config ?? {}, [globalDir, projectDir, effectiveProjectRoot].filter(Boolean), entry.module);
|
|
28184
28207
|
if (validated) {
|
|
28185
28208
|
if (pluginNames.has(validated.name)) {
|
|
28186
|
-
const logger = getSafeLogger6();
|
|
28187
28209
|
logger?.warn("plugins", `Plugin name collision: '${validated.name}' (config entry overrides previous)`);
|
|
28188
28210
|
}
|
|
28189
28211
|
loadedPlugins.push({
|
|
@@ -31115,10 +31137,10 @@ var init_parallel_executor = __esm(() => {
|
|
|
31115
31137
|
// src/pipeline/subscribers/events-writer.ts
|
|
31116
31138
|
import { appendFile as appendFile2, mkdir } from "fs/promises";
|
|
31117
31139
|
import { homedir as homedir5 } from "os";
|
|
31118
|
-
import { basename as
|
|
31140
|
+
import { basename as basename3, join as join38 } from "path";
|
|
31119
31141
|
function wireEventsWriter(bus, feature, runId, workdir) {
|
|
31120
31142
|
const logger = getSafeLogger();
|
|
31121
|
-
const project =
|
|
31143
|
+
const project = basename3(workdir);
|
|
31122
31144
|
const eventsDir = join38(homedir5(), ".nax", "events", project);
|
|
31123
31145
|
const eventsFile = join38(eventsDir, "events.jsonl");
|
|
31124
31146
|
let dirReady = false;
|
|
@@ -31280,10 +31302,10 @@ var init_interaction2 = __esm(() => {
|
|
|
31280
31302
|
// src/pipeline/subscribers/registry.ts
|
|
31281
31303
|
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
31282
31304
|
import { homedir as homedir6 } from "os";
|
|
31283
|
-
import { basename as
|
|
31305
|
+
import { basename as basename4, join as join39 } from "path";
|
|
31284
31306
|
function wireRegistry(bus, feature, runId, workdir) {
|
|
31285
31307
|
const logger = getSafeLogger();
|
|
31286
|
-
const project =
|
|
31308
|
+
const project = basename4(workdir);
|
|
31287
31309
|
const runDir = join39(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
|
|
31288
31310
|
const metaFile = join39(runDir, "meta.json");
|
|
31289
31311
|
const unsub = bus.on("run:started", (_ev) => {
|
|
@@ -32650,7 +32672,7 @@ async function setupRun(options) {
|
|
|
32650
32672
|
const globalPluginsDir = path17.join(os5.homedir(), ".nax", "plugins");
|
|
32651
32673
|
const projectPluginsDir = path17.join(workdir, "nax", "plugins");
|
|
32652
32674
|
const configPlugins = config2.plugins || [];
|
|
32653
|
-
const pluginRegistry = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir);
|
|
32675
|
+
const pluginRegistry = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins);
|
|
32654
32676
|
logger?.info("plugins", `Loaded ${pluginRegistry.plugins.length} plugins`, {
|
|
32655
32677
|
plugins: pluginRegistry.plugins.map((p) => ({ name: p.name, version: p.version, provides: p.provides }))
|
|
32656
32678
|
});
|
|
@@ -65158,6 +65180,9 @@ async function exportPromptCommand(options) {
|
|
|
65158
65180
|
// src/cli/init.ts
|
|
65159
65181
|
init_paths();
|
|
65160
65182
|
init_logger2();
|
|
65183
|
+
|
|
65184
|
+
// src/cli/init-context.ts
|
|
65185
|
+
init_logger2();
|
|
65161
65186
|
// src/cli/plugins.ts
|
|
65162
65187
|
init_loader5();
|
|
65163
65188
|
import * as os2 from "os";
|
|
@@ -65166,7 +65191,7 @@ async function pluginsListCommand(config2, workdir, overrideGlobalPluginsDir) {
|
|
|
65166
65191
|
const globalPluginsDir = overrideGlobalPluginsDir ?? path12.join(os2.homedir(), ".nax", "plugins");
|
|
65167
65192
|
const projectPluginsDir = path12.join(workdir, "nax", "plugins");
|
|
65168
65193
|
const configPlugins = config2.plugins || [];
|
|
65169
|
-
const registry2 = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir);
|
|
65194
|
+
const registry2 = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins);
|
|
65170
65195
|
const plugins = registry2.plugins;
|
|
65171
65196
|
if (plugins.length === 0) {
|
|
65172
65197
|
console.log("No plugins installed.");
|
|
@@ -65179,29 +65204,33 @@ To install plugins:`);
|
|
|
65179
65204
|
See https://github.com/nax/nax#plugins for more details.`);
|
|
65180
65205
|
return;
|
|
65181
65206
|
}
|
|
65207
|
+
const disabledSet = new Set(config2.disabledPlugins ?? []);
|
|
65182
65208
|
const rows = plugins.map((plugin) => {
|
|
65183
65209
|
const source = registry2.getSource(plugin.name);
|
|
65184
65210
|
const sourceStr = source ? formatSource(source.type, source.path) : "unknown";
|
|
65211
|
+
const isDisabled = disabledSet.has(plugin.name);
|
|
65185
65212
|
return {
|
|
65186
65213
|
name: plugin.name,
|
|
65187
65214
|
version: plugin.version,
|
|
65188
65215
|
provides: plugin.provides.join(", "),
|
|
65189
|
-
source: sourceStr
|
|
65216
|
+
source: sourceStr,
|
|
65217
|
+
enabled: isDisabled ? "disabled" : "enabled"
|
|
65190
65218
|
};
|
|
65191
65219
|
});
|
|
65192
65220
|
const widths = {
|
|
65193
65221
|
name: Math.max(4, ...rows.map((r) => r.name.length)),
|
|
65194
65222
|
version: Math.max(7, ...rows.map((r) => r.version.length)),
|
|
65195
65223
|
provides: Math.max(8, ...rows.map((r) => r.provides.length)),
|
|
65196
|
-
source: Math.max(6, ...rows.map((r) => r.source.length))
|
|
65224
|
+
source: Math.max(6, ...rows.map((r) => r.source.length)),
|
|
65225
|
+
enabled: Math.max(7, ...rows.map((r) => r.enabled.length))
|
|
65197
65226
|
};
|
|
65198
65227
|
console.log(`
|
|
65199
65228
|
Installed Plugins:
|
|
65200
65229
|
`);
|
|
65201
|
-
console.log(`${pad("Name", widths.name)} ${pad("Version", widths.version)} ${pad("Provides", widths.provides)} ${pad("Source", widths.source)}`);
|
|
65202
|
-
console.log(`${"-".repeat(widths.name)} ${"-".repeat(widths.version)} ${"-".repeat(widths.provides)} ${"-".repeat(widths.source)}`);
|
|
65230
|
+
console.log(`${pad("Name", widths.name)} ${pad("Version", widths.version)} ${pad("Provides", widths.provides)} ${pad("Source", widths.source)} ${pad("Status", widths.enabled)}`);
|
|
65231
|
+
console.log(`${"-".repeat(widths.name)} ${"-".repeat(widths.version)} ${"-".repeat(widths.provides)} ${"-".repeat(widths.source)} ${"-".repeat(widths.enabled)}`);
|
|
65203
65232
|
for (const row of rows) {
|
|
65204
|
-
console.log(`${pad(row.name, widths.name)} ${pad(row.version, widths.version)} ${pad(row.provides, widths.provides)} ${pad(row.source, widths.source)}`);
|
|
65233
|
+
console.log(`${pad(row.name, widths.name)} ${pad(row.version, widths.version)} ${pad(row.provides, widths.provides)} ${pad(row.source, widths.source)} ${pad(row.enabled, widths.enabled)}`);
|
|
65205
65234
|
}
|
|
65206
65235
|
console.log();
|
|
65207
65236
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context.md Generation (INIT-002)
|
|
3
|
+
*
|
|
4
|
+
* Generates context.md from filesystem scan with optional LLM enhancement.
|
|
5
|
+
* Default mode: template from scan (zero LLM cost)
|
|
6
|
+
* AI mode (--ai flag): LLM-powered narrative context
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { mkdir } from "node:fs/promises";
|
|
11
|
+
import { basename, join } from "node:path";
|
|
12
|
+
import { getLogger } from "../logger";
|
|
13
|
+
|
|
14
|
+
/** Project scan results */
|
|
15
|
+
export interface ProjectScan {
|
|
16
|
+
projectName: string;
|
|
17
|
+
fileTree: string[];
|
|
18
|
+
packageManifest: {
|
|
19
|
+
name?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
scripts?: Record<string, string>;
|
|
22
|
+
dependencies?: Record<string, string>;
|
|
23
|
+
} | null;
|
|
24
|
+
readmeSnippet: string | null;
|
|
25
|
+
entryPoints: string[];
|
|
26
|
+
configFiles: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Package manifest structure */
|
|
30
|
+
interface PackageManifest {
|
|
31
|
+
name?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
scripts?: Record<string, string>;
|
|
34
|
+
dependencies?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** initContext options */
|
|
38
|
+
export interface InitContextOptions {
|
|
39
|
+
ai?: boolean;
|
|
40
|
+
force?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Dependency injection for testing */
|
|
44
|
+
export const _deps = {
|
|
45
|
+
callLLM: async (_prompt: string): Promise<string> => {
|
|
46
|
+
// Placeholder implementation
|
|
47
|
+
// In production, this would call the nax LLM infrastructure
|
|
48
|
+
throw new Error("callLLM not implemented");
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Recursively find all files in a directory, excluding certain paths.
|
|
54
|
+
* Returns relative paths, limited to maxFiles entries.
|
|
55
|
+
*/
|
|
56
|
+
async function findFiles(dir: string, maxFiles = 200): Promise<string[]> {
|
|
57
|
+
// Use find command to locate files, excluding common directories
|
|
58
|
+
try {
|
|
59
|
+
const proc = Bun.spawnSync(
|
|
60
|
+
[
|
|
61
|
+
"find",
|
|
62
|
+
dir,
|
|
63
|
+
"-type",
|
|
64
|
+
"f",
|
|
65
|
+
"-not",
|
|
66
|
+
"-path",
|
|
67
|
+
"*/node_modules/*",
|
|
68
|
+
"-not",
|
|
69
|
+
"-path",
|
|
70
|
+
"*/.git/*",
|
|
71
|
+
"-not",
|
|
72
|
+
"-path",
|
|
73
|
+
"*/dist/*",
|
|
74
|
+
],
|
|
75
|
+
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (proc.success) {
|
|
79
|
+
const output = new TextDecoder().decode(proc.stdout);
|
|
80
|
+
const files = output
|
|
81
|
+
.trim()
|
|
82
|
+
.split("\n")
|
|
83
|
+
.filter((f) => f.length > 0)
|
|
84
|
+
.map((f) => f.replace(`${dir}/`, ""))
|
|
85
|
+
.slice(0, maxFiles);
|
|
86
|
+
return files;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// find command failed, use fallback
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read and parse package.json if it exists
|
|
97
|
+
*/
|
|
98
|
+
async function readPackageManifest(projectRoot: string): Promise<PackageManifest | null> {
|
|
99
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
100
|
+
|
|
101
|
+
if (!existsSync(packageJsonPath)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const content = await Bun.file(packageJsonPath).text();
|
|
107
|
+
const manifest = JSON.parse(content) as PackageManifest;
|
|
108
|
+
return {
|
|
109
|
+
name: manifest.name,
|
|
110
|
+
description: manifest.description,
|
|
111
|
+
scripts: manifest.scripts,
|
|
112
|
+
dependencies: manifest.dependencies,
|
|
113
|
+
};
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read first 100 lines of README.md if it exists
|
|
121
|
+
*/
|
|
122
|
+
async function readReadmeSnippet(projectRoot: string): Promise<string | null> {
|
|
123
|
+
const readmePath = join(projectRoot, "README.md");
|
|
124
|
+
|
|
125
|
+
if (!existsSync(readmePath)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const content = await Bun.file(readmePath).text();
|
|
131
|
+
const lines = content.split("\n");
|
|
132
|
+
return lines.slice(0, 100).join("\n");
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Detect entry points in the project
|
|
140
|
+
*/
|
|
141
|
+
async function detectEntryPoints(projectRoot: string): Promise<string[]> {
|
|
142
|
+
const candidates = ["src/index.ts", "src/main.ts", "main.go", "src/lib.rs"];
|
|
143
|
+
const found: string[] = [];
|
|
144
|
+
|
|
145
|
+
for (const candidate of candidates) {
|
|
146
|
+
const path = join(projectRoot, candidate);
|
|
147
|
+
if (existsSync(path)) {
|
|
148
|
+
found.push(candidate);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return found;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Detect config files in the project
|
|
157
|
+
*/
|
|
158
|
+
async function detectConfigFiles(projectRoot: string): Promise<string[]> {
|
|
159
|
+
const candidates = ["tsconfig.json", "biome.json", "turbo.json", ".env.example"];
|
|
160
|
+
const found: string[] = [];
|
|
161
|
+
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
const path = join(projectRoot, candidate);
|
|
164
|
+
if (existsSync(path)) {
|
|
165
|
+
found.push(candidate);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return found;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Scan a project for context information
|
|
174
|
+
*/
|
|
175
|
+
export async function scanProject(projectRoot: string): Promise<ProjectScan> {
|
|
176
|
+
const fileTree = await findFiles(projectRoot, 200);
|
|
177
|
+
const packageManifest = await readPackageManifest(projectRoot);
|
|
178
|
+
const readmeSnippet = await readReadmeSnippet(projectRoot);
|
|
179
|
+
const entryPoints = await detectEntryPoints(projectRoot);
|
|
180
|
+
const configFiles = await detectConfigFiles(projectRoot);
|
|
181
|
+
|
|
182
|
+
// Determine project name from package.json or directory basename
|
|
183
|
+
const projectName = packageManifest?.name || basename(projectRoot);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
projectName,
|
|
187
|
+
fileTree,
|
|
188
|
+
packageManifest,
|
|
189
|
+
readmeSnippet,
|
|
190
|
+
entryPoints,
|
|
191
|
+
configFiles,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate a markdown template for context.md from scan results
|
|
197
|
+
*/
|
|
198
|
+
export function generateContextTemplate(scan: ProjectScan): string {
|
|
199
|
+
const lines: string[] = [];
|
|
200
|
+
|
|
201
|
+
lines.push(`# ${scan.projectName}\n`);
|
|
202
|
+
|
|
203
|
+
if (scan.packageManifest?.description) {
|
|
204
|
+
lines.push(`${scan.packageManifest.description}\n`);
|
|
205
|
+
} else {
|
|
206
|
+
lines.push("<!-- TODO: Add project description -->\n");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (scan.entryPoints.length > 0) {
|
|
210
|
+
lines.push("## Entry Points\n");
|
|
211
|
+
for (const ep of scan.entryPoints) {
|
|
212
|
+
lines.push(`- ${ep}`);
|
|
213
|
+
}
|
|
214
|
+
lines.push("");
|
|
215
|
+
} else {
|
|
216
|
+
lines.push("## Entry Points\n");
|
|
217
|
+
lines.push("<!-- TODO: Document entry points -->\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (scan.fileTree.length > 0) {
|
|
221
|
+
lines.push("## Project Structure\n");
|
|
222
|
+
lines.push("```");
|
|
223
|
+
for (const file of scan.fileTree.slice(0, 20)) {
|
|
224
|
+
lines.push(file);
|
|
225
|
+
}
|
|
226
|
+
if (scan.fileTree.length > 20) {
|
|
227
|
+
lines.push(`... and ${scan.fileTree.length - 20} more files`);
|
|
228
|
+
}
|
|
229
|
+
lines.push("```\n");
|
|
230
|
+
} else {
|
|
231
|
+
lines.push("## Project Structure\n");
|
|
232
|
+
lines.push("<!-- TODO: Document project structure -->\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (scan.configFiles.length > 0) {
|
|
236
|
+
lines.push("## Configuration Files\n");
|
|
237
|
+
for (const cf of scan.configFiles) {
|
|
238
|
+
lines.push(`- ${cf}`);
|
|
239
|
+
}
|
|
240
|
+
lines.push("");
|
|
241
|
+
} else {
|
|
242
|
+
lines.push("## Configuration Files\n");
|
|
243
|
+
lines.push("<!-- TODO: Document configuration files -->\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (scan.packageManifest?.scripts) {
|
|
247
|
+
const hasScripts = Object.keys(scan.packageManifest.scripts).length > 0;
|
|
248
|
+
if (hasScripts) {
|
|
249
|
+
lines.push("## Scripts\n");
|
|
250
|
+
for (const [name, command] of Object.entries(scan.packageManifest.scripts)) {
|
|
251
|
+
lines.push(`- **${name}**: \`${command}\``);
|
|
252
|
+
}
|
|
253
|
+
lines.push("");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (scan.packageManifest?.dependencies) {
|
|
258
|
+
const deps = Object.keys(scan.packageManifest.dependencies);
|
|
259
|
+
if (deps.length > 0) {
|
|
260
|
+
lines.push("## Dependencies\n");
|
|
261
|
+
lines.push("<!-- TODO: Document key dependencies and their purpose -->\n");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
lines.push("## Development Guidelines\n");
|
|
266
|
+
lines.push("<!-- TODO: Document development guidelines and conventions -->\n");
|
|
267
|
+
|
|
268
|
+
return `${lines.join("\n").trim()}\n`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Generate context.md with LLM enhancement
|
|
273
|
+
*/
|
|
274
|
+
async function generateContextWithLLM(scan: ProjectScan): Promise<string> {
|
|
275
|
+
const logger = getLogger();
|
|
276
|
+
|
|
277
|
+
// Build LLM prompt from scan results
|
|
278
|
+
const scanSummary = `
|
|
279
|
+
Project: ${scan.projectName}
|
|
280
|
+
Entry Points: ${scan.entryPoints.join(", ") || "None detected"}
|
|
281
|
+
Config Files: ${scan.configFiles.join(", ") || "None detected"}
|
|
282
|
+
Total Files: ${scan.fileTree.length}
|
|
283
|
+
Description: ${scan.packageManifest?.description || "Not provided"}
|
|
284
|
+
`;
|
|
285
|
+
|
|
286
|
+
const prompt = `
|
|
287
|
+
You are a technical documentation expert. Generate a concise, well-structured context.md file for a software project based on this scan:
|
|
288
|
+
|
|
289
|
+
${scanSummary}
|
|
290
|
+
|
|
291
|
+
The context.md should include:
|
|
292
|
+
1. Project overview (name, purpose, key technologies)
|
|
293
|
+
2. Entry points and main modules
|
|
294
|
+
3. Key dependencies and why they're used
|
|
295
|
+
4. Development setup and common commands
|
|
296
|
+
5. Architecture overview (brief)
|
|
297
|
+
6. Development guidelines
|
|
298
|
+
|
|
299
|
+
Keep it under 2000 tokens. Use markdown formatting. Be specific to the detected stack and structure.
|
|
300
|
+
`;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const result = await _deps.callLLM(prompt);
|
|
304
|
+
logger.info("init", "Generated context.md with LLM");
|
|
305
|
+
return result;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
logger.warn(
|
|
308
|
+
"init",
|
|
309
|
+
`LLM context generation failed, falling back to template: ${err instanceof Error ? err.message : String(err)}`,
|
|
310
|
+
);
|
|
311
|
+
return generateContextTemplate(scan);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Initialize context.md for a project
|
|
317
|
+
*/
|
|
318
|
+
export async function initContext(projectRoot: string, options: InitContextOptions = {}): Promise<void> {
|
|
319
|
+
const logger = getLogger();
|
|
320
|
+
const naxDir = join(projectRoot, "nax");
|
|
321
|
+
const contextPath = join(naxDir, "context.md");
|
|
322
|
+
|
|
323
|
+
// Check if context.md already exists
|
|
324
|
+
if (existsSync(contextPath) && !options.force) {
|
|
325
|
+
logger.info("init", "context.md already exists, skipping (use --force to overwrite)", { path: contextPath });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Create nax directory if needed
|
|
330
|
+
if (!existsSync(naxDir)) {
|
|
331
|
+
await mkdir(naxDir, { recursive: true });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Scan the project
|
|
335
|
+
const scan = await scanProject(projectRoot);
|
|
336
|
+
|
|
337
|
+
// Generate content (template or LLM-enhanced)
|
|
338
|
+
let content: string;
|
|
339
|
+
if (options.ai) {
|
|
340
|
+
content = await generateContextWithLLM(scan);
|
|
341
|
+
} else {
|
|
342
|
+
content = generateContextTemplate(scan);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Write context.md
|
|
346
|
+
await Bun.write(contextPath, content);
|
|
347
|
+
logger.info("init", "Generated nax/context.md template from project scan", { path: contextPath });
|
|
348
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Stack Detection for nax init
|
|
3
|
+
*
|
|
4
|
+
* Scans the project root for stack indicators and builds quality.commands
|
|
5
|
+
* for nax/config.json.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
/** Detected project runtime */
|
|
12
|
+
export type Runtime = "bun" | "node" | "unknown";
|
|
13
|
+
|
|
14
|
+
/** Detected project language */
|
|
15
|
+
export type Language = "typescript" | "python" | "rust" | "go" | "unknown";
|
|
16
|
+
|
|
17
|
+
/** Detected linter */
|
|
18
|
+
export type Linter = "biome" | "eslint" | "ruff" | "clippy" | "golangci-lint" | "unknown";
|
|
19
|
+
|
|
20
|
+
/** Detected monorepo tooling */
|
|
21
|
+
export type Monorepo = "turborepo" | "none";
|
|
22
|
+
|
|
23
|
+
/** Full detected project stack */
|
|
24
|
+
export interface ProjectStack {
|
|
25
|
+
runtime: Runtime;
|
|
26
|
+
language: Language;
|
|
27
|
+
linter: Linter;
|
|
28
|
+
monorepo: Monorepo;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Quality commands derived from stack detection */
|
|
32
|
+
export interface QualityCommands {
|
|
33
|
+
typecheck?: string;
|
|
34
|
+
lint?: string;
|
|
35
|
+
test?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectRuntime(projectRoot: string): Runtime {
|
|
39
|
+
if (existsSync(join(projectRoot, "bun.lockb")) || existsSync(join(projectRoot, "bunfig.toml"))) {
|
|
40
|
+
return "bun";
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
existsSync(join(projectRoot, "package-lock.json")) ||
|
|
44
|
+
existsSync(join(projectRoot, "yarn.lock")) ||
|
|
45
|
+
existsSync(join(projectRoot, "pnpm-lock.yaml"))
|
|
46
|
+
) {
|
|
47
|
+
return "node";
|
|
48
|
+
}
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function detectLanguage(projectRoot: string): Language {
|
|
53
|
+
if (existsSync(join(projectRoot, "tsconfig.json"))) return "typescript";
|
|
54
|
+
if (existsSync(join(projectRoot, "pyproject.toml")) || existsSync(join(projectRoot, "setup.py"))) {
|
|
55
|
+
return "python";
|
|
56
|
+
}
|
|
57
|
+
if (existsSync(join(projectRoot, "Cargo.toml"))) return "rust";
|
|
58
|
+
if (existsSync(join(projectRoot, "go.mod"))) return "go";
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function detectLinter(projectRoot: string): Linter {
|
|
63
|
+
if (existsSync(join(projectRoot, "biome.json")) || existsSync(join(projectRoot, "biome.jsonc"))) {
|
|
64
|
+
return "biome";
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
existsSync(join(projectRoot, ".eslintrc.json")) ||
|
|
68
|
+
existsSync(join(projectRoot, ".eslintrc.js")) ||
|
|
69
|
+
existsSync(join(projectRoot, "eslint.config.js"))
|
|
70
|
+
) {
|
|
71
|
+
return "eslint";
|
|
72
|
+
}
|
|
73
|
+
return "unknown";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function detectMonorepo(projectRoot: string): Monorepo {
|
|
77
|
+
if (existsSync(join(projectRoot, "turbo.json"))) return "turborepo";
|
|
78
|
+
return "none";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Detect the project stack by scanning for indicator files.
|
|
83
|
+
*/
|
|
84
|
+
export function detectProjectStack(projectRoot: string): ProjectStack {
|
|
85
|
+
return {
|
|
86
|
+
runtime: detectRuntime(projectRoot),
|
|
87
|
+
language: detectLanguage(projectRoot),
|
|
88
|
+
linter: detectLinter(projectRoot),
|
|
89
|
+
monorepo: detectMonorepo(projectRoot),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveLintCommand(stack: ProjectStack, fallback: string): string {
|
|
94
|
+
if (stack.linter === "biome") return "biome check .";
|
|
95
|
+
if (stack.linter === "eslint") return "eslint .";
|
|
96
|
+
return fallback;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build quality.commands from a detected project stack.
|
|
101
|
+
*/
|
|
102
|
+
export function buildQualityCommands(stack: ProjectStack): QualityCommands {
|
|
103
|
+
if (stack.runtime === "bun" && stack.language === "typescript") {
|
|
104
|
+
return {
|
|
105
|
+
typecheck: "bun run tsc --noEmit",
|
|
106
|
+
lint: resolveLintCommand(stack, "bun run lint"),
|
|
107
|
+
test: "bun test",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (stack.runtime === "node" && stack.language === "typescript") {
|
|
112
|
+
return {
|
|
113
|
+
typecheck: "npx tsc --noEmit",
|
|
114
|
+
lint: resolveLintCommand(stack, "npm run lint"),
|
|
115
|
+
test: "npm test",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (stack.language === "python") {
|
|
120
|
+
return {
|
|
121
|
+
lint: "ruff check .",
|
|
122
|
+
test: "pytest",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (stack.language === "rust") {
|
|
127
|
+
return {
|
|
128
|
+
typecheck: "cargo check",
|
|
129
|
+
lint: "cargo clippy",
|
|
130
|
+
test: "cargo test",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (stack.language === "go") {
|
|
135
|
+
return {
|
|
136
|
+
typecheck: "go vet ./...",
|
|
137
|
+
lint: "golangci-lint run",
|
|
138
|
+
test: "go test ./...",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isStackDetected(stack: ProjectStack): boolean {
|
|
146
|
+
return stack.runtime !== "unknown" || stack.language !== "unknown";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build the full init config object from a detected project stack.
|
|
151
|
+
* Falls back to minimal config when stack is undetected.
|
|
152
|
+
*/
|
|
153
|
+
export function buildInitConfig(stack: ProjectStack): object {
|
|
154
|
+
if (!isStackDetected(stack)) {
|
|
155
|
+
return { version: 1 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const commands = buildQualityCommands(stack);
|
|
159
|
+
const hasCommands = Object.keys(commands).length > 0;
|
|
160
|
+
|
|
161
|
+
if (!hasCommands) {
|
|
162
|
+
return { version: 1 };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
version: 1,
|
|
167
|
+
quality: { commands },
|
|
168
|
+
};
|
|
169
|
+
}
|
package/src/cli/init.ts
CHANGED
|
@@ -8,8 +8,10 @@ import { existsSync } from "node:fs";
|
|
|
8
8
|
import { mkdir } from "node:fs/promises";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { globalConfigDir, projectConfigDir } from "../config/paths";
|
|
11
|
-
import { DEFAULT_CONFIG } from "../config/schema";
|
|
12
11
|
import { getLogger } from "../logger";
|
|
12
|
+
import { initContext } from "./init-context";
|
|
13
|
+
import { buildInitConfig, detectProjectStack } from "./init-detect";
|
|
14
|
+
import type { ProjectStack } from "./init-detect";
|
|
13
15
|
import { promptsInitCommand } from "./prompts";
|
|
14
16
|
|
|
15
17
|
/** Init command options */
|
|
@@ -20,10 +22,18 @@ export interface InitOptions {
|
|
|
20
22
|
projectRoot?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/** Options for initProject */
|
|
26
|
+
export interface InitProjectOptions {
|
|
27
|
+
/** Use LLM to generate context.md (--ai flag) */
|
|
28
|
+
ai?: boolean;
|
|
29
|
+
/** Force overwrite of existing files */
|
|
30
|
+
force?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
/**
|
|
24
34
|
* Gitignore entries added by nax init
|
|
25
35
|
*/
|
|
26
|
-
const NAX_GITIGNORE_ENTRIES = [".nax-verifier-verdict.json"];
|
|
36
|
+
const NAX_GITIGNORE_ENTRIES = [".nax-verifier-verdict.json", "nax.lock", "nax/**/runs/", "nax/metrics.json"];
|
|
27
37
|
|
|
28
38
|
/**
|
|
29
39
|
* Add nax-specific entries to .gitignore if not already present.
|
|
@@ -55,33 +65,58 @@ async function updateGitignore(projectRoot: string): Promise<void> {
|
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
/**
|
|
58
|
-
*
|
|
68
|
+
* Build a stack-aware constitution.md from the detected project stack.
|
|
59
69
|
*/
|
|
60
|
-
|
|
70
|
+
function buildConstitution(stack: ProjectStack): string {
|
|
71
|
+
const sections: string[] = [];
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
- Deliver high-quality, maintainable code
|
|
64
|
-
- Follow project conventions and best practices
|
|
65
|
-
- Maintain comprehensive test coverage
|
|
73
|
+
sections.push("# Project Constitution\n");
|
|
66
74
|
|
|
67
|
-
##
|
|
68
|
-
-
|
|
69
|
-
- Follow
|
|
70
|
-
-
|
|
75
|
+
sections.push("## Goals");
|
|
76
|
+
sections.push("- Deliver high-quality, maintainable code");
|
|
77
|
+
sections.push("- Follow project conventions and best practices");
|
|
78
|
+
sections.push("- Maintain comprehensive test coverage\n");
|
|
71
79
|
|
|
72
|
-
##
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
- Clear, descriptive naming
|
|
76
|
-
`;
|
|
80
|
+
sections.push("## Constraints");
|
|
81
|
+
sections.push("- Follow functional style for pure logic");
|
|
82
|
+
sections.push("- Keep files focused and under 400 lines\n");
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
84
|
+
if (stack.runtime === "bun") {
|
|
85
|
+
sections.push("## Bun-Native APIs");
|
|
86
|
+
sections.push("- Use `Bun.file()` for file reads, `Bun.write()` for file writes");
|
|
87
|
+
sections.push("- Use `Bun.spawn()` for subprocesses (never `child_process`)");
|
|
88
|
+
sections.push("- Use `Bun.sleep()` for delays");
|
|
89
|
+
sections.push("- Use `bun test` for running tests\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (stack.language === "typescript") {
|
|
93
|
+
sections.push("## strict TypeScript");
|
|
94
|
+
sections.push("- Enable strict mode in tsconfig.json");
|
|
95
|
+
sections.push("- No `any` in public APIs — use `unknown` + type guards");
|
|
96
|
+
sections.push("- Explicit return types on all exported functions\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (stack.language === "python") {
|
|
100
|
+
sections.push("## Python Standards");
|
|
101
|
+
sections.push("- Follow PEP 8 style guide for formatting and naming");
|
|
102
|
+
sections.push("- Add type hints to all function signatures");
|
|
103
|
+
sections.push("- Use type annotations for variables where non-obvious\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (stack.monorepo === "turborepo") {
|
|
107
|
+
sections.push("## Monorepo Conventions");
|
|
108
|
+
sections.push("- Respect package boundaries — do not import across packages without explicit dependency");
|
|
109
|
+
sections.push("- Each package should be independently buildable and testable");
|
|
110
|
+
sections.push("- Shared utilities go in a dedicated `packages/shared` (or equivalent) package\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
sections.push("## Preferences");
|
|
114
|
+
sections.push("- Prefer immutability over mutation");
|
|
115
|
+
sections.push("- Write tests first (TDD approach)");
|
|
116
|
+
sections.push("- Clear, descriptive naming");
|
|
117
|
+
|
|
118
|
+
return `${sections.join("\n")}\n`;
|
|
119
|
+
}
|
|
85
120
|
|
|
86
121
|
const MINIMAL_GLOBAL_CONFIG = {
|
|
87
122
|
version: 1,
|
|
@@ -113,7 +148,10 @@ async function initGlobal(): Promise<void> {
|
|
|
113
148
|
// Create ~/.nax/constitution.md if it doesn't exist
|
|
114
149
|
const constitutionPath = join(globalDir, "constitution.md");
|
|
115
150
|
if (!existsSync(constitutionPath)) {
|
|
116
|
-
await Bun.write(
|
|
151
|
+
await Bun.write(
|
|
152
|
+
constitutionPath,
|
|
153
|
+
buildConstitution({ runtime: "unknown", language: "unknown", linter: "unknown", monorepo: "none" }),
|
|
154
|
+
);
|
|
117
155
|
logger.info("init", "Created global constitution", { path: constitutionPath });
|
|
118
156
|
} else {
|
|
119
157
|
logger.info("init", "Global constitution already exists", { path: constitutionPath });
|
|
@@ -134,7 +172,7 @@ async function initGlobal(): Promise<void> {
|
|
|
134
172
|
/**
|
|
135
173
|
* Initialize project nax directory (nax/)
|
|
136
174
|
*/
|
|
137
|
-
export async function initProject(projectRoot: string): Promise<void> {
|
|
175
|
+
export async function initProject(projectRoot: string, options?: InitProjectOptions): Promise<void> {
|
|
138
176
|
const logger = getLogger();
|
|
139
177
|
const projectDir = projectConfigDir(projectRoot);
|
|
140
178
|
|
|
@@ -144,19 +182,32 @@ export async function initProject(projectRoot: string): Promise<void> {
|
|
|
144
182
|
logger.info("init", "Created project config directory", { path: projectDir });
|
|
145
183
|
}
|
|
146
184
|
|
|
185
|
+
// Detect project stack and build config
|
|
186
|
+
const stack = detectProjectStack(projectRoot);
|
|
187
|
+
const projectConfig = buildInitConfig(stack);
|
|
188
|
+
logger.info("init", "Detected project stack", {
|
|
189
|
+
runtime: stack.runtime,
|
|
190
|
+
language: stack.language,
|
|
191
|
+
linter: stack.linter,
|
|
192
|
+
monorepo: stack.monorepo,
|
|
193
|
+
});
|
|
194
|
+
|
|
147
195
|
// Create nax/config.json if it doesn't exist
|
|
148
196
|
const configPath = join(projectDir, "config.json");
|
|
149
197
|
if (!existsSync(configPath)) {
|
|
150
|
-
await Bun.write(configPath, `${JSON.stringify(
|
|
198
|
+
await Bun.write(configPath, `${JSON.stringify(projectConfig, null, 2)}\n`);
|
|
151
199
|
logger.info("init", "Created project config", { path: configPath });
|
|
152
200
|
} else {
|
|
153
201
|
logger.info("init", "Project config already exists", { path: configPath });
|
|
154
202
|
}
|
|
155
203
|
|
|
156
|
-
//
|
|
204
|
+
// Generate context.md (template or LLM-enhanced with --ai flag)
|
|
205
|
+
await initContext(projectRoot, { ai: options?.ai, force: options?.force });
|
|
206
|
+
|
|
207
|
+
// Create nax/constitution.md with stack-aware content
|
|
157
208
|
const constitutionPath = join(projectDir, "constitution.md");
|
|
158
|
-
if (!existsSync(constitutionPath)) {
|
|
159
|
-
await Bun.write(constitutionPath,
|
|
209
|
+
if (!existsSync(constitutionPath) || options?.force) {
|
|
210
|
+
await Bun.write(constitutionPath, buildConstitution(stack));
|
|
160
211
|
logger.info("init", "Created project constitution", { path: constitutionPath });
|
|
161
212
|
} else {
|
|
162
213
|
logger.info("init", "Project constitution already exists", { path: constitutionPath });
|
|
@@ -174,11 +225,25 @@ export async function initProject(projectRoot: string): Promise<void> {
|
|
|
174
225
|
// Update .gitignore to include nax-specific entries
|
|
175
226
|
await updateGitignore(projectRoot);
|
|
176
227
|
|
|
177
|
-
// Create prompt templates
|
|
228
|
+
// Create prompt templates
|
|
178
229
|
// Pass autoWireConfig: false to prevent auto-wiring prompts.overrides
|
|
179
230
|
// Templates are created but not activated until user explicitly configures them
|
|
180
231
|
await promptsInitCommand({ workdir: projectRoot, force: false, autoWireConfig: false });
|
|
181
232
|
|
|
233
|
+
// Print summary
|
|
234
|
+
console.log("\n[OK] nax init complete. Created files:");
|
|
235
|
+
console.log(" - nax/config.json");
|
|
236
|
+
console.log(" - nax/context.md");
|
|
237
|
+
console.log(" - nax/constitution.md");
|
|
238
|
+
console.log(" - nax/hooks/");
|
|
239
|
+
console.log(" - nax/templates/");
|
|
240
|
+
console.log("\nNext steps:");
|
|
241
|
+
console.log(" 1. Review nax/context.md and fill in TODOs");
|
|
242
|
+
console.log(" 2. Review nax/config.json and adjust quality commands");
|
|
243
|
+
console.log(" 3. Run: nax generate");
|
|
244
|
+
console.log(" 4. Run: nax plan");
|
|
245
|
+
console.log(" 5. Run: nax run");
|
|
246
|
+
|
|
182
247
|
logger.info("init", "Project config initialized successfully", { path: projectDir });
|
|
183
248
|
}
|
|
184
249
|
|
package/src/cli/plugins.ts
CHANGED
|
@@ -25,7 +25,13 @@ export async function pluginsListCommand(
|
|
|
25
25
|
const globalPluginsDir = overrideGlobalPluginsDir ?? path.join(os.homedir(), ".nax", "plugins");
|
|
26
26
|
const projectPluginsDir = path.join(workdir, "nax", "plugins");
|
|
27
27
|
const configPlugins = config.plugins || [];
|
|
28
|
-
const registry = await loadPlugins(
|
|
28
|
+
const registry = await loadPlugins(
|
|
29
|
+
globalPluginsDir,
|
|
30
|
+
projectPluginsDir,
|
|
31
|
+
configPlugins,
|
|
32
|
+
workdir,
|
|
33
|
+
config.disabledPlugins,
|
|
34
|
+
);
|
|
29
35
|
const plugins = registry.plugins;
|
|
30
36
|
|
|
31
37
|
if (plugins.length === 0) {
|
|
@@ -39,20 +45,24 @@ export async function pluginsListCommand(
|
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
// Build table data
|
|
48
|
+
const disabledSet = new Set(config.disabledPlugins ?? []);
|
|
42
49
|
const rows: Array<{
|
|
43
50
|
name: string;
|
|
44
51
|
version: string;
|
|
45
52
|
provides: string;
|
|
46
53
|
source: string;
|
|
54
|
+
enabled: string;
|
|
47
55
|
}> = plugins.map((plugin) => {
|
|
48
56
|
const source = registry.getSource(plugin.name);
|
|
49
57
|
const sourceStr = source ? formatSource(source.type, source.path) : "unknown";
|
|
58
|
+
const isDisabled = disabledSet.has(plugin.name);
|
|
50
59
|
|
|
51
60
|
return {
|
|
52
61
|
name: plugin.name,
|
|
53
62
|
version: plugin.version,
|
|
54
63
|
provides: plugin.provides.join(", "),
|
|
55
64
|
source: sourceStr,
|
|
65
|
+
enabled: isDisabled ? "disabled" : "enabled",
|
|
56
66
|
};
|
|
57
67
|
});
|
|
58
68
|
|
|
@@ -62,20 +72,21 @@ export async function pluginsListCommand(
|
|
|
62
72
|
version: Math.max(7, ...rows.map((r) => r.version.length)),
|
|
63
73
|
provides: Math.max(8, ...rows.map((r) => r.provides.length)),
|
|
64
74
|
source: Math.max(6, ...rows.map((r) => r.source.length)),
|
|
75
|
+
enabled: Math.max(7, ...rows.map((r) => r.enabled.length)),
|
|
65
76
|
};
|
|
66
77
|
|
|
67
78
|
// Display table
|
|
68
79
|
console.log("\nInstalled Plugins:\n");
|
|
69
80
|
console.log(
|
|
70
|
-
`${pad("Name", widths.name)} ${pad("Version", widths.version)} ${pad("Provides", widths.provides)} ${pad("Source", widths.source)}`,
|
|
81
|
+
`${pad("Name", widths.name)} ${pad("Version", widths.version)} ${pad("Provides", widths.provides)} ${pad("Source", widths.source)} ${pad("Status", widths.enabled)}`,
|
|
71
82
|
);
|
|
72
83
|
console.log(
|
|
73
|
-
`${"-".repeat(widths.name)} ${"-".repeat(widths.version)} ${"-".repeat(widths.provides)} ${"-".repeat(widths.source)}`,
|
|
84
|
+
`${"-".repeat(widths.name)} ${"-".repeat(widths.version)} ${"-".repeat(widths.provides)} ${"-".repeat(widths.source)} ${"-".repeat(widths.enabled)}`,
|
|
74
85
|
);
|
|
75
86
|
|
|
76
87
|
for (const row of rows) {
|
|
77
88
|
console.log(
|
|
78
|
-
`${pad(row.name, widths.name)} ${pad(row.version, widths.version)} ${pad(row.provides, widths.provides)} ${pad(row.source, widths.source)}`,
|
|
89
|
+
`${pad(row.name, widths.name)} ${pad(row.version, widths.version)} ${pad(row.provides, widths.provides)} ${pad(row.source, widths.source)} ${pad(row.enabled, widths.enabled)}`,
|
|
79
90
|
);
|
|
80
91
|
}
|
|
81
92
|
|
|
@@ -267,6 +267,7 @@ export interface OptimizerConfig {
|
|
|
267
267
|
export interface PluginConfigEntry {
|
|
268
268
|
module: string;
|
|
269
269
|
config?: Record<string, unknown>;
|
|
270
|
+
enabled?: boolean;
|
|
270
271
|
}
|
|
271
272
|
|
|
272
273
|
export interface HooksConfig {
|
|
@@ -435,6 +436,8 @@ export interface NaxConfig {
|
|
|
435
436
|
optimizer?: OptimizerConfig;
|
|
436
437
|
/** Plugin configurations (v0.10) */
|
|
437
438
|
plugins?: PluginConfigEntry[];
|
|
439
|
+
/** Disabled plugin names (v0.38.2) */
|
|
440
|
+
disabledPlugins?: string[];
|
|
438
441
|
/** Hooks configuration (v0.10) */
|
|
439
442
|
hooks?: HooksConfig;
|
|
440
443
|
/** Interaction settings (v0.15.0) */
|
package/src/config/schemas.ts
CHANGED
|
@@ -251,6 +251,7 @@ const OptimizerConfigSchema = z.object({
|
|
|
251
251
|
const PluginConfigEntrySchema = z.object({
|
|
252
252
|
module: z.string().min(1, "plugin.module must be non-empty"),
|
|
253
253
|
config: z.record(z.string(), z.unknown()).optional(),
|
|
254
|
+
enabled: z.boolean().default(true),
|
|
254
255
|
});
|
|
255
256
|
|
|
256
257
|
const HooksConfigSchema = z.object({
|
|
@@ -330,6 +331,7 @@ export const NaxConfigSchema = z
|
|
|
330
331
|
context: ContextConfigSchema,
|
|
331
332
|
optimizer: OptimizerConfigSchema.optional(),
|
|
332
333
|
plugins: z.array(PluginConfigEntrySchema).optional(),
|
|
334
|
+
disabledPlugins: z.array(z.string()).optional(),
|
|
333
335
|
hooks: HooksConfigSchema.optional(),
|
|
334
336
|
interaction: InteractionConfigSchema.optional(),
|
|
335
337
|
precheck: PrecheckConfigSchema.optional(),
|
|
@@ -171,7 +171,13 @@ export async function setupRun(options: RunSetupOptions): Promise<RunSetupResult
|
|
|
171
171
|
const globalPluginsDir = path.join(os.homedir(), ".nax", "plugins");
|
|
172
172
|
const projectPluginsDir = path.join(workdir, "nax", "plugins");
|
|
173
173
|
const configPlugins = config.plugins || [];
|
|
174
|
-
const pluginRegistry = await loadPlugins(
|
|
174
|
+
const pluginRegistry = await loadPlugins(
|
|
175
|
+
globalPluginsDir,
|
|
176
|
+
projectPluginsDir,
|
|
177
|
+
configPlugins,
|
|
178
|
+
workdir,
|
|
179
|
+
config.disabledPlugins,
|
|
180
|
+
);
|
|
175
181
|
|
|
176
182
|
// Log plugins loaded
|
|
177
183
|
logger?.info("plugins", `Loaded ${pluginRegistry.plugins.length} plugins`, {
|
package/src/plugins/loader.ts
CHANGED
|
@@ -50,6 +50,24 @@ export interface PluginSource {
|
|
|
50
50
|
path: string;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Extract plugin name from file path.
|
|
55
|
+
* For index files (e.g., /path/to/plugin/index.ts), returns the parent directory name.
|
|
56
|
+
* For single files (e.g., /path/to/plugin.ts), returns the filename without extension.
|
|
57
|
+
*
|
|
58
|
+
* @param pluginPath - Path to plugin file
|
|
59
|
+
* @returns Plugin name
|
|
60
|
+
*/
|
|
61
|
+
function extractPluginName(pluginPath: string): string {
|
|
62
|
+
const basename = path.basename(pluginPath);
|
|
63
|
+
if (basename === "index.ts" || basename === "index.js" || basename === "index.mjs") {
|
|
64
|
+
// For index files, use the parent directory name
|
|
65
|
+
return path.basename(path.dirname(pluginPath));
|
|
66
|
+
}
|
|
67
|
+
// For single files, use filename without extension
|
|
68
|
+
return basename.replace(/\.(ts|js|mjs)$/, "");
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
/**
|
|
54
72
|
* Plugin with source information.
|
|
55
73
|
*/
|
|
@@ -67,11 +85,13 @@ export interface LoadedPlugin {
|
|
|
67
85
|
* 3. Load explicit modules from config.plugins[]
|
|
68
86
|
*
|
|
69
87
|
* Each plugin is validated, then setup() is called with its config.
|
|
88
|
+
* Plugins can be disabled via config.plugins[].enabled or config.disabledPlugins[].
|
|
70
89
|
*
|
|
71
90
|
* @param globalDir - Global plugins directory (e.g., ~/.nax/plugins)
|
|
72
91
|
* @param projectDir - Project plugins directory (e.g., <project>/nax/plugins)
|
|
73
92
|
* @param configPlugins - Explicit plugin entries from config
|
|
74
93
|
* @param projectRoot - Project root directory for resolving relative paths in config
|
|
94
|
+
* @param disabledPlugins - List of plugin names to disable (auto-discovered plugins only)
|
|
75
95
|
* @returns PluginRegistry with all loaded plugins and their sources
|
|
76
96
|
*/
|
|
77
97
|
export async function loadPlugins(
|
|
@@ -79,18 +99,25 @@ export async function loadPlugins(
|
|
|
79
99
|
projectDir: string,
|
|
80
100
|
configPlugins: PluginConfigEntry[],
|
|
81
101
|
projectRoot?: string,
|
|
102
|
+
disabledPlugins?: string[],
|
|
82
103
|
): Promise<PluginRegistry> {
|
|
83
104
|
const loadedPlugins: LoadedPlugin[] = [];
|
|
84
105
|
const effectiveProjectRoot = projectRoot || projectDir;
|
|
85
106
|
const pluginNames = new Set<string>();
|
|
107
|
+
const disabledSet = new Set(disabledPlugins ?? []);
|
|
108
|
+
const logger = getSafeLogger();
|
|
86
109
|
|
|
87
110
|
// 1. Load plugins from global directory
|
|
88
111
|
const globalPlugins = await discoverPlugins(globalDir);
|
|
89
112
|
for (const plugin of globalPlugins) {
|
|
113
|
+
const pluginName = extractPluginName(plugin.path);
|
|
114
|
+
if (disabledSet.has(pluginName)) {
|
|
115
|
+
logger?.info("plugins", `Skipping disabled plugin: '${pluginName}' (global directory)`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
90
118
|
const validated = await loadAndValidatePlugin(plugin.path, {}, [globalDir]);
|
|
91
119
|
if (validated) {
|
|
92
120
|
if (pluginNames.has(validated.name)) {
|
|
93
|
-
const logger = getSafeLogger();
|
|
94
121
|
logger?.warn("plugins", `Plugin name collision: '${validated.name}' (global directory)`);
|
|
95
122
|
}
|
|
96
123
|
loadedPlugins.push({
|
|
@@ -104,10 +131,14 @@ export async function loadPlugins(
|
|
|
104
131
|
// 2. Load plugins from project directory
|
|
105
132
|
const projectPlugins = await discoverPlugins(projectDir);
|
|
106
133
|
for (const plugin of projectPlugins) {
|
|
134
|
+
const pluginName = extractPluginName(plugin.path);
|
|
135
|
+
if (disabledSet.has(pluginName)) {
|
|
136
|
+
logger?.info("plugins", `Skipping disabled plugin: '${pluginName}' (project directory)`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
107
139
|
const validated = await loadAndValidatePlugin(plugin.path, {}, [projectDir]);
|
|
108
140
|
if (validated) {
|
|
109
141
|
if (pluginNames.has(validated.name)) {
|
|
110
|
-
const logger = getSafeLogger();
|
|
111
142
|
logger?.warn("plugins", `Plugin name collision: '${validated.name}' (project directory overrides global)`);
|
|
112
143
|
}
|
|
113
144
|
loadedPlugins.push({
|
|
@@ -120,6 +151,11 @@ export async function loadPlugins(
|
|
|
120
151
|
|
|
121
152
|
// 3. Load plugins from config entries
|
|
122
153
|
for (const entry of configPlugins) {
|
|
154
|
+
// Check if plugin is explicitly disabled in config
|
|
155
|
+
if (entry.enabled === false) {
|
|
156
|
+
logger?.info("plugins", `Skipping disabled plugin: '${entry.module}'`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
123
159
|
// Resolve module path relative to effective project root for relative paths
|
|
124
160
|
const resolvedModule = resolveModulePath(entry.module, effectiveProjectRoot);
|
|
125
161
|
const validated = await loadAndValidatePlugin(
|
|
@@ -130,7 +166,6 @@ export async function loadPlugins(
|
|
|
130
166
|
);
|
|
131
167
|
if (validated) {
|
|
132
168
|
if (pluginNames.has(validated.name)) {
|
|
133
|
-
const logger = getSafeLogger();
|
|
134
169
|
logger?.warn("plugins", `Plugin name collision: '${validated.name}' (config entry overrides previous)`);
|
|
135
170
|
}
|
|
136
171
|
loadedPlugins.push({
|