@lenne.tech/cli 1.34.0 → 1.35.1
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/README.md +6 -5
- package/build/commands/config/validate.js +0 -4
- package/build/commands/deployment/create.js +218 -213
- package/build/commands/dev/init.js +2 -1
- package/build/commands/dev/status.js +2 -0
- package/build/commands/dev/test.js +2 -2
- package/build/commands/dev/up.js +8 -4
- package/build/commands/frontend/nuxt.js +6 -0
- package/build/commands/fullstack/add-api.js +6 -9
- package/build/commands/fullstack/add-app.js +5 -7
- package/build/commands/fullstack/init.js +8 -9
- package/build/commands/status.js +2 -0
- package/build/commands/ticket/list.js +2 -0
- package/build/extensions/server.js +140 -18
- package/build/lib/angular-environments.js +108 -0
- package/build/lib/dev-env.js +2 -2
- package/build/lib/dev-identity.js +38 -18
- package/build/lib/dev-project.js +20 -7
- package/build/lib/dev-state.js +17 -1
- package/build/lib/dev-test-session.js +31 -3
- package/build/lib/ensure-root-dockerignore.js +92 -0
- package/build/lib/fs-utils.js +19 -0
- package/build/lib/hoist-workspace-pnpm-config.js +2 -11
- package/build/lib/remove-nested-lockfiles.js +53 -0
- package/build/lib/turboops-config.js +93 -0
- package/build/lib/workspace-integration.js +96 -3
- package/build/templates/deployment/turboops.json.ejs +3 -0
- package/build/templates/vendor-scripts/migrate-store.js +25 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +1 -1
- package/docs/commands.md +30 -9
- package/docs/lt.config.md +9 -19
- package/package.json +1 -1
- package/build/templates/deployment/.github/workflows/pre-release.yml.ejs +0 -41
- package/build/templates/deployment/.github/workflows/release.yml.ejs +0 -41
- package/build/templates/deployment/.gitlab-ci.yml.ejs +0 -181
- package/build/templates/deployment/Dockerfile.app.ejs +0 -13
- package/build/templates/deployment/Dockerfile.ejs +0 -18
- package/build/templates/deployment/docker-compose.dev.yml.ejs +0 -99
- package/build/templates/deployment/docker-compose.prod.yml.ejs +0 -92
- package/build/templates/deployment/docker-compose.test.yml.ejs +0 -98
- package/build/templates/deployment/scripts/build-push.sh.ejs +0 -20
- package/build/templates/deployment/scripts/deploy.sh.ejs +0 -7
package/README.md
CHANGED
|
@@ -170,13 +170,14 @@ $ lt fullstack update
|
|
|
170
170
|
|
|
171
171
|
### Vendor-mode housekeeping
|
|
172
172
|
|
|
173
|
-
Vendor-mode projects
|
|
173
|
+
Vendor-mode projects carry no separate maintenance scripts under
|
|
174
|
+
`scripts/vendor/` anymore. The vendor-specific surface is now:
|
|
174
175
|
|
|
175
|
-
|
|
|
176
|
+
| Concern | Where | Invocation |
|
|
176
177
|
|---|---|---|
|
|
177
|
-
| `check
|
|
178
|
-
|
|
|
179
|
-
|
|
|
178
|
+
| Upstream freshness | Inline `check:vendor-freshness` script in `package.json` — reads the `src/core/VENDOR.md` baseline, compares against the npm registry, non-blocking | auto-invoked by `pnpm run check` / `check:fix` / `check:naf`, or `pnpm run check:vendor-freshness` |
|
|
179
|
+
| Sync from upstream | `nest-server-core-updater` Claude Code agent | `/lt-dev:backend:update-nest-server-core` |
|
|
180
|
+
| Propose local changes upstream | `nest-server-core-contributor` Claude Code agent | `/lt-dev:backend:contribute-nest-server-core` |
|
|
180
181
|
|
|
181
182
|
The vendor-mode baseline (upstream version + commit SHA) is recorded in
|
|
182
183
|
`src/core/VENDOR.md`. Log any substantial local patch there so the updater
|
|
@@ -9,276 +9,281 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.help = void 0;
|
|
12
13
|
const path_1 = require("path");
|
|
14
|
+
const angular_environments_1 = require("../../lib/angular-environments");
|
|
15
|
+
const turboops_config_1 = require("../../lib/turboops-config");
|
|
16
|
+
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
13
17
|
/**
|
|
14
|
-
* Create
|
|
18
|
+
* Create the TurboOps deployment config for a fullstack monorepo.
|
|
19
|
+
*
|
|
20
|
+
* The generic deployment files already ship with the monorepo template:
|
|
21
|
+
* - projects/api/Dockerfile + projects/app/Dockerfile (from the starters)
|
|
22
|
+
* - docker-compose.yml + .gitlab-ci.yml (TurboOps build + `turbo deploy`)
|
|
23
|
+
* so this command only writes the project-specific `.turboops.json` and prints the
|
|
24
|
+
* one-time TurboOps setup checklist (project, stages, CI/CD vars, DNS, stage env).
|
|
25
|
+
*
|
|
26
|
+
* The file is MERGED, never clobbered: any keys a user added by hand survive a
|
|
27
|
+
* re-run, and an unchanged file is left untouched.
|
|
28
|
+
*
|
|
29
|
+
* Angular apps additionally get their `environment.{prod,develop,test}.ts`
|
|
30
|
+
* rewritten to the deployed stage URLs (see lib/angular-environments.ts) — a
|
|
31
|
+
* Nuxt app reads those from the stage env at runtime, an Angular bundle bakes
|
|
32
|
+
* them in at build time. No-op for Nuxt projects.
|
|
33
|
+
*
|
|
34
|
+
* Legacy note: this used to generate a Docker-Swarm deployment (root
|
|
35
|
+
* Dockerfile/Dockerfile.app, docker-compose.{dev,test,prod}.yml, manual Traefik
|
|
36
|
+
* labels). That is obsolete on the TurboOps stack and has been removed. GitLab
|
|
37
|
+
* is the only supported pipeline — the lt-monorepo template ships
|
|
38
|
+
* `.gitlab-ci.yml`, not a GitHub deploy workflow.
|
|
15
39
|
*/
|
|
40
|
+
exports.help = {
|
|
41
|
+
configuration: 'commands.deployment.domain, commands.deployment.noConfirm, defaults.domain, defaults.noConfirm',
|
|
42
|
+
description: 'Create the TurboOps deployment config (.turboops.json) for a fullstack monorepo',
|
|
43
|
+
examples: [
|
|
44
|
+
'deployment create',
|
|
45
|
+
'deployment create MyProject --domain myproject.lenne.tech --noConfirm',
|
|
46
|
+
'deployment create MyProject --domain myproject.lenne.tech --project myproject',
|
|
47
|
+
],
|
|
48
|
+
features: [
|
|
49
|
+
'Writes `.turboops.json` to the workspace root (merges into an existing file)',
|
|
50
|
+
'Prints the one-time TurboOps setup checklist (project, stages, CI/CD vars, DNS, stage env)',
|
|
51
|
+
],
|
|
52
|
+
name: 'create',
|
|
53
|
+
options: [
|
|
54
|
+
{
|
|
55
|
+
description: 'Main domain of the project (e.g. myproject.lenne.tech)',
|
|
56
|
+
flag: '--domain',
|
|
57
|
+
required: false,
|
|
58
|
+
type: 'string',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
description: 'TurboOps project slug (registry namespace + login user). Default: kebab-case project name',
|
|
62
|
+
flag: '--project',
|
|
63
|
+
required: false,
|
|
64
|
+
type: 'string',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
default: false,
|
|
68
|
+
description: 'Skip confirmation prompts; resolve every value from CLI flags, lt.config, or defaults',
|
|
69
|
+
flag: '--noConfirm',
|
|
70
|
+
required: false,
|
|
71
|
+
type: 'boolean',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
16
75
|
const NewCommand = {
|
|
17
76
|
alias: ['dc'],
|
|
18
|
-
description: 'Create
|
|
77
|
+
description: 'Create TurboOps config',
|
|
19
78
|
hidden: false,
|
|
20
79
|
name: 'create',
|
|
21
80
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
22
|
-
var _a, _b, _c, _d, _e, _f, _g
|
|
23
|
-
|
|
24
|
-
const
|
|
81
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
82
|
+
const { config, filesystem, helper, parameters, print: { error, info, spin, success, warning }, prompt: { confirm }, strings: { kebabCase, pascalCase }, system, template, } = toolbox;
|
|
83
|
+
const fail = (message, result) => {
|
|
84
|
+
error(message);
|
|
85
|
+
if (!parameters.options.fromGluegunMenu)
|
|
86
|
+
process.exit(1);
|
|
87
|
+
return result;
|
|
88
|
+
};
|
|
25
89
|
// Load configuration
|
|
26
90
|
const ltConfig = config.loadConfig();
|
|
27
91
|
const configDomain = (_b = (_a = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _a === void 0 ? void 0 : _a.deployment) === null || _b === void 0 ? void 0 : _b.domain;
|
|
28
|
-
const configGitHub = (_d = (_c = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _c === void 0 ? void 0 : _c.deployment) === null || _d === void 0 ? void 0 : _d.gitHub;
|
|
29
|
-
const configGitLab = (_f = (_e = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _e === void 0 ? void 0 : _e.deployment) === null || _f === void 0 ? void 0 : _f.gitLab;
|
|
30
|
-
const configTestRunner = (_h = (_g = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _g === void 0 ? void 0 : _g.deployment) === null || _h === void 0 ? void 0 : _h.testRunner;
|
|
31
|
-
const configProdRunner = (_k = (_j = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _j === void 0 ? void 0 : _j.deployment) === null || _k === void 0 ? void 0 : _k.prodRunner;
|
|
32
|
-
// Load global defaults
|
|
33
92
|
const globalDomain = config.getGlobalDefault(ltConfig, 'domain');
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const cliProdRunner = parameters.options.prodRunner;
|
|
40
|
-
const cliNoConfirm = parameters.options.noConfirm;
|
|
41
|
-
// Determine noConfirm with priority: CLI > command > parent > global > default
|
|
93
|
+
const cliDomain = (0, turboops_config_1.optionString)(parameters.options.domain);
|
|
94
|
+
const cliProject = (0, turboops_config_1.optionString)(parameters.options.project);
|
|
95
|
+
// Priority: CLI > command config > global default > false. When set, every
|
|
96
|
+
// value must resolve from a flag, lt.config, or a default — no prompts, so
|
|
97
|
+
// AI agents and CI never block on an interactive question.
|
|
42
98
|
const noConfirm = config.getNoConfirm({
|
|
43
|
-
cliValue:
|
|
44
|
-
commandConfig: (
|
|
99
|
+
cliValue: parameters.options.noConfirm,
|
|
100
|
+
commandConfig: (_c = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _c === void 0 ? void 0 : _c.deployment,
|
|
45
101
|
config: ltConfig,
|
|
46
102
|
});
|
|
47
103
|
// Start timer
|
|
48
104
|
const timer = system.startTimer();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
105
|
+
info('Create TurboOps deployment config');
|
|
106
|
+
// `.turboops.json` must sit in the repo root — `.gitlab-ci.yml` reads it
|
|
107
|
+
// from there. Resolve the workspace root so running this from inside
|
|
108
|
+
// `projects/api/` still writes to the right place.
|
|
109
|
+
const cwd = filesystem.cwd();
|
|
110
|
+
const subProject = (0, workspace_integration_1.detectSubProjectContext)(cwd, filesystem);
|
|
111
|
+
const projectRoot = (_e = (_d = subProject === null || subProject === void 0 ? void 0 : subProject.workspaceRoot) !== null && _d !== void 0 ? _d : (0, workspace_integration_1.findWorkspaceRoot)(cwd, filesystem)) !== null && _e !== void 0 ? _e : cwd;
|
|
112
|
+
if (projectRoot !== cwd) {
|
|
113
|
+
info(`Using workspace root: ${projectRoot}`);
|
|
114
|
+
}
|
|
115
|
+
// Default project name from lt.json / package.json. Read-only: `patching.update`
|
|
116
|
+
// would re-serialize and rewrite the file just to look at one field. A
|
|
117
|
+
// malformed file is tolerated (best-effort default), never fatal.
|
|
52
118
|
let projectName = '';
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
projectName = data.name;
|
|
57
|
-
return data;
|
|
58
|
-
});
|
|
119
|
+
const ltName = (_f = (0, turboops_config_1.readJsonObject)(filesystem, (0, path_1.join)(projectRoot, 'lt.json')).value) === null || _f === void 0 ? void 0 : _f.name;
|
|
120
|
+
if (typeof ltName === 'string' && ltName) {
|
|
121
|
+
projectName = ltName;
|
|
59
122
|
}
|
|
60
123
|
if (!projectName) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
124
|
+
const pkgName = (_g = (0, turboops_config_1.readJsonObject)(filesystem, (0, path_1.join)(projectRoot, 'package.json')).value) === null || _g === void 0 ? void 0 : _g.name;
|
|
125
|
+
if (typeof pkgName === 'string' && pkgName) {
|
|
126
|
+
projectName = pascalCase(pkgName);
|
|
127
|
+
}
|
|
65
128
|
}
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
129
|
+
// Project name (CLI arg > detected name > interactive)
|
|
130
|
+
const cliName = (0, turboops_config_1.optionString)(parameters.first);
|
|
131
|
+
const name = cliName !== null && cliName !== void 0 ? cliName : (noConfirm
|
|
132
|
+
? projectName
|
|
133
|
+
: yield helper.getInput('', {
|
|
134
|
+
initial: projectName,
|
|
135
|
+
name: `project name (e.g. ${projectName || 'My new project'})`,
|
|
136
|
+
}));
|
|
71
137
|
if (!name) {
|
|
72
|
-
return
|
|
138
|
+
return fail(noConfirm
|
|
139
|
+
? 'No project name: pass it as the first argument (no lt.json / package.json "name" found).'
|
|
140
|
+
: 'No project name given.', 'deployment create: no name');
|
|
73
141
|
}
|
|
74
|
-
|
|
142
|
+
const slug = kebabCase(name);
|
|
143
|
+
// Main domain (priority: CLI > config > global > interactive/default)
|
|
75
144
|
let domain;
|
|
76
145
|
if (cliDomain) {
|
|
77
146
|
domain = cliDomain;
|
|
78
147
|
}
|
|
79
148
|
else if (configDomain) {
|
|
80
|
-
domain = configDomain.replace('{name}',
|
|
149
|
+
domain = configDomain.replace('{name}', slug);
|
|
81
150
|
info(`Using domain from lt.config commands.deployment: ${domain}`);
|
|
82
151
|
}
|
|
83
152
|
else if (globalDomain) {
|
|
84
|
-
domain = globalDomain.replace('{name}',
|
|
153
|
+
domain = globalDomain.replace('{name}', slug);
|
|
85
154
|
info(`Using domain from lt.config defaults: ${domain}`);
|
|
86
155
|
}
|
|
156
|
+
else if (noConfirm) {
|
|
157
|
+
// Honour the documented `[domain]` positional; only then fall back to the
|
|
158
|
+
// default — otherwise a `--noConfirm` caller's explicit domain is dropped.
|
|
159
|
+
const argDomain = (0, turboops_config_1.optionString)(parameters.second);
|
|
160
|
+
domain = argDomain !== null && argDomain !== void 0 ? argDomain : `${slug}.lenne.tech`;
|
|
161
|
+
info(argDomain ? `Using domain: ${domain}` : `Using default domain: ${domain}`);
|
|
162
|
+
}
|
|
87
163
|
else {
|
|
88
164
|
domain = yield helper.getInput(parameters.second, {
|
|
89
|
-
initial: `${
|
|
90
|
-
name: `main domain of the project (e.g. ${
|
|
165
|
+
initial: `${slug}.lenne.tech`,
|
|
166
|
+
name: `main domain of the project (e.g. ${slug}.lenne.tech)`,
|
|
91
167
|
});
|
|
92
168
|
}
|
|
93
169
|
if (!domain) {
|
|
94
|
-
return;
|
|
170
|
+
return fail('No domain given.', 'deployment create: no domain');
|
|
95
171
|
}
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
172
|
+
// `domain` is written verbatim into generated Angular environment.*.ts (baked
|
|
173
|
+
// into the browser bundle) and into copy-paste checklist lines, so validate it
|
|
174
|
+
// as a hostname — a stray quote / `$` / whitespace would inject into the
|
|
175
|
+
// generated source or emit a non-compiling config. Mirrors the slug check below.
|
|
176
|
+
if (!(0, turboops_config_1.isValidDomain)(domain)) {
|
|
177
|
+
return fail(`Invalid domain "${domain}" — expected a hostname like ${slug}.lenne.tech (letters, digits, dashes, dots).`, 'deployment create: invalid domain');
|
|
100
178
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
179
|
+
// TurboOps project slug (registry namespace + login user). Defaults to the kebab name.
|
|
180
|
+
const rawProject = cliProject !== null && cliProject !== void 0 ? cliProject : (noConfirm ? slug : yield helper.getInput('', { initial: slug, name: `TurboOps project slug (e.g. ${slug})` }));
|
|
181
|
+
if (!rawProject) {
|
|
182
|
+
return fail('No TurboOps project slug given.', 'deployment create: no project');
|
|
106
183
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
184
|
+
// `docker login -u "$TURBOOPS_PROJECT"` and `registry.turbo-ops.de/<project>`
|
|
185
|
+
// only accept a lowercase dash-separated slug.
|
|
186
|
+
const project = kebabCase(rawProject);
|
|
187
|
+
if (!(0, turboops_config_1.isValidProjectSlug)(project)) {
|
|
188
|
+
return fail(`Invalid TurboOps project slug "${rawProject}" — expected lowercase letters, digits and dashes.`, 'deployment create: invalid project slug');
|
|
112
189
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (cliGitLab !== undefined) {
|
|
116
|
-
gitLab = cliGitLab === true || cliGitLab === 'true';
|
|
190
|
+
if (project !== rawProject) {
|
|
191
|
+
warning(`Normalized TurboOps project slug "${rawProject}" → "${project}".`);
|
|
117
192
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
193
|
+
// Write `.turboops.json`. Merge instead of overwrite: the file is committed
|
|
194
|
+
// and a user may have added keys by hand; a re-run must not silently drop them.
|
|
195
|
+
const target = (0, path_1.join)(projectRoot, '.turboops.json');
|
|
196
|
+
const generateSpinner = spin('Generate .turboops.json');
|
|
197
|
+
const current = (0, turboops_config_1.readJsonObject)(filesystem, target);
|
|
198
|
+
const existing = current.value;
|
|
199
|
+
// File exists but did not parse into a usable object (malformed JSON, or a
|
|
200
|
+
// bare null/number/array). Don't silently clobber it.
|
|
201
|
+
if (current.found && !existing) {
|
|
202
|
+
generateSpinner.fail('.turboops.json exists but is not a usable JSON object');
|
|
203
|
+
if (noConfirm || !(yield confirm(`Replace the unusable ${target}?`, false))) {
|
|
204
|
+
return fail(`Refusing to overwrite unusable ${target}.`, 'deployment create: unusable config');
|
|
122
205
|
}
|
|
123
206
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
gitLab = yield confirm('Add GitLab pipeline?');
|
|
129
|
-
}
|
|
130
|
-
// GitLab test runner
|
|
131
|
-
let testRunner;
|
|
132
|
-
let prodRunner;
|
|
133
|
-
if (gitLab) {
|
|
134
|
-
// Determine testRunner with priority: CLI > config > interactive
|
|
135
|
-
if (cliTestRunner) {
|
|
136
|
-
testRunner = cliTestRunner;
|
|
137
|
-
}
|
|
138
|
-
else if (configTestRunner) {
|
|
139
|
-
testRunner = configTestRunner;
|
|
140
|
-
info(`Using test runner from lt.config: ${testRunner}`);
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
testRunner = yield helper.getInput('', {
|
|
144
|
-
initial: 'docker-swarm',
|
|
145
|
-
name: 'runner for test (tag in .gitlab-ci.yml, e.g. docker-swarm)',
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
if (!testRunner) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
// Determine prodRunner with priority: CLI > config > interactive
|
|
152
|
-
if (cliProdRunner) {
|
|
153
|
-
prodRunner = cliProdRunner;
|
|
154
|
-
}
|
|
155
|
-
else if (configProdRunner) {
|
|
156
|
-
prodRunner = configProdRunner;
|
|
157
|
-
info(`Using prod runner from lt.config: ${prodRunner}`);
|
|
207
|
+
if (existing) {
|
|
208
|
+
const merged = (0, turboops_config_1.mergeTurboOpsConfig)(existing, project);
|
|
209
|
+
if (!merged.changed) {
|
|
210
|
+
generateSpinner.succeed('.turboops.json already up to date');
|
|
158
211
|
}
|
|
159
212
|
else {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
213
|
+
if (merged.previousProject && merged.previousProject !== project) {
|
|
214
|
+
warning(`Changing TurboOps project "${merged.previousProject}" → "${project}".`);
|
|
215
|
+
}
|
|
216
|
+
// Trailing newline matches the template-generated form, so re-runs stay
|
|
217
|
+
// byte-stable and never produce a whitespace-only diff.
|
|
218
|
+
filesystem.write(target, `${JSON.stringify(merged.config, null, 2)}\n`);
|
|
219
|
+
generateSpinner.succeed('.turboops.json updated (existing keys preserved)');
|
|
167
220
|
}
|
|
168
221
|
}
|
|
169
|
-
// Set up initial props (to pass into templates)
|
|
170
|
-
const nameCamel = camelCase(name);
|
|
171
|
-
const nameKebab = kebabCase(name);
|
|
172
|
-
const namePascal = pascalCase(name);
|
|
173
|
-
// Check if directory
|
|
174
|
-
const cwd = filesystem.cwd();
|
|
175
|
-
const generateSpinner = spin('Generate files');
|
|
176
|
-
yield template.generate({
|
|
177
|
-
props: { nameCamel, nameKebab, namePascal },
|
|
178
|
-
target: (0, path_1.join)(cwd, 'scripts', 'build-push.sh'),
|
|
179
|
-
template: 'deployment/scripts/build-push.sh.ejs',
|
|
180
|
-
});
|
|
181
|
-
yield template.generate({
|
|
182
|
-
props: { nameCamel, nameKebab, namePascal },
|
|
183
|
-
target: (0, path_1.join)(cwd, 'scripts', 'deploy.sh'),
|
|
184
|
-
template: 'deployment/scripts/deploy.sh.ejs',
|
|
185
|
-
});
|
|
186
|
-
yield template.generate({
|
|
187
|
-
props: { nameCamel, nameKebab, namePascal },
|
|
188
|
-
target: (0, path_1.join)(cwd, 'Dockerfile'),
|
|
189
|
-
template: 'deployment/Dockerfile.ejs',
|
|
190
|
-
});
|
|
191
|
-
yield template.generate({
|
|
192
|
-
props: { nameCamel, nameKebab, namePascal },
|
|
193
|
-
target: (0, path_1.join)(cwd, 'Dockerfile.app'),
|
|
194
|
-
template: 'deployment/Dockerfile.app.ejs',
|
|
195
|
-
});
|
|
196
|
-
yield template.generate({
|
|
197
|
-
props: { nameCamel, nameKebab, namePascal },
|
|
198
|
-
target: (0, path_1.join)(cwd, 'docker-compose.dev.yml'),
|
|
199
|
-
template: 'deployment/docker-compose.dev.yml.ejs',
|
|
200
|
-
});
|
|
201
|
-
yield template.generate({
|
|
202
|
-
props: { nameCamel, nameKebab, namePascal },
|
|
203
|
-
target: (0, path_1.join)(cwd, 'docker-compose.test.yml'),
|
|
204
|
-
template: 'deployment/docker-compose.test.yml.ejs',
|
|
205
|
-
});
|
|
206
|
-
yield template.generate({
|
|
207
|
-
props: { nameCamel, nameKebab, namePascal },
|
|
208
|
-
target: (0, path_1.join)(cwd, 'docker-compose.prod.yml'),
|
|
209
|
-
template: 'deployment/docker-compose.prod.yml.ejs',
|
|
210
|
-
});
|
|
211
|
-
if (gitHub) {
|
|
212
|
-
yield template.generate({
|
|
213
|
-
props: { nameCamel, nameKebab, namePascal, url: domain },
|
|
214
|
-
target: (0, path_1.join)(cwd, '.github', 'workflows', 'pre-release.yml'),
|
|
215
|
-
template: 'deployment/.github/workflows/pre-release.yml.ejs',
|
|
216
|
-
});
|
|
217
|
-
yield template.generate({
|
|
218
|
-
props: { nameCamel, nameKebab, namePascal, url: domain },
|
|
219
|
-
target: (0, path_1.join)(cwd, '.github', 'workflows', 'release.yml'),
|
|
220
|
-
template: 'deployment/.github/workflows/release.yml.ejs',
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
if (gitLab) {
|
|
224
|
-
yield template.generate({
|
|
225
|
-
props: { nameCamel, nameKebab, namePascal, prodRunner, testRunner, url: domain },
|
|
226
|
-
target: (0, path_1.join)(cwd, '.gitlab-ci.yml'),
|
|
227
|
-
template: 'deployment/.gitlab-ci.yml.ejs',
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
generateSpinner.succeed('Files generated');
|
|
231
|
-
const environmentsSpinner = spin('Update app environment files');
|
|
232
|
-
const prodEnv = yield filesystem.exists('projects/app/src/environments/environment.prod.ts');
|
|
233
|
-
if (prodEnv) {
|
|
234
|
-
yield patching.patch('projects/app/src/environments/environment.prod.ts', {
|
|
235
|
-
insert: `https://api.${domain}`,
|
|
236
|
-
replace: new RegExp('http://127.0.0.1:3000', 'g'),
|
|
237
|
-
});
|
|
238
|
-
yield patching.patch('projects/app/src/environments/environment.prod.ts', {
|
|
239
|
-
insert: `wss://api.${domain}`,
|
|
240
|
-
replace: new RegExp('ws://127.0.0.1:3000', 'g'),
|
|
241
|
-
});
|
|
242
|
-
yield patching.patch('projects/app/src/environments/environment.prod.ts', {
|
|
243
|
-
insert: `https://${domain}`,
|
|
244
|
-
replace: new RegExp('http://127.0.0.1:4200', 'g'),
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
222
|
else {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
yield patching.patch('projects/app/src/environments/environment.test.ts', {
|
|
253
|
-
insert: `https://api.test.${domain}`,
|
|
254
|
-
replace: new RegExp('http://127.0.0.1:3000', 'g'),
|
|
255
|
-
});
|
|
256
|
-
yield patching.patch('projects/app/src/environments/environment.test.ts', {
|
|
257
|
-
insert: `wss://api.test.${domain}`,
|
|
258
|
-
replace: new RegExp('ws://127.0.0.1:3000', 'g'),
|
|
259
|
-
});
|
|
260
|
-
yield patching.patch('projects/app/src/environments/environment.test.ts', {
|
|
261
|
-
insert: `https://test.${domain}`,
|
|
262
|
-
replace: new RegExp('http://127.0.0.1:4200', 'g'),
|
|
223
|
+
yield template.generate({
|
|
224
|
+
props: { project },
|
|
225
|
+
target,
|
|
226
|
+
template: 'deployment/turboops.json.ejs',
|
|
263
227
|
});
|
|
228
|
+
generateSpinner.succeed('.turboops.json generated');
|
|
264
229
|
}
|
|
265
|
-
|
|
266
|
-
|
|
230
|
+
// Angular bakes its API URL into the bundle at build time, so the deployed
|
|
231
|
+
// stage URLs must be written into `environment.*.ts`. No-op for Nuxt, which
|
|
232
|
+
// reads them from the TurboOps stage env at runtime. Detect Angular from the
|
|
233
|
+
// presence of the environments dir, NOT from the patch count — a project
|
|
234
|
+
// using non-standard env filenames would otherwise be misread as Nuxt and
|
|
235
|
+
// ship a bundle that still calls localhost.
|
|
236
|
+
const isAngular = (0, angular_environments_1.findAngularEnvironmentsDir)(projectRoot) !== null;
|
|
237
|
+
if (isAngular) {
|
|
238
|
+
const envPatches = (0, angular_environments_1.patchAngularEnvironments)({ domain, projectRoot });
|
|
239
|
+
const changed = envPatches.filter((r) => r.patched);
|
|
240
|
+
if (changed.length > 0) {
|
|
241
|
+
for (const r of changed) {
|
|
242
|
+
success(`patched ${r.replacements}× in ${(0, path_1.relative)(projectRoot, r.file)} (deployed URLs)`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else if (envPatches.some((r) => r.recognized)) {
|
|
246
|
+
info('Angular environment files already point at the deployed URLs.');
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
warning('Angular project detected but no environment.{prod,develop,test}.ts with a rewritable URL was found — ' +
|
|
250
|
+
'set the deployed API/app URLs manually so the bundle does not call localhost.');
|
|
251
|
+
}
|
|
267
252
|
}
|
|
268
|
-
|
|
269
|
-
// We're done, so show what to do next
|
|
253
|
+
// One-time setup checklist
|
|
270
254
|
info('');
|
|
271
|
-
success(`
|
|
255
|
+
success(`TurboOps deployment wired for "${name}" in ${helper.msToMinutesAndSeconds(timer())}m.`);
|
|
272
256
|
info('');
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
257
|
+
info('One-time setup:');
|
|
258
|
+
info(` 1. TurboOps: create project "${project}" with stages "dev" and "production" on your server.`);
|
|
259
|
+
info(` - production domains: ${domain} (app) + api.${domain} (api)`);
|
|
260
|
+
info(` - dev domains: dev.${domain} (app) + api.dev.${domain} (api)`);
|
|
261
|
+
info(` 2. CI/CD variables: TURBOOPS_PROJECT=${project} and TURBOOPS_TOKEN=<token> (masked).`);
|
|
262
|
+
info(' GitLab: Settings > CI/CD > Variables. GitHub: a repo variable + a repo secret.');
|
|
263
|
+
info(' 3. DNS A/AAAA records -> your server IP. A wildcard matches exactly ONE label, so:');
|
|
264
|
+
info(` - ${domain} (apex — NOT covered by a wildcard)`);
|
|
265
|
+
info(` - *.${domain} (covers api.${domain} and dev.${domain})`);
|
|
266
|
+
info(` - *.dev.${domain} (covers api.dev.${domain})`);
|
|
267
|
+
info(' 4. Set the stage env vars in TurboOps (per stage), e.g. for production:');
|
|
268
|
+
info(` NODE_ENV=production, NSC__BASE_URL=https://api.${domain},`);
|
|
269
|
+
info(' NSC__MONGOOSE__URI, NSC__BETTER_AUTH__SECRET, NSC__AI__ENCRYPTION_SECRET,');
|
|
270
|
+
if (isAngular) {
|
|
271
|
+
info(' NSC__EMAIL__SMTP__*, NSC__EMAIL__DEFAULT_SENDER__EMAIL');
|
|
272
|
+
info(' The Angular app needs no URL env vars — they are baked into');
|
|
273
|
+
info(' environment.prod.ts / environment.develop.ts (patched above).');
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
info(' NSC__EMAIL__SMTP__*, NSC__EMAIL__DEFAULT_SENDER__EMAIL,');
|
|
277
|
+
info(` NUXT_PUBLIC_API_URL=https://api.${domain}, NUXT_API_URL=https://api.${domain},`);
|
|
278
|
+
info(` NUXT_PUBLIC_SITE_URL=https://${domain}`);
|
|
278
279
|
}
|
|
279
|
-
|
|
280
|
+
info(isAngular
|
|
281
|
+
? ' 5. Commit `.turboops.json` (read by `turbo deploy`) AND the patched environment.*.ts (baked into the build).'
|
|
282
|
+
: ' 5. Commit `.turboops.json` — `turbo deploy` reads it from the repo root in CI.');
|
|
280
283
|
info('');
|
|
281
|
-
|
|
284
|
+
info('Then: push `dev` -> the dev stage, push `main` -> the production stage');
|
|
285
|
+
info('(.gitlab-ci.yml, or .github/workflows/deploy.yml on GitHub). Tests gate the deploy.');
|
|
286
|
+
if (!parameters.options.fromGluegunMenu) {
|
|
282
287
|
process.exit();
|
|
283
288
|
}
|
|
284
289
|
// For tests
|
|
@@ -47,7 +47,8 @@ const InitCommand = {
|
|
|
47
47
|
const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox;
|
|
48
48
|
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
49
49
|
if (!layout.apiDir && !layout.appDir) {
|
|
50
|
-
error('No API (src/config.env.ts
|
|
50
|
+
error('No lt project detected at this path. Expected an API (src/config.env.ts or nest-cli.json), ' +
|
|
51
|
+
'an App (nuxt.config.ts), or a monorepo with projects/api and/or projects/app.');
|
|
51
52
|
if (!parameters.options.fromGluegunMenu)
|
|
52
53
|
process.exit(1);
|
|
53
54
|
return 'dev init: not a project';
|
|
@@ -128,6 +128,8 @@ const StatusCommand = {
|
|
|
128
128
|
const port = sub === 'api' ? entry.internalPorts.api : entry.internalPorts.app;
|
|
129
129
|
info(` ${sub.padEnd(6)} https://${host}${port ? colors.dim(` → 127.0.0.1:${port}`) : ''}`);
|
|
130
130
|
}
|
|
131
|
+
// `loadRegistry` strips dbName from API-less (App-only) entries, so its
|
|
132
|
+
// presence alone means there is a database to advertise.
|
|
131
133
|
if (entry.dbName)
|
|
132
134
|
info(` db mongodb://127.0.0.1/${entry.dbName}`);
|
|
133
135
|
// Patch status — quick view whether legacy ports are still in source.
|
|
@@ -224,9 +224,9 @@ const TestCommand = {
|
|
|
224
224
|
let exitCode = 1;
|
|
225
225
|
try {
|
|
226
226
|
const ctx = yield (0, dev_test_session_1.bringUpTestSession)(layout, identity, log, { devDbName });
|
|
227
|
-
const env = Object.assign(Object.assign(Object.assign({}, process.env), readBridgeEnv(layout.root)), {
|
|
227
|
+
const env = Object.assign(Object.assign(Object.assign(Object.assign({}, process.env), readBridgeEnv(layout.root)), {
|
|
228
228
|
// Playwright global-setup resets THIS db (allow-listed) before the suite.
|
|
229
|
-
MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` });
|
|
229
|
+
MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` }), (ctx.apiLogPath ? { NEST_SERVER_LOG: ctx.apiLogPath } : {}));
|
|
230
230
|
if (debug) {
|
|
231
231
|
env.PWDEBUG = '1';
|
|
232
232
|
env.HEADED = '1';
|
package/build/commands/dev/up.js
CHANGED
|
@@ -86,7 +86,8 @@ const UpCommand = {
|
|
|
86
86
|
const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox;
|
|
87
87
|
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
88
88
|
if (!layout.apiDir && !layout.appDir) {
|
|
89
|
-
error('No
|
|
89
|
+
error('No lt project detected at this path. Expected an API (src/config.env.ts or nest-cli.json), ' +
|
|
90
|
+
'an App (nuxt.config.ts), or a monorepo with projects/api and/or projects/app.');
|
|
90
91
|
if (!parameters.options.fromGluegunMenu)
|
|
91
92
|
process.exit(1);
|
|
92
93
|
return 'dev up: not a project';
|
|
@@ -271,7 +272,9 @@ const UpCommand = {
|
|
|
271
272
|
apiUpstreamPort: existingEntry === null || existingEntry === void 0 ? void 0 : existingEntry.internalPorts.api,
|
|
272
273
|
appHostname: (_d = identity.subdomains.app) === null || _d === void 0 ? void 0 : _d.hostname,
|
|
273
274
|
appUpstreamPort: existingEntry === null || existingEntry === void 0 ? void 0 : existingEntry.internalPorts.app,
|
|
274
|
-
|
|
275
|
+
// App-only projects have no database — don't advertise a Mongo URL
|
|
276
|
+
// that nothing listens on.
|
|
277
|
+
dbName: identity.subdomains.api ? ((_e = existingEntry === null || existingEntry === void 0 ? void 0 : existingEntry.dbName) !== null && _e !== void 0 ? _e : dbName) : undefined,
|
|
275
278
|
});
|
|
276
279
|
info('Run `lt dev down` first to force a full restart.');
|
|
277
280
|
if (!parameters.options.fromGluegunMenu)
|
|
@@ -317,7 +320,8 @@ const UpCommand = {
|
|
|
317
320
|
info(` app: https://${identity.subdomains.app.hostname} → 127.0.0.1:${appPort}`);
|
|
318
321
|
if (identity.subdomains.api)
|
|
319
322
|
info(` api: https://${identity.subdomains.api.hostname} → 127.0.0.1:${apiPort}`);
|
|
320
|
-
|
|
323
|
+
if (identity.subdomains.api)
|
|
324
|
+
info(` db: mongodb://127.0.0.1/${dbName}`);
|
|
321
325
|
info('');
|
|
322
326
|
// Build env per process.
|
|
323
327
|
const devEnv = (0, dev_env_1.buildDevEnv)({
|
|
@@ -414,7 +418,7 @@ const UpCommand = {
|
|
|
414
418
|
apiUpstreamPort: apiPort,
|
|
415
419
|
appHostname: (_l = identity.subdomains.app) === null || _l === void 0 ? void 0 : _l.hostname,
|
|
416
420
|
appUpstreamPort: appPort,
|
|
417
|
-
dbName,
|
|
421
|
+
dbName: identity.subdomains.api ? dbName : undefined,
|
|
418
422
|
});
|
|
419
423
|
info(colors.dim('Logs: <root>/.lt-dev/api.log, <root>/.lt-dev/app.log'));
|
|
420
424
|
for (const note of rotationNotes)
|
|
@@ -220,6 +220,12 @@ const NewCommand = {
|
|
|
220
220
|
dest: `./${projectDir}`,
|
|
221
221
|
projectName: projectDir,
|
|
222
222
|
});
|
|
223
|
+
// The vendor conversion rewrote package.json (dropped
|
|
224
|
+
// @lenne.tech/nuxt-extensions, added @nuxt/kit) AFTER the initial install,
|
|
225
|
+
// leaving pnpm-lock.yaml stale. Resync it — otherwise the first
|
|
226
|
+
// `pnpm install --frozen-lockfile` (CI, Docker build) fails with
|
|
227
|
+
// ERR_PNPM_OUTDATED_LOCKFILE and the project cannot be built/deployed.
|
|
228
|
+
yield toolbox.system.run(`cd "./${projectDir}" && pnpm install --no-frozen-lockfile`);
|
|
223
229
|
vendorSpinner.succeed('Frontend converted to vendor mode (app/core/)');
|
|
224
230
|
}
|
|
225
231
|
catch (err) {
|