@nage-api/cli 1.0.0-beta.2

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/cli.js ADDED
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ /**
3
+ * The `nage` command layer (PLAN.md §10).
4
+ *
5
+ * Argument parsing uses Node's own `util.parseArgs` rather than a framework:
6
+ * the command surface is small and regular, and a CLI that scaffolds
7
+ * dependency-light projects should be dependency-light itself.
8
+ *
9
+ * `run()` returns an exit code and writes through an injected output object, so
10
+ * every command is testable without spawning a process.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.CLI_VERSION = void 0;
14
+ exports.run = run;
15
+ exports.usage = usage;
16
+ const node_util_1 = require("node:util");
17
+ const promises_1 = require("node:fs/promises");
18
+ const node_path_1 = require("node:path");
19
+ const core_1 = require("@nage-api/core");
20
+ const file_tree_js_1 = require("./fs/file-tree.js");
21
+ const manifest_js_1 = require("./workspace/manifest.js");
22
+ const create_js_1 = require("./commands/create.js");
23
+ const generate_js_1 = require("./commands/generate.js");
24
+ const doctor_js_1 = require("./commands/doctor.js");
25
+ const features_js_1 = require("./commands/features.js");
26
+ exports.CLI_VERSION = '0.0.0';
27
+ const GLOBAL_FLAGS = {
28
+ app: { type: 'string' },
29
+ all: { type: 'boolean' },
30
+ force: { type: 'boolean' },
31
+ 'dry-run': { type: 'boolean' },
32
+ help: { type: 'boolean', short: 'h' },
33
+ version: { type: 'boolean', short: 'v' },
34
+ // create / new app
35
+ db: { type: 'string' },
36
+ 'first-app': { type: 'string' },
37
+ preset: { type: 'string' },
38
+ port: { type: 'string' },
39
+ standalone: { type: 'boolean' },
40
+ // generate
41
+ fields: { type: 'string' },
42
+ route: { type: 'string' },
43
+ package: { type: 'string' },
44
+ 'no-migration': { type: 'boolean' },
45
+ 'no-spec': { type: 'boolean' },
46
+ // doctor
47
+ legacy: { type: 'boolean' },
48
+ };
49
+ /** Run one command. Returns the process exit code. */
50
+ async function run(options) {
51
+ const { io } = options;
52
+ let parsed;
53
+ try {
54
+ parsed = (0, node_util_1.parseArgs)({
55
+ args: [...options.argv],
56
+ options: GLOBAL_FLAGS,
57
+ allowPositionals: true,
58
+ strict: true,
59
+ });
60
+ }
61
+ catch (error) {
62
+ io.err(`✖ ${error instanceof Error ? error.message : String(error)}\n`);
63
+ io.err(' Run `nage --help` to see the available commands.\n');
64
+ return 1;
65
+ }
66
+ const flags = parsed.values;
67
+ const [command, ...rest] = parsed.positionals;
68
+ if (flags.version === true) {
69
+ io.out(`${options.version ?? exports.CLI_VERSION}\n`);
70
+ return 0;
71
+ }
72
+ if (command === undefined || flags.help === true) {
73
+ io.out(usage());
74
+ return command === undefined && flags.help !== true ? 1 : 0;
75
+ }
76
+ try {
77
+ return await dispatch(command, rest, flags, options);
78
+ }
79
+ catch (error) {
80
+ // Framework errors carry an operator-facing message; anything else is a bug
81
+ // and its stack belongs on stderr.
82
+ if ((0, core_1.isNageError)(error)) {
83
+ io.err(`✖ ${error.message}\n`);
84
+ const hint = error.meta?.['hint'];
85
+ if (typeof hint === 'string')
86
+ io.err(` ${hint}\n`);
87
+ return 1;
88
+ }
89
+ io.err(`✖ ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
90
+ return 1;
91
+ }
92
+ }
93
+ async function dispatch(command, positionals, flags, options) {
94
+ const { io, cwd } = options;
95
+ const dryRun = flags['dry-run'] === true;
96
+ switch (command) {
97
+ case 'create': {
98
+ const name = requirePositional(positionals, 0, 'nage create <name>');
99
+ const plan = (0, create_js_1.planCreate)({
100
+ name,
101
+ ...(typeof flags['db'] === 'string' ? { engine: flags['db'] } : {}),
102
+ ...(typeof flags['first-app'] === 'string' ? { firstApp: flags['first-app'] } : {}),
103
+ ...(typeof flags['preset'] === 'string' ? { preset: flags['preset'] } : {}),
104
+ ...(flags['standalone'] === true ? { standalone: true } : {}),
105
+ });
106
+ const root = (0, node_path_1.join)((0, node_path_1.resolve)(cwd), plan.manifest.name);
107
+ if (!dryRun)
108
+ await (0, promises_1.mkdir)(root, { recursive: true });
109
+ const result = await (0, file_tree_js_1.commitFileTree)(plan.tree, { root, dryRun });
110
+ report(io, result, dryRun);
111
+ io.out(`\n${plan.notes.join('\n')}\n`);
112
+ return 0;
113
+ }
114
+ case 'new': {
115
+ const kind = requirePositional(positionals, 0, 'nage new app|package <name>');
116
+ const name = requirePositional(positionals, 1, `nage new ${kind} <name>`);
117
+ const { root, manifest } = await (0, manifest_js_1.loadWorkspace)(cwd);
118
+ const plan = kind === 'app'
119
+ ? await (0, create_js_1.planNewApp)({
120
+ root,
121
+ manifest,
122
+ name,
123
+ ...(typeof flags['preset'] === 'string' ? { preset: flags['preset'] } : {}),
124
+ ...(typeof flags['port'] === 'string' ? { port: Number(flags['port']) } : {}),
125
+ })
126
+ : kind === 'package'
127
+ ? await (0, create_js_1.planNewPackage)({ root, manifest, name })
128
+ : fail(`"${kind}" is not something \`nage new\` can create (app, package)`);
129
+ const result = await (0, file_tree_js_1.commitFileTree)(plan.tree, { root, dryRun });
130
+ report(io, result, dryRun);
131
+ io.out(`\n${plan.notes.join('\n')}\n`);
132
+ return 0;
133
+ }
134
+ case 'generate':
135
+ case 'g': {
136
+ const schematic = requirePositional(positionals, 0, 'nage g <schematic> <name>');
137
+ if (!generate_js_1.SCHEMATICS.includes(schematic)) {
138
+ return fail(`"${schematic}" is not a schematic (${generate_js_1.SCHEMATICS.join(', ')})`);
139
+ }
140
+ const name = requirePositional(positionals, 1, `nage g ${schematic} <name>`);
141
+ const { root, manifest } = await (0, manifest_js_1.loadWorkspace)(cwd);
142
+ const app = (0, manifest_js_1.resolveTargetApp)(manifest, {
143
+ root,
144
+ cwd,
145
+ ...(typeof flags['app'] === 'string' ? { app: flags['app'] } : {}),
146
+ });
147
+ const plan = await (0, generate_js_1.planGenerate)({
148
+ root,
149
+ manifest,
150
+ schematic: schematic,
151
+ name,
152
+ appName: app.name,
153
+ ...(typeof flags['fields'] === 'string' ? { fields: flags['fields'] } : {}),
154
+ ...(typeof flags['route'] === 'string' ? { route: flags['route'] } : {}),
155
+ ...(typeof flags['package'] === 'string' ? { entityPackage: flags['package'] } : {}),
156
+ ...(flags['no-migration'] === true ? { withMigration: false } : {}),
157
+ ...(flags['no-spec'] === true ? { withSpec: false } : {}),
158
+ });
159
+ const result = await (0, file_tree_js_1.commitFileTree)(plan.tree, { root, dryRun });
160
+ report(io, result, dryRun);
161
+ if (plan.notes.length > 0)
162
+ io.out(`\n${plan.notes.join('\n')}\n`);
163
+ return 0;
164
+ }
165
+ case 'add':
166
+ case 'remove': {
167
+ const target = requirePositional(positionals, 0, `nage ${command} <feature|app|package>`);
168
+ const { root, manifest } = await (0, manifest_js_1.loadWorkspace)(cwd);
169
+ if (command === 'remove' && (target === 'app' || target === 'package')) {
170
+ const name = requirePositional(positionals, 1, `nage remove ${target} <name>`);
171
+ const plan = target === 'app'
172
+ ? await (0, features_js_1.planRemoveApp)({ root, manifest, name })
173
+ : await (0, features_js_1.planRemovePackage)({ root, manifest, name });
174
+ const result = await (0, file_tree_js_1.commitFileTree)(plan.tree, { root, dryRun });
175
+ if (!dryRun && target === 'app')
176
+ await (0, file_tree_js_1.archiveDirectory)(root, `apps/${name}`);
177
+ report(io, result, dryRun);
178
+ io.out(`\n${plan.notes.join('\n')}\n`);
179
+ return 0;
180
+ }
181
+ const apps = (0, features_js_1.selectApps)(manifest, {
182
+ ...(typeof flags['app'] === 'string' ? { app: flags['app'] } : {}),
183
+ ...(flags['all'] === true ? { all: true } : {}),
184
+ }).map((app) => app.name);
185
+ const plan = command === 'add'
186
+ ? await (0, features_js_1.planAddFeature)({ root, manifest, feature: target, apps })
187
+ : await (0, features_js_1.planRemoveFeature)({ root, manifest, feature: target, apps });
188
+ const result = await (0, file_tree_js_1.commitFileTree)(plan.tree, { root, dryRun });
189
+ report(io, result, dryRun);
190
+ io.out(`\n${plan.notes.join('\n')}\n`);
191
+ return 0;
192
+ }
193
+ case 'list': {
194
+ const { manifest } = await (0, manifest_js_1.loadWorkspace)(cwd);
195
+ io.out((0, features_js_1.formatList)(manifest));
196
+ return 0;
197
+ }
198
+ case 'info': {
199
+ const { manifest } = await (0, manifest_js_1.loadWorkspace)(cwd);
200
+ io.out((0, features_js_1.formatInfo)(manifest, options.version ?? exports.CLI_VERSION));
201
+ return 0;
202
+ }
203
+ case 'doctor': {
204
+ const { root, manifest } = await (0, manifest_js_1.loadWorkspace)(cwd);
205
+ // No `env` is passed: `doctor` weighs the workspace's own `.env`, not the
206
+ // shell's, so the report is about the project rather than the machine.
207
+ const report_ = await (0, doctor_js_1.runDoctor)({
208
+ root,
209
+ manifest,
210
+ ...(flags['legacy'] === true ? { legacy: true } : {}),
211
+ ...(typeof flags['app'] === 'string' ? { app: flags['app'] } : {}),
212
+ });
213
+ io.out(`${(0, core_1.formatFindings)(report_.findings)}\n`);
214
+ // A non-zero exit is what makes `doctor` usable as a CI gate.
215
+ return report_.healthy ? 0 : 1;
216
+ }
217
+ default:
218
+ return fail(`"${command}" is not a nage command. Run \`nage --help\`.`);
219
+ }
220
+ }
221
+ function report(io, result, dryRun) {
222
+ const verb = dryRun ? 'Would write' : 'Wrote';
223
+ io.out(`${verb} ${String(result.written.length)} file(s):\n`);
224
+ for (const path of result.written)
225
+ io.out(` + ${path}\n`);
226
+ for (const path of result.skipped)
227
+ io.out(` · ${path} (exists, left alone)\n`);
228
+ }
229
+ function requirePositional(positionals, index, usageLine) {
230
+ const value = positionals[index];
231
+ if (value === undefined || value === '')
232
+ fail(`Missing argument. Usage: ${usageLine}`);
233
+ return value;
234
+ }
235
+ function fail(message) {
236
+ const error = new Error(message);
237
+ error.name = 'UsageError';
238
+ throw error;
239
+ }
240
+ function usage() {
241
+ return [
242
+ 'nage — scaffold and manage a @nage-api workspace',
243
+ '',
244
+ 'Usage: nage <command> [options]',
245
+ '',
246
+ 'Commands:',
247
+ ' create <name> Scaffold a workspace seeded with one app',
248
+ ' new app <name> Add an app to the current workspace',
249
+ ' new package <name> Add a shared local package',
250
+ ' g|generate <what> <name> resource, module, service, controller, entity, dto, migration, seed',
251
+ ' add <feature> Enable auth|cache|queue|storage|realtime|notify|observability',
252
+ ' remove <feature> Disable a feature',
253
+ ' remove app|package <name> Unregister an app or package',
254
+ ' list Show apps and packages',
255
+ ' info Show versions and workspace summary',
256
+ ' doctor Check workspace integrity and configuration',
257
+ '',
258
+ 'Options:',
259
+ ' --app <name> Target one app (defaults to the app the cwd is in)',
260
+ ' --all Target every app',
261
+ ' --db <engine> postgres|mysql|mariadb|sqlite|mongodb (create)',
262
+ ' --preset <p> minimal|api|api-realtime|worker',
263
+ ' --port <n> Port for a new app',
264
+ ' --fields <spec> "name:string,slug:string:unique" (generate)',
265
+ ' --route <path> Base route for a resource',
266
+ ' --package <name> Put a generated entity in a shared package',
267
+ ' --no-migration Skip the migration (generate resource)',
268
+ ' --no-spec Skip the tests (not recommended)',
269
+ ' --legacy Also scan for legacy insecure patterns (doctor)',
270
+ ' --dry-run Show what would change without writing',
271
+ ' -h, --help Show this help',
272
+ ' -v, --version Show the CLI version',
273
+ '',
274
+ ].join('\n');
275
+ }
276
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `nage create <name>` and `nage new app|package` (PLAN.md §10.1).
3
+ *
4
+ * Every command here returns the files it would write plus the manifest it
5
+ * would leave behind, so the caller can commit them in one transaction, print
6
+ * them for `--dry-run`, or assert on them in a test. Nothing writes directly.
7
+ */
8
+ import type { DatabaseDriver } from '@nage-api/contracts';
9
+ import { FileTree } from '../fs/file-tree.js';
10
+ import { type FeatureName, type Preset, type WorkspaceManifest } from '../workspace/manifest.js';
11
+ /** What a command intends to do, before anything touches the disk. */
12
+ export interface CommandPlan {
13
+ readonly tree: FileTree;
14
+ readonly manifest: WorkspaceManifest;
15
+ /** Lines printed after a successful run. */
16
+ readonly notes: readonly string[];
17
+ }
18
+ export interface CreateOptions {
19
+ readonly name: string;
20
+ readonly engine?: DatabaseDriver;
21
+ readonly firstApp?: string;
22
+ readonly preset?: Preset;
23
+ readonly features?: readonly FeatureName[];
24
+ readonly frameworkVersion?: string;
25
+ readonly packageManager?: 'pnpm' | 'npm' | 'yarn';
26
+ /** Emit a single app at the root instead of a workspace (§10.2). */
27
+ readonly standalone?: boolean;
28
+ }
29
+ export declare const DEFAULT_FRAMEWORK_VERSION = "^0.1.0";
30
+ /**
31
+ * Plan a new workspace.
32
+ *
33
+ * Workspace-first by §27.10: a lone app inside a workspace costs almost
34
+ * nothing, and growing to several apps later is then friction-free.
35
+ */
36
+ export declare function planCreate(options: CreateOptions): CommandPlan;
37
+ export interface NewAppOptions {
38
+ readonly root: string;
39
+ readonly manifest: WorkspaceManifest;
40
+ readonly name: string;
41
+ readonly preset?: Preset;
42
+ readonly port?: number;
43
+ readonly features?: readonly FeatureName[];
44
+ }
45
+ /** Plan an additional app inside an existing workspace. */
46
+ export declare function planNewApp(options: NewAppOptions): Promise<CommandPlan>;
47
+ export interface NewPackageOptions {
48
+ readonly root: string;
49
+ readonly manifest: WorkspaceManifest;
50
+ readonly name: string;
51
+ /** Import alias; defaults to `@app/<name>`. */
52
+ readonly alias?: string;
53
+ }
54
+ /** Plan a shared local package (§9.1: the team's own code, not the framework's). */
55
+ export declare function planNewPackage(options: NewPackageOptions): Promise<CommandPlan>;
56
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ /**
3
+ * `nage create <name>` and `nage new app|package` (PLAN.md §10.1).
4
+ *
5
+ * Every command here returns the files it would write plus the manifest it
6
+ * would leave behind, so the caller can commit them in one transaction, print
7
+ * them for `--dry-run`, or assert on them in a test. Nothing writes directly.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DEFAULT_FRAMEWORK_VERSION = void 0;
11
+ exports.planCreate = planCreate;
12
+ exports.planNewApp = planNewApp;
13
+ exports.planNewPackage = planNewPackage;
14
+ const promises_1 = require("node:fs/promises");
15
+ const node_path_1 = require("node:path");
16
+ const core_1 = require("@nage-api/core");
17
+ const file_tree_js_1 = require("../fs/file-tree.js");
18
+ const naming_js_1 = require("../naming.js");
19
+ const manifest_js_1 = require("../workspace/manifest.js");
20
+ const wiring_js_1 = require("../workspace/wiring.js");
21
+ const app_template_js_1 = require("../templates/app.template.js");
22
+ const workspace_template_js_1 = require("../templates/workspace.template.js");
23
+ exports.DEFAULT_FRAMEWORK_VERSION = '^0.1.0';
24
+ /**
25
+ * Plan a new workspace.
26
+ *
27
+ * Workspace-first by §27.10: a lone app inside a workspace costs almost
28
+ * nothing, and growing to several apps later is then friction-free.
29
+ */
30
+ function planCreate(options) {
31
+ const workspaceName = (0, naming_js_1.deriveNames)(options.name).kebab;
32
+ const appName = (0, naming_js_1.deriveNames)(options.firstApp ?? 'api').kebab;
33
+ const app = {
34
+ name: appName,
35
+ preset: options.preset ?? 'api',
36
+ port: manifest_js_1.DEFAULT_PORT,
37
+ features: [...(options.features ?? [])],
38
+ };
39
+ const manifest = {
40
+ ...(0, manifest_js_1.createManifest)({
41
+ name: workspaceName,
42
+ engine: options.engine ?? 'postgres',
43
+ frameworkVersion: options.frameworkVersion ?? exports.DEFAULT_FRAMEWORK_VERSION,
44
+ }),
45
+ apps: [app],
46
+ };
47
+ // The app's build project joins the root solution config, so `tsc -b` at the
48
+ // root covers everything. The reference is added to the template's own
49
+ // tsconfig rather than to a second copy of it, so there is exactly one
50
+ // planned version of the file.
51
+ const workspace = (0, workspace_template_js_1.workspaceFiles)({
52
+ manifest,
53
+ packageManager: options.packageManager ?? 'pnpm',
54
+ }).map((file) => file.path === 'tsconfig.json'
55
+ ? { ...file, contents: (0, wiring_js_1.addProjectReference)(file.contents, `apps/${appName}`) }
56
+ : file);
57
+ const tree = new file_tree_js_1.FileTree().addAll(workspace).addAll((0, app_template_js_1.appFiles)({ app, manifest }));
58
+ return {
59
+ tree,
60
+ manifest,
61
+ notes: [
62
+ `Created workspace "${workspaceName}" with app "${appName}".`,
63
+ '',
64
+ 'Next:',
65
+ ` cd ${workspaceName}`,
66
+ ' pnpm install',
67
+ ' cp .env.example .env',
68
+ ' pnpm dev',
69
+ ],
70
+ };
71
+ }
72
+ /** Plan an additional app inside an existing workspace. */
73
+ async function planNewApp(options) {
74
+ const name = (0, naming_js_1.deriveNames)(options.name).kebab;
75
+ if (options.manifest.apps.some((app) => app.name === name)) {
76
+ throw new core_1.ConfigurationError({
77
+ detail: `This workspace already has an app named "${name}"`,
78
+ meta: { app: name },
79
+ });
80
+ }
81
+ const port = options.port ?? (0, manifest_js_1.nextAvailablePort)(options.manifest);
82
+ const clash = options.manifest.apps.find((app) => app.port === port);
83
+ if (clash !== undefined) {
84
+ // Two apps on one port is a failure that only shows up when both are
85
+ // started, which is the worst time to find it.
86
+ throw new core_1.ConfigurationError({
87
+ detail: `Port ${String(port)} is already used by "${clash.name}"`,
88
+ meta: { port, app: clash.name },
89
+ });
90
+ }
91
+ const app = {
92
+ name,
93
+ preset: options.preset ?? 'api',
94
+ port,
95
+ features: [...(options.features ?? [])],
96
+ };
97
+ const manifest = {
98
+ ...options.manifest,
99
+ apps: [...options.manifest.apps, app],
100
+ };
101
+ const tree = new file_tree_js_1.FileTree().addAll((0, app_template_js_1.appFiles)({ app, manifest }));
102
+ tree.add({
103
+ path: 'nage.workspace.json',
104
+ contents: (0, manifest_js_1.serialiseManifest)(manifest),
105
+ onConflict: 'overwrite',
106
+ });
107
+ const rootTsconfig = await (0, promises_1.readFile)((0, node_path_1.join)(options.root, 'tsconfig.json'), 'utf8');
108
+ tree.add({
109
+ path: 'tsconfig.json',
110
+ contents: (0, wiring_js_1.addProjectReference)(rootTsconfig, `apps/${name}`),
111
+ onConflict: 'overwrite',
112
+ });
113
+ return {
114
+ tree,
115
+ manifest,
116
+ notes: [
117
+ `Added app "${name}" (${app.preset}) on port ${String(port)}.`,
118
+ '',
119
+ 'Next:',
120
+ ' pnpm install',
121
+ ` pnpm --filter ./apps/${name} dev`,
122
+ ],
123
+ };
124
+ }
125
+ /** Plan a shared local package (§9.1: the team's own code, not the framework's). */
126
+ async function planNewPackage(options) {
127
+ const name = (0, naming_js_1.deriveNames)(options.name).kebab;
128
+ const alias = options.alias ?? `@app/${name}`;
129
+ if (options.manifest.packages.some((entry) => entry.name === name)) {
130
+ throw new core_1.ConfigurationError({
131
+ detail: `This workspace already has a package named "${name}"`,
132
+ meta: { package: name },
133
+ });
134
+ }
135
+ const manifest = {
136
+ ...options.manifest,
137
+ packages: [...options.manifest.packages, { name, alias }],
138
+ };
139
+ const files = [
140
+ {
141
+ path: `packages/${name}/package.json`,
142
+ contents: (0, workspace_template_js_1.json)({
143
+ name: alias,
144
+ version: '0.0.0',
145
+ private: true,
146
+ type: 'commonjs',
147
+ main: 'dist/index.js',
148
+ types: 'dist/index.d.ts',
149
+ scripts: {
150
+ build: 'tsc -b tsconfig.build.json',
151
+ typecheck: 'tsc -p tsconfig.json --noEmit',
152
+ test: 'vitest run',
153
+ },
154
+ }),
155
+ },
156
+ {
157
+ path: `packages/${name}/tsconfig.json`,
158
+ contents: (0, workspace_template_js_1.json)({
159
+ extends: '../../tsconfig.base.json',
160
+ compilerOptions: { noEmit: true, types: ['node'] },
161
+ include: ['src/**/*.ts', 'test/**/*.ts'],
162
+ }),
163
+ },
164
+ {
165
+ path: `packages/${name}/tsconfig.build.json`,
166
+ contents: (0, workspace_template_js_1.json)({
167
+ extends: '../../tsconfig.base.json',
168
+ compilerOptions: { composite: true, rootDir: 'src', outDir: 'dist', types: ['node'] },
169
+ include: ['src/**/*.ts'],
170
+ }),
171
+ },
172
+ {
173
+ path: `packages/${name}/src/index.ts`,
174
+ contents: [
175
+ `/** Shared code for this workspace, imported as \`${alias}\`. */`,
176
+ '',
177
+ 'export {};',
178
+ '',
179
+ ].join('\n'),
180
+ },
181
+ ];
182
+ const tree = new file_tree_js_1.FileTree().addAll(files);
183
+ tree.add({
184
+ path: 'nage.workspace.json',
185
+ contents: (0, manifest_js_1.serialiseManifest)(manifest),
186
+ onConflict: 'overwrite',
187
+ });
188
+ // A shared package is only usable once the alias resolves and pnpm knows
189
+ // about the directory, so both are updated in the same transaction.
190
+ const tsconfigBase = await (0, promises_1.readFile)((0, node_path_1.join)(options.root, 'tsconfig.base.json'), 'utf8');
191
+ tree.add({
192
+ path: 'tsconfig.base.json',
193
+ contents: (0, wiring_js_1.addPathAlias)(tsconfigBase, alias, `./packages/${name}/src/index.ts`),
194
+ onConflict: 'overwrite',
195
+ });
196
+ const pnpmWorkspace = await (0, promises_1.readFile)((0, node_path_1.join)(options.root, 'pnpm-workspace.yaml'), 'utf8');
197
+ tree.add({
198
+ path: 'pnpm-workspace.yaml',
199
+ contents: (0, wiring_js_1.addWorkspaceGlob)(pnpmWorkspace, 'packages/*'),
200
+ onConflict: 'overwrite',
201
+ });
202
+ const rootTsconfig = await (0, promises_1.readFile)((0, node_path_1.join)(options.root, 'tsconfig.json'), 'utf8');
203
+ tree.add({
204
+ path: 'tsconfig.json',
205
+ contents: (0, wiring_js_1.addProjectReference)(rootTsconfig, `packages/${name}`),
206
+ onConflict: 'overwrite',
207
+ });
208
+ return {
209
+ tree,
210
+ manifest,
211
+ notes: [
212
+ `Added package "${name}" (${alias}).`,
213
+ '',
214
+ 'Import it as:',
215
+ ` import { … } from '${alias}';`,
216
+ ],
217
+ };
218
+ }
219
+ //# sourceMappingURL=create.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `nage doctor` (PLAN.md §10.1, §12).
3
+ *
4
+ * Two kinds of check, one report:
5
+ * - **workspace integrity** — duplicate app names or ports, orphan packages,
6
+ * apps missing from disk, version skew across apps;
7
+ * - **security** — delegated to `auditSecurity` in `@nage-api/core`, so the
8
+ * checks `doctor` reports and the ones that block a production boot are the
9
+ * same code rather than two lists that drift.
10
+ */
11
+ import type { SecurityFinding } from '@nage-api/contracts';
12
+ import type { WorkspaceManifest } from '../workspace/manifest.js';
13
+ export interface DoctorOptions {
14
+ readonly root: string;
15
+ readonly manifest: WorkspaceManifest;
16
+ /** Limit to one app. */
17
+ readonly app?: string;
18
+ /**
19
+ * Values used only to judge secret strength; never echoed.
20
+ *
21
+ * Defaults to the workspace's own `.env` — deliberately **not**
22
+ * `process.env`. The shell a developer runs `doctor` in carries CI tokens and
23
+ * unrelated variables whose names match any secret heuristic, and a report
24
+ * full of findings about the machine rather than the project is a report
25
+ * nobody reads.
26
+ */
27
+ readonly env?: Record<string, string | undefined>;
28
+ /** Also scan for the legacy insecure patterns (§23.3). */
29
+ readonly legacy?: boolean;
30
+ }
31
+ export interface DoctorReport {
32
+ readonly findings: readonly SecurityFinding[];
33
+ /** True when nothing critical or high was found. */
34
+ readonly healthy: boolean;
35
+ }
36
+ export declare function runDoctor(options: DoctorOptions): Promise<DoctorReport>;
37
+ /**
38
+ * Read the workspace's `.env`, if it has one.
39
+ *
40
+ * Just enough of the format to judge secret strength: `KEY=value`, `export`
41
+ * prefixes, `#` comments and one level of quoting. Nothing is interpolated and
42
+ * nothing is exported — the values are read, weighed and dropped.
43
+ */
44
+ export declare function readDotEnv(root: string): Promise<Record<string, string>>;
45
+ /** Duplicate names and ports — the two failures that only appear at run time. */
46
+ export declare function checkWorkspaceIntegrity(manifest: WorkspaceManifest): SecurityFinding[];
47
+ //# sourceMappingURL=doctor.d.ts.map