menloapp 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,8 +23,15 @@ npm i -g menloapp
23
23
  menloapp deploy
24
24
  ```
25
25
 
26
- Run deploy in your app’s public GitHub repository. It returns a live link like
26
+ Run deploy in your app’s folder. It returns a live link like
27
27
  **https://menloapp.lol/your-app**. Share it. New commits update the same app page.
28
+
29
+ Deploy takes care of the Git and GitHub steps, asking before each one. A plain
30
+ folder gets a Git repository (with a standard Xcode `.gitignore` that keeps
31
+ `.env`, signing keys and provisioning profiles out). Uncommitted changes can be
32
+ committed with your message, a missing GitHub repository is created as a public
33
+ repository under your account, and unpushed commits are pushed. Deploy never
34
+ commits files that look like secrets and stops on files GitHub would reject.
28
35
  Interactive Mac deploy copies the link and prints a ready-to-share X link.
29
36
  Empty app descriptions are filled from your public GitHub repository description;
30
37
  supplied copy is preserved.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "menloapp",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "MENLO: share living iOS apps from GitHub and try them on your own iPhone",
5
5
  "type": "module",
6
6
  "bin": {
package/src/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const NPM_CLI_VERSION = "1.6.0";
1
+ export const NPM_CLI_VERSION = "1.7.0";
2
2
  export const PRODUCT_VERSION = "1.3.0";
3
3
  export const MANIFEST_URL = "https://tohseno.com/releases/cli-1.3.0.json";
4
4
  export const MANIFEST_ORIGINS = new Set(["https://tohseno.com"]);
package/src/github.js CHANGED
@@ -6,6 +6,7 @@ import { choose, continueAfter, deployUI, openBrowser, shellQuote } from "./depl
6
6
 
7
7
  import { setupPresentation, readCommittedPresentation, updatePresentationLinks, fillPresentationDescription, fillPresentationIcon, commitPresentation, previewNeedsRefresh } from "./project-presentation.js";
8
8
  import { recordExperience } from "./record.js";
9
+ import { createGitHubRepository, initRepository, offerCommit, pushBranch } from "./repository-setup.js";
9
10
  import { previewTools } from "./experience-agent.js";
10
11
 
11
12
  export const MENLO_ORIGIN = "https://menloapp.lol";
@@ -121,6 +122,11 @@ export async function authenticate(ui, { fetcher = fetch, execute = run, env = p
121
122
  catch (error) { if (error.status === 401) throw new Error(`GitHub did not accept the new sign-in.\n${instructions}`); throw error; }
122
123
  }
123
124
 
125
+ function isAncestor(root, ancestor, commit) {
126
+ try { execFileSync("git", ["-C", root, "merge-base", "--is-ancestor", ancestor, commit], { stdio: "ignore", timeout: 20_000 }); return true; }
127
+ catch { return false; }
128
+ }
129
+
124
130
  function git(directory, ...args) {
125
131
  try { return run("git", ["-C", directory, ...args]); }
126
132
  catch { throw new Error("Open your app's Git repository, then run menloapp deploy. It needs a GitHub origin and at least one commit."); }
@@ -138,9 +144,34 @@ async function inspectRepository(options) {
138
144
  return { root, directory, explicitContainer, repository: githubRepository(remote) };
139
145
  }
140
146
 
147
+ /**
148
+ * Take the folder as far as a GitHub repository with at least one commit,
149
+ * asking before each step: git init, first commit, public GitHub repository.
150
+ */
151
+ async function prepareRepository(options, ui, { credential, user, fetcher, description }) {
152
+ let input;
153
+ try { input = await realpath(options.directory); } catch { throw new Error(`App directory not found: ${options.directory}\nRun menloapp deploy from your app's repository.`); }
154
+ const directory = /\.(xcodeproj|xcworkspace)$/.test(input) ? path.dirname(input) : input;
155
+ let root = optionalRun(run, "git", ["-C", directory, "rev-parse", "--show-toplevel"]);
156
+ if (!root) root = await initRepository(directory, ui);
157
+ if (!optionalRun(run, "git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"])) {
158
+ if (!ui.interactive) throw new Error("This repository has no commits yet.\nCommit your app (git add -A && git commit -m \"First version\"), then run menloapp deploy.");
159
+ if (!await offerCommit(root, ui, { first: true, name: path.basename(root) })) throw new Error("Deploy needs a first commit. Nothing was published.");
160
+ }
161
+ if (!optionalRun(run, "git", ["-C", root, "remote", "get-url", "origin"])) {
162
+ const call = (url, init) => api(url, { ...init, headers: { ...headers(credential), "Content-Type": "application/json" } }, fetcher);
163
+ await createGitHubRepository(root, ui, { credential, user, api: call, describe: description });
164
+ }
165
+ return inspectRepository(options);
166
+ }
167
+
141
168
  export async function inspectProject(options, ui = deployUI(options)) {
142
169
  const { root, directory, explicitContainer, repository } = await inspectRepository(options);
143
170
  while (git(root, "status", "--porcelain", "--untracked-files=normal")) {
171
+ if (ui.interactive) {
172
+ try { if (await offerCommit(root, ui, { name: path.basename(root) })) continue; }
173
+ catch (error) { ui.log(`\n${error.message}`); }
174
+ }
144
175
  await continueAfter(ui, "Your app has uncommitted changes. MENLO shares the code pushed to GitHub.\nIn another terminal, review git status, commit the files you want to share, and push them. Local files will not be uploaded automatically.");
145
176
  }
146
177
  const commit = git(root, "rev-parse", "HEAD");
@@ -232,21 +263,43 @@ export async function deploy(args, dependencies = {}) {
232
263
  const ui = dependencies.ui || deployUI(options);
233
264
  const fetcher = dependencies.fetcher || fetch;
234
265
  if (options.help) {
235
- ui.print("MENLO deploy\n\nmenloapp deploy [path]\n\nSign in to GitHub, choose your app if needed, and get a production app link.\nMENLO guides you through each step. Later pushes appear automatically.\n\nOptions: --project path/App.xcodeproj, --scheme Name, --app-slug your-app,\n --name Name, --dry-run (local checks only), --json (no prompts)\n --record (regenerate the automatic preview), --no-preview,\n --seconds 3–60, --simulator UDID");
266
+ ui.print("MENLO deploy\n\nmenloapp deploy [path]\n\nSign in to GitHub, choose your app if needed, and get a production app link.\nMENLO sets up Git and a public GitHub repository if needed, asking first.\nLater pushes appear automatically.\n\nOptions: --project path/App.xcodeproj, --scheme Name, --app-slug your-app,\n --name Name, --dry-run (local checks only), --json (no prompts)\n --record (regenerate the automatic preview), --no-preview,\n --seconds 3–60, --simulator UDID");
236
267
  return 0;
237
268
  }
238
269
  if (options.dryRun) { ui.print(JSON.stringify(await inspectProject(options, ui), null, 2)); return 0; }
239
- const local = await inspectRepository(options);
240
- ui.log(`MENLO deploy\nRepository: ${local.repository}`);
241
- const { credential } = await authenticate(ui, { ...dependencies, fetcher });
270
+ // A non-GitHub origin fails before sign-in; a missing repository or origin is set up after it.
271
+ const existingRoot = optionalRun(run, "git", ["-C", options.directory, "rev-parse", "--show-toplevel"]);
272
+ const existingRemote = existingRoot && optionalRun(run, "git", ["-C", existingRoot, "remote", "get-url", "origin"]);
273
+ if (existingRemote) githubRepository(existingRemote);
274
+ ui.log("MENLO deploy");
275
+ const { credential, user } = await authenticate(ui, { ...dependencies, fetcher });
276
+ const local = await prepareRepository(options, ui, { credential, user, fetcher, description: "" });
277
+ ui.log(`Repository: ${local.repository}`);
242
278
  const repo = await publicRepository(local.repository, credential, ui, fetcher);
279
+ const sleep = dependencies.sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
280
+ let pushed = 0;
243
281
  let project;
244
282
  while (true) {
245
283
  project = await inspectProject(options, ui);
246
284
  if (project.repository !== local.repository) throw new Error("The GitHub origin changed during deployment. Run menloapp deploy again to connect the new repository.");
247
- const head = await api(`https://api.github.com/repos/${project.repository}/commits/${encodeURIComponent(repo.default_branch)}`, { headers: headers(credential) }, fetcher);
248
- if (head.sha === project.commit) break;
285
+ let head;
286
+ try { head = await api(`https://api.github.com/repos/${project.repository}/commits/${encodeURIComponent(repo.default_branch)}`, { headers: headers(credential) }, fetcher); }
287
+ catch (error) { if (error.status !== 409 && error.status !== 404) throw error; head = null; /* An empty GitHub repository. */ }
288
+ if (head?.sha === project.commit) break;
289
+ // GitHub can take a moment to report a branch that was just pushed.
290
+ if (pushed && pushed < 4) { pushed++; await sleep(1500); continue; }
249
291
  const branch = git(local.root, "branch", "--show-current");
292
+ const ahead = branch === repo.default_branch && (!head || isAncestor(local.root, head.sha, project.commit));
293
+ if (ahead && ui.interactive && pushed === 0) {
294
+ const count = Number(optionalRun(run, "git", ["-C", local.root, "rev-list", "--count", head ? `${head.sha}..HEAD` : "HEAD"])) || 1;
295
+ const answer = await ui.ask(`Push ${count === 1 ? "your new commit" : `${count} new commits`} to GitHub's ${branch} branch? [Y/n] `);
296
+ if (!answer || /^y(es)?$/i.test(answer)) {
297
+ pushBranch(local.root, project.repository, branch, credential);
298
+ pushed = 1;
299
+ continue;
300
+ }
301
+ }
302
+ pushed = 0;
250
303
  const instruction = branch === repo.default_branch
251
304
  ? `Push your committed changes: git push origin ${shellQuote(branch)}\nIf GitHub has newer commits, bring them in first: git pull --ff-only origin ${shellQuote(branch)}`
252
305
  : `MENLO follows ${repo.default_branch}. Merge your changes into that branch and push it, then switch this checkout to ${repo.default_branch}.`;
@@ -271,7 +324,7 @@ export async function deploy(args, dependencies = {}) {
271
324
  }
272
325
  }
273
326
  if (generated.length) {
274
- (dependencies.commitPresentation || commitPresentation)(local.root, [...new Set(generated)], repo.default_branch);
327
+ (dependencies.commitPresentation || commitPresentation)(local.root, [...new Set(generated)], repo.default_branch, { repository: local.repository, credential });
275
328
  project = await inspectProject(options, ui);
276
329
  }
277
330
  ui.log(`App: ${project.name}\nProject: ${project.project} · ${project.scheme}\nConnecting this app to MENLO…`);
@@ -309,7 +362,7 @@ export async function deploy(args, dependencies = {}) {
309
362
  }
310
363
  if (result.public_url !== `${MENLO_ORIGIN}/${project.slug}`) throw new Error("MENLO did not confirm the expected app link. Run menloapp deploy again to verify it.");
311
364
  if (await updatePresentationLinks(local.root, project.repository, result.public_url)) {
312
- (dependencies.commitPresentation || commitPresentation)(local.root, ["menloapp/app.json"], repo.default_branch);
365
+ (dependencies.commitPresentation || commitPresentation)(local.root, ["menloapp/app.json"], repo.default_branch, { repository: local.repository, credential });
313
366
  ui.log("Saved your GitHub and MENLO links to menloapp/app.json.");
314
367
  }
315
368
  result = { ...result, githubRepo: `https://github.com/${project.repository}`, menloLink: result.public_url };
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { pushBranch } from "./repository-setup.js";
4
5
  import { PRESENTATION_PATH, MAX_PRESENTATION_BYTES, MAX_IMAGE_BYTES, MAX_VIDEO_BYTES, validatePresentation, presentationFiles, validateMediaBytes } from "./presentation.js";
5
6
 
6
7
  export function projectRoot(directory) {
@@ -134,7 +135,7 @@ export function previewNeedsRefresh(root, commit, presentation) {
134
135
  } catch { return true; }
135
136
  }
136
137
 
137
- export function commitPresentation(root, files, branch) {
138
+ export function commitPresentation(root, files, branch, { repository, credential } = {}) {
138
139
  const allowed = new Set(files);
139
140
  for (const file of allowed) if (!file.startsWith("menloapp/") || file.split("/").some(part => part === ".." || part === ".")) throw new Error("Only generated app presentation files can be committed by deploy.");
140
141
  const git = (...args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", timeout: 60_000, stdio: ["ignore", "pipe", "pipe"] }).trim();
@@ -145,7 +146,9 @@ export function commitPresentation(root, files, branch) {
145
146
  if (!trackedChanges.length && !untracked.length) return false;
146
147
  git("add", "--", ...allowed);
147
148
  git("commit", "-m", "Update MENLO app page", "--only", "--", ...allowed);
148
- try { git("push", "origin", branch); }
149
- catch { throw new Error(`The app page is committed locally, but the push did not complete. Run git push origin ${branch}, then menloapp deploy.`); }
149
+ try {
150
+ if (repository) pushBranch(root, repository, branch, credential);
151
+ else git("push", "origin", branch);
152
+ } catch { throw new Error(`The app page is committed locally, but the push did not complete. Run git push origin ${branch}, then menloapp deploy.`); }
150
153
  return true;
151
154
  }
@@ -0,0 +1,158 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+
6
+ // Deploy can take a plain folder all the way to a public GitHub repository.
7
+ // Every step that publishes code asks first, and nothing secret-looking is committed.
8
+
9
+ const GITHUB_FILE_LIMIT = 100 * 1024 * 1024;
10
+
11
+ export const XCODE_GITIGNORE = `# Xcode
12
+ build/
13
+ DerivedData/
14
+ xcuserdata/
15
+ *.xcuserstate
16
+ *.xcscmblueprint
17
+ *.xccheckout
18
+ .build/
19
+ .swiftpm/xcode/
20
+
21
+ # macOS
22
+ .DS_Store
23
+
24
+ # Secrets and signing material stay on this Mac
25
+ .env
26
+ .env.*
27
+ *.p8
28
+ *.p12
29
+ *.pem
30
+ *.key
31
+ *.cer
32
+ *.mobileprovision
33
+ *.provisionprofile
34
+ `;
35
+
36
+ // Files that usually hold credentials. Deploy refuses to publish them automatically.
37
+ const RISKY = [
38
+ /(?:^|\/)\.env(?:\.|$)/, /\.(?:p8|p12|pem|key|cer|mobileprovision|provisionprofile|keystore|jks)$/i,
39
+ /(?:^|\/)(?:credentials|secrets?)(?:\.[a-z]+)?$/i, /(?:^|\/)id_(?:rsa|ed25519|ecdsa)$/,
40
+ ];
41
+ export const riskyFile = file => RISKY.some(pattern => pattern.test(file));
42
+
43
+ const gitIn = (root, args, options = {}) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", timeout: 120_000, maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], ...options }).trim();
44
+
45
+ /** Files that `git add -A` would stage, relative to the repository root. */
46
+ function pendingFiles(root) {
47
+ const untracked = gitIn(root, ["ls-files", "--others", "--exclude-standard", "-z"]).split("\0").filter(Boolean);
48
+ let changed = [];
49
+ try { changed = gitIn(root, ["diff", "HEAD", "--name-only", "-z"]).split("\0").filter(Boolean); }
50
+ catch { changed = gitIn(root, ["ls-files", "-z"]).split("\0").filter(Boolean); }
51
+ return [...new Set([...changed, ...untracked])].sort();
52
+ }
53
+
54
+ async function checkPublishable(root, files) {
55
+ const risky = files.filter(riskyFile);
56
+ if (risky.length) throw new Error(`These files look like secrets, so Menlo will not publish them:\n${risky.slice(0, 10).map(file => ` ${file}`).join("\n")}\nAdd them to .gitignore (or remove them), then run menloapp deploy again.`);
57
+ const large = [];
58
+ for (const file of files) {
59
+ const info = await stat(path.join(root, file)).catch(() => null);
60
+ if (info?.isFile() && info.size > GITHUB_FILE_LIMIT) large.push(file);
61
+ }
62
+ if (large.length) throw new Error(`GitHub does not accept files over 100 MB:\n${large.map(file => ` ${file}`).join("\n")}\nAdd them to .gitignore, then run menloapp deploy again.`);
63
+ }
64
+
65
+ function summary(files) {
66
+ const shown = files.slice(0, 12).map(file => ` ${file}`).join("\n");
67
+ return files.length > 12 ? `${shown}\n …and ${files.length - 12} more` : shown;
68
+ }
69
+
70
+ const yes = (answer, fallback) => answer === "" ? fallback : /^y(es)?$/i.test(answer);
71
+
72
+ /** Make a plain app folder into a Git repository with one reviewed first commit. */
73
+ export async function initRepository(directory, ui) {
74
+ if (!ui.interactive) throw new Error("This folder is not a Git repository yet.\nRun menloapp deploy in a terminal and Menlo will set it up, or run:\n git init && git add -A && git commit -m \"First version\"");
75
+ const projects = execFileSync("find", [directory, "-maxdepth", "4", "(", "-name", "*.xcodeproj", "-o", "-name", "*.xcworkspace", ")", "-not", "-path", "*/DerivedData/*", "-not", "-path", "*/build/*", "-not", "-path", "*.xcodeproj/*"], { encoding: "utf8", timeout: 20_000 }).trim();
76
+ if (!projects) throw new Error("No Xcode project was found in this folder.\nRun menloapp deploy from your iOS app's folder.");
77
+ ui.log(`\nThis folder is not a Git repository yet. Menlo shares apps from public GitHub source,\nso it will set one up here (with a standard Xcode .gitignore that keeps secrets out).`);
78
+ if (!yes(await ui.ask("Set up Git here? [Y/n] "), true)) throw new Error("Deploy stopped. Nothing was changed.");
79
+ gitIn(directory, ["init", "-q", "--initial-branch=main"]);
80
+ const ignore = path.join(directory, ".gitignore");
81
+ try { await writeFile(ignore, XCODE_GITIGNORE, { flag: "wx" }); }
82
+ catch (error) { if (error.code !== "EEXIST") throw error; }
83
+ return directory;
84
+ }
85
+
86
+ /**
87
+ * Offer to commit pending work. Returns true when a commit was made.
88
+ * `first` describes a brand-new repository's first commit.
89
+ */
90
+ export async function offerCommit(root, ui, { first = false, name = "app" } = {}) {
91
+ const files = pendingFiles(root);
92
+ if (!files.length) return false;
93
+ await checkPublishable(root, files);
94
+ ui.log(`\n${first ? "Files for the first version" : "Uncommitted changes"} (${files.length}):\n${summary(files)}`);
95
+ const question = first ? `Commit ${files.length === 1 ? "this file" : `these ${files.length} files`}? [Y/n] ` : `Commit ${files.length === 1 ? "this change" : `these ${files.length} changes`} and publish them to GitHub? [y/N] `;
96
+ if (!yes(await ui.ask(question), first)) return false;
97
+ const message = (first ? "" : await ui.ask(`Commit message [Update ${name}]: `)) || (first ? `First version of ${name}` : `Update ${name}`);
98
+ gitIn(root, ["add", "-A"]);
99
+ try { gitIn(root, ["commit", "-q", "-m", message]); }
100
+ catch (error) {
101
+ const detail = String(error.stderr || "").trim();
102
+ throw new Error(`Git could not commit${detail ? `:\n${detail}` : "."}${/user\.(?:name|email)|identity/i.test(detail) ? "\nSet your Git name and email, then run menloapp deploy again:\n git config --global user.name \"Your Name\"\n git config --global user.email you@example.com" : ""}`);
103
+ }
104
+ return true;
105
+ }
106
+
107
+ /**
108
+ * Push a branch to GitHub. Use the person's own Git credentials first; if
109
+ * those are not set up, use the GitHub sign-in deploy already verified for this
110
+ * one push, passed through the environment and never saved.
111
+ */
112
+ export function pushBranch(root, repository, branch, credential, { upstream = false } = {}) {
113
+ const target = `HEAD:refs/heads/${branch}`;
114
+ const quiet = { ...process.env, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" };
115
+ try { gitIn(root, ["push", "-q", ...(upstream ? ["-u"] : []), "origin", target], { env: quiet, timeout: 600_000 }); return; }
116
+ catch { /* Fall back to the verified sign-in below. */ }
117
+ if (!credential) throw new Error(`Git could not push to GitHub. Run git push origin ${branch}, then menloapp deploy.`);
118
+ const header = `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${credential}`).toString("base64")}`;
119
+ const env = { ...quiet, GIT_CONFIG_COUNT: "2", GIT_CONFIG_KEY_0: "credential.helper", GIT_CONFIG_VALUE_0: "", GIT_CONFIG_KEY_1: "http.https://github.com/.extraheader", GIT_CONFIG_VALUE_1: header };
120
+ try { gitIn(root, ["push", "-q", `https://github.com/${repository}.git`, target], { env, timeout: 600_000 }); }
121
+ catch { throw new Error(`GitHub did not accept the push to ${repository}.\nRun git push origin ${branch} to see why, then menloapp deploy again.`); }
122
+ // Keep `git status` accurate after a push that bypassed the remote's configured URL.
123
+ try { gitIn(root, ["fetch", "-q", "origin", branch], { env: quiet, timeout: 60_000 }); } catch { /* Cosmetic only. */ }
124
+ }
125
+
126
+ /** A GitHub repository name derived from the folder, e.g. "My App!" → "My-App". */
127
+ export function repositoryName(directory) {
128
+ return path.basename(directory).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|-+$/g, "").slice(0, 100) || "my-app";
129
+ }
130
+
131
+ /** Create the public GitHub repository for this folder, connect it as origin, and push. */
132
+ export async function createGitHubRepository(root, ui, { credential, user, api, describe = "" }) {
133
+ if (!ui.interactive) throw new Error(`This repository has no GitHub origin.\nRun menloapp deploy in a terminal and Menlo will create the public GitHub repository, or run:\n gh repo create --public --source=. --remote=origin --push`);
134
+ let name = repositoryName(root);
135
+ ui.log(`\nThis app is not on GitHub yet. Menlo shares apps from public GitHub source:\nanyone can read the code and its history.`);
136
+ while (true) {
137
+ const answer = await ui.ask(`Create the public repository github.com/${user.login}/${name}? [Y/n, or type another name] `);
138
+ if (/^n(o)?$/i.test(answer)) throw new Error("Deploy stopped. Nothing was published.");
139
+ if (answer && !/^y(es)?$/i.test(answer)) {
140
+ if (!/^[A-Za-z0-9._-]{1,100}$/.test(answer) || /^\.+$/.test(answer)) { ui.log("Use letters, numbers, dots, hyphens and underscores."); continue; }
141
+ name = answer;
142
+ }
143
+ try {
144
+ await api("https://api.github.com/user/repos", { method: "POST", body: JSON.stringify({ name, private: false, ...(describe ? { description: describe.slice(0, 350) } : {}) }) });
145
+ break;
146
+ } catch (error) {
147
+ if (error.status === 422) { ui.log(`You already have a repository named ${name} on GitHub. Choose another name.`); name = `${name}-app`; continue; }
148
+ if (error.status === 403 || error.status === 404) throw new Error(`Your GitHub sign-in cannot create repositories.\nCreate it at https://github.com/new (public), then run:\n git remote add origin https://github.com/${user.login}/${name}.git\n menloapp deploy`);
149
+ throw error;
150
+ }
151
+ }
152
+ const repository = `${user.login}/${name}`;
153
+ gitIn(root, ["remote", "add", "origin", `https://github.com/${repository}.git`]);
154
+ const branch = gitIn(root, ["branch", "--show-current"]) || "main";
155
+ ui.log(`Created https://github.com/${repository}\nPushing ${branch}…`);
156
+ pushBranch(root, repository, branch, credential, { upstream: true });
157
+ return repository;
158
+ }