@lenne.tech/cli 1.18.0 → 1.20.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/bin/lt +6 -2
- package/build/commands/frontend/angular.js +110 -4
- package/build/commands/frontend/nuxt.js +184 -13
- package/build/commands/fullstack/add-api.js +360 -0
- package/build/commands/fullstack/add-app.js +284 -0
- package/build/commands/fullstack/init.js +46 -0
- package/build/commands/server/create.js +118 -7
- package/build/commands/status.js +42 -0
- package/build/commands/tools/ocr.js +171 -0
- package/build/lib/marker.js +218 -0
- package/build/lib/workspace-integration.js +351 -0
- package/docs/commands.md +150 -4
- package/package.json +2 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.getMarkerStatus = getMarkerStatus;
|
|
46
|
+
exports.installMarker = installMarker;
|
|
47
|
+
exports.resolveDevice = resolveDevice;
|
|
48
|
+
exports.runMarker = runMarker;
|
|
49
|
+
/**
|
|
50
|
+
* marker-pdf integration for the lt CLI.
|
|
51
|
+
*
|
|
52
|
+
* Marker (https://github.com/datalab-to/marker) is a PyTorch-based
|
|
53
|
+
* PDF → Markdown converter with first-class layout, table and equation
|
|
54
|
+
* support. On Apple Silicon it leverages Metal Performance Shaders
|
|
55
|
+
* (MPS) for GPU-accelerated inference.
|
|
56
|
+
*
|
|
57
|
+
* The CLI keeps marker in an isolated Python virtualenv under
|
|
58
|
+
* `~/.lt/marker/.venv/` so that:
|
|
59
|
+
* - we do not pollute the user's global Python environment
|
|
60
|
+
* - the ~3 GB of model weights are downloaded only once
|
|
61
|
+
* - subsequent runs start instantly (cached models)
|
|
62
|
+
*/
|
|
63
|
+
const child_process_1 = require("child_process");
|
|
64
|
+
const fs_1 = require("fs");
|
|
65
|
+
const promises_1 = require("fs/promises");
|
|
66
|
+
const os_1 = require("os");
|
|
67
|
+
const path_1 = require("path");
|
|
68
|
+
const util_1 = require("util");
|
|
69
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
70
|
+
const MARKER_HOME = (0, path_1.join)((0, os_1.homedir)(), '.lt', 'marker');
|
|
71
|
+
const VENV_DIR = (0, path_1.join)(MARKER_HOME, '.venv');
|
|
72
|
+
const VENV_BIN = (0, path_1.join)(VENV_DIR, 'bin');
|
|
73
|
+
const VENV_PYTHON = (0, path_1.join)(VENV_BIN, 'python3');
|
|
74
|
+
const VENV_MARKER_SINGLE = (0, path_1.join)(VENV_BIN, 'marker_single');
|
|
75
|
+
const VENV_MARKER_BATCH = (0, path_1.join)(VENV_BIN, 'marker');
|
|
76
|
+
/**
|
|
77
|
+
* Detect tool availability.
|
|
78
|
+
*/
|
|
79
|
+
function getMarkerStatus() {
|
|
80
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
const status = {
|
|
82
|
+
installed: false,
|
|
83
|
+
pythonAvailable: false,
|
|
84
|
+
uvAvailable: false,
|
|
85
|
+
venvPath: VENV_DIR,
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
yield execAsync('python3 --version');
|
|
89
|
+
status.pythonAvailable = true;
|
|
90
|
+
}
|
|
91
|
+
catch (_a) {
|
|
92
|
+
// python3 missing
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
yield execAsync('uv --version');
|
|
96
|
+
status.uvAvailable = true;
|
|
97
|
+
}
|
|
98
|
+
catch (_b) {
|
|
99
|
+
// uv missing — we'll fall back to python -m venv + pip
|
|
100
|
+
}
|
|
101
|
+
status.installed = (0, fs_1.existsSync)(VENV_MARKER_SINGLE) && (0, fs_1.existsSync)(VENV_MARKER_BATCH);
|
|
102
|
+
return status;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Install marker-pdf into ~/.lt/marker/.venv.
|
|
107
|
+
*
|
|
108
|
+
* Preferred path: `uv venv --python 3.12` + `uv pip install marker-pdf psutil`.
|
|
109
|
+
* Fallback: `python3 -m venv` + `pip install`.
|
|
110
|
+
*/
|
|
111
|
+
function installMarker() {
|
|
112
|
+
return __awaiter(this, arguments, void 0, function* (opts = {}) {
|
|
113
|
+
var _a;
|
|
114
|
+
const log = (_a = opts.onProgress) !== null && _a !== void 0 ? _a : (() => { });
|
|
115
|
+
const status = yield getMarkerStatus();
|
|
116
|
+
if (status.installed) {
|
|
117
|
+
log('marker already installed');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!status.pythonAvailable) {
|
|
121
|
+
throw new Error('python3 is required but not found in PATH. Install Python 3.10+ (e.g. via Homebrew: `brew install python@3.12`)');
|
|
122
|
+
}
|
|
123
|
+
yield (0, promises_1.mkdir)(MARKER_HOME, { recursive: true });
|
|
124
|
+
const useUv = status.uvAvailable;
|
|
125
|
+
// 1. Create virtualenv
|
|
126
|
+
if (useUv) {
|
|
127
|
+
log('Creating venv with uv (Python 3.12)…');
|
|
128
|
+
yield execAsync(`uv venv --python 3.12 "${VENV_DIR}"`, { cwd: MARKER_HOME });
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
log('Creating venv with python3 (uv not found, falling back)…');
|
|
132
|
+
yield execAsync(`python3 -m venv "${VENV_DIR}"`, { cwd: MARKER_HOME });
|
|
133
|
+
}
|
|
134
|
+
// 2. Install marker-pdf + psutil
|
|
135
|
+
// psutil is needed by the marker batch CLI; it is a soft dep on some
|
|
136
|
+
// marker-pdf releases, so we install it explicitly.
|
|
137
|
+
// We use shell quoting to handle macOS "Library" / spaces in paths.
|
|
138
|
+
const cmd = useUv
|
|
139
|
+
? `uv pip install --python "${VENV_PYTHON}" marker-pdf psutil`
|
|
140
|
+
: `"${VENV_BIN}/pip" install marker-pdf psutil`;
|
|
141
|
+
log('Installing marker-pdf + dependencies (~3 GB models will download on first run)…');
|
|
142
|
+
// Increase maxBuffer because pip output is large
|
|
143
|
+
yield execAsync(cmd, { cwd: MARKER_HOME, maxBuffer: 100 * 1024 * 1024 });
|
|
144
|
+
if (!(0, fs_1.existsSync)(VENV_MARKER_SINGLE)) {
|
|
145
|
+
throw new Error(`marker installation finished but ${VENV_MARKER_SINGLE} not found`);
|
|
146
|
+
}
|
|
147
|
+
log('marker installed successfully');
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Decide the correct TORCH_DEVICE for this machine.
|
|
152
|
+
*/
|
|
153
|
+
function resolveDevice(requested = 'auto') {
|
|
154
|
+
if (requested !== 'auto')
|
|
155
|
+
return requested;
|
|
156
|
+
if (process.platform === 'darwin' && process.arch === 'arm64')
|
|
157
|
+
return 'mps';
|
|
158
|
+
// We don't probe nvidia-smi here — let PyTorch decide CUDA at runtime
|
|
159
|
+
return 'cpu';
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Run marker on a single PDF or a directory of PDFs.
|
|
163
|
+
*/
|
|
164
|
+
function runMarker(inputPath, opts) {
|
|
165
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
166
|
+
var _a;
|
|
167
|
+
const status = yield getMarkerStatus();
|
|
168
|
+
if (!status.installed) {
|
|
169
|
+
throw new Error('marker is not installed. Run `lt tools ocr --install` first.');
|
|
170
|
+
}
|
|
171
|
+
const isDir = (0, fs_1.existsSync)(inputPath) && (yield Promise.resolve().then(() => __importStar(require('fs')))).statSync(inputPath).isDirectory();
|
|
172
|
+
const bin = isDir ? VENV_MARKER_BATCH : VENV_MARKER_SINGLE;
|
|
173
|
+
const args = [];
|
|
174
|
+
if (isDir) {
|
|
175
|
+
args.push(inputPath);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
args.push(inputPath);
|
|
179
|
+
}
|
|
180
|
+
args.push('--output_dir', opts.outputDir);
|
|
181
|
+
args.push('--output_format', (_a = opts.outputFormat) !== null && _a !== void 0 ? _a : 'markdown');
|
|
182
|
+
if (opts.disableImages)
|
|
183
|
+
args.push('--disable_image_extraction');
|
|
184
|
+
if (isDir) {
|
|
185
|
+
if (opts.skipExisting)
|
|
186
|
+
args.push('--skip_existing');
|
|
187
|
+
if (opts.workers && opts.workers > 0)
|
|
188
|
+
args.push('--workers', String(opts.workers));
|
|
189
|
+
}
|
|
190
|
+
const device = resolveDevice(opts.device);
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
var _a;
|
|
193
|
+
const proc = (0, child_process_1.spawn)(bin, args, {
|
|
194
|
+
env: Object.assign(Object.assign({}, process.env), { TORCH_DEVICE: device }),
|
|
195
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
196
|
+
});
|
|
197
|
+
const onLine = (_a = opts.onLine) !== null && _a !== void 0 ? _a : ((l) => process.stdout.write(`${l}\n`));
|
|
198
|
+
const handleStream = (stream) => {
|
|
199
|
+
let buf = '';
|
|
200
|
+
stream.on('data', (chunk) => {
|
|
201
|
+
var _a;
|
|
202
|
+
buf += chunk.toString();
|
|
203
|
+
const lines = buf.split(/\r?\n/);
|
|
204
|
+
buf = (_a = lines.pop()) !== null && _a !== void 0 ? _a : '';
|
|
205
|
+
for (const line of lines)
|
|
206
|
+
if (line)
|
|
207
|
+
onLine(line);
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
handleStream(proc.stdout);
|
|
211
|
+
handleStream(proc.stderr);
|
|
212
|
+
proc.on('close', (code) => {
|
|
213
|
+
resolve({ exitCode: code !== null && code !== void 0 ? code : 0 });
|
|
214
|
+
});
|
|
215
|
+
proc.on('error', reject);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Helpers for integrating an API (`projects/api/`) or an App
|
|
4
|
+
* (`projects/app/`) into a fullstack workspace. Used by `lt fullstack
|
|
5
|
+
* init` (full-workspace flow) and by `lt fullstack add-api` /
|
|
6
|
+
* `lt fullstack add-app` (incremental flow on an already-existing
|
|
7
|
+
* workspace that only ships one half of the stack).
|
|
8
|
+
*
|
|
9
|
+
* The functions here own the small amount of "monorepo glue" that sits
|
|
10
|
+
* between the framework-agnostic setup primitives in
|
|
11
|
+
* `extensions/server.ts` and `extensions/frontend-helper.ts`:
|
|
12
|
+
*
|
|
13
|
+
* - writing `projects/api/lt.config.json` with the resolved api/
|
|
14
|
+
* framework mode so that follow-up generators (lt server module
|
|
15
|
+
* etc.) pick it up without re-probing,
|
|
16
|
+
* - patching the frontend `.env` with the project-specific storage
|
|
17
|
+
* prefix,
|
|
18
|
+
* - running the experimental `bun run rename` step for the
|
|
19
|
+
* `--next` nest-base template,
|
|
20
|
+
* - running the post-install format pass on the touched sub-project,
|
|
21
|
+
*
|
|
22
|
+
* They deliberately do NOT manage workspace-level concerns
|
|
23
|
+
* (lt-monorepo clone, root CLAUDE.md patching, top-level pnpm install,
|
|
24
|
+
* git initialisation). Those stay in `commands/fullstack/init.ts`
|
|
25
|
+
* because they only happen once per workspace creation. add-api /
|
|
26
|
+
* add-app run on an already-installed workspace and only need to wire
|
|
27
|
+
* in the new sub-project plus run a workspace-wide install.
|
|
28
|
+
*/
|
|
29
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
30
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
31
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
32
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
33
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
34
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
35
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.detectSubProjectContext = detectSubProjectContext;
|
|
40
|
+
exports.detectWorkspaceLayout = detectWorkspaceLayout;
|
|
41
|
+
exports.findWorkspaceRoot = findWorkspaceRoot;
|
|
42
|
+
exports.isNonInteractive = isNonInteractive;
|
|
43
|
+
exports.runExperimentalNestBaseRename = runExperimentalNestBaseRename;
|
|
44
|
+
exports.runStandaloneWorkspaceGate = runStandaloneWorkspaceGate;
|
|
45
|
+
exports.shouldProceedAsStandalone = shouldProceedAsStandalone;
|
|
46
|
+
exports.writeApiConfig = writeApiConfig;
|
|
47
|
+
/**
|
|
48
|
+
* Detect whether the current working directory IS a sub-project of an
|
|
49
|
+
* lt-monorepo workspace (i.e. cwd is `projects/api/` or `projects/app/`,
|
|
50
|
+
* or any nested directory thereof). Returns the workspace root + the
|
|
51
|
+
* sub-project kind so the caller can give a precise hint like
|
|
52
|
+
* "you're inside projects/api — go up to the workspace root".
|
|
53
|
+
*
|
|
54
|
+
* Returns `null` when cwd is not inside such a sub-project.
|
|
55
|
+
*/
|
|
56
|
+
function detectSubProjectContext(startDir, filesystem) {
|
|
57
|
+
const root = findWorkspaceRoot(startDir, filesystem);
|
|
58
|
+
if (!root)
|
|
59
|
+
return null;
|
|
60
|
+
// If the start dir IS the root, we're not inside a sub-project.
|
|
61
|
+
if (root === startDir)
|
|
62
|
+
return null;
|
|
63
|
+
const apiDir = filesystem.path(root, 'projects', 'api');
|
|
64
|
+
const appDir = filesystem.path(root, 'projects', 'app');
|
|
65
|
+
// Resolve absolute prefixes for a clean startsWith compare. We use
|
|
66
|
+
// the gluegun-resolved paths because all callers go through it.
|
|
67
|
+
const startAbs = filesystem.path(startDir);
|
|
68
|
+
if (startAbs === apiDir || startAbs.startsWith(`${apiDir}/`)) {
|
|
69
|
+
return { kind: 'api', subProjectDir: apiDir, workspaceRoot: root };
|
|
70
|
+
}
|
|
71
|
+
if (startAbs === appDir || startAbs.startsWith(`${appDir}/`)) {
|
|
72
|
+
return { kind: 'app', subProjectDir: appDir, workspaceRoot: root };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Detect what's already present in a workspace directory. Used by the
|
|
78
|
+
* fullstack commands to decide whether to perform a full init, only
|
|
79
|
+
* add the missing sub-project, or refuse because both halves already
|
|
80
|
+
* exist.
|
|
81
|
+
*/
|
|
82
|
+
function detectWorkspaceLayout(workspaceDir, filesystem) {
|
|
83
|
+
const projectsDir = `${workspaceDir}/projects`;
|
|
84
|
+
const apiDir = `${projectsDir}/api`;
|
|
85
|
+
const appDir = `${projectsDir}/app`;
|
|
86
|
+
// `hasWorkspaceMarker` covers pnpm-workspace.yaml, npm/yarn
|
|
87
|
+
// `workspaces` in package.json, and the `projects/` directory
|
|
88
|
+
// convention. Mirrors what `findWorkspaceRoot` walks for.
|
|
89
|
+
const hasWorkspace = hasWorkspaceMarker(workspaceDir, filesystem);
|
|
90
|
+
// For "hasApi"/"hasApp", a directory existing is not enough — empty
|
|
91
|
+
// or stub directories from a partially-cloned monorepo would yield
|
|
92
|
+
// false positives. Require at least a package.json inside.
|
|
93
|
+
const hasApi = filesystem.exists(`${apiDir}/package.json`) === 'file';
|
|
94
|
+
const hasApp = filesystem.exists(`${appDir}/package.json`) === 'file';
|
|
95
|
+
return { hasApi, hasApp, hasWorkspace, workspaceDir };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Walk up from `startDir` until a workspace marker is found or the
|
|
99
|
+
* filesystem root is reached. Limited to 6 levels to avoid pathological
|
|
100
|
+
* scans on deeply-nested CWDs (e.g. inside a temp dir hierarchy).
|
|
101
|
+
*
|
|
102
|
+
* Returns the directory that contains the marker, or `null` if none
|
|
103
|
+
* was found within the search budget. Used by the standalone commands
|
|
104
|
+
* to detect the case "user is inside `projects/api/` and ran
|
|
105
|
+
* `lt frontend nuxt` there".
|
|
106
|
+
*/
|
|
107
|
+
function findWorkspaceRoot(startDir, filesystem, maxDepth = 6) {
|
|
108
|
+
// Resolve to an absolute path so the parent traversal is reliable.
|
|
109
|
+
// gluegun's filesystem.path is a join helper; we want the OS path.
|
|
110
|
+
let cur = startDir;
|
|
111
|
+
// First check the start dir itself.
|
|
112
|
+
if (hasWorkspaceMarker(cur, filesystem))
|
|
113
|
+
return cur;
|
|
114
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
115
|
+
const parent = filesystem.path(cur, '..');
|
|
116
|
+
if (parent === cur)
|
|
117
|
+
return null;
|
|
118
|
+
if (hasWorkspaceMarker(parent, filesystem))
|
|
119
|
+
return parent;
|
|
120
|
+
cur = parent;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Treat the caller as "non-interactive" (KI/CI) when either
|
|
126
|
+
* - `--noConfirm` was passed explicitly, OR
|
|
127
|
+
* - stdin is not a TTY (typical for `claude < script.txt`, piped CI
|
|
128
|
+
* runs, or any agent that captures the CLI's stdout).
|
|
129
|
+
*
|
|
130
|
+
* The TTY check catches AI agents that call `lt …` without
|
|
131
|
+
* `--noConfirm` (Claude Code does this) and would otherwise hit the
|
|
132
|
+
* `confirm()` prompt forever. Caller can opt out via `force`.
|
|
133
|
+
*
|
|
134
|
+
* Exposed so commands can derive their `noConfirm` value once and
|
|
135
|
+
* pass the same boolean to `shouldProceedAsStandalone`.
|
|
136
|
+
*/
|
|
137
|
+
function isNonInteractive(noConfirmFlag) {
|
|
138
|
+
if (noConfirmFlag)
|
|
139
|
+
return true;
|
|
140
|
+
// process.stdin may be undefined in some test runners — guard.
|
|
141
|
+
return Boolean(process.stdin && process.stdin.isTTY === false);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Run the experimental `bun run rename <projectDir>` step. Only relevant
|
|
145
|
+
* for the `--next` nest-base template (it ships hard-coded `nest-base`
|
|
146
|
+
* references in package.json, README.md, portless.yml, and
|
|
147
|
+
* docker-compose.yml). Failures are non-fatal — the workspace is still
|
|
148
|
+
* usable, and the user can re-run the rename script manually.
|
|
149
|
+
*
|
|
150
|
+
* Returns true if the step was attempted (regardless of success). The
|
|
151
|
+
* caller decides whether to surface a warning.
|
|
152
|
+
*/
|
|
153
|
+
function runExperimentalNestBaseRename(options) {
|
|
154
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
155
|
+
const { apiDir, patching, projectDir, system } = options;
|
|
156
|
+
// setupServerForFullstack already patched package.json to set
|
|
157
|
+
// `name = projectDir`. The rename planner reads that name as the
|
|
158
|
+
// "old" slug, which would short-circuit the rest of the rewrites
|
|
159
|
+
// because they still say `nest-base`. Restore the canonical
|
|
160
|
+
// `name = "nest-base"` first so the planner has a coherent starting
|
|
161
|
+
// state across all four files.
|
|
162
|
+
yield patching.update(`${apiDir}/package.json`, (config) => {
|
|
163
|
+
config.name = 'nest-base';
|
|
164
|
+
return config;
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
yield system.run(`cd ${apiDir} && bun run rename ${projectDir}`);
|
|
168
|
+
return { attempted: true };
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
return { attempted: true, error: err };
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Print + prompt + decision for a standalone scaffolding command's
|
|
177
|
+
* workspace gate. Centralises the ~25 lines of identical logic that
|
|
178
|
+
* `lt server create`, `lt frontend nuxt`, and `lt frontend angular`
|
|
179
|
+
* each had inline.
|
|
180
|
+
*
|
|
181
|
+
* Side effects (intentionally bundled — easier to reason about as one
|
|
182
|
+
* unit, and the three commands all want the same shape):
|
|
183
|
+
* - prints the workspace-detected note + sub-project hint
|
|
184
|
+
* - asks the user via `confirm()` when interactive
|
|
185
|
+
* - prints the refusal/abort reason via `print.error` when refused
|
|
186
|
+
* - calls `process.exit(1)` on refusal (unless `fromGluegunMenu`)
|
|
187
|
+
*
|
|
188
|
+
* Returns `true` when the caller may proceed, `false` when the caller
|
|
189
|
+
* has already been told to abort and should `return` from its `run`.
|
|
190
|
+
*
|
|
191
|
+
* The shape stays narrow on purpose — adding more knobs (e.g.
|
|
192
|
+
* "abort message format") would just push the duplication elsewhere.
|
|
193
|
+
*/
|
|
194
|
+
function runStandaloneWorkspaceGate(options) {
|
|
195
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
196
|
+
var _a;
|
|
197
|
+
const { cwd, filesystem, force, fromGluegunMenu, noConfirmFlag, pieceName, print: { confirm, error, info }, projectKind, suggestion, } = options;
|
|
198
|
+
// Sub-project hint: if the user is inside `projects/api/` or
|
|
199
|
+
// `projects/app/` of a workspace, point them at the root explicitly
|
|
200
|
+
// — the standalone path would otherwise clone a sibling tree
|
|
201
|
+
// *inside* the existing sub-project, which is almost never wanted.
|
|
202
|
+
const subProject = detectSubProjectContext(cwd, filesystem);
|
|
203
|
+
if (subProject) {
|
|
204
|
+
info('');
|
|
205
|
+
info(`You appear to be inside projects/${subProject.kind}/ of a workspace at ${subProject.workspaceRoot}.`);
|
|
206
|
+
info(` → Run \`${suggestion}\` from the workspace root instead.`);
|
|
207
|
+
error(`Refusing to create a standalone ${projectKind} from inside a sub-project. cd to the workspace root first.`);
|
|
208
|
+
if (!fromGluegunMenu)
|
|
209
|
+
process.exit(1);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const layout = detectWorkspaceLayout(cwd, filesystem);
|
|
213
|
+
if (!layout.hasWorkspace) {
|
|
214
|
+
return true; // Plain dir → standalone is fine, no gate.
|
|
215
|
+
}
|
|
216
|
+
const alreadyHas = pieceName === 'api' ? layout.hasApi : layout.hasApp;
|
|
217
|
+
info('');
|
|
218
|
+
info('Note: current directory looks like a fullstack workspace (pnpm-workspace.yaml, package.json#workspaces, or projects/).');
|
|
219
|
+
if (!alreadyHas) {
|
|
220
|
+
info(` → To integrate the ${pieceName} into this workspace, use \`${suggestion}\`.`);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
info(` → projects/${pieceName}/ already exists in this workspace; this command would create a separate ${projectKind}.`);
|
|
224
|
+
}
|
|
225
|
+
const nonInteractive = isNonInteractive(noConfirmFlag);
|
|
226
|
+
const userConfirmed = nonInteractive
|
|
227
|
+
? undefined
|
|
228
|
+
: yield confirm(`Proceed with standalone ${projectKind} creation anyway?`, false);
|
|
229
|
+
const decision = shouldProceedAsStandalone({
|
|
230
|
+
force,
|
|
231
|
+
nonInteractive,
|
|
232
|
+
projectKind,
|
|
233
|
+
suggestion,
|
|
234
|
+
userConfirmed,
|
|
235
|
+
});
|
|
236
|
+
if (!decision.proceed) {
|
|
237
|
+
error((_a = decision.reason) !== null && _a !== void 0 ? _a : 'Aborted.');
|
|
238
|
+
if (!fromGluegunMenu)
|
|
239
|
+
process.exit(1);
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
if (nonInteractive && force) {
|
|
243
|
+
info(' --force set — continuing despite workspace context.');
|
|
244
|
+
}
|
|
245
|
+
info('');
|
|
246
|
+
return true;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Decide whether a standalone scaffolding command (`lt server create`,
|
|
251
|
+
* `lt frontend nuxt`, `lt frontend angular`) should run inside a
|
|
252
|
+
* directory that already looks like a fullstack workspace.
|
|
253
|
+
*
|
|
254
|
+
* Three modes:
|
|
255
|
+
*
|
|
256
|
+
* - **interactive** (the user can answer a prompt) — caller asks via
|
|
257
|
+
* `confirm()` and passes the result as `userConfirmed`.
|
|
258
|
+
* - **non-interactive without force** — refuse. This is the path AI
|
|
259
|
+
* agents and CI scripts take by default (either `--noConfirm` was
|
|
260
|
+
* set, or stdin isn't a TTY). Forcing them onto the workspace-
|
|
261
|
+
* aware command (`add-api` / `add-app`) prevents stray side-by-
|
|
262
|
+
* side clones that pnpm-workspace.yaml does not pick up.
|
|
263
|
+
* - **non-interactive with --force** — proceed, but the caller
|
|
264
|
+
* should log a hint so the override is visible in CI logs.
|
|
265
|
+
*
|
|
266
|
+
* The function does NOT print or prompt. It only decides; the caller
|
|
267
|
+
* owns the interaction surface.
|
|
268
|
+
*/
|
|
269
|
+
function shouldProceedAsStandalone(options) {
|
|
270
|
+
const { force, nonInteractive, projectKind, suggestion, userConfirmed } = options;
|
|
271
|
+
// Interactive caller already gave an explicit yes/no.
|
|
272
|
+
if (userConfirmed !== undefined) {
|
|
273
|
+
return userConfirmed
|
|
274
|
+
? { proceed: true }
|
|
275
|
+
: { proceed: false, reason: `Aborted standalone ${projectKind} creation. Use \`${suggestion}\` instead.` };
|
|
276
|
+
}
|
|
277
|
+
// Non-interactive path: refuse unless --force is set. This is the
|
|
278
|
+
// AI-agent / CI default — fail loud rather than silently produce a
|
|
279
|
+
// stray clone that does not integrate with the workspace.
|
|
280
|
+
if (nonInteractive && !force) {
|
|
281
|
+
return {
|
|
282
|
+
proceed: false,
|
|
283
|
+
reason: `Refusing to create a standalone ${projectKind} inside an existing fullstack workspace ` +
|
|
284
|
+
`(non-interactive caller detected). ` +
|
|
285
|
+
`Use \`${suggestion}\` for the workspace-aware flow, or pass --force to override (rare).`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// Non-interactive + force: caller knows what they want.
|
|
289
|
+
return { proceed: true };
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Write `projects/api/lt.config.json` with the apiMode and frameworkMode
|
|
293
|
+
* baked in so follow-up generators (lt server module, addProp,
|
|
294
|
+
* permissions) can pick the correct controller type and detect vendor
|
|
295
|
+
* mode without re-probing the file tree.
|
|
296
|
+
*
|
|
297
|
+
* Idempotent: overwrites whatever was at `lt.config.json` so a re-run
|
|
298
|
+
* after an apiMode change reflects the new value.
|
|
299
|
+
*/
|
|
300
|
+
function writeApiConfig(options) {
|
|
301
|
+
const { apiDir, apiMode, filesystem, frameworkMode } = options;
|
|
302
|
+
filesystem.write(filesystem.path(apiDir, 'lt.config.json'), {
|
|
303
|
+
commands: {
|
|
304
|
+
server: {
|
|
305
|
+
module: {
|
|
306
|
+
controller: apiMode,
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
meta: {
|
|
311
|
+
apiMode,
|
|
312
|
+
frameworkMode,
|
|
313
|
+
version: '1.0.0',
|
|
314
|
+
},
|
|
315
|
+
}, { jsonIndent: 2 });
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Probe a single directory for workspace markers. Pure helper used by
|
|
319
|
+
* `detectWorkspaceLayout` and `findWorkspaceRoot`.
|
|
320
|
+
*
|
|
321
|
+
* Recognised markers (any one is sufficient):
|
|
322
|
+
* - `pnpm-workspace.yaml` — pnpm workspace
|
|
323
|
+
* - `package.json` with `workspaces` field — npm/yarn/bun workspaces
|
|
324
|
+
* - `projects/` directory — lt-monorepo convention
|
|
325
|
+
*
|
|
326
|
+
* Returns false for `node_modules`-style directories that may contain
|
|
327
|
+
* a stray `package.json` with `workspaces`.
|
|
328
|
+
*/
|
|
329
|
+
function hasWorkspaceMarker(dir, filesystem) {
|
|
330
|
+
var _a;
|
|
331
|
+
if (filesystem.exists(`${dir}/pnpm-workspace.yaml`) === 'file')
|
|
332
|
+
return true;
|
|
333
|
+
if (filesystem.exists(`${dir}/projects`) === 'dir')
|
|
334
|
+
return true;
|
|
335
|
+
const pkgPath = `${dir}/package.json`;
|
|
336
|
+
if (filesystem.exists(pkgPath) !== 'file')
|
|
337
|
+
return false;
|
|
338
|
+
const pkg = filesystem.read(pkgPath, 'json');
|
|
339
|
+
if (!pkg)
|
|
340
|
+
return false;
|
|
341
|
+
// npm/yarn workspaces: `workspaces` is either an array of globs
|
|
342
|
+
// (`["packages/*"]`) or an object with a `packages` array
|
|
343
|
+
// (yarn classic). Both count.
|
|
344
|
+
const ws = pkg.workspaces;
|
|
345
|
+
if (Array.isArray(ws) && ws.length > 0)
|
|
346
|
+
return true;
|
|
347
|
+
if (ws && typeof ws === 'object' && Array.isArray(ws.packages)) {
|
|
348
|
+
return ((_a = ws.packages) !== null && _a !== void 0 ? _a : []).length > 0;
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|