@lenne.tech/cli 1.33.0 → 1.34.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/build/commands/codex/codex.js +23 -0
- package/build/commands/codex/plugins.js +103 -0
- package/build/commands/codex/shortcuts.js +110 -0
- package/build/commands/dev/test.js +10 -4
- package/build/commands/dev/up.js +9 -3
- package/build/commands/frontend/nuxt.js +4 -7
- package/build/commands/fullstack/init.js +26 -11
- package/build/commands/ticket/start.js +6 -4
- package/build/extensions/frontend-helper.js +44 -39
- package/build/extensions/server.js +214 -4
- package/build/lib/codex-cli.js +56 -0
- package/build/lib/codex-plugin-utils.js +102 -0
- package/build/lib/dev-package-manager.js +89 -0
- package/build/lib/dev-patches.js +4 -1
- package/build/lib/dev-test-session.js +30 -16
- package/build/lib/dev-ticket.js +12 -6
- package/build/lib/vendor-claude-md.js +7 -2
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
/**
|
|
13
|
+
* Codex commands
|
|
14
|
+
*/
|
|
15
|
+
module.exports = {
|
|
16
|
+
alias: ['cx'],
|
|
17
|
+
description: 'Codex commands',
|
|
18
|
+
name: 'codex',
|
|
19
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
20
|
+
yield toolbox.helper.showMenu('codex');
|
|
21
|
+
return 'codex';
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
const codex_cli_1 = require("../../lib/codex-cli");
|
|
15
|
+
const codex_plugin_utils_1 = require("../../lib/codex-plugin-utils");
|
|
16
|
+
const CODEX_PLUGIN_NAME = 'lt-dev';
|
|
17
|
+
/**
|
|
18
|
+
* Install/update lenne.tech Codex plugin, custom agents, and prompt wrappers.
|
|
19
|
+
*/
|
|
20
|
+
const PluginsCommand = {
|
|
21
|
+
alias: ['p'],
|
|
22
|
+
description: 'Install Codex plugins',
|
|
23
|
+
name: 'plugins',
|
|
24
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
25
|
+
const { helper, print: { error, info, spin, success, warning }, system, } = toolbox;
|
|
26
|
+
const timer = system.startTimer();
|
|
27
|
+
const marketplaceRoot = String(toolbox.parameters.options.path || codex_cli_1.DEFAULT_CODEX_MARKETPLACE_ROOT);
|
|
28
|
+
const pluginRoot = (0, path_1.join)(marketplaceRoot, 'plugins', CODEX_PLUGIN_NAME);
|
|
29
|
+
const marketplaceName = (0, codex_plugin_utils_1.readCodexMarketplaceName)(marketplaceRoot);
|
|
30
|
+
if (!marketplaceName) {
|
|
31
|
+
error(`Codex marketplace not found at ${marketplaceRoot}`);
|
|
32
|
+
info('');
|
|
33
|
+
info('Expected: .agents/plugins/marketplace.json');
|
|
34
|
+
info('Run this from a checkout that contains the generated codex marketplace, or pass --path=<marketplace-root>.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (!(0, fs_1.existsSync)(pluginRoot)) {
|
|
38
|
+
error(`Codex plugin not found: ${pluginRoot}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const cli = (0, codex_cli_1.findCodexCli)();
|
|
42
|
+
if (!cli) {
|
|
43
|
+
error('Codex CLI not found. Please install Codex first.');
|
|
44
|
+
info('');
|
|
45
|
+
info('Installation: https://developers.openai.com/codex');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const listSpinner = spin('Checking Codex marketplaces');
|
|
49
|
+
const listResult = (0, codex_cli_1.runCodexCommand)(cli, ['plugin', 'marketplace', 'list', '--json']);
|
|
50
|
+
const marketplaceConfigured = listResult.success &&
|
|
51
|
+
(listResult.output.includes(`"name": "${marketplaceName}"`) || listResult.output.includes(marketplaceRoot));
|
|
52
|
+
if (marketplaceConfigured) {
|
|
53
|
+
listSpinner.succeed(`Marketplace ${marketplaceName} already configured`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
listSpinner.text = `Adding marketplace ${marketplaceName}`;
|
|
57
|
+
const addResult = (0, codex_cli_1.runCodexCommand)(cli, ['plugin', 'marketplace', 'add', marketplaceRoot, '--json']);
|
|
58
|
+
if (addResult.success || addResult.output.includes('already')) {
|
|
59
|
+
listSpinner.succeed(`Marketplace ${marketplaceName} added`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
listSpinner.fail(`Failed to add marketplace ${marketplaceName}`);
|
|
63
|
+
error(addResult.output);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const installSpinner = spin(`Installing/updating ${CODEX_PLUGIN_NAME}`);
|
|
68
|
+
const installResult = (0, codex_cli_1.runCodexCommand)(cli, ['plugin', 'add', `${CODEX_PLUGIN_NAME}@${marketplaceName}`, '--json']);
|
|
69
|
+
if (installResult.success || installResult.output.includes('already')) {
|
|
70
|
+
installSpinner.succeed(`${CODEX_PLUGIN_NAME} installed`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
installSpinner.fail(`Failed to install ${CODEX_PLUGIN_NAME}`);
|
|
74
|
+
error(installResult.output);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const agentSpinner = spin('Installing Codex custom agents');
|
|
78
|
+
const agentCount = (0, codex_plugin_utils_1.installCodexAgents)(pluginRoot);
|
|
79
|
+
agentSpinner.succeed(`${agentCount} custom agents installed`);
|
|
80
|
+
const promptSpinner = spin('Installing Codex prompt wrappers');
|
|
81
|
+
const promptCount = (0, codex_plugin_utils_1.installCodexPrompts)(pluginRoot);
|
|
82
|
+
promptSpinner.succeed(`${promptCount} prompt wrappers installed`);
|
|
83
|
+
const contents = (0, codex_plugin_utils_1.readLocalCodexPluginContents)(pluginRoot);
|
|
84
|
+
info('');
|
|
85
|
+
success(`Codex setup completed in ${helper.msToMinutesAndSeconds(timer())}m.`);
|
|
86
|
+
info('');
|
|
87
|
+
info('Installed:');
|
|
88
|
+
info(` Plugin: ${CODEX_PLUGIN_NAME}@${marketplaceName}`);
|
|
89
|
+
info(` Skills (${contents.skills.length}): ${contents.skills.join(', ')}`);
|
|
90
|
+
info(` MCP Servers (${contents.mcpServers.length}): ${contents.mcpServers.join(', ')}`);
|
|
91
|
+
info(` Hooks: ${contents.hooks}`);
|
|
92
|
+
info(` Custom agents: ${agentCount}`);
|
|
93
|
+
info(` Prompt wrappers: ${promptCount}`);
|
|
94
|
+
info('');
|
|
95
|
+
warning('Restart Codex or start a new thread so new skills, agents, prompts, and MCP tools are loaded.');
|
|
96
|
+
info('Use former lt-dev commands via /prompts:lt-dev-... or by asking Codex to use the lt-dev command router.');
|
|
97
|
+
if (!toolbox.parameters.options.fromGluegunMenu) {
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
return 'codex plugins installed';
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
exports.default = PluginsCommand;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const shell_config_1 = require("../../lib/shell-config");
|
|
13
|
+
const CODEX_SHORTCUTS = [
|
|
14
|
+
{
|
|
15
|
+
alias: 'x',
|
|
16
|
+
command: 'codex --sandbox workspace-write --ask-for-approval on-request',
|
|
17
|
+
description: 'Start new Codex session',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
alias: 'xr',
|
|
21
|
+
command: 'codex resume',
|
|
22
|
+
description: 'Select and resume previous Codex session',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
alias: 'xf',
|
|
26
|
+
command: 'LT_PLUGIN_HOOKS_SKIP=1 codex --sandbox workspace-write --ask-for-approval on-request',
|
|
27
|
+
description: 'Start Codex in fast mode (skip lenne.tech plugin detect hooks)',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
alias: 'xp',
|
|
31
|
+
command: 'lt codex plugins',
|
|
32
|
+
description: 'Install/update lenne.tech Codex plugin setup',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
const ShortcutsCommand = {
|
|
36
|
+
alias: ['s'],
|
|
37
|
+
description: 'Install Codex shell shortcuts',
|
|
38
|
+
name: 'shortcuts',
|
|
39
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
40
|
+
const { print: { error, info, success }, } = toolbox;
|
|
41
|
+
const shellConfig = (0, shell_config_1.getPreferredShellConfig)();
|
|
42
|
+
if (!shellConfig) {
|
|
43
|
+
error('Could not detect shell configuration file.');
|
|
44
|
+
info('Supported shells: zsh, bash');
|
|
45
|
+
if (!toolbox.parameters.options.fromGluegunMenu) {
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
return 'shortcuts: no shell config found';
|
|
49
|
+
}
|
|
50
|
+
info(`Shell: ${shellConfig.shell}`);
|
|
51
|
+
info(`Config: ${shellConfig.path}`);
|
|
52
|
+
info('');
|
|
53
|
+
const existingAliases = [];
|
|
54
|
+
const missingAliases = [];
|
|
55
|
+
for (const shortcut of CODEX_SHORTCUTS) {
|
|
56
|
+
if ((0, shell_config_1.checkAliasInFile)(shellConfig.path, shortcut.alias)) {
|
|
57
|
+
existingAliases.push(shortcut);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
missingAliases.push(shortcut);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (existingAliases.length > 0) {
|
|
64
|
+
info('Already installed:');
|
|
65
|
+
for (const { alias, description } of existingAliases) {
|
|
66
|
+
info(` ${alias} - ${description}`);
|
|
67
|
+
}
|
|
68
|
+
info('');
|
|
69
|
+
}
|
|
70
|
+
if (missingAliases.length === 0) {
|
|
71
|
+
success('All Codex shortcuts are already installed!');
|
|
72
|
+
info('');
|
|
73
|
+
info('Available shortcuts:');
|
|
74
|
+
for (const { alias, command, description } of CODEX_SHORTCUTS) {
|
|
75
|
+
info(` ${alias} - ${description}`);
|
|
76
|
+
info(` ${command}`);
|
|
77
|
+
}
|
|
78
|
+
if (!toolbox.parameters.options.fromGluegunMenu) {
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
return 'shortcuts: already installed';
|
|
82
|
+
}
|
|
83
|
+
const added = (0, shell_config_1.addAliasBlockToShellConfig)(shellConfig.path, missingAliases, 'Codex shortcuts - Added by lenne.tech CLI');
|
|
84
|
+
if (added) {
|
|
85
|
+
info('');
|
|
86
|
+
success(`Added ${missingAliases.length} shortcut${missingAliases.length > 1 ? 's' : ''} to ${shellConfig.path}`);
|
|
87
|
+
info('');
|
|
88
|
+
info(`Run: source ${shellConfig.path}`);
|
|
89
|
+
info('Or restart your terminal to apply changes.');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
error(`Failed to write to ${shellConfig.path}`);
|
|
93
|
+
info('');
|
|
94
|
+
info('To add manually, add these lines to your shell config:');
|
|
95
|
+
info('');
|
|
96
|
+
for (const { alias, command } of missingAliases) {
|
|
97
|
+
info(`alias ${alias}='${command}'`);
|
|
98
|
+
}
|
|
99
|
+
if (!toolbox.parameters.options.fromGluegunMenu) {
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
return 'shortcuts: write failed';
|
|
103
|
+
}
|
|
104
|
+
if (!toolbox.parameters.options.fromGluegunMenu) {
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
return `shortcuts: ${missingAliases.length} added`;
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
exports.default = ShortcutsCommand;
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
const fs_1 = require("fs");
|
|
13
13
|
const caddy_1 = require("../../lib/caddy");
|
|
14
14
|
const dev_env_bridge_1 = require("../../lib/dev-env-bridge");
|
|
15
|
+
const dev_package_manager_1 = require("../../lib/dev-package-manager");
|
|
15
16
|
const dev_process_1 = require("../../lib/dev-process");
|
|
16
17
|
const dev_project_1 = require("../../lib/dev-project");
|
|
17
18
|
const dev_test_session_1 = require("../../lib/dev-test-session");
|
|
@@ -75,7 +76,6 @@ const TestCommand = {
|
|
|
75
76
|
const keep = Boolean(parameters.options.keep) || parameters.options.teardown === false;
|
|
76
77
|
const debug = Boolean(parameters.options.debug);
|
|
77
78
|
const forwarded = parameters.array || [];
|
|
78
|
-
const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
|
|
79
79
|
// `--shard N` → run the suite split across N fully-isolated stacks in
|
|
80
80
|
// parallel. A bare `--shard` defaults to 2 — the stable sweet spot for a
|
|
81
81
|
// heavy built-SSR suite (N>=3 over-subscribes the perf cores → flaky; see
|
|
@@ -102,7 +102,8 @@ const TestCommand = {
|
|
|
102
102
|
return 'dev test: no api';
|
|
103
103
|
}
|
|
104
104
|
info(colors.bold(`Running API tests for "${identity.slug}" (isolated DB)`));
|
|
105
|
-
const
|
|
105
|
+
const apiPm = (0, dev_package_manager_1.pickPackageManager)(layout.apiDir);
|
|
106
|
+
const code = yield (0, dev_process_1.runChildInherit)(apiPm.bin, apiPm.runScript('test:e2e', forwarded), {
|
|
106
107
|
cwd: layout.apiDir,
|
|
107
108
|
env: process.env,
|
|
108
109
|
});
|
|
@@ -174,10 +175,11 @@ const TestCommand = {
|
|
|
174
175
|
try {
|
|
175
176
|
info('');
|
|
176
177
|
info(colors.bold(`Running isolated Playwright E2E for "${identity.slug}" sharded across ${shardTotal} stacks`));
|
|
178
|
+
const shardPm = (0, dev_package_manager_1.pickPackageManager)(layout.appDir);
|
|
177
179
|
shardExit = yield (0, dev_test_session_1.runShardedTestSession)(layout, identity, log, {
|
|
178
180
|
devDbName,
|
|
179
181
|
forwarded,
|
|
180
|
-
|
|
182
|
+
pm: shardPm,
|
|
181
183
|
total: shardTotal,
|
|
182
184
|
});
|
|
183
185
|
}
|
|
@@ -233,7 +235,11 @@ const TestCommand = {
|
|
|
233
235
|
info(colors.bold(`Running isolated Playwright E2E for "${identity.slug}"`));
|
|
234
236
|
info(colors.dim(` app: ${ctx.appUrl} db: ${ctx.dbName}`));
|
|
235
237
|
info('');
|
|
236
|
-
|
|
238
|
+
const appPm = (0, dev_package_manager_1.pickPackageManager)(layout.appDir);
|
|
239
|
+
exitCode = yield (0, dev_process_1.runChildInherit)(appPm.bin, appPm.runScript('test:e2e', forwarded), {
|
|
240
|
+
cwd: layout.appDir,
|
|
241
|
+
env,
|
|
242
|
+
});
|
|
237
243
|
}
|
|
238
244
|
catch (e) {
|
|
239
245
|
error(`Failed to run isolated E2E: ${e.message}`);
|
package/build/commands/dev/up.js
CHANGED
|
@@ -14,6 +14,7 @@ const path_1 = require("path");
|
|
|
14
14
|
const caddy_1 = require("../../lib/caddy");
|
|
15
15
|
const dev_env_1 = require("../../lib/dev-env");
|
|
16
16
|
const dev_env_bridge_1 = require("../../lib/dev-env-bridge");
|
|
17
|
+
const dev_package_manager_1 = require("../../lib/dev-package-manager");
|
|
17
18
|
const dev_patches_1 = require("../../lib/dev-patches");
|
|
18
19
|
const dev_process_1 = require("../../lib/dev-process");
|
|
19
20
|
const dev_project_1 = require("../../lib/dev-project");
|
|
@@ -326,7 +327,6 @@ const UpCommand = {
|
|
|
326
327
|
dbName,
|
|
327
328
|
identity,
|
|
328
329
|
});
|
|
329
|
-
const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
|
|
330
330
|
const pids = {};
|
|
331
331
|
const rotationNotes = [];
|
|
332
332
|
const started = [];
|
|
@@ -350,7 +350,12 @@ const UpCommand = {
|
|
|
350
350
|
}
|
|
351
351
|
else {
|
|
352
352
|
yield reclaimPort(existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api, apiPort, apiHealth !== null && apiHealth !== void 0 ? apiHealth : 'dead');
|
|
353
|
-
|
|
353
|
+
// Per-component PM detection: a monorepo may have an npm api and a
|
|
354
|
+
// pnpm app, and the legacy hard-coded `pnpm start` would silently
|
|
355
|
+
// regenerate a foreign lockfile + crash on un-approved build
|
|
356
|
+
// scripts when run against an npm-only project.
|
|
357
|
+
const apiPm = (0, dev_package_manager_1.pickPackageManager)(layout.apiDir);
|
|
358
|
+
const apiResult = (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), {
|
|
354
359
|
cwd: layout.apiDir,
|
|
355
360
|
env: devEnv.api.env,
|
|
356
361
|
logFile: (0, path_1.join)(layout.root, '.lt-dev', 'api.log'),
|
|
@@ -371,7 +376,8 @@ const UpCommand = {
|
|
|
371
376
|
}
|
|
372
377
|
else {
|
|
373
378
|
yield reclaimPort(existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.app, appPort, appHealth !== null && appHealth !== void 0 ? appHealth : 'dead');
|
|
374
|
-
const
|
|
379
|
+
const appPm = (0, dev_package_manager_1.pickPackageManager)(layout.appDir);
|
|
380
|
+
const appResult = (0, dev_process_1.spawnDetached)(appPm.bin, appPm.runScript('dev'), {
|
|
375
381
|
cwd: layout.appDir,
|
|
376
382
|
env: devEnv.app.env,
|
|
377
383
|
logFile: (0, path_1.join)(layout.root, '.lt-dev', 'app.log'),
|
|
@@ -15,7 +15,7 @@ const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
|
15
15
|
* Create a new nuxt workspace
|
|
16
16
|
*
|
|
17
17
|
* Standalone counterpart to `lt fullstack init` / `lt fullstack add-app`
|
|
18
|
-
* for Nuxt: clones nuxt-base-starter
|
|
18
|
+
* for Nuxt: clones nuxt-base-starter into
|
|
19
19
|
* a brand-new directory. Mirrors the same surface area as add-app where
|
|
20
20
|
* applicable so behaviour is consistent across the four flows.
|
|
21
21
|
*/
|
|
@@ -170,7 +170,7 @@ const NewCommand = {
|
|
|
170
170
|
info('Dry-run plan:');
|
|
171
171
|
info(` name: ${projName}`);
|
|
172
172
|
info(` projectDir: ${projectDir}`);
|
|
173
|
-
info(` branch: ${branch || '(default
|
|
173
|
+
info(` branch: ${branch || '(default branch)'}`);
|
|
174
174
|
info(` copy: ${copyPath || '(none)'}`);
|
|
175
175
|
info(` link: ${linkPath || '(none)'}`);
|
|
176
176
|
info(` frontendFrameworkMode: ${frontendFrameworkMode}`);
|
|
@@ -183,11 +183,8 @@ const NewCommand = {
|
|
|
183
183
|
else if (copyPath) {
|
|
184
184
|
info(` 1. copy ${copyPath} → ./${projectDir}`);
|
|
185
185
|
}
|
|
186
|
-
else if (branch) {
|
|
187
|
-
info(` 1. clone nuxt-base-starter (branch: ${branch}) → ./${projectDir}`);
|
|
188
|
-
}
|
|
189
186
|
else {
|
|
190
|
-
info(` 1.
|
|
187
|
+
info(` 1. clone nuxt-base-starter (${branch ? `branch: ${branch}` : 'default branch'}) → ./${projectDir}`);
|
|
191
188
|
}
|
|
192
189
|
if (frontendFrameworkMode === 'vendor') {
|
|
193
190
|
info(` 2. clone @lenne.tech/nuxt-extensions → /tmp`);
|
|
@@ -206,7 +203,7 @@ const NewCommand = {
|
|
|
206
203
|
branch,
|
|
207
204
|
copyPath,
|
|
208
205
|
linkPath,
|
|
209
|
-
skipInstall:
|
|
206
|
+
skipInstall: false, // clone-based setup installs here (create-nuxt-base no longer used)
|
|
210
207
|
});
|
|
211
208
|
if (!result.success) {
|
|
212
209
|
baseSpinner.fail(`Failed to setup nuxt workspace: ${result.path}`);
|
|
@@ -33,7 +33,7 @@ const NewCommand = {
|
|
|
33
33
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
34
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
|
|
35
35
|
// Retrieve the tools we need
|
|
36
|
-
const { config, filesystem, frontendHelper, git, helper, parameters, patching, print: { colors, error, info, spin, success }, prompt: { ask, confirm }, server, strings: { kebabCase }, system, template, } = toolbox;
|
|
36
|
+
const { config, filesystem, frontendHelper, git, helper, parameters, patching, print: { colors, error, info, spin, success, warning }, prompt: { ask, confirm }, server, strings: { kebabCase }, system, template, } = toolbox;
|
|
37
37
|
// Start timer
|
|
38
38
|
const timer = system.startTimer();
|
|
39
39
|
// Info
|
|
@@ -209,7 +209,9 @@ const NewCommand = {
|
|
|
209
209
|
// `/lt-dev:backend:update-nest-server-core`; local patches are logged
|
|
210
210
|
// in src/core/VENDOR.md.
|
|
211
211
|
//
|
|
212
|
-
// Default is
|
|
212
|
+
// Default is 'vendor' — the lt CLI integrates the framework core directly
|
|
213
|
+
// (clones the starter, vendors core into src/core/), which is the
|
|
214
|
+
// recommended setup; pass `--framework-mode npm` for the classic npm dep.
|
|
213
215
|
let frameworkMode;
|
|
214
216
|
if (experimental) {
|
|
215
217
|
frameworkMode = 'npm';
|
|
@@ -226,16 +228,16 @@ const NewCommand = {
|
|
|
226
228
|
info(`Using framework mode from lt.config: ${frameworkMode}`);
|
|
227
229
|
}
|
|
228
230
|
else if (noConfirm) {
|
|
229
|
-
frameworkMode = '
|
|
230
|
-
info('Using default framework mode:
|
|
231
|
+
frameworkMode = 'vendor';
|
|
232
|
+
info('Using default framework mode: vendor (noConfirm mode)');
|
|
231
233
|
}
|
|
232
234
|
else {
|
|
233
235
|
const frameworkModeChoice = yield ask({
|
|
234
236
|
choices: [
|
|
235
237
|
'npm - @lenne.tech/nest-server as npm dependency (classic, stable)',
|
|
236
|
-
'vendor - framework core vendored into projects/api/src/core/ (
|
|
238
|
+
'vendor - framework core vendored into projects/api/src/core/ (default, allows local patches)',
|
|
237
239
|
],
|
|
238
|
-
initial:
|
|
240
|
+
initial: 1,
|
|
239
241
|
message: 'Framework consumption mode?',
|
|
240
242
|
name: 'frameworkMode',
|
|
241
243
|
type: 'select',
|
|
@@ -257,11 +259,11 @@ const NewCommand = {
|
|
|
257
259
|
info(`Using frontend framework mode from lt.config: ${frontendFrameworkMode}`);
|
|
258
260
|
}
|
|
259
261
|
else if (noConfirm) {
|
|
260
|
-
frontendFrameworkMode = '
|
|
262
|
+
frontendFrameworkMode = 'vendor';
|
|
261
263
|
}
|
|
262
264
|
else {
|
|
263
|
-
// Default to
|
|
264
|
-
frontendFrameworkMode = '
|
|
265
|
+
// Default to vendor without asking (unless user sets it explicitly)
|
|
266
|
+
frontendFrameworkMode = 'vendor';
|
|
265
267
|
}
|
|
266
268
|
// Determine remote push settings with priority: CLI > config > interactive
|
|
267
269
|
// Git is always initialized; the question is whether to push to a remote
|
|
@@ -273,8 +275,13 @@ const NewCommand = {
|
|
|
273
275
|
if (pushToRemote) {
|
|
274
276
|
gitLink = cliGitLink || configGitLink;
|
|
275
277
|
if (!gitLink) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
+
// Git is always initialized locally (dev branch + initial commit);
|
|
279
|
+
// --git only controls the remote push. Without a link there is
|
|
280
|
+
// nothing to push to, so degrade to a local-only init instead of
|
|
281
|
+
// aborting the whole scaffold — consistent with the config and
|
|
282
|
+
// interactive branches below.
|
|
283
|
+
warning('--git true without --git-link: initializing local git only (no remote push).');
|
|
284
|
+
pushToRemote = false;
|
|
278
285
|
}
|
|
279
286
|
}
|
|
280
287
|
}
|
|
@@ -314,6 +321,14 @@ const NewCommand = {
|
|
|
314
321
|
}
|
|
315
322
|
}
|
|
316
323
|
}
|
|
324
|
+
else if (noConfirm) {
|
|
325
|
+
// Default in noConfirm mode: git true. Git is always initialized locally
|
|
326
|
+
// (dev branch + initial commit); push to a remote only when a gitLink is
|
|
327
|
+
// configured, otherwise stay local-only (nothing to push to).
|
|
328
|
+
gitLink = configGitLink;
|
|
329
|
+
pushToRemote = !!gitLink;
|
|
330
|
+
info(`Using default git: true${gitLink ? ' (push to configured gitLink)' : ' (local only — no --git-link configured)'}`);
|
|
331
|
+
}
|
|
317
332
|
// Determine branches and copy/link paths with priority: CLI > config
|
|
318
333
|
const apiBranch = cliApiBranch || configApiBranch;
|
|
319
334
|
// Under `--next`, default the nuxt-base-starter ref to the `next`
|
|
@@ -97,14 +97,16 @@ const StartCommand = {
|
|
|
97
97
|
}
|
|
98
98
|
// 2. tag the worktree with its ticket id (makes lt dev * ticket-aware).
|
|
99
99
|
(0, dev_ticket_1.writeTicketMarker)(worktreePath, id);
|
|
100
|
-
// 3. install deps
|
|
100
|
+
// 3. install deps. Auto-detects pnpm/yarn/npm from the worktree's
|
|
101
|
+
// lockfile so an npm-only repo doesn't get a foreign pnpm-lock
|
|
102
|
+
// injected on every ticket start.
|
|
101
103
|
if (parameters.options.install !== false) {
|
|
102
|
-
info(colors.dim('Installing dependencies
|
|
104
|
+
info(colors.dim('Installing dependencies …'));
|
|
103
105
|
try {
|
|
104
|
-
(0, dev_ticket_1.
|
|
106
|
+
(0, dev_ticket_1.installWorktreeDeps)(worktreePath);
|
|
105
107
|
}
|
|
106
108
|
catch (e) {
|
|
107
|
-
warning(`
|
|
109
|
+
warning(`install failed (${e.message}) — continuing; run it manually in the worktree.`);
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
// 4. bring the isolated stack up (re-invokes THIS lt build so the marker is
|
|
@@ -141,49 +141,54 @@ class FrontendHelper {
|
|
|
141
141
|
return __awaiter(this, arguments, void 0, function* (dest, options = {}) {
|
|
142
142
|
const { system, templateHelper } = this.toolbox;
|
|
143
143
|
const { branch, copyPath, linkPath, skipInstall } = options;
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
// Run install if not skipped and not a symlink
|
|
165
|
-
if (!skipInstall && result.method !== 'link') {
|
|
166
|
-
try {
|
|
167
|
-
const { pm } = this.toolbox;
|
|
168
|
-
yield system.run(`cd "${dest}" && ${pm.install(pm.detect(dest))}`);
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
return { method: result.method, path: dest, success: false };
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return { method: result.method, path: result.path, success: true };
|
|
144
|
+
// Source the Nuxt app from the nuxt-base-starter repo (priority:
|
|
145
|
+
// link > copy > clone), mirroring the backend (nest-server-starter)
|
|
146
|
+
// flow. The default — no link/copy/branch — clones the repo's default
|
|
147
|
+
// branch, so a fresh init always integrates the CURRENT GitHub template.
|
|
148
|
+
//
|
|
149
|
+
// This deliberately replaces the former `create-nuxt-base@latest` npx
|
|
150
|
+
// path: that package is marked `private` and is no longer published
|
|
151
|
+
// (nuxt-base-starter commit b534af2 — "distribute via lt CLI fullstack
|
|
152
|
+
// template"), so `@latest` was frozen at an outdated copy and the
|
|
153
|
+
// bundled template drifted from GitHub. Cloning the repo directly is
|
|
154
|
+
// the same mechanism the backend already uses and removes that drift.
|
|
155
|
+
const result = yield templateHelper.setup(dest, {
|
|
156
|
+
branch,
|
|
157
|
+
copyPath,
|
|
158
|
+
isNuxt: true,
|
|
159
|
+
linkPath,
|
|
160
|
+
repoUrl: linkPath || copyPath ? undefined : 'https://github.com/lenneTech/nuxt-base-starter.git',
|
|
161
|
+
});
|
|
162
|
+
if (!result.success) {
|
|
163
|
+
return { method: result.method, path: result.path, success: false };
|
|
175
164
|
}
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
165
|
+
// After a clone, flatten the wrapper layout so `dest` IS the Nuxt app
|
|
166
|
+
// (the cloned root carries the scaffolder; the app lives in
|
|
167
|
+
// `nuxt-base-template/` — see flattenNuxtBaseTemplate). Skip on link
|
|
168
|
+
// mode: a symlink points at the user's checkout and must not be torn out.
|
|
169
|
+
if (result.method === 'clone') {
|
|
170
|
+
yield this.flattenNuxtBaseTemplate(dest);
|
|
171
|
+
}
|
|
172
|
+
// Normalize the sub-project package name to a stable, valid workspace
|
|
173
|
+
// name ("app"). The template ships as `nuxt-base-template`; the former
|
|
174
|
+
// create-nuxt-base path renamed it the same way. Skip link mode — a
|
|
175
|
+
// symlinked checkout is shared with upstream and must not be mutated.
|
|
176
|
+
if (result.method !== 'link') {
|
|
181
177
|
yield this.fixPackageName(dest);
|
|
182
|
-
return { method: 'npx', path: dest, success: true };
|
|
183
178
|
}
|
|
184
|
-
|
|
185
|
-
|
|
179
|
+
// Run install unless skipped or a symlink. Fullstack init installs at
|
|
180
|
+
// the monorepo level (skipInstall: true); standalone `lt frontend nuxt`
|
|
181
|
+
// installs here (create-nuxt-base used to handle that).
|
|
182
|
+
if (!skipInstall && result.method !== 'link') {
|
|
183
|
+
try {
|
|
184
|
+
const { pm } = this.toolbox;
|
|
185
|
+
yield system.run(`cd "${dest}" && ${pm.install(pm.detect(dest))}`);
|
|
186
|
+
}
|
|
187
|
+
catch (_a) {
|
|
188
|
+
return { method: result.method, path: dest, success: false };
|
|
189
|
+
}
|
|
186
190
|
}
|
|
191
|
+
return { method: result.method, path: result.path, success: true };
|
|
187
192
|
});
|
|
188
193
|
}
|
|
189
194
|
/**
|
|
@@ -1006,6 +1006,20 @@ class Server {
|
|
|
1006
1006
|
// Best-effort — if we can't read upstream pkg, the starter's own
|
|
1007
1007
|
// deps should still cover most of the framework's needs.
|
|
1008
1008
|
}
|
|
1009
|
+
// Snapshot the upstream lockfile too. The shallow clone ships
|
|
1010
|
+
// pnpm-lock.yaml, which pins EXACT versions for every transitive
|
|
1011
|
+
// dependency the vendored core imports directly (cron, jose,
|
|
1012
|
+
// fs-capacitor, graphql-ws, ws, …) but that nest-server never declares
|
|
1013
|
+
// as a direct dep. Step 5b below resolves the core's import closure
|
|
1014
|
+
// against this so those packages become direct project deps.
|
|
1015
|
+
let upstreamLockRaw = '';
|
|
1016
|
+
try {
|
|
1017
|
+
upstreamLockRaw = filesystem.read(`${tmpClone}/pnpm-lock.yaml`) || '';
|
|
1018
|
+
}
|
|
1019
|
+
catch (_b) {
|
|
1020
|
+
// Best-effort — without the lockfile, the closure step falls back to
|
|
1021
|
+
// the upstream package.json ranges (still better than nothing).
|
|
1022
|
+
}
|
|
1009
1023
|
// Snapshot the upstream CLAUDE.md for section-merge into projects/api/CLAUDE.md.
|
|
1010
1024
|
// The nest-server CLAUDE.md contains framework-specific instructions that
|
|
1011
1025
|
// Claude Code needs to work correctly with the vendored source (API conventions,
|
|
@@ -1018,7 +1032,7 @@ class Server {
|
|
|
1018
1032
|
upstreamClaudeMd = claudeMdContent;
|
|
1019
1033
|
}
|
|
1020
1034
|
}
|
|
1021
|
-
catch (
|
|
1035
|
+
catch (_c) {
|
|
1022
1036
|
// Non-fatal — if missing, the project CLAUDE.md just won't get upstream sections.
|
|
1023
1037
|
}
|
|
1024
1038
|
// Snapshot the upstream commit SHA for traceability in VENDOR.md.
|
|
@@ -1027,7 +1041,7 @@ class Server {
|
|
|
1027
1041
|
const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
|
|
1028
1042
|
upstreamCommit = (sha || '').trim();
|
|
1029
1043
|
}
|
|
1030
|
-
catch (
|
|
1044
|
+
catch (_d) {
|
|
1031
1045
|
// Non-fatal — VENDOR.md will just show an empty SHA.
|
|
1032
1046
|
}
|
|
1033
1047
|
try {
|
|
@@ -1310,7 +1324,7 @@ class Server {
|
|
|
1310
1324
|
}
|
|
1311
1325
|
}
|
|
1312
1326
|
}
|
|
1313
|
-
catch (
|
|
1327
|
+
catch (_e) {
|
|
1314
1328
|
// skip unreadable file
|
|
1315
1329
|
}
|
|
1316
1330
|
}
|
|
@@ -1420,6 +1434,25 @@ class Server {
|
|
|
1420
1434
|
}
|
|
1421
1435
|
}
|
|
1422
1436
|
}
|
|
1437
|
+
// ── 5b. Backfill the vendored core's transitive import closure ──
|
|
1438
|
+
//
|
|
1439
|
+
// The merge above only covers nest-server's *declared* dependencies.
|
|
1440
|
+
// The vendored core ALSO imports packages that used to arrive
|
|
1441
|
+
// transitively through the @lenne.tech/nest-server npm dep
|
|
1442
|
+
// (cron←@nestjs/schedule, jose←better-auth, fs-capacitor←graphql-upload,
|
|
1443
|
+
// graphql-ws / ws). Once vendored, those imports are direct, so under
|
|
1444
|
+
// pnpm's isolated node_modules they are unresolvable unless declared
|
|
1445
|
+
// as direct deps → TS2307 at build time. Scan the core's bare imports
|
|
1446
|
+
// and add any missing package (+ matching @types) at the EXACT version
|
|
1447
|
+
// pinned by the upstream lockfile.
|
|
1448
|
+
this.gatherVendorCoreImportClosure({
|
|
1449
|
+
coreDir,
|
|
1450
|
+
deps,
|
|
1451
|
+
devDeps,
|
|
1452
|
+
lockRaw: upstreamLockRaw,
|
|
1453
|
+
upstreamDeps,
|
|
1454
|
+
upstreamDevDeps,
|
|
1455
|
+
});
|
|
1423
1456
|
// Add a script to run the local bin/migrate.js. The starter's
|
|
1424
1457
|
// existing migrate:* scripts are already correct for npm mode; we
|
|
1425
1458
|
// need them pointing at the local bin + local ts-compiler.
|
|
@@ -1692,7 +1725,13 @@ class Server {
|
|
|
1692
1725
|
// Scan all consumer files for stale bare-specifier imports that the
|
|
1693
1726
|
// codemod should have rewritten. A single miss causes a compile error,
|
|
1694
1727
|
// so catching it here with a clear message saves the user debugging time.
|
|
1695
|
-
const staleImports = this.findStaleImports(dest, '@lenne.tech/nest-server'
|
|
1728
|
+
const staleImports = this.findStaleImports(dest, '@lenne.tech/nest-server',
|
|
1729
|
+
// Match only real import/export/require specifiers in single/double
|
|
1730
|
+
// quotes — NOT comment references like
|
|
1731
|
+
// `node_modules/@lenne.tech/nest-server/...` (backticks, no keyword),
|
|
1732
|
+
// which the codemod legitimately leaves untouched. A naive substring
|
|
1733
|
+
// match flagged config.env.ts's JSDoc path as a false positive.
|
|
1734
|
+
/(?:from|import|export|require)\s*\(?\s*['"]@lenne\.tech\/nest-server(?:\/[^'"]*)?['"]/);
|
|
1696
1735
|
if (staleImports.length > 0) {
|
|
1697
1736
|
const { print } = this.toolbox;
|
|
1698
1737
|
print.warning(`⚠ ${staleImports.length} file(s) still contain '@lenne.tech/nest-server' imports after vendor conversion:`);
|
|
@@ -2341,6 +2380,177 @@ class Server {
|
|
|
2341
2380
|
}
|
|
2342
2381
|
return stale;
|
|
2343
2382
|
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Backfills the vendored core's transitive import closure into the
|
|
2385
|
+
* project's dependencies. After vendoring, `src/core/**` imports packages
|
|
2386
|
+
* that previously arrived transitively via the `@lenne.tech/nest-server`
|
|
2387
|
+
* npm dependency (e.g. `cron`, `jose`, `fs-capacitor`, `graphql-ws`,
|
|
2388
|
+
* `ws`). nest-server never declares these as direct deps, so the
|
|
2389
|
+
* upstream-deps merge misses them — yet the vendored code imports them
|
|
2390
|
+
* directly, which pnpm's isolated node_modules cannot resolve → TS2307 at
|
|
2391
|
+
* build time.
|
|
2392
|
+
*
|
|
2393
|
+
* Scans every `.ts` file under the vendored core for bare import
|
|
2394
|
+
* specifiers, drops Node builtins and already-declared packages, and adds
|
|
2395
|
+
* the remainder (plus any matching `@types/*`) using EXACT versions read
|
|
2396
|
+
* from the upstream `pnpm-lock.yaml`. Mutates `deps`/`devDeps` in place.
|
|
2397
|
+
*/
|
|
2398
|
+
gatherVendorCoreImportClosure(options) {
|
|
2399
|
+
const { coreDir, deps, devDeps, lockRaw, upstreamDeps, upstreamDevDeps } = options;
|
|
2400
|
+
const { print } = this.toolbox;
|
|
2401
|
+
const builtins = new Set(require('module').builtinModules);
|
|
2402
|
+
// Compare two dotted versions numerically (major.minor.patch) — used to
|
|
2403
|
+
// pick the highest when the lockfile pins multiple copies (e.g. ws 7/8).
|
|
2404
|
+
const compareVersions = (a, b) => {
|
|
2405
|
+
const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
|
|
2406
|
+
const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
|
|
2407
|
+
for (let i = 0; i < 3; i++) {
|
|
2408
|
+
if ((pa[i] || 0) !== (pb[i] || 0)) {
|
|
2409
|
+
return (pa[i] || 0) - (pb[i] || 0);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
return 0;
|
|
2413
|
+
};
|
|
2414
|
+
// Parse `<name>@<version>` keys from the lockfile's `packages:` section
|
|
2415
|
+
// into a name→version map (highest version wins). pnpm-lock v9 lists
|
|
2416
|
+
// every package (direct + transitive) there with a clean 2-space indent.
|
|
2417
|
+
const parseLockVersions = (raw) => {
|
|
2418
|
+
const map = {};
|
|
2419
|
+
let inPackages = false;
|
|
2420
|
+
for (const line of raw.split('\n')) {
|
|
2421
|
+
if (/^packages:\s*$/.test(line)) {
|
|
2422
|
+
inPackages = true;
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
if (inPackages && /^\S/.test(line)) {
|
|
2426
|
+
break; // reached the next top-level section
|
|
2427
|
+
}
|
|
2428
|
+
if (!inPackages) {
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
const matched = line.match(/^ {2}(['"]?)(.+?)\1:\s*$/);
|
|
2432
|
+
if (!matched) {
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
let key = matched[2] || '';
|
|
2436
|
+
const paren = key.indexOf('('); // strip pnpm peer suffix
|
|
2437
|
+
if (paren >= 0) {
|
|
2438
|
+
key = key.slice(0, paren);
|
|
2439
|
+
}
|
|
2440
|
+
const at = key.lastIndexOf('@');
|
|
2441
|
+
if (at <= 0) {
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
const name = key.slice(0, at);
|
|
2445
|
+
const version = key.slice(at + 1);
|
|
2446
|
+
if (!/^\d/.test(version)) {
|
|
2447
|
+
continue; // ranges / non-versions
|
|
2448
|
+
}
|
|
2449
|
+
const current = map[name];
|
|
2450
|
+
if (!current || compareVersions(version, current) > 0) {
|
|
2451
|
+
map[name] = version;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
return map;
|
|
2455
|
+
};
|
|
2456
|
+
// Reduce an import specifier to its package name (or null for relative
|
|
2457
|
+
// paths and Node builtins, which need no dependency entry).
|
|
2458
|
+
const packageNameOf = (spec) => {
|
|
2459
|
+
if (!spec || spec.startsWith('.') || spec.startsWith('/')) {
|
|
2460
|
+
return null;
|
|
2461
|
+
}
|
|
2462
|
+
const bare = spec.startsWith('node:') ? spec.slice('node:'.length) : spec;
|
|
2463
|
+
const parts = bare.split('/');
|
|
2464
|
+
const name = bare.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] || '';
|
|
2465
|
+
// Drop Node builtins, the framework package itself (never a dependency
|
|
2466
|
+
// of its own vendored code), and anything that is not a syntactically
|
|
2467
|
+
// valid npm package name (defensive guard).
|
|
2468
|
+
if (!name || builtins.has(name) || name === '@lenne.tech/nest-server') {
|
|
2469
|
+
return null;
|
|
2470
|
+
}
|
|
2471
|
+
return /^(?:@[\w.~-]+\/)?[\w.~-]+$/.test(name) ? name : null;
|
|
2472
|
+
};
|
|
2473
|
+
// Collect every bare specifier imported by the vendored core. Parse the
|
|
2474
|
+
// TypeScript AST (ts-morph) rather than scanning text, so that prose in
|
|
2475
|
+
// comments/strings — a JSDoc `import … from '@lenne.tech/nest-server'`
|
|
2476
|
+
// example, or an English `… distinguishable from "never set"` — is never
|
|
2477
|
+
// mistaken for a real import (which a naive regex did).
|
|
2478
|
+
const imported = new Set();
|
|
2479
|
+
const { Project, SyntaxKind } = require('ts-morph');
|
|
2480
|
+
const scanProject = new Project({
|
|
2481
|
+
skipAddingFilesFromTsConfig: true,
|
|
2482
|
+
skipFileDependencyResolution: true,
|
|
2483
|
+
});
|
|
2484
|
+
const sourceFiles = scanProject.addSourceFilesAtPaths(`${coreDir}/**/*.ts`);
|
|
2485
|
+
for (const sourceFile of sourceFiles) {
|
|
2486
|
+
// `import … from 'x'` and `export … from 'x'`
|
|
2487
|
+
for (const decl of [...sourceFile.getImportDeclarations(), ...sourceFile.getExportDeclarations()]) {
|
|
2488
|
+
const spec = decl.getModuleSpecifierValue();
|
|
2489
|
+
const name = spec ? packageNameOf(spec) : null;
|
|
2490
|
+
if (name) {
|
|
2491
|
+
imported.add(name);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
// `import x = require('x')`
|
|
2495
|
+
for (const decl of sourceFile.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration)) {
|
|
2496
|
+
const ref = decl
|
|
2497
|
+
.getModuleReference()
|
|
2498
|
+
.getText()
|
|
2499
|
+
.match(/^require\(\s*['"]([^'"]+)['"]\s*\)$/);
|
|
2500
|
+
const name = ref ? packageNameOf(ref[1] || '') : null;
|
|
2501
|
+
if (name) {
|
|
2502
|
+
imported.add(name);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
// Dynamic `import('x')` / `require('x')` calls
|
|
2506
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
2507
|
+
const callee = call.getExpression().getText();
|
|
2508
|
+
if (callee !== 'import' && callee !== 'require') {
|
|
2509
|
+
continue;
|
|
2510
|
+
}
|
|
2511
|
+
const arg = call.getArguments()[0];
|
|
2512
|
+
if (arg && arg.getKind() === SyntaxKind.StringLiteral) {
|
|
2513
|
+
const name = packageNameOf(arg.getLiteralText());
|
|
2514
|
+
if (name) {
|
|
2515
|
+
imported.add(name);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
const lockVersions = parseLockVersions(lockRaw);
|
|
2521
|
+
const added = [];
|
|
2522
|
+
const unresolved = [];
|
|
2523
|
+
for (const name of [...imported].sort()) {
|
|
2524
|
+
if (name in deps || name in devDeps) {
|
|
2525
|
+
continue; // already declared by the starter or the upstream merge
|
|
2526
|
+
}
|
|
2527
|
+
const version = lockVersions[name] || upstreamDeps[name] || upstreamDevDeps[name];
|
|
2528
|
+
if (!version) {
|
|
2529
|
+
unresolved.push(name);
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
deps[name] = version;
|
|
2533
|
+
added.push(`${name}@${version}`);
|
|
2534
|
+
// Add a matching @types/* (for packages that ship no own types) so the
|
|
2535
|
+
// vendored core type-checks. Handles scoped names
|
|
2536
|
+
// (@scope/x → @types/scope__x) too.
|
|
2537
|
+
const typesName = name.startsWith('@') ? `@types/${name.slice(1).replace('/', '__')}` : `@types/${name}`;
|
|
2538
|
+
if (!(typesName in deps) && !(typesName in devDeps)) {
|
|
2539
|
+
const typesVersion = lockVersions[typesName] || upstreamDevDeps[typesName];
|
|
2540
|
+
if (typesVersion) {
|
|
2541
|
+
devDeps[typesName] = typesVersion;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
if (added.length > 0) {
|
|
2546
|
+
print.info(` vendored core closure: added ${added.length} transitive dep(s) → ${added.join(', ')}`);
|
|
2547
|
+
}
|
|
2548
|
+
if (unresolved.length > 0) {
|
|
2549
|
+
print.warning(`⚠ ${unresolved.length} vendored-core import(s) could not be resolved to a version ` +
|
|
2550
|
+
`(absent from the upstream lock and package.json): ${unresolved.join(', ')}. ` +
|
|
2551
|
+
'Add them to projects/api/package.json manually if the build fails.');
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2344
2554
|
/**
|
|
2345
2555
|
* Checks whether an import specifier resolves to the vendored core directory.
|
|
2346
2556
|
* Used during vendor→npm import rewriting.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_CODEX_MARKETPLACE_ROOT = void 0;
|
|
4
|
+
exports.findCodexCli = findCodexCli;
|
|
5
|
+
exports.runCodexCommand = runCodexCommand;
|
|
6
|
+
/**
|
|
7
|
+
* Codex CLI utilities.
|
|
8
|
+
*/
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
const os_1 = require("os");
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
exports.DEFAULT_CODEX_MARKETPLACE_ROOT = process.env.LT_CODEX_MARKETPLACE_ROOT || '/Users/kaihaase/code/lenneTech/codex';
|
|
14
|
+
function findCodexCli() {
|
|
15
|
+
const possiblePaths = [
|
|
16
|
+
(0, path_1.join)((0, os_1.homedir)(), '.local', 'bin', 'codex'),
|
|
17
|
+
(0, path_1.join)((0, os_1.homedir)(), '.codex', 'bin', 'codex'),
|
|
18
|
+
'/usr/local/bin/codex',
|
|
19
|
+
'/opt/homebrew/bin/codex',
|
|
20
|
+
'/usr/bin/codex',
|
|
21
|
+
];
|
|
22
|
+
for (const path of possiblePaths) {
|
|
23
|
+
if ((0, fs_1.existsSync)(path)) {
|
|
24
|
+
return path;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const result = (0, child_process_1.spawnSync)('which', ['codex'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
29
|
+
const path = (result.stdout || '').trim();
|
|
30
|
+
if (result.status === 0 && path && (0, fs_1.existsSync)(path)) {
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (_a) {
|
|
35
|
+
// Codex CLI not found in PATH.
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function runCodexCommand(cli, args) {
|
|
40
|
+
try {
|
|
41
|
+
const result = (0, child_process_1.spawnSync)(cli, args, {
|
|
42
|
+
encoding: 'utf-8',
|
|
43
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
output: result.stdout + result.stderr,
|
|
47
|
+
success: result.status === 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return {
|
|
52
|
+
output: err.message,
|
|
53
|
+
success: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMPTY_CODEX_PLUGIN_CONTENTS = void 0;
|
|
4
|
+
exports.installCodexAgents = installCodexAgents;
|
|
5
|
+
exports.installCodexPrompts = installCodexPrompts;
|
|
6
|
+
exports.readCodexMarketplaceName = readCodexMarketplaceName;
|
|
7
|
+
exports.readLocalCodexPluginContents = readLocalCodexPluginContents;
|
|
8
|
+
/**
|
|
9
|
+
* Codex plugin setup helpers.
|
|
10
|
+
*/
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
const os_1 = require("os");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
const json_utils_1 = require("./json-utils");
|
|
15
|
+
exports.EMPTY_CODEX_PLUGIN_CONTENTS = {
|
|
16
|
+
agents: [],
|
|
17
|
+
hooks: 0,
|
|
18
|
+
mcpServers: [],
|
|
19
|
+
prompts: [],
|
|
20
|
+
skills: [],
|
|
21
|
+
};
|
|
22
|
+
function installCodexAgents(pluginRoot) {
|
|
23
|
+
const sourceDir = (0, path_1.join)(pluginRoot, 'codex-agents');
|
|
24
|
+
if (!(0, fs_1.existsSync)(sourceDir)) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const targetDir = (0, path_1.join)((0, os_1.homedir)(), '.codex', 'agents');
|
|
28
|
+
(0, fs_1.mkdirSync)(targetDir, { recursive: true });
|
|
29
|
+
let count = 0;
|
|
30
|
+
for (const entry of (0, fs_1.readdirSync)(sourceDir, { withFileTypes: true })) {
|
|
31
|
+
if (!entry.isFile() || !entry.name.endsWith('.toml'))
|
|
32
|
+
continue;
|
|
33
|
+
(0, fs_1.copyFileSync)((0, path_1.join)(sourceDir, entry.name), (0, path_1.join)(targetDir, entry.name));
|
|
34
|
+
count += 1;
|
|
35
|
+
}
|
|
36
|
+
return count;
|
|
37
|
+
}
|
|
38
|
+
function installCodexPrompts(pluginRoot) {
|
|
39
|
+
const sourceDir = (0, path_1.join)(pluginRoot, 'prompts');
|
|
40
|
+
if (!(0, fs_1.existsSync)(sourceDir)) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
const targetDir = (0, path_1.join)((0, os_1.homedir)(), '.codex', 'prompts');
|
|
44
|
+
(0, fs_1.mkdirSync)(targetDir, { recursive: true });
|
|
45
|
+
let count = 0;
|
|
46
|
+
for (const entry of (0, fs_1.readdirSync)(sourceDir, { withFileTypes: true })) {
|
|
47
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
48
|
+
continue;
|
|
49
|
+
(0, fs_1.copyFileSync)((0, path_1.join)(sourceDir, entry.name), (0, path_1.join)(targetDir, entry.name));
|
|
50
|
+
count += 1;
|
|
51
|
+
}
|
|
52
|
+
return count;
|
|
53
|
+
}
|
|
54
|
+
function readCodexMarketplaceName(root) {
|
|
55
|
+
const path = (0, path_1.join)(root, '.agents', 'plugins', 'marketplace.json');
|
|
56
|
+
if (!(0, fs_1.existsSync)(path)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const parsed = (0, json_utils_1.safeJsonParse)((0, fs_1.readFileSync)(path, 'utf-8'));
|
|
60
|
+
return (parsed === null || parsed === void 0 ? void 0 : parsed.name) || null;
|
|
61
|
+
}
|
|
62
|
+
function readLocalCodexPluginContents(pluginRoot) {
|
|
63
|
+
const result = Object.assign({}, exports.EMPTY_CODEX_PLUGIN_CONTENTS);
|
|
64
|
+
const skillsDir = (0, path_1.join)(pluginRoot, 'skills');
|
|
65
|
+
if ((0, fs_1.existsSync)(skillsDir)) {
|
|
66
|
+
result.skills = (0, fs_1.readdirSync)(skillsDir, { withFileTypes: true })
|
|
67
|
+
.filter((entry) => entry.isDirectory())
|
|
68
|
+
.map((entry) => entry.name)
|
|
69
|
+
.sort();
|
|
70
|
+
}
|
|
71
|
+
const agentsDir = (0, path_1.join)(pluginRoot, 'codex-agents');
|
|
72
|
+
if ((0, fs_1.existsSync)(agentsDir)) {
|
|
73
|
+
result.agents = (0, fs_1.readdirSync)(agentsDir, { withFileTypes: true })
|
|
74
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.toml'))
|
|
75
|
+
.map((entry) => entry.name.replace(/\.toml$/, ''))
|
|
76
|
+
.sort();
|
|
77
|
+
}
|
|
78
|
+
const promptsDir = (0, path_1.join)(pluginRoot, 'prompts');
|
|
79
|
+
if ((0, fs_1.existsSync)(promptsDir)) {
|
|
80
|
+
result.prompts = (0, fs_1.readdirSync)(promptsDir, { withFileTypes: true })
|
|
81
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
82
|
+
.map((entry) => entry.name.replace(/\.md$/, ''))
|
|
83
|
+
.sort();
|
|
84
|
+
}
|
|
85
|
+
const hooksPath = (0, path_1.join)(pluginRoot, 'hooks', 'hooks.json');
|
|
86
|
+
if ((0, fs_1.existsSync)(hooksPath)) {
|
|
87
|
+
const parsed = (0, json_utils_1.safeJsonParse)((0, fs_1.readFileSync)(hooksPath, 'utf-8'));
|
|
88
|
+
if (parsed === null || parsed === void 0 ? void 0 : parsed.hooks) {
|
|
89
|
+
for (const groups of Object.values(parsed.hooks)) {
|
|
90
|
+
for (const group of groups) {
|
|
91
|
+
result.hooks += Array.isArray(group.hooks) ? group.hooks.length : 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const mcpPath = (0, path_1.join)(pluginRoot, '.mcp.json');
|
|
97
|
+
if ((0, fs_1.existsSync)(mcpPath)) {
|
|
98
|
+
const parsed = (0, json_utils_1.safeJsonParse)((0, fs_1.readFileSync)(mcpPath, 'utf-8'));
|
|
99
|
+
result.mcpServers = Object.keys((parsed === null || parsed === void 0 ? void 0 : parsed.mcpServers) || {}).sort();
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pickPackageManager = pickPackageManager;
|
|
4
|
+
/**
|
|
5
|
+
* Pick the package manager `lt dev` should drive for a given project dir.
|
|
6
|
+
*
|
|
7
|
+
* `lt dev up` and the related test/ticket flows used to hard-code `pnpm`
|
|
8
|
+
* for every monorepo. That breaks npm-only and yarn-only projects: a
|
|
9
|
+
* `pnpm install` invoked from inside `pnpm start` regenerates a
|
|
10
|
+
* pnpm-lock.yaml (which the project's .gitignore then refuses to track),
|
|
11
|
+
* fails on un-approved build scripts (bcrypt, sharp, esbuild) and exits
|
|
12
|
+
* non-zero — the supervised api/app processes die immediately and
|
|
13
|
+
* `lt dev status` reports them as `dead`.
|
|
14
|
+
*
|
|
15
|
+
* Detection order — highest precedence first:
|
|
16
|
+
* 1. `LT_PM_BIN` env var (generic override).
|
|
17
|
+
* 2. `LT_PNPM_BIN` env var (legacy — kept for backwards compatibility).
|
|
18
|
+
* 3. `pnpm-lock.yaml` in `cwd` → pnpm
|
|
19
|
+
* 4. `yarn.lock` in `cwd` → yarn
|
|
20
|
+
* 5. `package-lock.json` in `cwd` → npm
|
|
21
|
+
* 6. Fallback: `pnpm` (preserves the historical default so nothing
|
|
22
|
+
* breaks for the projects this CLI was originally written for).
|
|
23
|
+
*
|
|
24
|
+
* The detection is per-cwd so a monorepo with a pnpm api + npm app gets
|
|
25
|
+
* the correct command per component.
|
|
26
|
+
*/
|
|
27
|
+
const node_fs_1 = require("node:fs");
|
|
28
|
+
const node_path_1 = require("node:path");
|
|
29
|
+
/**
|
|
30
|
+
* Resolve which package manager to drive for `cwd`. Pure — only filesystem
|
|
31
|
+
* existence checks, no exec. The override env vars take precedence so a
|
|
32
|
+
* CI pipeline can pin the manager without touching the lockfile.
|
|
33
|
+
*
|
|
34
|
+
* `cwd` is expected to be a project root (i.e. where the lockfile lives).
|
|
35
|
+
* For a monorepo with separate api/app dirs, call this once per dir.
|
|
36
|
+
*/
|
|
37
|
+
function pickPackageManager(cwd, env = process.env) {
|
|
38
|
+
const override = env.LT_PM_BIN || env.LT_PNPM_BIN;
|
|
39
|
+
if (override) {
|
|
40
|
+
return buildCommand(override, inferNameFromBin(override));
|
|
41
|
+
}
|
|
42
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(cwd, 'pnpm-lock.yaml'))) {
|
|
43
|
+
return buildCommand('pnpm', 'pnpm');
|
|
44
|
+
}
|
|
45
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(cwd, 'yarn.lock'))) {
|
|
46
|
+
return buildCommand('yarn', 'yarn');
|
|
47
|
+
}
|
|
48
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(cwd, 'package-lock.json'))) {
|
|
49
|
+
return buildCommand('npm', 'npm');
|
|
50
|
+
}
|
|
51
|
+
// Historical default — keep so projects without a lockfile (fresh
|
|
52
|
+
// scaffolds, vendored monorepos) behave exactly as before.
|
|
53
|
+
return buildCommand('pnpm', 'pnpm');
|
|
54
|
+
}
|
|
55
|
+
function buildCommand(bin, name) {
|
|
56
|
+
const isNpm = name === 'npm';
|
|
57
|
+
return {
|
|
58
|
+
bin,
|
|
59
|
+
exec(binary, args = []) {
|
|
60
|
+
// `npm exec` consumes the args before the binary name and treats
|
|
61
|
+
// anything after as its own flags unless separated by `--`. pnpm
|
|
62
|
+
// and yarn route them through unchanged. Without the separator
|
|
63
|
+
// `npm exec playwright test --shard=1/2` would parse `--shard`
|
|
64
|
+
// as an npm option, NOT a Playwright one.
|
|
65
|
+
return isNpm ? ['exec', '--', binary, ...args] : ['exec', binary, ...args];
|
|
66
|
+
},
|
|
67
|
+
installArgs: ['install'],
|
|
68
|
+
name,
|
|
69
|
+
runScript(script, extra = []) {
|
|
70
|
+
// pnpm + yarn accept the bare-script shortcut (`pnpm dev`), npm
|
|
71
|
+
// does not. Using `run <script>` is universally accepted so we
|
|
72
|
+
// route every manager through the same call.
|
|
73
|
+
return ['run', script, ...extra];
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function inferNameFromBin(bin) {
|
|
78
|
+
const lower = bin.toLowerCase();
|
|
79
|
+
if (lower.endsWith('pnpm') || lower.includes('/pnpm')) {
|
|
80
|
+
return 'pnpm';
|
|
81
|
+
}
|
|
82
|
+
if (lower.endsWith('yarn') || lower.includes('/yarn')) {
|
|
83
|
+
return 'yarn';
|
|
84
|
+
}
|
|
85
|
+
if (lower.endsWith('npm') || lower.includes('/npm')) {
|
|
86
|
+
return 'npm';
|
|
87
|
+
}
|
|
88
|
+
return 'unknown';
|
|
89
|
+
}
|
package/build/lib/dev-patches.js
CHANGED
|
@@ -127,7 +127,10 @@ function patchClaudeMd(file, options) {
|
|
|
127
127
|
}
|
|
128
128
|
else {
|
|
129
129
|
const sep = content.endsWith('\n\n') ? '' : content.endsWith('\n') ? '\n' : '\n\n';
|
|
130
|
-
|
|
130
|
+
// No trailing newline: oxfmt strips it from .md files, so emitting one
|
|
131
|
+
// makes a freshly patched CLAUDE.md fail `format:check` (read-only) until
|
|
132
|
+
// the next `check` auto-fix. Keep the block flush with EOF.
|
|
133
|
+
next = `${content}${sep}${block}`;
|
|
131
134
|
}
|
|
132
135
|
if (next === content)
|
|
133
136
|
return { file, patched: false, replacements: 0 };
|
|
@@ -47,6 +47,7 @@ const caddy_1 = require("./caddy");
|
|
|
47
47
|
const dev_env_1 = require("./dev-env");
|
|
48
48
|
const dev_env_bridge_1 = require("./dev-env-bridge");
|
|
49
49
|
const dev_identity_1 = require("./dev-identity");
|
|
50
|
+
const dev_package_manager_1 = require("./dev-package-manager");
|
|
50
51
|
const dev_patches_1 = require("./dev-patches");
|
|
51
52
|
const dev_process_1 = require("./dev-process");
|
|
52
53
|
const dev_project_1 = require("./dev-project");
|
|
@@ -189,15 +190,17 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
189
190
|
dbName,
|
|
190
191
|
identity: testIdentity,
|
|
191
192
|
});
|
|
192
|
-
const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
|
|
193
193
|
const pids = {};
|
|
194
|
-
// --- API: compiled (`node dist`) for stability; fall back to
|
|
195
|
-
// `skipBuild` (sibling shards) reuses the dist the
|
|
194
|
+
// --- API: compiled (`node dist`) for stability; fall back to the project's
|
|
195
|
+
// own dev start script. `skipBuild` (sibling shards) reuses the dist the
|
|
196
|
+
// first shard produced. Per-component PM detection mirrors `lt dev up`:
|
|
197
|
+
// a monorepo with an npm api and a pnpm app must drive each correctly. ---
|
|
196
198
|
if (layout.apiDir && apiPort) {
|
|
199
|
+
const apiPm = (0, dev_package_manager_1.pickPackageManager)(layout.apiDir);
|
|
197
200
|
let build = 0;
|
|
198
201
|
if (!skipBuild) {
|
|
199
202
|
log.info(log.dim('Building API (compiled, for stable long runs) …'));
|
|
200
|
-
build = yield (0, dev_process_1.runChildInherit)(
|
|
203
|
+
build = yield (0, dev_process_1.runChildInherit)(apiPm.bin, apiPm.runScript('build'), { cwd: layout.apiDir, env: process.env });
|
|
201
204
|
}
|
|
202
205
|
const entry = ['dist/src/main.js', 'dist/main.js']
|
|
203
206
|
.map((rel) => (0, path_1.join)(layout.apiDir, rel))
|
|
@@ -212,8 +215,8 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
212
215
|
});
|
|
213
216
|
}
|
|
214
217
|
else {
|
|
215
|
-
log.warn(
|
|
216
|
-
apiSpawn = (0, dev_process_1.spawnDetached)(
|
|
218
|
+
log.warn(`compiled API not available — falling back to \`${apiPm.bin} start\` (ts-node).`);
|
|
219
|
+
apiSpawn = (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), {
|
|
217
220
|
cwd: layout.apiDir,
|
|
218
221
|
env: apiEnv,
|
|
219
222
|
logFile: (0, path_1.join)(layout.root, '.lt-dev', names.apiLog),
|
|
@@ -231,10 +234,14 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
231
234
|
// must be a cross-subdomain DOMAIN cookie — see the project's parseCookieHeader).
|
|
232
235
|
// Rebuilt every run so the suite never hits stale code (no build-skip / reuse). ---
|
|
233
236
|
if (layout.appDir && appPort) {
|
|
237
|
+
const appPm = (0, dev_package_manager_1.pickPackageManager)(layout.appDir);
|
|
234
238
|
let appBuild = 0;
|
|
235
239
|
if (!skipBuild) {
|
|
236
240
|
log.info(log.dim('Building App (nuxt build, for speed + prod-fidelity) …'));
|
|
237
|
-
appBuild = yield (0, dev_process_1.runChildInherit)(
|
|
241
|
+
appBuild = yield (0, dev_process_1.runChildInherit)(appPm.bin, appPm.runScript('build'), {
|
|
242
|
+
cwd: layout.appDir,
|
|
243
|
+
env: devEnv.app.env,
|
|
244
|
+
});
|
|
238
245
|
}
|
|
239
246
|
const appEntry = ['.output/server/index.mjs']
|
|
240
247
|
.map((rel) => (0, path_1.join)(layout.appDir, rel))
|
|
@@ -248,8 +255,8 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
248
255
|
});
|
|
249
256
|
}
|
|
250
257
|
else {
|
|
251
|
-
log.warn(
|
|
252
|
-
appSpawn = (0, dev_process_1.spawnDetached)(
|
|
258
|
+
log.warn(`built app not available — falling back to \`${appPm.bin} dev\` (slower: cold-compiles routes).`);
|
|
259
|
+
appSpawn = (0, dev_process_1.spawnDetached)(appPm.bin, appPm.runScript('dev'), {
|
|
253
260
|
cwd: layout.appDir,
|
|
254
261
|
env: devEnv.app.env,
|
|
255
262
|
logFile: (0, path_1.join)(layout.root, '.lt-dev', names.appLog),
|
|
@@ -340,13 +347,20 @@ function runShardedTestSession(layout, baseIdentity, log, opts) {
|
|
|
340
347
|
// every navigation) without loosening them for serial runs.
|
|
341
348
|
const env = Object.assign(Object.assign({}, ctx.appEnv), { LT_DEV_TEST_SHARDS: String(total), MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` });
|
|
342
349
|
const logFile = (0, path_1.join)(layout.root, '.lt-dev', `shard.${index}.test.log`);
|
|
343
|
-
// Invoke Playwright DIRECTLY via `
|
|
344
|
-
// forwarding option flags through
|
|
345
|
-
// passed the separator on to Playwright, which
|
|
346
|
-
// `--reporter` as file FILTERS (not options) →
|
|
347
|
-
// suite.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
+
// Invoke Playwright DIRECTLY via the manager's `exec` (NOT `<pm> run
|
|
351
|
+
// test:e2e -- …`): forwarding option flags through `<pm> run`'s `--`
|
|
352
|
+
// is unreliable — pnpm passed the separator on to Playwright, which
|
|
353
|
+
// then read `--shard` / `--reporter` as file FILTERS (not options) →
|
|
354
|
+
// every shard ran the whole suite. `<pm> exec` hands args straight
|
|
355
|
+
// to the binary (mirrors CI); the helper inserts `--` for npm so
|
|
356
|
+
// those flags don't get re-parsed as npm's own.
|
|
357
|
+
const args = opts.pm.exec('playwright', [
|
|
358
|
+
'test',
|
|
359
|
+
`--shard=${index}/${total}`,
|
|
360
|
+
'--reporter=line',
|
|
361
|
+
...opts.forwarded,
|
|
362
|
+
]);
|
|
363
|
+
const code = yield (0, dev_process_1.runChildToFile)(opts.pm.bin, args, { cwd: appDir, env, logFile });
|
|
350
364
|
return { code, index, logFile };
|
|
351
365
|
})));
|
|
352
366
|
// Aggregate per-shard exit codes into a single result.
|
package/build/lib/dev-ticket.js
CHANGED
|
@@ -8,8 +8,8 @@ exports.dropDatabase = dropDatabase;
|
|
|
8
8
|
exports.gitBranchExists = gitBranchExists;
|
|
9
9
|
exports.gitFetch = gitFetch;
|
|
10
10
|
exports.gitMainRepoRoot = gitMainRepoRoot;
|
|
11
|
+
exports.installWorktreeDeps = installWorktreeDeps;
|
|
11
12
|
exports.listWorktrees = listWorktrees;
|
|
12
|
-
exports.pnpmInstall = pnpmInstall;
|
|
13
13
|
exports.readTicketMarker = readTicketMarker;
|
|
14
14
|
exports.resolveDevIdentity = resolveDevIdentity;
|
|
15
15
|
exports.worktreeAdd = worktreeAdd;
|
|
@@ -43,6 +43,7 @@ const fs_1 = require("fs");
|
|
|
43
43
|
const os_1 = require("os");
|
|
44
44
|
const path_1 = require("path");
|
|
45
45
|
const dev_identity_1 = require("./dev-identity");
|
|
46
|
+
const dev_package_manager_1 = require("./dev-package-manager");
|
|
46
47
|
const dev_patches_1 = require("./dev-patches");
|
|
47
48
|
const dev_project_1 = require("./dev-project");
|
|
48
49
|
const dev_state_1 = require("./dev-state");
|
|
@@ -183,6 +184,16 @@ function gitMainRepoRoot(cwd) {
|
|
|
183
184
|
const commonDir = git(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
184
185
|
return (0, path_1.dirname)(commonDir);
|
|
185
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Install dependencies in a freshly-created worktree. Auto-detects the
|
|
189
|
+
* project's package manager from its lockfile (pnpm hard-links from the
|
|
190
|
+
* shared store → fast; npm + yarn install normally). Falls back to pnpm
|
|
191
|
+
* for fresh scaffolds without a lockfile yet.
|
|
192
|
+
*/
|
|
193
|
+
function installWorktreeDeps(dir) {
|
|
194
|
+
const pm = (0, dev_package_manager_1.pickPackageManager)(dir);
|
|
195
|
+
(0, child_process_1.execFileSync)(pm.bin, pm.installArgs, { cwd: dir, stdio: 'inherit' });
|
|
196
|
+
}
|
|
186
197
|
/** List all worktrees of the repo (parsed from `git worktree list --porcelain`). */
|
|
187
198
|
function listWorktrees(repoDir) {
|
|
188
199
|
let out = '';
|
|
@@ -208,11 +219,6 @@ function listWorktrees(repoDir) {
|
|
|
208
219
|
result.push(finalizeWorktree(current));
|
|
209
220
|
return result;
|
|
210
221
|
}
|
|
211
|
-
/** Install dependencies in a freshly-created worktree (pnpm hard-links from the shared store → fast). */
|
|
212
|
-
function pnpmInstall(dir) {
|
|
213
|
-
const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
|
|
214
|
-
(0, child_process_1.execFileSync)(pnpmBin, ['install'], { cwd: dir, stdio: 'inherit' });
|
|
215
|
-
}
|
|
216
222
|
/** Read the ticket id this worktree is tagged with, or null. */
|
|
217
223
|
function readTicketMarker(root) {
|
|
218
224
|
const file = (0, path_1.join)(root, dev_state_1.paths.sessionDir, TICKET_MARKER);
|
|
@@ -210,9 +210,14 @@ function upsertVendorBlock(content, marker, newBlock) {
|
|
|
210
210
|
}
|
|
211
211
|
return content.replace(blockRegex(marker), newBlock);
|
|
212
212
|
}
|
|
213
|
-
/** Join block lines into the canonical `marker … --- ` shape (ends with `---\n`). */
|
|
213
|
+
/** Join block lines into the canonical `marker … --- ` shape (ends with `---\n\n`). */
|
|
214
214
|
function block(lines) {
|
|
215
|
-
|
|
215
|
+
// Blank line AFTER the `---` too, so the prepended block is separated from
|
|
216
|
+
// the following template heading by an empty line — oxfmt requires a blank
|
|
217
|
+
// line between a thematic break and a heading, otherwise `format:check`
|
|
218
|
+
// fails on a freshly vendored CLAUDE.md. blockRegex's `---\s*\n?` absorbs
|
|
219
|
+
// the extra newline, so removeVendorBlock stays round-trip-safe.
|
|
220
|
+
return [...lines, '', '---', '', ''].join('\n');
|
|
216
221
|
}
|
|
217
222
|
/** Build the regex that matches an existing block from its marker to the first `---`. */
|
|
218
223
|
function blockRegex(marker) {
|