laracrew 0.2.1 → 0.3.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/CHANGELOG.md +236 -188
- package/README.md +84 -10
- package/dist/index.js +1017 -542
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/program.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import
|
|
4
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
5
|
+
import path12 from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
|
|
@@ -315,6 +315,7 @@ var mergeServiceFragments = (document, dir) => {
|
|
|
315
315
|
const existing = Array.isArray(document.services) ? document.services : [];
|
|
316
316
|
return { ...document, services: [...existing, ...extra] };
|
|
317
317
|
};
|
|
318
|
+
var validateStack = (document, file) => validate(stackSchema, document, file);
|
|
318
319
|
var loadStack = (name, env = process.env) => {
|
|
319
320
|
const dir = stackDir(name, env);
|
|
320
321
|
const file = stackFile(name, env);
|
|
@@ -345,41 +346,192 @@ var listTasks = (env = process.env) => {
|
|
|
345
346
|
};
|
|
346
347
|
|
|
347
348
|
// src/cli/render/colors.ts
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
349
|
+
import { Chalk, supportsColor as chalkDetected } from "chalk";
|
|
350
|
+
|
|
351
|
+
// src/core/logs/sanitize.ts
|
|
352
|
+
var ESC = String.fromCharCode(27);
|
|
353
|
+
var BEL = String.fromCharCode(7);
|
|
354
|
+
var CSI = new RegExp(ESC + "\\[[0-?]*[ -/]*[@-~]", "g");
|
|
355
|
+
var OSC = new RegExp(ESC + "\\][^" + BEL + ESC + "]*(?:" + BEL + "|" + ESC + "\\\\)", "g");
|
|
356
|
+
var SINGLE = new RegExp(ESC + "[=>78MD]", "g");
|
|
357
|
+
var SGR = new RegExp(ESC + "\\[[0-9;]*m", "g");
|
|
358
|
+
var stripAnsi = (text) => text.replace(SGR, "");
|
|
359
|
+
var TAB = 9;
|
|
360
|
+
var ESC_CODE = 27;
|
|
361
|
+
var sanitize = (input) => {
|
|
362
|
+
let out = input.replace(OSC, "").replace(SINGLE, "");
|
|
363
|
+
out = out.replace(CSI, (seq) => seq.endsWith("m") ? seq : "");
|
|
364
|
+
if (out.includes("\r")) {
|
|
365
|
+
const segments = out.split("\r").filter((segment) => segment !== "");
|
|
366
|
+
out = segments.length > 0 ? segments[segments.length - 1] : "";
|
|
367
|
+
}
|
|
368
|
+
return out.replace(new RegExp("\\p{Cc}", "gu"), (character) => {
|
|
369
|
+
const code = character.charCodeAt(0);
|
|
370
|
+
return code === TAB || code === ESC_CODE ? character : "";
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
var LineSplitter = class {
|
|
374
|
+
#buffer = "";
|
|
375
|
+
#maxLineLength;
|
|
376
|
+
constructor(maxLineLength = 16384) {
|
|
377
|
+
this.#maxLineLength = maxLineLength;
|
|
378
|
+
}
|
|
379
|
+
push(chunk) {
|
|
380
|
+
this.#buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
381
|
+
const lines = [];
|
|
382
|
+
let index = this.#buffer.indexOf("\n");
|
|
383
|
+
while (index !== -1) {
|
|
384
|
+
const line = this.#buffer.slice(0, index).replace(/\r$/, "");
|
|
385
|
+
lines.push(sanitize(line));
|
|
386
|
+
this.#buffer = this.#buffer.slice(index + 1);
|
|
387
|
+
index = this.#buffer.indexOf("\n");
|
|
388
|
+
}
|
|
389
|
+
if (this.#buffer.length > this.#maxLineLength) {
|
|
390
|
+
lines.push(sanitize(this.#buffer));
|
|
391
|
+
this.#buffer = "";
|
|
392
|
+
}
|
|
393
|
+
return lines;
|
|
394
|
+
}
|
|
395
|
+
/** Emits whatever is left when the stream closes. */
|
|
396
|
+
flush() {
|
|
397
|
+
if (this.#buffer === "") return [];
|
|
398
|
+
const line = sanitize(this.#buffer.replace(/\r$/, ""));
|
|
399
|
+
this.#buffer = "";
|
|
400
|
+
return line === "" ? [] : [line];
|
|
401
|
+
}
|
|
363
402
|
};
|
|
403
|
+
|
|
404
|
+
// src/cli/render/colors.ts
|
|
405
|
+
var NAMES = [
|
|
406
|
+
"reset",
|
|
407
|
+
"bold",
|
|
408
|
+
"dim",
|
|
409
|
+
"italic",
|
|
410
|
+
"underline",
|
|
411
|
+
"black",
|
|
412
|
+
"red",
|
|
413
|
+
"green",
|
|
414
|
+
"yellow",
|
|
415
|
+
"blue",
|
|
416
|
+
"magenta",
|
|
417
|
+
"cyan",
|
|
418
|
+
"white",
|
|
419
|
+
"gray"
|
|
420
|
+
];
|
|
364
421
|
var supportsColor = (stream = process.stdout, env = process.env) => {
|
|
365
422
|
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
366
|
-
if (env.FORCE_COLOR !== void 0
|
|
423
|
+
if (env.FORCE_COLOR !== void 0) return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "false";
|
|
367
424
|
if (env.TERM === "dumb") return false;
|
|
368
425
|
return Boolean(stream.isTTY);
|
|
369
426
|
};
|
|
427
|
+
var levelFor = (enabled) => enabled ? Math.max(1, chalkDetected ? chalkDetected.level : 1) : 0;
|
|
428
|
+
var createColor = (enabled) => new Chalk({ level: levelFor(enabled) });
|
|
429
|
+
var color = createColor(supportsColor());
|
|
370
430
|
var createPainter = (enabled) => {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
return paint2;
|
|
431
|
+
const chalk = createColor(enabled);
|
|
432
|
+
return Object.assign((name, text) => chalk[name](text), { enabled });
|
|
374
433
|
};
|
|
375
434
|
var paint = createPainter(supportsColor());
|
|
376
|
-
var asColorName = (value) => value && value
|
|
377
|
-
var stripAnsi = (text) => text.replace(/\[[0-9;]*m/g, "");
|
|
435
|
+
var asColorName = (value) => value && NAMES.includes(value) ? value : "cyan";
|
|
378
436
|
var padEnd = (text, width) => {
|
|
379
437
|
const visible = stripAnsi(text).length;
|
|
380
438
|
return visible >= width ? text : text + " ".repeat(width - visible);
|
|
381
439
|
};
|
|
382
440
|
|
|
441
|
+
// src/tui/theme.ts
|
|
442
|
+
var asciiOnly = (env = process.env) => env.LARACREW_ASCII === "1" || env.TERM === "dumb";
|
|
443
|
+
var UNICODE = {
|
|
444
|
+
running: "\u25CF",
|
|
445
|
+
ready: "\u25CF",
|
|
446
|
+
starting: "\u25D0",
|
|
447
|
+
stopped: "\u25FC",
|
|
448
|
+
failed: "\u2716",
|
|
449
|
+
queued: "\u25CB",
|
|
450
|
+
branch: "\u251C\u2500",
|
|
451
|
+
lastBranch: "\u2514\u2500",
|
|
452
|
+
pipe: "\u2502",
|
|
453
|
+
pointer: "\u25B8",
|
|
454
|
+
restart: "\u27F3",
|
|
455
|
+
rule: "\u2500",
|
|
456
|
+
dot: "\xB7",
|
|
457
|
+
tick: "\u2714",
|
|
458
|
+
warn: "\u25B2",
|
|
459
|
+
arrow: "\u2192"
|
|
460
|
+
};
|
|
461
|
+
var ASCII = {
|
|
462
|
+
running: "*",
|
|
463
|
+
ready: "*",
|
|
464
|
+
starting: "o",
|
|
465
|
+
stopped: "#",
|
|
466
|
+
failed: "X",
|
|
467
|
+
queued: ".",
|
|
468
|
+
branch: "|-",
|
|
469
|
+
lastBranch: "`-",
|
|
470
|
+
pipe: "|",
|
|
471
|
+
pointer: ">",
|
|
472
|
+
restart: "r",
|
|
473
|
+
rule: "-",
|
|
474
|
+
dot: ".",
|
|
475
|
+
tick: "v",
|
|
476
|
+
warn: "!",
|
|
477
|
+
arrow: "->"
|
|
478
|
+
};
|
|
479
|
+
var glyphs = (env = process.env) => asciiOnly(env) ? ASCII : UNICODE;
|
|
480
|
+
var STATE_STYLE = {
|
|
481
|
+
queued: { glyph: "queued", color: "gray", label: "QUEUED" },
|
|
482
|
+
starting: { glyph: "starting", color: "yellow", label: "STARTING" },
|
|
483
|
+
ready: { glyph: "ready", color: "cyan", label: "READY" },
|
|
484
|
+
running: { glyph: "running", color: "green", label: "RUNNING" },
|
|
485
|
+
stopping: { glyph: "starting", color: "yellow", label: "STOPPING" },
|
|
486
|
+
stopped: { glyph: "stopped", color: "gray", label: "STOPPED" },
|
|
487
|
+
crashed: { glyph: "failed", color: "red", label: "CRASHED" },
|
|
488
|
+
backoff: { glyph: "starting", color: "yellow", label: "RESTARTING" },
|
|
489
|
+
failed: { glyph: "failed", color: "red", label: "FAILED" }
|
|
490
|
+
};
|
|
491
|
+
var formatUptime = (ms) => {
|
|
492
|
+
if (ms === void 0 || ms < 0) return "--:--:--";
|
|
493
|
+
const total = Math.floor(ms / 1e3);
|
|
494
|
+
const hours = Math.floor(total / 3600);
|
|
495
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
496
|
+
const seconds = total % 60;
|
|
497
|
+
return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
|
|
498
|
+
};
|
|
499
|
+
var formatDuration = (ms) => {
|
|
500
|
+
if (ms === void 0 || ms < 0) return "\u2014";
|
|
501
|
+
const total = Math.floor(ms / 1e3);
|
|
502
|
+
if (total < 60) return `${total}s`;
|
|
503
|
+
const minutes = Math.floor(total / 60);
|
|
504
|
+
const seconds = total % 60;
|
|
505
|
+
if (minutes < 60) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
506
|
+
const hours = Math.floor(minutes / 60);
|
|
507
|
+
return `${hours}h ${String(minutes % 60).padStart(2, "0")}m`;
|
|
508
|
+
};
|
|
509
|
+
var formatClock = (at) => {
|
|
510
|
+
const date = new Date(at);
|
|
511
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()].map((part) => String(part).padStart(2, "0")).join(":");
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/cli/render/marks.ts
|
|
515
|
+
var marks = (env = process.env) => {
|
|
516
|
+
const glyph = glyphs(env);
|
|
517
|
+
return {
|
|
518
|
+
ok: color.green(glyph.tick),
|
|
519
|
+
fail: color.red(glyph.failed),
|
|
520
|
+
warn: color.yellow(glyph.warn),
|
|
521
|
+
skip: color.gray(glyph.dot),
|
|
522
|
+
hint: color.cyan(glyph.pointer),
|
|
523
|
+
arrow: color.gray(glyph.arrow)
|
|
524
|
+
};
|
|
525
|
+
};
|
|
526
|
+
var nextSteps = (rows, env = process.env) => {
|
|
527
|
+
const mark = marks(env);
|
|
528
|
+
const width = Math.max(...rows.map(([command]) => command.length));
|
|
529
|
+
const lines = rows.map(
|
|
530
|
+
([command, why]) => command ? ` ${mark.hint} ${color.cyan(padEnd(command, width))} ${why}` : `${" ".repeat(width + 6)}${why}`
|
|
531
|
+
);
|
|
532
|
+
return [color.bold("Next"), ...lines].join("\n");
|
|
533
|
+
};
|
|
534
|
+
|
|
383
535
|
// src/cli/commands/doctor.ts
|
|
384
536
|
import { existsSync as existsSync4 } from "fs";
|
|
385
537
|
import path7 from "path";
|
|
@@ -1063,57 +1215,6 @@ var RingBuffer = class {
|
|
|
1063
1215
|
}
|
|
1064
1216
|
};
|
|
1065
1217
|
|
|
1066
|
-
// src/core/logs/sanitize.ts
|
|
1067
|
-
var ESC = String.fromCharCode(27);
|
|
1068
|
-
var BEL = String.fromCharCode(7);
|
|
1069
|
-
var CSI = new RegExp(ESC + "\\[[0-?]*[ -/]*[@-~]", "g");
|
|
1070
|
-
var OSC = new RegExp(ESC + "\\][^" + BEL + ESC + "]*(?:" + BEL + "|" + ESC + "\\\\)", "g");
|
|
1071
|
-
var SINGLE = new RegExp(ESC + "[=>78MD]", "g");
|
|
1072
|
-
var TAB = 9;
|
|
1073
|
-
var ESC_CODE = 27;
|
|
1074
|
-
var sanitize = (input) => {
|
|
1075
|
-
let out = input.replace(OSC, "").replace(SINGLE, "");
|
|
1076
|
-
out = out.replace(CSI, (seq) => seq.endsWith("m") ? seq : "");
|
|
1077
|
-
if (out.includes("\r")) {
|
|
1078
|
-
const segments = out.split("\r").filter((segment) => segment !== "");
|
|
1079
|
-
out = segments.length > 0 ? segments[segments.length - 1] : "";
|
|
1080
|
-
}
|
|
1081
|
-
return out.replace(new RegExp("\\p{Cc}", "gu"), (character) => {
|
|
1082
|
-
const code = character.charCodeAt(0);
|
|
1083
|
-
return code === TAB || code === ESC_CODE ? character : "";
|
|
1084
|
-
});
|
|
1085
|
-
};
|
|
1086
|
-
var LineSplitter = class {
|
|
1087
|
-
#buffer = "";
|
|
1088
|
-
#maxLineLength;
|
|
1089
|
-
constructor(maxLineLength = 16384) {
|
|
1090
|
-
this.#maxLineLength = maxLineLength;
|
|
1091
|
-
}
|
|
1092
|
-
push(chunk) {
|
|
1093
|
-
this.#buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
1094
|
-
const lines = [];
|
|
1095
|
-
let index = this.#buffer.indexOf("\n");
|
|
1096
|
-
while (index !== -1) {
|
|
1097
|
-
const line = this.#buffer.slice(0, index).replace(/\r$/, "");
|
|
1098
|
-
lines.push(sanitize(line));
|
|
1099
|
-
this.#buffer = this.#buffer.slice(index + 1);
|
|
1100
|
-
index = this.#buffer.indexOf("\n");
|
|
1101
|
-
}
|
|
1102
|
-
if (this.#buffer.length > this.#maxLineLength) {
|
|
1103
|
-
lines.push(sanitize(this.#buffer));
|
|
1104
|
-
this.#buffer = "";
|
|
1105
|
-
}
|
|
1106
|
-
return lines;
|
|
1107
|
-
}
|
|
1108
|
-
/** Emits whatever is left when the stream closes. */
|
|
1109
|
-
flush() {
|
|
1110
|
-
if (this.#buffer === "") return [];
|
|
1111
|
-
const line = sanitize(this.#buffer.replace(/\r$/, ""));
|
|
1112
|
-
this.#buffer = "";
|
|
1113
|
-
return line === "" ? [] : [line];
|
|
1114
|
-
}
|
|
1115
|
-
};
|
|
1116
|
-
|
|
1117
1218
|
// src/core/process/stop.ts
|
|
1118
1219
|
import { spawn as spawn2 } from "child_process";
|
|
1119
1220
|
var IS_WINDOWS2 = process.platform === "win32";
|
|
@@ -1612,6 +1713,45 @@ var Supervisor = class {
|
|
|
1612
1713
|
const process2 = this.#requireProcess(name);
|
|
1613
1714
|
await process2.restart();
|
|
1614
1715
|
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Restarts every service that is `running` right now and nothing else: a stopped or idle
|
|
1718
|
+
* service stays where it is, a failed one is not resurrected, one still starting is left to
|
|
1719
|
+
* finish. Dependency order, parallel within a level, so a dependency is back and ready
|
|
1720
|
+
* before the services that need it are bounced. Returns the names restarted, in that order.
|
|
1721
|
+
*/
|
|
1722
|
+
async restartRunning() {
|
|
1723
|
+
const isTarget = (process2) => !process2.service.external && process2.state === "running";
|
|
1724
|
+
const wanted = new Set(this.processes.filter(isTarget).map((process2) => process2.service.name));
|
|
1725
|
+
if (wanted.size === 0) return [];
|
|
1726
|
+
this.#bus.emit({
|
|
1727
|
+
type: "notice",
|
|
1728
|
+
level: "info",
|
|
1729
|
+
message: `restarting ${wanted.size} running service${wanted.size === 1 ? "" : "s"}: ${[...wanted].join(", ")}`,
|
|
1730
|
+
at: Date.now()
|
|
1731
|
+
});
|
|
1732
|
+
const restarted = /* @__PURE__ */ new Set();
|
|
1733
|
+
for (const level of this.stack.levels) {
|
|
1734
|
+
await Promise.all(
|
|
1735
|
+
level.filter((name) => wanted.has(name)).map(async (name) => {
|
|
1736
|
+
const process2 = this.#processes.get(name);
|
|
1737
|
+
if (!isTarget(process2)) return;
|
|
1738
|
+
try {
|
|
1739
|
+
await process2.restart();
|
|
1740
|
+
restarted.add(name);
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
this.#bus.emit({
|
|
1743
|
+
type: "notice",
|
|
1744
|
+
level: "error",
|
|
1745
|
+
service: name,
|
|
1746
|
+
message: `restart failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1747
|
+
at: Date.now()
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
})
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
return this.stack.levels.flat().filter((name) => restarted.has(name));
|
|
1754
|
+
}
|
|
1615
1755
|
async stopService(name) {
|
|
1616
1756
|
await this.#requireProcess(name).stop();
|
|
1617
1757
|
}
|
|
@@ -1682,6 +1822,7 @@ var time = (date) => `${String(date.getHours()).padStart(2, "0")}:${String(date.
|
|
|
1682
1822
|
var attachPlainRenderer = (bus, options) => {
|
|
1683
1823
|
const write2 = options.write ?? ((text) => write(text));
|
|
1684
1824
|
const painter = options.painter ?? paint;
|
|
1825
|
+
const glyph = options.glyph ?? glyphs();
|
|
1685
1826
|
const showTime = options.timestamps ?? true;
|
|
1686
1827
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1687
1828
|
const colorOf = new Map(options.stack.services.map((service) => [service.name, asColorName(service.color)]));
|
|
@@ -1692,10 +1833,19 @@ var attachPlainRenderer = (bus, options) => {
|
|
|
1692
1833
|
write2(`${stamp2}${prefix} ${painter("gray", "|")} ${text}
|
|
1693
1834
|
`);
|
|
1694
1835
|
};
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1697
|
-
|
|
1836
|
+
const note = (tone, text) => {
|
|
1837
|
+
switch (tone) {
|
|
1838
|
+
case "ok":
|
|
1839
|
+
return `${painter("green", glyph.tick)} ${painter("green", text)}`;
|
|
1840
|
+
case "warn":
|
|
1841
|
+
return `${painter("yellow", glyph.warn)} ${painter("yellow", text)}`;
|
|
1842
|
+
case "error":
|
|
1843
|
+
return `${painter("red", glyph.failed)} ${painter("red", text)}`;
|
|
1844
|
+
default:
|
|
1845
|
+
return painter("gray", text);
|
|
1846
|
+
}
|
|
1698
1847
|
};
|
|
1848
|
+
const system = (text, tone = "info") => line("laracrew", note(tone, text), "bold");
|
|
1699
1849
|
const handle = (event) => {
|
|
1700
1850
|
switch (event.type) {
|
|
1701
1851
|
case "service:log":
|
|
@@ -1710,7 +1860,7 @@ var attachPlainRenderer = (bus, options) => {
|
|
|
1710
1860
|
break;
|
|
1711
1861
|
}
|
|
1712
1862
|
case "stack:ready":
|
|
1713
|
-
system(`${event.stack} ready in ${(event.durationMs / 1e3).toFixed(1)}s
|
|
1863
|
+
system(`${event.stack} ready in ${(event.durationMs / 1e3).toFixed(1)}s`, "ok");
|
|
1714
1864
|
break;
|
|
1715
1865
|
case "stack:stopping":
|
|
1716
1866
|
system(`stopping ${event.stack}\u2026`);
|
|
@@ -1722,30 +1872,32 @@ var attachPlainRenderer = (bus, options) => {
|
|
|
1722
1872
|
line(event.service, painter("gray", `$ ${event.command}`));
|
|
1723
1873
|
break;
|
|
1724
1874
|
case "service:ready":
|
|
1725
|
-
line(event.service,
|
|
1875
|
+
line(event.service, note("ok", `ready (${event.probe}) in ${event.durationMs}ms`));
|
|
1726
1876
|
break;
|
|
1727
1877
|
case "service:exit":
|
|
1728
1878
|
if (!event.intentional) {
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
);
|
|
1879
|
+
const clean = event.code === 0 && !event.signal;
|
|
1880
|
+
const why = event.signal ? `signal ${event.signal}` : `code ${event.code}`;
|
|
1881
|
+
line(event.service, note(clean ? "warn" : "error", `exited (${why})`));
|
|
1733
1882
|
}
|
|
1734
1883
|
break;
|
|
1735
1884
|
case "service:restart":
|
|
1736
1885
|
line(
|
|
1737
1886
|
event.service,
|
|
1738
|
-
painter("yellow",
|
|
1887
|
+
`${painter("yellow", glyph.restart)} ${painter(
|
|
1888
|
+
"yellow",
|
|
1889
|
+
`restarting in ${event.delayMs}ms (attempt ${event.attempt}/${event.maxAttempts})`
|
|
1890
|
+
)}`
|
|
1739
1891
|
);
|
|
1740
1892
|
break;
|
|
1741
1893
|
case "service:state":
|
|
1742
1894
|
if (event.to === "failed") {
|
|
1743
|
-
line(event.service,
|
|
1895
|
+
line(event.service, note("error", `failed${event.detail ? `: ${event.detail}` : ""}`));
|
|
1744
1896
|
}
|
|
1745
1897
|
break;
|
|
1746
1898
|
case "notice":
|
|
1747
1899
|
if (event.service) {
|
|
1748
|
-
line(event.service,
|
|
1900
|
+
line(event.service, note(event.level, event.message));
|
|
1749
1901
|
} else {
|
|
1750
1902
|
system(event.message, event.level);
|
|
1751
1903
|
}
|
|
@@ -1834,6 +1986,7 @@ var handleKey = (ui, key, rowCount) => {
|
|
|
1834
1986
|
case "t":
|
|
1835
1987
|
return { ui, command: { type: "toggle", index: ui.selected } };
|
|
1836
1988
|
case "r":
|
|
1989
|
+
if (key.shift) return { ui, command: { type: "restart-running" } };
|
|
1837
1990
|
return { ui, command: { type: "restart", index: ui.selected } };
|
|
1838
1991
|
case "a":
|
|
1839
1992
|
return none({ ...ui, view: ui.view === "merged" ? "overview" : "merged", scrollBack: 0, follow: true });
|
|
@@ -1885,92 +2038,23 @@ var handleKey = (ui, key, rowCount) => {
|
|
|
1885
2038
|
}
|
|
1886
2039
|
};
|
|
1887
2040
|
|
|
1888
|
-
// src/tui/
|
|
1889
|
-
var
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
restart: "\u27F3",
|
|
1902
|
-
rule: "\u2500",
|
|
1903
|
-
dot: "\xB7",
|
|
1904
|
-
tick: "\u2714"
|
|
2041
|
+
// src/tui/screen.ts
|
|
2042
|
+
var initialUiState = () => ({
|
|
2043
|
+
view: "overview",
|
|
2044
|
+
selected: 0,
|
|
2045
|
+
follow: true,
|
|
2046
|
+
scrollBack: 0,
|
|
2047
|
+
confirmQuit: false,
|
|
2048
|
+
phase: "booting",
|
|
2049
|
+
note: "Booting\u2026"
|
|
2050
|
+
});
|
|
2051
|
+
var pad = (text, width) => {
|
|
2052
|
+
const visible = stripAnsi(text).length;
|
|
2053
|
+
return visible >= width ? text : text + " ".repeat(width - visible);
|
|
1905
2054
|
};
|
|
1906
|
-
var
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
starting: "o",
|
|
1910
|
-
stopped: "#",
|
|
1911
|
-
failed: "X",
|
|
1912
|
-
queued: ".",
|
|
1913
|
-
branch: "|-",
|
|
1914
|
-
lastBranch: "`-",
|
|
1915
|
-
pipe: "|",
|
|
1916
|
-
pointer: ">",
|
|
1917
|
-
restart: "r",
|
|
1918
|
-
rule: "-",
|
|
1919
|
-
dot: ".",
|
|
1920
|
-
tick: "v"
|
|
1921
|
-
};
|
|
1922
|
-
var glyphs = (env = process.env) => asciiOnly(env) ? ASCII : UNICODE;
|
|
1923
|
-
var STATE_STYLE = {
|
|
1924
|
-
queued: { glyph: "queued", color: "gray", label: "QUEUED" },
|
|
1925
|
-
starting: { glyph: "starting", color: "yellow", label: "STARTING" },
|
|
1926
|
-
ready: { glyph: "ready", color: "cyan", label: "READY" },
|
|
1927
|
-
running: { glyph: "running", color: "green", label: "RUNNING" },
|
|
1928
|
-
stopping: { glyph: "starting", color: "yellow", label: "STOPPING" },
|
|
1929
|
-
stopped: { glyph: "stopped", color: "gray", label: "STOPPED" },
|
|
1930
|
-
crashed: { glyph: "failed", color: "red", label: "CRASHED" },
|
|
1931
|
-
backoff: { glyph: "starting", color: "yellow", label: "RESTARTING" },
|
|
1932
|
-
failed: { glyph: "failed", color: "red", label: "FAILED" }
|
|
1933
|
-
};
|
|
1934
|
-
var formatUptime = (ms) => {
|
|
1935
|
-
if (ms === void 0 || ms < 0) return "--:--:--";
|
|
1936
|
-
const total = Math.floor(ms / 1e3);
|
|
1937
|
-
const hours = Math.floor(total / 3600);
|
|
1938
|
-
const minutes = Math.floor(total % 3600 / 60);
|
|
1939
|
-
const seconds = total % 60;
|
|
1940
|
-
return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
|
|
1941
|
-
};
|
|
1942
|
-
var formatDuration = (ms) => {
|
|
1943
|
-
if (ms === void 0 || ms < 0) return "\u2014";
|
|
1944
|
-
const total = Math.floor(ms / 1e3);
|
|
1945
|
-
if (total < 60) return `${total}s`;
|
|
1946
|
-
const minutes = Math.floor(total / 60);
|
|
1947
|
-
const seconds = total % 60;
|
|
1948
|
-
if (minutes < 60) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
1949
|
-
const hours = Math.floor(minutes / 60);
|
|
1950
|
-
return `${hours}h ${String(minutes % 60).padStart(2, "0")}m`;
|
|
1951
|
-
};
|
|
1952
|
-
var formatClock = (at) => {
|
|
1953
|
-
const date = new Date(at);
|
|
1954
|
-
return [date.getHours(), date.getMinutes(), date.getSeconds()].map((part) => String(part).padStart(2, "0")).join(":");
|
|
1955
|
-
};
|
|
1956
|
-
|
|
1957
|
-
// src/tui/screen.ts
|
|
1958
|
-
var initialUiState = () => ({
|
|
1959
|
-
view: "overview",
|
|
1960
|
-
selected: 0,
|
|
1961
|
-
follow: true,
|
|
1962
|
-
scrollBack: 0,
|
|
1963
|
-
confirmQuit: false,
|
|
1964
|
-
phase: "booting",
|
|
1965
|
-
note: "Booting\u2026"
|
|
1966
|
-
});
|
|
1967
|
-
var pad = (text, width) => {
|
|
1968
|
-
const visible = stripAnsi(text).length;
|
|
1969
|
-
return visible >= width ? text : text + " ".repeat(width - visible);
|
|
1970
|
-
};
|
|
1971
|
-
var truncate = (text, width) => {
|
|
1972
|
-
if (width <= 1) return "";
|
|
1973
|
-
return stripAnsi(text).length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
2055
|
+
var truncate = (text, width) => {
|
|
2056
|
+
if (width <= 1) return "";
|
|
2057
|
+
return stripAnsi(text).length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
1974
2058
|
};
|
|
1975
2059
|
var rule = (width, glyph) => glyph.rule.repeat(Math.max(0, width));
|
|
1976
2060
|
var header = ({ stack, ui, size, paint: paint2, glyph, uptimeMs }) => {
|
|
@@ -1997,6 +2081,7 @@ var KEYS = {
|
|
|
1997
2081
|
["enter", "inspect log"],
|
|
1998
2082
|
["0-9", "jump"],
|
|
1999
2083
|
["r", "restart"],
|
|
2084
|
+
["R", "restart all running"],
|
|
2000
2085
|
["s", "stop/start"],
|
|
2001
2086
|
["a", "all logs"],
|
|
2002
2087
|
["?", "help"],
|
|
@@ -2008,6 +2093,7 @@ var KEYS = {
|
|
|
2008
2093
|
["g/G", "top/bottom"],
|
|
2009
2094
|
["f", "follow"],
|
|
2010
2095
|
["r", "restart"],
|
|
2096
|
+
["R", "restart all running"],
|
|
2011
2097
|
["q", "quit"]
|
|
2012
2098
|
],
|
|
2013
2099
|
merged: [
|
|
@@ -2015,6 +2101,7 @@ var KEYS = {
|
|
|
2015
2101
|
["up/down", "scroll"],
|
|
2016
2102
|
["g/G", "top/bottom"],
|
|
2017
2103
|
["f", "follow"],
|
|
2104
|
+
["R", "restart all running"],
|
|
2018
2105
|
["q", "quit"]
|
|
2019
2106
|
],
|
|
2020
2107
|
help: [["any key", "back"]]
|
|
@@ -2053,8 +2140,8 @@ var processLine = (row, isSelected, isLast, nameWidth, { paint: paint2, glyph, s
|
|
|
2053
2140
|
var dependencyLine = (row, nameWidth, { paint: paint2, glyph, size }) => {
|
|
2054
2141
|
const style = STATE_STYLE[row.state];
|
|
2055
2142
|
const label = row.state === "ready" || row.state === "running" ? "ready" : style.label.toLowerCase();
|
|
2056
|
-
const
|
|
2057
|
-
const line = ` ${paint2(
|
|
2143
|
+
const color2 = row.state === "ready" || row.state === "running" ? "green" : style.color;
|
|
2144
|
+
const line = ` ${paint2(color2, glyph[style.glyph])} ${pad(row.name, nameWidth)}${paint2(color2, pad(label, 11))}${paint2("gray", row.probe ?? "")}`;
|
|
2058
2145
|
return truncate(line, size.columns + (line.length - stripAnsi(line).length));
|
|
2059
2146
|
};
|
|
2060
2147
|
var dependencies = (input, nameWidth) => {
|
|
@@ -2141,6 +2228,7 @@ var help = ({ paint: paint2, glyph }) => [
|
|
|
2141
2228
|
["esc, s", "back to the stack overview"],
|
|
2142
2229
|
["a", "merged log across every service"],
|
|
2143
2230
|
["r", "restart the selected process (graceful)"],
|
|
2231
|
+
["R", "restart every running process, in dependency order; stopped and idle stay put"],
|
|
2144
2232
|
["t", "toggle the selected process: stop if running, start if stopped"],
|
|
2145
2233
|
["f", "follow / pause tailing in a log view"],
|
|
2146
2234
|
["g / G", "jump to the top / bottom of a log"],
|
|
@@ -2311,7 +2399,7 @@ var runTui = async (supervisor, bus, options = {}) => {
|
|
|
2311
2399
|
});
|
|
2312
2400
|
const basePainter = createPainter(supportsColor(stdout, env));
|
|
2313
2401
|
const paint2 = Object.assign(
|
|
2314
|
-
(
|
|
2402
|
+
(color2, text) => basePainter(color2, text),
|
|
2315
2403
|
{ enabled: basePainter.enabled }
|
|
2316
2404
|
);
|
|
2317
2405
|
const glyph = glyphs(env);
|
|
@@ -2370,7 +2458,7 @@ var runTui = async (supervisor, bus, options = {}) => {
|
|
|
2370
2458
|
};
|
|
2371
2459
|
const perform = (command) => {
|
|
2372
2460
|
const stack = buildStackView(supervisor);
|
|
2373
|
-
const row = stack.rows[
|
|
2461
|
+
const row = stack.rows["index" in command ? command.index : ui.selected];
|
|
2374
2462
|
switch (command.type) {
|
|
2375
2463
|
case "quit":
|
|
2376
2464
|
void shutdown("user quit");
|
|
@@ -2384,6 +2472,14 @@ var runTui = async (supervisor, bus, options = {}) => {
|
|
|
2384
2472
|
setNote(`restarting ${row.name}\u2026`);
|
|
2385
2473
|
void supervisor.restart(row.name).then(() => setNote(`${row.name} restarted`)).catch((error) => setNote(`restart failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
2386
2474
|
break;
|
|
2475
|
+
case "restart-running":
|
|
2476
|
+
setNote("restarting every running process\u2026");
|
|
2477
|
+
void supervisor.restartRunning().then(
|
|
2478
|
+
(names) => setNote(
|
|
2479
|
+
names.length === 0 ? "nothing is running \u2014 nothing to restart" : `restarted ${names.length} process${names.length === 1 ? "" : "es"}: ${names.join(", ")}`
|
|
2480
|
+
)
|
|
2481
|
+
).catch((error) => setNote(`restart failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
2482
|
+
break;
|
|
2387
2483
|
case "toggle": {
|
|
2388
2484
|
if (!row) break;
|
|
2389
2485
|
if (row.external) {
|
|
@@ -2471,6 +2567,7 @@ var shouldUseTui = (options, stdout = process.stdout) => !options.plain && !opti
|
|
|
2471
2567
|
var upCommand = async (name, options = {}) => {
|
|
2472
2568
|
const stack = prepareStack(name, options);
|
|
2473
2569
|
const bus = new EventBus();
|
|
2570
|
+
const mark = marks(options.env);
|
|
2474
2571
|
if (shouldUseTui(options)) {
|
|
2475
2572
|
const supervisor2 = new Supervisor(stack, bus, options.env ? { env: options.env } : {});
|
|
2476
2573
|
return runTui(supervisor2, bus, options.env ? { env: options.env } : {});
|
|
@@ -2486,7 +2583,8 @@ var upCommand = async (name, options = {}) => {
|
|
|
2486
2583
|
};
|
|
2487
2584
|
const onSignal = (signal) => {
|
|
2488
2585
|
if (stopping) {
|
|
2489
|
-
console.error(
|
|
2586
|
+
console.error(`
|
|
2587
|
+
${mark.fail} ${color.red.bold("forced exit \u2014 some children may survive")}`);
|
|
2490
2588
|
process.exit(130);
|
|
2491
2589
|
}
|
|
2492
2590
|
void shutdown(`received ${signal}`);
|
|
@@ -2500,7 +2598,7 @@ var upCommand = async (name, options = {}) => {
|
|
|
2500
2598
|
await supervisor.up();
|
|
2501
2599
|
await waitUntilIdle(supervisor, () => stopping);
|
|
2502
2600
|
} catch (error) {
|
|
2503
|
-
console.error(
|
|
2601
|
+
console.error(`${mark.fail} ${color.red(error instanceof Error ? error.message : String(error))}`);
|
|
2504
2602
|
exitCode = 1;
|
|
2505
2603
|
} finally {
|
|
2506
2604
|
await shutdown("done");
|
|
@@ -2816,303 +2914,67 @@ var runDoctor = async (stackName, env = process.env) => {
|
|
|
2816
2914
|
};
|
|
2817
2915
|
var doctorCommand = async (stackName, env = process.env) => {
|
|
2818
2916
|
const findings = await runDoctor(stackName, env);
|
|
2819
|
-
const
|
|
2917
|
+
const mark = marks(env);
|
|
2918
|
+
console.log(`${color.bold("doctor")} ${color.gray(glyphs(env).dot)} ${color.cyan.bold(stackName)}
|
|
2919
|
+
`);
|
|
2920
|
+
const lead = { ok: mark.ok, warn: mark.warn, error: mark.fail };
|
|
2921
|
+
const tone = { ok: (text) => text, warn: color.yellow, error: color.red };
|
|
2820
2922
|
for (const finding of findings) {
|
|
2821
|
-
console.log(`${
|
|
2822
|
-
if (finding.hint) console.log(` ${
|
|
2923
|
+
console.log(`${lead[finding.level]} ${tone[finding.level](finding.message)}`);
|
|
2924
|
+
if (finding.hint) console.log(` ${color.gray(finding.hint)}`);
|
|
2823
2925
|
}
|
|
2824
2926
|
const errors = findings.filter((finding) => finding.level === "error").length;
|
|
2825
2927
|
const warnings = findings.filter((finding) => finding.level === "warn").length;
|
|
2928
|
+
const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
2929
|
+
const tail = warnings > 0 ? color.yellow(`, ${count(warnings, "warning")}`) : "";
|
|
2826
2930
|
console.log(
|
|
2827
|
-
`
|
|
2828
|
-
${
|
|
2931
|
+
errors === 0 ? `
|
|
2932
|
+
${mark.ok} ${color.green.bold("no blocking problems")}${tail}` : `
|
|
2933
|
+
${mark.fail} ${color.red.bold(count(errors, "problem"))}${tail}`
|
|
2829
2934
|
);
|
|
2830
2935
|
return errors > 0 ? 1 : 0;
|
|
2831
2936
|
};
|
|
2832
2937
|
|
|
2833
|
-
// src/cli/commands/
|
|
2834
|
-
import { mkdirSync as
|
|
2835
|
-
import
|
|
2836
|
-
|
|
2837
|
-
// src/cli/commands/templates.ts
|
|
2838
|
-
var CONFIG_YAML = `# laracrew global preferences
|
|
2839
|
-
# theme: auto | dark | light | mono | high-contrast
|
|
2840
|
-
theme: auto
|
|
2841
|
-
|
|
2842
|
-
# Polling interval for queue depth and stream lag, in milliseconds.
|
|
2843
|
-
metricsIntervalMs: 2000
|
|
2844
|
-
|
|
2845
|
-
# The stack \`laracrew up\` boots when you don't name one.
|
|
2846
|
-
# defaultStack: dual
|
|
2847
|
-
`;
|
|
2848
|
-
var PROJECTS_YAML = `# Your projects, referenced by key from any stack. A project is just a root directory
|
|
2849
|
-
# plus the defaults its services inherit \u2014 cwd, .env and colour. Any language.
|
|
2850
|
-
# Paths may use forward slashes on Windows.
|
|
2851
|
-
#
|
|
2852
|
-
# Declaring projects here is optional: a service can carry its own \`cwd\` instead.
|
|
2853
|
-
projects: {}
|
|
2854
|
-
|
|
2855
|
-
# Example \u2014 delete the \`{}\` above and uncomment:
|
|
2856
|
-
#
|
|
2857
|
-
# projects:
|
|
2858
|
-
# api:
|
|
2859
|
-
# path: D:/work/api
|
|
2860
|
-
# php: php # only used by \`stop: { artisan: ... }\`; ignore it for non-PHP projects
|
|
2861
|
-
# envFile: .env # read for \${project.env:KEY} tokens, and by \`doctor\`
|
|
2862
|
-
# color: cyan
|
|
2863
|
-
# web:
|
|
2864
|
-
# path: D:/work/web
|
|
2865
|
-
# envFile: .env
|
|
2866
|
-
# color: magenta
|
|
2867
|
-
`;
|
|
2868
|
-
var EXAMPLE_STACK_YAML = `# A runnable demo stack \u2014 no Laravel project required.
|
|
2869
|
-
# Try: laracrew up example
|
|
2870
|
-
# Then: laracrew up example --only workers
|
|
2871
|
-
name: example
|
|
2872
|
-
description: Three fake services that prove laracrew works on this machine
|
|
2873
|
-
|
|
2874
|
-
# \`laracrew link example\` installs this as a global command you can run from anywhere.
|
|
2875
|
-
command: demo
|
|
2876
|
-
|
|
2877
|
-
defaults:
|
|
2878
|
-
restart: on-failure
|
|
2879
|
-
backoff: { initialMs: 500, maxMs: 5000, factor: 2, maxRestarts: 5 }
|
|
2880
|
-
stop: { graceMs: 5000 }
|
|
2881
|
-
|
|
2882
|
-
services:
|
|
2883
|
-
- name: demo:heartbeat
|
|
2884
|
-
cmd: ["node", "-e", "let n=0; setInterval(() => console.log('heartbeat ' + (++n)), 1000)"]
|
|
2885
|
-
groups: [workers]
|
|
2886
|
-
color: cyan
|
|
2887
|
-
|
|
2888
|
-
- name: demo:worker
|
|
2889
|
-
cmd: ["node", "-e", "let n=0; setInterval(() => console.log('processed job #' + (++n)), 1700)"]
|
|
2890
|
-
groups: [workers]
|
|
2891
|
-
needs: [demo:heartbeat]
|
|
2892
|
-
color: magenta
|
|
2893
|
-
|
|
2894
|
-
- name: demo:slow-starter
|
|
2895
|
-
cmd: ["node", "-e", "setTimeout(() => console.log('listening on 9'), 1200); setInterval(() => {}, 1000)"]
|
|
2896
|
-
groups: [http]
|
|
2897
|
-
ready: { logMatch: "listening on", timeoutMs: 10000 }
|
|
2898
|
-
color: green
|
|
2938
|
+
// src/cli/commands/draft.ts
|
|
2939
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync3, realpathSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2940
|
+
import path9 from "path";
|
|
2941
|
+
import { isMap, isSeq, parseDocument } from "yaml";
|
|
2899
2942
|
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2943
|
+
// src/cli/commands/link.ts
|
|
2944
|
+
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, unlinkSync, writeFileSync } from "fs";
|
|
2945
|
+
import path8 from "path";
|
|
2946
|
+
import { fileURLToPath } from "url";
|
|
2947
|
+
var IS_WINDOWS3 = process.platform === "win32";
|
|
2948
|
+
var MARKER = "laracrew-generated";
|
|
2949
|
+
var RESERVED = /* @__PURE__ */ new Set(["laracrew", "node", "npm", "npx", "php", "git", "cd", "ls", "rm", "sh", "bash"]);
|
|
2950
|
+
var VALID_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
2951
|
+
var entryScript = (argv = process.argv) => {
|
|
2952
|
+
const main = argv[1];
|
|
2953
|
+
if (!main) return fileURLToPath(import.meta.url);
|
|
2954
|
+
const resolved = path8.resolve(main);
|
|
2955
|
+
if (!resolved.endsWith(".ts")) return resolved;
|
|
2956
|
+
const dist = path8.join(path8.dirname(path8.dirname(resolved)), "dist", "index.js");
|
|
2957
|
+
if (existsSync5(dist)) return dist;
|
|
2958
|
+
throw new ConfigError("run `npm run build` first \u2014 a generated command has to point at dist/index.js");
|
|
2959
|
+
};
|
|
2960
|
+
var currentTarget = (stack) => ({
|
|
2961
|
+
node: process.execPath,
|
|
2962
|
+
entry: entryScript(),
|
|
2963
|
+
stack
|
|
2964
|
+
});
|
|
2965
|
+
var forward = (value) => value.replace(/\\/g, "/");
|
|
2966
|
+
var shShim = ({ node, entry, stack }) => `#!/bin/sh
|
|
2967
|
+
# ${MARKER}: boots the "${stack}" stack. Regenerate with \`laracrew link ${stack}\`.
|
|
2968
|
+
exec "${forward(node)}" "${forward(entry)}" up ${stack} "$@"
|
|
2903
2969
|
`;
|
|
2904
|
-
var
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
description: API + Portal with queues, streams and schedulers
|
|
2908
|
-
|
|
2909
|
-
# Run \`laracrew link dual\` and this whole set boots by typing \`dual\` from anywhere.
|
|
2910
|
-
command: dual
|
|
2911
|
-
|
|
2912
|
-
use: [api, portal]
|
|
2913
|
-
|
|
2914
|
-
defaults:
|
|
2915
|
-
restart: on-failure
|
|
2916
|
-
backoff: { initialMs: 1000, maxMs: 30000, factor: 2, maxRestarts: 10 }
|
|
2917
|
-
stop: { graceMs: 10000 }
|
|
2918
|
-
logs: { maxLines: 5000, toFile: false }
|
|
2919
|
-
|
|
2920
|
-
services:
|
|
2921
|
-
- name: redis
|
|
2922
|
-
external: true # laracrew checks it, never starts it
|
|
2923
|
-
ready: { tcp: "127.0.0.1:6379" }
|
|
2924
|
-
|
|
2925
|
-
- name: api:serve
|
|
2926
|
-
project: api
|
|
2927
|
-
cmd: php artisan serve --port=\${port:8000}
|
|
2928
|
-
groups: [http]
|
|
2929
|
-
needs: [redis]
|
|
2930
|
-
ready: { http: "http://127.0.0.1:8000/up", timeoutMs: 20000 }
|
|
2931
|
-
url: http://127.0.0.1:8000
|
|
2932
|
-
|
|
2933
|
-
- name: api:queue
|
|
2934
|
-
project: api
|
|
2935
|
-
cmd: php artisan queue:work redis --queue=high,default --tries=3 --timeout=90
|
|
2936
|
-
groups: [workers]
|
|
2937
|
-
needs: [redis]
|
|
2938
|
-
stop: { artisan: "queue:restart", graceMs: 15000 }
|
|
2939
|
-
watch:
|
|
2940
|
-
paths: [app, config, routes, database]
|
|
2941
|
-
strategy: artisan-queue-restart
|
|
2942
|
-
debounceMs: 800
|
|
2943
|
-
metrics:
|
|
2944
|
-
queues: [high, default]
|
|
2945
|
-
|
|
2946
|
-
- name: api:streams
|
|
2947
|
-
project: api
|
|
2948
|
-
cmd: php artisan streams:listen orders
|
|
2949
|
-
groups: [workers, streams]
|
|
2950
|
-
needs: [redis]
|
|
2951
|
-
metrics:
|
|
2952
|
-
streams:
|
|
2953
|
-
- { key: orders, group: api-consumers }
|
|
2954
|
-
|
|
2955
|
-
- name: api:schedule
|
|
2956
|
-
project: api
|
|
2957
|
-
cmd: php artisan schedule:work
|
|
2958
|
-
groups: [workers]
|
|
2959
|
-
needs: [redis]
|
|
2960
|
-
|
|
2961
|
-
- name: portal:serve
|
|
2962
|
-
project: portal
|
|
2963
|
-
cmd: php artisan serve --port=\${port:8001}
|
|
2964
|
-
groups: [http]
|
|
2965
|
-
needs: [redis, api:serve] # don't boot until the API answers
|
|
2966
|
-
ready: { http: "http://127.0.0.1:8001/up", timeoutMs: 20000 }
|
|
2967
|
-
url: http://127.0.0.1:8001
|
|
2968
|
-
|
|
2969
|
-
- name: portal:queue
|
|
2970
|
-
project: portal
|
|
2971
|
-
cmd: php artisan queue:work redis --queue=default --tries=3
|
|
2972
|
-
groups: [workers]
|
|
2973
|
-
needs: [redis, api:serve]
|
|
2974
|
-
stop: { artisan: "queue:restart" }
|
|
2975
|
-
watch: { paths: [app, config], strategy: artisan-queue-restart }
|
|
2976
|
-
metrics: { queues: [default] }
|
|
2977
|
-
|
|
2978
|
-
- name: portal:streams
|
|
2979
|
-
project: portal
|
|
2980
|
-
cmd: php artisan streams:listen inventory
|
|
2981
|
-
groups: [workers, streams]
|
|
2982
|
-
needs: [redis]
|
|
2983
|
-
metrics:
|
|
2984
|
-
streams:
|
|
2985
|
-
- { key: inventory, group: portal-consumers }
|
|
2986
|
-
|
|
2987
|
-
profiles:
|
|
2988
|
-
light:
|
|
2989
|
-
except: [assets]
|
|
2990
|
-
workers-only:
|
|
2991
|
-
only: [workers]
|
|
2970
|
+
var cmdShim = ({ node, entry, stack }) => `@ECHO OFF\r
|
|
2971
|
+
REM ${MARKER}: boots the "${stack}" stack. Regenerate with \`laracrew link ${stack}\`.\r
|
|
2972
|
+
"${node}" "${entry}" up ${stack} %*\r
|
|
2992
2973
|
`;
|
|
2993
|
-
var
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
steps:
|
|
2998
|
-
- { project: api, cmd: php artisan migrate:fresh --seed }
|
|
2999
|
-
- { project: portal, cmd: php artisan migrate:fresh --seed }
|
|
3000
|
-
- parallel:
|
|
3001
|
-
- { project: api, cmd: php artisan cache:clear }
|
|
3002
|
-
- { project: portal, cmd: php artisan cache:clear }
|
|
3003
|
-
`;
|
|
3004
|
-
var HOME_README = `# ~/.laracrew
|
|
3005
|
-
|
|
3006
|
-
Your laracrew configuration. Safe to keep in git \u2014 nothing here is secret unless you put
|
|
3007
|
-
secrets in it.
|
|
3008
|
-
|
|
3009
|
-
config.yaml global preferences
|
|
3010
|
-
projects.yaml your Laravel projects, referenced by key
|
|
3011
|
-
stacks/<name>/ one folder per stack; stack.yaml is the definition
|
|
3012
|
-
tasks/<name>.yaml one-shot sequences (laracrew run <name>)
|
|
3013
|
-
fragments/ shared YAML any stack can reuse
|
|
3014
|
-
logs/ per-run log files, when defaults.logs.toFile is on
|
|
3015
|
-
|
|
3016
|
-
Start here:
|
|
3017
|
-
|
|
3018
|
-
laracrew init --examples # add a demo stack and a two-project template
|
|
3019
|
-
laracrew ls # what is defined
|
|
3020
|
-
laracrew doctor <stack> # check a stack before booting it
|
|
3021
|
-
laracrew link <stack> # give a stack its own global command
|
|
3022
|
-
`;
|
|
3023
|
-
|
|
3024
|
-
// src/cli/commands/init.ts
|
|
3025
|
-
var runInit = (env = process.env, options = {}) => {
|
|
3026
|
-
const home2 = paths(env);
|
|
3027
|
-
const created = [];
|
|
3028
|
-
const skipped = [];
|
|
3029
|
-
for (const dir of [home2.root, home2.stacksDir, home2.tasksDir, home2.fragmentsDir, home2.logsDir, home2.runDir]) {
|
|
3030
|
-
mkdirSync2(dir, { recursive: true });
|
|
3031
|
-
}
|
|
3032
|
-
const files = [
|
|
3033
|
-
[home2.configFile, CONFIG_YAML],
|
|
3034
|
-
[home2.projectsFile, PROJECTS_YAML],
|
|
3035
|
-
[path8.join(home2.root, "README.md"), HOME_README]
|
|
3036
|
-
];
|
|
3037
|
-
if (options.examples) {
|
|
3038
|
-
mkdirSync2(path8.join(home2.stacksDir, "example"), { recursive: true });
|
|
3039
|
-
mkdirSync2(path8.join(home2.stacksDir, "dual"), { recursive: true });
|
|
3040
|
-
files.push(
|
|
3041
|
-
[path8.join(home2.stacksDir, "example", "stack.yaml"), EXAMPLE_STACK_YAML],
|
|
3042
|
-
[path8.join(home2.stacksDir, "dual", "stack.yaml"), REAL_STACK_TEMPLATE],
|
|
3043
|
-
[path8.join(home2.tasksDir, "reset.yaml"), RESET_TASK_YAML]
|
|
3044
|
-
);
|
|
3045
|
-
}
|
|
3046
|
-
for (const [file, contents] of files) {
|
|
3047
|
-
if (existsSync5(file)) {
|
|
3048
|
-
skipped.push(file);
|
|
3049
|
-
continue;
|
|
3050
|
-
}
|
|
3051
|
-
writeFileSync(file, contents, "utf8");
|
|
3052
|
-
created.push(file);
|
|
3053
|
-
}
|
|
3054
|
-
return { root: home2.root, created, skipped, examples: options.examples ?? false };
|
|
3055
|
-
};
|
|
3056
|
-
var initCommand = (options = {}, env = process.env) => {
|
|
3057
|
-
const result = runInit(env, options);
|
|
3058
|
-
console.log(`${paint("bold", "laracrew")} home: ${result.root}`);
|
|
3059
|
-
for (const file of result.created) console.log(` ${paint("green", "+")} ${path8.relative(result.root, file)}`);
|
|
3060
|
-
for (const file of result.skipped) {
|
|
3061
|
-
console.log(` ${paint("gray", "\xB7")} ${path8.relative(result.root, file)} ${paint("gray", "(kept)")}`);
|
|
3062
|
-
}
|
|
3063
|
-
if (result.examples) {
|
|
3064
|
-
console.log(`
|
|
3065
|
-
${paint("bold", "Next:")}
|
|
3066
|
-
${paint("cyan", "laracrew up example")} run the demo stack \u2014 proves it works, no Laravel needed
|
|
3067
|
-
${paint("cyan", "laracrew link example")} install it as a global command you can type from anywhere
|
|
3068
|
-
edit ${paint("gray", path8.join(result.root, "projects.yaml"))} with your project paths,
|
|
3069
|
-
then ${paint("cyan", "laracrew doctor dual")} and ${paint("cyan", "laracrew up dual")}
|
|
3070
|
-
`);
|
|
3071
|
-
return;
|
|
3072
|
-
}
|
|
3073
|
-
console.log(`
|
|
3074
|
-
${paint("bold", "Next:")}
|
|
3075
|
-
${paint("cyan", "laracrew init --examples")} add a runnable demo stack and a two-project template
|
|
3076
|
-
or write your own at ${paint("gray", path8.join(result.root, "stacks", "<name>", "stack.yaml"))}
|
|
3077
|
-
then ${paint("cyan", "laracrew ls")} and ${paint("cyan", "laracrew up <name>")}
|
|
3078
|
-
`);
|
|
3079
|
-
};
|
|
3080
|
-
|
|
3081
|
-
// src/cli/commands/link.ts
|
|
3082
|
-
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
3083
|
-
import path9 from "path";
|
|
3084
|
-
import { fileURLToPath } from "url";
|
|
3085
|
-
var IS_WINDOWS3 = process.platform === "win32";
|
|
3086
|
-
var MARKER = "laracrew-generated";
|
|
3087
|
-
var RESERVED = /* @__PURE__ */ new Set(["laracrew", "node", "npm", "npx", "php", "git", "cd", "ls", "rm", "sh", "bash"]);
|
|
3088
|
-
var VALID_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
3089
|
-
var entryScript = (argv = process.argv) => {
|
|
3090
|
-
const main = argv[1];
|
|
3091
|
-
if (!main) return fileURLToPath(import.meta.url);
|
|
3092
|
-
const resolved = path9.resolve(main);
|
|
3093
|
-
if (!resolved.endsWith(".ts")) return resolved;
|
|
3094
|
-
const dist = path9.join(path9.dirname(path9.dirname(resolved)), "dist", "index.js");
|
|
3095
|
-
if (existsSync6(dist)) return dist;
|
|
3096
|
-
throw new ConfigError("run `npm run build` first \u2014 a generated command has to point at dist/index.js");
|
|
3097
|
-
};
|
|
3098
|
-
var currentTarget = (stack) => ({
|
|
3099
|
-
node: process.execPath,
|
|
3100
|
-
entry: entryScript(),
|
|
3101
|
-
stack
|
|
3102
|
-
});
|
|
3103
|
-
var forward = (value) => value.replace(/\\/g, "/");
|
|
3104
|
-
var shShim = ({ node, entry, stack }) => `#!/bin/sh
|
|
3105
|
-
# ${MARKER}: boots the "${stack}" stack. Regenerate with \`laracrew link ${stack}\`.
|
|
3106
|
-
exec "${forward(node)}" "${forward(entry)}" up ${stack} "$@"
|
|
3107
|
-
`;
|
|
3108
|
-
var cmdShim = ({ node, entry, stack }) => `@ECHO OFF\r
|
|
3109
|
-
REM ${MARKER}: boots the "${stack}" stack. Regenerate with \`laracrew link ${stack}\`.\r
|
|
3110
|
-
"${node}" "${entry}" up ${stack} %*\r
|
|
3111
|
-
`;
|
|
3112
|
-
var ps1Shim = ({ node, entry, stack }) => `#!/usr/bin/env pwsh
|
|
3113
|
-
# ${MARKER}: boots the "${stack}" stack. Regenerate with \`laracrew link ${stack}\`.
|
|
3114
|
-
& "${node}" "${entry}" up ${stack} @args
|
|
3115
|
-
exit $LASTEXITCODE
|
|
2974
|
+
var ps1Shim = ({ node, entry, stack }) => `#!/usr/bin/env pwsh
|
|
2975
|
+
# ${MARKER}: boots the "${stack}" stack. Regenerate with \`laracrew link ${stack}\`.
|
|
2976
|
+
& "${node}" "${entry}" up ${stack} @args
|
|
2977
|
+
exit $LASTEXITCODE
|
|
3116
2978
|
`;
|
|
3117
2979
|
var shimFiles = (name, target) => {
|
|
3118
2980
|
const posix = { file: name, contents: shShim(target), exec: true };
|
|
@@ -3124,13 +2986,13 @@ var shimFiles = (name, target) => {
|
|
|
3124
2986
|
{ file: `${name}.ps1`, contents: ps1Shim(target), exec: true }
|
|
3125
2987
|
];
|
|
3126
2988
|
};
|
|
3127
|
-
var pathDirs = (env) => (env.PATH ?? env.Path ?? "").split(
|
|
2989
|
+
var pathDirs = (env) => (env.PATH ?? env.Path ?? "").split(path8.delimiter).filter(Boolean);
|
|
3128
2990
|
var samePath = (a, b) => IS_WINDOWS3 ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
3129
2991
|
var ownBinDir = (env) => {
|
|
3130
2992
|
for (const dir of pathDirs(env)) {
|
|
3131
2993
|
for (const candidate of IS_WINDOWS3 ? ["laracrew.cmd", "laracrew"] : ["laracrew"]) {
|
|
3132
2994
|
try {
|
|
3133
|
-
if (
|
|
2995
|
+
if (existsSync5(path8.join(dir, candidate))) return dir;
|
|
3134
2996
|
} catch {
|
|
3135
2997
|
}
|
|
3136
2998
|
}
|
|
@@ -3138,14 +3000,14 @@ var ownBinDir = (env) => {
|
|
|
3138
3000
|
return void 0;
|
|
3139
3001
|
};
|
|
3140
3002
|
var resolveBinDir = (env = process.env) => {
|
|
3141
|
-
if (env.LARACREW_BIN) return { dir:
|
|
3003
|
+
if (env.LARACREW_BIN) return { dir: path8.resolve(env.LARACREW_BIN), onPath: true };
|
|
3142
3004
|
const own = ownBinDir(env);
|
|
3143
3005
|
if (own) return { dir: own, onPath: true };
|
|
3144
|
-
const conventional = IS_WINDOWS3 ? env.APPDATA ?
|
|
3145
|
-
if (conventional &&
|
|
3006
|
+
const conventional = IS_WINDOWS3 ? env.APPDATA ? path8.join(env.APPDATA, "npm") : void 0 : path8.dirname(process.execPath);
|
|
3007
|
+
if (conventional && existsSync5(conventional) && pathDirs(env).some((dir) => samePath(dir, conventional))) {
|
|
3146
3008
|
return { dir: conventional, onPath: true };
|
|
3147
3009
|
}
|
|
3148
|
-
return { dir:
|
|
3010
|
+
return { dir: path8.join(paths(env).root, "bin"), onPath: false };
|
|
3149
3011
|
};
|
|
3150
3012
|
var isOurs = (file) => {
|
|
3151
3013
|
try {
|
|
@@ -3168,14 +3030,14 @@ var linkStack = (stackName, options = {}) => {
|
|
|
3168
3030
|
details: ["pick another with `--as <name>`, or pass --force if you really mean it"]
|
|
3169
3031
|
});
|
|
3170
3032
|
}
|
|
3171
|
-
const resolved = options.dir ? { dir:
|
|
3172
|
-
|
|
3033
|
+
const resolved = options.dir ? { dir: path8.resolve(options.dir), onPath: true } : resolveBinDir(env);
|
|
3034
|
+
mkdirSync2(resolved.dir, { recursive: true });
|
|
3173
3035
|
const target = currentTarget(stackName);
|
|
3174
3036
|
const planned = shimFiles(name, target);
|
|
3175
3037
|
let replaced = false;
|
|
3176
3038
|
for (const { file } of planned) {
|
|
3177
|
-
const full =
|
|
3178
|
-
if (!
|
|
3039
|
+
const full = path8.join(resolved.dir, file);
|
|
3040
|
+
if (!existsSync5(full)) continue;
|
|
3179
3041
|
if (isOurs(full)) {
|
|
3180
3042
|
replaced = true;
|
|
3181
3043
|
continue;
|
|
@@ -3189,8 +3051,8 @@ var linkStack = (stackName, options = {}) => {
|
|
|
3189
3051
|
}
|
|
3190
3052
|
const written = [];
|
|
3191
3053
|
for (const { file, contents, exec } of planned) {
|
|
3192
|
-
const full =
|
|
3193
|
-
|
|
3054
|
+
const full = path8.join(resolved.dir, file);
|
|
3055
|
+
writeFileSync(full, contents, "utf8");
|
|
3194
3056
|
if (exec && !IS_WINDOWS3) chmodSync(full, 493);
|
|
3195
3057
|
else if (exec) {
|
|
3196
3058
|
try {
|
|
@@ -3204,11 +3066,11 @@ var linkStack = (stackName, options = {}) => {
|
|
|
3204
3066
|
};
|
|
3205
3067
|
var unlinkCommandName = (name, options = {}) => {
|
|
3206
3068
|
const env = options.env ?? process.env;
|
|
3207
|
-
const dir = options.dir ?
|
|
3069
|
+
const dir = options.dir ? path8.resolve(options.dir) : resolveBinDir(env).dir;
|
|
3208
3070
|
const removed = [];
|
|
3209
3071
|
for (const candidate of [name, `${name}.cmd`, `${name}.ps1`]) {
|
|
3210
|
-
const full =
|
|
3211
|
-
if (!
|
|
3072
|
+
const full = path8.join(dir, candidate);
|
|
3073
|
+
if (!existsSync5(full)) continue;
|
|
3212
3074
|
if (!isOurs(full)) {
|
|
3213
3075
|
throw new ConfigError(`"${full}" was not created by laracrew \u2014 leaving it alone`);
|
|
3214
3076
|
}
|
|
@@ -3220,12 +3082,12 @@ var unlinkCommandName = (name, options = {}) => {
|
|
|
3220
3082
|
};
|
|
3221
3083
|
var listLinks = (options = {}) => {
|
|
3222
3084
|
const env = options.env ?? process.env;
|
|
3223
|
-
const dir = options.dir ?
|
|
3224
|
-
if (!
|
|
3085
|
+
const dir = options.dir ? path8.resolve(options.dir) : resolveBinDir(env).dir;
|
|
3086
|
+
if (!existsSync5(dir)) return [];
|
|
3225
3087
|
const found = [];
|
|
3226
3088
|
for (const entry of readdirSync2(dir).sort()) {
|
|
3227
3089
|
if (entry.endsWith(".cmd") || entry.endsWith(".ps1")) continue;
|
|
3228
|
-
const full =
|
|
3090
|
+
const full = path8.join(dir, entry);
|
|
3229
3091
|
let contents;
|
|
3230
3092
|
try {
|
|
3231
3093
|
contents = readFileSync3(full, "utf8");
|
|
@@ -3258,47 +3120,621 @@ var linkCommand = (stackName, options, env = process.env) => {
|
|
|
3258
3120
|
})
|
|
3259
3121
|
);
|
|
3260
3122
|
}
|
|
3123
|
+
const mark = marks(env);
|
|
3261
3124
|
if (results.length === 0) {
|
|
3262
|
-
console.log(
|
|
3125
|
+
console.log(
|
|
3126
|
+
`${mark.warn} ${color.yellow("nothing to link \u2014 add `command: <name>` to the stacks you want a command for")}`
|
|
3127
|
+
);
|
|
3263
3128
|
return 0;
|
|
3264
3129
|
}
|
|
3265
3130
|
for (const result of results) {
|
|
3266
3131
|
const verb = result.replaced ? "updated" : "created";
|
|
3267
3132
|
console.log(
|
|
3268
|
-
`${
|
|
3133
|
+
`${mark.ok} ${color.green(verb)} ${color.bold(result.name)} ${mark.arrow} ${color.gray(`laracrew up ${result.stack}`)}`
|
|
3269
3134
|
);
|
|
3270
3135
|
}
|
|
3271
3136
|
const first = results[0];
|
|
3272
|
-
console.log(
|
|
3273
|
-
in ${first.dir}`));
|
|
3137
|
+
console.log(`
|
|
3138
|
+
${color.gray(`in ${first.dir}`)}`);
|
|
3274
3139
|
if (!first.onPath) {
|
|
3275
3140
|
console.log(
|
|
3276
3141
|
`
|
|
3277
|
-
${
|
|
3278
|
-
` + (IS_WINDOWS3 ? ` ${
|
|
3142
|
+
${mark.warn} ${color.yellow("that directory is not on your PATH.")} Add it once:
|
|
3143
|
+
` + (IS_WINDOWS3 ? ` ${color.cyan(`setx PATH "%PATH%;${first.dir}"`)} ${color.gray("(then reopen the terminal)")}` : ` ${color.cyan(`echo 'export PATH="$PATH:${first.dir}"' >> ~/.bashrc`)}`)
|
|
3279
3144
|
);
|
|
3280
3145
|
} else {
|
|
3281
3146
|
console.log(`
|
|
3282
|
-
Run it from anywhere: ${
|
|
3283
|
-
console.log(
|
|
3147
|
+
Run it from anywhere: ${color.cyan.bold(first.name)}`);
|
|
3148
|
+
console.log(color.gray(`Flags pass straight through: ${first.name} --only workers`));
|
|
3284
3149
|
}
|
|
3285
3150
|
return 0;
|
|
3286
3151
|
};
|
|
3287
3152
|
var unlinkCommandAction = (name, env = process.env) => {
|
|
3288
3153
|
const removed = unlinkCommandName(name, { env });
|
|
3289
|
-
|
|
3154
|
+
const mark = marks(env);
|
|
3155
|
+
for (const file of removed) console.log(`${mark.ok} removed ${color.gray(file)}`);
|
|
3290
3156
|
return 0;
|
|
3291
3157
|
};
|
|
3292
3158
|
|
|
3293
|
-
// src/cli/commands/
|
|
3294
|
-
|
|
3159
|
+
// src/cli/commands/draft.ts
|
|
3160
|
+
var SUFFIX = ".laracrew.yaml";
|
|
3161
|
+
var DRAFT_FILE = /^(.+)\.laracrew\.ya?ml$/i;
|
|
3162
|
+
var NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
3163
|
+
var SERVICE_NAME = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/;
|
|
3164
|
+
var RUNNERS = /* @__PURE__ */ new Set(["node", "npm", "npx", "yarn", "pnpm", "bun", "deno", "python", "python3", "php", "run", "exec", "poetry"]);
|
|
3165
|
+
var forward2 = (value) => value.replace(/\\/g, "/");
|
|
3166
|
+
var canonical = (value) => {
|
|
3167
|
+
try {
|
|
3168
|
+
return realpathSync.native(value);
|
|
3169
|
+
} catch {
|
|
3170
|
+
return path9.resolve(value);
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
var samePath2 = (a, b) => {
|
|
3174
|
+
const [x, y] = [canonical(a), canonical(b)];
|
|
3175
|
+
return process.platform === "win32" ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
3176
|
+
};
|
|
3177
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3178
|
+
var draftFile = (name, cwd = process.cwd()) => path9.join(cwd, `${name}${SUFFIX}`);
|
|
3179
|
+
var listDrafts = (cwd = process.cwd()) => readdirSync3(cwd).map((entry) => ({ entry, match: DRAFT_FILE.exec(entry) })).filter((item) => item.match !== null).map(({ entry, match }) => ({ name: match[1], file: path9.join(cwd, entry) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
3180
|
+
var resolveDraft = (cwd = process.cwd(), name, pick = "--draft <name>") => {
|
|
3181
|
+
if (name) {
|
|
3182
|
+
const file = draftFile(name, cwd);
|
|
3183
|
+
if (!existsSync6(file)) {
|
|
3184
|
+
throw new ConfigError(`no draft named "${name}" in ${cwd}`, { details: ["start one with `laracrew draft <name>`"] });
|
|
3185
|
+
}
|
|
3186
|
+
return { name, file };
|
|
3187
|
+
}
|
|
3188
|
+
const drafts = listDrafts(cwd);
|
|
3189
|
+
if (drafts.length === 1) return drafts[0];
|
|
3190
|
+
if (drafts.length === 0) {
|
|
3191
|
+
throw new ConfigError(`no draft in ${cwd}`, { details: ["start one with `laracrew draft <name>`"] });
|
|
3192
|
+
}
|
|
3193
|
+
throw new ConfigError(`several drafts here: ${drafts.map((draft) => draft.name).join(", ")} \u2014 pick one with ${pick}`);
|
|
3194
|
+
};
|
|
3195
|
+
var template = (name) => `# Built with \`laracrew draft\`. Add services with \`laracrew draft add\`, install the stack with
|
|
3196
|
+
# \`laracrew draft publish\`, or edit by hand \u2014 this is an ordinary stack.yaml.
|
|
3197
|
+
#
|
|
3198
|
+
# laracrew draft add --project api --path C:/work/api --command "php artisan queue:work"
|
|
3199
|
+
# laracrew draft add --project api --command "php artisan schedule:work"
|
|
3200
|
+
# laracrew draft add --command "npm run dev" --default-state stopped
|
|
3201
|
+
name: ${name}
|
|
3202
|
+
command: ${name} # \`laracrew link ${name}\` installs this as a global command
|
|
3203
|
+
projects: {}
|
|
3204
|
+
services: []
|
|
3205
|
+
`;
|
|
3206
|
+
var createDraft = (name, options = {}) => {
|
|
3207
|
+
const cwd = options.cwd ?? process.cwd();
|
|
3208
|
+
if (!NAME.test(name)) {
|
|
3209
|
+
throw new ConfigError(`"${name}" is not a usable stack name \u2014 letters, digits, dot, dash and underscore only`);
|
|
3210
|
+
}
|
|
3211
|
+
if (name === "add" || name === "publish") {
|
|
3212
|
+
throw new ConfigError(`"${name}" would clash with \`laracrew draft ${name}\` \u2014 pick another name`);
|
|
3213
|
+
}
|
|
3214
|
+
const file = draftFile(name, cwd);
|
|
3215
|
+
if (existsSync6(file) && !options.force) {
|
|
3216
|
+
throw new ConfigError(`${path9.basename(file)} already exists here`, {
|
|
3217
|
+
details: ["add to it with `laracrew draft add`, or pass --force to start over"]
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
writeFileSync2(file, template(name), "utf8");
|
|
3221
|
+
return file;
|
|
3222
|
+
};
|
|
3223
|
+
var readDraft = (file) => {
|
|
3224
|
+
const doc = parseDocument(readFileSync4(file, "utf8"), { prettyErrors: true });
|
|
3225
|
+
const [problem] = doc.errors;
|
|
3226
|
+
if (problem) throw new ConfigError(`invalid YAML: ${problem.message}`, { file });
|
|
3227
|
+
if (!isMap(doc.contents)) throw new ConfigError("the draft should be a YAML mapping (name:, services: \u2026)", { file });
|
|
3228
|
+
return doc;
|
|
3229
|
+
};
|
|
3230
|
+
var mapAt = (doc, key, file) => {
|
|
3231
|
+
let node = doc.get(key);
|
|
3232
|
+
if (node === void 0 || node === null) {
|
|
3233
|
+
node = doc.createNode({});
|
|
3234
|
+
doc.set(key, node);
|
|
3235
|
+
}
|
|
3236
|
+
if (!isMap(node)) throw new ConfigError(`\`${key}:\` should be a map`, { file });
|
|
3237
|
+
return node;
|
|
3238
|
+
};
|
|
3239
|
+
var seqAt = (doc, key, file) => {
|
|
3240
|
+
let node = doc.get(key);
|
|
3241
|
+
if (node === void 0 || node === null) {
|
|
3242
|
+
node = doc.createNode([]);
|
|
3243
|
+
doc.set(key, node);
|
|
3244
|
+
}
|
|
3245
|
+
if (!isSeq(node)) throw new ConfigError(`\`${key}:\` should be a list`, { file });
|
|
3246
|
+
return node;
|
|
3247
|
+
};
|
|
3248
|
+
var deriveServiceName = (command, project) => {
|
|
3249
|
+
const artisan = /\bartisan\s+([A-Za-z0-9:_.-]+)/.exec(command);
|
|
3250
|
+
let verb = artisan?.[1];
|
|
3251
|
+
if (!verb) {
|
|
3252
|
+
const words = command.trim().split(/\s+/).filter((word2) => word2 && !word2.startsWith("-")).map((word2) => word2.split(/[\\/]/).pop() ?? word2);
|
|
3253
|
+
const word = words.find((candidate) => !RUNNERS.has(candidate.toLowerCase())) ?? words[0] ?? "service";
|
|
3254
|
+
verb = word.replace(/\.[A-Za-z0-9]+$/, "");
|
|
3255
|
+
}
|
|
3256
|
+
const clean = verb.toLowerCase().replace(/[^a-z0-9:_.-]+/g, "-").replace(/^[-.:]+|[-.:]+$/g, "") || "service";
|
|
3257
|
+
return project ? `${project}:${clean}` : clean;
|
|
3258
|
+
};
|
|
3259
|
+
var uniqueName = (base, taken) => {
|
|
3260
|
+
if (!taken.has(base)) return base;
|
|
3261
|
+
for (let n = 2; ; n += 1) {
|
|
3262
|
+
const candidate = `${base}-${n}`;
|
|
3263
|
+
if (!taken.has(candidate)) return candidate;
|
|
3264
|
+
}
|
|
3265
|
+
};
|
|
3266
|
+
var parseState = (value) => {
|
|
3267
|
+
if (value === void 0 || value === "running") return true;
|
|
3268
|
+
if (value === "stopped") return false;
|
|
3269
|
+
throw new ConfigError(`--default-state is "running" or "stopped", not "${value}"`);
|
|
3270
|
+
};
|
|
3271
|
+
var addToDraft = (options) => {
|
|
3272
|
+
const cwd = options.cwd ?? process.cwd();
|
|
3273
|
+
const command = options.command.trim();
|
|
3274
|
+
if (!command) throw new ConfigError('--command needs something to run, e.g. "php artisan queue:work"');
|
|
3275
|
+
const autostart = parseState(options.state);
|
|
3276
|
+
if (options.project !== void 0 && !NAME.test(options.project)) {
|
|
3277
|
+
throw new ConfigError(`"${options.project}" is not a usable project key \u2014 letters, digits, dot, dash and underscore only`);
|
|
3278
|
+
}
|
|
3279
|
+
const { name: draftName, file } = resolveDraft(cwd, options.draft);
|
|
3280
|
+
const doc = readDraft(file);
|
|
3281
|
+
let project;
|
|
3282
|
+
if (options.project) {
|
|
3283
|
+
const projects = mapAt(doc, "projects", file);
|
|
3284
|
+
const wanted = options.path ? forward2(path9.resolve(cwd, options.path)) : void 0;
|
|
3285
|
+
const existing = projects.get(options.project);
|
|
3286
|
+
if (existing !== void 0 && existing !== null) {
|
|
3287
|
+
const current = isMap(existing) ? String(existing.get("path") ?? "") : "";
|
|
3288
|
+
if (wanted && !samePath2(current, wanted)) {
|
|
3289
|
+
throw new ConfigError(`project "${options.project}" already points at ${current}`, {
|
|
3290
|
+
file,
|
|
3291
|
+
details: ["a draft keeps one path per project \u2014 leave --path off to reuse it, or pick another key"]
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
project = { key: options.project, path: current, created: false };
|
|
3295
|
+
} else {
|
|
3296
|
+
const dir = wanted ?? forward2(cwd);
|
|
3297
|
+
projects.set(options.project, doc.createNode({ path: dir }));
|
|
3298
|
+
projects.flow = false;
|
|
3299
|
+
project = { key: options.project, path: dir, created: true };
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
const services = seqAt(doc, "services", file);
|
|
3303
|
+
const taken = new Set(services.items.map((item) => isMap(item) ? String(item.get("name") ?? "") : ""));
|
|
3304
|
+
let name;
|
|
3305
|
+
if (options.name) {
|
|
3306
|
+
if (!SERVICE_NAME.test(options.name)) {
|
|
3307
|
+
throw new ConfigError(`"${options.name}" is not a usable service name \u2014 letters, digits, colon, dot, dash and underscore only`);
|
|
3308
|
+
}
|
|
3309
|
+
if (taken.has(options.name)) throw new ConfigError(`a service named "${options.name}" is already in the draft`, { file });
|
|
3310
|
+
name = options.name;
|
|
3311
|
+
} else {
|
|
3312
|
+
name = uniqueName(deriveServiceName(command, options.project), taken);
|
|
3313
|
+
}
|
|
3314
|
+
const service = { name };
|
|
3315
|
+
if (options.project) service.project = options.project;
|
|
3316
|
+
service.cmd = command;
|
|
3317
|
+
const serviceCwd = options.project ? void 0 : forward2(path9.resolve(cwd, options.path ?? "."));
|
|
3318
|
+
if (serviceCwd) service.cwd = serviceCwd;
|
|
3319
|
+
if (!autostart) service.autostart = false;
|
|
3320
|
+
services.add(doc.createNode(service));
|
|
3321
|
+
services.flow = false;
|
|
3322
|
+
writeFileSync2(file, doc.toString(), "utf8");
|
|
3323
|
+
return {
|
|
3324
|
+
draft: draftName,
|
|
3325
|
+
file,
|
|
3326
|
+
service: { name, cmd: command, autostart, ...serviceCwd ? { cwd: serviceCwd } : {} },
|
|
3327
|
+
...project ? { project } : {},
|
|
3328
|
+
services: services.items.length
|
|
3329
|
+
};
|
|
3330
|
+
};
|
|
3331
|
+
var describeDraft = (cwd = process.cwd(), name) => {
|
|
3332
|
+
const ref = resolveDraft(cwd, name, "laracrew draft <name>");
|
|
3333
|
+
const raw = parseYaml(readFileSync4(ref.file, "utf8"), ref.file);
|
|
3334
|
+
const record = isRecord2(raw) ? raw : {};
|
|
3335
|
+
const stackName = typeof record.name === "string" ? record.name : ref.name;
|
|
3336
|
+
const projects = isRecord2(record.projects) ? Object.entries(record.projects).map(([key, value]) => ({
|
|
3337
|
+
key,
|
|
3338
|
+
path: isRecord2(value) && typeof value.path === "string" ? value.path : ""
|
|
3339
|
+
})) : [];
|
|
3340
|
+
const services = Array.isArray(record.services) ? record.services.filter(isRecord2).map((entry) => ({
|
|
3341
|
+
name: typeof entry.name === "string" ? entry.name : "?",
|
|
3342
|
+
cmd: Array.isArray(entry.cmd) ? entry.cmd.map(String).join(" ") : typeof entry.cmd === "string" ? entry.cmd : "",
|
|
3343
|
+
...typeof entry.project === "string" ? { project: entry.project } : {},
|
|
3344
|
+
autostart: entry.autostart !== false
|
|
3345
|
+
})) : [];
|
|
3346
|
+
return {
|
|
3347
|
+
name: ref.name,
|
|
3348
|
+
file: ref.file,
|
|
3349
|
+
stackName,
|
|
3350
|
+
command: typeof record.command === "string" ? record.command : stackName,
|
|
3351
|
+
projects,
|
|
3352
|
+
services
|
|
3353
|
+
};
|
|
3354
|
+
};
|
|
3355
|
+
var publishDraft = (options = {}) => {
|
|
3356
|
+
const cwd = options.cwd ?? process.cwd();
|
|
3357
|
+
const env = options.env ?? process.env;
|
|
3358
|
+
const { name: draftName, file } = resolveDraft(cwd, options.draft);
|
|
3359
|
+
const source = readFileSync4(file, "utf8");
|
|
3360
|
+
const raw = parseYaml(source, file);
|
|
3361
|
+
if (isRecord2(raw) && Array.isArray(raw.services) && raw.services.length === 0) {
|
|
3362
|
+
throw new ConfigError("the draft has no services yet", {
|
|
3363
|
+
file,
|
|
3364
|
+
details: ['add one with `laracrew draft add --command "\u2026"`']
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
const config = validateStack(raw, file);
|
|
3368
|
+
const target = stackFile(config.name, env);
|
|
3369
|
+
const replaced = existsSync6(target);
|
|
3370
|
+
if (replaced && !options.force) {
|
|
3371
|
+
throw new ConfigError(`a stack named "${config.name}" already exists`, {
|
|
3372
|
+
file: target,
|
|
3373
|
+
details: ["pass --force to replace it, or change `name:` in the draft"]
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
mkdirSync3(stackDir(config.name, env), { recursive: true });
|
|
3377
|
+
writeFileSync2(target, source, "utf8");
|
|
3378
|
+
return { name: config.name, draft: draftName, file, target, replaced, config };
|
|
3379
|
+
};
|
|
3380
|
+
var plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
3381
|
+
var draftCommand = (name, options = {}, cwd = process.cwd(), env = process.env) => {
|
|
3382
|
+
const mark = marks(env);
|
|
3383
|
+
if (name && (options.force || !existsSync6(draftFile(name, cwd)))) {
|
|
3384
|
+
const file = createDraft(name, { cwd, ...options.force ? { force: true } : {} });
|
|
3385
|
+
console.log(`${mark.ok} started draft ${color.bold(name)} ${mark.arrow} ${color.gray(path9.basename(file))}`);
|
|
3386
|
+
console.log(
|
|
3387
|
+
`
|
|
3388
|
+
${nextSteps(
|
|
3389
|
+
[
|
|
3390
|
+
["laracrew draft add", `--project api --path C:/work/api --command ${color.green('"php artisan queue:work"')}`],
|
|
3391
|
+
["", `--project api --command ${color.green('"php artisan schedule:work"')} ${color.gray("the path is remembered")}`],
|
|
3392
|
+
["", `--command ${color.green('"npm run dev"')} --default-state stopped ${color.gray("idle until you press s")}`],
|
|
3393
|
+
["laracrew draft", "show what the draft holds so far"],
|
|
3394
|
+
["laracrew draft publish", `install it as the ${color.cyan(name)} stack in ~/.laracrew`]
|
|
3395
|
+
],
|
|
3396
|
+
env
|
|
3397
|
+
)}
|
|
3398
|
+
`
|
|
3399
|
+
);
|
|
3400
|
+
return 0;
|
|
3401
|
+
}
|
|
3402
|
+
const draft = describeDraft(cwd, name);
|
|
3403
|
+
console.log(`${color.bold("draft")} ${color.gray("\xB7")} ${color.cyan.bold(draft.stackName)} ${color.gray(path9.basename(draft.file))}`);
|
|
3404
|
+
if (draft.command !== draft.stackName) console.log(` ${color.gray(`command: ${draft.command}`)}`);
|
|
3405
|
+
console.log(`
|
|
3406
|
+
${color.bold("PROJECTS")} ${color.gray(`(${draft.projects.length})`)}`);
|
|
3407
|
+
if (draft.projects.length === 0) console.log(color.gray(" none \u2014 `--project <key> --path <dir>` on the next add"));
|
|
3408
|
+
const keyWidth = Math.max(4, ...draft.projects.map((project) => project.key.length));
|
|
3409
|
+
for (const project of draft.projects) {
|
|
3410
|
+
console.log(` ${color.magenta.bold(padEnd(project.key, keyWidth))} ${project.path}`);
|
|
3411
|
+
}
|
|
3412
|
+
console.log(`
|
|
3413
|
+
${color.bold("SERVICES")} ${color.gray(`(${draft.services.length})`)}`);
|
|
3414
|
+
if (draft.services.length === 0) console.log(color.gray(' none yet \u2014 `laracrew draft add --command "\u2026"`'));
|
|
3415
|
+
const nameWidth = Math.max(4, ...draft.services.map((service) => service.name.length));
|
|
3416
|
+
for (const service of draft.services) {
|
|
3417
|
+
const idle = service.autostart ? "" : ` ${color.yellow("idle until started")}`;
|
|
3418
|
+
console.log(` ${color.cyan.bold(padEnd(service.name, nameWidth))} ${service.cmd}${idle}`);
|
|
3419
|
+
}
|
|
3420
|
+
console.log(
|
|
3421
|
+
`
|
|
3422
|
+
${nextSteps(
|
|
3423
|
+
[
|
|
3424
|
+
["laracrew draft add", `--command ${color.green('"\u2026"')} to add another`],
|
|
3425
|
+
["laracrew draft publish", `install it as the ${color.cyan(draft.stackName)} stack`]
|
|
3426
|
+
],
|
|
3427
|
+
env
|
|
3428
|
+
)}
|
|
3429
|
+
`
|
|
3430
|
+
);
|
|
3431
|
+
return 0;
|
|
3432
|
+
};
|
|
3433
|
+
var draftAddCommand = (options, cwd = process.cwd(), env = process.env) => {
|
|
3434
|
+
const result = addToDraft({ ...options, cwd });
|
|
3435
|
+
const mark = marks(env);
|
|
3436
|
+
const idle = result.service.autostart ? "" : ` ${color.yellow("idle until started")}`;
|
|
3437
|
+
console.log(`${mark.ok} added ${color.bold(result.service.name)} ${mark.arrow} ${color.gray(result.service.cmd)}${idle}`);
|
|
3438
|
+
if (result.project?.created) {
|
|
3439
|
+
const guessed = options.path ? "" : ` ${color.yellow("this folder \u2014 pass --path if that is wrong")}`;
|
|
3440
|
+
console.log(` ${mark.ok} project ${color.magenta.bold(result.project.key)} ${mark.arrow} ${color.gray(result.project.path)}${guessed}`);
|
|
3441
|
+
}
|
|
3442
|
+
if (result.service.cwd) console.log(` ${color.gray(`runs in ${result.service.cwd}`)}`);
|
|
3443
|
+
console.log(
|
|
3444
|
+
` ${color.gray(`${plural(result.services, "service")} in ${path9.basename(result.file)} \u2014 \`laracrew draft publish\` when done`)}`
|
|
3445
|
+
);
|
|
3446
|
+
return 0;
|
|
3447
|
+
};
|
|
3448
|
+
var draftPublishCommand = (options = {}, cwd = process.cwd(), env = process.env) => {
|
|
3449
|
+
const result = publishDraft({
|
|
3450
|
+
cwd,
|
|
3451
|
+
env,
|
|
3452
|
+
...options.draft ? { draft: options.draft } : {},
|
|
3453
|
+
...options.force ? { force: true } : {}
|
|
3454
|
+
});
|
|
3455
|
+
const mark = marks(env);
|
|
3456
|
+
const command = result.config.command ?? result.name;
|
|
3457
|
+
console.log(
|
|
3458
|
+
`${mark.ok} ${color.green(result.replaced ? "replaced" : "published")} ${color.bold(result.name)} ${mark.arrow} ${color.gray(result.target)}`
|
|
3459
|
+
);
|
|
3460
|
+
console.log(
|
|
3461
|
+
` ${color.gray(
|
|
3462
|
+
`${plural(Object.keys(result.config.projects).length, "project")}, ${plural(result.config.services.length, "service")}; the draft stays at ${path9.basename(result.file)}`
|
|
3463
|
+
)}`
|
|
3464
|
+
);
|
|
3465
|
+
if (options.link) {
|
|
3466
|
+
console.log("");
|
|
3467
|
+
const code = linkCommand(result.name, {}, env);
|
|
3468
|
+
if (code !== 0) return code;
|
|
3469
|
+
}
|
|
3470
|
+
const steps = [
|
|
3471
|
+
[`laracrew doctor ${result.name}`, "check paths, ports and PHP before the first boot"],
|
|
3472
|
+
[`laracrew up ${result.name}`, "boot it"]
|
|
3473
|
+
];
|
|
3474
|
+
if (!options.link) steps.push([`laracrew link ${result.name}`, `type ${color.cyan(command)} from anywhere to boot it`]);
|
|
3475
|
+
console.log(`
|
|
3476
|
+
${nextSteps(steps, env)}
|
|
3477
|
+
`);
|
|
3478
|
+
return 0;
|
|
3479
|
+
};
|
|
3480
|
+
|
|
3481
|
+
// src/cli/commands/init.ts
|
|
3482
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, existsSync as existsSync7 } from "fs";
|
|
3295
3483
|
import path10 from "path";
|
|
3296
3484
|
|
|
3485
|
+
// src/cli/commands/templates.ts
|
|
3486
|
+
var CONFIG_YAML = `# laracrew global preferences
|
|
3487
|
+
# theme: auto | dark | light | mono | high-contrast
|
|
3488
|
+
theme: auto
|
|
3489
|
+
|
|
3490
|
+
# Polling interval for queue depth and stream lag, in milliseconds.
|
|
3491
|
+
metricsIntervalMs: 2000
|
|
3492
|
+
|
|
3493
|
+
# The stack \`laracrew up\` boots when you don't name one.
|
|
3494
|
+
# defaultStack: dual
|
|
3495
|
+
`;
|
|
3496
|
+
var PROJECTS_YAML = `# Your projects, referenced by key from any stack. A project is just a root directory
|
|
3497
|
+
# plus the defaults its services inherit \u2014 cwd, .env and colour. Any language.
|
|
3498
|
+
# Paths may use forward slashes on Windows.
|
|
3499
|
+
#
|
|
3500
|
+
# Declaring projects here is optional: a service can carry its own \`cwd\` instead.
|
|
3501
|
+
projects: {}
|
|
3502
|
+
|
|
3503
|
+
# Example \u2014 delete the \`{}\` above and uncomment:
|
|
3504
|
+
#
|
|
3505
|
+
# projects:
|
|
3506
|
+
# api:
|
|
3507
|
+
# path: D:/work/api
|
|
3508
|
+
# php: php # only used by \`stop: { artisan: ... }\`; ignore it for non-PHP projects
|
|
3509
|
+
# envFile: .env # read for \${project.env:KEY} tokens, and by \`doctor\`
|
|
3510
|
+
# color: cyan
|
|
3511
|
+
# web:
|
|
3512
|
+
# path: D:/work/web
|
|
3513
|
+
# envFile: .env
|
|
3514
|
+
# color: magenta
|
|
3515
|
+
`;
|
|
3516
|
+
var EXAMPLE_STACK_YAML = `# A runnable demo stack \u2014 no Laravel project required.
|
|
3517
|
+
# Try: laracrew up example
|
|
3518
|
+
# Then: laracrew up example --only workers
|
|
3519
|
+
name: example
|
|
3520
|
+
description: Three fake services that prove laracrew works on this machine
|
|
3521
|
+
|
|
3522
|
+
# \`laracrew link example\` installs this as a global command you can run from anywhere.
|
|
3523
|
+
command: demo
|
|
3524
|
+
|
|
3525
|
+
defaults:
|
|
3526
|
+
restart: on-failure
|
|
3527
|
+
backoff: { initialMs: 500, maxMs: 5000, factor: 2, maxRestarts: 5 }
|
|
3528
|
+
stop: { graceMs: 5000 }
|
|
3529
|
+
|
|
3530
|
+
services:
|
|
3531
|
+
- name: demo:heartbeat
|
|
3532
|
+
cmd: ["node", "-e", "let n=0; setInterval(() => console.log('heartbeat ' + (++n)), 1000)"]
|
|
3533
|
+
groups: [workers]
|
|
3534
|
+
color: cyan
|
|
3535
|
+
|
|
3536
|
+
- name: demo:worker
|
|
3537
|
+
cmd: ["node", "-e", "let n=0; setInterval(() => console.log('processed job #' + (++n)), 1700)"]
|
|
3538
|
+
groups: [workers]
|
|
3539
|
+
needs: [demo:heartbeat]
|
|
3540
|
+
color: magenta
|
|
3541
|
+
|
|
3542
|
+
- name: demo:slow-starter
|
|
3543
|
+
cmd: ["node", "-e", "setTimeout(() => console.log('listening on 9'), 1200); setInterval(() => {}, 1000)"]
|
|
3544
|
+
groups: [http]
|
|
3545
|
+
ready: { logMatch: "listening on", timeoutMs: 10000 }
|
|
3546
|
+
color: green
|
|
3547
|
+
|
|
3548
|
+
profiles:
|
|
3549
|
+
quiet:
|
|
3550
|
+
except: [http]
|
|
3551
|
+
`;
|
|
3552
|
+
var REAL_STACK_TEMPLATE = `# Two interconnected Laravel projects: queues, Redis stream listeners, schedulers.
|
|
3553
|
+
# Fill in the project paths in ~/.laracrew/projects.yaml first, then: laracrew up dual
|
|
3554
|
+
name: dual
|
|
3555
|
+
description: API + Portal with queues, streams and schedulers
|
|
3556
|
+
|
|
3557
|
+
# Run \`laracrew link dual\` and this whole set boots by typing \`dual\` from anywhere.
|
|
3558
|
+
command: dual
|
|
3559
|
+
|
|
3560
|
+
use: [api, portal]
|
|
3561
|
+
|
|
3562
|
+
defaults:
|
|
3563
|
+
restart: on-failure
|
|
3564
|
+
backoff: { initialMs: 1000, maxMs: 30000, factor: 2, maxRestarts: 10 }
|
|
3565
|
+
stop: { graceMs: 10000 }
|
|
3566
|
+
logs: { maxLines: 5000, toFile: false }
|
|
3567
|
+
|
|
3568
|
+
services:
|
|
3569
|
+
- name: redis
|
|
3570
|
+
external: true # laracrew checks it, never starts it
|
|
3571
|
+
ready: { tcp: "127.0.0.1:6379" }
|
|
3572
|
+
|
|
3573
|
+
- name: api:serve
|
|
3574
|
+
project: api
|
|
3575
|
+
cmd: php artisan serve --port=\${port:8000}
|
|
3576
|
+
groups: [http]
|
|
3577
|
+
needs: [redis]
|
|
3578
|
+
ready: { http: "http://127.0.0.1:8000/up", timeoutMs: 20000 }
|
|
3579
|
+
url: http://127.0.0.1:8000
|
|
3580
|
+
|
|
3581
|
+
- name: api:queue
|
|
3582
|
+
project: api
|
|
3583
|
+
cmd: php artisan queue:work redis --queue=high,default --tries=3 --timeout=90
|
|
3584
|
+
groups: [workers]
|
|
3585
|
+
needs: [redis]
|
|
3586
|
+
stop: { artisan: "queue:restart", graceMs: 15000 }
|
|
3587
|
+
watch:
|
|
3588
|
+
paths: [app, config, routes, database]
|
|
3589
|
+
strategy: artisan-queue-restart
|
|
3590
|
+
debounceMs: 800
|
|
3591
|
+
metrics:
|
|
3592
|
+
queues: [high, default]
|
|
3593
|
+
|
|
3594
|
+
- name: api:streams
|
|
3595
|
+
project: api
|
|
3596
|
+
cmd: php artisan streams:listen orders
|
|
3597
|
+
groups: [workers, streams]
|
|
3598
|
+
needs: [redis]
|
|
3599
|
+
metrics:
|
|
3600
|
+
streams:
|
|
3601
|
+
- { key: orders, group: api-consumers }
|
|
3602
|
+
|
|
3603
|
+
- name: api:schedule
|
|
3604
|
+
project: api
|
|
3605
|
+
cmd: php artisan schedule:work
|
|
3606
|
+
groups: [workers]
|
|
3607
|
+
needs: [redis]
|
|
3608
|
+
|
|
3609
|
+
- name: portal:serve
|
|
3610
|
+
project: portal
|
|
3611
|
+
cmd: php artisan serve --port=\${port:8001}
|
|
3612
|
+
groups: [http]
|
|
3613
|
+
needs: [redis, api:serve] # don't boot until the API answers
|
|
3614
|
+
ready: { http: "http://127.0.0.1:8001/up", timeoutMs: 20000 }
|
|
3615
|
+
url: http://127.0.0.1:8001
|
|
3616
|
+
|
|
3617
|
+
- name: portal:queue
|
|
3618
|
+
project: portal
|
|
3619
|
+
cmd: php artisan queue:work redis --queue=default --tries=3
|
|
3620
|
+
groups: [workers]
|
|
3621
|
+
needs: [redis, api:serve]
|
|
3622
|
+
stop: { artisan: "queue:restart" }
|
|
3623
|
+
watch: { paths: [app, config], strategy: artisan-queue-restart }
|
|
3624
|
+
metrics: { queues: [default] }
|
|
3625
|
+
|
|
3626
|
+
- name: portal:streams
|
|
3627
|
+
project: portal
|
|
3628
|
+
cmd: php artisan streams:listen inventory
|
|
3629
|
+
groups: [workers, streams]
|
|
3630
|
+
needs: [redis]
|
|
3631
|
+
metrics:
|
|
3632
|
+
streams:
|
|
3633
|
+
- { key: inventory, group: portal-consumers }
|
|
3634
|
+
|
|
3635
|
+
profiles:
|
|
3636
|
+
light:
|
|
3637
|
+
except: [assets]
|
|
3638
|
+
workers-only:
|
|
3639
|
+
only: [workers]
|
|
3640
|
+
`;
|
|
3641
|
+
var RESET_TASK_YAML = `# laracrew run reset
|
|
3642
|
+
name: reset
|
|
3643
|
+
description: Wipe and reseed both databases in the right order
|
|
3644
|
+
pauseServices: [workers] # stopped before the steps, restarted after
|
|
3645
|
+
steps:
|
|
3646
|
+
- { project: api, cmd: php artisan migrate:fresh --seed }
|
|
3647
|
+
- { project: portal, cmd: php artisan migrate:fresh --seed }
|
|
3648
|
+
- parallel:
|
|
3649
|
+
- { project: api, cmd: php artisan cache:clear }
|
|
3650
|
+
- { project: portal, cmd: php artisan cache:clear }
|
|
3651
|
+
`;
|
|
3652
|
+
var HOME_README = `# ~/.laracrew
|
|
3653
|
+
|
|
3654
|
+
Your laracrew configuration. Safe to keep in git \u2014 nothing here is secret unless you put
|
|
3655
|
+
secrets in it.
|
|
3656
|
+
|
|
3657
|
+
config.yaml global preferences
|
|
3658
|
+
projects.yaml your Laravel projects, referenced by key
|
|
3659
|
+
stacks/<name>/ one folder per stack; stack.yaml is the definition
|
|
3660
|
+
tasks/<name>.yaml one-shot sequences (laracrew run <name>)
|
|
3661
|
+
fragments/ shared YAML any stack can reuse
|
|
3662
|
+
logs/ per-run log files, when defaults.logs.toFile is on
|
|
3663
|
+
|
|
3664
|
+
Start here:
|
|
3665
|
+
|
|
3666
|
+
laracrew init --examples # add a demo stack and a two-project template
|
|
3667
|
+
laracrew draft <name> # build a stack in a project folder, one command at a time
|
|
3668
|
+
laracrew ls # what is defined
|
|
3669
|
+
laracrew doctor <stack> # check a stack before booting it
|
|
3670
|
+
laracrew link <stack> # give a stack its own global command
|
|
3671
|
+
`;
|
|
3672
|
+
|
|
3673
|
+
// src/cli/commands/init.ts
|
|
3674
|
+
var runInit = (env = process.env, options = {}) => {
|
|
3675
|
+
const home2 = paths(env);
|
|
3676
|
+
const created = [];
|
|
3677
|
+
const skipped = [];
|
|
3678
|
+
for (const dir of [home2.root, home2.stacksDir, home2.tasksDir, home2.fragmentsDir, home2.logsDir, home2.runDir]) {
|
|
3679
|
+
mkdirSync4(dir, { recursive: true });
|
|
3680
|
+
}
|
|
3681
|
+
const files = [
|
|
3682
|
+
[home2.configFile, CONFIG_YAML],
|
|
3683
|
+
[home2.projectsFile, PROJECTS_YAML],
|
|
3684
|
+
[path10.join(home2.root, "README.md"), HOME_README]
|
|
3685
|
+
];
|
|
3686
|
+
if (options.examples) {
|
|
3687
|
+
mkdirSync4(path10.join(home2.stacksDir, "example"), { recursive: true });
|
|
3688
|
+
mkdirSync4(path10.join(home2.stacksDir, "dual"), { recursive: true });
|
|
3689
|
+
files.push(
|
|
3690
|
+
[path10.join(home2.stacksDir, "example", "stack.yaml"), EXAMPLE_STACK_YAML],
|
|
3691
|
+
[path10.join(home2.stacksDir, "dual", "stack.yaml"), REAL_STACK_TEMPLATE],
|
|
3692
|
+
[path10.join(home2.tasksDir, "reset.yaml"), RESET_TASK_YAML]
|
|
3693
|
+
);
|
|
3694
|
+
}
|
|
3695
|
+
for (const [file, contents] of files) {
|
|
3696
|
+
if (existsSync7(file)) {
|
|
3697
|
+
skipped.push(file);
|
|
3698
|
+
continue;
|
|
3699
|
+
}
|
|
3700
|
+
writeFileSync3(file, contents, "utf8");
|
|
3701
|
+
created.push(file);
|
|
3702
|
+
}
|
|
3703
|
+
return { root: home2.root, created, skipped, examples: options.examples ?? false };
|
|
3704
|
+
};
|
|
3705
|
+
var initCommand = (options = {}, env = process.env) => {
|
|
3706
|
+
const result = runInit(env, options);
|
|
3707
|
+
const mark = marks(env);
|
|
3708
|
+
console.log(`${color.bold("laracrew home")} ${color.gray(result.root)}`);
|
|
3709
|
+
for (const file of result.created) console.log(` ${mark.ok} ${path10.relative(result.root, file)}`);
|
|
3710
|
+
for (const file of result.skipped) {
|
|
3711
|
+
console.log(` ${mark.skip} ${color.gray(`${path10.relative(result.root, file)} (kept)`)}`);
|
|
3712
|
+
}
|
|
3713
|
+
const steps = result.examples ? [
|
|
3714
|
+
["laracrew up example", "run the demo stack \u2014 proves it works, no Laravel needed"],
|
|
3715
|
+
["laracrew link example", "install it as a global command you can type from anywhere"],
|
|
3716
|
+
["laracrew doctor dual", `check the real stack once ${color.gray(path10.join(result.root, "projects.yaml"))}`],
|
|
3717
|
+
["", `has your project paths, then ${color.cyan("laracrew up dual")}`]
|
|
3718
|
+
] : [
|
|
3719
|
+
["laracrew init --examples", "add a runnable demo stack and a two-project template,"],
|
|
3720
|
+
["", `or write your own at ${color.gray(path10.join(result.root, "stacks", "<name>", "stack.yaml"))}`],
|
|
3721
|
+
["laracrew draft <name>", "or build one in a project folder, one command at a time"],
|
|
3722
|
+
["laracrew up <name>", `boot it \u2014 ${color.cyan("laracrew ls")} shows what is defined`]
|
|
3723
|
+
];
|
|
3724
|
+
console.log(`
|
|
3725
|
+
${nextSteps(steps, env)}
|
|
3726
|
+
`);
|
|
3727
|
+
};
|
|
3728
|
+
|
|
3729
|
+
// src/cli/commands/logs.ts
|
|
3730
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4 } from "fs";
|
|
3731
|
+
import path11 from "path";
|
|
3732
|
+
|
|
3297
3733
|
// src/core/logs/read.ts
|
|
3298
|
-
import { openSync as openSync2, readSync, closeSync as closeSync2, statSync as statSync3, existsSync as
|
|
3734
|
+
import { openSync as openSync2, readSync, closeSync as closeSync2, statSync as statSync3, existsSync as existsSync8 } from "fs";
|
|
3299
3735
|
var CHUNK = 64 * 1024;
|
|
3300
3736
|
var tailFile = (file, limit) => {
|
|
3301
|
-
if (!
|
|
3737
|
+
if (!existsSync8(file) || limit <= 0) return [];
|
|
3302
3738
|
const size = statSync3(file).size;
|
|
3303
3739
|
if (size === 0) return [];
|
|
3304
3740
|
const handle = openSync2(file, "r");
|
|
@@ -3345,10 +3781,10 @@ var parseSince = (input) => {
|
|
|
3345
3781
|
return Number.isNaN(absolute) ? void 0 : absolute;
|
|
3346
3782
|
};
|
|
3347
3783
|
var followFile = (file, options) => {
|
|
3348
|
-
let position =
|
|
3784
|
+
let position = existsSync8(file) ? statSync3(file).size : 0;
|
|
3349
3785
|
let carry = "";
|
|
3350
3786
|
const read = () => {
|
|
3351
|
-
if (!
|
|
3787
|
+
if (!existsSync8(file)) return;
|
|
3352
3788
|
const size = statSync3(file).size;
|
|
3353
3789
|
if (size === position) return;
|
|
3354
3790
|
if (size < position) {
|
|
@@ -3377,23 +3813,23 @@ var followFile = (file, options) => {
|
|
|
3377
3813
|
};
|
|
3378
3814
|
|
|
3379
3815
|
// src/cli/commands/logs.ts
|
|
3380
|
-
var logDirFor = (stackName, env = process.env) =>
|
|
3816
|
+
var logDirFor = (stackName, env = process.env) => path11.join(paths(env).logsDir, stackName);
|
|
3381
3817
|
var targetsFor = (stackName, env = process.env) => {
|
|
3382
3818
|
const stack = prepareStack(stackName, { env });
|
|
3383
3819
|
const dir = logDirFor(stackName, env);
|
|
3384
3820
|
const names = fileNamesFor(stack.services.map((service) => service.name));
|
|
3385
3821
|
return stack.services.map((service) => ({
|
|
3386
3822
|
service: service.name,
|
|
3387
|
-
file:
|
|
3823
|
+
file: path11.join(dir, names.get(service.name)),
|
|
3388
3824
|
color: service.color
|
|
3389
|
-
})).filter((target) =>
|
|
3825
|
+
})).filter((target) => existsSync9(target.file));
|
|
3390
3826
|
};
|
|
3391
3827
|
var formatEntry = (entry, width, withName) => {
|
|
3392
3828
|
const time2 = new Date(entry.at || Date.now());
|
|
3393
3829
|
const clock = [time2.getHours(), time2.getMinutes(), time2.getSeconds()].map((part) => String(part).padStart(2, "0")).join(":");
|
|
3394
|
-
const who = withName ? `${
|
|
3395
|
-
const body = entry.stream === "stderr" ?
|
|
3396
|
-
return `${
|
|
3830
|
+
const who = withName ? `${color.cyan(padEnd(entry.service, width))} ` : "";
|
|
3831
|
+
const body = entry.stream === "stderr" ? color.red(entry.line) : entry.line;
|
|
3832
|
+
return `${color.gray(clock)} ${who}${color.gray("|")} ${body}
|
|
3397
3833
|
`;
|
|
3398
3834
|
};
|
|
3399
3835
|
var logsCommand = async (service, options = {}) => {
|
|
@@ -3402,25 +3838,24 @@ var logsCommand = async (service, options = {}) => {
|
|
|
3402
3838
|
if (!stackName) throw new ConfigError("name a stack with --stack, or run `laracrew logs` from a known stack");
|
|
3403
3839
|
const dir = logDirFor(stackName, env);
|
|
3404
3840
|
const available = targetsFor(stackName, env);
|
|
3841
|
+
const mark = marks(env);
|
|
3405
3842
|
if (options.list) {
|
|
3406
3843
|
if (available.length === 0) {
|
|
3407
|
-
console.log(
|
|
3408
|
-
console.log(
|
|
3844
|
+
console.log(`${mark.warn} ${color.yellow(`no log files yet in ${dir}`)}`);
|
|
3845
|
+
console.log(` ${color.gray("they appear once the stack has run at least once")}`);
|
|
3409
3846
|
return 0;
|
|
3410
3847
|
}
|
|
3411
|
-
console.log(
|
|
3848
|
+
console.log(`${color.bold("logs")} ${color.gray(glyphs(env).dot)} ${color.cyan.bold(stackName)}`);
|
|
3849
|
+
console.log(` ${color.gray(dir)}`);
|
|
3412
3850
|
for (const target of available) {
|
|
3413
|
-
console.log(` ${
|
|
3851
|
+
console.log(` ${color.cyan(target.service)} ${color.gray(path11.basename(target.file))}`);
|
|
3414
3852
|
}
|
|
3415
3853
|
return 0;
|
|
3416
3854
|
}
|
|
3417
3855
|
if (available.length === 0) {
|
|
3418
|
-
console.error(
|
|
3856
|
+
console.error(`${mark.warn} ${color.yellow(`no log files in ${dir}`)}`);
|
|
3419
3857
|
console.error(
|
|
3420
|
-
|
|
3421
|
-
"gray",
|
|
3422
|
-
"run the stack at least once, and check `defaults.logs.toFile` is not set to false"
|
|
3423
|
-
)
|
|
3858
|
+
` ${color.gray("run the stack at least once, and check `defaults.logs.toFile` is not set to false")}`
|
|
3424
3859
|
);
|
|
3425
3860
|
return 1;
|
|
3426
3861
|
}
|
|
@@ -3482,11 +3917,12 @@ var logsCommand = async (service, options = {}) => {
|
|
|
3482
3917
|
};
|
|
3483
3918
|
|
|
3484
3919
|
// src/cli/commands/ls.ts
|
|
3485
|
-
import { existsSync as
|
|
3920
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3486
3921
|
var lsCommand = (env = process.env, json = false) => {
|
|
3487
3922
|
const home2 = paths(env);
|
|
3488
|
-
|
|
3489
|
-
|
|
3923
|
+
const mark = marks(env);
|
|
3924
|
+
if (!existsSync10(home2.root)) {
|
|
3925
|
+
console.error(`${mark.warn} ${color.yellow("no laracrew home yet")} \u2014 run ${color.cyan("laracrew init")}`);
|
|
3490
3926
|
return 1;
|
|
3491
3927
|
}
|
|
3492
3928
|
const stackNames = listStacks(env);
|
|
@@ -3519,43 +3955,46 @@ var lsCommand = (env = process.env, json = false) => {
|
|
|
3519
3955
|
console.log(JSON.stringify({ root: home2.root, stacks, tasks: taskNames, projects }, null, 2));
|
|
3520
3956
|
return 0;
|
|
3521
3957
|
}
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3958
|
+
const heading = (label, count) => `
|
|
3959
|
+
${color.bold(label)} ${color.gray(`(${count})`)}`;
|
|
3960
|
+
console.log(`${color.bold("laracrew")} ${color.gray(home2.root)}`);
|
|
3961
|
+
console.log(heading("STACKS", stacks.length));
|
|
3962
|
+
if (stacks.length === 0) {
|
|
3963
|
+
console.log(color.gray(" none \u2014 run `laracrew init --examples`, or write stacks/<name>/stack.yaml"));
|
|
3964
|
+
}
|
|
3526
3965
|
const width = Math.max(4, ...stacks.map((stack) => stack.name.length));
|
|
3966
|
+
const indent = " ".repeat(width + 4);
|
|
3527
3967
|
for (const stack of stacks) {
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
console.log(
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
console.log(` ${" ".repeat(width)} ${paint("gray", `profiles: ${stack.profiles.join(", ")}`)}`);
|
|
3968
|
+
if (stack.valid) {
|
|
3969
|
+
const meta = padEnd(`${stack.services} service${stack.services === 1 ? "" : "s"}`, 11);
|
|
3970
|
+
console.log(` ${color.cyan.bold(padEnd(stack.name, width))} ${color.gray(meta)} ${stack.description}`.trimEnd());
|
|
3971
|
+
} else {
|
|
3972
|
+
console.log(
|
|
3973
|
+
`${mark.fail} ${color.red.bold(padEnd(stack.name, width))} ${color.red(padEnd("invalid", 11))} ${color.red(stack.description)}`
|
|
3974
|
+
);
|
|
3536
3975
|
}
|
|
3976
|
+
if (stack.command) console.log(`${indent}${color.green(`$ ${stack.command}`)} ${color.gray("global command")}`);
|
|
3977
|
+
if (stack.profiles.length > 0) console.log(`${indent}${color.gray(`profiles: ${stack.profiles.join(", ")}`)}`);
|
|
3537
3978
|
}
|
|
3538
|
-
console.log(`
|
|
3539
|
-
${paint("bold", "PROJECTS")}`);
|
|
3540
3979
|
const projectKeys = Object.keys(projects);
|
|
3541
|
-
|
|
3980
|
+
console.log(heading("PROJECTS", projectKeys.length));
|
|
3981
|
+
if (projectKeys.length === 0) console.log(color.gray(" none \u2014 add them to projects.yaml"));
|
|
3542
3982
|
const projectWidth = Math.max(4, ...projectKeys.map((key) => key.length));
|
|
3543
3983
|
for (const [key, project] of Object.entries(projects)) {
|
|
3544
|
-
const exists =
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
);
|
|
3548
|
-
}
|
|
3549
|
-
console.log(
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
for (const task of taskNames) console.log(` ${paint("green", task)}`);
|
|
3984
|
+
const exists = existsSync10(project.path);
|
|
3985
|
+
const lead = exists ? " " : `${mark.fail} `;
|
|
3986
|
+
const where = exists ? project.path : color.red(`${project.path} (missing)`);
|
|
3987
|
+
console.log(`${lead}${color.magenta.bold(padEnd(key, projectWidth))} ${where}`);
|
|
3988
|
+
}
|
|
3989
|
+
console.log(heading("TASKS", taskNames.length));
|
|
3990
|
+
if (taskNames.length === 0) console.log(color.gray(" none"));
|
|
3991
|
+
for (const task of taskNames) console.log(` ${color.green(task)}`);
|
|
3553
3992
|
console.log("");
|
|
3554
3993
|
return 0;
|
|
3555
3994
|
};
|
|
3556
3995
|
|
|
3557
3996
|
// src/cli/program.ts
|
|
3558
|
-
var VERSION = true ? "0.
|
|
3997
|
+
var VERSION = true ? "0.3.0" : versionFromPackageJson();
|
|
3559
3998
|
var resolveStackName = (explicit, env = process.env) => {
|
|
3560
3999
|
if (explicit) return explicit;
|
|
3561
4000
|
const configured = loadGlobalConfig(env).defaultStack;
|
|
@@ -3573,10 +4012,38 @@ var resolveStackName = (explicit, env = process.env) => {
|
|
|
3573
4012
|
var collect = (value, previous) => [...previous, value];
|
|
3574
4013
|
var buildProgram = () => {
|
|
3575
4014
|
const program = new Command();
|
|
3576
|
-
program.name("laracrew").description("Boot and supervise all the long-running processes of your projects with one command.").version(VERSION, "-v, --version").showHelpAfterError()
|
|
4015
|
+
program.name("laracrew").description("Boot and supervise all the long-running processes of your projects with one command.").version(VERSION, "-v, --version").showHelpAfterError().configureHelp({
|
|
4016
|
+
styleTitle: (text) => color.bold(text),
|
|
4017
|
+
styleCommandText: (text) => color.cyan(text),
|
|
4018
|
+
styleSubcommandText: (text) => color.cyan(text),
|
|
4019
|
+
styleOptionText: (text) => color.green(text),
|
|
4020
|
+
styleArgumentText: (text) => color.yellow(text)
|
|
4021
|
+
}).configureOutput({
|
|
4022
|
+
// One colour decision for the whole program — ours, so NO_COLOR is honoured here too.
|
|
4023
|
+
getOutHasColors: () => color.level > 0,
|
|
4024
|
+
getErrHasColors: () => color.level > 0,
|
|
4025
|
+
outputError: (message, write2) => write2(`${marks().fail} ${color.red(message.trimEnd())}
|
|
4026
|
+
`)
|
|
4027
|
+
});
|
|
3577
4028
|
program.command("init").description("create ~/.laracrew").option("--examples", "also write a runnable demo stack and a two-project template").action((options) => {
|
|
3578
4029
|
initCommand({ examples: options.examples ?? false });
|
|
3579
4030
|
});
|
|
4031
|
+
const draft = program.command("draft").description("build a stack step by step in the current folder, then publish it").argument("[name]", "start a draft with this stack name, or show it if it exists; omit to show the only draft here").option("--force", "start over when a draft of that name already exists here").action((name, options) => {
|
|
4032
|
+
process.exitCode = draftCommand(name, options);
|
|
4033
|
+
});
|
|
4034
|
+
draft.command("add").description("append a service to the draft in this folder").requiredOption("--command, --cmd <line>", 'what to run, e.g. "php artisan queue:work"').option("--project <key>", "the project it runs in \u2014 any short key, e.g. api").option("--path <dir>", "the project's folder, or where the command runs when there is no --project; defaults to this folder").option("--name <service>", "service name (default: project + command, e.g. api:queue:work)").option("--default-state <state>", '"running" (default) or "stopped": defined, but idle until you start it').option("--draft <name>", "which draft, when this folder holds several").action((options) => {
|
|
4035
|
+
process.exitCode = draftAddCommand({
|
|
4036
|
+
command: options.command ?? options.cmd,
|
|
4037
|
+
project: options.project,
|
|
4038
|
+
path: options.path,
|
|
4039
|
+
name: options.name,
|
|
4040
|
+
state: options.defaultState,
|
|
4041
|
+
draft: options.draft
|
|
4042
|
+
});
|
|
4043
|
+
});
|
|
4044
|
+
draft.command("publish").description("validate the draft and install it as a stack in ~/.laracrew").option("--force", "replace a stack of the same name").option("--link", "also install the global command, as `laracrew link` would").option("--draft <name>", "which draft, when this folder holds several").action((options) => {
|
|
4045
|
+
process.exitCode = draftPublishCommand(options);
|
|
4046
|
+
});
|
|
3580
4047
|
program.command("ls").alias("list").description("list stacks, projects and tasks").option("--json", "machine-readable output").action((options) => {
|
|
3581
4048
|
process.exitCode = lsCommand(process.env, options.json ?? false);
|
|
3582
4049
|
});
|
|
@@ -3620,13 +4087,19 @@ var buildProgram = () => {
|
|
|
3620
4087
|
});
|
|
3621
4088
|
return program;
|
|
3622
4089
|
};
|
|
4090
|
+
var renderConfigError = (error) => {
|
|
4091
|
+
const lines = [`${marks().fail} ${color.red.bold("config error")}`];
|
|
4092
|
+
if (error.file) lines.push(` ${color.gray(error.file)}`);
|
|
4093
|
+
lines.push(` ${error.message}`);
|
|
4094
|
+
for (const detail of error.details) lines.push(` ${color.gray("-")} ${detail}`);
|
|
4095
|
+
return lines.join("\n");
|
|
4096
|
+
};
|
|
3623
4097
|
var run = async (argv = process.argv) => {
|
|
3624
4098
|
try {
|
|
3625
4099
|
await buildProgram().parseAsync(argv);
|
|
3626
4100
|
} catch (error) {
|
|
3627
4101
|
if (error instanceof ConfigError) {
|
|
3628
|
-
console.error(
|
|
3629
|
-
${error.format()}`);
|
|
4102
|
+
console.error(renderConfigError(error));
|
|
3630
4103
|
process.exitCode = 1;
|
|
3631
4104
|
return;
|
|
3632
4105
|
}
|
|
@@ -3637,7 +4110,9 @@ ${error.format()}`);
|
|
|
3637
4110
|
// src/index.ts
|
|
3638
4111
|
installPipeGuards();
|
|
3639
4112
|
run().catch((error) => {
|
|
3640
|
-
|
|
4113
|
+
const [head, ...trace] = (error instanceof Error ? error.stack ?? error.message : String(error)).split("\n");
|
|
4114
|
+
console.error(`${marks().fail} ${color.red.bold(head ?? "unexpected error")}`);
|
|
4115
|
+
if (trace.length > 0) console.error(color.gray(trace.join("\n")));
|
|
3641
4116
|
process.exitCode = 1;
|
|
3642
4117
|
});
|
|
3643
4118
|
//# sourceMappingURL=index.js.map
|