@roulabs/mx 1.11.0 → 2.1.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 +17 -10
- package/bin/mx.js +721 -130
- package/package.json +1 -1
- package/templates/CLAUDE.md +69 -18
- package/templates/repo/health.sh +23 -0
- package/templates/repo/hydrate.sh +28 -0
package/bin/mx.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
5
5
|
import * as path9 from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
@@ -49,6 +49,56 @@ function realpath(p) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// ../../packages/core/src/git.ts
|
|
53
|
+
import { execFileSync } from "child_process";
|
|
54
|
+
function git(args, opts = {}) {
|
|
55
|
+
try {
|
|
56
|
+
const out = execFileSync("git", args, {
|
|
57
|
+
encoding: "utf8",
|
|
58
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
59
|
+
...opts
|
|
60
|
+
});
|
|
61
|
+
return (out == null ? "" : String(out)).trim();
|
|
62
|
+
} catch (e) {
|
|
63
|
+
const err = e;
|
|
64
|
+
const msg = (err.stderr ?? err.stdout ?? err.message ?? "").toString().trim();
|
|
65
|
+
throw new MxError(`git ${args.join(" ")} failed: ${msg}`, "GIT");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function gitQuiet(args, opts = {}) {
|
|
69
|
+
try {
|
|
70
|
+
const out = execFileSync("git", args, {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
73
|
+
...opts
|
|
74
|
+
});
|
|
75
|
+
return (out == null ? "" : String(out)).trim();
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function currentBranch(repoPath2) {
|
|
81
|
+
return gitQuiet(["-C", repoPath2, "rev-parse", "--abbrev-ref", "HEAD"]) ?? "(detached)";
|
|
82
|
+
}
|
|
83
|
+
function remoteUrl(repoPath2) {
|
|
84
|
+
return gitQuiet(["-C", repoPath2, "remote", "get-url", "origin"]) ?? gitQuiet(["-C", repoPath2, "config", "--get", "remote.origin.url"]) ?? null;
|
|
85
|
+
}
|
|
86
|
+
function branchExists(repoPath2, branch) {
|
|
87
|
+
return gitQuiet(["-C", repoPath2, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]) !== null;
|
|
88
|
+
}
|
|
89
|
+
function isDirty(worktreePath2) {
|
|
90
|
+
const s = gitQuiet(["-C", worktreePath2, "status", "--porcelain"]);
|
|
91
|
+
return s == null ? false : s.length > 0;
|
|
92
|
+
}
|
|
93
|
+
function remoteBranchList(repoPath2) {
|
|
94
|
+
const out = gitQuiet(["-C", repoPath2, "for-each-ref", "--format=%(refname)", "refs/remotes/origin"]);
|
|
95
|
+
if (!out) return [];
|
|
96
|
+
return out.split("\n").map((s) => s.trim()).filter((s) => s && !s.endsWith("/HEAD")).map((s) => s.replace(/^refs\/remotes\/origin\//, ""));
|
|
97
|
+
}
|
|
98
|
+
function resolveBase(repoPath2, base) {
|
|
99
|
+
return gitQuiet(["-C", repoPath2, "rev-parse", "--verify", `${base}^{commit}`]) ?? gitQuiet(["-C", repoPath2, "rev-parse", "--verify", `origin/${base}^{commit}`]) ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
52
102
|
// ../../packages/core/src/json.ts
|
|
53
103
|
import * as fs2 from "fs";
|
|
54
104
|
function readJson(file) {
|
|
@@ -87,6 +137,20 @@ function stampContextIndex(targetDir, templatesDir2) {
|
|
|
87
137
|
fs3.copyFileSync(src, dest);
|
|
88
138
|
return dest;
|
|
89
139
|
}
|
|
140
|
+
var REPO_SCRIPTS = ["hydrate.sh", "health.sh"];
|
|
141
|
+
function stampRepoScripts(containerDir, templatesDir2) {
|
|
142
|
+
const created = [];
|
|
143
|
+
for (const name of REPO_SCRIPTS) {
|
|
144
|
+
const dest = path2.join(containerDir, name);
|
|
145
|
+
if (exists(dest)) continue;
|
|
146
|
+
const src = path2.join(templatesDir2, "repo", name);
|
|
147
|
+
if (!exists(src)) throw new MxError(`missing template: ${src}`, "NO_TEMPLATE");
|
|
148
|
+
fs3.copyFileSync(src, dest);
|
|
149
|
+
fs3.chmodSync(dest, 493);
|
|
150
|
+
created.push(dest);
|
|
151
|
+
}
|
|
152
|
+
return created;
|
|
153
|
+
}
|
|
90
154
|
|
|
91
155
|
// ../../packages/core/src/runtime.ts
|
|
92
156
|
var DEFAULT_RUNTIME = path3.join(os.homedir(), "mx");
|
|
@@ -96,9 +160,40 @@ function defaultRuntime() {
|
|
|
96
160
|
var reposDir = (root) => path3.join(root, "repos");
|
|
97
161
|
var worksDir = (root) => path3.join(root, "works");
|
|
98
162
|
var repoPath = (root, name) => path3.join(reposDir(root), name);
|
|
163
|
+
var repoGitDir = (root, name) => path3.join(repoPath(root, name), "git");
|
|
164
|
+
var repoHydrateScript = (root, name) => path3.join(repoPath(root, name), "hydrate.sh");
|
|
165
|
+
var repoHealthScript = (root, name) => path3.join(repoPath(root, name), "health.sh");
|
|
99
166
|
var workDir = (root, name) => path3.join(worksDir(root), name);
|
|
100
167
|
var workManifest = (root, name) => path3.join(workDir(root, name), "work.json");
|
|
101
168
|
var workspaceFile = (root, name) => path3.join(workDir(root, name), `${name}.code-workspace`);
|
|
169
|
+
var worktreesDir = (root, name) => path3.join(workDir(root, name), "wt");
|
|
170
|
+
var worktreePath = (root, name, repo) => path3.join(worktreesDir(root, name), repo);
|
|
171
|
+
var RUNTIME_VERSION = 2;
|
|
172
|
+
var mxConfigFile = (root) => path3.join(root, "mx.json");
|
|
173
|
+
function readMxConfig(root) {
|
|
174
|
+
const f = mxConfigFile(root);
|
|
175
|
+
if (!exists(f)) return null;
|
|
176
|
+
return readJson(f);
|
|
177
|
+
}
|
|
178
|
+
function readRuntimeVersion(root) {
|
|
179
|
+
const cfg = readMxConfig(root);
|
|
180
|
+
if (!cfg) return 1;
|
|
181
|
+
const n = cfg.version;
|
|
182
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
183
|
+
throw new MxError(`invalid version in ${mxConfigFile(root)}: ${JSON.stringify(n)}`, "BAD_VERSION");
|
|
184
|
+
}
|
|
185
|
+
return n;
|
|
186
|
+
}
|
|
187
|
+
function writeRuntimeVersion(root, version) {
|
|
188
|
+
const existing = readMxConfig(root) ?? {};
|
|
189
|
+
writeJson(mxConfigFile(root), { ...existing, version });
|
|
190
|
+
}
|
|
191
|
+
function versionMismatchMessage(actual) {
|
|
192
|
+
if (actual > RUNTIME_VERSION) {
|
|
193
|
+
return `runtime is v${actual}, newer than this mx supports (v${RUNTIME_VERSION}). Upgrade your mx CLI: \`npm i -g @roulabs/mx@latest\`.`;
|
|
194
|
+
}
|
|
195
|
+
return `runtime is v${actual} but this mx supports runtime v${RUNTIME_VERSION}. Run \`mx migrate\` to upgrade the runtime.`;
|
|
196
|
+
}
|
|
102
197
|
function discoverRuntime(opts = {}) {
|
|
103
198
|
const p = opts.runtime || process.env.MX_RUNTIME || DEFAULT_RUNTIME;
|
|
104
199
|
return path3.resolve(p);
|
|
@@ -108,10 +203,82 @@ function requireRuntime(opts = {}) {
|
|
|
108
203
|
if (!exists(path3.join(root, ".mx-root"))) {
|
|
109
204
|
throw new MxError(`not an mx runtime (no .mx-root): ${root} \u2014 run \`mx init\``, "NO_RUNTIME");
|
|
110
205
|
}
|
|
206
|
+
if (!opts.allowVersionMismatch) {
|
|
207
|
+
const v = readRuntimeVersion(root);
|
|
208
|
+
if (v !== RUNTIME_VERSION) {
|
|
209
|
+
throw new MxError(versionMismatchMessage(v), "RUNTIME_VERSION_MISMATCH");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
111
212
|
return root;
|
|
112
213
|
}
|
|
113
214
|
function listRepoNames(root) {
|
|
114
|
-
return listDirs(reposDir(root)).filter((n) => isGitRepo(
|
|
215
|
+
return listDirs(reposDir(root)).filter((n) => isGitRepo(repoGitDir(root, n)));
|
|
216
|
+
}
|
|
217
|
+
function migrateRepoLayout(root) {
|
|
218
|
+
const migrated = [];
|
|
219
|
+
for (const name of listDirs(reposDir(root))) {
|
|
220
|
+
const container = repoPath(root, name);
|
|
221
|
+
const gitdir = repoGitDir(root, name);
|
|
222
|
+
if (isGitRepo(gitdir)) continue;
|
|
223
|
+
if (!isGitRepo(container)) continue;
|
|
224
|
+
const tmp = path3.join(reposDir(root), `.${name}.mxmig`);
|
|
225
|
+
if (exists(tmp)) fs4.rmSync(tmp, { recursive: true, force: true });
|
|
226
|
+
fs4.renameSync(container, tmp);
|
|
227
|
+
fs4.mkdirSync(container, { recursive: true });
|
|
228
|
+
fs4.renameSync(tmp, gitdir);
|
|
229
|
+
try {
|
|
230
|
+
git(["-C", gitdir, "worktree", "repair"]);
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
migrated.push(container);
|
|
234
|
+
}
|
|
235
|
+
return migrated;
|
|
236
|
+
}
|
|
237
|
+
function migrateWorkLayout(root) {
|
|
238
|
+
const changed = [];
|
|
239
|
+
for (const name of listWorkNames(root)) {
|
|
240
|
+
let work;
|
|
241
|
+
try {
|
|
242
|
+
work = readWork(root, name);
|
|
243
|
+
} catch {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const wd = workDir(root, name);
|
|
247
|
+
const wtDir = path3.join(wd, "wt");
|
|
248
|
+
for (const wt of work.worktrees ?? []) {
|
|
249
|
+
const flat = path3.join(wd, wt.repo);
|
|
250
|
+
const dest = path3.join(wtDir, wt.repo);
|
|
251
|
+
if (exists(dest) || !exists(flat)) continue;
|
|
252
|
+
fs4.mkdirSync(wtDir, { recursive: true });
|
|
253
|
+
try {
|
|
254
|
+
git(["-C", repoGitDir(root, wt.repo), "worktree", "move", flat, dest]);
|
|
255
|
+
} catch {
|
|
256
|
+
fs4.renameSync(flat, dest);
|
|
257
|
+
try {
|
|
258
|
+
git(["-C", repoGitDir(root, wt.repo), "worktree", "repair", dest]);
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
changed.push(dest);
|
|
263
|
+
}
|
|
264
|
+
const wsFile = workspaceFile(root, name);
|
|
265
|
+
if (exists(wsFile)) {
|
|
266
|
+
const ws = readJson(wsFile);
|
|
267
|
+
const repos = new Set((work.worktrees ?? []).map((w) => w.repo));
|
|
268
|
+
let touched = false;
|
|
269
|
+
for (const f of ws.folders ?? []) {
|
|
270
|
+
if (f.path && !f.path.startsWith("wt/") && repos.has(f.path)) {
|
|
271
|
+
f.path = `wt/${f.path}`;
|
|
272
|
+
touched = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (touched) {
|
|
276
|
+
writeJson(wsFile, ws);
|
|
277
|
+
changed.push(wsFile);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return changed;
|
|
115
282
|
}
|
|
116
283
|
function listWorkNames(root) {
|
|
117
284
|
return listDirs(worksDir(root)).filter((n) => exists(workManifest(root, n)));
|
|
@@ -147,7 +314,7 @@ function inferContext(root) {
|
|
|
147
314
|
return rel.split(path3.sep);
|
|
148
315
|
};
|
|
149
316
|
const w = segmentsUnder(worksDir(root));
|
|
150
|
-
if (w) return { work: w[0], repo: w[1] ?? null };
|
|
317
|
+
if (w) return { work: w[0], repo: w[1] === "wt" ? w[2] ?? null : null };
|
|
151
318
|
const r = segmentsUnder(reposDir(root));
|
|
152
319
|
if (r) return { work: null, repo: r[0] };
|
|
153
320
|
return { work: null, repo: null };
|
|
@@ -155,6 +322,15 @@ function inferContext(root) {
|
|
|
155
322
|
function initRuntime(target0, templatesDir2) {
|
|
156
323
|
const target = path3.resolve(target0);
|
|
157
324
|
const created = [];
|
|
325
|
+
if (exists(path3.join(target, ".mx-root"))) {
|
|
326
|
+
const v = readRuntimeVersion(target);
|
|
327
|
+
if (v !== RUNTIME_VERSION) {
|
|
328
|
+
throw new MxError(
|
|
329
|
+
`cannot init: existing runtime at ${target} is v${v}, but this mx supports v${RUNTIME_VERSION}. ` + (v < RUNTIME_VERSION ? "Run `mx migrate` to upgrade it." : "Upgrade your mx CLI."),
|
|
330
|
+
"RUNTIME_VERSION_MISMATCH"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
158
334
|
for (const d of [target, reposDir(target), worksDir(target)]) {
|
|
159
335
|
if (!exists(d)) {
|
|
160
336
|
fs4.mkdirSync(d, { recursive: true });
|
|
@@ -166,6 +342,10 @@ function initRuntime(target0, templatesDir2) {
|
|
|
166
342
|
fs4.writeFileSync(marker, "");
|
|
167
343
|
created.push(marker);
|
|
168
344
|
}
|
|
345
|
+
if (!exists(mxConfigFile(target))) {
|
|
346
|
+
writeRuntimeVersion(target, RUNTIME_VERSION);
|
|
347
|
+
created.push(mxConfigFile(target));
|
|
348
|
+
}
|
|
169
349
|
created.push(stampClaudeMd(target, templatesDir2));
|
|
170
350
|
const ctxIndex = stampContextIndex(target, templatesDir2);
|
|
171
351
|
if (ctxIndex) created.push(ctxIndex);
|
|
@@ -174,14 +354,55 @@ function initRuntime(target0, templatesDir2) {
|
|
|
174
354
|
}
|
|
175
355
|
function ensureWorkScaffolding(root, workName) {
|
|
176
356
|
const created = [];
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
357
|
+
const wd = workDir(root, workName);
|
|
358
|
+
for (const d of ["wt", "scripts", "files", "tmp", "sessions"]) {
|
|
359
|
+
const p = path3.join(wd, d);
|
|
360
|
+
if (!exists(p)) {
|
|
361
|
+
fs4.mkdirSync(p, { recursive: true });
|
|
362
|
+
created.push(p);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const claudeMd = path3.join(wd, "CLAUDE.md");
|
|
366
|
+
if (!exists(claudeMd)) {
|
|
367
|
+
fs4.writeFileSync(claudeMd, workClaudeMd(workName));
|
|
368
|
+
created.push(claudeMd);
|
|
369
|
+
}
|
|
370
|
+
const settings = path3.join(wd, ".claude", "settings.json");
|
|
371
|
+
if (!exists(settings)) {
|
|
372
|
+
fs4.mkdirSync(path3.dirname(settings), { recursive: true });
|
|
373
|
+
fs4.writeFileSync(settings, workClaudeSettings(root));
|
|
374
|
+
created.push(settings);
|
|
181
375
|
}
|
|
182
376
|
return created;
|
|
183
377
|
}
|
|
184
|
-
function
|
|
378
|
+
function workClaudeMd(name) {
|
|
379
|
+
return `<!--
|
|
380
|
+
Work-specific CLAUDE.md for "${name}".
|
|
381
|
+
|
|
382
|
+
This file loads alongside the runtime's CLAUDE.md (the mx rules) for every Claude
|
|
383
|
+
session started in this work folder. Put rules and context specific to THIS work
|
|
384
|
+
here: what you're building, conventions, gotchas, which repo is your lane, etc.
|
|
385
|
+
mx never overwrites this file after creating it \u2014 it's yours to edit.
|
|
386
|
+
|
|
387
|
+
Keep ad-hoc files OUT of the work root (it holds mx-native files). Use:
|
|
388
|
+
files/ artifacts worth keeping
|
|
389
|
+
tmp/ throwaway scratch \u2014 may be deleted at any time, no guarantees
|
|
390
|
+
scripts/ ad-hoc scripts for this work
|
|
391
|
+
-->
|
|
392
|
+
`;
|
|
393
|
+
}
|
|
394
|
+
function workClaudeSettings(root) {
|
|
395
|
+
const contextDir = path3.join(root, "context");
|
|
396
|
+
const indexPath = path3.join(contextDir, "INDEX.json");
|
|
397
|
+
const command = `echo '# mx context registry \u2014 open ${contextDir}/<path>.md for relevant entries:'; cat '${indexPath}' 2>/dev/null`;
|
|
398
|
+
const settings = {
|
|
399
|
+
hooks: {
|
|
400
|
+
SessionStart: [{ matcher: "*", hooks: [{ type: "command", command }] }]
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
return JSON.stringify(settings, null, 2) + "\n";
|
|
404
|
+
}
|
|
405
|
+
function syncRuntime(root, templatesDir2) {
|
|
185
406
|
const updated = [];
|
|
186
407
|
updated.push(stampClaudeMd(root, templatesDir2));
|
|
187
408
|
const ctxIndex = stampContextIndex(root, templatesDir2);
|
|
@@ -189,97 +410,103 @@ function updateRuntime(root, templatesDir2) {
|
|
|
189
410
|
for (const workName of listWorkNames(root)) {
|
|
190
411
|
updated.push(...ensureWorkScaffolding(root, workName));
|
|
191
412
|
}
|
|
413
|
+
for (const repo of listRepoNames(root)) {
|
|
414
|
+
updated.push(...stampRepoScripts(repoPath(root, repo), templatesDir2));
|
|
415
|
+
}
|
|
192
416
|
removeStaleRuntimeReadme(root);
|
|
193
417
|
return { runtime: root, updated };
|
|
194
418
|
}
|
|
195
419
|
|
|
196
|
-
// ../../packages/core/src/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
return (out == null ? "" : String(out)).trim();
|
|
210
|
-
} catch (e) {
|
|
211
|
-
const err = e;
|
|
212
|
-
const msg = (err.stderr ?? err.stdout ?? err.message ?? "").toString().trim();
|
|
213
|
-
throw new MxError(`git ${args.join(" ")} failed: ${msg}`, "GIT");
|
|
420
|
+
// ../../packages/core/src/migrations.ts
|
|
421
|
+
var STEPS = {
|
|
422
|
+
1: {
|
|
423
|
+
from: 1,
|
|
424
|
+
to: 2,
|
|
425
|
+
run: (root) => {
|
|
426
|
+
const changed = [];
|
|
427
|
+
changed.push(...migrateRepoLayout(root));
|
|
428
|
+
changed.push(...migrateWorkLayout(root));
|
|
429
|
+
for (const work of listWorkNames(root)) changed.push(...ensureWorkScaffolding(root, work));
|
|
430
|
+
writeRuntimeVersion(root, 2);
|
|
431
|
+
return changed;
|
|
432
|
+
}
|
|
214
433
|
}
|
|
215
|
-
}
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
221
|
-
...opts
|
|
222
|
-
});
|
|
223
|
-
return (out == null ? "" : String(out)).trim();
|
|
224
|
-
} catch {
|
|
225
|
-
return null;
|
|
434
|
+
};
|
|
435
|
+
function migrateRuntime(root) {
|
|
436
|
+
const from = readRuntimeVersion(root);
|
|
437
|
+
if (from === RUNTIME_VERSION) {
|
|
438
|
+
return { from, to: RUNTIME_VERSION, applied: [], changed: [] };
|
|
226
439
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
440
|
+
if (from > RUNTIME_VERSION) {
|
|
441
|
+
throw new MxError(
|
|
442
|
+
`runtime is v${from}, newer than this mx supports (v${RUNTIME_VERSION}). Upgrade your mx CLI: \`npm i -g @roulabs/mx@latest\`.`,
|
|
443
|
+
"CLI_TOO_OLD"
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
for (let v = from; v < RUNTIME_VERSION; v++) {
|
|
447
|
+
if (!STEPS[v]) {
|
|
448
|
+
throw new MxError(
|
|
449
|
+
`this mx has no migration from runtime v${v} to v${v + 1} \u2014 cannot upgrade v${from} \u2192 v${RUNTIME_VERSION}.`,
|
|
450
|
+
"NO_MIGRATION"
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const applied = [];
|
|
455
|
+
const changed = [];
|
|
456
|
+
for (let v = from; v < RUNTIME_VERSION; v++) {
|
|
457
|
+
const step = STEPS[v];
|
|
458
|
+
changed.push(...step.run(root));
|
|
459
|
+
applied.push({ from: step.from, to: step.to });
|
|
460
|
+
}
|
|
461
|
+
return { from, to: RUNTIME_VERSION, applied, changed };
|
|
248
462
|
}
|
|
249
463
|
|
|
250
464
|
// ../../packages/core/src/repos.ts
|
|
465
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
466
|
+
import * as fs5 from "fs";
|
|
467
|
+
import * as path4 from "path";
|
|
251
468
|
function repoNameFromUrl(url) {
|
|
252
469
|
const base = url.split("/").pop() || url;
|
|
253
470
|
return base.replace(/\.git$/, "");
|
|
254
471
|
}
|
|
255
472
|
function repoAdd(root, url, name0) {
|
|
256
473
|
const name = name0 || repoNameFromUrl(url);
|
|
257
|
-
const
|
|
258
|
-
if (exists(
|
|
259
|
-
|
|
260
|
-
|
|
474
|
+
const container = repoPath(root, name);
|
|
475
|
+
if (exists(container)) throw new MxError(`repo already exists: ${name}`, "EXISTS");
|
|
476
|
+
const gitdir = repoGitDir(root, name);
|
|
477
|
+
fs5.mkdirSync(container, { recursive: true });
|
|
478
|
+
git(["clone", url, gitdir], { stdio: ["ignore", "inherit", "inherit"] });
|
|
479
|
+
return { name, path: container, remote: remoteUrl(gitdir), branch: currentBranch(gitdir) };
|
|
261
480
|
}
|
|
262
481
|
function listReposInfo(root) {
|
|
263
482
|
return listRepoNames(root).map((name) => ({
|
|
264
483
|
name,
|
|
265
|
-
branch
|
|
266
|
-
|
|
484
|
+
// path is the container; branch/remote come from the git/ clone.
|
|
485
|
+
path: repoPath(root, name),
|
|
486
|
+
branch: currentBranch(repoGitDir(root, name)),
|
|
487
|
+
remote: remoteUrl(repoGitDir(root, name))
|
|
267
488
|
}));
|
|
268
489
|
}
|
|
269
490
|
function repoFetch(root, name) {
|
|
270
|
-
const rp =
|
|
491
|
+
const rp = repoGitDir(root, name);
|
|
271
492
|
if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
|
|
272
493
|
git(["-C", rp, "fetch", "--all", "--prune", "--tags"]);
|
|
494
|
+
const current = currentBranch(rp);
|
|
273
495
|
gitQuiet(["-C", rp, "merge", "--ff-only", "@{u}"]);
|
|
496
|
+
const base = originDefaultBranch(rp);
|
|
497
|
+
if (base && base !== current) {
|
|
498
|
+
gitQuiet(["-C", rp, "fetch", ".", `refs/remotes/origin/${base}:refs/heads/${base}`]);
|
|
499
|
+
}
|
|
274
500
|
return { name, branch: currentBranch(rp), remoteBranches: remoteBranchList(rp) };
|
|
275
501
|
}
|
|
276
502
|
function repoInfo(root, name) {
|
|
277
|
-
const rp =
|
|
503
|
+
const rp = repoGitDir(root, name);
|
|
278
504
|
if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
|
|
279
505
|
const usedBy = listWorkNames(root).filter((w) => findWorktree(readWork(root, w), name));
|
|
280
506
|
return {
|
|
281
507
|
name,
|
|
282
|
-
path:
|
|
508
|
+
path: repoPath(root, name),
|
|
509
|
+
// the container (repo home)
|
|
283
510
|
branch: currentBranch(rp),
|
|
284
511
|
remote: remoteUrl(rp),
|
|
285
512
|
worktreesInWorks: usedBy
|
|
@@ -321,8 +548,30 @@ function lastFetched(rp) {
|
|
|
321
548
|
if (!exists(fh)) return null;
|
|
322
549
|
return fs5.statSync(fh).mtime.toISOString();
|
|
323
550
|
}
|
|
551
|
+
function runHealthScript(root, name) {
|
|
552
|
+
const script = repoHealthScript(root, name);
|
|
553
|
+
if (!exists(script)) return null;
|
|
554
|
+
try {
|
|
555
|
+
const out = execFileSync2(script, [], {
|
|
556
|
+
cwd: repoGitDir(root, name),
|
|
557
|
+
encoding: "utf8",
|
|
558
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
559
|
+
env: {
|
|
560
|
+
...process.env,
|
|
561
|
+
MX_RUNTIME: root,
|
|
562
|
+
MX_REPO: name,
|
|
563
|
+
MX_REPO_PATH: repoPath(root, name),
|
|
564
|
+
MX_GIT_DIR: repoGitDir(root, name)
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
const trimmed = out.trim();
|
|
568
|
+
return trimmed.length ? trimmed : null;
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
324
573
|
function repoHealth(root, name) {
|
|
325
|
-
const rp =
|
|
574
|
+
const rp = repoGitDir(root, name);
|
|
326
575
|
if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
|
|
327
576
|
const defaultBranch = originDefaultBranch(rp);
|
|
328
577
|
const current = currentBranchOrNull(rp);
|
|
@@ -355,7 +604,8 @@ function repoHealth(root, name) {
|
|
|
355
604
|
}
|
|
356
605
|
return {
|
|
357
606
|
name,
|
|
358
|
-
path:
|
|
607
|
+
path: repoPath(root, name),
|
|
608
|
+
// the container (repo home)
|
|
359
609
|
defaultBranch,
|
|
360
610
|
currentBranch: current,
|
|
361
611
|
isOnDefault,
|
|
@@ -366,15 +616,15 @@ function repoHealth(root, name) {
|
|
|
366
616
|
lastFetchedAt,
|
|
367
617
|
worktreesInWorks,
|
|
368
618
|
healthy: issues.length === 0,
|
|
369
|
-
issues
|
|
619
|
+
issues,
|
|
620
|
+
extra: runHealthScript(root, name)
|
|
370
621
|
};
|
|
371
622
|
}
|
|
372
623
|
function listRepoHealth(root) {
|
|
373
624
|
return listRepoNames(root).map((name) => repoHealth(root, name));
|
|
374
625
|
}
|
|
375
626
|
function repoRemove(root, name) {
|
|
376
|
-
|
|
377
|
-
if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
|
|
627
|
+
if (!isGitRepo(repoGitDir(root, name))) throw new MxError(`no such repo: ${name}`, "NO_REPO");
|
|
378
628
|
const usedBy = listWorkNames(root).filter((w) => findWorktree(readWork(root, w), name));
|
|
379
629
|
if (usedBy.length) {
|
|
380
630
|
throw new MxError(
|
|
@@ -382,25 +632,25 @@ function repoRemove(root, name) {
|
|
|
382
632
|
"IN_USE"
|
|
383
633
|
);
|
|
384
634
|
}
|
|
385
|
-
fs5.rmSync(
|
|
635
|
+
fs5.rmSync(repoPath(root, name), { recursive: true, force: true });
|
|
386
636
|
return { name, removed: true };
|
|
387
637
|
}
|
|
388
638
|
|
|
389
639
|
// ../../packages/core/src/works.ts
|
|
390
640
|
import * as fs6 from "fs";
|
|
391
|
-
import * as path5 from "path";
|
|
392
641
|
function addFolderToWorkspace(root, name, repo) {
|
|
393
642
|
const file = workspaceFile(root, name);
|
|
394
643
|
const ws = exists(file) ? readJson(file) : { folders: [], settings: {} };
|
|
395
644
|
ws.folders = ws.folders ?? [];
|
|
396
|
-
|
|
645
|
+
const rel = `wt/${repo}`;
|
|
646
|
+
if (!ws.folders.some((f) => f.path === rel)) ws.folders.push({ name: repo, path: rel });
|
|
397
647
|
writeJson(file, ws);
|
|
398
648
|
}
|
|
399
649
|
function removeFolderFromWorkspace(root, name, repo) {
|
|
400
650
|
const file = workspaceFile(root, name);
|
|
401
651
|
if (!exists(file)) return;
|
|
402
652
|
const ws = readJson(file);
|
|
403
|
-
ws.folders = (ws.folders ?? []).filter((f) => f.path !== repo);
|
|
653
|
+
ws.folders = (ws.folders ?? []).filter((f) => f.path !== `wt/${repo}`);
|
|
404
654
|
writeJson(file, ws);
|
|
405
655
|
}
|
|
406
656
|
function clearWorkspaceFolders(root, name) {
|
|
@@ -423,6 +673,7 @@ function workNew(root, name, description = "") {
|
|
|
423
673
|
function listWorksInfo(root, opts = {}) {
|
|
424
674
|
return listWorkNames(root).map((name) => ({
|
|
425
675
|
...readWork(root, name),
|
|
676
|
+
path: workDir(root, name),
|
|
426
677
|
sessions: countSessions(root, name)
|
|
427
678
|
})).filter((w) => {
|
|
428
679
|
if (opts.onlyArchived) return w.isArchived === true;
|
|
@@ -446,13 +697,14 @@ function workPath(root, name) {
|
|
|
446
697
|
}
|
|
447
698
|
function worktreeAdd(root, name, repo, opts = {}) {
|
|
448
699
|
const work = readWork(root, name);
|
|
449
|
-
const rp =
|
|
700
|
+
const rp = repoGitDir(root, repo);
|
|
450
701
|
if (!isGitRepo(rp)) throw new MxError(`no such repo: ${repo}`, "NO_REPO");
|
|
451
702
|
if (findWorktree(work, repo)) {
|
|
452
703
|
throw new MxError(`work "${name}" already has worktree for ${repo}`, "EXISTS");
|
|
453
704
|
}
|
|
454
705
|
const branch = opts.branch || name;
|
|
455
|
-
const dest =
|
|
706
|
+
const dest = worktreePath(root, name, repo);
|
|
707
|
+
fs6.mkdirSync(worktreesDir(root, name), { recursive: true });
|
|
456
708
|
if (branchExists(rp, branch)) {
|
|
457
709
|
git(["-C", rp, "worktree", "add", dest, branch]);
|
|
458
710
|
} else {
|
|
@@ -479,11 +731,11 @@ function worktreeRemove(root, name, repo) {
|
|
|
479
731
|
const work = readWork(root, name);
|
|
480
732
|
const wt = findWorktree(work, repo);
|
|
481
733
|
if (!wt) throw new MxError(`work "${name}" has no worktree for ${repo}`, "NO_WORKTREE");
|
|
482
|
-
const dest =
|
|
734
|
+
const dest = worktreePath(root, name, repo);
|
|
483
735
|
if (exists(dest) && isDirty(dest)) {
|
|
484
736
|
throw new MxError(`worktree ${repo} has uncommitted changes \u2014 commit or discard them first`, "DIRTY");
|
|
485
737
|
}
|
|
486
|
-
git(["-C",
|
|
738
|
+
git(["-C", repoGitDir(root, repo), "worktree", "remove", dest]);
|
|
487
739
|
work.worktrees = work.worktrees.filter((w) => w.repo !== repo);
|
|
488
740
|
writeWork(root, work);
|
|
489
741
|
removeFolderFromWorkspace(root, name, repo);
|
|
@@ -499,7 +751,7 @@ function workDestroy(root, name, opts = {}) {
|
|
|
499
751
|
const work = readWork(root, name);
|
|
500
752
|
const dirty = [];
|
|
501
753
|
for (const wt of work.worktrees ?? []) {
|
|
502
|
-
const dest =
|
|
754
|
+
const dest = worktreePath(root, name, wt.repo);
|
|
503
755
|
if (exists(dest) && isDirty(dest)) dirty.push(wt.repo);
|
|
504
756
|
}
|
|
505
757
|
if (dirty.length) {
|
|
@@ -510,8 +762,8 @@ function workDestroy(root, name, opts = {}) {
|
|
|
510
762
|
}
|
|
511
763
|
const removed = [];
|
|
512
764
|
for (const wt of work.worktrees ?? []) {
|
|
513
|
-
const dest =
|
|
514
|
-
if (exists(dest)) git(["-C",
|
|
765
|
+
const dest = worktreePath(root, name, wt.repo);
|
|
766
|
+
if (exists(dest)) git(["-C", repoGitDir(root, wt.repo), "worktree", "remove", dest]);
|
|
515
767
|
removed.push(wt.repo);
|
|
516
768
|
}
|
|
517
769
|
fs6.rmSync(workDir(root, name), { recursive: true, force: true });
|
|
@@ -524,7 +776,7 @@ function archiveWork(root, name) {
|
|
|
524
776
|
}
|
|
525
777
|
const dirty = [];
|
|
526
778
|
for (const wt of work.worktrees ?? []) {
|
|
527
|
-
const dest =
|
|
779
|
+
const dest = worktreePath(root, name, wt.repo);
|
|
528
780
|
if (exists(dest) && isDirty(dest)) dirty.push(wt.repo);
|
|
529
781
|
}
|
|
530
782
|
if (dirty.length) {
|
|
@@ -535,8 +787,8 @@ function archiveWork(root, name) {
|
|
|
535
787
|
}
|
|
536
788
|
const removed = [];
|
|
537
789
|
for (const wt of work.worktrees ?? []) {
|
|
538
|
-
const dest =
|
|
539
|
-
if (exists(dest)) git(["-C",
|
|
790
|
+
const dest = worktreePath(root, name, wt.repo);
|
|
791
|
+
if (exists(dest)) git(["-C", repoGitDir(root, wt.repo), "worktree", "remove", dest]);
|
|
540
792
|
removed.push(wt.repo);
|
|
541
793
|
}
|
|
542
794
|
clearWorkspaceFolders(root, name);
|
|
@@ -558,7 +810,7 @@ function unarchiveWork(root, name, overrides = {}) {
|
|
|
558
810
|
}));
|
|
559
811
|
const missing = [];
|
|
560
812
|
for (const d of desired) {
|
|
561
|
-
const rp =
|
|
813
|
+
const rp = repoGitDir(root, d.repo);
|
|
562
814
|
if (!isGitRepo(rp)) {
|
|
563
815
|
throw new MxError(`pristine clone missing for repo: ${d.repo}`, "NO_REPO");
|
|
564
816
|
}
|
|
@@ -574,8 +826,9 @@ function unarchiveWork(root, name, overrides = {}) {
|
|
|
574
826
|
}
|
|
575
827
|
const restored = [];
|
|
576
828
|
for (const d of desired) {
|
|
577
|
-
const dest =
|
|
578
|
-
|
|
829
|
+
const dest = worktreePath(root, name, d.repo);
|
|
830
|
+
fs6.mkdirSync(worktreesDir(root, name), { recursive: true });
|
|
831
|
+
git(["-C", repoGitDir(root, d.repo), "worktree", "add", dest, d.branch]);
|
|
579
832
|
addFolderToWorkspace(root, name, d.repo);
|
|
580
833
|
restored.push({ repo: d.repo, branch: d.branch, path: dest, ports: d.ports });
|
|
581
834
|
}
|
|
@@ -654,9 +907,9 @@ function portList(root, name) {
|
|
|
654
907
|
}
|
|
655
908
|
|
|
656
909
|
// ../../packages/core/src/status.ts
|
|
657
|
-
import * as
|
|
910
|
+
import * as path5 from "path";
|
|
658
911
|
function countContextEntries(root) {
|
|
659
|
-
const indexPath =
|
|
912
|
+
const indexPath = path5.join(root, "context", "INDEX.json");
|
|
660
913
|
if (!exists(indexPath)) return 0;
|
|
661
914
|
try {
|
|
662
915
|
const data = readJson(indexPath);
|
|
@@ -671,6 +924,7 @@ function statusRuntime(root, opts = {}) {
|
|
|
671
924
|
const works = opts.onlyArchived ? all.filter((w) => w.isArchived === true) : opts.includeArchived ? all : all.filter((w) => w.isArchived !== true);
|
|
672
925
|
return {
|
|
673
926
|
runtime: root,
|
|
927
|
+
version: readRuntimeVersion(root),
|
|
674
928
|
context: { entries: countContextEntries(root) },
|
|
675
929
|
repos: listReposInfo(root),
|
|
676
930
|
works,
|
|
@@ -696,7 +950,9 @@ function parseArgs(argv) {
|
|
|
696
950
|
force: false,
|
|
697
951
|
yes: false,
|
|
698
952
|
all: false,
|
|
699
|
-
archived: false
|
|
953
|
+
archived: false,
|
|
954
|
+
open: false,
|
|
955
|
+
noHydrate: false
|
|
700
956
|
};
|
|
701
957
|
for (let i = 0; i < argv.length; i++) {
|
|
702
958
|
const a = argv[i];
|
|
@@ -714,6 +970,10 @@ function parseArgs(argv) {
|
|
|
714
970
|
flags.all = true;
|
|
715
971
|
} else if (a === "--archived") {
|
|
716
972
|
flags.archived = true;
|
|
973
|
+
} else if (a === "--open" || a === "-o") {
|
|
974
|
+
flags.open = true;
|
|
975
|
+
} else if (a === "--no-hydrate") {
|
|
976
|
+
flags.noHydrate = true;
|
|
717
977
|
} else if (a.startsWith("--") && a.includes("=")) {
|
|
718
978
|
const eq = a.indexOf("=");
|
|
719
979
|
const key = VALUE_FLAGS[a.slice(0, eq)];
|
|
@@ -730,6 +990,7 @@ function parseArgs(argv) {
|
|
|
730
990
|
|
|
731
991
|
// src/output.ts
|
|
732
992
|
import { spawnSync } from "child_process";
|
|
993
|
+
import * as os2 from "os";
|
|
733
994
|
var USE_STYLE = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
734
995
|
function wrap(s, code) {
|
|
735
996
|
return USE_STYLE ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
@@ -746,6 +1007,11 @@ function check() {
|
|
|
746
1007
|
function warn() {
|
|
747
1008
|
return "\u26A0";
|
|
748
1009
|
}
|
|
1010
|
+
function tildify(p) {
|
|
1011
|
+
const home = os2.homedir();
|
|
1012
|
+
if (p === home) return "~";
|
|
1013
|
+
return p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
|
|
1014
|
+
}
|
|
749
1015
|
function confirmYesNo(prompt) {
|
|
750
1016
|
if (!process.stdin.isTTY) return false;
|
|
751
1017
|
process.stdout.write(prompt);
|
|
@@ -790,28 +1056,34 @@ var HELP = `mx \u2014 control panel for the mx runtime
|
|
|
790
1056
|
|
|
791
1057
|
Global:
|
|
792
1058
|
mx init [path] scaffold/adopt a runtime (default ~/mx)
|
|
793
|
-
mx
|
|
794
|
-
mx
|
|
1059
|
+
mx info [--all] [--porcelain] show runtime version, repos, works, ports (active only by default; --all to include archived; alias: mx i)
|
|
1060
|
+
mx sync re-stamp runtime files (CLAUDE.md, scaffolding) from current templates \u2014 same-major, non-breaking
|
|
1061
|
+
mx update self-update the mx CLI within its major (npm i -g); flags a newer major if one exists
|
|
1062
|
+
mx migrate upgrade an older-version runtime to the version this CLI supports (the only command allowed on a mismatched runtime)
|
|
795
1063
|
mx help | version
|
|
796
1064
|
|
|
797
1065
|
Repos (pristine clones):
|
|
798
1066
|
mx repo add <git-url> [--name <n>] clone a repo into the runtime
|
|
799
1067
|
mx repo ls [--porcelain]
|
|
800
|
-
mx repo -n <name>
|
|
1068
|
+
mx repo -n <name> path print the repo container path (cd "$(mx repo -n <name> path)")
|
|
1069
|
+
mx repo -n <name> fetch git fetch (+ ff the checked-out and base branches)
|
|
1070
|
+
mx repo fetch --all fetch every repo, one by one
|
|
801
1071
|
mx repo -n <name> info [--porcelain]
|
|
802
1072
|
mx repo health [--porcelain] pure-local health summary for every pristine clone
|
|
803
1073
|
mx repo -n <name> health [--porcelain] detailed health for one pristine clone
|
|
804
1074
|
mx repo -n <name> rm refuses if any work uses it
|
|
805
1075
|
|
|
806
1076
|
Works (features):
|
|
807
|
-
mx work new <name> [--description <t>]
|
|
1077
|
+
mx work new <name> [--description <t>] [-o|--open] creates folder + empty work.json + sessions/; -o opens a fullscreen Terminal (cd'd in) + editor on the workspace (macOS)
|
|
808
1078
|
mx work ls [--all|--archived] [--porcelain] default: active only; --all includes archived; --archived shows archived only
|
|
809
1079
|
mx work -n <name> info [--porcelain]
|
|
810
1080
|
mx work -n <name> path print the work folder path (cd "$(mx work -n <name> path)")
|
|
1081
|
+
mx work -n <name> open (or -o) open the work's fullscreen Terminal + editor layout (macOS)
|
|
811
1082
|
mx work -n <name> describe <text>
|
|
812
|
-
mx work -n <name> worktree add <repo> [--branch <b>] [--base <ref>]
|
|
1083
|
+
mx work -n <name> worktree add <repo> [--branch <b>] [--base <ref>] [--no-hydrate] runs the repo's hydrate.sh after add unless --no-hydrate
|
|
813
1084
|
mx work -n <name> worktree ls [--porcelain]
|
|
814
1085
|
mx work -n <name> worktree rm <repo> refuses on uncommitted changes; keeps branch
|
|
1086
|
+
mx work -n <name> worktree hydrate <repo> re-run the repo's hydrate.sh against its worktree
|
|
815
1087
|
mx work -n <name> port set <repo> <service> [<port>] auto-picks a free port if omitted
|
|
816
1088
|
mx work -n <name> port unset <repo> <service>
|
|
817
1089
|
mx work -n <name> port ls [--porcelain]
|
|
@@ -827,20 +1099,89 @@ Runtime discovery: --runtime <path> -> $MX_RUNTIME -> default ~/mx.
|
|
|
827
1099
|
`;
|
|
828
1100
|
|
|
829
1101
|
// src/commands/global.ts
|
|
830
|
-
import * as
|
|
1102
|
+
import * as path7 from "path";
|
|
831
1103
|
|
|
832
1104
|
// src/paths.ts
|
|
833
|
-
import
|
|
1105
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1106
|
+
import * as path6 from "path";
|
|
834
1107
|
import { fileURLToPath } from "url";
|
|
835
1108
|
function templatesDir() {
|
|
836
1109
|
if (process.env.MX_TEMPLATES_DIR) return process.env.MX_TEMPLATES_DIR;
|
|
837
|
-
const here =
|
|
838
|
-
return
|
|
1110
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
1111
|
+
return path6.join(here, "..", "templates");
|
|
1112
|
+
}
|
|
1113
|
+
function cliVersion() {
|
|
1114
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
1115
|
+
const pkg = JSON.parse(readFileSync2(path6.join(here, "..", "package.json"), "utf8"));
|
|
1116
|
+
return pkg.version;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/selfupdate.ts
|
|
1120
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1121
|
+
var PKG = "@roulabs/mx";
|
|
1122
|
+
function major(v) {
|
|
1123
|
+
return Number.parseInt(v.split(".")[0], 10) || 0;
|
|
1124
|
+
}
|
|
1125
|
+
function npmAvailable() {
|
|
1126
|
+
try {
|
|
1127
|
+
const r = spawnSync2("npm", ["--version"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
1128
|
+
return r.status === 0;
|
|
1129
|
+
} catch {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function npmViewJson(spec, field) {
|
|
1134
|
+
const r = spawnSync2("npm", ["view", spec, field, "--json"], {
|
|
1135
|
+
encoding: "utf8",
|
|
1136
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1137
|
+
});
|
|
1138
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
1139
|
+
try {
|
|
1140
|
+
return JSON.parse(r.stdout);
|
|
1141
|
+
} catch {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function latestForRange(range) {
|
|
1146
|
+
const v = npmViewJson(`${PKG}@${range}`, "version");
|
|
1147
|
+
if (typeof v === "string") return v;
|
|
1148
|
+
if (Array.isArray(v) && v.length) return String(v[v.length - 1]);
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
function latestOverall() {
|
|
1152
|
+
const v = npmViewJson(PKG, "version");
|
|
1153
|
+
return typeof v === "string" ? v : null;
|
|
1154
|
+
}
|
|
1155
|
+
function selfUpdate(porcelain2) {
|
|
1156
|
+
const current = cliVersion();
|
|
1157
|
+
const curMajor = major(current);
|
|
1158
|
+
const info = {
|
|
1159
|
+
package: PKG,
|
|
1160
|
+
current,
|
|
1161
|
+
npmAvailable: false,
|
|
1162
|
+
latestInMajor: null,
|
|
1163
|
+
updated: false,
|
|
1164
|
+
installFailed: false,
|
|
1165
|
+
newMajor: null
|
|
1166
|
+
};
|
|
1167
|
+
if (!npmAvailable()) return info;
|
|
1168
|
+
info.npmAvailable = true;
|
|
1169
|
+
info.latestInMajor = latestForRange(`^${curMajor}`);
|
|
1170
|
+
const overall = latestOverall();
|
|
1171
|
+
if (overall && major(overall) > curMajor) info.newMajor = major(overall);
|
|
1172
|
+
if (info.latestInMajor && info.latestInMajor !== current) {
|
|
1173
|
+
const r = spawnSync2("npm", ["i", "-g", `${PKG}@^${curMajor}`], {
|
|
1174
|
+
stdio: porcelain2 ? ["ignore", "pipe", "pipe"] : "inherit"
|
|
1175
|
+
});
|
|
1176
|
+
if (r.status === 0) info.updated = true;
|
|
1177
|
+
else info.installFailed = true;
|
|
1178
|
+
}
|
|
1179
|
+
return info;
|
|
839
1180
|
}
|
|
840
1181
|
|
|
841
1182
|
// src/commands/global.ts
|
|
842
1183
|
function runtimeEnvHint(runtime) {
|
|
843
|
-
const envRuntime = process.env.MX_RUNTIME ?
|
|
1184
|
+
const envRuntime = process.env.MX_RUNTIME ? path7.resolve(process.env.MX_RUNTIME) : null;
|
|
844
1185
|
if (envRuntime === runtime) {
|
|
845
1186
|
return ["", `${dim("$MX_RUNTIME already points here \u2014 you're set.")}`];
|
|
846
1187
|
}
|
|
@@ -868,28 +1209,71 @@ function runGlobal(positionals, flags) {
|
|
|
868
1209
|
}, res);
|
|
869
1210
|
return;
|
|
870
1211
|
}
|
|
871
|
-
case "
|
|
1212
|
+
case "info": {
|
|
872
1213
|
const root = requireRuntime({ runtime: flags.runtime });
|
|
873
1214
|
const data = statusRuntime(root, { includeArchived: flags.all });
|
|
874
1215
|
emit(() => renderStatus(data), data);
|
|
875
1216
|
return;
|
|
876
1217
|
}
|
|
877
|
-
case "
|
|
1218
|
+
case "sync": {
|
|
878
1219
|
const root = requireRuntime({ runtime: flags.runtime });
|
|
879
|
-
const res =
|
|
1220
|
+
const res = syncRuntime(root, templatesDir());
|
|
880
1221
|
emit(() => {
|
|
881
|
-
console.log(`${check()}
|
|
1222
|
+
console.log(`${check()} Synced runtime at ${bold(res.runtime)}`);
|
|
882
1223
|
for (const p of res.updated) console.log(` ${dim(`+ ${p}`)}`);
|
|
883
1224
|
}, res);
|
|
884
1225
|
return;
|
|
885
1226
|
}
|
|
1227
|
+
case "migrate": {
|
|
1228
|
+
const root = requireRuntime({ runtime: flags.runtime, allowVersionMismatch: true });
|
|
1229
|
+
const res = migrateRuntime(root);
|
|
1230
|
+
emit(() => {
|
|
1231
|
+
if (res.applied.length === 0) {
|
|
1232
|
+
console.log(`${check()} Runtime already at v${res.to} \u2014 nothing to migrate.`);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const steps = res.applied.map((a) => `v${a.from}\u2192v${a.to}`).join(", ");
|
|
1236
|
+
console.log(
|
|
1237
|
+
`${check()} Migrated runtime ${bold(`v${res.from}\u2192v${res.to}`)} ${dim(`(${steps})`)}`
|
|
1238
|
+
);
|
|
1239
|
+
for (const p of res.changed) console.log(` ${dim(`~ ${p}`)}`);
|
|
1240
|
+
}, res);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
case "update": {
|
|
1244
|
+
const info = selfUpdate(flags.porcelain);
|
|
1245
|
+
emit(() => renderSelfUpdate(info), info);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
886
1248
|
default:
|
|
887
1249
|
throw new MxError(`unknown command: ${positionals[0]}`, "BAD_ARGS");
|
|
888
1250
|
}
|
|
889
1251
|
}
|
|
1252
|
+
function renderSelfUpdate(info) {
|
|
1253
|
+
const curMajor = Number.parseInt(info.current.split(".")[0], 10) || 0;
|
|
1254
|
+
const manual = `npm i -g ${info.package}@^${curMajor}`;
|
|
1255
|
+
if (!info.npmAvailable) {
|
|
1256
|
+
console.log(`${warn()} npm not found \u2014 update manually: ${bold(manual)}`);
|
|
1257
|
+
} else if (info.installFailed) {
|
|
1258
|
+
console.log(`${warn()} self-update failed \u2014 try: ${bold(manual)}`);
|
|
1259
|
+
} else if (info.updated) {
|
|
1260
|
+
console.log(
|
|
1261
|
+
`${check()} Updated mx to ${bold(`v${info.latestInMajor}`)} ${dim(`(was v${info.current})`)}`
|
|
1262
|
+
);
|
|
1263
|
+
} else {
|
|
1264
|
+
console.log(`${check()} mx is up to date ${dim(`(v${info.current}, latest in v${curMajor}.x)`)}`);
|
|
1265
|
+
}
|
|
1266
|
+
if (info.newMajor) {
|
|
1267
|
+
console.log();
|
|
1268
|
+
console.log(`${warn()} A new major release ${bold(`mx v${info.newMajor}`)} is available.`);
|
|
1269
|
+
console.log(
|
|
1270
|
+
` ${dim(`Upgrade (optional): npm i -g ${info.package}@${info.newMajor}, then `)}${bold("mx migrate")}`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
890
1274
|
function renderStatus(data) {
|
|
891
1275
|
console.log();
|
|
892
|
-
console.log(` ${bold("mx")} ${dim("\xB7")} ${data.runtime}`);
|
|
1276
|
+
console.log(` ${bold("mx")} ${dim(`v${data.version}`)} ${dim("\xB7")} ${data.runtime}`);
|
|
893
1277
|
console.log();
|
|
894
1278
|
console.log(` ${bold("context")} ${dim(`(${data.context.entries})`)}`);
|
|
895
1279
|
console.log();
|
|
@@ -897,21 +1281,19 @@ function renderStatus(data) {
|
|
|
897
1281
|
if (data.repos.length === 0) {
|
|
898
1282
|
console.log(` ${dim("none yet \u2014 `mx repo add <git-url>`")}`);
|
|
899
1283
|
} else {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
console.log(` \u2022 ${name} ${branch} ${remote}`);
|
|
1284
|
+
for (let i = 0; i < data.repos.length; i++) {
|
|
1285
|
+
if (i > 0) console.log();
|
|
1286
|
+
const r = data.repos[i];
|
|
1287
|
+
console.log(` \u2022 ${bold(r.name)}`);
|
|
1288
|
+
console.log(` ${dim(tildify(r.path))}`);
|
|
1289
|
+
console.log(` ${dim(`${r.branch} ${r.remote ?? "(no remote)"}`)}`);
|
|
907
1290
|
}
|
|
908
1291
|
}
|
|
909
1292
|
console.log();
|
|
910
1293
|
const visibleArchived = data.works.filter((w) => w.isArchived === true);
|
|
911
1294
|
const visibleActive = data.works.filter((w) => w.isArchived !== true);
|
|
912
1295
|
const hiddenArchived = data.archivedWorksCount - visibleArchived.length;
|
|
913
|
-
|
|
914
|
-
console.log(` ${bold("works")} ${worksCount}`);
|
|
1296
|
+
console.log(` ${bold("works")}`);
|
|
915
1297
|
if (data.works.length === 0) {
|
|
916
1298
|
const empty = hiddenArchived > 0 ? `${hiddenArchived} archived hidden \u2014 pass --all to show` : "none yet \u2014 `mx work new <name>`";
|
|
917
1299
|
console.log(` ${dim(empty)}`);
|
|
@@ -930,6 +1312,7 @@ function renderStatus(data) {
|
|
|
930
1312
|
const chip = w.isArchived === true ? ` ${dim(`[archived ${(w.archived_at ?? "").slice(0, 10)}]`)}` : "";
|
|
931
1313
|
const styledName = w.isArchived === true ? dim(w.name) : bold(w.name);
|
|
932
1314
|
console.log(` \u2022 ${styledName}${chip}`);
|
|
1315
|
+
console.log(` ${dim(tildify(w.path))}`);
|
|
933
1316
|
if (wts.length === 0) {
|
|
934
1317
|
console.log(` ${dim("(no worktrees)")}`);
|
|
935
1318
|
continue;
|
|
@@ -958,6 +1341,7 @@ function dispatchRepo(positionals, flags) {
|
|
|
958
1341
|
case "add": {
|
|
959
1342
|
const url = need(positionals[2], "usage: mx repo add <git-url> [--name <n>]");
|
|
960
1343
|
const res = repoAdd(root, url, flags.name);
|
|
1344
|
+
stampRepoScripts(repoPath(root, res.name), templatesDir());
|
|
961
1345
|
emit(() => console.log(`${check()} cloned ${bold(res.name)} ${dim(`\u2192 ${res.path}`)}`), res);
|
|
962
1346
|
return;
|
|
963
1347
|
}
|
|
@@ -968,21 +1352,53 @@ function dispatchRepo(positionals, flags) {
|
|
|
968
1352
|
console.log(dim("no repos yet \u2014 `mx repo add <git-url>`"));
|
|
969
1353
|
return;
|
|
970
1354
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
console.log(`\u2022 ${name} ${branch} ${remote}`);
|
|
1355
|
+
for (let i = 0; i < repos.length; i++) {
|
|
1356
|
+
if (i > 0) console.log();
|
|
1357
|
+
const r = repos[i];
|
|
1358
|
+
console.log(`\u2022 ${bold(r.name)}`);
|
|
1359
|
+
console.log(` ${dim(tildify(r.path))}`);
|
|
1360
|
+
console.log(` ${dim(`${r.branch} ${r.remote ?? "(no remote)"}`)}`);
|
|
978
1361
|
}
|
|
979
1362
|
}, repos);
|
|
980
1363
|
return;
|
|
981
1364
|
}
|
|
1365
|
+
case "path": {
|
|
1366
|
+
const name = need(
|
|
1367
|
+
flags.name || ctxRepo,
|
|
1368
|
+
"which repo? pass -n <name> or run inside a repo (mx repo -n <name> path)"
|
|
1369
|
+
);
|
|
1370
|
+
const res = repoInfo(root, name);
|
|
1371
|
+
emit(() => console.log(res.path), { name: res.name, path: res.path });
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
982
1374
|
case "fetch": {
|
|
1375
|
+
if (flags.all) {
|
|
1376
|
+
const names = listReposInfo(root).map((r) => r.name);
|
|
1377
|
+
if (names.length === 0) {
|
|
1378
|
+
emit(() => console.log(dim("no repos yet \u2014 `mx repo add <git-url>`")), []);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
const out = [];
|
|
1382
|
+
const lines = [];
|
|
1383
|
+
for (const n of names) {
|
|
1384
|
+
try {
|
|
1385
|
+
const r = repoFetch(root, n);
|
|
1386
|
+
out.push(r);
|
|
1387
|
+
lines.push(
|
|
1388
|
+
`${check()} ${bold(r.name)} ${dim(`\u2014 ${r.remoteBranches.length} branch(es) on origin, now on ${r.branch}`)}`
|
|
1389
|
+
);
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
const msg = e instanceof MxError ? e.message : String(e);
|
|
1392
|
+
out.push({ name: n, error: msg });
|
|
1393
|
+
lines.push(`${warn()} ${bold(n)} ${dim(`\u2014 ${msg}`)}`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
emit(() => lines.forEach((l) => console.log(l)), out);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
983
1399
|
const name = need(
|
|
984
1400
|
flags.name || ctxRepo,
|
|
985
|
-
"which repo? pass -n <name
|
|
1401
|
+
"which repo? pass -n <name>, run inside a repo, or use --all (mx repo -n <name> fetch)"
|
|
986
1402
|
);
|
|
987
1403
|
const res = repoFetch(root, name);
|
|
988
1404
|
emit(
|
|
@@ -1116,6 +1532,115 @@ function renderHealthDetail(h) {
|
|
|
1116
1532
|
const hint = r.hint ? ` ${dim(r.hint)}` : "";
|
|
1117
1533
|
console.log(` ${label} ${value}${marker}${hint}`);
|
|
1118
1534
|
}
|
|
1535
|
+
if (h.extra) {
|
|
1536
|
+
console.log();
|
|
1537
|
+
console.log(` ${dim("health.sh")}`);
|
|
1538
|
+
for (const line of h.extra.split("\n")) console.log(` ${dim(line)}`);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/commands/work.ts
|
|
1543
|
+
import * as path8 from "path";
|
|
1544
|
+
|
|
1545
|
+
// src/open.ts
|
|
1546
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1547
|
+
function osascript(script) {
|
|
1548
|
+
try {
|
|
1549
|
+
execFileSync3("osascript", ["-e", script], { stdio: ["ignore", "ignore", "pipe"] });
|
|
1550
|
+
} catch (e) {
|
|
1551
|
+
const err = e;
|
|
1552
|
+
throw new MxError(
|
|
1553
|
+
`osascript failed: ${(err.stderr ?? err.message ?? "").toString().trim()}`,
|
|
1554
|
+
"OSASCRIPT"
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
function aplStr(s) {
|
|
1559
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1560
|
+
}
|
|
1561
|
+
function openWorkLayout(workdir, workspace) {
|
|
1562
|
+
if (process.platform !== "darwin") {
|
|
1563
|
+
throw new MxError("-o/--open is only supported on macOS", "UNSUPPORTED");
|
|
1564
|
+
}
|
|
1565
|
+
let editorProcess = "Cursor";
|
|
1566
|
+
try {
|
|
1567
|
+
execFileSync3("open", ["-a", "Cursor", workspace], { stdio: "ignore" });
|
|
1568
|
+
} catch {
|
|
1569
|
+
try {
|
|
1570
|
+
execFileSync3("open", ["-a", "Visual Studio Code", workspace], { stdio: "ignore" });
|
|
1571
|
+
editorProcess = "Code";
|
|
1572
|
+
} catch {
|
|
1573
|
+
editorProcess = "";
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
osascript(
|
|
1577
|
+
[
|
|
1578
|
+
'tell application "Terminal"',
|
|
1579
|
+
" activate",
|
|
1580
|
+
" set winCountBefore to count of windows",
|
|
1581
|
+
` do script "cd \\"${aplStr(workdir)}\\""`,
|
|
1582
|
+
" delay 0.5",
|
|
1583
|
+
" set winCountAfter to count of windows",
|
|
1584
|
+
"end tell",
|
|
1585
|
+
"if winCountAfter is less than or equal to winCountBefore then",
|
|
1586
|
+
' tell application "System Events" to tell process "Terminal"',
|
|
1587
|
+
" try",
|
|
1588
|
+
' click menu item "Move Tab to New Window" of menu "Window" of menu bar 1',
|
|
1589
|
+
" delay 0.4",
|
|
1590
|
+
" end try",
|
|
1591
|
+
" end tell",
|
|
1592
|
+
"end if",
|
|
1593
|
+
'tell application "System Events" to tell process "Terminal"',
|
|
1594
|
+
" try",
|
|
1595
|
+
' set value of attribute "AXFullScreen" of window 1 to true',
|
|
1596
|
+
" end try",
|
|
1597
|
+
"end tell"
|
|
1598
|
+
].join("\n")
|
|
1599
|
+
);
|
|
1600
|
+
if (editorProcess) {
|
|
1601
|
+
const appName = editorProcess === "Code" ? "Visual Studio Code" : "Cursor";
|
|
1602
|
+
osascript(
|
|
1603
|
+
[
|
|
1604
|
+
`tell application "${appName}" to activate`,
|
|
1605
|
+
// wait until the app is frontmost (cold launch can take a few seconds)
|
|
1606
|
+
'tell application "System Events"',
|
|
1607
|
+
" set n to 0",
|
|
1608
|
+
` repeat until (exists process "${editorProcess}") and (frontmost of process "${editorProcess}" is true)`,
|
|
1609
|
+
" delay 0.3",
|
|
1610
|
+
" set n to n + 1",
|
|
1611
|
+
" if n > 40 then exit repeat",
|
|
1612
|
+
" end repeat",
|
|
1613
|
+
"end tell",
|
|
1614
|
+
// let the workbench finish loading so it accepts the keybinding
|
|
1615
|
+
"delay 1.2",
|
|
1616
|
+
'tell application "System Events" to key code 3 using {control down, command down}'
|
|
1617
|
+
].join("\n")
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/hydrate.ts
|
|
1623
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1624
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1625
|
+
function runWorktreeHydrate(ctx, quiet) {
|
|
1626
|
+
const script = repoHydrateScript(ctx.root, ctx.repo);
|
|
1627
|
+
if (!existsSync2(script)) return { ran: false, ok: true, missing: true };
|
|
1628
|
+
const env = {
|
|
1629
|
+
...process.env,
|
|
1630
|
+
MX_RUNTIME: ctx.root,
|
|
1631
|
+
MX_WORK: ctx.work,
|
|
1632
|
+
MX_REPO: ctx.repo,
|
|
1633
|
+
MX_WORKTREE_PATH: ctx.worktreePath,
|
|
1634
|
+
MX_BRANCH: ctx.branch,
|
|
1635
|
+
MX_BASE: ctx.base ?? "",
|
|
1636
|
+
MX_WORK_PATH: workPath(ctx.root, ctx.work).path
|
|
1637
|
+
};
|
|
1638
|
+
const r = spawnSync3(script, [ctx.worktreePath, ctx.branch], {
|
|
1639
|
+
cwd: ctx.worktreePath,
|
|
1640
|
+
env,
|
|
1641
|
+
stdio: quiet ? ["ignore", "ignore", "ignore"] : "inherit"
|
|
1642
|
+
});
|
|
1643
|
+
return { ran: true, ok: r.status === 0, missing: false };
|
|
1119
1644
|
}
|
|
1120
1645
|
|
|
1121
1646
|
// src/commands/work.ts
|
|
@@ -1124,15 +1649,25 @@ function need2(v, msg) {
|
|
|
1124
1649
|
return v;
|
|
1125
1650
|
}
|
|
1126
1651
|
function dispatchWork(positionals, flags) {
|
|
1127
|
-
|
|
1652
|
+
let action = positionals[1];
|
|
1653
|
+
if (!action && flags.open) action = "open";
|
|
1128
1654
|
if (action === "new") {
|
|
1129
1655
|
const root2 = requireRuntime({ runtime: flags.runtime });
|
|
1130
|
-
const name2 = need2(positionals[2], "usage: mx work new <name> [--description <text>]");
|
|
1656
|
+
const name2 = need2(positionals[2], "usage: mx work new <name> [--description <text>] [-o|--open]");
|
|
1131
1657
|
const res = workNew(root2, name2, flags.description ?? "");
|
|
1132
1658
|
emit(() => {
|
|
1133
1659
|
console.log(`${check()} created work ${bold(res.name)}`);
|
|
1134
1660
|
console.log(` ${dim(res.path)}`);
|
|
1135
1661
|
}, res);
|
|
1662
|
+
if (flags.open) {
|
|
1663
|
+
try {
|
|
1664
|
+
openWorkLayout(res.path, workspaceFile(root2, res.name));
|
|
1665
|
+
} catch (e) {
|
|
1666
|
+
const msg = e instanceof MxError ? e.message : String(e);
|
|
1667
|
+
process.stderr.write(`${warn()} ${dim(`could not open layout: ${msg}`)}
|
|
1668
|
+
`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1136
1671
|
return;
|
|
1137
1672
|
}
|
|
1138
1673
|
if (action === "ls") {
|
|
@@ -1157,6 +1692,7 @@ function dispatchWork(positionals, flags) {
|
|
|
1157
1692
|
const chip = w.isArchived === true ? ` ${dim(`[archived ${(w.archived_at ?? "").slice(0, 10)}]`)}` : "";
|
|
1158
1693
|
const styledName = w.isArchived === true ? dim(w.name) : bold(w.name);
|
|
1159
1694
|
console.log(`\u2022 ${styledName}${chip}`);
|
|
1695
|
+
console.log(` ${dim(tildify(w.path))}`);
|
|
1160
1696
|
if (w.description) {
|
|
1161
1697
|
console.log(` ${dim(`\u2014 ${w.description}`)}`);
|
|
1162
1698
|
}
|
|
@@ -1204,6 +1740,22 @@ function dispatchWork(positionals, flags) {
|
|
|
1204
1740
|
emit(() => console.log(res.path), res);
|
|
1205
1741
|
return;
|
|
1206
1742
|
}
|
|
1743
|
+
case "open": {
|
|
1744
|
+
const res = workPath(root, name);
|
|
1745
|
+
try {
|
|
1746
|
+
openWorkLayout(res.path, workspaceFile(root, name));
|
|
1747
|
+
} catch (e) {
|
|
1748
|
+
const msg = e instanceof MxError ? e.message : String(e);
|
|
1749
|
+
process.stderr.write(`${warn()} ${dim(`could not open layout: ${msg}`)}
|
|
1750
|
+
`);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
emit(
|
|
1754
|
+
() => console.log(`${check()} opened ${bold(name)} ${dim("(Terminal + editor)")}`),
|
|
1755
|
+
{ work: name, opened: true }
|
|
1756
|
+
);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1207
1759
|
case "describe": {
|
|
1208
1760
|
const text = need2(positionals[2], "usage: mx work -n <name> describe <text>");
|
|
1209
1761
|
const work = workDescribe(root, name, text);
|
|
@@ -1294,7 +1846,7 @@ function workWorktree(root, name, positionals, flags) {
|
|
|
1294
1846
|
case "add": {
|
|
1295
1847
|
const repo = need2(
|
|
1296
1848
|
positionals[3],
|
|
1297
|
-
"usage: mx work -n <name> worktree add <repo> [--branch <b>] [--base <ref>]"
|
|
1849
|
+
"usage: mx work -n <name> worktree add <repo> [--branch <b>] [--base <ref>] [--no-hydrate]"
|
|
1298
1850
|
);
|
|
1299
1851
|
const res = worktreeAdd(root, name, repo, { branch: flags.branch, base: flags.base });
|
|
1300
1852
|
emit(
|
|
@@ -1303,6 +1855,18 @@ function workWorktree(root, name, positionals, flags) {
|
|
|
1303
1855
|
),
|
|
1304
1856
|
res
|
|
1305
1857
|
);
|
|
1858
|
+
if (!flags.noHydrate) {
|
|
1859
|
+
const outcome = runWorktreeHydrate(
|
|
1860
|
+
{ root, work: name, repo: res.repo, worktreePath: res.path, branch: res.branch, base: flags.base },
|
|
1861
|
+
flags.porcelain
|
|
1862
|
+
);
|
|
1863
|
+
if (outcome.ran && !outcome.ok && !flags.porcelain) {
|
|
1864
|
+
process.stderr.write(
|
|
1865
|
+
`${warn()} ${dim(`hydrate.sh for ${res.repo} exited non-zero \u2014 worktree kept. Re-run: mx work -n ${name} worktree hydrate ${res.repo}`)}
|
|
1866
|
+
`
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1306
1870
|
return;
|
|
1307
1871
|
}
|
|
1308
1872
|
case "ls": {
|
|
@@ -1334,6 +1898,31 @@ function workWorktree(root, name, positionals, flags) {
|
|
|
1334
1898
|
);
|
|
1335
1899
|
return;
|
|
1336
1900
|
}
|
|
1901
|
+
case "hydrate": {
|
|
1902
|
+
const repo = need2(positionals[3], "usage: mx work -n <name> worktree hydrate <repo>");
|
|
1903
|
+
const wt = worktreeList(root, name).find((w) => w.repo === repo);
|
|
1904
|
+
if (!wt) throw new MxError(`work "${name}" has no worktree for ${repo}`, "NO_WORKTREE");
|
|
1905
|
+
const worktreePath2 = path8.join(workPath(root, name).path, "wt", repo);
|
|
1906
|
+
const outcome = runWorktreeHydrate(
|
|
1907
|
+
{ root, work: name, repo, worktreePath: worktreePath2, branch: wt.branch },
|
|
1908
|
+
flags.porcelain
|
|
1909
|
+
);
|
|
1910
|
+
if (outcome.missing) {
|
|
1911
|
+
emit(
|
|
1912
|
+
() => console.log(dim(`no hydrate.sh for ${repo} \u2014 nothing to run`)),
|
|
1913
|
+
{ work: name, repo, ran: false }
|
|
1914
|
+
);
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (!outcome.ok) throw new MxError(`hydrate.sh for ${repo} exited non-zero`, "HYDRATE_FAILED");
|
|
1918
|
+
emit(() => console.log(`${check()} hydrated ${bold(repo)}`), {
|
|
1919
|
+
work: name,
|
|
1920
|
+
repo,
|
|
1921
|
+
ran: true,
|
|
1922
|
+
ok: true
|
|
1923
|
+
});
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1337
1926
|
default:
|
|
1338
1927
|
throw new MxError(`unknown worktree command: ${sub ?? "(none)"}`, "BAD_ARGS");
|
|
1339
1928
|
}
|
|
@@ -1404,13 +1993,13 @@ function workPort(root, name, positionals) {
|
|
|
1404
1993
|
// src/main.ts
|
|
1405
1994
|
var VERSION = (() => {
|
|
1406
1995
|
const here = path9.dirname(fileURLToPath2(import.meta.url));
|
|
1407
|
-
const pkg = JSON.parse(
|
|
1996
|
+
const pkg = JSON.parse(readFileSync3(path9.join(here, "..", "package.json"), "utf8"));
|
|
1408
1997
|
return pkg.version;
|
|
1409
1998
|
})();
|
|
1410
1999
|
function main() {
|
|
1411
2000
|
const { positionals, flags } = parseArgs(process.argv.slice(2));
|
|
1412
2001
|
setPorcelain(flags.porcelain);
|
|
1413
|
-
if (positionals[0] === "
|
|
2002
|
+
if (positionals[0] === "i") positionals[0] = "info";
|
|
1414
2003
|
try {
|
|
1415
2004
|
if (flags.version || positionals[0] === "version") {
|
|
1416
2005
|
emit(() => console.log(`mx ${VERSION}`), { version: VERSION });
|
|
@@ -1422,8 +2011,10 @@ function main() {
|
|
|
1422
2011
|
}
|
|
1423
2012
|
switch (positionals[0]) {
|
|
1424
2013
|
case "init":
|
|
1425
|
-
case "
|
|
2014
|
+
case "info":
|
|
2015
|
+
case "sync":
|
|
1426
2016
|
case "update":
|
|
2017
|
+
case "migrate":
|
|
1427
2018
|
return runGlobal(positionals, flags);
|
|
1428
2019
|
case "repo":
|
|
1429
2020
|
return dispatchRepo(positionals, flags);
|