@launchmatic/cli 0.6.5 → 0.7.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.
Files changed (2) hide show
  1. package/dist/index.js +159 -3
  2. 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();
@@ -26154,8 +26231,9 @@ async function initManifest(opts) {
26154
26231
  process.exitCode = 1;
26155
26232
  return;
26156
26233
  }
26157
- const spin = ora("Discovering services...").start();
26234
+ const spin = ora("Discovering services and infra...").start();
26158
26235
  const discovered = discoverServices(repoRoot);
26236
+ const discoveredInfra = discoverInfra(repoRoot);
26159
26237
  spin.stop();
26160
26238
  if (discovered.length === 0) {
26161
26239
  console.error(source_default.red("No services discovered."));
@@ -26170,6 +26248,14 @@ Discovered ${discovered.length} service${discovered.length === 1 ? "" : "s"}:`))
26170
26248
  const fw = s.framework ? source_default.dim(` [${s.framework}]`) : "";
26171
26249
  console.log(` ${source_default.cyan(s.name)} ${source_default.dim(s.rootDir)}${fw}`);
26172
26250
  }
26251
+ if (discoveredInfra.length > 0) {
26252
+ console.log(source_default.bold(`
26253
+ Discovered ${discoveredInfra.length} infra dependenc${discoveredInfra.length === 1 ? "y" : "ies"} (from docker-compose):`));
26254
+ for (const i of discoveredInfra) {
26255
+ const v = i.version ? source_default.dim(` ${i.version}`) : "";
26256
+ console.log(` ${source_default.magenta(i.name)} ${source_default.dim(i.engine)}${v}`);
26257
+ }
26258
+ }
26173
26259
  const { data: teams } = await api("/api/teams");
26174
26260
  if (!teams || teams.length === 0) {
26175
26261
  console.error(source_default.red("No teams found on your account."));
@@ -26270,15 +26356,76 @@ Discovered ${discovered.length} service${discovered.length === 1 ? "" : "s"}:`))
26270
26356
  port: s.port
26271
26357
  });
26272
26358
  }
26359
+ const infra = [];
26360
+ if (discoveredInfra.length > 0) {
26361
+ const shouldProvision = opts.yes || (await prompt6(
26362
+ `
26363
+ Provision ${discoveredInfra.length} database${discoveredInfra.length === 1 ? "" : "s"} on Launchmatic? [Y/n]: `
26364
+ )).toLowerCase().replace(/^$/, "y").startsWith("y");
26365
+ if (shouldProvision) {
26366
+ const { data: existingDbs } = await api(`/api/databases?projectId=${projectId}`);
26367
+ for (const inf of discoveredInfra) {
26368
+ const dbName = `${projectSlug}-${inf.name}`.slice(0, 100);
26369
+ const already = existingDbs.find(
26370
+ (d) => d.name === dbName && d.engine === inf.engine
26371
+ );
26372
+ let databaseId = already?.id;
26373
+ if (!databaseId) {
26374
+ try {
26375
+ const created = await api(
26376
+ "/api/databases",
26377
+ {
26378
+ method: "POST",
26379
+ body: JSON.stringify({
26380
+ name: dbName,
26381
+ engine: inf.engine,
26382
+ projectId,
26383
+ // version is optional — let the API pick the default if undefined
26384
+ ...inf.version ? { version: inf.version } : {}
26385
+ })
26386
+ }
26387
+ );
26388
+ databaseId = created.data.id;
26389
+ console.log(source_default.green(`Provisioned ${source_default.bold(inf.engine)} ${source_default.dim(`(${dbName})`)}`));
26390
+ } catch (err) {
26391
+ console.warn(
26392
+ source_default.yellow(
26393
+ `Could not provision ${inf.name}: ${err instanceof Error ? err.message : String(err)}`
26394
+ )
26395
+ );
26396
+ }
26397
+ } else {
26398
+ console.log(source_default.dim(`Using existing ${inf.engine} ${dbName}`));
26399
+ }
26400
+ infra.push({
26401
+ name: inf.name,
26402
+ engine: inf.engine,
26403
+ ...inf.version ? { version: inf.version } : {},
26404
+ ...databaseId ? { databaseId } : {}
26405
+ });
26406
+ }
26407
+ } else {
26408
+ for (const inf of discoveredInfra) {
26409
+ infra.push({
26410
+ name: inf.name,
26411
+ engine: inf.engine,
26412
+ ...inf.version ? { version: inf.version } : {}
26413
+ });
26414
+ }
26415
+ console.log(source_default.dim("Skipped provisioning \u2014 entries recorded in manifest only."));
26416
+ }
26417
+ }
26273
26418
  const manifest = {
26274
26419
  version: 1,
26275
26420
  teamId,
26276
26421
  projectId,
26277
- services
26422
+ services,
26423
+ ...infra.length > 0 ? { infra } : {}
26278
26424
  };
26279
26425
  writeManifest(manifest, repoRoot);
26280
26426
  console.log();
26281
- console.log(source_default.green(`\u2713 Wrote ${source_default.bold(MANIFEST_FILE)} (${services.length} services)`));
26427
+ const infraStr = infra.length > 0 ? `, ${infra.length} infra` : "";
26428
+ console.log(source_default.green(`\u2713 Wrote ${source_default.bold(MANIFEST_FILE)} (${services.length} services${infraStr})`));
26282
26429
  console.log(source_default.dim("Next: ") + source_default.bold("lm up") + source_default.dim(" to deploy everything."));
26283
26430
  }
26284
26431
  async function listManifest() {
@@ -26294,6 +26441,15 @@ async function listManifest() {
26294
26441
  const sid = s.serviceId ? source_default.dim(` ${s.serviceId}`) : source_default.yellow(" (not yet created)");
26295
26442
  console.log(` ${source_default.cyan(s.name.padEnd(16))} ${source_default.dim(s.rootDir)}${fw}${sid}`);
26296
26443
  }
26444
+ if (manifest.infra && manifest.infra.length > 0) {
26445
+ console.log(source_default.bold(`
26446
+ ${manifest.infra.length} infra:`));
26447
+ for (const i of manifest.infra) {
26448
+ const ver = i.version ? source_default.dim(` ${i.version}`) : "";
26449
+ const did = i.databaseId ? source_default.dim(` ${i.databaseId}`) : source_default.yellow(" (not yet provisioned)");
26450
+ console.log(` ${source_default.magenta(i.name.padEnd(16))} ${source_default.dim(i.engine)}${ver}${did}`);
26451
+ }
26452
+ }
26297
26453
  }
26298
26454
  async function up(opts) {
26299
26455
  if (!requireLogin3()) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launchmatic/cli",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "description": "Launchmatic CLI — deploy from your terminal",
5
5
  "private": false,
6
6
  "type": "module",