@light-cloud/cli 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,6 +21,14 @@ $ lc deploy
21
21
  └ Live at https://my-app.acme.light-cloud.io
22
22
  ```
23
23
 
24
+ ## No account yet? Start here
25
+
26
+ ```
27
+ $ npx @light-cloud/cli login --device --email you@example.com
28
+ ```
29
+
30
+ Type the 8-character code it prints at `console.light-cloud.com/device` from any device (approve the link in the email we send for a new address). That creates the account — free plan, no password, no form — and signs the terminal in. `lc billing card add --plan pro` later, when you need a paid plan; the card goes through a Stripe-hosted link, never through `lc`.
31
+
24
32
  ## Install
25
33
 
26
34
  ```bash
package/dist/index.js CHANGED
@@ -756,6 +756,16 @@ var LightCloudApi = class {
756
756
  environmentId
757
757
  });
758
758
  }
759
+ /** Visitor password gate. No password turns the gate off. */
760
+ async setEnvironmentPassword(organisationId, environmentId, password2) {
761
+ const enabled = Boolean(password2);
762
+ return this.client.post("/api/environments/password", {
763
+ targetOrganisationId: organisationId,
764
+ environmentId,
765
+ enabled,
766
+ ...enabled ? { password: password2 } : {}
767
+ });
768
+ }
759
769
  async createEnvironment(request) {
760
770
  return this.client.post("/api/environments/create", request);
761
771
  }
@@ -1696,6 +1706,21 @@ function packageSource(options = {}) {
1696
1706
  });
1697
1707
  }
1698
1708
 
1709
+ // src/lib/upload/storage-error.ts
1710
+ function describeStorageUploadFailure(response) {
1711
+ switch (response.status) {
1712
+ case 401:
1713
+ case 403:
1714
+ return `Light Cloud storage refused the upload (${response.status}). This is a platform-side fault, not your account, plan or permissions \u2014 deploy from a git repository for now and report it to Light Cloud support.`;
1715
+ case 400:
1716
+ return "the upload link was rejected, most likely expired \u2014 run the deploy again.";
1717
+ case 413:
1718
+ return "the archive is too large for a single upload \u2014 exclude build output and large assets, or deploy from a git repository.";
1719
+ default:
1720
+ return `Light Cloud storage answered ${response.status}${response.statusText ? ` ${response.statusText}` : ""} \u2014 try again in a moment.`;
1721
+ }
1722
+ }
1723
+
1699
1724
  // src/commands/shared.ts
1700
1725
  function contextFrom(command) {
1701
1726
  const options = command.optsWithGlobals();
@@ -1811,9 +1836,12 @@ async function uploadSource(ctx, directory, detection) {
1811
1836
  });
1812
1837
  if (!put.ok) {
1813
1838
  spin.stop("Upload failed", 1);
1814
- throw new CliError(`Upload rejected by storage (${put.status} ${put.statusText}).`, { hint: "Try again in a moment." });
1839
+ throw new CliError(`Upload failed: ${describeStorageUploadFailure(put)}`, {
1840
+ hint: put.status === 403 ? "Deploy from a git repository meanwhile: lc deploy --repo <owner/name>." : "Try again in a moment."
1841
+ });
1815
1842
  }
