@valbuild/cli 0.144.3 → 0.144.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @valbuild/cli
2
2
 
3
+ ## 0.144.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [#830](https://github.com/valbuild/val/pull/830) [`0ee047a`](https://github.com/valbuild/val/commit/0ee047a5c527f0122b4aa1aa167a0fbe4849fc37) Thanks [@freekh](https://github.com/freekh)! - `val connect` now opens [admin.val.build/new](https://admin.val.build/new?hosting=own) in your browser, on "your own hosting", with the GitHub repository and the `val.config.ts` project it found filled in. It says what it found, and prints the link too, for when no browser opens.
8
+
9
+ `npm create @valbuild` now ends by saying how to connect the new project: push it to GitHub, then pick it on admin.val.build/new or run `val connect`. `@valbuild/init` links to the same page, and now reads the GitHub repository from `origin` correctly.
10
+
11
+ - Updated dependencies [[`39e8db3`](https://github.com/valbuild/val/commit/39e8db34cf56c6c43ffff0d3acfe8e0839fa55a9)]:
12
+ - @valbuild/server@0.144.5
13
+ - @valbuild/shared@0.144.5
14
+ - @valbuild/language-server@0.144.5
15
+
16
+ ## 0.144.4
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [[`e70a261`](https://github.com/valbuild/val/commit/e70a261ed8c0938b635214721fc4e445704b1f1f)]:
21
+ - @valbuild/tanstack-build@0.144.4
22
+ - @valbuild/shared@0.144.4
23
+ - @valbuild/language-server@0.144.4
24
+ - @valbuild/server@0.144.4
25
+
3
26
  ## 0.144.3
4
27
 
5
28
  ### Patch Changes
@@ -11,6 +11,7 @@ var fs = require('fs/promises');
11
11
  var internal = require('@valbuild/shared/internal');
12
12
  var fs$1 = require('fs');
13
13
  var ts = require('typescript');
14
+ var child_process = require('child_process');
14
15
  var JSZip = require('jszip');
15
16
  var readline = require('readline');
16
17
  var crypto = require('crypto');
@@ -1169,25 +1170,83 @@ function requireVersion(packageName, key) {
1169
1170
  }
1170
1171
 
1171
1172
  const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
1173
+
1174
+ /**
1175
+ * `val connect`: opens admin's `/new` on "your own hosting", with what it can
1176
+ * tell from here filled in -- the project `val.config.ts` names and the
1177
+ * GitHub repository `origin` points at -- so the person only confirms. The
1178
+ * link is printed too, for when no browser opens (over SSH, in a container).
1179
+ */
1172
1180
  async function connect(options) {
1173
1181
  const {
1174
1182
  root
1175
1183
  } = options;
1176
1184
  const projectRoot = root ? path__default["default"].resolve(root) : process.cwd();
1177
- const maybeProject = await tryGetProject(projectRoot);
1178
- const maybeGitRemote = await tryGetGitRemote(projectRoot);
1179
- const maybeGitOwnerAndRepo = maybeGitRemote !== null ? getGitHubOwnerAndRepo(maybeGitRemote) : null;
1180
- const params = new URLSearchParams();
1181
- params.set("step", "create-project");
1182
- if (maybeProject) {
1183
- params.set("org", maybeProject.orgName);
1184
- params.set("project", maybeProject.projectName);
1185
- }
1186
- if (maybeGitOwnerAndRepo) {
1187
- params.set("github_repo", [maybeGitOwnerAndRepo.owner, maybeGitOwnerAndRepo.repo].join("/"));
1188
- }
1189
- const url = `${host}/connect?${params.toString()}`;
1190
- console.log(pc__default["default"].dim(`\nFollow the instructions in your browser to complete the setup:\n${pc__default["default"].cyan(url)}\n`));
1185
+ const project = await tryGetProject(projectRoot);
1186
+ const gitRemote = await tryGetGitRemote(projectRoot);
1187
+ const repo = gitRemote !== null ? getGitHubOwnerAndRepo(gitRemote) : null;
1188
+ console.log("");
1189
+ if (project) {
1190
+ console.log(`Found ${pc__default["default"].bold(`${project.orgName}/${project.projectName}`)} in val.config.ts`);
1191
+ }
1192
+ if (repo) {
1193
+ console.log(`Found ${pc__default["default"].bold(`${repo.owner}/${repo.repo}`)} on GitHub (git remote "origin")`);
1194
+ } else {
1195
+ console.log(pc__default["default"].dim("No GitHub repository found: push this project to GitHub, or pick or create one in the browser."));
1196
+ }
1197
+ const url = connectUrl(host, {
1198
+ project,
1199
+ repo
1200
+ });
1201
+ const opened = openInBrowser(url);
1202
+ console.log(`\n${opened ? "Finish in your browser. If it did not open, go to:" : "Finish in your browser:"}\n${pc__default["default"].cyan(url)}\n`);
1203
+ }
1204
+
1205
+ /**
1206
+ * Where `val connect` sends the person: `/new` on "your own hosting"
1207
+ * (`hosting=own`), with the repository and the project's names when they are
1208
+ * known. Older CLIs go to `/connect`, which redirects here.
1209
+ */
1210
+ function connectUrl(host, found) {
1211
+ const params = new URLSearchParams({
1212
+ hosting: "own"
1213
+ });
1214
+ if (found.repo) {
1215
+ params.set("repo", `${found.repo.owner}/${found.repo.repo}`);
1216
+ }
1217
+ if (found.project) {
1218
+ params.set("org", found.project.orgName);
1219
+ params.set("project", found.project.projectName);
1220
+ }
1221
+ return `${host.replace(/\/+$/, "")}/new?${params.toString()}`;
1222
+ }
1223
+
1224
+ /**
1225
+ * Opens `url` in the default browser, without waiting for it. False when
1226
+ * there is clearly none to open (CI, no terminal), so the caller can say so;
1227
+ * a browser that fails to start later is covered by the printed link.
1228
+ */
1229
+ function openInBrowser(url) {
1230
+ if (process.env.CI || !process.stdout.isTTY) return false;
1231
+ const windows = process.platform === "win32";
1232
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : windows ?
1233
+ // `start` is a cmd built-in: its first quoted argument is a window
1234
+ // title, and `&` would end the command unless escaped.
1235
+ ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
1236
+ try {
1237
+ const child = child_process.spawn(command, args, {
1238
+ detached: true,
1239
+ stdio: "ignore",
1240
+ windowsVerbatimArguments: windows
1241
+ });
1242
+ child.on("error", () => {
1243
+ // No opener installed: the printed link is the way in.
1244
+ });
1245
+ child.unref();
1246
+ return true;
1247
+ } catch {
1248
+ return false;
1249
+ }
1191
1250
  }
1192
1251
  async function tryGetProject(projectRoot) {
1193
1252
  const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
@@ -1205,22 +1264,18 @@ async function tryGetProject(projectRoot) {
1205
1264
  }
1206
1265
  return null;
1207
1266
  }
1267
+
1268
+ /**
1269
+ * The GitHub repository a remote URL names: `git@github.com:acme/site.git`,
1270
+ * `ssh://git@github.com/acme/site.git` or `https://[user@]github.com/acme/site`.
1271
+ * Null for any other host.
1272
+ */
1208
1273
  function getGitHubOwnerAndRepo(gitRemote) {
1209
- const sshMatch = gitRemote.match(/git@github\.com:(.+?)\/(.+?)(\.git)?$/);
1210
- if (sshMatch) {
1211
- return {
1212
- owner: sshMatch[1],
1213
- repo: sshMatch[2]
1214
- };
1215
- }
1216
- const httpsMatch = gitRemote.match(/https:\/\/github\.com\/(.+?)\/(.+?)(\.git)?$/);
1217
- if (httpsMatch) {
1218
- return {
1219
- owner: httpsMatch[1],
1220
- repo: httpsMatch[2]
1221
- };
1222
- }
1223
- return null;
1274
+ const match = gitRemote.trim().match(/^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/(?:[^@/\s]+@)?github\.com\/)([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
1275
+ return match ? {
1276
+ owner: match[1],
1277
+ repo: match[2]
1278
+ } : null;
1224
1279
  }
1225
1280
  async function tryGetGitRemote(root) {
1226
1281
  try {
@@ -4082,7 +4137,8 @@ async function main() {
4082
4137
 
4083
4138
 
4084
4139
  Command: connect
4085
- Description: connect your local project to a Val Build project at admin.val.build
4140
+ Description: open admin.val.build to connect this project, with its repository
4141
+ and val.config.ts project filled in
4086
4142
  Options:
4087
4143
  --root [root], -r [root] Set project root directory (default process.cwd())
4088
4144
 
@@ -11,6 +11,7 @@ var fs = require('fs/promises');
11
11
  var internal = require('@valbuild/shared/internal');
12
12
  var fs$1 = require('fs');
13
13
  var ts = require('typescript');
14
+ var child_process = require('child_process');
14
15
  var JSZip = require('jszip');
15
16
  var readline = require('readline');
16
17
  var crypto = require('crypto');
@@ -1169,25 +1170,83 @@ function requireVersion(packageName, key) {
1169
1170
  }
1170
1171
 
1171
1172
  const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
1173
+
1174
+ /**
1175
+ * `val connect`: opens admin's `/new` on "your own hosting", with what it can
1176
+ * tell from here filled in -- the project `val.config.ts` names and the
1177
+ * GitHub repository `origin` points at -- so the person only confirms. The
1178
+ * link is printed too, for when no browser opens (over SSH, in a container).
1179
+ */
1172
1180
  async function connect(options) {
1173
1181
  const {
1174
1182
  root
1175
1183
  } = options;
1176
1184
  const projectRoot = root ? path__default["default"].resolve(root) : process.cwd();
1177
- const maybeProject = await tryGetProject(projectRoot);
1178
- const maybeGitRemote = await tryGetGitRemote(projectRoot);
1179
- const maybeGitOwnerAndRepo = maybeGitRemote !== null ? getGitHubOwnerAndRepo(maybeGitRemote) : null;
1180
- const params = new URLSearchParams();
1181
- params.set("step", "create-project");
1182
- if (maybeProject) {
1183
- params.set("org", maybeProject.orgName);
1184
- params.set("project", maybeProject.projectName);
1185
- }
1186
- if (maybeGitOwnerAndRepo) {
1187
- params.set("github_repo", [maybeGitOwnerAndRepo.owner, maybeGitOwnerAndRepo.repo].join("/"));
1188
- }
1189
- const url = `${host}/connect?${params.toString()}`;
1190
- console.log(pc__default["default"].dim(`\nFollow the instructions in your browser to complete the setup:\n${pc__default["default"].cyan(url)}\n`));
1185
+ const project = await tryGetProject(projectRoot);
1186
+ const gitRemote = await tryGetGitRemote(projectRoot);
1187
+ const repo = gitRemote !== null ? getGitHubOwnerAndRepo(gitRemote) : null;
1188
+ console.log("");
1189
+ if (project) {
1190
+ console.log(`Found ${pc__default["default"].bold(`${project.orgName}/${project.projectName}`)} in val.config.ts`);
1191
+ }
1192
+ if (repo) {
1193
+ console.log(`Found ${pc__default["default"].bold(`${repo.owner}/${repo.repo}`)} on GitHub (git remote "origin")`);
1194
+ } else {
1195
+ console.log(pc__default["default"].dim("No GitHub repository found: push this project to GitHub, or pick or create one in the browser."));
1196
+ }
1197
+ const url = connectUrl(host, {
1198
+ project,
1199
+ repo
1200
+ });
1201
+ const opened = openInBrowser(url);
1202
+ console.log(`\n${opened ? "Finish in your browser. If it did not open, go to:" : "Finish in your browser:"}\n${pc__default["default"].cyan(url)}\n`);
1203
+ }
1204
+
1205
+ /**
1206
+ * Where `val connect` sends the person: `/new` on "your own hosting"
1207
+ * (`hosting=own`), with the repository and the project's names when they are
1208
+ * known. Older CLIs go to `/connect`, which redirects here.
1209
+ */
1210
+ function connectUrl(host, found) {
1211
+ const params = new URLSearchParams({
1212
+ hosting: "own"
1213
+ });
1214
+ if (found.repo) {
1215
+ params.set("repo", `${found.repo.owner}/${found.repo.repo}`);
1216
+ }
1217
+ if (found.project) {
1218
+ params.set("org", found.project.orgName);
1219
+ params.set("project", found.project.projectName);
1220
+ }
1221
+ return `${host.replace(/\/+$/, "")}/new?${params.toString()}`;
1222
+ }
1223
+
1224
+ /**
1225
+ * Opens `url` in the default browser, without waiting for it. False when
1226
+ * there is clearly none to open (CI, no terminal), so the caller can say so;
1227
+ * a browser that fails to start later is covered by the printed link.
1228
+ */
1229
+ function openInBrowser(url) {
1230
+ if (process.env.CI || !process.stdout.isTTY) return false;
1231
+ const windows = process.platform === "win32";
1232
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : windows ?
1233
+ // `start` is a cmd built-in: its first quoted argument is a window
1234
+ // title, and `&` would end the command unless escaped.
1235
+ ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
1236
+ try {
1237
+ const child = child_process.spawn(command, args, {
1238
+ detached: true,
1239
+ stdio: "ignore",
1240
+ windowsVerbatimArguments: windows
1241
+ });
1242
+ child.on("error", () => {
1243
+ // No opener installed: the printed link is the way in.
1244
+ });
1245
+ child.unref();
1246
+ return true;
1247
+ } catch {
1248
+ return false;
1249
+ }
1191
1250
  }
1192
1251
  async function tryGetProject(projectRoot) {
1193
1252
  const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
@@ -1205,22 +1264,18 @@ async function tryGetProject(projectRoot) {
1205
1264
  }
1206
1265
  return null;
1207
1266
  }
1267
+
1268
+ /**
1269
+ * The GitHub repository a remote URL names: `git@github.com:acme/site.git`,
1270
+ * `ssh://git@github.com/acme/site.git` or `https://[user@]github.com/acme/site`.
1271
+ * Null for any other host.
1272
+ */
1208
1273
  function getGitHubOwnerAndRepo(gitRemote) {
1209
- const sshMatch = gitRemote.match(/git@github\.com:(.+?)\/(.+?)(\.git)?$/);
1210
- if (sshMatch) {
1211
- return {
1212
- owner: sshMatch[1],
1213
- repo: sshMatch[2]
1214
- };
1215
- }
1216
- const httpsMatch = gitRemote.match(/https:\/\/github\.com\/(.+?)\/(.+?)(\.git)?$/);
1217
- if (httpsMatch) {
1218
- return {
1219
- owner: httpsMatch[1],
1220
- repo: httpsMatch[2]
1221
- };
1222
- }
1223
- return null;
1274
+ const match = gitRemote.trim().match(/^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/(?:[^@/\s]+@)?github\.com\/)([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
1275
+ return match ? {
1276
+ owner: match[1],
1277
+ repo: match[2]
1278
+ } : null;
1224
1279
  }
1225
1280
  async function tryGetGitRemote(root) {
1226
1281
  try {
@@ -4082,7 +4137,8 @@ async function main() {
4082
4137
 
4083
4138
 
4084
4139
  Command: connect
4085
- Description: connect your local project to a Val Build project at admin.val.build
4140
+ Description: open admin.val.build to connect this project, with its repository
4141
+ and val.config.ts project filled in
4086
4142
  Options:
4087
4143
  --root [root], -r [root] Set project root directory (default process.cwd())
4088
4144
 
@@ -9,6 +9,7 @@ import fs from 'fs/promises';
9
9
  import { resolveSchemaSourceFixes, traverseSchemaSource, parseBuildTarget, parseDeclare, parseArtifacts, parseVerify, parsePromote, parseStatus, parsePublishToken, PublishProtocolError, parseProblems } from '@valbuild/shared/internal';
10
10
  import fs$1 from 'fs';
11
11
  import ts from 'typescript';
12
+ import { spawn } from 'child_process';
12
13
  import JSZip from 'jszip';
13
14
  import readline from 'readline';
14
15
  import crypto from 'crypto';
@@ -1134,25 +1135,83 @@ function requireVersion(packageName, key) {
1134
1135
  }
1135
1136
 
1136
1137
  const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
1138
+
1139
+ /**
1140
+ * `val connect`: opens admin's `/new` on "your own hosting", with what it can
1141
+ * tell from here filled in -- the project `val.config.ts` names and the
1142
+ * GitHub repository `origin` points at -- so the person only confirms. The
1143
+ * link is printed too, for when no browser opens (over SSH, in a container).
1144
+ */
1137
1145
  async function connect(options) {
1138
1146
  const {
1139
1147
  root
1140
1148
  } = options;
1141
1149
  const projectRoot = root ? path.resolve(root) : process.cwd();
1142
- const maybeProject = await tryGetProject(projectRoot);
1143
- const maybeGitRemote = await tryGetGitRemote(projectRoot);
1144
- const maybeGitOwnerAndRepo = maybeGitRemote !== null ? getGitHubOwnerAndRepo(maybeGitRemote) : null;
1145
- const params = new URLSearchParams();
1146
- params.set("step", "create-project");
1147
- if (maybeProject) {
1148
- params.set("org", maybeProject.orgName);
1149
- params.set("project", maybeProject.projectName);
1150
- }
1151
- if (maybeGitOwnerAndRepo) {
1152
- params.set("github_repo", [maybeGitOwnerAndRepo.owner, maybeGitOwnerAndRepo.repo].join("/"));
1153
- }
1154
- const url = `${host}/connect?${params.toString()}`;
1155
- console.log(pc.dim(`\nFollow the instructions in your browser to complete the setup:\n${pc.cyan(url)}\n`));
1150
+ const project = await tryGetProject(projectRoot);
1151
+ const gitRemote = await tryGetGitRemote(projectRoot);
1152
+ const repo = gitRemote !== null ? getGitHubOwnerAndRepo(gitRemote) : null;
1153
+ console.log("");
1154
+ if (project) {
1155
+ console.log(`Found ${pc.bold(`${project.orgName}/${project.projectName}`)} in val.config.ts`);
1156
+ }
1157
+ if (repo) {
1158
+ console.log(`Found ${pc.bold(`${repo.owner}/${repo.repo}`)} on GitHub (git remote "origin")`);
1159
+ } else {
1160
+ console.log(pc.dim("No GitHub repository found: push this project to GitHub, or pick or create one in the browser."));
1161
+ }
1162
+ const url = connectUrl(host, {
1163
+ project,
1164
+ repo
1165
+ });
1166
+ const opened = openInBrowser(url);
1167
+ console.log(`\n${opened ? "Finish in your browser. If it did not open, go to:" : "Finish in your browser:"}\n${pc.cyan(url)}\n`);
1168
+ }
1169
+
1170
+ /**
1171
+ * Where `val connect` sends the person: `/new` on "your own hosting"
1172
+ * (`hosting=own`), with the repository and the project's names when they are
1173
+ * known. Older CLIs go to `/connect`, which redirects here.
1174
+ */
1175
+ function connectUrl(host, found) {
1176
+ const params = new URLSearchParams({
1177
+ hosting: "own"
1178
+ });
1179
+ if (found.repo) {
1180
+ params.set("repo", `${found.repo.owner}/${found.repo.repo}`);
1181
+ }
1182
+ if (found.project) {
1183
+ params.set("org", found.project.orgName);
1184
+ params.set("project", found.project.projectName);
1185
+ }
1186
+ return `${host.replace(/\/+$/, "")}/new?${params.toString()}`;
1187
+ }
1188
+
1189
+ /**
1190
+ * Opens `url` in the default browser, without waiting for it. False when
1191
+ * there is clearly none to open (CI, no terminal), so the caller can say so;
1192
+ * a browser that fails to start later is covered by the printed link.
1193
+ */
1194
+ function openInBrowser(url) {
1195
+ if (process.env.CI || !process.stdout.isTTY) return false;
1196
+ const windows = process.platform === "win32";
1197
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : windows ?
1198
+ // `start` is a cmd built-in: its first quoted argument is a window
1199
+ // title, and `&` would end the command unless escaped.
1200
+ ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
1201
+ try {
1202
+ const child = spawn(command, args, {
1203
+ detached: true,
1204
+ stdio: "ignore",
1205
+ windowsVerbatimArguments: windows
1206
+ });
1207
+ child.on("error", () => {
1208
+ // No opener installed: the printed link is the way in.
1209
+ });
1210
+ child.unref();
1211
+ return true;
1212
+ } catch {
1213
+ return false;
1214
+ }
1156
1215
  }
1157
1216
  async function tryGetProject(projectRoot) {
1158
1217
  const valConfigFile = await findAndEvalValConfigFile(projectRoot);
@@ -1170,22 +1229,18 @@ async function tryGetProject(projectRoot) {
1170
1229
  }
1171
1230
  return null;
1172
1231
  }
1232
+
1233
+ /**
1234
+ * The GitHub repository a remote URL names: `git@github.com:acme/site.git`,
1235
+ * `ssh://git@github.com/acme/site.git` or `https://[user@]github.com/acme/site`.
1236
+ * Null for any other host.
1237
+ */
1173
1238
  function getGitHubOwnerAndRepo(gitRemote) {
1174
- const sshMatch = gitRemote.match(/git@github\.com:(.+?)\/(.+?)(\.git)?$/);
1175
- if (sshMatch) {
1176
- return {
1177
- owner: sshMatch[1],
1178
- repo: sshMatch[2]
1179
- };
1180
- }
1181
- const httpsMatch = gitRemote.match(/https:\/\/github\.com\/(.+?)\/(.+?)(\.git)?$/);
1182
- if (httpsMatch) {
1183
- return {
1184
- owner: httpsMatch[1],
1185
- repo: httpsMatch[2]
1186
- };
1187
- }
1188
- return null;
1239
+ const match = gitRemote.trim().match(/^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/(?:[^@/\s]+@)?github\.com\/)([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
1240
+ return match ? {
1241
+ owner: match[1],
1242
+ repo: match[2]
1243
+ } : null;
1189
1244
  }
1190
1245
  async function tryGetGitRemote(root) {
1191
1246
  try {
@@ -4047,7 +4102,8 @@ async function main() {
4047
4102
 
4048
4103
 
4049
4104
  Command: connect
4050
- Description: connect your local project to a Val Build project at admin.val.build
4105
+ Description: open admin.val.build to connect this project, with its repository
4106
+ and val.config.ts project filled in
4051
4107
  Options:
4052
4108
  --root [root], -r [root] Set project root directory (default process.cwd())
4053
4109
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@valbuild/cli",
3
3
  "private": false,
4
- "version": "0.144.3",
4
+ "version": "0.144.5",
5
5
  "description": "Val CLI tools",
6
6
  "repository": {
7
7
  "type": "git",
@@ -28,9 +28,9 @@
28
28
  "zod-validation-error": "^5.0.0",
29
29
  "@valbuild/core": "0.144.0",
30
30
  "@valbuild/eslint-plugin": "0.125.0",
31
- "@valbuild/language-server": "0.144.3",
32
- "@valbuild/server": "0.144.3",
33
- "@valbuild/shared": "0.144.2"
31
+ "@valbuild/language-server": "0.144.5",
32
+ "@valbuild/server": "0.144.5",
33
+ "@valbuild/shared": "0.144.5"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "prettier": "*",
@@ -38,7 +38,7 @@
38
38
  "rolldown": ">=1.2.5",
39
39
  "@tanstack/router-generator": ">=1.167.0",
40
40
  "@tanstack/router-plugin": ">=1.168.0",
41
- "@valbuild/tanstack-build": "^0.144.2"
41
+ "@valbuild/tanstack-build": "^0.144.4"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@valbuild/tanstack-build": {
@@ -70,7 +70,7 @@
70
70
  "prettier": "~3.8.5",
71
71
  "rolldown": "^1.2.5",
72
72
  "tsx": "^4.23.12",
73
- "@valbuild/tanstack-build": "0.144.2"
73
+ "@valbuild/tanstack-build": "0.144.4"
74
74
  },
75
75
  "scripts": {
76
76
  "start": "tsx src/cli.ts --",
package/src/cli.ts CHANGED
@@ -47,7 +47,8 @@ async function main(): Promise<void> {
47
47
 
48
48
 
49
49
  Command: connect
50
- Description: connect your local project to a Val Build project at admin.val.build
50
+ Description: open admin.val.build to connect this project, with its repository
51
+ and val.config.ts project filled in
51
52
  Options:
52
53
  --root [root], -r [root] Set project root directory (default process.cwd())
53
54
 
@@ -0,0 +1,49 @@
1
+ import { connectUrl, getGitHubOwnerAndRepo } from "./connect";
2
+
3
+ describe("val connect", () => {
4
+ test("opens /new on your own hosting, with the repository and the project filled in", () => {
5
+ const url = new URL(
6
+ connectUrl("https://admin.val.build", {
7
+ project: { orgName: "acme", projectName: "website" },
8
+ repo: { owner: "acme-gh", repo: "site" },
9
+ }),
10
+ );
11
+ expect(url.origin + url.pathname).toBe("https://admin.val.build/new");
12
+ expect(Object.fromEntries(url.searchParams)).toEqual({
13
+ hosting: "own",
14
+ repo: "acme-gh/site",
15
+ org: "acme",
16
+ project: "website",
17
+ });
18
+ });
19
+
20
+ test("with nothing found, it is /new on your own hosting alone", () => {
21
+ expect(
22
+ connectUrl("http://localhost:3000/", { project: null, repo: null }),
23
+ ).toBe("http://localhost:3000/new?hosting=own");
24
+ });
25
+
26
+ test("the repository is read from origin over ssh or https", () => {
27
+ expect(getGitHubOwnerAndRepo("git@github.com:acme/site.git")).toEqual({
28
+ owner: "acme",
29
+ repo: "site",
30
+ });
31
+ expect(getGitHubOwnerAndRepo("https://github.com/acme/site")).toEqual({
32
+ owner: "acme",
33
+ repo: "site",
34
+ });
35
+ expect(getGitHubOwnerAndRepo("ssh://git@github.com/acme/site.git")).toEqual(
36
+ { owner: "acme", repo: "site" },
37
+ );
38
+ expect(
39
+ getGitHubOwnerAndRepo("https://me@github.com/acme/site.git"),
40
+ ).toEqual({ owner: "acme", repo: "site" });
41
+ // Host names are case-insensitive.
42
+ expect(getGitHubOwnerAndRepo("https://GitHub.com/acme/site.git")).toEqual({
43
+ owner: "acme",
44
+ repo: "site",
45
+ });
46
+ expect(getGitHubOwnerAndRepo("git@gitlab.com:acme/site.git")).toBeNull();
47
+ expect(getGitHubOwnerAndRepo("git@notgithub.com:acme/site.git")).toBeNull();
48
+ });
49
+ });
package/src/connect.ts CHANGED
@@ -1,40 +1,105 @@
1
1
  import pc from "picocolors";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
+ import { spawn } from "child_process";
4
5
  import { findAndEvalValConfigFile } from "./utils/evalValConfigFile";
5
6
 
6
7
  const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
7
8
 
9
+ /**
10
+ * `val connect`: opens admin's `/new` on "your own hosting", with what it can
11
+ * tell from here filled in -- the project `val.config.ts` names and the
12
+ * GitHub repository `origin` points at -- so the person only confirms. The
13
+ * link is printed too, for when no browser opens (over SSH, in a container).
14
+ */
8
15
  export async function connect(options: { root?: string }) {
9
16
  const { root } = options;
10
17
  const projectRoot = root ? path.resolve(root) : process.cwd();
11
18
 
12
- const maybeProject = await tryGetProject(projectRoot);
13
- const maybeGitRemote = await tryGetGitRemote(projectRoot);
14
- const maybeGitOwnerAndRepo =
15
- maybeGitRemote !== null ? getGitHubOwnerAndRepo(maybeGitRemote) : null;
19
+ const project = await tryGetProject(projectRoot);
20
+ const gitRemote = await tryGetGitRemote(projectRoot);
21
+ const repo = gitRemote !== null ? getGitHubOwnerAndRepo(gitRemote) : null;
16
22
 
17
- const params = new URLSearchParams();
18
- params.set("step", "create-project");
19
- if (maybeProject) {
20
- params.set("org", maybeProject.orgName);
21
- params.set("project", maybeProject.projectName);
23
+ console.log("");
24
+ if (project) {
25
+ console.log(
26
+ `Found ${pc.bold(`${project.orgName}/${project.projectName}`)} in val.config.ts`,
27
+ );
22
28
  }
23
- if (maybeGitOwnerAndRepo) {
24
- params.set(
25
- "github_repo",
26
- [maybeGitOwnerAndRepo.owner, maybeGitOwnerAndRepo.repo].join("/"),
29
+ if (repo) {
30
+ console.log(
31
+ `Found ${pc.bold(`${repo.owner}/${repo.repo}`)} on GitHub (git remote "origin")`,
32
+ );
33
+ } else {
34
+ console.log(
35
+ pc.dim(
36
+ "No GitHub repository found: push this project to GitHub, or pick or create one in the browser.",
37
+ ),
27
38
  );
28
39
  }
29
- const url = `${host}/connect?${params.toString()}`;
30
40
 
41
+ const url = connectUrl(host, { project, repo });
42
+ const opened = openInBrowser(url);
31
43
  console.log(
32
- pc.dim(
33
- `\nFollow the instructions in your browser to complete the setup:\n${pc.cyan(url)}\n`,
34
- ),
44
+ `\n${opened ? "Finish in your browser. If it did not open, go to:" : "Finish in your browser:"}\n${pc.cyan(url)}\n`,
35
45
  );
36
46
  }
37
47
 
48
+ /**
49
+ * Where `val connect` sends the person: `/new` on "your own hosting"
50
+ * (`hosting=own`), with the repository and the project's names when they are
51
+ * known. Older CLIs go to `/connect`, which redirects here.
52
+ */
53
+ export function connectUrl(
54
+ host: string,
55
+ found: {
56
+ project: { orgName: string; projectName: string } | null;
57
+ repo: { owner: string; repo: string } | null;
58
+ },
59
+ ): string {
60
+ const params = new URLSearchParams({ hosting: "own" });
61
+ if (found.repo) {
62
+ params.set("repo", `${found.repo.owner}/${found.repo.repo}`);
63
+ }
64
+ if (found.project) {
65
+ params.set("org", found.project.orgName);
66
+ params.set("project", found.project.projectName);
67
+ }
68
+ return `${host.replace(/\/+$/, "")}/new?${params.toString()}`;
69
+ }
70
+
71
+ /**
72
+ * Opens `url` in the default browser, without waiting for it. False when
73
+ * there is clearly none to open (CI, no terminal), so the caller can say so;
74
+ * a browser that fails to start later is covered by the printed link.
75
+ */
76
+ function openInBrowser(url: string): boolean {
77
+ if (process.env.CI || !process.stdout.isTTY) return false;
78
+ const windows = process.platform === "win32";
79
+ const [command, args] =
80
+ process.platform === "darwin"
81
+ ? ["open", [url]]
82
+ : windows
83
+ ? // `start` is a cmd built-in: its first quoted argument is a window
84
+ // title, and `&` would end the command unless escaped.
85
+ ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]]
86
+ : ["xdg-open", [url]];
87
+ try {
88
+ const child = spawn(command, args, {
89
+ detached: true,
90
+ stdio: "ignore",
91
+ windowsVerbatimArguments: windows,
92
+ });
93
+ child.on("error", () => {
94
+ // No opener installed: the printed link is the way in.
95
+ });
96
+ child.unref();
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
38
103
  async function tryGetProject(projectRoot: string): Promise<{
39
104
  orgName: string;
40
105
  projectName: string;
@@ -60,20 +125,20 @@ async function tryGetProject(projectRoot: string): Promise<{
60
125
  return null;
61
126
  }
62
127
 
63
- function getGitHubOwnerAndRepo(
128
+ /**
129
+ * The GitHub repository a remote URL names: `git@github.com:acme/site.git`,
130
+ * `ssh://git@github.com/acme/site.git` or `https://[user@]github.com/acme/site`.
131
+ * Null for any other host.
132
+ */
133
+ export function getGitHubOwnerAndRepo(
64
134
  gitRemote: string,
65
135
  ): { owner: string; repo: string } | null {
66
- const sshMatch = gitRemote.match(/git@github\.com:(.+?)\/(.+?)(\.git)?$/);
67
- if (sshMatch) {
68
- return { owner: sshMatch[1], repo: sshMatch[2] };
69
- }
70
- const httpsMatch = gitRemote.match(
71
- /https:\/\/github\.com\/(.+?)\/(.+?)(\.git)?$/,
72
- );
73
- if (httpsMatch) {
74
- return { owner: httpsMatch[1], repo: httpsMatch[2] };
75
- }
76
- return null;
136
+ const match = gitRemote
137
+ .trim()
138
+ .match(
139
+ /^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/(?:[^@/\s]+@)?github\.com\/)([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i,
140
+ );
141
+ return match ? { owner: match[1], repo: match[2] } : null;
77
142
  }
78
143
 
79
144
  async function tryGetGitRemote(root: string): Promise<string | null> {