@lenne.tech/cli 1.35.1 → 1.36.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.
|
@@ -15,13 +15,16 @@ const dev_process_1 = require("../../lib/dev-process");
|
|
|
15
15
|
const dev_project_1 = require("../../lib/dev-project");
|
|
16
16
|
const dev_state_1 = require("../../lib/dev-state");
|
|
17
17
|
const dev_ticket_1 = require("../../lib/dev-ticket");
|
|
18
|
+
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
18
19
|
/**
|
|
19
20
|
* `lt ticket start <name>` — spin up a fully-isolated parallel dev environment
|
|
20
21
|
* for a ticket or feature, in seconds:
|
|
21
22
|
*
|
|
22
|
-
* 1. `git fetch` + create a git WORKTREE on a fresh branch from
|
|
23
|
-
* (
|
|
24
|
-
*
|
|
23
|
+
* 1. `git fetch` + create a git WORKTREE on a fresh branch from the repo's base
|
|
24
|
+
* branch (`origin/dev` → `origin/develop` → remote HEAD → …, or `--base`;
|
|
25
|
+
* the user is asked when none of them exists) so every ticket starts
|
|
26
|
+
* independent from the latest integration state, at a sibling folder
|
|
27
|
+
* `<parent>/<slug>-<id>`,
|
|
25
28
|
* 2. tag it with a `.lt-dev/ticket` marker (makes every `lt dev *` in it
|
|
26
29
|
* ticket-aware),
|
|
27
30
|
* 3. `pnpm install` (hard-links from the shared store → fast),
|
|
@@ -63,7 +66,6 @@ const StartCommand = {
|
|
|
63
66
|
const base = (0, dev_identity_1.buildIdentity)(mainRepoRoot);
|
|
64
67
|
const id = (0, dev_ticket_1.deriveTicketId)(name, parameters.options.as != null ? String(parameters.options.as) : undefined);
|
|
65
68
|
const branch = typeof parameters.options.branch === 'string' ? parameters.options.branch : (0, dev_ticket_1.defaultTicketBranch)(name);
|
|
66
|
-
const baseRef = typeof parameters.options.base === 'string' ? parameters.options.base : 'origin/dev';
|
|
67
69
|
const ticketIdentity = (0, dev_identity_1.buildTicketIdentity)(base, id);
|
|
68
70
|
const dbName = (0, dev_project_1.deriveTicketDbName)((0, dev_project_1.deriveDbName)(layout.apiDir, base.slug), id);
|
|
69
71
|
const worktreePath = (0, dev_ticket_1.worktreePathFor)(mainRepoRoot, base.slug, id);
|
|
@@ -80,14 +82,55 @@ const StartCommand = {
|
|
|
80
82
|
}
|
|
81
83
|
info('');
|
|
82
84
|
info(colors.bold(`Starting ticket "${id}" → ${ticketIdentity.slug}`));
|
|
83
|
-
info(colors.dim(` branch: ${branch}
|
|
85
|
+
info(colors.dim(` branch: ${branch}`));
|
|
84
86
|
info(colors.dim(` worktree: ${worktreePath}`));
|
|
85
87
|
info('');
|
|
86
|
-
// 1. fetch
|
|
88
|
+
// 1. fetch, so the base ref below is resolved against the freshest remote
|
|
89
|
+
// state. Best-effort: offline / auth-less repos still get their worktree
|
|
90
|
+
// from whatever refs exist locally.
|
|
91
|
+
info(colors.dim('Fetching …'));
|
|
87
92
|
try {
|
|
88
|
-
info(colors.dim(`Fetching + creating worktree from ${baseRef} …`));
|
|
89
93
|
(0, dev_ticket_1.gitFetch)(mainRepoRoot);
|
|
90
|
-
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
warning(`git fetch failed (${e.message.split('\n')[0]}) — continuing with the local refs.`);
|
|
97
|
+
}
|
|
98
|
+
// 2. resolve the ref the fresh branch starts from. Only needed for a NEW
|
|
99
|
+
// branch — an existing one is simply checked out into the worktree.
|
|
100
|
+
let baseRef = null;
|
|
101
|
+
if (!(0, dev_ticket_1.gitBranchExists)(mainRepoRoot, branch)) {
|
|
102
|
+
const explicit = typeof parameters.options.base === 'string' ? parameters.options.base : undefined;
|
|
103
|
+
const resolution = (0, dev_ticket_1.resolveBaseRef)(mainRepoRoot, explicit);
|
|
104
|
+
baseRef = resolution.ref;
|
|
105
|
+
if (!baseRef) {
|
|
106
|
+
// No base branch found (repos differ: `dev`, `develop`, `main`, …) — ask
|
|
107
|
+
// the user rather than failing on a guess. Non-interactive callers (CI /
|
|
108
|
+
// AI agents) must pass `--base` instead.
|
|
109
|
+
if (resolution.explicit) {
|
|
110
|
+
error(`Base ref "${explicit}" does not exist in this repository.`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
error(`No base branch found — tried: ${resolution.candidates.join(', ')}`);
|
|
114
|
+
}
|
|
115
|
+
if ((0, workspace_integration_1.isNonInteractive)(Boolean(parameters.options.noConfirm))) {
|
|
116
|
+
info(colors.dim(` Pass one explicitly: lt ticket start ${name} --base <ref>`));
|
|
117
|
+
if (!parameters.options.fromGluegunMenu)
|
|
118
|
+
process.exit(1);
|
|
119
|
+
return 'ticket start: no base ref';
|
|
120
|
+
}
|
|
121
|
+
baseRef = yield askForBaseRef(toolbox, mainRepoRoot);
|
|
122
|
+
if (!baseRef) {
|
|
123
|
+
if (!parameters.options.fromGluegunMenu)
|
|
124
|
+
process.exit(1);
|
|
125
|
+
return 'ticket start: no base ref';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
info(colors.dim(` base: ${baseRef}`));
|
|
129
|
+
}
|
|
130
|
+
// 3. worktree (fresh branch from the base ref → independent).
|
|
131
|
+
try {
|
|
132
|
+
info(colors.dim(`Creating worktree${baseRef ? ` from ${baseRef}` : ` on existing branch ${branch}`} …`));
|
|
133
|
+
(0, dev_ticket_1.worktreeAdd)(mainRepoRoot, worktreePath, branch, baseRef !== null && baseRef !== void 0 ? baseRef : '');
|
|
91
134
|
}
|
|
92
135
|
catch (e) {
|
|
93
136
|
error(`git worktree setup failed: ${e.message}`);
|
|
@@ -95,9 +138,9 @@ const StartCommand = {
|
|
|
95
138
|
process.exit(1);
|
|
96
139
|
return 'ticket start: worktree failed';
|
|
97
140
|
}
|
|
98
|
-
//
|
|
141
|
+
// 4. tag the worktree with its ticket id (makes lt dev * ticket-aware).
|
|
99
142
|
(0, dev_ticket_1.writeTicketMarker)(worktreePath, id);
|
|
100
|
-
//
|
|
143
|
+
// 5. install deps. Auto-detects pnpm/yarn/npm from the worktree's
|
|
101
144
|
// lockfile so an npm-only repo doesn't get a foreign pnpm-lock
|
|
102
145
|
// injected on every ticket start.
|
|
103
146
|
if (parameters.options.install !== false) {
|
|
@@ -109,7 +152,7 @@ const StartCommand = {
|
|
|
109
152
|
warning(`install failed (${e.message}) — continuing; run it manually in the worktree.`);
|
|
110
153
|
}
|
|
111
154
|
}
|
|
112
|
-
//
|
|
155
|
+
// 6. bring the isolated stack up (re-invokes THIS lt build so the marker is
|
|
113
156
|
// honoured even before a global recompile). `--no-up` just scaffolds.
|
|
114
157
|
if (parameters.options.up !== false) {
|
|
115
158
|
info('');
|
|
@@ -140,4 +183,47 @@ const StartCommand = {
|
|
|
140
183
|
return `ticket start: ${id}`;
|
|
141
184
|
}),
|
|
142
185
|
};
|
|
186
|
+
/**
|
|
187
|
+
* Ask which ref the ticket branch should start from — the last resort when no
|
|
188
|
+
* base branch was found (repos name theirs differently: `dev`, `develop`,
|
|
189
|
+
* `main`, …). Offers the repo's branches (most recently committed first) plus a
|
|
190
|
+
* free-text escape hatch, and re-asks until the ref actually resolves, so a typo
|
|
191
|
+
* fails here instead of inside `git worktree add`.
|
|
192
|
+
*/
|
|
193
|
+
function askForBaseRef(toolbox, repoDir) {
|
|
194
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
195
|
+
var _a;
|
|
196
|
+
const { print: { colors, info, warning }, prompt, } = toolbox;
|
|
197
|
+
const MANUAL = 'Other — type a ref …';
|
|
198
|
+
const choices = (0, dev_ticket_1.listBaseRefChoices)(repoDir);
|
|
199
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
200
|
+
let ref;
|
|
201
|
+
if (choices.length) {
|
|
202
|
+
const answer = yield prompt.ask({
|
|
203
|
+
choices: [...choices, MANUAL],
|
|
204
|
+
message: 'Which branch should this ticket start from?',
|
|
205
|
+
name: 'ref',
|
|
206
|
+
type: 'select',
|
|
207
|
+
});
|
|
208
|
+
ref = answer.ref;
|
|
209
|
+
}
|
|
210
|
+
// No branches at all (fresh repo), or the user picked the escape hatch.
|
|
211
|
+
if (!choices.length || ref === MANUAL) {
|
|
212
|
+
const answer = yield prompt.ask({
|
|
213
|
+
message: 'Base ref (branch, tag or commit):',
|
|
214
|
+
name: 'ref',
|
|
215
|
+
type: 'input',
|
|
216
|
+
});
|
|
217
|
+
ref = (_a = answer.ref) === null || _a === void 0 ? void 0 : _a.trim();
|
|
218
|
+
}
|
|
219
|
+
if (!ref)
|
|
220
|
+
return null; // aborted / empty
|
|
221
|
+
if ((0, dev_ticket_1.gitRefExists)(repoDir, ref))
|
|
222
|
+
return ref;
|
|
223
|
+
warning(`"${ref}" does not resolve to a commit — pick another one.`);
|
|
224
|
+
}
|
|
225
|
+
info(colors.dim(' No usable base ref — aborting.'));
|
|
226
|
+
return null;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
143
229
|
module.exports = StartCommand;
|
package/build/lib/dev-ticket.js
CHANGED
|
@@ -8,9 +8,12 @@ exports.dropDatabase = dropDatabase;
|
|
|
8
8
|
exports.gitBranchExists = gitBranchExists;
|
|
9
9
|
exports.gitFetch = gitFetch;
|
|
10
10
|
exports.gitMainRepoRoot = gitMainRepoRoot;
|
|
11
|
+
exports.gitRefExists = gitRefExists;
|
|
11
12
|
exports.installWorktreeDeps = installWorktreeDeps;
|
|
13
|
+
exports.listBaseRefChoices = listBaseRefChoices;
|
|
12
14
|
exports.listWorktrees = listWorktrees;
|
|
13
15
|
exports.readTicketMarker = readTicketMarker;
|
|
16
|
+
exports.resolveBaseRef = resolveBaseRef;
|
|
14
17
|
exports.resolveDevIdentity = resolveDevIdentity;
|
|
15
18
|
exports.worktreeAdd = worktreeAdd;
|
|
16
19
|
exports.worktreeDirtyOnlyAutoDiscardable = worktreeDirtyOnlyAutoDiscardable;
|
|
@@ -24,8 +27,9 @@ exports.writeTicketMarker = writeTicketMarker;
|
|
|
24
27
|
* command group).
|
|
25
28
|
*
|
|
26
29
|
* The model: ONE git repo, N git worktrees — one per ticket/feature — each on
|
|
27
|
-
* its own branch (created fresh from
|
|
28
|
-
*
|
|
30
|
+
* its own branch (created fresh from the repo's base branch — see
|
|
31
|
+
* {@link resolveBaseRef} — so tickets are independent), each running its own
|
|
32
|
+
* `lt dev` stack on a SUFFIXED identity:
|
|
29
33
|
*
|
|
30
34
|
* ticket "DEV-2200" → id "2200" → svl-2200.localhost / api.svl-2200.localhost
|
|
31
35
|
* worktree <parent>/svl-2200/ branch feat/DEV-2200
|
|
@@ -184,6 +188,16 @@ function gitMainRepoRoot(cwd) {
|
|
|
184
188
|
const commonDir = git(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
185
189
|
return (0, path_1.dirname)(commonDir);
|
|
186
190
|
}
|
|
191
|
+
/** True if `ref` resolves to a commit in the repo (any ref kind: local, remote, tag, sha). */
|
|
192
|
+
function gitRefExists(repoDir, ref) {
|
|
193
|
+
try {
|
|
194
|
+
(0, child_process_1.execFileSync)('git', ['-C', repoDir, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { stdio: 'ignore' });
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
catch (_a) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
187
201
|
/**
|
|
188
202
|
* Install dependencies in a freshly-created worktree. Auto-detects the
|
|
189
203
|
* project's package manager from its lockfile (pnpm hard-links from the
|
|
@@ -194,6 +208,31 @@ function installWorktreeDeps(dir) {
|
|
|
194
208
|
const pm = (0, dev_package_manager_1.pickPackageManager)(dir);
|
|
195
209
|
(0, child_process_1.execFileSync)(pm.bin, pm.installArgs, { cwd: dir, stdio: 'inherit' });
|
|
196
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Branches offered when the user has to pick a base ref interactively (no
|
|
213
|
+
* candidate matched). Remote + local branches, most recently committed first,
|
|
214
|
+
* `origin/HEAD` filtered out (it is an alias, not a branch).
|
|
215
|
+
*/
|
|
216
|
+
function listBaseRefChoices(repoDir, limit = 25) {
|
|
217
|
+
let out = '';
|
|
218
|
+
try {
|
|
219
|
+
out = git(repoDir, [
|
|
220
|
+
'for-each-ref',
|
|
221
|
+
'--sort=-committerdate',
|
|
222
|
+
'--format=%(refname:short)',
|
|
223
|
+
'refs/remotes',
|
|
224
|
+
'refs/heads',
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
catch (_a) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
const refs = out
|
|
231
|
+
.split(/\r?\n/)
|
|
232
|
+
.map((l) => l.trim())
|
|
233
|
+
.filter((l) => l && !l.endsWith('/HEAD'));
|
|
234
|
+
return [...new Set(refs)].slice(0, limit);
|
|
235
|
+
}
|
|
197
236
|
/** List all worktrees of the repo (parsed from `git worktree list --porcelain`). */
|
|
198
237
|
function listWorktrees(repoDir) {
|
|
199
238
|
let out = '';
|
|
@@ -232,6 +271,29 @@ function readTicketMarker(root) {
|
|
|
232
271
|
return null;
|
|
233
272
|
}
|
|
234
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Resolve the ref a fresh ticket branch is created from.
|
|
276
|
+
*
|
|
277
|
+
* An explicit `--base` always wins (and is reported as missing when it does not
|
|
278
|
+
* resolve). Otherwise the repo's base branch is DISCOVERED, because it is not
|
|
279
|
+
* called the same everywhere — `nest-server` uses `develop`, the lt starters use
|
|
280
|
+
* `dev`, GitHub defaults to `main`:
|
|
281
|
+
*
|
|
282
|
+
* origin/dev → origin/develop → the remote's HEAD → origin/main → origin/master
|
|
283
|
+
* → the same names as LOCAL branches (repo without a remote)
|
|
284
|
+
*
|
|
285
|
+
* `ref: null` means none of them exists — the caller then asks the user
|
|
286
|
+
* (`lt ticket start`) instead of failing on a hard-coded `origin/dev`.
|
|
287
|
+
*/
|
|
288
|
+
function resolveBaseRef(repoDir, explicit) {
|
|
289
|
+
var _a;
|
|
290
|
+
const wanted = explicit === null || explicit === void 0 ? void 0 : explicit.trim();
|
|
291
|
+
if (wanted) {
|
|
292
|
+
return { candidates: [wanted], explicit: true, ref: gitRefExists(repoDir, wanted) ? wanted : null };
|
|
293
|
+
}
|
|
294
|
+
const candidates = baseRefCandidates(repoDir);
|
|
295
|
+
return { candidates, explicit: false, ref: (_a = candidates.find((ref) => gitRefExists(repoDir, ref))) !== null && _a !== void 0 ? _a : null };
|
|
296
|
+
}
|
|
235
297
|
/**
|
|
236
298
|
* Resolve the dev identity + DB name for a project root, ticket-aware.
|
|
237
299
|
*
|
|
@@ -336,6 +398,26 @@ function writeTicketMarker(root, id) {
|
|
|
336
398
|
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
337
399
|
(0, fs_1.writeFileSync)((0, path_1.join)(dir, TICKET_MARKER), `${id}\n`, 'utf8');
|
|
338
400
|
}
|
|
401
|
+
/** Branch names a project may use as its integration branch, in preference order. */
|
|
402
|
+
const BASE_BRANCH_NAMES = ['dev', 'develop', 'main', 'master'];
|
|
403
|
+
/**
|
|
404
|
+
* The refs {@link resolveBaseRef} probes, in order. Remote branches first (a
|
|
405
|
+
* ticket must start from the freshest integration state), the remote's own HEAD
|
|
406
|
+
* ahead of the guessed `main`/`master`, and the local branches last so a repo
|
|
407
|
+
* without a remote still works.
|
|
408
|
+
*/
|
|
409
|
+
function baseRefCandidates(repoDir) {
|
|
410
|
+
const remoteHead = gitRemoteHead(repoDir);
|
|
411
|
+
const ordered = [
|
|
412
|
+
'origin/dev',
|
|
413
|
+
'origin/develop',
|
|
414
|
+
...(remoteHead ? [remoteHead] : []),
|
|
415
|
+
'origin/main',
|
|
416
|
+
'origin/master',
|
|
417
|
+
...BASE_BRANCH_NAMES,
|
|
418
|
+
];
|
|
419
|
+
return ordered.filter((ref, i) => ordered.indexOf(ref) === i); // dedupe, order preserved
|
|
420
|
+
}
|
|
339
421
|
/**
|
|
340
422
|
* Split a worktree's uncommitted changes into "auto-discardable" (framework-
|
|
341
423
|
* generated files + pristine lt-dev self-heal patches) and "real" developer work.
|
|
@@ -363,6 +445,23 @@ function finalizeWorktree(partial) {
|
|
|
363
445
|
function git(cwd, args) {
|
|
364
446
|
return (0, child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8' }).trim();
|
|
365
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* The branch a remote's HEAD points at (`origin/main`, `origin/develop`, …), or
|
|
450
|
+
* null. stderr is swallowed: a repo whose `refs/remotes/<remote>/HEAD` was never
|
|
451
|
+
* set is the normal case here, not an error worth printing.
|
|
452
|
+
*/
|
|
453
|
+
function gitRemoteHead(repoDir, remote = 'origin') {
|
|
454
|
+
try {
|
|
455
|
+
const out = (0, child_process_1.execFileSync)('git', ['-C', repoDir, 'symbolic-ref', '--short', `refs/remotes/${remote}/HEAD`], {
|
|
456
|
+
encoding: 'utf8',
|
|
457
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
458
|
+
});
|
|
459
|
+
return out.trim() || null;
|
|
460
|
+
}
|
|
461
|
+
catch (_a) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
366
465
|
/**
|
|
367
466
|
* `git status --porcelain` lines, each kept VERBATIM (not trimmed).
|
|
368
467
|
*
|
|
@@ -316,7 +316,7 @@
|
|
|
316
316
|
<div class="page">
|
|
317
317
|
<span class="eyebrow">Baustein 2</span>
|
|
318
318
|
<h2><code style="font-size:.8em">lt ticket</code> — mehrere Tickets gleichzeitig</h2>
|
|
319
|
-
<p class="lead">Ein Repo, beliebig viele Tickets — jedes in einem eigenen <b>git worktree</b> mit eigenem <code>lt dev</code>-Stack. Frisch aus <code>origin/dev</code>, in Sekunden startklar, voneinander komplett unabhängig.</p>
|
|
319
|
+
<p class="lead">Ein Repo, beliebig viele Tickets — jedes in einem eigenen <b>git worktree</b> mit eigenem <code>lt dev</code>-Stack. Frisch aus dem Basis-Branch (<code>origin/dev</code>, <code>origin/develop</code>, …), in Sekunden startklar, voneinander komplett unabhängig.</p>
|
|
320
320
|
|
|
321
321
|
<figure>
|
|
322
322
|
<!-- DIAGRAM B: one repo -> N worktrees -> N stacks -->
|
|
@@ -375,7 +375,7 @@
|
|
|
375
375
|
</figure>
|
|
376
376
|
|
|
377
377
|
<h3>So einfach geht's</h3>
|
|
378
|
-
<pre><span class="k">lt ticket start</span> <span class="a">DEV-2200</span> <span class="c"># fetch + worktree aus
|
|
378
|
+
<pre><span class="k">lt ticket start</span> <span class="a">DEV-2200</span> <span class="c"># fetch + worktree aus dem Basis-Branch + install + lt dev up</span>
|
|
379
379
|
<span class="c">→ <span class="s">https://svl-2200.localhost</span> · DB svl-…-2200 (leer)</span>
|
|
380
380
|
<span class="k">lt ticket start</span> <span class="a">login-fix</span> <span class="c"># kein Ticket? freier Name → svl-login-fix.localhost</span>
|
|
381
381
|
<span class="k">lt ticket start</span> <span class="a">DEV-2200 --as cof</span> <span class="c"># Kurzname überschreiben; --branch / --base ebenso</span>
|
|
@@ -391,7 +391,7 @@
|
|
|
391
391
|
<p class="small">Jedes Ticket bekommt <code><slug>-<id></code> in URL, DB, Ports, Caddy-Block, Ordnername. Verwechslung ausgeschlossen.</p></div>
|
|
392
392
|
<div class="card"><div class="ico" style="background:linear-gradient(135deg,#06b6d4,#0e7490)">🌱</div>
|
|
393
393
|
<h3>Immer frisch aus dev</h3>
|
|
394
|
-
<p class="small">Jeder Start macht <code>git fetch</code> und zweigt
|
|
394
|
+
<p class="small">Jeder Start macht <code>git fetch</code> und zweigt vom Basis-Branch ab — gesucht wird <code>origin/dev</code>, dann <code>origin/develop</code>, dann der Remote-HEAD (<code>main</code>/<code>master</code>). Findet sich keiner, fragt die CLI nach dem Branch. <code>--base</code> setzt ihn direkt (Pflicht im nicht-interaktiven Betrieb).</p></div>
|
|
395
395
|
<div class="card"><div class="ico" style="background:linear-gradient(135deg,#10b981,#047857)">⚡</div>
|
|
396
396
|
<h3>0 s & leicht</h3>
|
|
397
397
|
<p class="small">Worktree teilt das <code>.git</code> — kein Re-Clone. Ein <code>git fetch</code> aktualisiert alle. pnpm hardlinkt aus dem Store.</p></div>
|