@launchmatic/cli 0.4.0 → 0.6.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 (4) hide show
  1. package/README.md +178 -178
  2. package/dist/c.js +0 -0
  3. package/dist/index.js +1078 -133
  4. package/package.json +46 -37
package/dist/index.js CHANGED
@@ -972,7 +972,7 @@ var require_command = __commonJS({
972
972
  "use strict";
973
973
  var EventEmitter = __require("events").EventEmitter;
974
974
  var childProcess2 = __require("child_process");
975
- var path6 = __require("path");
975
+ var path7 = __require("path");
976
976
  var fs8 = __require("fs");
977
977
  var process22 = __require("process");
978
978
  var { Argument: Argument2, humanReadableArgName } = require_argument();
@@ -1905,9 +1905,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
1905
1905
  let launchWithNode = false;
1906
1906
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1907
1907
  function findFile(baseDir, baseName) {
1908
- const localBin = path6.resolve(baseDir, baseName);
1908
+ const localBin = path7.resolve(baseDir, baseName);
1909
1909
  if (fs8.existsSync(localBin)) return localBin;
1910
- if (sourceExt.includes(path6.extname(baseName))) return void 0;
1910
+ if (sourceExt.includes(path7.extname(baseName))) return void 0;
1911
1911
  const foundExt = sourceExt.find(
1912
1912
  (ext) => fs8.existsSync(`${localBin}${ext}`)
1913
1913
  );
@@ -1925,17 +1925,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
1925
1925
  } catch (err) {
1926
1926
  resolvedScriptPath = this._scriptPath;
1927
1927
  }
1928
- executableDir = path6.resolve(
1929
- path6.dirname(resolvedScriptPath),
1928
+ executableDir = path7.resolve(
1929
+ path7.dirname(resolvedScriptPath),
1930
1930
  executableDir
1931
1931
  );
1932
1932
  }
1933
1933
  if (executableDir) {
1934
1934
  let localFile = findFile(executableDir, executableFile);
1935
1935
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1936
- const legacyName = path6.basename(
1936
+ const legacyName = path7.basename(
1937
1937
  this._scriptPath,
1938
- path6.extname(this._scriptPath)
1938
+ path7.extname(this._scriptPath)
1939
1939
  );
1940
1940
  if (legacyName !== this._name) {
1941
1941
  localFile = findFile(
@@ -1946,7 +1946,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1946
1946
  }
1947
1947
  executableFile = localFile || executableFile;
1948
1948
  }
1949
- launchWithNode = sourceExt.includes(path6.extname(executableFile));
1949
+ launchWithNode = sourceExt.includes(path7.extname(executableFile));
1950
1950
  let proc;
1951
1951
  if (process22.platform !== "win32") {
1952
1952
  if (launchWithNode) {
@@ -2786,7 +2786,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2786
2786
  * @return {Command}
2787
2787
  */
2788
2788
  nameFromFilename(filename) {
2789
- this._name = path6.basename(filename, path6.extname(filename));
2789
+ this._name = path7.basename(filename, path7.extname(filename));
2790
2790
  return this;
2791
2791
  }
2792
2792
  /**
@@ -2800,9 +2800,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2800
2800
  * @param {string} [path]
2801
2801
  * @return {(string|null|Command)}
2802
2802
  */
2803
- executableDir(path7) {
2804
- if (path7 === void 0) return this._executableDir;
2805
- this._executableDir = path7;
2803
+ executableDir(path8) {
2804
+ if (path8 === void 0) return this._executableDir;
2805
+ this._executableDir = path8;
2806
2806
  return this;
2807
2807
  }
2808
2808
  /**
@@ -5797,16 +5797,16 @@ var require_validate = __commonJS({
5797
5797
  const matches = RELATIVE_JSON_POINTER.exec($data);
5798
5798
  if (!matches)
5799
5799
  throw new Error(`Invalid JSON-pointer: ${$data}`);
5800
- const up = +matches[1];
5800
+ const up2 = +matches[1];
5801
5801
  jsonPointer = matches[2];
5802
5802
  if (jsonPointer === "#") {
5803
- if (up >= dataLevel)
5804
- throw new Error(errorMsg("property/index", up));
5805
- return dataPathArr[dataLevel - up];
5803
+ if (up2 >= dataLevel)
5804
+ throw new Error(errorMsg("property/index", up2));
5805
+ return dataPathArr[dataLevel - up2];
5806
5806
  }
5807
- if (up > dataLevel)
5808
- throw new Error(errorMsg("data", up));
5809
- data = dataNames[dataLevel - up];
5807
+ if (up2 > dataLevel)
5808
+ throw new Error(errorMsg("data", up2));
5809
+ data = dataNames[dataLevel - up2];
5810
5810
  if (!jsonPointer)
5811
5811
  return data;
5812
5812
  }
@@ -5819,8 +5819,8 @@ var require_validate = __commonJS({
5819
5819
  }
5820
5820
  }
5821
5821
  return expr;
5822
- function errorMsg(pointerType, up) {
5823
- return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}`;
5822
+ function errorMsg(pointerType, up2) {
5823
+ return `Cannot access ${pointerType} ${up2} levels up, current level is ${dataLevel}`;
5824
5824
  }
5825
5825
  }
5826
5826
  exports.getData = getData;
@@ -5984,7 +5984,7 @@ var require_compile = __commonJS({
5984
5984
  const schOrFunc = root.refs[ref];
5985
5985
  if (schOrFunc)
5986
5986
  return schOrFunc;
5987
- let _sch = resolve4.call(this, root, ref);
5987
+ let _sch = resolve5.call(this, root, ref);
5988
5988
  if (_sch === void 0) {
5989
5989
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
5990
5990
  const { schemaId } = this.opts;
@@ -6011,7 +6011,7 @@ var require_compile = __commonJS({
6011
6011
  function sameSchemaEnv(s1, s2) {
6012
6012
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
6013
6013
  }
6014
- function resolve4(root, ref) {
6014
+ function resolve5(root, ref) {
6015
6015
  let sch;
6016
6016
  while (typeof (sch = this.refs[ref]) == "string")
6017
6017
  ref = sch;
@@ -6226,8 +6226,8 @@ var require_utils = __commonJS({
6226
6226
  }
6227
6227
  return ind;
6228
6228
  }
6229
- function removeDotSegments(path6) {
6230
- let input = path6;
6229
+ function removeDotSegments(path7) {
6230
+ let input = path7;
6231
6231
  const output = [];
6232
6232
  let nextSlash = -1;
6233
6233
  let len = 0;
@@ -6426,8 +6426,8 @@ var require_schemes = __commonJS({
6426
6426
  wsComponent.secure = void 0;
6427
6427
  }
6428
6428
  if (wsComponent.resourceName) {
6429
- const [path6, query] = wsComponent.resourceName.split("?");
6430
- wsComponent.path = path6 && path6 !== "/" ? path6 : void 0;
6429
+ const [path7, query] = wsComponent.resourceName.split("?");
6430
+ wsComponent.path = path7 && path7 !== "/" ? path7 : void 0;
6431
6431
  wsComponent.query = query;
6432
6432
  wsComponent.resourceName = void 0;
6433
6433
  }
@@ -6586,55 +6586,55 @@ var require_fast_uri = __commonJS({
6586
6586
  }
6587
6587
  return uri;
6588
6588
  }
6589
- function resolve4(baseURI, relativeURI, options) {
6589
+ function resolve5(baseURI, relativeURI, options) {
6590
6590
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
6591
6591
  const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
6592
6592
  schemelessOptions.skipEscape = true;
6593
6593
  return serialize(resolved, schemelessOptions);
6594
6594
  }
6595
- function resolveComponent(base, relative2, options, skipNormalization) {
6595
+ function resolveComponent(base, relative3, options, skipNormalization) {
6596
6596
  const target = {};
6597
6597
  if (!skipNormalization) {
6598
6598
  base = parse(serialize(base, options), options);
6599
- relative2 = parse(serialize(relative2, options), options);
6599
+ relative3 = parse(serialize(relative3, options), options);
6600
6600
  }
6601
6601
  options = options || {};
6602
- if (!options.tolerant && relative2.scheme) {
6603
- target.scheme = relative2.scheme;
6604
- target.userinfo = relative2.userinfo;
6605
- target.host = relative2.host;
6606
- target.port = relative2.port;
6607
- target.path = removeDotSegments(relative2.path || "");
6608
- target.query = relative2.query;
6602
+ if (!options.tolerant && relative3.scheme) {
6603
+ target.scheme = relative3.scheme;
6604
+ target.userinfo = relative3.userinfo;
6605
+ target.host = relative3.host;
6606
+ target.port = relative3.port;
6607
+ target.path = removeDotSegments(relative3.path || "");
6608
+ target.query = relative3.query;
6609
6609
  } else {
6610
- if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
6611
- target.userinfo = relative2.userinfo;
6612
- target.host = relative2.host;
6613
- target.port = relative2.port;
6614
- target.path = removeDotSegments(relative2.path || "");
6615
- target.query = relative2.query;
6610
+ if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
6611
+ target.userinfo = relative3.userinfo;
6612
+ target.host = relative3.host;
6613
+ target.port = relative3.port;
6614
+ target.path = removeDotSegments(relative3.path || "");
6615
+ target.query = relative3.query;
6616
6616
  } else {
6617
- if (!relative2.path) {
6617
+ if (!relative3.path) {
6618
6618
  target.path = base.path;
6619
- if (relative2.query !== void 0) {
6620
- target.query = relative2.query;
6619
+ if (relative3.query !== void 0) {
6620
+ target.query = relative3.query;
6621
6621
  } else {
6622
6622
  target.query = base.query;
6623
6623
  }
6624
6624
  } else {
6625
- if (relative2.path[0] === "/") {
6626
- target.path = removeDotSegments(relative2.path);
6625
+ if (relative3.path[0] === "/") {
6626
+ target.path = removeDotSegments(relative3.path);
6627
6627
  } else {
6628
6628
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
6629
- target.path = "/" + relative2.path;
6629
+ target.path = "/" + relative3.path;
6630
6630
  } else if (!base.path) {
6631
- target.path = relative2.path;
6631
+ target.path = relative3.path;
6632
6632
  } else {
6633
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
6633
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
6634
6634
  }
6635
6635
  target.path = removeDotSegments(target.path);
6636
6636
  }
6637
- target.query = relative2.query;
6637
+ target.query = relative3.query;
6638
6638
  }
6639
6639
  target.userinfo = base.userinfo;
6640
6640
  target.host = base.host;
@@ -6642,7 +6642,7 @@ var require_fast_uri = __commonJS({
6642
6642
  }
6643
6643
  target.scheme = base.scheme;
6644
6644
  }
6645
- target.fragment = relative2.fragment;
6645
+ target.fragment = relative3.fragment;
6646
6646
  return target;
6647
6647
  }
6648
6648
  function equal(uriA, uriB, options) {
@@ -6813,7 +6813,7 @@ var require_fast_uri = __commonJS({
6813
6813
  var fastUri = {
6814
6814
  SCHEMES,
6815
6815
  normalize,
6816
- resolve: resolve4,
6816
+ resolve: resolve5,
6817
6817
  resolveComponent,
6818
6818
  equal,
6819
6819
  serialize,
@@ -18766,14 +18766,14 @@ var baseOpen = async (options) => {
18766
18766
  }
18767
18767
  const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
18768
18768
  if (options.wait) {
18769
- return new Promise((resolve4, reject) => {
18769
+ return new Promise((resolve5, reject) => {
18770
18770
  subprocess.once("error", reject);
18771
18771
  subprocess.once("close", (exitCode) => {
18772
18772
  if (!options.allowNonzeroExitCode && exitCode > 0) {
18773
18773
  reject(new Error(`Exited with code ${exitCode}`));
18774
18774
  return;
18775
18775
  }
18776
- resolve4(subprocess);
18776
+ resolve5(subprocess);
18777
18777
  });
18778
18778
  });
18779
18779
  }
@@ -18866,12 +18866,12 @@ var disallowedKeys = /* @__PURE__ */ new Set([
18866
18866
  "constructor"
18867
18867
  ]);
18868
18868
  var digits = new Set("0123456789");
18869
- function getPathSegments(path6) {
18869
+ function getPathSegments(path7) {
18870
18870
  const parts = [];
18871
18871
  let currentSegment = "";
18872
18872
  let currentPart = "start";
18873
18873
  let isIgnoring = false;
18874
- for (const character of path6) {
18874
+ for (const character of path7) {
18875
18875
  switch (character) {
18876
18876
  case "\\": {
18877
18877
  if (currentPart === "index") {
@@ -18993,11 +18993,11 @@ function assertNotStringIndex(object, key) {
18993
18993
  throw new Error("Cannot use string index");
18994
18994
  }
18995
18995
  }
18996
- function getProperty(object, path6, value) {
18997
- if (!isObject(object) || typeof path6 !== "string") {
18996
+ function getProperty(object, path7, value) {
18997
+ if (!isObject(object) || typeof path7 !== "string") {
18998
18998
  return value === void 0 ? object : value;
18999
18999
  }
19000
- const pathArray = getPathSegments(path6);
19000
+ const pathArray = getPathSegments(path7);
19001
19001
  if (pathArray.length === 0) {
19002
19002
  return value;
19003
19003
  }
@@ -19017,12 +19017,12 @@ function getProperty(object, path6, value) {
19017
19017
  }
19018
19018
  return object === void 0 ? value : object;
19019
19019
  }
19020
- function setProperty(object, path6, value) {
19021
- if (!isObject(object) || typeof path6 !== "string") {
19020
+ function setProperty(object, path7, value) {
19021
+ if (!isObject(object) || typeof path7 !== "string") {
19022
19022
  return object;
19023
19023
  }
19024
19024
  const root = object;
19025
- const pathArray = getPathSegments(path6);
19025
+ const pathArray = getPathSegments(path7);
19026
19026
  for (let index = 0; index < pathArray.length; index++) {
19027
19027
  const key = pathArray[index];
19028
19028
  assertNotStringIndex(object, key);
@@ -19035,11 +19035,11 @@ function setProperty(object, path6, value) {
19035
19035
  }
19036
19036
  return root;
19037
19037
  }
19038
- function deleteProperty(object, path6) {
19039
- if (!isObject(object) || typeof path6 !== "string") {
19038
+ function deleteProperty(object, path7) {
19039
+ if (!isObject(object) || typeof path7 !== "string") {
19040
19040
  return false;
19041
19041
  }
19042
- const pathArray = getPathSegments(path6);
19042
+ const pathArray = getPathSegments(path7);
19043
19043
  for (let index = 0; index < pathArray.length; index++) {
19044
19044
  const key = pathArray[index];
19045
19045
  assertNotStringIndex(object, key);
@@ -19053,11 +19053,11 @@ function deleteProperty(object, path6) {
19053
19053
  }
19054
19054
  }
19055
19055
  }
19056
- function hasProperty(object, path6) {
19057
- if (!isObject(object) || typeof path6 !== "string") {
19056
+ function hasProperty(object, path7) {
19057
+ if (!isObject(object) || typeof path7 !== "string") {
19058
19058
  return false;
19059
19059
  }
19060
- const pathArray = getPathSegments(path6);
19060
+ const pathArray = getPathSegments(path7);
19061
19061
  if (pathArray.length === 0) {
19062
19062
  return false;
19063
19063
  }
@@ -19176,7 +19176,7 @@ var retryifyAsync = (fn, options) => {
19176
19176
  throw error;
19177
19177
  const delay = Math.round(interval * Math.random());
19178
19178
  if (delay > 0) {
19179
- const delayPromise = new Promise((resolve4) => setTimeout(resolve4, delay));
19179
+ const delayPromise = new Promise((resolve5) => setTimeout(resolve5, delay));
19180
19180
  return delayPromise.then(() => attempt.apply(void 0, args));
19181
19181
  } else {
19182
19182
  return attempt.apply(void 0, args);
@@ -20180,9 +20180,9 @@ async function getWsToken() {
20180
20180
  );
20181
20181
  return data.token;
20182
20182
  }
20183
- async function api(path6, options = {}) {
20183
+ async function api(path7, options = {}) {
20184
20184
  const { accessToken } = getTokens();
20185
- const url = `${getApiUrl()}${path6}`;
20185
+ const url = `${getApiUrl()}${path7}`;
20186
20186
  const headers = {
20187
20187
  "Content-Type": "application/json",
20188
20188
  ...options.headers || {}
@@ -20211,9 +20211,9 @@ async function api(path6, options = {}) {
20211
20211
  }
20212
20212
  return res.json();
20213
20213
  }
20214
- async function* streamApi(path6, options = {}) {
20214
+ async function* streamApi(path7, options = {}) {
20215
20215
  const { accessToken } = getTokens();
20216
- const url = `${getApiUrl()}${path6}`;
20216
+ const url = `${getApiUrl()}${path7}`;
20217
20217
  const headers = {
20218
20218
  "Content-Type": "application/json",
20219
20219
  Accept: "text/event-stream",
@@ -20351,7 +20351,7 @@ function registerLogin(program3) {
20351
20351
  });
20352
20352
  }
20353
20353
  function waitForOAuth(apiUrl) {
20354
- return new Promise((resolve4, reject) => {
20354
+ return new Promise((resolve5, reject) => {
20355
20355
  let settled = false;
20356
20356
  function settle() {
20357
20357
  if (settled) return false;
@@ -20373,7 +20373,7 @@ function waitForOAuth(apiUrl) {
20373
20373
  res.writeHead(200, { "Content-Type": "text/html" });
20374
20374
  res.end(authPage(true), () => {
20375
20375
  if (settle()) {
20376
- resolve4({ accessToken, refreshToken });
20376
+ resolve5({ accessToken, refreshToken });
20377
20377
  }
20378
20378
  });
20379
20379
  } else {
@@ -20397,7 +20397,7 @@ function waitForOAuth(apiUrl) {
20397
20397
  const rt = pastedUrl.searchParams.get("refresh_token");
20398
20398
  if (at && rt) {
20399
20399
  if (settle()) {
20400
- resolve4({ accessToken: at, refreshToken: rt });
20400
+ resolve5({ accessToken: at, refreshToken: rt });
20401
20401
  }
20402
20402
  }
20403
20403
  } catch {
@@ -21317,13 +21317,13 @@ function configPath() {
21317
21317
  return resolve(process.cwd(), CONFIG_FILE);
21318
21318
  }
21319
21319
  function readContext() {
21320
- const path6 = configPath();
21321
- if (!existsSync(path6)) {
21320
+ const path7 = configPath();
21321
+ if (!existsSync(path7)) {
21322
21322
  throw new Error(
21323
21323
  `No ${CONFIG_FILE} found in current directory. Run "lm init" first.`
21324
21324
  );
21325
21325
  }
21326
- const raw = readFileSync(path6, "utf-8");
21326
+ const raw = readFileSync(path7, "utf-8");
21327
21327
  try {
21328
21328
  return JSON.parse(raw);
21329
21329
  } catch {
@@ -21339,9 +21339,9 @@ function contextExists() {
21339
21339
  return existsSync(configPath());
21340
21340
  }
21341
21341
  function removeContext() {
21342
- const path6 = configPath();
21343
- if (existsSync(path6)) {
21344
- unlinkSync(path6);
21342
+ const path7 = configPath();
21343
+ if (existsSync(path7)) {
21344
+ unlinkSync(path7);
21345
21345
  }
21346
21346
  }
21347
21347
 
@@ -21803,10 +21803,10 @@ function prompt(question) {
21803
21803
  input: process.stdin,
21804
21804
  output: process.stdout
21805
21805
  });
21806
- return new Promise((resolve4) => {
21806
+ return new Promise((resolve5) => {
21807
21807
  rl.question(question, (answer) => {
21808
21808
  rl.close();
21809
- resolve4(answer.trim());
21809
+ resolve5(answer.trim());
21810
21810
  });
21811
21811
  });
21812
21812
  }
@@ -21824,7 +21824,13 @@ var wrapper_default = import_websocket.default;
21824
21824
 
21825
21825
  // src/commands/deploy.ts
21826
21826
  function registerDeploy(program3) {
21827
- program3.command("deploy").description("Build and deploy the current service").action(async () => {
21827
+ program3.command("deploy").description("Build and deploy the current service").option(
21828
+ "--dockerfile <path>",
21829
+ "Path to Dockerfile relative to repo root (e.g. apps/web/Dockerfile). Use when build context must be the repo root but the Dockerfile lives in a subdirectory (monorepos). Pass an empty string to clear."
21830
+ ).option(
21831
+ "--root-dir <path>",
21832
+ "Root directory for the build (default: /)"
21833
+ ).option("--build-cmd <cmd>", "Override the build command").option("--start-cmd <cmd>", "Override the start command").action(async (opts) => {
21828
21834
  if (!isLoggedIn()) {
21829
21835
  console.error(source_default.red('Not logged in. Run "lm login" first.'));
21830
21836
  process.exitCode = 1;
@@ -21863,13 +21869,60 @@ function registerDeploy(program3) {
21863
21869
  }
21864
21870
  }
21865
21871
  if (!gitInfo.hasRemote) {
21866
- console.error(
21867
- source_default.red(
21868
- "No git remote found. Add a GitHub remote and push your code first."
21869
- )
21870
- );
21871
- process.exitCode = 1;
21872
- return;
21872
+ const repoSpinner = ora("No git remote \u2014 creating GitHub repository...").start();
21873
+ try {
21874
+ try {
21875
+ execFileSync4("git", ["rev-parse", "--git-dir"], { cwd, stdio: "pipe" });
21876
+ } catch {
21877
+ execFileSync4("git", ["init"], { cwd, stdio: "pipe" });
21878
+ }
21879
+ try {
21880
+ const status = execFileSync4("git", ["status", "--porcelain"], {
21881
+ cwd,
21882
+ encoding: "utf-8",
21883
+ stdio: ["pipe", "pipe", "pipe"]
21884
+ });
21885
+ if (status.trim().length > 0) {
21886
+ execFileSync4("git", ["add", "-A"], { cwd, stdio: "pipe" });
21887
+ execFileSync4(
21888
+ "git",
21889
+ ["commit", "-m", "Initial commit"],
21890
+ { cwd, stdio: "pipe" }
21891
+ );
21892
+ }
21893
+ } catch {
21894
+ }
21895
+ const { data: repoData } = await api(`/api/services/${ctx.serviceId}/create-repo`, {
21896
+ method: "POST",
21897
+ body: JSON.stringify({})
21898
+ });
21899
+ try {
21900
+ execFileSync4("git", ["remote", "add", "origin", repoData.cloneUrl], { cwd, stdio: "pipe" });
21901
+ } catch {
21902
+ execFileSync4("git", ["remote", "set-url", "origin", repoData.cloneUrl], { cwd, stdio: "pipe" });
21903
+ }
21904
+ const localBranch = (() => {
21905
+ try {
21906
+ return execFileSync4("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd, encoding: "utf-8" }).trim();
21907
+ } catch {
21908
+ return "main";
21909
+ }
21910
+ })();
21911
+ execFileSync4("git", ["push", "-u", "origin", localBranch], { cwd, stdio: ["pipe", "pipe", "pipe"] });
21912
+ repoSpinner.succeed(`Repository created: ${source_default.dim(repoData.repoFullName)} (private)`);
21913
+ gitInfo = getGitInfo(cwd);
21914
+ } catch (err) {
21915
+ repoSpinner.fail(
21916
+ `Could not auto-create repo: ${err instanceof Error ? err.message : "Unknown error"}`
21917
+ );
21918
+ console.error(
21919
+ source_default.red(
21920
+ "Add a GitHub remote manually (git remote add origin ...) or re-run 'lm init'."
21921
+ )
21922
+ );
21923
+ process.exitCode = 1;
21924
+ return;
21925
+ }
21873
21926
  }
21874
21927
  const commitSha = gitInfo.commitSha || "";
21875
21928
  const shortSha = commitSha ? commitSha.slice(0, 7) : "unknown";
@@ -21894,6 +21947,12 @@ function registerDeploy(program3) {
21894
21947
  if (detection.startCmd) patch.startCmd = detection.startCmd;
21895
21948
  if (detection.port) patch.port = detection.port;
21896
21949
  if (detection.framework) patch.framework = detection.framework;
21950
+ if (opts.dockerfile !== void 0) {
21951
+ patch.dockerfilePath = opts.dockerfile === "" ? null : opts.dockerfile;
21952
+ }
21953
+ if (opts.rootDir) patch.rootDir = opts.rootDir;
21954
+ if (opts.buildCmd) patch.buildCmd = opts.buildCmd;
21955
+ if (opts.startCmd) patch.startCmd = opts.startCmd;
21897
21956
  if (Object.keys(patch).length > 0) {
21898
21957
  try {
21899
21958
  await api(`/api/services/${ctx.serviceId}`, {
@@ -21917,7 +21976,7 @@ function registerDeploy(program3) {
21917
21976
  const apiUrl = getApiUrl();
21918
21977
  const wsUrl = apiUrl.replace(/^http/, "ws");
21919
21978
  const wsToken = await getWsToken();
21920
- await new Promise((resolve4, reject) => {
21979
+ await new Promise((resolve5, reject) => {
21921
21980
  let statusResolved = false;
21922
21981
  const logWs = new wrapper_default(
21923
21982
  `${wsUrl}/ws/logs/${deployment.id}?token=${wsToken}`
@@ -21958,7 +22017,7 @@ function registerDeploy(program3) {
21958
22017
  spinner.succeed(
21959
22018
  source_default.green(`Deployed \u2192 ${source_default.bold(`https://${url}`)}`)
21960
22019
  );
21961
- resolve4();
22020
+ resolve5();
21962
22021
  } else if (parsed.status === "FAILED" && !statusResolved) {
21963
22022
  statusResolved = true;
21964
22023
  logWs.close();
@@ -21983,7 +22042,7 @@ function registerDeploy(program3) {
21983
22042
  if (!statusResolved) {
21984
22043
  statusResolved = true;
21985
22044
  logWs.close();
21986
- pollDeploymentStatus(deployment.id, spinner).then(resolve4).catch(reject);
22045
+ pollDeploymentStatus(deployment.id, spinner).then(resolve5).catch(reject);
21987
22046
  }
21988
22047
  });
21989
22048
  });
@@ -22306,10 +22365,10 @@ function prompt2(question) {
22306
22365
  input: process.stdin,
22307
22366
  output: process.stdout
22308
22367
  });
22309
- return new Promise((resolve4) => {
22368
+ return new Promise((resolve5) => {
22310
22369
  rl.question(question, (answer) => {
22311
22370
  rl.close();
22312
- resolve4(answer.trim());
22371
+ resolve5(answer.trim());
22313
22372
  });
22314
22373
  });
22315
22374
  }
@@ -22324,10 +22383,10 @@ function prompt3(question) {
22324
22383
  input: process.stdin,
22325
22384
  output: process.stdout
22326
22385
  });
22327
- return new Promise((resolve4) => {
22386
+ return new Promise((resolve5) => {
22328
22387
  rl.question(question, (answer) => {
22329
22388
  rl.close();
22330
- resolve4(answer.trim());
22389
+ resolve5(answer.trim());
22331
22390
  });
22332
22391
  });
22333
22392
  }
@@ -22573,10 +22632,10 @@ function toSlug2(name) {
22573
22632
  }
22574
22633
  function askInput(question) {
22575
22634
  const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
22576
- return new Promise((resolve4) => {
22635
+ return new Promise((resolve5) => {
22577
22636
  rl.question(question, (answer) => {
22578
22637
  rl.close();
22579
- resolve4(answer.trim());
22638
+ resolve5(answer.trim());
22580
22639
  });
22581
22640
  });
22582
22641
  }
@@ -22864,7 +22923,7 @@ Detected: ${runtimeLabel} on port ${detection.port}`));
22864
22923
  const apiUrl = getApiUrl();
22865
22924
  const wsUrl = apiUrl.replace(/^http/, "ws");
22866
22925
  const wsToken = await getWsToken();
22867
- await new Promise((resolve4, reject) => {
22926
+ await new Promise((resolve5, reject) => {
22868
22927
  let done = false;
22869
22928
  const logWs = new wrapper_default(
22870
22929
  `${wsUrl}/ws/logs/${deployment.id}?token=${wsToken}`
@@ -22901,7 +22960,7 @@ Detected: ${runtimeLabel} on port ${detection.port}`));
22901
22960
  deploySpinner.succeed(
22902
22961
  source_default.green(`Live \u2192 ${source_default.bold(`https://${url}`)}`)
22903
22962
  );
22904
- resolve4();
22963
+ resolve5();
22905
22964
  } else if (parsed.status === "FAILED" && !done) {
22906
22965
  done = true;
22907
22966
  logWs.close();
@@ -22924,7 +22983,7 @@ Detected: ${runtimeLabel} on port ${detection.port}`));
22924
22983
  if (!done) {
22925
22984
  done = true;
22926
22985
  logWs.close();
22927
- pollStatus(deployment.id, deploySpinner).then(resolve4).catch(reject);
22986
+ pollStatus(deployment.id, deploySpinner).then(resolve5).catch(reject);
22928
22987
  }
22929
22988
  });
22930
22989
  });
@@ -22986,12 +23045,12 @@ async function ensureChromium() {
22986
23045
  process.exit(1);
22987
23046
  }
22988
23047
  }
22989
- const path6 = findChromium();
22990
- if (!path6) {
23048
+ const path7 = findChromium();
23049
+ if (!path7) {
22991
23050
  console.error(source_default.red("Chromium installed but not found on disk."));
22992
23051
  process.exit(1);
22993
23052
  }
22994
- return path6;
23053
+ return path7;
22995
23054
  }
22996
23055
  async function launchBrowser(headless = true) {
22997
23056
  const pw = await import("playwright-core");
@@ -23319,8 +23378,8 @@ function getRepo() {
23319
23378
  }
23320
23379
  return { owner: info.repoOwner, name: info.repoName, branch: info.repoBranch };
23321
23380
  }
23322
- async function ghApi(path6, options) {
23323
- return api(`/api/github${path6}`, options);
23381
+ async function ghApi(path7, options) {
23382
+ return api(`/api/github${path7}`, options);
23324
23383
  }
23325
23384
  function timeAgo(date) {
23326
23385
  const diff = Date.now() - new Date(date).getTime();
@@ -23410,9 +23469,9 @@ function registerRepo(program3) {
23410
23469
  }
23411
23470
  const spinner = ora("Fetching issues...").start();
23412
23471
  try {
23413
- let path6 = `/repos/${owner}/${name}/issues?state=${opts.state}&limit=${opts.limit}`;
23414
- if (opts.labels) path6 += `&labels=${encodeURIComponent(opts.labels)}`;
23415
- const { data } = await ghApi(path6);
23472
+ let path7 = `/repos/${owner}/${name}/issues?state=${opts.state}&limit=${opts.limit}`;
23473
+ if (opts.labels) path7 += `&labels=${encodeURIComponent(opts.labels)}`;
23474
+ const { data } = await ghApi(path7);
23416
23475
  spinner.stop();
23417
23476
  const issues = (data || []).filter((i) => !i.pull_request);
23418
23477
  if (issues.length === 0) {
@@ -23580,11 +23639,11 @@ function registerRepo(program3) {
23580
23639
  process.exitCode = 1;
23581
23640
  }
23582
23641
  });
23583
- repo.command("browse").alias("web").description("Open repository in browser").argument("[path]", "File or path to open").option("-b, --branch <branch>", "Branch name").action(async (path6, opts) => {
23642
+ repo.command("browse").alias("web").description("Open repository in browser").argument("[path]", "File or path to open").option("-b, --branch <branch>", "Branch name").action(async (path7, opts) => {
23584
23643
  const { owner, name, branch: currentBranch } = getRepo();
23585
23644
  const branch = opts.branch || currentBranch;
23586
23645
  let url = `https://github.com/${owner}/${name}`;
23587
- if (path6) url += `/blob/${branch}/${path6}`;
23646
+ if (path7) url += `/blob/${branch}/${path7}`;
23588
23647
  else if (branch !== "main" && branch !== "master") url += `/tree/${branch}`;
23589
23648
  openUrl(url);
23590
23649
  console.log(source_default.dim(`Opened ${url}`));
@@ -23648,10 +23707,10 @@ async function getTeamId() {
23648
23707
  }
23649
23708
  function prompt4(question) {
23650
23709
  const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
23651
- return new Promise((resolve4) => {
23710
+ return new Promise((resolve5) => {
23652
23711
  rl.question(question, (answer) => {
23653
23712
  rl.close();
23654
- resolve4(answer.trim());
23713
+ resolve5(answer.trim());
23655
23714
  });
23656
23715
  });
23657
23716
  }
@@ -23747,13 +23806,13 @@ function registerDb(program3) {
23747
23806
  const url = data.connectionUrl;
23748
23807
  if (opts.copy) {
23749
23808
  try {
23750
- const { execFileSync: execFileSync8 } = await import("child_process");
23809
+ const { execFileSync: execFileSync10 } = await import("child_process");
23751
23810
  if (process.platform === "win32") {
23752
- execFileSync8("cmd", ["/c", `echo|set /p="${url}"| clip`], { stdio: "pipe" });
23811
+ execFileSync10("cmd", ["/c", `echo|set /p="${url}"| clip`], { stdio: "pipe" });
23753
23812
  } else if (process.platform === "darwin") {
23754
- execFileSync8("pbcopy", [], { input: url, stdio: ["pipe", "pipe", "pipe"] });
23813
+ execFileSync10("pbcopy", [], { input: url, stdio: ["pipe", "pipe", "pipe"] });
23755
23814
  } else {
23756
- execFileSync8("xclip", ["-selection", "clipboard"], { input: url, stdio: ["pipe", "pipe", "pipe"] });
23815
+ execFileSync10("xclip", ["-selection", "clipboard"], { input: url, stdio: ["pipe", "pipe", "pipe"] });
23757
23816
  }
23758
23817
  console.log(source_default.green("\u2713 Connection URL copied to clipboard"));
23759
23818
  } catch {
@@ -23916,7 +23975,7 @@ function registerDb(program3) {
23916
23975
  process.exitCode = 1;
23917
23976
  }
23918
23977
  });
23919
- db.command("create <name>").description("Create a new database").option("-e, --engine <engine>", "Engine: postgresql, redis, mongodb", "postgresql").option("-v, --version <version>", "Engine version").option("-s, --storage <mb>", "Storage size in MB", "1024").option("--service <serviceId>", "Link to a service").option("--project <projectId>", "Project to attach to").action(async (name, opts) => {
23978
+ db.command("create <name>").description("Create a new database").option("-e, --engine <engine>", "Engine: postgresql, postgis, redis, mongodb", "postgresql").option("-v, --version <version>", "Engine version").option("-s, --storage <mb>", "Storage size in MB", "1024").option("--service <serviceId>", "Link to a service").option("--project <projectId>", "Project to attach to").action(async (name, opts) => {
23920
23979
  requireLogin2();
23921
23980
  const spinner = ora(`Creating ${opts.engine} database "${name}"...`).start();
23922
23981
  try {
@@ -23991,8 +24050,8 @@ function registerDb(program3) {
23991
24050
  body: JSON.stringify({ command: ["pg_dump", "-U", "postgres", "--no-owner", "--no-acl"] })
23992
24051
  });
23993
24052
  if (opts.output) {
23994
- const { writeFileSync: writeFileSync3 } = await import("fs");
23995
- writeFileSync3(opts.output, data.output);
24053
+ const { writeFileSync: writeFileSync4 } = await import("fs");
24054
+ writeFileSync4(opts.output, data.output);
23996
24055
  spinner?.succeed(`Dump saved \u2192 ${source_default.cyan(opts.output)}`);
23997
24056
  } else {
23998
24057
  process.stdout.write(data.output);
@@ -24020,8 +24079,8 @@ function registerDb(program3) {
24020
24079
  requireLogin2();
24021
24080
  const spinner = ora(`Running ${file}...`).start();
24022
24081
  try {
24023
- const { readFileSync: readFileSync6 } = await import("fs");
24024
- const sql = readFileSync6(file, "utf-8");
24082
+ const { readFileSync: readFileSync8 } = await import("fs");
24083
+ const sql = readFileSync8(file, "utf-8");
24025
24084
  const { data } = await api(`/api/databases/${dbId}/query`, {
24026
24085
  method: "POST",
24027
24086
  body: JSON.stringify({ sql })
@@ -25345,10 +25404,10 @@ function printToolTrace(calls) {
25345
25404
  }
25346
25405
  function prompt5(question) {
25347
25406
  const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
25348
- return new Promise((resolve4) => {
25407
+ return new Promise((resolve5) => {
25349
25408
  rl.question(question, (answer) => {
25350
25409
  rl.close();
25351
- resolve4(answer);
25410
+ resolve5(answer);
25352
25411
  });
25353
25412
  });
25354
25413
  }
@@ -25465,9 +25524,9 @@ function registerAgent(program3) {
25465
25524
  };
25466
25525
  let done = false;
25467
25526
  while (!done) {
25468
- const line = await new Promise((resolve4) => {
25469
- rl.question(source_default.cyan("> "), (answer) => resolve4(answer));
25470
- rl.once("close", () => resolve4(null));
25527
+ const line = await new Promise((resolve5) => {
25528
+ rl.question(source_default.cyan("> "), (answer) => resolve5(answer));
25529
+ rl.once("close", () => resolve5(null));
25471
25530
  });
25472
25531
  if (line === null) {
25473
25532
  done = true;
@@ -25481,9 +25540,890 @@ function registerAgent(program3) {
25481
25540
  });
25482
25541
  }
25483
25542
 
25543
+ // src/commands/workflows.ts
25544
+ async function loadStirrupServices() {
25545
+ const teams = await api("/api/teams");
25546
+ const teamIds = teams.success ? teams.data.map((t) => t.id) : [];
25547
+ const allServices = [];
25548
+ for (const teamId of teamIds) {
25549
+ const projects = await api(
25550
+ `/api/projects?teamId=${teamId}`
25551
+ );
25552
+ if (!projects.success) continue;
25553
+ for (const project of projects.data) {
25554
+ const services = await api(
25555
+ `/api/services?projectId=${project.id}`
25556
+ );
25557
+ if (!services.success) continue;
25558
+ for (const svc of services.data) {
25559
+ if (svc.framework === "Stirrup") {
25560
+ allServices.push({ ...svc, projectName: project.name });
25561
+ }
25562
+ }
25563
+ }
25564
+ }
25565
+ return allServices;
25566
+ }
25567
+ async function listAllWorkflows() {
25568
+ const spin = ora("Discovering Stirrup services...").start();
25569
+ let services;
25570
+ try {
25571
+ services = await loadStirrupServices();
25572
+ } catch (err) {
25573
+ spin.fail(`Could not list services: ${err instanceof Error ? err.message : String(err)}`);
25574
+ process.exit(1);
25575
+ }
25576
+ spin.succeed(`Found ${services.length} Stirrup service${services.length === 1 ? "" : "s"}`);
25577
+ if (services.length === 0) {
25578
+ console.log(source_default.dim("\nNo Stirrup services deployed. Push a stirrup-ai project and try again."));
25579
+ return;
25580
+ }
25581
+ for (const svc of services) {
25582
+ console.log();
25583
+ console.log(source_default.bold(`${svc.projectName} / ${svc.slug}`) + source_default.dim(` (${svc.id})`));
25584
+ try {
25585
+ const res = await api(`/api/services/${svc.id}/workflows`);
25586
+ if (!res.success || !res.data) {
25587
+ console.log(source_default.red(" could not load workflows"));
25588
+ continue;
25589
+ }
25590
+ if (res.data.requiresAuth) {
25591
+ console.log(source_default.yellow(" needs STIRRUP_API_TOKEN env var"));
25592
+ if (res.data.note) console.log(source_default.dim(" " + res.data.note));
25593
+ continue;
25594
+ }
25595
+ if (res.data.workflows.length === 0) {
25596
+ console.log(source_default.dim(" no workflows defined"));
25597
+ continue;
25598
+ }
25599
+ for (const wf of res.data.workflows) {
25600
+ console.log(
25601
+ ` ${source_default.cyan(wf.id)} ${source_default.dim(`(${wf.nodeCount} nodes${wf.triggers.length ? `, triggers: ${wf.triggers.join(",")}` : ""})`)}`
25602
+ );
25603
+ if (wf.description) console.log(source_default.dim(` ${wf.description}`));
25604
+ }
25605
+ } catch (err) {
25606
+ const msg = err instanceof Error ? err.message : String(err);
25607
+ console.log(source_default.red(` unreachable: ${msg}`));
25608
+ }
25609
+ }
25610
+ console.log();
25611
+ console.log(source_default.dim("Run a workflow:") + " " + source_default.bold("lm workflows run <serviceId>/<workflowId> --input '{...}'"));
25612
+ }
25613
+ async function runWorkflow(target, opts) {
25614
+ const m = target.match(/^([^/]+)\/(.+)$/);
25615
+ if (!m) {
25616
+ console.error(source_default.red("Target must be <serviceId>/<workflowId>"));
25617
+ process.exit(1);
25618
+ }
25619
+ const [, serviceId, workflowId] = m;
25620
+ let input = {};
25621
+ if (opts.input) {
25622
+ try {
25623
+ input = JSON.parse(opts.input);
25624
+ } catch (err) {
25625
+ console.error(source_default.red(`Input must be valid JSON: ${err instanceof Error ? err.message : String(err)}`));
25626
+ process.exit(1);
25627
+ }
25628
+ }
25629
+ const spin = ora(`Triggering ${source_default.cyan(workflowId)}...`).start();
25630
+ try {
25631
+ const res = await api(
25632
+ `/api/services/${serviceId}/workflows/${encodeURIComponent(workflowId)}/run`,
25633
+ { method: "POST", body: JSON.stringify(input) }
25634
+ );
25635
+ if (!res.success) {
25636
+ spin.fail(`Trigger failed: ${typeof res.error === "string" ? res.error : JSON.stringify(res.error)}`);
25637
+ process.exit(1);
25638
+ }
25639
+ spin.succeed("Triggered");
25640
+ console.log(JSON.stringify(res.data, null, 2));
25641
+ } catch (err) {
25642
+ spin.fail(`Trigger failed: ${err instanceof Error ? err.message : String(err)}`);
25643
+ process.exit(1);
25644
+ }
25645
+ }
25646
+ async function showRuns(serviceId) {
25647
+ const spin = ora("Loading recent runs...").start();
25648
+ try {
25649
+ const res = await api(
25650
+ `/api/services/${serviceId}/workflows/runs`
25651
+ );
25652
+ spin.stop();
25653
+ if (!res.success || !res.data) {
25654
+ console.log(source_default.red("Could not load runs."));
25655
+ return;
25656
+ }
25657
+ if (res.data.runs.length === 0) {
25658
+ console.log(source_default.dim("No runs yet."));
25659
+ return;
25660
+ }
25661
+ for (const r of res.data.runs) {
25662
+ const dur = r.durationMs != null ? `${(r.durationMs / 1e3).toFixed(1)}s` : "\u2014";
25663
+ console.log(
25664
+ `${source_default.cyan(r.workflowId)} ${source_default.dim(r.id)} ${source_default.bold(r.status)} ${source_default.dim(dur)} ${source_default.dim(r.startedAt ?? "")}`
25665
+ );
25666
+ }
25667
+ } catch (err) {
25668
+ spin.fail(`Could not load runs: ${err instanceof Error ? err.message : String(err)}`);
25669
+ }
25670
+ }
25671
+ function registerWorkflows(program3) {
25672
+ const cmd = program3.command("workflows").alias("wf").description("List and trigger Stirrup workflows on your deployed services");
25673
+ cmd.command("list").alias("ls").description("List Stirrup workflows across all your services").action(listAllWorkflows);
25674
+ cmd.command("run <target>").description("Trigger a workflow. Target format: <serviceId>/<workflowId>").option("-i, --input <json>", "JSON input payload to send to the workflow").action(runWorkflow);
25675
+ cmd.command("runs <serviceId>").description("Show recent runs for the workflows on a service").action(showRuns);
25676
+ }
25677
+
25678
+ // src/commands/image.ts
25679
+ import { writeFile, mkdir } from "fs/promises";
25680
+ import path6 from "path";
25681
+ function extFromMime(mime) {
25682
+ if (mime.includes("png")) return "png";
25683
+ if (mime.includes("jpeg") || mime.includes("jpg")) return "jpg";
25684
+ if (mime.includes("webp")) return "webp";
25685
+ if (mime.includes("gif")) return "gif";
25686
+ return "bin";
25687
+ }
25688
+ function slugifyForFilename(s) {
25689
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "image";
25690
+ }
25691
+ async function generateCmd(promptWords, opts) {
25692
+ if (!isLoggedIn()) {
25693
+ console.error(source_default.red('Not logged in. Run "lm login" first.'));
25694
+ process.exitCode = 1;
25695
+ return;
25696
+ }
25697
+ const userPrompt = promptWords.join(" ").trim();
25698
+ if (!userPrompt) {
25699
+ console.error(source_default.red('Prompt is required. Usage: lm image gen "a watercolor mountain"'));
25700
+ process.exitCode = 1;
25701
+ return;
25702
+ }
25703
+ const count = Math.min(Math.max(parseInt(opts.count ?? "1") || 1, 1), 4);
25704
+ const validRatios = ["1:1", "16:9", "9:16", "4:3", "3:4"];
25705
+ const aspectRatio = opts.aspectRatio && validRatios.includes(opts.aspectRatio) ? opts.aspectRatio : "1:1";
25706
+ const outDir = path6.resolve(opts.outDir ?? process.cwd());
25707
+ const spinner = ora(`Generating ${count} image${count === 1 ? "" : "s"} (${aspectRatio})...`).start();
25708
+ try {
25709
+ const res = await api("/api/nanobanana2/generate", {
25710
+ method: "POST",
25711
+ body: JSON.stringify({ prompt: userPrompt, count, aspectRatio })
25712
+ });
25713
+ spinner.text = "Writing files...";
25714
+ await mkdir(outDir, { recursive: true });
25715
+ const baseName = opts.name ? slugifyForFilename(opts.name) : slugifyForFilename(userPrompt);
25716
+ const written = [];
25717
+ for (let i = 0; i < res.data.images.length; i++) {
25718
+ const img = res.data.images[i];
25719
+ const ext = extFromMime(img.mimeType);
25720
+ const suffix = res.data.images.length === 1 ? "" : `-${i + 1}`;
25721
+ const fileName = `${baseName}${suffix}.${ext}`;
25722
+ const filePath = path6.join(outDir, fileName);
25723
+ await writeFile(filePath, Buffer.from(img.data, "base64"));
25724
+ written.push(filePath);
25725
+ }
25726
+ spinner.succeed(
25727
+ source_default.green(
25728
+ `Wrote ${written.length} image${written.length === 1 ? "" : "s"} ${source_default.dim(`(${res.data.usage.totalTokens} tokens)`)}`
25729
+ )
25730
+ );
25731
+ for (const p of written) console.log(" " + source_default.dim(p));
25732
+ if (res.data.text?.trim()) {
25733
+ console.log();
25734
+ console.log(source_default.dim(res.data.text.trim()));
25735
+ }
25736
+ } catch (err) {
25737
+ spinner.fail(source_default.red(`Generate failed: ${err instanceof Error ? err.message : String(err)}`));
25738
+ process.exitCode = 1;
25739
+ }
25740
+ }
25741
+ async function usageCmd() {
25742
+ if (!isLoggedIn()) {
25743
+ console.error(source_default.red('Not logged in. Run "lm login" first.'));
25744
+ process.exitCode = 1;
25745
+ return;
25746
+ }
25747
+ const spin = ora("Fetching usage...").start();
25748
+ try {
25749
+ const res = await api("/api/nanobanana2/usage");
25750
+ spin.stop();
25751
+ const d = res.data;
25752
+ console.log(source_default.bold(`Plan: ${d.planTier}`));
25753
+ console.log(
25754
+ `Tokens: ${source_default.cyan(d.tokens.used.toLocaleString())} / ${d.tokens.limit.toLocaleString()} ` + source_default.dim(`(${d.tokens.remaining.toLocaleString()} remaining)`)
25755
+ );
25756
+ console.log(
25757
+ `Images: ${source_default.cyan(d.images.used.toString())} / ${d.images.limit.toString()} ` + source_default.dim(`(${d.images.remaining} remaining)`)
25758
+ );
25759
+ console.log(source_default.dim(`Resets: ${new Date(d.resetAt).toLocaleString()}`));
25760
+ } catch (err) {
25761
+ spin.fail(source_default.red(`Usage failed: ${err instanceof Error ? err.message : String(err)}`));
25762
+ process.exitCode = 1;
25763
+ }
25764
+ }
25765
+ async function historyCmd(opts) {
25766
+ if (!isLoggedIn()) {
25767
+ console.error(source_default.red('Not logged in. Run "lm login" first.'));
25768
+ process.exitCode = 1;
25769
+ return;
25770
+ }
25771
+ const limit = Math.min(Math.max(parseInt(opts.limit ?? "20") || 20, 1), 50);
25772
+ const spin = ora("Loading history...").start();
25773
+ try {
25774
+ const res = await api(`/api/nanobanana2/history?limit=${limit}`);
25775
+ spin.stop();
25776
+ if (res.data.items.length === 0) {
25777
+ console.log(source_default.dim("No generations yet."));
25778
+ return;
25779
+ }
25780
+ for (const item of res.data.items) {
25781
+ const when = new Date(item.createdAt).toLocaleString();
25782
+ const truncated = item.prompt.length > 80 ? item.prompt.slice(0, 77) + "..." : item.prompt;
25783
+ console.log(
25784
+ `${source_default.dim(item.id)} ${source_default.cyan(item.status.padEnd(10))} ${source_default.bold(`${item.imageCount} img`)} ${source_default.dim(when)}`
25785
+ );
25786
+ console.log(" " + truncated);
25787
+ }
25788
+ } catch (err) {
25789
+ spin.fail(source_default.red(`History failed: ${err instanceof Error ? err.message : String(err)}`));
25790
+ process.exitCode = 1;
25791
+ }
25792
+ }
25793
+ function registerImage(program3) {
25794
+ const cmd = program3.command("image").alias("img").description("Generate images via Launchmatic (NanoBanana2 / Gemini 2.5 Flash Image)");
25795
+ cmd.command("generate <prompt...>").alias("gen").description('Generate images from a prompt. Usage: lm image gen "watercolor mountain"').option("-c, --count <n>", "How many images (1\u20134)", "1").option("-a, --aspect-ratio <ratio>", "1:1, 16:9, 9:16, 4:3, or 3:4", "1:1").option("-o, --out-dir <dir>", "Where to write files (default: cwd)").option("-n, --name <name>", "Base filename (default: derived from prompt)").action(generateCmd);
25796
+ cmd.command("usage").description("Show current image/token quota").action(usageCmd);
25797
+ cmd.command("history").description("List recent image generations (metadata only)").option("-l, --limit <n>", "Max entries to show (1\u201350)", "20").action(historyCmd);
25798
+ }
25799
+
25800
+ // src/commands/monorepo.ts
25801
+ import { execFileSync as execFileSync9 } from "child_process";
25802
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
25803
+ import { join as join5 } from "path";
25804
+ import readline7 from "readline";
25805
+
25806
+ // src/monorepo.ts
25807
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
25808
+ import { execFileSync as execFileSync8 } from "child_process";
25809
+ import { join as join4, relative as relative2, sep, posix } from "path";
25810
+ var MANIFEST_FILE = "launchmatic.json";
25811
+ function findRepoRoot(start = process.cwd()) {
25812
+ try {
25813
+ return execFileSync8("git", ["rev-parse", "--show-toplevel"], {
25814
+ cwd: start,
25815
+ encoding: "utf-8",
25816
+ stdio: ["pipe", "pipe", "pipe"]
25817
+ }).trim();
25818
+ } catch {
25819
+ return start;
25820
+ }
25821
+ }
25822
+ function manifestPath(repoRoot = findRepoRoot()) {
25823
+ return join4(repoRoot, MANIFEST_FILE);
25824
+ }
25825
+ function readManifest(repoRoot = findRepoRoot()) {
25826
+ const p = manifestPath(repoRoot);
25827
+ if (!existsSync6(p)) return null;
25828
+ try {
25829
+ const parsed = JSON.parse(readFileSync6(p, "utf-8"));
25830
+ if (parsed.version !== 1 || !Array.isArray(parsed.services)) {
25831
+ throw new Error(`${MANIFEST_FILE} has unexpected shape`);
25832
+ }
25833
+ return parsed;
25834
+ } catch (err) {
25835
+ throw new Error(`Could not read ${MANIFEST_FILE}: ${err instanceof Error ? err.message : String(err)}`);
25836
+ }
25837
+ }
25838
+ function writeManifest(manifest, repoRoot = findRepoRoot()) {
25839
+ writeFileSync3(manifestPath(repoRoot), JSON.stringify(manifest, null, 2) + "\n");
25840
+ }
25841
+ function discoverServices(repoRoot = findRepoRoot()) {
25842
+ const globs = readWorkspaceGlobs(repoRoot);
25843
+ const dirs = /* @__PURE__ */ new Set();
25844
+ for (const glob of globs) {
25845
+ for (const dir of expandGlob(repoRoot, glob)) {
25846
+ dirs.add(dir);
25847
+ }
25848
+ }
25849
+ if (globs.length === 0) {
25850
+ for (const conv of ["apps", "services"]) {
25851
+ const base = join4(repoRoot, conv);
25852
+ if (existsSync6(base) && statSync2(base).isDirectory()) {
25853
+ for (const entry of readdirSync3(base)) {
25854
+ const full = join4(base, entry);
25855
+ if (statSync2(full).isDirectory()) dirs.add(full);
25856
+ }
25857
+ }
25858
+ }
25859
+ }
25860
+ const out = [];
25861
+ for (const absDir of dirs) {
25862
+ if (!isLikelyDeployable(absDir)) continue;
25863
+ const detection = detectLocal(absDir);
25864
+ out.push({
25865
+ name: deriveName(repoRoot, absDir),
25866
+ rootDir: toPosix(relative2(repoRoot, absDir)),
25867
+ framework: detection.framework,
25868
+ buildCmd: detection.buildCmd,
25869
+ startCmd: detection.startCmd,
25870
+ port: detection.port
25871
+ });
25872
+ }
25873
+ out.sort((a, b) => a.rootDir.localeCompare(b.rootDir));
25874
+ return out;
25875
+ }
25876
+ function readWorkspaceGlobs(repoRoot) {
25877
+ const globs = [];
25878
+ const pnpmFile = join4(repoRoot, "pnpm-workspace.yaml");
25879
+ if (existsSync6(pnpmFile)) {
25880
+ const text = readFileSync6(pnpmFile, "utf-8");
25881
+ let inPackages = false;
25882
+ for (const rawLine of text.split(/\r?\n/)) {
25883
+ const line = rawLine.replace(/#.*$/, "").trimEnd();
25884
+ if (/^packages\s*:/i.test(line)) {
25885
+ inPackages = true;
25886
+ continue;
25887
+ }
25888
+ if (inPackages) {
25889
+ const m = line.match(/^\s*-\s*['"]?([^'"#]+?)['"]?\s*$/);
25890
+ if (m) {
25891
+ globs.push(m[1].trim());
25892
+ continue;
25893
+ }
25894
+ if (line.trim() && !line.startsWith(" ") && !line.startsWith(" ")) {
25895
+ inPackages = false;
25896
+ }
25897
+ }
25898
+ }
25899
+ }
25900
+ const pkgFile = join4(repoRoot, "package.json");
25901
+ if (existsSync6(pkgFile)) {
25902
+ try {
25903
+ const pkg = JSON.parse(readFileSync6(pkgFile, "utf-8"));
25904
+ if (Array.isArray(pkg.workspaces)) {
25905
+ globs.push(...pkg.workspaces.filter((g) => typeof g === "string"));
25906
+ } else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) {
25907
+ globs.push(...pkg.workspaces.packages.filter((g) => typeof g === "string"));
25908
+ }
25909
+ } catch {
25910
+ }
25911
+ }
25912
+ return globs;
25913
+ }
25914
+ function expandGlob(repoRoot, glob) {
25915
+ const cleaned = glob.replace(/^\.\//, "").replace(/\/$/, "");
25916
+ if (cleaned.endsWith("/*")) {
25917
+ const base = join4(repoRoot, cleaned.slice(0, -2));
25918
+ if (!existsSync6(base) || !statSync2(base).isDirectory()) return [];
25919
+ return readdirSync3(base).map((entry) => join4(base, entry)).filter((p) => {
25920
+ try {
25921
+ return statSync2(p).isDirectory();
25922
+ } catch {
25923
+ return false;
25924
+ }
25925
+ });
25926
+ }
25927
+ const abs = join4(repoRoot, cleaned);
25928
+ if (existsSync6(abs) && statSync2(abs).isDirectory()) return [abs];
25929
+ return [];
25930
+ }
25931
+ function isLikelyDeployable(absDir) {
25932
+ if (existsSync6(join4(absDir, "Dockerfile"))) return true;
25933
+ const pkgPath = join4(absDir, "package.json");
25934
+ if (existsSync6(pkgPath)) {
25935
+ try {
25936
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
25937
+ if (pkg.scripts?.start || pkg.scripts?.dev || pkg.scripts?.serve) return true;
25938
+ if (pkg.bin) return true;
25939
+ } catch {
25940
+ }
25941
+ }
25942
+ if (existsSync6(join4(absDir, "next.config.js")) || existsSync6(join4(absDir, "next.config.mjs")) || existsSync6(join4(absDir, "next.config.ts")) || existsSync6(join4(absDir, "go.mod")) || existsSync6(join4(absDir, "Cargo.toml")) || existsSync6(join4(absDir, "manage.py")) || existsSync6(join4(absDir, "pyproject.toml")) || existsSync6(join4(absDir, "Gemfile"))) {
25943
+ return true;
25944
+ }
25945
+ return false;
25946
+ }
25947
+ function deriveName(repoRoot, absDir) {
25948
+ const rel = toPosix(relative2(repoRoot, absDir));
25949
+ const parts = rel.split("/");
25950
+ return parts[parts.length - 1].toLowerCase().replace(/[^a-z0-9-]/g, "-");
25951
+ }
25952
+ function toPosix(p) {
25953
+ return p.split(sep).join(posix.sep);
25954
+ }
25955
+ function changedFilesSince(repoRoot, baseRef) {
25956
+ const ref = baseRef ?? autoBaseRef(repoRoot);
25957
+ if (!ref) return [];
25958
+ try {
25959
+ const out = execFileSync8("git", ["diff", "--name-only", `${ref}..HEAD`], {
25960
+ cwd: repoRoot,
25961
+ encoding: "utf-8",
25962
+ stdio: ["pipe", "pipe", "pipe"]
25963
+ });
25964
+ return out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
25965
+ } catch {
25966
+ return [];
25967
+ }
25968
+ }
25969
+ function autoBaseRef(repoRoot) {
25970
+ try {
25971
+ const branch = execFileSync8("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
25972
+ cwd: repoRoot,
25973
+ encoding: "utf-8",
25974
+ stdio: ["pipe", "pipe", "pipe"]
25975
+ }).trim();
25976
+ if (branch && branch !== "HEAD") {
25977
+ try {
25978
+ execFileSync8("git", ["rev-parse", "--verify", `origin/${branch}`], {
25979
+ cwd: repoRoot,
25980
+ stdio: ["pipe", "pipe", "pipe"]
25981
+ });
25982
+ return `origin/${branch}`;
25983
+ } catch {
25984
+ }
25985
+ }
25986
+ } catch {
25987
+ }
25988
+ try {
25989
+ execFileSync8("git", ["rev-parse", "--verify", "HEAD~1"], {
25990
+ cwd: repoRoot,
25991
+ stdio: ["pipe", "pipe", "pipe"]
25992
+ });
25993
+ return "HEAD~1";
25994
+ } catch {
25995
+ return null;
25996
+ }
25997
+ }
25998
+ function serviceWasChanged(rootDir, changedPaths) {
25999
+ if (changedPaths.length === 0) return true;
26000
+ const normalized = rootDir.replace(/\\/g, "/").replace(/^\.?\//, "").replace(/\/$/, "");
26001
+ if (normalized === "" || normalized === "." || normalized === "/") return true;
26002
+ const prefix = normalized + "/";
26003
+ return changedPaths.some((p) => p === normalized || p.startsWith(prefix));
26004
+ }
26005
+
26006
+ // src/commands/monorepo.ts
26007
+ function requireLogin3() {
26008
+ if (!isLoggedIn()) {
26009
+ console.error(source_default.red('Not logged in. Run "lm login" first.'));
26010
+ process.exitCode = 1;
26011
+ return false;
26012
+ }
26013
+ return true;
26014
+ }
26015
+ function prompt6(question) {
26016
+ const rl = readline7.createInterface({ input: process.stdin, output: process.stdout });
26017
+ return new Promise((resolve5) => {
26018
+ rl.question(question, (a) => {
26019
+ rl.close();
26020
+ resolve5(a.trim());
26021
+ });
26022
+ });
26023
+ }
26024
+ async function initManifest(opts) {
26025
+ if (!requireLogin3()) return;
26026
+ const repoRoot = findRepoRoot();
26027
+ const path7 = manifestPath(repoRoot);
26028
+ if (existsSync7(path7)) {
26029
+ console.error(source_default.yellow(`${MANIFEST_FILE} already exists at ${path7}`));
26030
+ console.error(source_default.dim("Edit it manually or delete and re-run."));
26031
+ process.exitCode = 1;
26032
+ return;
26033
+ }
26034
+ const spin = ora("Discovering services...").start();
26035
+ const discovered = discoverServices(repoRoot);
26036
+ spin.stop();
26037
+ if (discovered.length === 0) {
26038
+ console.error(source_default.red("No services discovered."));
26039
+ console.error(source_default.dim("Looked for: pnpm-workspace.yaml, package.json#workspaces, apps/*, services/*"));
26040
+ console.error(source_default.dim("If your services live elsewhere, create launchmatic.json by hand."));
26041
+ process.exitCode = 1;
26042
+ return;
26043
+ }
26044
+ console.log(source_default.bold(`
26045
+ Discovered ${discovered.length} service${discovered.length === 1 ? "" : "s"}:`));
26046
+ for (const s of discovered) {
26047
+ const fw = s.framework ? source_default.dim(` [${s.framework}]`) : "";
26048
+ console.log(` ${source_default.cyan(s.name)} ${source_default.dim(s.rootDir)}${fw}`);
26049
+ }
26050
+ const { data: teams } = await api("/api/teams");
26051
+ if (!teams || teams.length === 0) {
26052
+ console.error(source_default.red("No teams found on your account."));
26053
+ process.exitCode = 1;
26054
+ return;
26055
+ }
26056
+ let teamId = opts.team;
26057
+ if (!teamId) {
26058
+ if (teams.length === 1 || opts.yes) {
26059
+ teamId = teams[0].id;
26060
+ } else {
26061
+ console.log(source_default.bold("\nTeams:"));
26062
+ teams.forEach((t, i) => console.log(` ${i + 1}. ${t.name} ${source_default.dim(`(${t.slug})`)}`));
26063
+ const ans = await prompt6("Pick a team (number): ");
26064
+ teamId = teams[parseInt(ans) - 1]?.id ?? teams[0].id;
26065
+ }
26066
+ }
26067
+ const projectName = opts.project || opts.name || (existsSync7(join5(repoRoot, "package.json")) ? (() => {
26068
+ try {
26069
+ const pkg = JSON.parse(readFileSync7(join5(repoRoot, "package.json"), "utf-8"));
26070
+ return pkg.name?.replace(/^@.*\//, "") || "monorepo";
26071
+ } catch {
26072
+ return "monorepo";
26073
+ }
26074
+ })() : "monorepo");
26075
+ const projectSlug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "monorepo";
26076
+ const { data: existingProjects } = await api(
26077
+ `/api/projects?teamId=${teamId}`
26078
+ );
26079
+ let projectId = existingProjects.find((p) => p.slug === projectSlug)?.id;
26080
+ if (!projectId) {
26081
+ const create = await api("/api/projects", {
26082
+ method: "POST",
26083
+ body: JSON.stringify({ name: projectName, slug: projectSlug, teamId })
26084
+ });
26085
+ projectId = create.data.id;
26086
+ console.log(source_default.green(`Created project ${source_default.bold(projectName)}`));
26087
+ } else {
26088
+ console.log(source_default.dim(`Using existing project ${projectName}`));
26089
+ }
26090
+ const { data: existingServices } = await api(`/api/services?projectId=${projectId}`);
26091
+ const gitInfo = getGitInfo(repoRoot);
26092
+ const services = [];
26093
+ for (const s of discovered) {
26094
+ const slug = s.name;
26095
+ let svcId = existingServices.find((es) => es.slug === slug || es.rootDir === `/${s.rootDir}` || es.rootDir === s.rootDir)?.id;
26096
+ if (!svcId) {
26097
+ const payload = {
26098
+ name: s.name,
26099
+ slug,
26100
+ type: "WEB",
26101
+ projectId,
26102
+ rootDir: `/${s.rootDir}`,
26103
+ port: s.port
26104
+ };
26105
+ if (s.framework) payload.framework = s.framework;
26106
+ if (s.buildCmd) payload.buildCmd = s.buildCmd;
26107
+ if (s.startCmd) payload.startCmd = s.startCmd;
26108
+ if (gitInfo.repoOwner && gitInfo.repoName) {
26109
+ payload.repoOwner = gitInfo.repoOwner;
26110
+ payload.repoName = gitInfo.repoName;
26111
+ payload.repoBranch = gitInfo.repoBranch;
26112
+ payload.repoUrl = `https://github.com/${gitInfo.repoOwner}/${gitInfo.repoName}.git`;
26113
+ }
26114
+ try {
26115
+ const created = await api("/api/services", {
26116
+ method: "POST",
26117
+ body: JSON.stringify(payload)
26118
+ });
26119
+ svcId = created.data.id;
26120
+ console.log(source_default.green(`Created service ${source_default.bold(s.name)}`));
26121
+ } catch (err) {
26122
+ console.warn(source_default.yellow(`Could not create service ${s.name}: ${err instanceof Error ? err.message : String(err)}`));
26123
+ continue;
26124
+ }
26125
+ } else {
26126
+ console.log(source_default.dim(`Using existing service ${s.name}`));
26127
+ }
26128
+ services.push({
26129
+ name: s.name,
26130
+ rootDir: s.rootDir,
26131
+ serviceId: svcId,
26132
+ framework: s.framework,
26133
+ buildCmd: s.buildCmd,
26134
+ startCmd: s.startCmd,
26135
+ port: s.port
26136
+ });
26137
+ }
26138
+ const manifest = {
26139
+ version: 1,
26140
+ teamId,
26141
+ projectId,
26142
+ services
26143
+ };
26144
+ writeManifest(manifest, repoRoot);
26145
+ console.log();
26146
+ console.log(source_default.green(`\u2713 Wrote ${source_default.bold(MANIFEST_FILE)} (${services.length} services)`));
26147
+ console.log(source_default.dim("Next: ") + source_default.bold("lm up") + source_default.dim(" to deploy everything."));
26148
+ }
26149
+ async function listManifest() {
26150
+ const manifest = readManifest();
26151
+ if (!manifest) {
26152
+ console.error(source_default.red(`No ${MANIFEST_FILE} in this repo. Run ${source_default.bold("lm monorepo init")}.`));
26153
+ process.exitCode = 1;
26154
+ return;
26155
+ }
26156
+ console.log(source_default.bold(`${manifest.services.length} service${manifest.services.length === 1 ? "" : "s"}:`));
26157
+ for (const s of manifest.services) {
26158
+ const fw = s.framework ? source_default.dim(` [${s.framework}]`) : "";
26159
+ const sid = s.serviceId ? source_default.dim(` ${s.serviceId}`) : source_default.yellow(" (not yet created)");
26160
+ console.log(` ${source_default.cyan(s.name.padEnd(16))} ${source_default.dim(s.rootDir)}${fw}${sid}`);
26161
+ }
26162
+ }
26163
+ async function up(opts) {
26164
+ if (!requireLogin3()) return;
26165
+ const repoRoot = findRepoRoot();
26166
+ const manifest = readManifest(repoRoot);
26167
+ if (!manifest) {
26168
+ console.error(source_default.red(`No ${MANIFEST_FILE} found. Run ${source_default.bold("lm monorepo init")} first.`));
26169
+ process.exitCode = 1;
26170
+ return;
26171
+ }
26172
+ const gitInfo = getGitInfo(repoRoot);
26173
+ if (gitInfo.hasUnpushed && gitInfo.hasRemote) {
26174
+ const pushSpin = ora("Pushing commits to remote...").start();
26175
+ try {
26176
+ execFileSync9("git", ["push", "origin", gitInfo.repoBranch], {
26177
+ cwd: repoRoot,
26178
+ stdio: ["pipe", "pipe", "pipe"]
26179
+ });
26180
+ pushSpin.succeed("Pushed commits to remote");
26181
+ } catch (err) {
26182
+ pushSpin.fail(`Push failed: ${err instanceof Error ? err.message : String(err)}`);
26183
+ process.exitCode = 1;
26184
+ return;
26185
+ }
26186
+ }
26187
+ const wantNames = opts.service && opts.service.length > 0 ? new Set(opts.service) : null;
26188
+ const changed = opts.all ? [] : changedFilesSince(repoRoot, opts.base);
26189
+ if (!opts.all && changed.length > 0) {
26190
+ console.log(source_default.dim(`Diff base: ${opts.base ?? "auto"} \u2014 ${changed.length} file${changed.length === 1 ? "" : "s"} changed`));
26191
+ }
26192
+ const selected = [];
26193
+ const skipped = [];
26194
+ for (const s of manifest.services) {
26195
+ if (wantNames && !wantNames.has(s.name)) {
26196
+ skipped.push({ name: s.name, reason: "not in --service filter" });
26197
+ continue;
26198
+ }
26199
+ if (!s.serviceId) {
26200
+ skipped.push({ name: s.name, reason: "no serviceId \u2014 re-run lm monorepo init" });
26201
+ continue;
26202
+ }
26203
+ if (!opts.all && changed.length > 0 && !serviceWasChanged(s.rootDir, changed)) {
26204
+ skipped.push({ name: s.name, reason: "no changes in rootDir" });
26205
+ continue;
26206
+ }
26207
+ selected.push({ entry: s, reason: opts.all ? "--all" : changed.length === 0 ? "no diff base" : "rootDir touched" });
26208
+ }
26209
+ if (selected.length === 0) {
26210
+ console.log(source_default.yellow("Nothing to deploy."));
26211
+ for (const s of skipped) console.log(source_default.dim(` - ${s.name}: ${s.reason}`));
26212
+ console.log(source_default.dim(`Force a deploy of all services with ${source_default.bold("lm up --all")}.`));
26213
+ return;
26214
+ }
26215
+ console.log(source_default.bold(`Deploying ${selected.length} service${selected.length === 1 ? "" : "s"}:`));
26216
+ for (const s of selected) console.log(` ${source_default.cyan(s.entry.name)} ${source_default.dim(`(${s.reason})`)}`);
26217
+ if (skipped.length > 0) {
26218
+ console.log(source_default.dim(`Skipped:`));
26219
+ for (const s of skipped) console.log(source_default.dim(` - ${s.name}: ${s.reason}`));
26220
+ }
26221
+ console.log();
26222
+ const results = await Promise.allSettled(
26223
+ selected.map(async ({ entry }) => {
26224
+ const res = await api("/api/deployments", {
26225
+ method: "POST",
26226
+ body: JSON.stringify({
26227
+ serviceId: entry.serviceId,
26228
+ branch: gitInfo.repoBranch,
26229
+ commitSha: gitInfo.commitSha
26230
+ })
26231
+ });
26232
+ return { name: entry.name, deploymentId: res.data.id };
26233
+ })
26234
+ );
26235
+ let okCount = 0;
26236
+ let failCount = 0;
26237
+ for (let i = 0; i < results.length; i++) {
26238
+ const r = results[i];
26239
+ const name = selected[i].entry.name;
26240
+ if (r.status === "fulfilled") {
26241
+ okCount++;
26242
+ console.log(source_default.green(`\u2713 ${name}`) + source_default.dim(` queued (deployment ${r.value.deploymentId})`));
26243
+ } else {
26244
+ failCount++;
26245
+ const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
26246
+ console.log(source_default.red(`\u2717 ${name}`) + source_default.dim(` ${msg}`));
26247
+ }
26248
+ }
26249
+ console.log();
26250
+ console.log(
26251
+ source_default.bold(`Deployed ${okCount}/${selected.length}`) + source_default.dim(` Track progress: lm status / lm logs --service <name>`)
26252
+ );
26253
+ if (failCount > 0) process.exitCode = 1;
26254
+ }
26255
+ function registerMonorepo(program3) {
26256
+ const cmd = program3.command("monorepo").alias("mono").description("Manage the launchmatic.json manifest for multi-service repos");
26257
+ cmd.command("init").description("Auto-discover services and write launchmatic.json").option("-y, --yes", "Don't prompt \u2014 accept all defaults").option("--team <id>", "Team ID to attach the project to").option("--project <name>", "Project name (default: derived from repo)").option("-n, --name <name>", "Alias for --project").action(initManifest);
26258
+ cmd.command("list").alias("ls").description("Print services declared in launchmatic.json").action(listManifest);
26259
+ }
26260
+ function registerUp(program3) {
26261
+ program3.command("up").description("Deploy every service in launchmatic.json (Railway-style multi-service deploy)").option("-s, --service <name...>", "Only deploy these services (by name)").option("-a, --all", "Skip the changed-only filter \u2014 deploy every service").option("--base <ref>", "Git ref to diff against (default: origin/<branch> or HEAD~1)").action(up);
26262
+ }
26263
+
26264
+ // src/commands/template.ts
26265
+ import readline8 from "readline";
26266
+ function requireLogin4() {
26267
+ if (!isLoggedIn()) {
26268
+ console.error(source_default.red('Not logged in. Run "lm login" first.'));
26269
+ process.exitCode = 1;
26270
+ return false;
26271
+ }
26272
+ return true;
26273
+ }
26274
+ function prompt7(question) {
26275
+ const rl = readline8.createInterface({ input: process.stdin, output: process.stdout });
26276
+ return new Promise((resolve5) => {
26277
+ rl.question(question, (a) => {
26278
+ rl.close();
26279
+ resolve5(a.trim());
26280
+ });
26281
+ });
26282
+ }
26283
+ async function listCmd(opts) {
26284
+ const spin = ora("Loading templates...").start();
26285
+ try {
26286
+ const res = await api("/api/templates");
26287
+ spin.stop();
26288
+ const filtered = opts.category ? res.data.filter((t) => t.category === opts.category.toLowerCase()) : res.data;
26289
+ if (filtered.length === 0) {
26290
+ console.log(source_default.dim("No templates match."));
26291
+ return;
26292
+ }
26293
+ const byCategory = /* @__PURE__ */ new Map();
26294
+ for (const t of filtered) {
26295
+ const arr = byCategory.get(t.category) ?? [];
26296
+ arr.push(t);
26297
+ byCategory.set(t.category, arr);
26298
+ }
26299
+ for (const [cat, templates] of byCategory) {
26300
+ console.log();
26301
+ console.log(source_default.bold.underline(cat));
26302
+ for (const t of templates) {
26303
+ console.log(` ${source_default.cyan(t.slug.padEnd(20))} ${source_default.bold(t.name)}`);
26304
+ console.log(` ${source_default.dim(" ".repeat(20))} ${source_default.dim(t.description)}`);
26305
+ const chips = [...t.tags];
26306
+ if (t.databases.length > 0) chips.push(...t.databases.map((d) => `db:${d}`));
26307
+ if (chips.length > 0) console.log(` ${source_default.dim(" ".repeat(20))} ${source_default.dim(chips.join(" \xB7 "))}`);
26308
+ }
26309
+ }
26310
+ console.log();
26311
+ console.log(source_default.dim(`Deploy with: ${source_default.bold("lm template deploy <slug>")}`));
26312
+ } catch (err) {
26313
+ spin.fail(`Could not load templates: ${err instanceof Error ? err.message : String(err)}`);
26314
+ process.exitCode = 1;
26315
+ }
26316
+ }
26317
+ async function deployCmd(slug, opts) {
26318
+ if (!requireLogin4()) return;
26319
+ let detail;
26320
+ try {
26321
+ const res = await api(`/api/templates/${slug}`);
26322
+ detail = res.data;
26323
+ } catch (err) {
26324
+ console.error(source_default.red(`Could not find template "${slug}": ${err instanceof Error ? err.message : String(err)}`));
26325
+ process.exitCode = 1;
26326
+ return;
26327
+ }
26328
+ let projectId = opts.project;
26329
+ if (!projectId && contextExists()) {
26330
+ try {
26331
+ projectId = readContext().projectId;
26332
+ } catch {
26333
+ }
26334
+ }
26335
+ if (!projectId) {
26336
+ const { data: teams } = await api("/api/teams");
26337
+ const teamId = teams[0]?.id;
26338
+ if (!teamId) {
26339
+ console.error(source_default.red("No teams found on your account."));
26340
+ process.exitCode = 1;
26341
+ return;
26342
+ }
26343
+ const { data: projects } = await api(
26344
+ `/api/projects?teamId=${teamId}`
26345
+ );
26346
+ if (projects.length === 0) {
26347
+ console.error(source_default.red("No projects yet. Pass --project <projectId> or create one in the dashboard."));
26348
+ process.exitCode = 1;
26349
+ return;
26350
+ }
26351
+ if (projects.length === 1 || opts.yes) {
26352
+ projectId = projects[0].id;
26353
+ } else {
26354
+ console.log(source_default.bold("Pick a project:"));
26355
+ projects.forEach((p, i) => console.log(` ${i + 1}. ${p.name} ${source_default.dim(`(${p.slug})`)}`));
26356
+ const ans = await prompt7("Project number: ");
26357
+ projectId = projects[parseInt(ans) - 1]?.id ?? projects[0].id;
26358
+ }
26359
+ }
26360
+ const envVars = {};
26361
+ for (const pair of opts.env ?? []) {
26362
+ const eq = pair.indexOf("=");
26363
+ if (eq < 0) {
26364
+ console.error(source_default.red(`--env expects KEY=value, got ${pair}`));
26365
+ process.exitCode = 1;
26366
+ return;
26367
+ }
26368
+ envVars[pair.slice(0, eq)] = pair.slice(eq + 1);
26369
+ }
26370
+ for (const v of detail.envVars.filter((e) => e.required)) {
26371
+ if (envVars[v.key]) continue;
26372
+ if (opts.yes) {
26373
+ console.error(source_default.red(`--yes set but required env var ${v.key} missing.`));
26374
+ process.exitCode = 1;
26375
+ return;
26376
+ }
26377
+ console.log(source_default.dim(`
26378
+ ${v.description}`));
26379
+ const example = v.example ? source_default.dim(` (e.g. ${v.example})`) : "";
26380
+ const ans = await prompt7(`${source_default.cyan(v.key)}${example}: `);
26381
+ if (!ans) {
26382
+ console.error(source_default.red(`Required env var ${v.key} cannot be empty.`));
26383
+ process.exitCode = 1;
26384
+ return;
26385
+ }
26386
+ envVars[v.key] = ans;
26387
+ }
26388
+ console.log();
26389
+ console.log(source_default.bold(`Deploying template ${source_default.cyan(detail.slug)}: ${detail.name}`));
26390
+ if (detail.databases.length > 0) {
26391
+ console.log(source_default.dim(` + ${detail.databases.join(", ")} ${detail.databases.length === 1 ? "database" : "databases"}`));
26392
+ }
26393
+ console.log();
26394
+ const spin = ora("Generating starter code...").start();
26395
+ try {
26396
+ const res = await api(`/api/templates/${slug}/deploy`, {
26397
+ method: "POST",
26398
+ body: JSON.stringify({
26399
+ projectId,
26400
+ name: opts.name,
26401
+ envVars
26402
+ })
26403
+ });
26404
+ spin.succeed(source_default.green("Template queued"));
26405
+ console.log();
26406
+ console.log(` Service ID: ${source_default.cyan(res.data.serviceId)}`);
26407
+ console.log(` Generation ID: ${source_default.dim(res.data.generationId)}`);
26408
+ console.log();
26409
+ console.log(
26410
+ source_default.dim(`Track progress: ${source_default.bold(`lm deployments list --service ${res.data.serviceId}`)}`)
26411
+ );
26412
+ console.log(source_default.dim(`Stream logs: ${source_default.bold(`lm logs --service ${res.data.serviceId}`)}`));
26413
+ } catch (err) {
26414
+ spin.fail(`Template deploy failed: ${err instanceof Error ? err.message : String(err)}`);
26415
+ process.exitCode = 1;
26416
+ }
26417
+ }
26418
+ function registerTemplate(program3) {
26419
+ const cmd = program3.command("template").alias("tpl").description("Curated starter templates \u2014 Railway-style one-shot deploy");
26420
+ cmd.command("list").alias("ls").description("List available templates").option("-c, --category <cat>", "Filter by category (fullstack, backend, frontend, ai, data, devtools)").action(listCmd);
26421
+ cmd.command("deploy <slug>").description("Deploy a template into a project").option("-p, --project <projectId>", "Project to deploy into (default: from .launchmatic.json or first project)").option("-n, --name <name>", "Service name override (default: template name)").option("-e, --env <KEY=value...>", "Set required env vars inline").option("-y, --yes", "Don't prompt \u2014 fail if required env vars are missing").action(deployCmd);
26422
+ }
26423
+
25484
26424
  // src/index.ts
25485
26425
  var program2 = new Command();
25486
- program2.name("lm").description("Launchmatic CLI \u2014 deploy from your terminal").version("0.4.0");
26426
+ program2.name("lm").description("Launchmatic CLI \u2014 deploy from your terminal").version("0.5.1");
25487
26427
  registerLogin(program2);
25488
26428
  registerInit(program2);
25489
26429
  registerDeploy(program2);
@@ -25504,4 +26444,9 @@ registerPreview(program2);
25504
26444
  registerApiKey(program2);
25505
26445
  registerDeployments(program2);
25506
26446
  registerAgent(program2);
26447
+ registerWorkflows(program2);
26448
+ registerImage(program2);
26449
+ registerMonorepo(program2);
26450
+ registerUp(program2);
26451
+ registerTemplate(program2);
25507
26452
  program2.parse();