1816
- await ctx.api.completeUpload(org.id, session.uploadId, {
1843
+ spin.message("Inspecting source");
1844
+ const server = await ctx.api.completeUpload(org.id, session.uploadId, {
1817
1845
  detectedFramework: detection.framework,
1818
1846
  detectedRuntime: detection.runtime,
1819
1847
  detectedDeploymentType: detection.deploymentType,
@@ -1821,7 +1849,11 @@ async function uploadSource(ctx, directory, detection) {
1821
1849
  detectedOutputDirectory: detection.outputDirectory
1822
1850
  });
1823
1851
  spin.stop(`Uploaded ${pack.fileCount} files ${c.dim(`(${formatBytes(pack.buffer.length)})`)}`);
1824
- return { uploadId: session.uploadId, fileCount: pack.fileCount, totalSize: pack.totalSize, archiveSize: pack.buffer.length };
1852
+ if (server.detectionSource === "server" && server.detectedFramework && server.detectedFramework !== detection.framework) {
1853
+ log.info(`Light Cloud read the source as ${server.detectedFramework} (${server.detectedDeploymentType}).`);
1854
+ }
1855
+ if (server.configWarning) log.warn(server.configWarning);
1856
+ return { uploadId: session.uploadId, fileCount: pack.fileCount, totalSize: pack.totalSize, archiveSize: pack.buffer.length, server };
1825
1857
  }
1826
1858
  function describeDetection(detection) {
1827
1859
  const parts = [];
@@ -2101,6 +2133,8 @@ async function browserSignIn(consoleUrl, openIt) {
2101
2133
  out(`${c.dim("\u2502")} Open this link in your browser to sign in:`);
2102
2134
  }
2103
2135
  out(`${c.dim("\u2502")} ${link(flow.url)}`);
2136
+ if (isJson()) process.stderr.write(`Open this link to sign in: ${flow.url}
2137
+ `);
2104
2138
  const spin = spinner2();
2105
2139
  spin.start("Waiting for you to sign in\u2026");
2106
2140
  try {
@@ -2134,6 +2168,8 @@ async function deviceSignIn(client, emailFlag) {
2134
2168
  out(`${c.dim("\u2502")} ${c.bold(flow.userCode)}`);
2135
2169
  out(`${c.dim("\u2502")}`);
2136
2170
  out(`${c.dim("\u2502")} ${c.dim(`The code expires in ${minutes} minutes.`)}`);
2171
+ if (isJson()) process.stderr.write(`Enter code ${flow.userCode} at ${flow.verificationUrl} (expires in ${minutes} min)
2172
+ `);
2137
2173
  if (!flow.emailSent) {
2138
2174
  log.warn("The confirmation email could not be sent; an existing account can still approve by signing in on the page above.");
2139
2175
  }
@@ -3045,7 +3081,8 @@ function registerDeployCommand(program2) {
3045
3081
  out(`${c.dim("\u2502")} ${c.bold(app.name)} ${c.dim(sym.arrow)} ${env.name}${env.is_production ? c.accent(` ${sym.star}`) : ""}`);
3046
3082
  }
3047
3083
  let uploadId;
3048
- if (options.upload) {
3084
+ const linkedHere = isUploadApp && ctx.project?.config.applicationId === app.id;
3085
+ if (options.upload || linkedHere) {
3049
3086
  uploadId = await uploadForDeploy(ctx, options.dir);
3050
3087
  } else if (isUploadApp) {
3051
3088
  if (!isJson()) out(`${c.dim("\u2502")} Source: last upload ${c.dim("(pass --upload to send this folder)")}`);
@@ -3343,6 +3380,20 @@ function registerEnvCommands(program2) {
3343
3380
  if (result.dedicated_addon_notice) log.info(result.dedicated_addon_notice);
3344
3381
  });
3345
3382
  });
3383
+ env.command("password [env]").description("Gate the site behind a visitor password, or make it public again").option("-a, --app <app>", "app name or id").option("--set <password>", "password visitors must enter (6-128 characters)").option("--off", "remove the password gate").action(async (ref, options, command) => {
3384
+ const ctx = contextFrom(command);
3385
+ if (!options.set && !options.off) {
3386
+ throw new CliError("Nothing to change.", { hint: "Pass --set <password> or --off.", exitCode: EXIT.USAGE });
3387
+ }
3388
+ const app = await ctx.resolveApp(options.app);
3389
+ const target = await ctx.resolveEnv(app, ref);
3390
+ const org = await ctx.resolveOrg();
3391
+ const result = await ctx.api.setEnvironmentPassword(org.id, target.id, options.off ? void 0 : options.set);
3392
+ emit(result, () => {
3393
+ if (result.passwordEnabled) log.success(`${app.name} / ${target.name} is password protected.`);
3394
+ else log.success(`${app.name} / ${target.name} is public.`);
3395
+ });
3396
+ });
3346
3397
  const vars = env.command("vars").description("Environment variables");
3347
3398
  vars.command("list", { isDefault: true }).description("List variables (values masked unless --reveal)").option("-a, --app <app>", "app name or id").option("-e, --env <env>", "environment (default: production)").option("--reveal", "print values in clear").action(async (options, command) => {
3348
3399
  const ctx = contextFrom(command);
@@ -3492,7 +3543,6 @@ function printEnvDetails(env) {
3492
3543
  ["Memory", env.memory ?? void 0],
3493
3544
  ["Instances", env.min_instances != null || env.max_instances != null ? `${env.min_instances ?? 0} ${c.dim(sym.arrow)} ${env.max_instances ?? "?"}` : void 0],
3494
3545
  ["Password", env.password_enabled ? "protected" : void 0],
3495
- ["Region", env.cloud_run_region ?? void 0],
3496
3546
  ["Variables", `${Object.keys(env.environment_vars ?? {}).length}`],
3497
3547
  ["Last deploy", relativeTime(env.last_deployed_at)],
3498
3548
  ["ID", c.dim(env.id)]
@@ -3552,7 +3602,7 @@ function registerInitCommands(program2) {
3552
3602
  outro2(`Linked ${c.bold(app.name)} ${c.dim(sym.arrow)} ${c.dim(file)}`);
3553
3603
  });
3554
3604
  });
3555
- program2.command("create").description("Create an app from a git repository or a local folder (non-interactive friendly)").option("-n, --name <name>", "app name (default: folder or repository name)").option("-r, --repo <url>", "GitHub repository URL (default: the origin remote)").option("-b, --branch <branch>", "branch to deploy (default: current branch or main)").option("-u, --upload", "upload the folder instead of connecting a repository").option("-d, --dir <path>", "project folder", ".").option("-f, --framework <id>", "framework id (see `lc frameworks`)").option("-t, --type <type>", "deployment type: static or container").option("--build <command>", "build command").option("--output <dir>", "output directory for static sites").option("--port <port>", "container port").option("--root <dir>", "monorepo root directory inside the repository").option("--no-deploy", "create without deploying").option("--no-watch", "do not follow the first deployment").option("--no-link", "do not write a .lightcloud file").action(async (options, command) => {
3605
+ program2.command("create").description("Create an app from a git repository or a local folder (non-interactive friendly)").option("-n, --name <name>", "app name (default: folder or repository name)").option("-r, --repo <url>", "GitHub repository URL (default: the origin remote)").option("-b, --branch <branch>", "branch to deploy (default: current branch or main)").option("-u, --upload", "upload the folder instead of connecting a repository").option("-d, --dir <path>", "project folder", ".").option("-f, --framework <id>", "framework id (see `lc frameworks`)").option("-t, --type <type>", "deployment type: static or container").option("--build <command>", "build command").option("--output <dir>", "output directory for static sites").option("--port <port>", "container port").option("--root <dir>", "monorepo root directory inside the repository").option("--no-deploy", "create without deploying").option("--no-watch", "do not follow the first deployment").option("--no-link", "do not write a .lightcloud file").option("--password <password>", "gate the site behind a visitor password (6-128 characters)").action(async (options, command) => {
3556
3606
  const ctx = contextFrom(command);
3557
3607
  const directory = path9.resolve(options.dir);
3558
3608
  ctx.requireAuth();
@@ -3566,8 +3616,13 @@ function registerInitCommands(program2) {
3566
3616
  }
3567
3617
  let url = app.deployed_url ?? void 0;
3568
3618
  let status = app.status;
3619
+ const env = (app.environments ?? []).find((e) => e.is_production) ?? app.environments?.[0];
3620
+ if (options.password && env) {
3621
+ const org = await ctx.resolveOrg();
3622
+ await ctx.api.setEnvironmentPassword(org.id, env.id, options.password);
3623
+ if (!isJson()) log.info("Visitors will be asked for the password.");
3624
+ }
3569
3625
  if (options.deploy && options.watch) {
3570
- const env = (app.environments ?? []).find((e) => e.is_production) ?? app.environments?.[0];
3571
3626
  const org = await ctx.resolveOrg();
3572
3627
  const result = await watchResource(ctx.api, env ? { kind: "environment", id: env.id, organisationId: org.id } : { kind: "application", id: app.id, organisationId: org.id });
3573
3628
  url = result.update.deployed_url ?? url;
@@ -3739,6 +3794,15 @@ async function createApp(ctx, directory, options, extra = {}) {
3739
3794
  const name = await pickName(options.name ?? path9.basename(directory), interactive);
3740
3795
  const definition = framework ? getFrameworkById(framework) : void 0;
3741
3796
  const upload = await uploadSource(ctx, directory, { ...detection, framework, deploymentType, buildCommand, outputDirectory });
3797
+ const server = upload.server.detectionSource === "server" ? upload.server : void 0;
3798
+ if (server) {
3799
+ framework = options.framework ?? server.detectedFramework ?? framework;
3800
+ deploymentType = options.type ?? server.detectedDeploymentType ?? deploymentType;
3801
+ buildCommand = options.build ?? server.detectedBuildCommand ?? buildCommand;
3802
+ outputDirectory = options.output ?? server.detectedOutputDirectory ?? outputDirectory;
3803
+ containerPort = options.port ? Number(options.port) : server.detectedContainerPort ?? containerPort;
3804
+ }
3805
+ const finalDefinition = framework ? getFrameworkById(framework) : definition;
3742
3806
  const spinCreate = spinner2();
3743
3807
  spinCreate.start(`Creating ${name}`);
3744
3808
  const app = await ctx.api.createFromUpload({
@@ -3747,7 +3811,7 @@ async function createApp(ctx, directory, options, extra = {}) {
3747
3811
  uploadId: upload.uploadId,
3748
3812
  deploymentType,
3749
3813
  framework: framework ?? (detection.hasDockerfile ? "custom" : void 0),
3750
- runtime: definition?.runtime ?? (detection.hasDockerfile ? "custom" : void 0),
3814
+ runtime: server?.detectedRuntime ?? finalDefinition?.runtime ?? (detection.hasDockerfile ? "custom" : void 0),
3751
3815
  buildCommand,
3752
3816
  outputDirectory: deploymentType === "static" ? outputDirectory : void 0,
3753
3817
  containerPort: deploymentType === "container" ? containerPort : void 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@light-cloud/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Light Cloud command-line interface: deploy, watch, inspect and manage applications, environments and databases from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {