@roulabs/mx 1.11.0 → 2.0.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 +16 -9
- package/bin/mx.js +641 -106
- 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,98 @@ 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"]);
|
|
273
494
|
gitQuiet(["-C", rp, "merge", "--ff-only", "@{u}"]);
|
|
274
495
|
return { name, branch: currentBranch(rp), remoteBranches: remoteBranchList(rp) };
|
|
275
496
|
}
|
|
276
497
|
function repoInfo(root, name) {
|
|
277
|
-
const rp =
|
|
498
|
+
const rp = repoGitDir(root, name);
|
|
278
499
|
if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
|
|
279
500
|
const usedBy = listWorkNames(root).filter((w) => findWorktree(readWork(root, w), name));
|
|
280
501
|
return {
|
|
281
502
|
name,
|
|
282
|
-
path:
|
|
503
|
+
path: repoPath(root, name),
|
|
504
|
+
// the container (repo home)
|
|
283
505
|
branch: currentBranch(rp),
|
|
284
506
|
remote: remoteUrl(rp),
|
|
285
507
|
worktreesInWorks: usedBy
|
|
@@ -321,8 +543,30 @@ function lastFetched(rp) {
|
|
|
321
543
|
if (!exists(fh)) return null;
|
|
322
544
|
return fs5.statSync(fh).mtime.toISOString();
|
|
323
545
|
}
|
|
546
|
+
function runHealthScript(root, name) {
|
|
547
|
+
const script = repoHealthScript(root, name);
|
|
548
|
+
if (!exists(script)) return null;
|
|
549
|
+
try {
|
|
550
|
+
const out = execFileSync2(script, [], {
|
|
551
|
+
cwd: repoGitDir(root, name),
|
|
552
|
+
encoding: "utf8",
|
|
553
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
554
|
+
env: {
|
|
555
|
+
...process.env,
|
|
556
|
+
MX_RUNTIME: root,
|
|
557
|
+
MX_REPO: name,
|
|
558
|
+
MX_REPO_PATH: repoPath(root, name),
|
|
559
|
+
MX_GIT_DIR: repoGitDir(root, name)
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
const trimmed = out.trim();
|
|
563
|
+
return trimmed.length ? trimmed : null;
|
|
564
|
+
} catch {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
324
568
|
function repoHealth(root, name) {
|
|
325
|
-
const rp =
|
|
569
|
+
const rp = repoGitDir(root, name);
|
|
326
570
|
if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
|
|
327
571
|
const defaultBranch = originDefaultBranch(rp);
|
|
328
572
|
const current = currentBranchOrNull(rp);
|
|
@@ -355,7 +599,8 @@ function repoHealth(root, name) {
|
|
|
355
599
|
}
|
|
356
600
|
return {
|
|
357
601
|
name,
|
|
358
|
-
path:
|
|
602
|
+
path: repoPath(root, name),
|
|
603
|
+
// the container (repo home)
|
|
359
604
|
defaultBranch,
|
|
360
605
|
currentBranch: current,
|
|
361
606
|
isOnDefault,
|
|
@@ -366,15 +611,15 @@ function repoHealth(root, name) {
|
|
|
366
611
|
lastFetchedAt,
|
|
367
612
|
worktreesInWorks,
|
|
368
613
|
healthy: issues.length === 0,
|
|
369
|
-
issues
|
|
614
|
+
issues,
|
|
615
|
+
extra: runHealthScript(root, name)
|
|
370
616
|
};
|
|
371
617
|
}
|
|
372
618
|
function listRepoHealth(root) {
|
|
373
619
|
return listRepoNames(root).map((name) => repoHealth(root, name));
|
|
374
620
|
}
|
|
375
621
|
function repoRemove(root, name) {
|
|
376
|
-
|
|
377
|
-
if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
|
|
622
|
+
if (!isGitRepo(repoGitDir(root, name))) throw new MxError(`no such repo: ${name}`, "NO_REPO");
|
|
378
623
|
const usedBy = listWorkNames(root).filter((w) => findWorktree(readWork(root, w), name));
|
|
379
624
|
if (usedBy.length) {
|
|
380
625
|
throw new MxError(
|
|
@@ -382,25 +627,25 @@ function repoRemove(root, name) {
|
|
|
382
627
|
"IN_USE"
|
|
383
628
|
);
|
|
384
629
|
}
|
|
385
|
-
fs5.rmSync(
|
|
630
|
+
fs5.rmSync(repoPath(root, name), { recursive: true, force: true });
|
|
386
631
|
return { name, removed: true };
|
|
387
632
|
}
|
|
388
633
|
|
|
389
634
|
// ../../packages/core/src/works.ts
|
|
390
635
|
import * as fs6 from "fs";
|
|
391
|
-
import * as path5 from "path";
|
|
392
636
|
function addFolderToWorkspace(root, name, repo) {
|
|
393
637
|
const file = workspaceFile(root, name);
|
|
394
638
|
const ws = exists(file) ? readJson(file) : { folders: [], settings: {} };
|
|
395
639
|
ws.folders = ws.folders ?? [];
|
|
396
|
-
|
|
640
|
+
const rel = `wt/${repo}`;
|
|
641
|
+
if (!ws.folders.some((f) => f.path === rel)) ws.folders.push({ name: repo, path: rel });
|
|
397
642
|
writeJson(file, ws);
|
|
398
643
|
}
|
|
399
644
|
function removeFolderFromWorkspace(root, name, repo) {
|
|
400
645
|
const file = workspaceFile(root, name);
|
|
401
646
|
if (!exists(file)) return;
|
|
402
647
|
const ws = readJson(file);
|
|
403
|
-
ws.folders = (ws.folders ?? []).filter((f) => f.path !== repo);
|
|
648
|
+
ws.folders = (ws.folders ?? []).filter((f) => f.path !== `wt/${repo}`);
|
|
404
649
|
writeJson(file, ws);
|
|
405
650
|
}
|
|
406
651
|
function clearWorkspaceFolders(root, name) {
|
|
@@ -423,6 +668,7 @@ function workNew(root, name, description = "") {
|
|
|
423
668
|
function listWorksInfo(root, opts = {}) {
|
|
424
669
|
return listWorkNames(root).map((name) => ({
|
|
425
670
|
...readWork(root, name),
|
|
671
|
+
path: workDir(root, name),
|
|
426
672
|
sessions: countSessions(root, name)
|
|
427
673
|
})).filter((w) => {
|
|
428
674
|
if (opts.onlyArchived) return w.isArchived === true;
|
|
@@ -446,13 +692,14 @@ function workPath(root, name) {
|
|
|
446
692
|
}
|
|
447
693
|
function worktreeAdd(root, name, repo, opts = {}) {
|
|
448
694
|
const work = readWork(root, name);
|
|
449
|
-
const rp =
|
|
695
|
+
const rp = repoGitDir(root, repo);
|
|
450
696
|
if (!isGitRepo(rp)) throw new MxError(`no such repo: ${repo}`, "NO_REPO");
|
|
451
697
|
if (findWorktree(work, repo)) {
|
|
452
698
|
throw new MxError(`work "${name}" already has worktree for ${repo}`, "EXISTS");
|
|
453
699
|
}
|
|
454
700
|
const branch = opts.branch || name;
|
|
455
|
-
const dest =
|
|
701
|
+
const dest = worktreePath(root, name, repo);
|
|
702
|
+
fs6.mkdirSync(worktreesDir(root, name), { recursive: true });
|
|
456
703
|
if (branchExists(rp, branch)) {
|
|
457
704
|
git(["-C", rp, "worktree", "add", dest, branch]);
|
|
458
705
|
} else {
|
|
@@ -479,11 +726,11 @@ function worktreeRemove(root, name, repo) {
|
|
|
479
726
|
const work = readWork(root, name);
|
|
480
727
|
const wt = findWorktree(work, repo);
|
|
481
728
|
if (!wt) throw new MxError(`work "${name}" has no worktree for ${repo}`, "NO_WORKTREE");
|
|
482
|
-
const dest =
|
|
729
|
+
const dest = worktreePath(root, name, repo);
|
|
483
730
|
if (exists(dest) && isDirty(dest)) {
|
|
484
731
|
throw new MxError(`worktree ${repo} has uncommitted changes \u2014 commit or discard them first`, "DIRTY");
|
|
485
732
|
}
|
|
486
|
-
git(["-C",
|
|
733
|
+
git(["-C", repoGitDir(root, repo), "worktree", "remove", dest]);
|
|
487
734
|
work.worktrees = work.worktrees.filter((w) => w.repo !== repo);
|
|
488
735
|
writeWork(root, work);
|
|
489
736
|
removeFolderFromWorkspace(root, name, repo);
|
|
@@ -499,7 +746,7 @@ function workDestroy(root, name, opts = {}) {
|
|
|
499
746
|
const work = readWork(root, name);
|
|
500
747
|
const dirty = [];
|
|
501
748
|
for (const wt of work.worktrees ?? []) {
|
|
502
|
-
const dest =
|
|
749
|
+
const dest = worktreePath(root, name, wt.repo);
|
|
503
750
|
if (exists(dest) && isDirty(dest)) dirty.push(wt.repo);
|
|
504
751
|
}
|
|
505
752
|
if (dirty.length) {
|
|
@@ -510,8 +757,8 @@ function workDestroy(root, name, opts = {}) {
|
|
|
510
757
|
}
|
|
511
758
|
const removed = [];
|
|
512
759
|
for (const wt of work.worktrees ?? []) {
|
|
513
|
-
const dest =
|
|
514
|
-
if (exists(dest)) git(["-C",
|
|
760
|
+
const dest = worktreePath(root, name, wt.repo);
|
|
761
|
+
if (exists(dest)) git(["-C", repoGitDir(root, wt.repo), "worktree", "remove", dest]);
|
|
515
762
|
removed.push(wt.repo);
|
|
516
763
|
}
|
|
517
764
|
fs6.rmSync(workDir(root, name), { recursive: true, force: true });
|
|
@@ -524,7 +771,7 @@ function archiveWork(root, name) {
|
|
|
524
771
|
}
|
|
525
772
|
const dirty = [];
|
|
526
773
|
for (const wt of work.worktrees ?? []) {
|
|
527
|
-
const dest =
|
|
774
|
+
const dest = worktreePath(root, name, wt.repo);
|
|
528
775
|
if (exists(dest) && isDirty(dest)) dirty.push(wt.repo);
|
|
529
776
|
}
|
|
530
777
|
if (dirty.length) {
|
|
@@ -535,8 +782,8 @@ function archiveWork(root, name) {
|
|
|
535
782
|
}
|
|
536
783
|
const removed = [];
|
|
537
784
|
for (const wt of work.worktrees ?? []) {
|
|
538
|
-
const dest =
|
|
539
|
-
if (exists(dest)) git(["-C",
|
|
785
|
+
const dest = worktreePath(root, name, wt.repo);
|
|
786
|
+
if (exists(dest)) git(["-C", repoGitDir(root, wt.repo), "worktree", "remove", dest]);
|
|
540
787
|
removed.push(wt.repo);
|
|
541
788
|
}
|
|
542
789
|
clearWorkspaceFolders(root, name);
|
|
@@ -558,7 +805,7 @@ function unarchiveWork(root, name, overrides = {}) {
|
|
|
558
805
|
}));
|
|
559
806
|
const missing = [];
|
|
560
807
|
for (const d of desired) {
|
|
561
|
-
const rp =
|
|
808
|
+
const rp = repoGitDir(root, d.repo);
|
|
562
809
|
if (!isGitRepo(rp)) {
|
|
563
810
|
throw new MxError(`pristine clone missing for repo: ${d.repo}`, "NO_REPO");
|
|
564
811
|
}
|
|
@@ -574,8 +821,9 @@ function unarchiveWork(root, name, overrides = {}) {
|
|
|
574
821
|
}
|
|
575
822
|
const restored = [];
|
|
576
823
|
for (const d of desired) {
|
|
577
|
-
const dest =
|
|
578
|
-
|
|
824
|
+
const dest = worktreePath(root, name, d.repo);
|
|
825
|
+
fs6.mkdirSync(worktreesDir(root, name), { recursive: true });
|
|
826
|
+
git(["-C", repoGitDir(root, d.repo), "worktree", "add", dest, d.branch]);
|
|
579
827
|
addFolderToWorkspace(root, name, d.repo);
|
|
580
828
|
restored.push({ repo: d.repo, branch: d.branch, path: dest, ports: d.ports });
|
|
581
829
|
}
|
|
@@ -654,9 +902,9 @@ function portList(root, name) {
|
|
|
654
902
|
}
|
|
655
903
|
|
|
656
904
|
// ../../packages/core/src/status.ts
|
|
657
|
-
import * as
|
|
905
|
+
import * as path5 from "path";
|
|
658
906
|
function countContextEntries(root) {
|
|
659
|
-
const indexPath =
|
|
907
|
+
const indexPath = path5.join(root, "context", "INDEX.json");
|
|
660
908
|
if (!exists(indexPath)) return 0;
|
|
661
909
|
try {
|
|
662
910
|
const data = readJson(indexPath);
|
|
@@ -696,7 +944,9 @@ function parseArgs(argv) {
|
|
|
696
944
|
force: false,
|
|
697
945
|
yes: false,
|
|
698
946
|
all: false,
|
|
699
|
-
archived: false
|
|
947
|
+
archived: false,
|
|
948
|
+
open: false,
|
|
949
|
+
noHydrate: false
|
|
700
950
|
};
|
|
701
951
|
for (let i = 0; i < argv.length; i++) {
|
|
702
952
|
const a = argv[i];
|
|
@@ -714,6 +964,10 @@ function parseArgs(argv) {
|
|
|
714
964
|
flags.all = true;
|
|
715
965
|
} else if (a === "--archived") {
|
|
716
966
|
flags.archived = true;
|
|
967
|
+
} else if (a === "--open" || a === "-o") {
|
|
968
|
+
flags.open = true;
|
|
969
|
+
} else if (a === "--no-hydrate") {
|
|
970
|
+
flags.noHydrate = true;
|
|
717
971
|
} else if (a.startsWith("--") && a.includes("=")) {
|
|
718
972
|
const eq = a.indexOf("=");
|
|
719
973
|
const key = VALUE_FLAGS[a.slice(0, eq)];
|
|
@@ -730,6 +984,7 @@ function parseArgs(argv) {
|
|
|
730
984
|
|
|
731
985
|
// src/output.ts
|
|
732
986
|
import { spawnSync } from "child_process";
|
|
987
|
+
import * as os2 from "os";
|
|
733
988
|
var USE_STYLE = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
734
989
|
function wrap(s, code) {
|
|
735
990
|
return USE_STYLE ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
@@ -746,6 +1001,11 @@ function check() {
|
|
|
746
1001
|
function warn() {
|
|
747
1002
|
return "\u26A0";
|
|
748
1003
|
}
|
|
1004
|
+
function tildify(p) {
|
|
1005
|
+
const home = os2.homedir();
|
|
1006
|
+
if (p === home) return "~";
|
|
1007
|
+
return p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
|
|
1008
|
+
}
|
|
749
1009
|
function confirmYesNo(prompt) {
|
|
750
1010
|
if (!process.stdin.isTTY) return false;
|
|
751
1011
|
process.stdout.write(prompt);
|
|
@@ -791,7 +1051,9 @@ var HELP = `mx \u2014 control panel for the mx runtime
|
|
|
791
1051
|
Global:
|
|
792
1052
|
mx init [path] scaffold/adopt a runtime (default ~/mx)
|
|
793
1053
|
mx status [--all] [--porcelain] show runtime, repos, works, ports (active only by default; --all to include archived; aliases: mx s, mx st)
|
|
794
|
-
mx
|
|
1054
|
+
mx sync re-stamp runtime files (CLAUDE.md, scaffolding) from current templates \u2014 same-major, non-breaking
|
|
1055
|
+
mx update self-update the mx CLI within its major (npm i -g); flags a newer major if one exists
|
|
1056
|
+
mx migrate upgrade an older-version runtime to the version this CLI supports (the only command allowed on a mismatched runtime)
|
|
795
1057
|
mx help | version
|
|
796
1058
|
|
|
797
1059
|
Repos (pristine clones):
|
|
@@ -804,14 +1066,15 @@ Repos (pristine clones):
|
|
|
804
1066
|
mx repo -n <name> rm refuses if any work uses it
|
|
805
1067
|
|
|
806
1068
|
Works (features):
|
|
807
|
-
mx work new <name> [--description <t>]
|
|
1069
|
+
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
1070
|
mx work ls [--all|--archived] [--porcelain] default: active only; --all includes archived; --archived shows archived only
|
|
809
1071
|
mx work -n <name> info [--porcelain]
|
|
810
1072
|
mx work -n <name> path print the work folder path (cd "$(mx work -n <name> path)")
|
|
811
1073
|
mx work -n <name> describe <text>
|
|
812
|
-
mx work -n <name> worktree add <repo> [--branch <b>] [--base <ref>]
|
|
1074
|
+
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
1075
|
mx work -n <name> worktree ls [--porcelain]
|
|
814
1076
|
mx work -n <name> worktree rm <repo> refuses on uncommitted changes; keeps branch
|
|
1077
|
+
mx work -n <name> worktree hydrate <repo> re-run the repo's hydrate.sh against its worktree
|
|
815
1078
|
mx work -n <name> port set <repo> <service> [<port>] auto-picks a free port if omitted
|
|
816
1079
|
mx work -n <name> port unset <repo> <service>
|
|
817
1080
|
mx work -n <name> port ls [--porcelain]
|
|
@@ -827,20 +1090,89 @@ Runtime discovery: --runtime <path> -> $MX_RUNTIME -> default ~/mx.
|
|
|
827
1090
|
`;
|
|
828
1091
|
|
|
829
1092
|
// src/commands/global.ts
|
|
830
|
-
import * as
|
|
1093
|
+
import * as path7 from "path";
|
|
831
1094
|
|
|
832
1095
|
// src/paths.ts
|
|
833
|
-
import
|
|
1096
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1097
|
+
import * as path6 from "path";
|
|
834
1098
|
import { fileURLToPath } from "url";
|
|
835
1099
|
function templatesDir() {
|
|
836
1100
|
if (process.env.MX_TEMPLATES_DIR) return process.env.MX_TEMPLATES_DIR;
|
|
837
|
-
const here =
|
|
838
|
-
return
|
|
1101
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
1102
|
+
return path6.join(here, "..", "templates");
|
|
1103
|
+
}
|
|
1104
|
+
function cliVersion() {
|
|
1105
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
1106
|
+
const pkg = JSON.parse(readFileSync2(path6.join(here, "..", "package.json"), "utf8"));
|
|
1107
|
+
return pkg.version;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/selfupdate.ts
|
|
1111
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1112
|
+
var PKG = "@roulabs/mx";
|
|
1113
|
+
function major(v) {
|
|
1114
|
+
return Number.parseInt(v.split(".")[0], 10) || 0;
|
|
1115
|
+
}
|
|
1116
|
+
function npmAvailable() {
|
|
1117
|
+
try {
|
|
1118
|
+
const r = spawnSync2("npm", ["--version"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
1119
|
+
return r.status === 0;
|
|
1120
|
+
} catch {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
function npmViewJson(spec, field) {
|
|
1125
|
+
const r = spawnSync2("npm", ["view", spec, field, "--json"], {
|
|
1126
|
+
encoding: "utf8",
|
|
1127
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1128
|
+
});
|
|
1129
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
1130
|
+
try {
|
|
1131
|
+
return JSON.parse(r.stdout);
|
|
1132
|
+
} catch {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function latestForRange(range) {
|
|
1137
|
+
const v = npmViewJson(`${PKG}@${range}`, "version");
|
|
1138
|
+
if (typeof v === "string") return v;
|
|
1139
|
+
if (Array.isArray(v) && v.length) return String(v[v.length - 1]);
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
function latestOverall() {
|
|
1143
|
+
const v = npmViewJson(PKG, "version");
|
|
1144
|
+
return typeof v === "string" ? v : null;
|
|
1145
|
+
}
|
|
1146
|
+
function selfUpdate(porcelain2) {
|
|
1147
|
+
const current = cliVersion();
|
|
1148
|
+
const curMajor = major(current);
|
|
1149
|
+
const info = {
|
|
1150
|
+
package: PKG,
|
|
1151
|
+
current,
|
|
1152
|
+
npmAvailable: false,
|
|
1153
|
+
latestInMajor: null,
|
|
1154
|
+
updated: false,
|
|
1155
|
+
installFailed: false,
|
|
1156
|
+
newMajor: null
|
|
1157
|
+
};
|
|
1158
|
+
if (!npmAvailable()) return info;
|
|
1159
|
+
info.npmAvailable = true;
|
|
1160
|
+
info.latestInMajor = latestForRange(`^${curMajor}`);
|
|
1161
|
+
const overall = latestOverall();
|
|
1162
|
+
if (overall && major(overall) > curMajor) info.newMajor = major(overall);
|
|
1163
|
+
if (info.latestInMajor && info.latestInMajor !== current) {
|
|
1164
|
+
const r = spawnSync2("npm", ["i", "-g", `${PKG}@^${curMajor}`], {
|
|
1165
|
+
stdio: porcelain2 ? ["ignore", "pipe", "pipe"] : "inherit"
|
|
1166
|
+
});
|
|
1167
|
+
if (r.status === 0) info.updated = true;
|
|
1168
|
+
else info.installFailed = true;
|
|
1169
|
+
}
|
|
1170
|
+
return info;
|
|
839
1171
|
}
|
|
840
1172
|
|
|
841
1173
|
// src/commands/global.ts
|
|
842
1174
|
function runtimeEnvHint(runtime) {
|
|
843
|
-
const envRuntime = process.env.MX_RUNTIME ?
|
|
1175
|
+
const envRuntime = process.env.MX_RUNTIME ? path7.resolve(process.env.MX_RUNTIME) : null;
|
|
844
1176
|
if (envRuntime === runtime) {
|
|
845
1177
|
return ["", `${dim("$MX_RUNTIME already points here \u2014 you're set.")}`];
|
|
846
1178
|
}
|
|
@@ -874,19 +1206,62 @@ function runGlobal(positionals, flags) {
|
|
|
874
1206
|
emit(() => renderStatus(data), data);
|
|
875
1207
|
return;
|
|
876
1208
|
}
|
|
877
|
-
case "
|
|
1209
|
+
case "sync": {
|
|
878
1210
|
const root = requireRuntime({ runtime: flags.runtime });
|
|
879
|
-
const res =
|
|
1211
|
+
const res = syncRuntime(root, templatesDir());
|
|
880
1212
|
emit(() => {
|
|
881
|
-
console.log(`${check()}
|
|
1213
|
+
console.log(`${check()} Synced runtime at ${bold(res.runtime)}`);
|
|
882
1214
|
for (const p of res.updated) console.log(` ${dim(`+ ${p}`)}`);
|
|
883
1215
|
}, res);
|
|
884
1216
|
return;
|
|
885
1217
|
}
|
|
1218
|
+
case "migrate": {
|
|
1219
|
+
const root = requireRuntime({ runtime: flags.runtime, allowVersionMismatch: true });
|
|
1220
|
+
const res = migrateRuntime(root);
|
|
1221
|
+
emit(() => {
|
|
1222
|
+
if (res.applied.length === 0) {
|
|
1223
|
+
console.log(`${check()} Runtime already at v${res.to} \u2014 nothing to migrate.`);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const steps = res.applied.map((a) => `v${a.from}\u2192v${a.to}`).join(", ");
|
|
1227
|
+
console.log(
|
|
1228
|
+
`${check()} Migrated runtime ${bold(`v${res.from}\u2192v${res.to}`)} ${dim(`(${steps})`)}`
|
|
1229
|
+
);
|
|
1230
|
+
for (const p of res.changed) console.log(` ${dim(`~ ${p}`)}`);
|
|
1231
|
+
}, res);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
case "update": {
|
|
1235
|
+
const info = selfUpdate(flags.porcelain);
|
|
1236
|
+
emit(() => renderSelfUpdate(info), info);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
886
1239
|
default:
|
|
887
1240
|
throw new MxError(`unknown command: ${positionals[0]}`, "BAD_ARGS");
|
|
888
1241
|
}
|
|
889
1242
|
}
|
|
1243
|
+
function renderSelfUpdate(info) {
|
|
1244
|
+
const curMajor = Number.parseInt(info.current.split(".")[0], 10) || 0;
|
|
1245
|
+
const manual = `npm i -g ${info.package}@^${curMajor}`;
|
|
1246
|
+
if (!info.npmAvailable) {
|
|
1247
|
+
console.log(`${warn()} npm not found \u2014 update manually: ${bold(manual)}`);
|
|
1248
|
+
} else if (info.installFailed) {
|
|
1249
|
+
console.log(`${warn()} self-update failed \u2014 try: ${bold(manual)}`);
|
|
1250
|
+
} else if (info.updated) {
|
|
1251
|
+
console.log(
|
|
1252
|
+
`${check()} Updated mx to ${bold(`v${info.latestInMajor}`)} ${dim(`(was v${info.current})`)}`
|
|
1253
|
+
);
|
|
1254
|
+
} else {
|
|
1255
|
+
console.log(`${check()} mx is up to date ${dim(`(v${info.current}, latest in v${curMajor}.x)`)}`);
|
|
1256
|
+
}
|
|
1257
|
+
if (info.newMajor) {
|
|
1258
|
+
console.log();
|
|
1259
|
+
console.log(`${warn()} A new major release ${bold(`mx v${info.newMajor}`)} is available.`);
|
|
1260
|
+
console.log(
|
|
1261
|
+
` ${dim(`Upgrade (optional): npm i -g ${info.package}@${info.newMajor}, then `)}${bold("mx migrate")}`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
890
1265
|
function renderStatus(data) {
|
|
891
1266
|
console.log();
|
|
892
1267
|
console.log(` ${bold("mx")} ${dim("\xB7")} ${data.runtime}`);
|
|
@@ -958,6 +1333,7 @@ function dispatchRepo(positionals, flags) {
|
|
|
958
1333
|
case "add": {
|
|
959
1334
|
const url = need(positionals[2], "usage: mx repo add <git-url> [--name <n>]");
|
|
960
1335
|
const res = repoAdd(root, url, flags.name);
|
|
1336
|
+
stampRepoScripts(repoPath(root, res.name), templatesDir());
|
|
961
1337
|
emit(() => console.log(`${check()} cloned ${bold(res.name)} ${dim(`\u2192 ${res.path}`)}`), res);
|
|
962
1338
|
return;
|
|
963
1339
|
}
|
|
@@ -975,6 +1351,7 @@ function dispatchRepo(positionals, flags) {
|
|
|
975
1351
|
const branch = dim(r.branch.padEnd(branchW));
|
|
976
1352
|
const remote = dim(r.remote ?? "(no remote)");
|
|
977
1353
|
console.log(`\u2022 ${name} ${branch} ${remote}`);
|
|
1354
|
+
console.log(` ${dim(tildify(r.path))}`);
|
|
978
1355
|
}
|
|
979
1356
|
}, repos);
|
|
980
1357
|
return;
|
|
@@ -1116,6 +1493,115 @@ function renderHealthDetail(h) {
|
|
|
1116
1493
|
const hint = r.hint ? ` ${dim(r.hint)}` : "";
|
|
1117
1494
|
console.log(` ${label} ${value}${marker}${hint}`);
|
|
1118
1495
|
}
|
|
1496
|
+
if (h.extra) {
|
|
1497
|
+
console.log();
|
|
1498
|
+
console.log(` ${dim("health.sh")}`);
|
|
1499
|
+
for (const line of h.extra.split("\n")) console.log(` ${dim(line)}`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// src/commands/work.ts
|
|
1504
|
+
import * as path8 from "path";
|
|
1505
|
+
|
|
1506
|
+
// src/open.ts
|
|
1507
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1508
|
+
function osascript(script) {
|
|
1509
|
+
try {
|
|
1510
|
+
execFileSync3("osascript", ["-e", script], { stdio: ["ignore", "ignore", "pipe"] });
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
const err = e;
|
|
1513
|
+
throw new MxError(
|
|
1514
|
+
`osascript failed: ${(err.stderr ?? err.message ?? "").toString().trim()}`,
|
|
1515
|
+
"OSASCRIPT"
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
function aplStr(s) {
|
|
1520
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1521
|
+
}
|
|
1522
|
+
function openWorkLayout(workdir, workspace) {
|
|
1523
|
+
if (process.platform !== "darwin") {
|
|
1524
|
+
throw new MxError("-o/--open is only supported on macOS", "UNSUPPORTED");
|
|
1525
|
+
}
|
|
1526
|
+
let editorProcess = "Cursor";
|
|
1527
|
+
try {
|
|
1528
|
+
execFileSync3("open", ["-a", "Cursor", workspace], { stdio: "ignore" });
|
|
1529
|
+
} catch {
|
|
1530
|
+
try {
|
|
1531
|
+
execFileSync3("open", ["-a", "Visual Studio Code", workspace], { stdio: "ignore" });
|
|
1532
|
+
editorProcess = "Code";
|
|
1533
|
+
} catch {
|
|
1534
|
+
editorProcess = "";
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
osascript(
|
|
1538
|
+
[
|
|
1539
|
+
'tell application "Terminal"',
|
|
1540
|
+
" activate",
|
|
1541
|
+
" set winCountBefore to count of windows",
|
|
1542
|
+
` do script "cd \\"${aplStr(workdir)}\\""`,
|
|
1543
|
+
" delay 0.5",
|
|
1544
|
+
" set winCountAfter to count of windows",
|
|
1545
|
+
"end tell",
|
|
1546
|
+
"if winCountAfter is less than or equal to winCountBefore then",
|
|
1547
|
+
' tell application "System Events" to tell process "Terminal"',
|
|
1548
|
+
" try",
|
|
1549
|
+
' click menu item "Move Tab to New Window" of menu "Window" of menu bar 1',
|
|
1550
|
+
" delay 0.4",
|
|
1551
|
+
" end try",
|
|
1552
|
+
" end tell",
|
|
1553
|
+
"end if",
|
|
1554
|
+
'tell application "System Events" to tell process "Terminal"',
|
|
1555
|
+
" try",
|
|
1556
|
+
' set value of attribute "AXFullScreen" of window 1 to true',
|
|
1557
|
+
" end try",
|
|
1558
|
+
"end tell"
|
|
1559
|
+
].join("\n")
|
|
1560
|
+
);
|
|
1561
|
+
if (editorProcess) {
|
|
1562
|
+
const appName = editorProcess === "Code" ? "Visual Studio Code" : "Cursor";
|
|
1563
|
+
osascript(
|
|
1564
|
+
[
|
|
1565
|
+
`tell application "${appName}" to activate`,
|
|
1566
|
+
// wait until the app is frontmost (cold launch can take a few seconds)
|
|
1567
|
+
'tell application "System Events"',
|
|
1568
|
+
" set n to 0",
|
|
1569
|
+
` repeat until (exists process "${editorProcess}") and (frontmost of process "${editorProcess}" is true)`,
|
|
1570
|
+
" delay 0.3",
|
|
1571
|
+
" set n to n + 1",
|
|
1572
|
+
" if n > 40 then exit repeat",
|
|
1573
|
+
" end repeat",
|
|
1574
|
+
"end tell",
|
|
1575
|
+
// let the workbench finish loading so it accepts the keybinding
|
|
1576
|
+
"delay 1.2",
|
|
1577
|
+
'tell application "System Events" to key code 3 using {control down, command down}'
|
|
1578
|
+
].join("\n")
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/hydrate.ts
|
|
1584
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1585
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1586
|
+
function runWorktreeHydrate(ctx, quiet) {
|
|
1587
|
+
const script = repoHydrateScript(ctx.root, ctx.repo);
|
|
1588
|
+
if (!existsSync2(script)) return { ran: false, ok: true, missing: true };
|
|
1589
|
+
const env = {
|
|
1590
|
+
...process.env,
|
|
1591
|
+
MX_RUNTIME: ctx.root,
|
|
1592
|
+
MX_WORK: ctx.work,
|
|
1593
|
+
MX_REPO: ctx.repo,
|
|
1594
|
+
MX_WORKTREE_PATH: ctx.worktreePath,
|
|
1595
|
+
MX_BRANCH: ctx.branch,
|
|
1596
|
+
MX_BASE: ctx.base ?? "",
|
|
1597
|
+
MX_WORK_PATH: workPath(ctx.root, ctx.work).path
|
|
1598
|
+
};
|
|
1599
|
+
const r = spawnSync3(script, [ctx.worktreePath, ctx.branch], {
|
|
1600
|
+
cwd: ctx.worktreePath,
|
|
1601
|
+
env,
|
|
1602
|
+
stdio: quiet ? ["ignore", "ignore", "ignore"] : "inherit"
|
|
1603
|
+
});
|
|
1604
|
+
return { ran: true, ok: r.status === 0, missing: false };
|
|
1119
1605
|
}
|
|
1120
1606
|
|
|
1121
1607
|
// src/commands/work.ts
|
|
@@ -1127,12 +1613,21 @@ function dispatchWork(positionals, flags) {
|
|
|
1127
1613
|
const action = positionals[1];
|
|
1128
1614
|
if (action === "new") {
|
|
1129
1615
|
const root2 = requireRuntime({ runtime: flags.runtime });
|
|
1130
|
-
const name2 = need2(positionals[2], "usage: mx work new <name> [--description <text>]");
|
|
1616
|
+
const name2 = need2(positionals[2], "usage: mx work new <name> [--description <text>] [-o|--open]");
|
|
1131
1617
|
const res = workNew(root2, name2, flags.description ?? "");
|
|
1132
1618
|
emit(() => {
|
|
1133
1619
|
console.log(`${check()} created work ${bold(res.name)}`);
|
|
1134
1620
|
console.log(` ${dim(res.path)}`);
|
|
1135
1621
|
}, res);
|
|
1622
|
+
if (flags.open) {
|
|
1623
|
+
try {
|
|
1624
|
+
openWorkLayout(res.path, workspaceFile(root2, res.name));
|
|
1625
|
+
} catch (e) {
|
|
1626
|
+
const msg = e instanceof MxError ? e.message : String(e);
|
|
1627
|
+
process.stderr.write(`${warn()} ${dim(`could not open layout: ${msg}`)}
|
|
1628
|
+
`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1136
1631
|
return;
|
|
1137
1632
|
}
|
|
1138
1633
|
if (action === "ls") {
|
|
@@ -1157,6 +1652,7 @@ function dispatchWork(positionals, flags) {
|
|
|
1157
1652
|
const chip = w.isArchived === true ? ` ${dim(`[archived ${(w.archived_at ?? "").slice(0, 10)}]`)}` : "";
|
|
1158
1653
|
const styledName = w.isArchived === true ? dim(w.name) : bold(w.name);
|
|
1159
1654
|
console.log(`\u2022 ${styledName}${chip}`);
|
|
1655
|
+
console.log(` ${dim(tildify(w.path))}`);
|
|
1160
1656
|
if (w.description) {
|
|
1161
1657
|
console.log(` ${dim(`\u2014 ${w.description}`)}`);
|
|
1162
1658
|
}
|
|
@@ -1294,7 +1790,7 @@ function workWorktree(root, name, positionals, flags) {
|
|
|
1294
1790
|
case "add": {
|
|
1295
1791
|
const repo = need2(
|
|
1296
1792
|
positionals[3],
|
|
1297
|
-
"usage: mx work -n <name> worktree add <repo> [--branch <b>] [--base <ref>]"
|
|
1793
|
+
"usage: mx work -n <name> worktree add <repo> [--branch <b>] [--base <ref>] [--no-hydrate]"
|
|
1298
1794
|
);
|
|
1299
1795
|
const res = worktreeAdd(root, name, repo, { branch: flags.branch, base: flags.base });
|
|
1300
1796
|
emit(
|
|
@@ -1303,6 +1799,18 @@ function workWorktree(root, name, positionals, flags) {
|
|
|
1303
1799
|
),
|
|
1304
1800
|
res
|
|
1305
1801
|
);
|
|
1802
|
+
if (!flags.noHydrate) {
|
|
1803
|
+
const outcome = runWorktreeHydrate(
|
|
1804
|
+
{ root, work: name, repo: res.repo, worktreePath: res.path, branch: res.branch, base: flags.base },
|
|
1805
|
+
flags.porcelain
|
|
1806
|
+
);
|
|
1807
|
+
if (outcome.ran && !outcome.ok && !flags.porcelain) {
|
|
1808
|
+
process.stderr.write(
|
|
1809
|
+
`${warn()} ${dim(`hydrate.sh for ${res.repo} exited non-zero \u2014 worktree kept. Re-run: mx work -n ${name} worktree hydrate ${res.repo}`)}
|
|
1810
|
+
`
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1306
1814
|
return;
|
|
1307
1815
|
}
|
|
1308
1816
|
case "ls": {
|
|
@@ -1334,6 +1842,31 @@ function workWorktree(root, name, positionals, flags) {
|
|
|
1334
1842
|
);
|
|
1335
1843
|
return;
|
|
1336
1844
|
}
|
|
1845
|
+
case "hydrate": {
|
|
1846
|
+
const repo = need2(positionals[3], "usage: mx work -n <name> worktree hydrate <repo>");
|
|
1847
|
+
const wt = worktreeList(root, name).find((w) => w.repo === repo);
|
|
1848
|
+
if (!wt) throw new MxError(`work "${name}" has no worktree for ${repo}`, "NO_WORKTREE");
|
|
1849
|
+
const worktreePath2 = path8.join(workPath(root, name).path, "wt", repo);
|
|
1850
|
+
const outcome = runWorktreeHydrate(
|
|
1851
|
+
{ root, work: name, repo, worktreePath: worktreePath2, branch: wt.branch },
|
|
1852
|
+
flags.porcelain
|
|
1853
|
+
);
|
|
1854
|
+
if (outcome.missing) {
|
|
1855
|
+
emit(
|
|
1856
|
+
() => console.log(dim(`no hydrate.sh for ${repo} \u2014 nothing to run`)),
|
|
1857
|
+
{ work: name, repo, ran: false }
|
|
1858
|
+
);
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (!outcome.ok) throw new MxError(`hydrate.sh for ${repo} exited non-zero`, "HYDRATE_FAILED");
|
|
1862
|
+
emit(() => console.log(`${check()} hydrated ${bold(repo)}`), {
|
|
1863
|
+
work: name,
|
|
1864
|
+
repo,
|
|
1865
|
+
ran: true,
|
|
1866
|
+
ok: true
|
|
1867
|
+
});
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1337
1870
|
default:
|
|
1338
1871
|
throw new MxError(`unknown worktree command: ${sub ?? "(none)"}`, "BAD_ARGS");
|
|
1339
1872
|
}
|
|
@@ -1404,7 +1937,7 @@ function workPort(root, name, positionals) {
|
|
|
1404
1937
|
// src/main.ts
|
|
1405
1938
|
var VERSION = (() => {
|
|
1406
1939
|
const here = path9.dirname(fileURLToPath2(import.meta.url));
|
|
1407
|
-
const pkg = JSON.parse(
|
|
1940
|
+
const pkg = JSON.parse(readFileSync3(path9.join(here, "..", "package.json"), "utf8"));
|
|
1408
1941
|
return pkg.version;
|
|
1409
1942
|
})();
|
|
1410
1943
|
function main() {
|
|
@@ -1423,7 +1956,9 @@ function main() {
|
|
|
1423
1956
|
switch (positionals[0]) {
|
|
1424
1957
|
case "init":
|
|
1425
1958
|
case "status":
|
|
1959
|
+
case "sync":
|
|
1426
1960
|
case "update":
|
|
1961
|
+
case "migrate":
|
|
1427
1962
|
return runGlobal(positionals, flags);
|
|
1428
1963
|
case "repo":
|
|
1429
1964
|
return dispatchRepo(positionals, flags);
|