@lenne.tech/cli 1.33.0 → 1.35.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/README.md +6 -5
- 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/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 +12 -6
- package/build/commands/dev/up.js +17 -7
- package/build/commands/frontend/nuxt.js +4 -7
- package/build/commands/fullstack/add-api.js +6 -9
- package/build/commands/fullstack/add-app.js +5 -7
- package/build/commands/fullstack/init.js +34 -20
- package/build/commands/status.js +2 -0
- package/build/commands/ticket/list.js +2 -0
- package/build/commands/ticket/start.js +6 -4
- package/build/extensions/frontend-helper.js +44 -39
- package/build/extensions/server.js +354 -22
- package/build/lib/angular-environments.js +108 -0
- package/build/lib/codex-cli.js +56 -0
- package/build/lib/codex-plugin-utils.js +102 -0
- package/build/lib/dev-identity.js +38 -18
- package/build/lib/dev-package-manager.js +89 -0
- package/build/lib/dev-patches.js +4 -1
- package/build/lib/dev-project.js +20 -7
- package/build/lib/dev-state.js +17 -1
- package/build/lib/dev-test-session.js +61 -19
- package/build/lib/dev-ticket.js +12 -6
- 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/vendor-claude-md.js +7 -2
- 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
|
@@ -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.
|
|
@@ -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
|
}
|
|
@@ -222,9 +224,9 @@ const TestCommand = {
|
|
|
222
224
|
let exitCode = 1;
|
|
223
225
|
try {
|
|
224
226
|
const ctx = yield (0, dev_test_session_1.bringUpTestSession)(layout, identity, log, { devDbName });
|
|
225
|
-
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)), {
|
|
226
228
|
// Playwright global-setup resets THIS db (allow-listed) before the suite.
|
|
227
|
-
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 } : {}));
|
|
228
230
|
if (debug) {
|
|
229
231
|
env.PWDEBUG = '1';
|
|
230
232
|
env.HEADED = '1';
|
|
@@ -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}`);
|