@launchmatic/cli 0.6.5 → 0.7.1
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/dist/index.js +260 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21863,6 +21863,83 @@ function readManifest(repoRoot = findRepoRoot()) {
|
|
|
21863
21863
|
function writeManifest(manifest, repoRoot = findRepoRoot()) {
|
|
21864
21864
|
writeFileSync3(manifestPath(repoRoot), JSON.stringify(manifest, null, 2) + "\n");
|
|
21865
21865
|
}
|
|
21866
|
+
var COMPOSE_FILES = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
|
|
21867
|
+
var INFRA_IMAGE_MATCHERS = [
|
|
21868
|
+
{ engine: "POSTGIS", pattern: /^(postgis\/postgis|imresamu\/postgis|.*\/postgis)$/i },
|
|
21869
|
+
{ engine: "POSTGRESQL", pattern: /^(library\/)?(postgres|postgresql)$/i },
|
|
21870
|
+
{ engine: "REDIS", pattern: /^(library\/)?(redis|valkey\/valkey)$/i },
|
|
21871
|
+
{ engine: "MONGODB", pattern: /^(library\/)?mongo(db)?$/i },
|
|
21872
|
+
{ engine: "MYSQL", pattern: /^(library\/)?(mysql|mariadb)$/i }
|
|
21873
|
+
];
|
|
21874
|
+
function matchInfraImage(image) {
|
|
21875
|
+
const [imageRef, tag] = image.split(":");
|
|
21876
|
+
const lastSlash = imageRef.lastIndexOf("/");
|
|
21877
|
+
const namespace = lastSlash > 0 ? imageRef.slice(0, lastSlash) : "library";
|
|
21878
|
+
const repo = lastSlash > 0 ? imageRef.slice(lastSlash + 1) : imageRef;
|
|
21879
|
+
const candidate = `${namespace}/${repo}`;
|
|
21880
|
+
for (const m of INFRA_IMAGE_MATCHERS) {
|
|
21881
|
+
if (m.pattern.test(candidate) || m.pattern.test(repo)) {
|
|
21882
|
+
const version = tag ? tag.replace(/-(alpine|bullseye|bookworm|slim|debian).*$/i, "") : void 0;
|
|
21883
|
+
return { engine: m.engine, version };
|
|
21884
|
+
}
|
|
21885
|
+
}
|
|
21886
|
+
return null;
|
|
21887
|
+
}
|
|
21888
|
+
function discoverInfra(repoRoot = findRepoRoot()) {
|
|
21889
|
+
for (const file of COMPOSE_FILES) {
|
|
21890
|
+
const full = join2(repoRoot, file);
|
|
21891
|
+
if (!existsSync3(full)) continue;
|
|
21892
|
+
try {
|
|
21893
|
+
return parseComposeInfra(readFileSync3(full, "utf-8"));
|
|
21894
|
+
} catch {
|
|
21895
|
+
return [];
|
|
21896
|
+
}
|
|
21897
|
+
}
|
|
21898
|
+
return [];
|
|
21899
|
+
}
|
|
21900
|
+
function parseComposeInfra(yaml) {
|
|
21901
|
+
const lines = yaml.split(/\r?\n/);
|
|
21902
|
+
const out = [];
|
|
21903
|
+
let inServices = false;
|
|
21904
|
+
let currentService = null;
|
|
21905
|
+
let servicesIndent = -1;
|
|
21906
|
+
for (const raw of lines) {
|
|
21907
|
+
const line = raw.replace(/#.*$/, "").replace(/\s+$/, "");
|
|
21908
|
+
if (!line.trim()) continue;
|
|
21909
|
+
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
21910
|
+
if (!inServices) {
|
|
21911
|
+
if (/^services\s*:\s*$/.test(line)) {
|
|
21912
|
+
inServices = true;
|
|
21913
|
+
servicesIndent = indent;
|
|
21914
|
+
}
|
|
21915
|
+
continue;
|
|
21916
|
+
}
|
|
21917
|
+
if (indent <= servicesIndent && !/^\s*-/.test(line)) {
|
|
21918
|
+
inServices = false;
|
|
21919
|
+
currentService = null;
|
|
21920
|
+
continue;
|
|
21921
|
+
}
|
|
21922
|
+
const isServiceName = /^\s*([A-Za-z0-9_-]+)\s*:\s*$/.test(line);
|
|
21923
|
+
if (isServiceName && indent === servicesIndent + 2) {
|
|
21924
|
+
currentService = line.trim().replace(/:$/, "");
|
|
21925
|
+
continue;
|
|
21926
|
+
}
|
|
21927
|
+
if (currentService) {
|
|
21928
|
+
const m = line.match(/^\s*image\s*:\s*['"]?([^'"\s]+)['"]?\s*$/);
|
|
21929
|
+
if (m) {
|
|
21930
|
+
const matched = matchInfraImage(m[1]);
|
|
21931
|
+
if (matched) {
|
|
21932
|
+
out.push({
|
|
21933
|
+
name: currentService,
|
|
21934
|
+
engine: matched.engine,
|
|
21935
|
+
version: matched.version
|
|
21936
|
+
});
|
|
21937
|
+
}
|
|
21938
|
+
}
|
|
21939
|
+
}
|
|
21940
|
+
}
|
|
21941
|
+
return out;
|
|
21942
|
+
}
|
|
21866
21943
|
function discoverServices(repoRoot = findRepoRoot()) {
|
|
21867
21944
|
const globs = readWorkspaceGlobs(repoRoot);
|
|
21868
21945
|
const dirs = /* @__PURE__ */ new Set();
|
|
@@ -26144,6 +26221,74 @@ function prompt6(question) {
|
|
|
26144
26221
|
});
|
|
26145
26222
|
});
|
|
26146
26223
|
}
|
|
26224
|
+
function parseIndices(spec, max) {
|
|
26225
|
+
const out = /* @__PURE__ */ new Set();
|
|
26226
|
+
for (const token of spec.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
26227
|
+
const range = token.match(/^(\d+)-(\d+)$/);
|
|
26228
|
+
if (range) {
|
|
26229
|
+
const lo = parseInt(range[1], 10) - 1;
|
|
26230
|
+
const hi = parseInt(range[2], 10) - 1;
|
|
26231
|
+
if (lo >= 0 && hi < max && lo <= hi) {
|
|
26232
|
+
for (let i = lo; i <= hi; i++) out.add(i);
|
|
26233
|
+
}
|
|
26234
|
+
continue;
|
|
26235
|
+
}
|
|
26236
|
+
const n = parseInt(token, 10);
|
|
26237
|
+
if (Number.isFinite(n) && n >= 1 && n <= max) out.add(n - 1);
|
|
26238
|
+
}
|
|
26239
|
+
return [...out].sort((a, b) => a - b);
|
|
26240
|
+
}
|
|
26241
|
+
async function reviewList(label, items, render, editable) {
|
|
26242
|
+
if (items.length === 0) return items;
|
|
26243
|
+
let current = [...items];
|
|
26244
|
+
for (let round = 0; round < 5; round++) {
|
|
26245
|
+
console.log();
|
|
26246
|
+
console.log(source_default.bold(`Review ${label}:`));
|
|
26247
|
+
current.forEach((item, i) => {
|
|
26248
|
+
console.log(` ${source_default.dim(String(i + 1).padStart(2))}. ${render(item)}`);
|
|
26249
|
+
});
|
|
26250
|
+
const action = (await prompt6(
|
|
26251
|
+
`Action \u2014 [Enter] accept, [r] remove, [e] edit, [a] add? `
|
|
26252
|
+
)).toLowerCase();
|
|
26253
|
+
if (!action || action === "a" && current.length === items.length) {
|
|
26254
|
+
if (!action) break;
|
|
26255
|
+
}
|
|
26256
|
+
if (action === "r") {
|
|
26257
|
+
const spec = await prompt6("Indices to remove (e.g. 1,3-4): ");
|
|
26258
|
+
const drop = new Set(parseIndices(spec, current.length));
|
|
26259
|
+
if (drop.size > 0) {
|
|
26260
|
+
current = current.filter((_, i) => !drop.has(i));
|
|
26261
|
+
console.log(source_default.dim(`Removed ${drop.size} item${drop.size === 1 ? "" : "s"}.`));
|
|
26262
|
+
}
|
|
26263
|
+
continue;
|
|
26264
|
+
}
|
|
26265
|
+
if (action === "e") {
|
|
26266
|
+
const spec = await prompt6("Indices to edit (e.g. 2): ");
|
|
26267
|
+
const editIdx = parseIndices(spec, current.length);
|
|
26268
|
+
for (const idx of editIdx) {
|
|
26269
|
+
const item = { ...current[idx] };
|
|
26270
|
+
console.log(source_default.dim(`
|
|
26271
|
+
Editing ${render(item)}`));
|
|
26272
|
+
for (const field of editable) {
|
|
26273
|
+
const cur = item[field.key];
|
|
26274
|
+
const display = cur === void 0 || cur === null ? source_default.dim("unset") : source_default.cyan(String(cur));
|
|
26275
|
+
const ans = await prompt6(` ${field.label} [${display}]: `);
|
|
26276
|
+
if (ans) {
|
|
26277
|
+
if (field.numeric) {
|
|
26278
|
+
const n = parseInt(ans, 10);
|
|
26279
|
+
if (Number.isFinite(n)) item[field.key] = n;
|
|
26280
|
+
} else {
|
|
26281
|
+
item[field.key] = ans;
|
|
26282
|
+
}
|
|
26283
|
+
}
|
|
26284
|
+
}
|
|
26285
|
+
current[idx] = item;
|
|
26286
|
+
}
|
|
26287
|
+
continue;
|
|
26288
|
+
}
|
|
26289
|
+
}
|
|
26290
|
+
return current;
|
|
26291
|
+
}
|
|
26147
26292
|
async function initManifest(opts) {
|
|
26148
26293
|
if (!requireLogin3()) return;
|
|
26149
26294
|
const repoRoot = findRepoRoot();
|
|
@@ -26154,8 +26299,9 @@ async function initManifest(opts) {
|
|
|
26154
26299
|
process.exitCode = 1;
|
|
26155
26300
|
return;
|
|
26156
26301
|
}
|
|
26157
|
-
const spin = ora("Discovering services...").start();
|
|
26158
|
-
|
|
26302
|
+
const spin = ora("Discovering services and infra...").start();
|
|
26303
|
+
let discovered = discoverServices(repoRoot);
|
|
26304
|
+
let discoveredInfra = discoverInfra(repoRoot);
|
|
26159
26305
|
spin.stop();
|
|
26160
26306
|
if (discovered.length === 0) {
|
|
26161
26307
|
console.error(source_default.red("No services discovered."));
|
|
@@ -26170,6 +26316,46 @@ Discovered ${discovered.length} service${discovered.length === 1 ? "" : "s"}:`))
|
|
|
26170
26316
|
const fw = s.framework ? source_default.dim(` [${s.framework}]`) : "";
|
|
26171
26317
|
console.log(` ${source_default.cyan(s.name)} ${source_default.dim(s.rootDir)}${fw}`);
|
|
26172
26318
|
}
|
|
26319
|
+
if (discoveredInfra.length > 0) {
|
|
26320
|
+
console.log(source_default.bold(`
|
|
26321
|
+
Discovered ${discoveredInfra.length} infra dependenc${discoveredInfra.length === 1 ? "y" : "ies"} (from docker-compose):`));
|
|
26322
|
+
for (const i of discoveredInfra) {
|
|
26323
|
+
const v = i.version ? source_default.dim(` ${i.version}`) : "";
|
|
26324
|
+
console.log(` ${source_default.magenta(i.name)} ${source_default.dim(i.engine)}${v}`);
|
|
26325
|
+
}
|
|
26326
|
+
}
|
|
26327
|
+
if (!opts.yes) {
|
|
26328
|
+
discovered = await reviewList(
|
|
26329
|
+
`services`,
|
|
26330
|
+
discovered,
|
|
26331
|
+
(s) => `${source_default.cyan(s.name)} ${source_default.dim(s.rootDir)}${s.framework ? source_default.dim(` [${s.framework}]`) : ""}`,
|
|
26332
|
+
[
|
|
26333
|
+
{ key: "name", label: "name" },
|
|
26334
|
+
{ key: "rootDir", label: "rootDir" },
|
|
26335
|
+
{ key: "framework", label: "framework" },
|
|
26336
|
+
{ key: "buildCmd", label: "buildCmd" },
|
|
26337
|
+
{ key: "startCmd", label: "startCmd" },
|
|
26338
|
+
{ key: "port", label: "port", numeric: true }
|
|
26339
|
+
]
|
|
26340
|
+
);
|
|
26341
|
+
if (discovered.length === 0) {
|
|
26342
|
+
console.error(source_default.red("All services removed. Nothing to create."));
|
|
26343
|
+
process.exitCode = 1;
|
|
26344
|
+
return;
|
|
26345
|
+
}
|
|
26346
|
+
if (discoveredInfra.length > 0) {
|
|
26347
|
+
discoveredInfra = await reviewList(
|
|
26348
|
+
`infra`,
|
|
26349
|
+
discoveredInfra,
|
|
26350
|
+
(i) => `${source_default.magenta(i.name)} ${source_default.dim(i.engine)}${i.version ? source_default.dim(` ${i.version}`) : ""}`,
|
|
26351
|
+
[
|
|
26352
|
+
{ key: "name", label: "name" },
|
|
26353
|
+
{ key: "engine", label: "engine (POSTGRESQL/POSTGIS/REDIS/MONGODB/MYSQL)" },
|
|
26354
|
+
{ key: "version", label: "version" }
|
|
26355
|
+
]
|
|
26356
|
+
);
|
|
26357
|
+
}
|
|
26358
|
+
}
|
|
26173
26359
|
const { data: teams } = await api("/api/teams");
|
|
26174
26360
|
if (!teams || teams.length === 0) {
|
|
26175
26361
|
console.error(source_default.red("No teams found on your account."));
|
|
@@ -26270,15 +26456,76 @@ Discovered ${discovered.length} service${discovered.length === 1 ? "" : "s"}:`))
|
|
|
26270
26456
|
port: s.port
|
|
26271
26457
|
});
|
|
26272
26458
|
}
|
|
26459
|
+
const infra = [];
|
|
26460
|
+
if (discoveredInfra.length > 0) {
|
|
26461
|
+
const shouldProvision = opts.yes || (await prompt6(
|
|
26462
|
+
`
|
|
26463
|
+
Provision ${discoveredInfra.length} database${discoveredInfra.length === 1 ? "" : "s"} on Launchmatic? [Y/n]: `
|
|
26464
|
+
)).toLowerCase().replace(/^$/, "y").startsWith("y");
|
|
26465
|
+
if (shouldProvision) {
|
|
26466
|
+
const { data: existingDbs } = await api(`/api/databases?projectId=${projectId}`);
|
|
26467
|
+
for (const inf of discoveredInfra) {
|
|
26468
|
+
const dbName = `${projectSlug}-${inf.name}`.slice(0, 100);
|
|
26469
|
+
const already = existingDbs.find(
|
|
26470
|
+
(d) => d.name === dbName && d.engine === inf.engine
|
|
26471
|
+
);
|
|
26472
|
+
let databaseId = already?.id;
|
|
26473
|
+
if (!databaseId) {
|
|
26474
|
+
try {
|
|
26475
|
+
const created = await api(
|
|
26476
|
+
"/api/databases",
|
|
26477
|
+
{
|
|
26478
|
+
method: "POST",
|
|
26479
|
+
body: JSON.stringify({
|
|
26480
|
+
name: dbName,
|
|
26481
|
+
engine: inf.engine,
|
|
26482
|
+
projectId,
|
|
26483
|
+
// version is optional — let the API pick the default if undefined
|
|
26484
|
+
...inf.version ? { version: inf.version } : {}
|
|
26485
|
+
})
|
|
26486
|
+
}
|
|
26487
|
+
);
|
|
26488
|
+
databaseId = created.data.id;
|
|
26489
|
+
console.log(source_default.green(`Provisioned ${source_default.bold(inf.engine)} ${source_default.dim(`(${dbName})`)}`));
|
|
26490
|
+
} catch (err) {
|
|
26491
|
+
console.warn(
|
|
26492
|
+
source_default.yellow(
|
|
26493
|
+
`Could not provision ${inf.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
26494
|
+
)
|
|
26495
|
+
);
|
|
26496
|
+
}
|
|
26497
|
+
} else {
|
|
26498
|
+
console.log(source_default.dim(`Using existing ${inf.engine} ${dbName}`));
|
|
26499
|
+
}
|
|
26500
|
+
infra.push({
|
|
26501
|
+
name: inf.name,
|
|
26502
|
+
engine: inf.engine,
|
|
26503
|
+
...inf.version ? { version: inf.version } : {},
|
|
26504
|
+
...databaseId ? { databaseId } : {}
|
|
26505
|
+
});
|
|
26506
|
+
}
|
|
26507
|
+
} else {
|
|
26508
|
+
for (const inf of discoveredInfra) {
|
|
26509
|
+
infra.push({
|
|
26510
|
+
name: inf.name,
|
|
26511
|
+
engine: inf.engine,
|
|
26512
|
+
...inf.version ? { version: inf.version } : {}
|
|
26513
|
+
});
|
|
26514
|
+
}
|
|
26515
|
+
console.log(source_default.dim("Skipped provisioning \u2014 entries recorded in manifest only."));
|
|
26516
|
+
}
|
|
26517
|
+
}
|
|
26273
26518
|
const manifest = {
|
|
26274
26519
|
version: 1,
|
|
26275
26520
|
teamId,
|
|
26276
26521
|
projectId,
|
|
26277
|
-
services
|
|
26522
|
+
services,
|
|
26523
|
+
...infra.length > 0 ? { infra } : {}
|
|
26278
26524
|
};
|
|
26279
26525
|
writeManifest(manifest, repoRoot);
|
|
26280
26526
|
console.log();
|
|
26281
|
-
|
|
26527
|
+
const infraStr = infra.length > 0 ? `, ${infra.length} infra` : "";
|
|
26528
|
+
console.log(source_default.green(`\u2713 Wrote ${source_default.bold(MANIFEST_FILE)} (${services.length} services${infraStr})`));
|
|
26282
26529
|
console.log(source_default.dim("Next: ") + source_default.bold("lm up") + source_default.dim(" to deploy everything."));
|
|
26283
26530
|
}
|
|
26284
26531
|
async function listManifest() {
|
|
@@ -26294,6 +26541,15 @@ async function listManifest() {
|
|
|
26294
26541
|
const sid = s.serviceId ? source_default.dim(` ${s.serviceId}`) : source_default.yellow(" (not yet created)");
|
|
26295
26542
|
console.log(` ${source_default.cyan(s.name.padEnd(16))} ${source_default.dim(s.rootDir)}${fw}${sid}`);
|
|
26296
26543
|
}
|
|
26544
|
+
if (manifest.infra && manifest.infra.length > 0) {
|
|
26545
|
+
console.log(source_default.bold(`
|
|
26546
|
+
${manifest.infra.length} infra:`));
|
|
26547
|
+
for (const i of manifest.infra) {
|
|
26548
|
+
const ver = i.version ? source_default.dim(` ${i.version}`) : "";
|
|
26549
|
+
const did = i.databaseId ? source_default.dim(` ${i.databaseId}`) : source_default.yellow(" (not yet provisioned)");
|
|
26550
|
+
console.log(` ${source_default.magenta(i.name.padEnd(16))} ${source_default.dim(i.engine)}${ver}${did}`);
|
|
26551
|
+
}
|
|
26552
|
+
}
|
|
26297
26553
|
}
|
|
26298
26554
|
async function up(opts) {
|
|
26299
26555
|
if (!requireLogin3()) return;
|