@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.
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * `nage.workspace.json` — the source of truth for the workspace (PLAN.md §10.4).
4
+ *
5
+ * Successor to the legacy single-file `.nac-metadata.json`, extended for many
6
+ * apps. `new app` / `remove app` / `add` / `list` / `doctor` all read and write
7
+ * it, and it is serialised deterministically so a regenerated manifest produces
8
+ * an empty diff rather than reordered noise.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.DEFAULT_PORT = exports.MANIFEST_FILE = void 0;
12
+ exports.createManifest = createManifest;
13
+ exports.serialiseManifest = serialiseManifest;
14
+ exports.parseManifest = parseManifest;
15
+ exports.findWorkspaceRoot = findWorkspaceRoot;
16
+ exports.loadWorkspace = loadWorkspace;
17
+ exports.resolveTargetApp = resolveTargetApp;
18
+ exports.nextAvailablePort = nextAvailablePort;
19
+ const promises_1 = require("node:fs/promises");
20
+ const node_fs_1 = require("node:fs");
21
+ const node_path_1 = require("node:path");
22
+ const core_1 = require("@nage-api/core");
23
+ exports.MANIFEST_FILE = 'nage.workspace.json';
24
+ exports.DEFAULT_PORT = 3000;
25
+ function createManifest(input) {
26
+ return {
27
+ version: 1,
28
+ name: input.name,
29
+ engine: input.engine,
30
+ frameworkVersion: input.frameworkVersion,
31
+ apps: [],
32
+ packages: [],
33
+ };
34
+ }
35
+ /** Serialise deterministically: sorted collections, stable key order, trailing newline. */
36
+ function serialiseManifest(manifest) {
37
+ const ordered = {
38
+ version: manifest.version,
39
+ name: manifest.name,
40
+ engine: manifest.engine,
41
+ frameworkVersion: manifest.frameworkVersion,
42
+ apps: [...manifest.apps]
43
+ .sort((left, right) => left.name.localeCompare(right.name))
44
+ .map((app) => ({
45
+ name: app.name,
46
+ preset: app.preset,
47
+ port: app.port,
48
+ features: [...app.features].sort(),
49
+ })),
50
+ packages: [...manifest.packages]
51
+ .sort((left, right) => left.name.localeCompare(right.name))
52
+ .map((entry) => ({ name: entry.name, alias: entry.alias })),
53
+ };
54
+ return `${JSON.stringify(ordered, null, 2)}\n`;
55
+ }
56
+ /** Parse and validate a manifest, naming what is wrong rather than throwing a cast error. */
57
+ function parseManifest(contents, location = exports.MANIFEST_FILE) {
58
+ let raw;
59
+ try {
60
+ raw = JSON.parse(contents);
61
+ }
62
+ catch (error) {
63
+ throw new core_1.ConfigurationError({
64
+ detail: `${location} is not valid JSON`,
65
+ cause: error,
66
+ meta: { location },
67
+ });
68
+ }
69
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
70
+ throw new core_1.ConfigurationError({ detail: `${location} must contain an object` });
71
+ }
72
+ const candidate = raw;
73
+ const name = requireString(candidate.name, 'name', location);
74
+ const engine = requireString(candidate.engine, 'engine', location);
75
+ const frameworkVersion = requireString(candidate.frameworkVersion, 'frameworkVersion', location);
76
+ return {
77
+ version: 1,
78
+ name,
79
+ engine: engine,
80
+ frameworkVersion,
81
+ apps: [...(candidate.apps ?? [])],
82
+ packages: [...(candidate.packages ?? [])],
83
+ };
84
+ }
85
+ function requireString(value, field, location) {
86
+ if (typeof value !== 'string') {
87
+ throw new core_1.ConfigurationError({
88
+ detail: `${location} is missing "${field}"`,
89
+ meta: { location, field },
90
+ });
91
+ }
92
+ return value;
93
+ }
94
+ /**
95
+ * Walk up from `start` looking for a workspace root.
96
+ *
97
+ * Commands can then be run from anywhere inside the workspace, which is how
98
+ * `--app` stays optional: the current directory usually says which app is meant.
99
+ */
100
+ function findWorkspaceRoot(start) {
101
+ let current = (0, node_path_1.resolve)(start);
102
+ for (;;) {
103
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(current, exports.MANIFEST_FILE)))
104
+ return current;
105
+ const parent = (0, node_path_1.dirname)(current);
106
+ if (parent === current)
107
+ return undefined;
108
+ current = parent;
109
+ }
110
+ }
111
+ /** Load the manifest for the workspace containing `start`. */
112
+ async function loadWorkspace(start) {
113
+ const root = findWorkspaceRoot(start);
114
+ if (root === undefined) {
115
+ throw new core_1.ConfigurationError({
116
+ detail: `No ${exports.MANIFEST_FILE} found in this directory or any parent`,
117
+ meta: { start, hint: 'Run this inside a workspace, or create one with `nage create`.' },
118
+ });
119
+ }
120
+ const contents = await (0, promises_1.readFile)((0, node_path_1.join)(root, exports.MANIFEST_FILE), 'utf8');
121
+ return { root, manifest: parseManifest(contents) };
122
+ }
123
+ /** Which app a command targets: `--app`, or the one the cwd sits inside. */
124
+ function resolveTargetApp(manifest, options) {
125
+ if (options.app !== undefined) {
126
+ const named = manifest.apps.find((app) => app.name === options.app);
127
+ if (named === undefined) {
128
+ throw new core_1.ConfigurationError({
129
+ detail: `This workspace has no app named "${options.app}"`,
130
+ meta: { app: options.app, available: manifest.apps.map((app) => app.name) },
131
+ });
132
+ }
133
+ return named;
134
+ }
135
+ const relativeCwd = (0, node_path_1.resolve)(options.cwd)
136
+ .slice((0, node_path_1.resolve)(options.root).length + 1)
137
+ .split('\\')
138
+ .join('/');
139
+ const segments = relativeCwd.split('/');
140
+ if (segments[0] === 'apps' && segments[1] !== undefined) {
141
+ const inferred = manifest.apps.find((app) => app.name === segments[1]);
142
+ if (inferred !== undefined)
143
+ return inferred;
144
+ }
145
+ if (manifest.apps.length === 1 && manifest.apps[0] !== undefined)
146
+ return manifest.apps[0];
147
+ throw new core_1.ConfigurationError({
148
+ detail: manifest.apps.length === 0
149
+ ? 'This workspace has no apps yet; add one with `nage new app <name>`'
150
+ : 'Several apps exist and the current directory is not inside one — pass --app <name>',
151
+ meta: { available: manifest.apps.map((app) => app.name) },
152
+ });
153
+ }
154
+ /** Next free port, so two apps never default to the same one. */
155
+ function nextAvailablePort(manifest) {
156
+ const used = new Set(manifest.apps.map((app) => app.port));
157
+ let port = exports.DEFAULT_PORT;
158
+ while (used.has(port))
159
+ port += 1;
160
+ return port;
161
+ }
162
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Keeping the workspace's own files in step (PLAN.md §10.5).
3
+ *
4
+ * Adding an app or a shared package touches four files besides the new
5
+ * directory: `nage.workspace.json`, `pnpm-workspace.yaml`, `turbo.json` and
6
+ * `tsconfig.base.json` path aliases. These functions produce the updated
7
+ * contents; the caller writes them through `commitFileTree`, so either all five
8
+ * changes land or none do.
9
+ *
10
+ * Each is idempotent — running `new app` twice for the same name adds nothing
11
+ * the second time, which is what makes a failed-then-retried command safe.
12
+ */
13
+ /** Add a glob to `pnpm-workspace.yaml`'s `packages:` list if it is absent. */
14
+ export declare function addWorkspaceGlob(contents: string, glob: string): string;
15
+ /** Add a path alias to `tsconfig.base.json`, so `@app/domain` resolves. */
16
+ export declare function addPathAlias(contents: string, alias: string, target: string): string;
17
+ /** Remove an alias and its wildcard — the reverse, for `remove app`. */
18
+ export declare function removePathAlias(contents: string, alias: string): string;
19
+ /**
20
+ * Register a project reference in the root solution tsconfig, keeping the list
21
+ * sorted so two `new app` runs in either order produce the same file.
22
+ */
23
+ export declare function addProjectReference(contents: string, path: string): string;
24
+ export declare function removeProjectReference(contents: string, path: string): string;
25
+ /**
26
+ * Declare a turbo task input for an app.
27
+ *
28
+ * Turbo discovers packages from the pnpm workspace, so nothing is required per
29
+ * app; this only records app-specific outputs (a Dockerfile build context, say)
30
+ * when a preset needs them.
31
+ */
32
+ export declare function addTurboTaskOutput(contents: string, task: string, output: string): string;
33
+ //# sourceMappingURL=wiring.d.ts.map
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ /**
3
+ * Keeping the workspace's own files in step (PLAN.md §10.5).
4
+ *
5
+ * Adding an app or a shared package touches four files besides the new
6
+ * directory: `nage.workspace.json`, `pnpm-workspace.yaml`, `turbo.json` and
7
+ * `tsconfig.base.json` path aliases. These functions produce the updated
8
+ * contents; the caller writes them through `commitFileTree`, so either all five
9
+ * changes land or none do.
10
+ *
11
+ * Each is idempotent — running `new app` twice for the same name adds nothing
12
+ * the second time, which is what makes a failed-then-retried command safe.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.addWorkspaceGlob = addWorkspaceGlob;
16
+ exports.addPathAlias = addPathAlias;
17
+ exports.removePathAlias = removePathAlias;
18
+ exports.addProjectReference = addProjectReference;
19
+ exports.removeProjectReference = removeProjectReference;
20
+ exports.addTurboTaskOutput = addTurboTaskOutput;
21
+ const core_1 = require("@nage-api/core");
22
+ /** Add a glob to `pnpm-workspace.yaml`'s `packages:` list if it is absent. */
23
+ function addWorkspaceGlob(contents, glob) {
24
+ const lines = contents.split('\n');
25
+ const packagesIndex = lines.findIndex((line) => /^packages:\s*$/.test(line));
26
+ if (packagesIndex === -1) {
27
+ throw new core_1.ConfigurationError({
28
+ detail: 'pnpm-workspace.yaml has no `packages:` list to extend',
29
+ meta: { glob },
30
+ });
31
+ }
32
+ let end = packagesIndex + 1;
33
+ while (end < lines.length && /^\s*-\s/.test(lines[end] ?? ''))
34
+ end += 1;
35
+ const entries = lines.slice(packagesIndex + 1, end);
36
+ const quoted = ` - '${glob}'`;
37
+ if (entries.some((entry) => entry.trim() === `- '${glob}'` || entry.trim() === `- "${glob}"`)) {
38
+ return contents;
39
+ }
40
+ return [...lines.slice(0, end), quoted, ...lines.slice(end)].join('\n');
41
+ }
42
+ /** Add a path alias to `tsconfig.base.json`, so `@app/domain` resolves. */
43
+ function addPathAlias(contents, alias, target) {
44
+ const config = parseJson(contents, 'tsconfig.base.json');
45
+ const compilerOptions = (config['compilerOptions'] ?? {});
46
+ const paths = { ...(compilerOptions['paths'] ?? {}) };
47
+ paths[alias] = [target];
48
+ paths[`${alias}/*`] = [`${target.replace(/\/index\.ts$/, '')}/*`];
49
+ const ordered = Object.fromEntries(Object.entries(paths).sort(([left], [right]) => left.localeCompare(right)));
50
+ return `${JSON.stringify({ ...config, compilerOptions: { ...compilerOptions, paths: ordered } }, null, 2)}\n`;
51
+ }
52
+ /** Remove an alias and its wildcard — the reverse, for `remove app`. */
53
+ function removePathAlias(contents, alias) {
54
+ const config = parseJson(contents, 'tsconfig.base.json');
55
+ const compilerOptions = (config['compilerOptions'] ?? {});
56
+ const removed = new Set([alias, `${alias}/*`]);
57
+ const paths = Object.fromEntries(Object.entries((compilerOptions['paths'] ?? {})).filter(([key]) => !removed.has(key)));
58
+ return `${JSON.stringify({ ...config, compilerOptions: { ...compilerOptions, paths } }, null, 2)}\n`;
59
+ }
60
+ /**
61
+ * Register a project reference in the root solution tsconfig, keeping the list
62
+ * sorted so two `new app` runs in either order produce the same file.
63
+ */
64
+ function addProjectReference(contents, path) {
65
+ const config = parseJson(contents, 'tsconfig.json');
66
+ const references = [...(config['references'] ?? [])];
67
+ if (references.some((reference) => reference.path === path))
68
+ return contents;
69
+ references.push({ path });
70
+ references.sort((left, right) => left.path.localeCompare(right.path));
71
+ return `${JSON.stringify({ ...config, references }, null, 2)}\n`;
72
+ }
73
+ function removeProjectReference(contents, path) {
74
+ const config = parseJson(contents, 'tsconfig.json');
75
+ const references = (config['references'] ?? []).filter((reference) => reference.path !== path);
76
+ return `${JSON.stringify({ ...config, references }, null, 2)}\n`;
77
+ }
78
+ /**
79
+ * Declare a turbo task input for an app.
80
+ *
81
+ * Turbo discovers packages from the pnpm workspace, so nothing is required per
82
+ * app; this only records app-specific outputs (a Dockerfile build context, say)
83
+ * when a preset needs them.
84
+ */
85
+ function addTurboTaskOutput(contents, task, output) {
86
+ const config = parseJson(contents, 'turbo.json');
87
+ const tasks = { ...(config['tasks'] ?? {}) };
88
+ const existing = { ...(tasks[task] ?? {}) };
89
+ const outputs = [...(existing['outputs'] ?? [])];
90
+ if (!outputs.includes(output))
91
+ outputs.push(output);
92
+ outputs.sort();
93
+ tasks[task] = { ...existing, outputs };
94
+ return `${JSON.stringify({ ...config, tasks }, null, 2)}\n`;
95
+ }
96
+ function parseJson(contents, location) {
97
+ try {
98
+ const parsed = JSON.parse(contents);
99
+ if (typeof parsed !== 'object' || parsed === null) {
100
+ throw new Error('not an object');
101
+ }
102
+ return parsed;
103
+ }
104
+ catch (error) {
105
+ throw new core_1.ConfigurationError({
106
+ detail: `${location} could not be parsed; the workspace may have been edited by hand`,
107
+ cause: error,
108
+ meta: { location },
109
+ });
110
+ }
111
+ }
112
+ //# sourceMappingURL=wiring.js.map
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@nage-api/cli",
3
+ "version": "1.0.0-beta.2",
4
+ "description": "The nage CLI — scaffold and manage a @nage-api workspace, its apps, packages and resources",
5
+ "license": "Apache-2.0",
6
+ "type": "commonjs",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "bin": {
11
+ "nage": "./dist/main.js"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "!dist/.tsbuildinfo",
23
+ "!dist/**/*.map",
24
+ "README.md"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "@nage-api/contracts": "1.0.0-beta.2",
31
+ "@nage-api/core": "1.0.0-beta.2"
32
+ },
33
+ "devDependencies": {
34
+ "@swc/core": "1.15.47",
35
+ "@types/node": "22.20.1",
36
+ "@vitest/coverage-v8": "4.1.10",
37
+ "rimraf": "6.1.3",
38
+ "typescript": "5.9.3",
39
+ "unplugin-swc": "1.5.11",
40
+ "vitest": "4.1.10"
41
+ },
42
+ "engines": {
43
+ "node": ">=22.0.0"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc -b tsconfig.build.json",
47
+ "clean": "rimraf dist .turbo",
48
+ "typecheck": "tsc -p tsconfig.json --noEmit",
49
+ "test": "vitest run"
50
+ }
51
+ }