@valbuild/cli 0.144.4 → 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 +13 -0
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +86 -30
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +86 -30
- package/cli/dist/valbuild-cli-cli.esm.js +86 -30
- package/package.json +4 -4
- package/src/cli.ts +2 -1
- package/src/connect.test.ts +49 -0
- package/src/connect.ts +94 -29
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
3
16
|
## 0.144.4
|
|
4
17
|
|
|
5
18
|
### 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
|
|
1178
|
-
const
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
const url =
|
|
1190
|
-
|
|
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
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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:
|
|
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
|
|
1178
|
-
const
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
const url =
|
|
1190
|
-
|
|
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
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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:
|
|
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
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
}
|
|
1154
|
-
const url =
|
|
1155
|
-
|
|
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
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
32
|
-
"@valbuild/server": "0.144.
|
|
33
|
-
"@valbuild/shared": "0.144.
|
|
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": "*",
|
package/src/cli.ts
CHANGED
|
@@ -47,7 +47,8 @@ async function main(): Promise<void> {
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
Command: connect
|
|
50
|
-
Description:
|
|
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
|
|
13
|
-
const
|
|
14
|
-
const
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 (
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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> {
|