@getmonoceros/workbench 1.5.3 → 1.6.1
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/dist/bin.js +717 -138
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -274,9 +274,8 @@ var SOLUTION_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
|
274
274
|
var APT_PACKAGE_NAME_RE = /^[a-z0-9][a-z0-9.+-]*$/;
|
|
275
275
|
var FEATURE_REF_RE = /^[a-z0-9.-]+(\/[a-z0-9._-]+)+:[a-z0-9._-]+$/;
|
|
276
276
|
var INSTALL_URL_RE = /^https:\/\/[A-Za-z0-9.\-_~/:?#[\]@!&'()*+,;=%]+$/;
|
|
277
|
-
var REPO_URL_RE = /^[A-Za-z0-9@:/+_~.#=&?-]+$/;
|
|
278
|
-
var
|
|
279
|
-
var REPO_BRANCH_RE = /^[A-Za-z0-9._/-]+$/;
|
|
277
|
+
var REPO_URL_RE = /^https:\/\/[A-Za-z0-9@:/+_~.#=&?-]+$/;
|
|
278
|
+
var REPO_PATH_RE = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
|
|
280
279
|
var POSTGRES_URL_RE = /^postgres(ql)?:\/\//;
|
|
281
280
|
var REGEX = {
|
|
282
281
|
solutionName: SOLUTION_NAME_RE,
|
|
@@ -284,10 +283,20 @@ var REGEX = {
|
|
|
284
283
|
featureRef: FEATURE_REF_RE,
|
|
285
284
|
installUrl: INSTALL_URL_RE,
|
|
286
285
|
repoUrl: REPO_URL_RE,
|
|
287
|
-
|
|
288
|
-
repoBranch: REPO_BRANCH_RE,
|
|
286
|
+
repoPath: REPO_PATH_RE,
|
|
289
287
|
postgresUrl: POSTGRES_URL_RE
|
|
290
288
|
};
|
|
289
|
+
var PROVIDER_VALUES = [
|
|
290
|
+
"github",
|
|
291
|
+
"gitlab",
|
|
292
|
+
"bitbucket",
|
|
293
|
+
"gitea"
|
|
294
|
+
];
|
|
295
|
+
var KNOWN_PROVIDER_HOSTS = {
|
|
296
|
+
"github.com": "github",
|
|
297
|
+
"gitlab.com": "gitlab",
|
|
298
|
+
"bitbucket.org": "bitbucket"
|
|
299
|
+
};
|
|
291
300
|
var CONFIG_SCHEMA_VERSION = 1;
|
|
292
301
|
var FeatureOptionValueSchema = z.union([
|
|
293
302
|
z.string(),
|
|
@@ -301,23 +310,39 @@ var FeatureEntrySchema = z.object({
|
|
|
301
310
|
),
|
|
302
311
|
options: z.record(z.string(), FeatureOptionValueSchema).optional()
|
|
303
312
|
});
|
|
313
|
+
var GitUserSchema = z.object({
|
|
314
|
+
name: z.string().min(1),
|
|
315
|
+
email: z.string().min(3).regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, "Invalid email")
|
|
316
|
+
});
|
|
304
317
|
var RepoEntrySchema = z.object({
|
|
305
318
|
url: z.string().regex(
|
|
306
319
|
REPO_URL_RE,
|
|
307
|
-
"Invalid repo URL.
|
|
320
|
+
"Invalid repo URL. Only HTTPS URLs are supported (https://...). SSH-style URLs (git@host:..., ssh://...) are not in scope \u2014 see ADR 0006."
|
|
308
321
|
),
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
"Invalid repo
|
|
322
|
+
path: z.string().regex(
|
|
323
|
+
REPO_PATH_RE,
|
|
324
|
+
"Invalid repo path. Use letters/digits/'._-', forward slashes for nested folders, no leading or trailing slash."
|
|
325
|
+
).refine(
|
|
326
|
+
(p) => !p.split("/").some((seg) => seg === ".." || seg === "."),
|
|
327
|
+
'Repo path segments cannot be "." or "..".'
|
|
312
328
|
).optional(),
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
329
|
+
// Per-repo git identity override. Falls back to the container-level
|
|
330
|
+
// `git.user` (which itself falls back to the host's
|
|
331
|
+
// `git config --global` at apply time). Useful when a single
|
|
332
|
+
// container clones multiple repos that need different committer
|
|
333
|
+
// identities — e.g. work GitHub org vs personal projects.
|
|
334
|
+
git: z.object({
|
|
335
|
+
user: GitUserSchema.optional()
|
|
336
|
+
}).optional(),
|
|
337
|
+
// Provider hint for the pre-flight credential check. For the three
|
|
338
|
+
// canonical hosts (github.com / gitlab.com / bitbucket.org) the
|
|
339
|
+
// provider is auto-detected and this field is unnecessary. For any
|
|
340
|
+
// other host (self-hosted GitLab on a custom domain, Gitea, …) the
|
|
341
|
+
// builder MUST declare the provider so apply can suggest the right
|
|
342
|
+
// CLI setup (`glab auth login --hostname <host>` etc.) when
|
|
343
|
+
// credentials are missing. Enforced at apply pre-flight, not at
|
|
344
|
+
// parse time — see ADR 0006.
|
|
345
|
+
provider: z.enum(PROVIDER_VALUES).optional()
|
|
321
346
|
});
|
|
322
347
|
var ExternalServicesSchema = z.object({
|
|
323
348
|
postgres: z.string().regex(
|
|
@@ -567,8 +592,7 @@ var APT_PACKAGE_NAME_RE2 = /^[a-z0-9][a-z0-9.+-]*$/;
|
|
|
567
592
|
var FEATURE_REF_RE2 = /^[a-z0-9.-]+(\/[a-z0-9._-]+)+:[a-z0-9._-]+$/;
|
|
568
593
|
var INSTALL_URL_RE2 = /^https:\/\/[A-Za-z0-9.\-_~/:?#[\]@!&'()*+,;=%]+$/;
|
|
569
594
|
var REPO_URL_RE2 = /^[A-Za-z0-9@:/+_~.#=&?-]+$/;
|
|
570
|
-
var
|
|
571
|
-
var REPO_BRANCH_RE2 = /^[A-Za-z0-9._/-]+$/;
|
|
595
|
+
var REPO_PATH_RE2 = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
|
|
572
596
|
function deriveRepoName(url) {
|
|
573
597
|
const lastSep = Math.max(url.lastIndexOf("/"), url.lastIndexOf(":"));
|
|
574
598
|
const tail = url.slice(lastSep + 1);
|
|
@@ -621,29 +645,29 @@ function validateOptions(opts) {
|
|
|
621
645
|
);
|
|
622
646
|
}
|
|
623
647
|
}
|
|
624
|
-
const
|
|
648
|
+
const seenRepoPaths = /* @__PURE__ */ new Set();
|
|
625
649
|
for (const repo of opts.repos ?? []) {
|
|
626
650
|
if (!REPO_URL_RE2.test(repo.url)) {
|
|
627
651
|
throw new Error(
|
|
628
652
|
`Invalid repo URL: ${JSON.stringify(repo.url)}. Use HTTPS or SSH/git@ form; no shell metacharacters.`
|
|
629
653
|
);
|
|
630
654
|
}
|
|
631
|
-
if (!
|
|
655
|
+
if (!REPO_PATH_RE2.test(repo.path)) {
|
|
632
656
|
throw new Error(
|
|
633
|
-
`Invalid repo
|
|
657
|
+
`Invalid repo path: ${JSON.stringify(repo.path)}. Use letters/digits/'._-', forward slashes for nested folders, no leading or trailing slash.`
|
|
634
658
|
);
|
|
635
659
|
}
|
|
636
|
-
if (repo.
|
|
660
|
+
if (repo.path.split("/").some((seg) => seg === ".." || seg === ".")) {
|
|
637
661
|
throw new Error(
|
|
638
|
-
`Invalid
|
|
662
|
+
`Invalid repo path: ${JSON.stringify(repo.path)}. Path segments cannot be "." or "..".`
|
|
639
663
|
);
|
|
640
664
|
}
|
|
641
|
-
if (
|
|
665
|
+
if (seenRepoPaths.has(repo.path)) {
|
|
642
666
|
throw new Error(
|
|
643
|
-
`Duplicate repo
|
|
667
|
+
`Duplicate repo path: ${JSON.stringify(repo.path)}. Each projects/<path> folder must be unique \u2014 pass --path to disambiguate.`
|
|
644
668
|
);
|
|
645
669
|
}
|
|
646
|
-
|
|
670
|
+
seenRepoPaths.add(repo.path);
|
|
647
671
|
}
|
|
648
672
|
}
|
|
649
673
|
function normalizeOptions(opts) {
|
|
@@ -658,9 +682,7 @@ function normalizeOptions(opts) {
|
|
|
658
682
|
) : void 0;
|
|
659
683
|
const installUrls = opts.installUrls ? [...new Set(opts.installUrls)] : void 0;
|
|
660
684
|
const repos = opts.repos ? Array.from(
|
|
661
|
-
new Map(
|
|
662
|
-
opts.repos.map((r) => [`${r.url}${r.name}${r.branch ?? ""}`, r])
|
|
663
|
-
).values()
|
|
685
|
+
new Map(opts.repos.map((r) => [`${r.url}${r.path}`, r])).values()
|
|
664
686
|
) : void 0;
|
|
665
687
|
return {
|
|
666
688
|
name: opts.name,
|
|
@@ -676,19 +698,6 @@ function normalizeOptions(opts) {
|
|
|
676
698
|
function needsCompose(opts) {
|
|
677
699
|
return opts.services.length > 0;
|
|
678
700
|
}
|
|
679
|
-
var SSH_AGENT_TARGET = "/ssh-agent";
|
|
680
|
-
var GIT_SSH_COMMAND = "ssh -o StrictHostKeyChecking=accept-new";
|
|
681
|
-
function buildRepoAuthMounts() {
|
|
682
|
-
return [
|
|
683
|
-
`source=\${localEnv:SSH_AUTH_SOCK},target=${SSH_AGENT_TARGET},type=bind`
|
|
684
|
-
];
|
|
685
|
-
}
|
|
686
|
-
function buildRepoAuthEnv() {
|
|
687
|
-
return {
|
|
688
|
-
SSH_AUTH_SOCK: SSH_AGENT_TARGET,
|
|
689
|
-
GIT_SSH_COMMAND
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
701
|
function resolveFeatures(opts) {
|
|
693
702
|
const resolved = [];
|
|
694
703
|
for (const langSpec of opts.languages) {
|
|
@@ -807,8 +816,6 @@ function buildDevcontainerJson(opts) {
|
|
|
807
816
|
);
|
|
808
817
|
}
|
|
809
818
|
}
|
|
810
|
-
const wantsRepoAuth = (opts.repos?.length ?? 0) > 0;
|
|
811
|
-
const repoAuthEnv = wantsRepoAuth ? { containerEnv: buildRepoAuthEnv() } : {};
|
|
812
819
|
if (needsCompose(opts)) {
|
|
813
820
|
return {
|
|
814
821
|
name: opts.name,
|
|
@@ -819,14 +826,10 @@ function buildDevcontainerJson(opts) {
|
|
|
819
826
|
remoteUser: "node",
|
|
820
827
|
forwardPorts: [3e3, 4e3],
|
|
821
828
|
postCreateCommand: ".devcontainer/post-create.sh",
|
|
822
|
-
...featuresField ?? {}
|
|
823
|
-
...repoAuthEnv
|
|
829
|
+
...featuresField ?? {}
|
|
824
830
|
};
|
|
825
831
|
}
|
|
826
|
-
const mounts = [
|
|
827
|
-
...wantsRepoAuth ? buildRepoAuthMounts() : [],
|
|
828
|
-
...homeMounts
|
|
829
|
-
];
|
|
832
|
+
const mounts = [...homeMounts];
|
|
830
833
|
const mountsField = mounts.length > 0 ? { mounts } : {};
|
|
831
834
|
return {
|
|
832
835
|
name: opts.name,
|
|
@@ -836,8 +839,7 @@ function buildDevcontainerJson(opts) {
|
|
|
836
839
|
runArgs: ["--cap-add=NET_ADMIN"],
|
|
837
840
|
forwardPorts: [3e3, 4e3],
|
|
838
841
|
postCreateCommand: ".devcontainer/post-create.sh",
|
|
839
|
-
...featuresField ?? {}
|
|
840
|
-
...repoAuthEnv
|
|
842
|
+
...featuresField ?? {}
|
|
841
843
|
};
|
|
842
844
|
}
|
|
843
845
|
function buildComposeYaml(opts) {
|
|
@@ -859,13 +861,6 @@ function buildComposeYaml(opts) {
|
|
|
859
861
|
lines.push(` - ../home/${sub}:/home/node/${sub}`);
|
|
860
862
|
}
|
|
861
863
|
}
|
|
862
|
-
const wantsRepoAuth = (opts.repos?.length ?? 0) > 0;
|
|
863
|
-
if (wantsRepoAuth) {
|
|
864
|
-
lines.push(` - \${SSH_AUTH_SOCK:-/dev/null}:${SSH_AGENT_TARGET}`);
|
|
865
|
-
lines.push(" environment:");
|
|
866
|
-
lines.push(` SSH_AUTH_SOCK: ${SSH_AGENT_TARGET}`);
|
|
867
|
-
lines.push(` GIT_SSH_COMMAND: "${GIT_SSH_COMMAND}"`);
|
|
868
|
-
}
|
|
869
864
|
for (const svcId of opts.services) {
|
|
870
865
|
const def = SERVICE_CATALOG[svcId];
|
|
871
866
|
if (!def) continue;
|
|
@@ -887,10 +882,11 @@ function buildComposeYaml(opts) {
|
|
|
887
882
|
function buildCodeWorkspaceJson(opts) {
|
|
888
883
|
const folders = [{ path: "." }];
|
|
889
884
|
const sortedRepos = [...opts.repos ?? []].sort(
|
|
890
|
-
(a, b) => a.
|
|
885
|
+
(a, b) => a.path.localeCompare(b.path)
|
|
891
886
|
);
|
|
892
887
|
for (const repo of sortedRepos) {
|
|
893
|
-
|
|
888
|
+
const label = repo.path.split("/").pop() ?? repo.path;
|
|
889
|
+
folders.push({ path: `projects/${repo.path}`, name: label });
|
|
894
890
|
}
|
|
895
891
|
return { folders };
|
|
896
892
|
}
|
|
@@ -957,22 +953,34 @@ function buildPostCreateScript(opts) {
|
|
|
957
953
|
lines.push(
|
|
958
954
|
"",
|
|
959
955
|
"# Repos managed by `monoceros add-repo`. Each entry is cloned",
|
|
960
|
-
"# into `projects/<
|
|
956
|
+
"# into `projects/<path>/` if (and only if) the directory does",
|
|
961
957
|
"# not exist yet. Existing project subfolders are left alone so",
|
|
962
|
-
"# local changes survive `monoceros apply` rebuilds.",
|
|
958
|
+
"# local changes survive `monoceros apply` rebuilds. Nested",
|
|
959
|
+
"# `<path>` (e.g. apps/web) is created via `mkdir -p` before the",
|
|
960
|
+
"# clone so the parent directories exist.",
|
|
963
961
|
"mkdir -p projects"
|
|
964
962
|
);
|
|
965
963
|
for (const repo of opts.repos) {
|
|
966
|
-
const
|
|
967
|
-
|
|
964
|
+
const parent = repo.path.includes("/") ? repo.path.slice(0, repo.path.lastIndexOf("/")) : null;
|
|
965
|
+
if (parent) {
|
|
966
|
+
lines.push(`mkdir -p "projects/${parent}"`);
|
|
967
|
+
}
|
|
968
968
|
lines.push(
|
|
969
|
-
`if [ ! -d "projects/${repo.
|
|
970
|
-
` echo "\u2192 Cloning ${repo.
|
|
971
|
-
` git clone
|
|
969
|
+
`if [ ! -d "projects/${repo.path}" ]; then`,
|
|
970
|
+
` echo "\u2192 Cloning ${repo.path} from ${repo.url}\u2026"`,
|
|
971
|
+
` git clone "${repo.url}" "projects/${repo.path}"`,
|
|
972
972
|
`else`,
|
|
973
|
-
` echo "\u2192 projects/${repo.
|
|
973
|
+
` echo "\u2192 projects/${repo.path} already exists, skipping clone"`,
|
|
974
974
|
`fi`
|
|
975
975
|
);
|
|
976
|
+
if (repo.gitUser) {
|
|
977
|
+
const safeName = repo.gitUser.name.replace(/"/g, '\\"');
|
|
978
|
+
const safeEmail = repo.gitUser.email.replace(/"/g, '\\"');
|
|
979
|
+
lines.push(
|
|
980
|
+
`git -C "projects/${repo.path}" config user.name "${safeName}"`,
|
|
981
|
+
`git -C "projects/${repo.path}" config user.email "${safeEmail}"`
|
|
982
|
+
);
|
|
983
|
+
}
|
|
976
984
|
}
|
|
977
985
|
}
|
|
978
986
|
return lines.join("\n") + "\n";
|
|
@@ -1123,25 +1131,56 @@ function addFeatureToDoc(doc, ref, options = {}) {
|
|
|
1123
1131
|
}
|
|
1124
1132
|
function addRepoToDoc(doc, repo) {
|
|
1125
1133
|
const seq = ensureSeq(doc, "repos");
|
|
1126
|
-
const repoName = repo.name ?? deriveRepoName(repo.url);
|
|
1127
1134
|
for (const item of seq.items) {
|
|
1128
1135
|
if (!isMap(item)) continue;
|
|
1129
1136
|
const url = item.get("url");
|
|
1130
1137
|
if (url !== repo.url) continue;
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1138
|
+
const existingPath = item.get("path");
|
|
1139
|
+
const effectivePath = typeof existingPath === "string" ? existingPath : deriveRepoName(url);
|
|
1140
|
+
if (effectivePath !== repo.path) continue;
|
|
1141
|
+
const existingGit = item.get("git", true);
|
|
1142
|
+
const existingUser = existingGit && isMap(existingGit) ? existingGit.get("user", true) : null;
|
|
1143
|
+
const existingName = existingUser && isMap(existingUser) ? existingUser.get("name") : null;
|
|
1144
|
+
const existingEmail = existingUser && isMap(existingUser) ? existingUser.get("email") : null;
|
|
1145
|
+
const existingGitUser = typeof existingName === "string" && typeof existingEmail === "string" ? { name: existingName, email: existingEmail } : void 0;
|
|
1146
|
+
const sameGitUser = (existingGitUser?.name ?? null) === (repo.gitUser?.name ?? null) && (existingGitUser?.email ?? null) === (repo.gitUser?.email ?? null);
|
|
1147
|
+
const existingProvider = item.get("provider");
|
|
1148
|
+
const sameProvider = (typeof existingProvider === "string" ? existingProvider : null) === (repo.provider ?? null);
|
|
1149
|
+
if (sameGitUser && sameProvider) {
|
|
1135
1150
|
return false;
|
|
1136
1151
|
}
|
|
1152
|
+
if (repo.gitUser) {
|
|
1153
|
+
const gitMap = new YAMLMap();
|
|
1154
|
+
const userMap = new YAMLMap();
|
|
1155
|
+
userMap.set("name", repo.gitUser.name);
|
|
1156
|
+
userMap.set("email", repo.gitUser.email);
|
|
1157
|
+
gitMap.set("user", userMap);
|
|
1158
|
+
item.set("git", gitMap);
|
|
1159
|
+
} else {
|
|
1160
|
+
item.delete("git");
|
|
1161
|
+
}
|
|
1162
|
+
if (repo.provider) {
|
|
1163
|
+
item.set("provider", repo.provider);
|
|
1164
|
+
} else {
|
|
1165
|
+
item.delete("provider");
|
|
1166
|
+
}
|
|
1167
|
+
return true;
|
|
1137
1168
|
}
|
|
1138
1169
|
const entry2 = new YAMLMap();
|
|
1139
1170
|
entry2.set("url", repo.url);
|
|
1140
|
-
if (repo.
|
|
1141
|
-
entry2.set("
|
|
1171
|
+
if (repo.path !== deriveRepoName(repo.url)) {
|
|
1172
|
+
entry2.set("path", repo.path);
|
|
1173
|
+
}
|
|
1174
|
+
if (repo.gitUser) {
|
|
1175
|
+
const gitMap = new YAMLMap();
|
|
1176
|
+
const userMap = new YAMLMap();
|
|
1177
|
+
userMap.set("name", repo.gitUser.name);
|
|
1178
|
+
userMap.set("email", repo.gitUser.email);
|
|
1179
|
+
gitMap.set("user", userMap);
|
|
1180
|
+
entry2.set("git", gitMap);
|
|
1142
1181
|
}
|
|
1143
|
-
if (repo.
|
|
1144
|
-
entry2.set("
|
|
1182
|
+
if (repo.provider) {
|
|
1183
|
+
entry2.set("provider", repo.provider);
|
|
1145
1184
|
}
|
|
1146
1185
|
seq.add(entry2);
|
|
1147
1186
|
return true;
|
|
@@ -1174,16 +1213,16 @@ function removeFeatureFromDoc(doc, ref) {
|
|
|
1174
1213
|
pruneEmptySeq(doc, "features");
|
|
1175
1214
|
return true;
|
|
1176
1215
|
}
|
|
1177
|
-
function removeRepoFromDoc(doc,
|
|
1216
|
+
function removeRepoFromDoc(doc, urlOrPath) {
|
|
1178
1217
|
const seq = doc.get("repos", true);
|
|
1179
1218
|
if (!seq || !isSeq(seq)) return false;
|
|
1180
1219
|
const idx = seq.items.findIndex((item) => {
|
|
1181
1220
|
if (!isMap(item)) return false;
|
|
1182
1221
|
const url = item.get("url");
|
|
1183
|
-
if (url ===
|
|
1184
|
-
const
|
|
1185
|
-
const
|
|
1186
|
-
return
|
|
1222
|
+
if (url === urlOrPath) return true;
|
|
1223
|
+
const path13 = item.get("path");
|
|
1224
|
+
const effectivePath = typeof path13 === "string" ? path13 : typeof url === "string" ? deriveRepoName(url) : void 0;
|
|
1225
|
+
return effectivePath === urlOrPath;
|
|
1187
1226
|
});
|
|
1188
1227
|
if (idx < 0) return false;
|
|
1189
1228
|
seq.items.splice(idx, 1);
|
|
@@ -1225,21 +1264,65 @@ function runAddAptPackages(input) {
|
|
|
1225
1264
|
}
|
|
1226
1265
|
return mutate(input, (doc) => addAptPackagesToDoc(doc, input.packages));
|
|
1227
1266
|
}
|
|
1228
|
-
function runAddRepo(input) {
|
|
1267
|
+
async function runAddRepo(input) {
|
|
1229
1268
|
const url = input.url.trim();
|
|
1230
1269
|
if (url.length === 0) {
|
|
1231
1270
|
throw new Error(
|
|
1232
1271
|
"Missing repo URL. Usage: monoceros add-repo <containername> <url>."
|
|
1233
1272
|
);
|
|
1234
1273
|
}
|
|
1235
|
-
const
|
|
1274
|
+
const path13 = (input.path ?? deriveRepoName(url)).trim();
|
|
1275
|
+
const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
|
|
1276
|
+
const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
|
|
1277
|
+
if (hasName !== hasEmail) {
|
|
1278
|
+
throw new Error(
|
|
1279
|
+
"--git-name and --git-email must be set together. Pass both, or neither."
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
const explicitProvider = normalizeProvider(input.provider);
|
|
1283
|
+
let host;
|
|
1284
|
+
try {
|
|
1285
|
+
host = url.startsWith("https://") ? new URL(url).hostname : void 0;
|
|
1286
|
+
} catch {
|
|
1287
|
+
host = void 0;
|
|
1288
|
+
}
|
|
1289
|
+
const canonical = host ? KNOWN_PROVIDER_HOSTS[host.toLowerCase()] : void 0;
|
|
1290
|
+
if (host && !canonical && !explicitProvider) {
|
|
1291
|
+
throw new Error(
|
|
1292
|
+
`Host '${host}' is not a canonical Git provider Monoceros can auto-detect (github.com / gitlab.com / bitbucket.org). Pass --provider=github|gitlab|bitbucket so the credential-helper hints know which CLI to suggest.`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
if (canonical && explicitProvider && explicitProvider !== canonical) {
|
|
1296
|
+
throw new Error(
|
|
1297
|
+
`--provider=${explicitProvider} contradicts host '${host}' (auto-detected as ${canonical}). Drop --provider for canonical hosts, or fix the value.`
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
|
|
1236
1301
|
const entry2 = {
|
|
1237
1302
|
url,
|
|
1238
|
-
|
|
1239
|
-
...
|
|
1303
|
+
path: path13,
|
|
1304
|
+
...hasName && hasEmail ? {
|
|
1305
|
+
gitUser: {
|
|
1306
|
+
name: input.gitName.trim(),
|
|
1307
|
+
email: input.gitEmail.trim()
|
|
1308
|
+
}
|
|
1309
|
+
} : {},
|
|
1310
|
+
...providerToWrite ? { provider: providerToWrite } : {}
|
|
1240
1311
|
};
|
|
1241
1312
|
return mutate(input, (doc) => addRepoToDoc(doc, entry2));
|
|
1242
1313
|
}
|
|
1314
|
+
function normalizeProvider(raw) {
|
|
1315
|
+
if (typeof raw !== "string") return void 0;
|
|
1316
|
+
const trimmed = raw.trim();
|
|
1317
|
+
if (trimmed.length === 0) return void 0;
|
|
1318
|
+
const lowered = trimmed.toLowerCase();
|
|
1319
|
+
if (!PROVIDER_VALUES.includes(lowered)) {
|
|
1320
|
+
throw new Error(
|
|
1321
|
+
`Invalid --provider value: ${JSON.stringify(raw)}. Allowed: ${PROVIDER_VALUES.join(", ")}.`
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
return lowered;
|
|
1325
|
+
}
|
|
1243
1326
|
function runAddFromUrl(input) {
|
|
1244
1327
|
const url = input.url.trim();
|
|
1245
1328
|
if (url.length === 0) {
|
|
@@ -1551,7 +1634,7 @@ var addRepoCommand = defineCommand4({
|
|
|
1551
1634
|
meta: {
|
|
1552
1635
|
name: "add-repo",
|
|
1553
1636
|
group: "edit",
|
|
1554
|
-
description: "Add a git repo to the container config. Cloned into projects/<
|
|
1637
|
+
description: "Add a git repo to the container config. Cloned into projects/<path>/ on container build. Idempotent \u2014 existing project subfolders are left alone. Destination path derived from URL by default; override with --path (supports nested subfolders like apps/web). Branches/PRs are git-level concerns: clone, then `git checkout` inside the container."
|
|
1555
1638
|
},
|
|
1556
1639
|
args: {
|
|
1557
1640
|
name: {
|
|
@@ -1564,13 +1647,21 @@ var addRepoCommand = defineCommand4({
|
|
|
1564
1647
|
description: "Git URL (HTTPS or SSH/git@ form). E.g. https://github.com/foo/bar.git, git@github.com:foo/bar.git.",
|
|
1565
1648
|
required: true
|
|
1566
1649
|
},
|
|
1567
|
-
|
|
1650
|
+
path: {
|
|
1651
|
+
type: "string",
|
|
1652
|
+
description: "Destination under projects/. Subfolders via `/` (e.g. apps/web). Default: URL-derived single segment (bar.git \u2192 bar)."
|
|
1653
|
+
},
|
|
1654
|
+
"git-name": {
|
|
1655
|
+
type: "string",
|
|
1656
|
+
description: "Per-repo git committer name. Overrides the container-level git.user.name for this repo only. Pair with --git-email."
|
|
1657
|
+
},
|
|
1658
|
+
"git-email": {
|
|
1568
1659
|
type: "string",
|
|
1569
|
-
description: "
|
|
1660
|
+
description: "Per-repo git committer email. Overrides the container-level git.user.email for this repo only. Pair with --git-name."
|
|
1570
1661
|
},
|
|
1571
|
-
|
|
1662
|
+
provider: {
|
|
1572
1663
|
type: "string",
|
|
1573
|
-
description: "
|
|
1664
|
+
description: "Git provider for credential-helper guidance: github | gitlab | bitbucket. Required when the URL host is not github.com, gitlab.com, or bitbucket.org \u2014 Monoceros uses this to suggest the right CLI (gh / glab / Atlassian token) on missing credentials."
|
|
1574
1665
|
},
|
|
1575
1666
|
yes: {
|
|
1576
1667
|
type: "boolean",
|
|
@@ -1584,8 +1675,10 @@ var addRepoCommand = defineCommand4({
|
|
|
1584
1675
|
const result = await runAddRepo({
|
|
1585
1676
|
name: args.name,
|
|
1586
1677
|
url: args.url,
|
|
1587
|
-
...typeof args.
|
|
1588
|
-
...typeof args
|
|
1678
|
+
...typeof args.path === "string" ? { path: args.path } : {},
|
|
1679
|
+
...typeof args["git-name"] === "string" ? { gitName: args["git-name"] } : {},
|
|
1680
|
+
...typeof args["git-email"] === "string" ? { gitEmail: args["git-email"] } : {},
|
|
1681
|
+
...typeof args.provider === "string" ? { provider: args.provider } : {},
|
|
1589
1682
|
yes: args.yes
|
|
1590
1683
|
});
|
|
1591
1684
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
@@ -1804,11 +1897,13 @@ function solutionConfigToCreateOptions(config, featureDefaults = {}) {
|
|
|
1804
1897
|
if (config.repos.length > 0) {
|
|
1805
1898
|
result.repos = config.repos.map((r) => ({
|
|
1806
1899
|
url: r.url,
|
|
1807
|
-
// `
|
|
1808
|
-
//
|
|
1809
|
-
// `
|
|
1810
|
-
|
|
1811
|
-
|
|
1900
|
+
// `path` is optional in the yml; CreateOptions requires it.
|
|
1901
|
+
// When the yml omits `path`, fall back to the URL-derived
|
|
1902
|
+
// single-segment default (`https://.../foo.git` → `foo`),
|
|
1903
|
+
// which lands the clone at `projects/foo/`.
|
|
1904
|
+
path: r.path ?? deriveRepoName(r.url),
|
|
1905
|
+
...r.git?.user ? { gitUser: { name: r.git.user.name, email: r.git.user.email } } : {},
|
|
1906
|
+
...r.provider ? { provider: r.provider } : {}
|
|
1812
1907
|
}));
|
|
1813
1908
|
}
|
|
1814
1909
|
return result;
|
|
@@ -2020,7 +2115,10 @@ async function runStart(opts) {
|
|
|
2020
2115
|
const logger = opts.logger ?? { info: (msg) => consola8.info(msg) };
|
|
2021
2116
|
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
2022
2117
|
logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
|
|
2023
|
-
return spawnFn(
|
|
2118
|
+
return spawnFn(
|
|
2119
|
+
["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
|
|
2120
|
+
opts.root
|
|
2121
|
+
);
|
|
2024
2122
|
}
|
|
2025
2123
|
async function runContainerCycle(root, opts) {
|
|
2026
2124
|
const { hasCompose, logger } = opts;
|
|
@@ -2054,7 +2152,13 @@ async function runContainerCycle(root, opts) {
|
|
|
2054
2152
|
logger.info(`Recreating image-mode devcontainer at ${root}\u2026`);
|
|
2055
2153
|
const spawnFn = opts.devcontainerSpawn ?? spawnDevcontainer;
|
|
2056
2154
|
return spawnFn(
|
|
2057
|
-
[
|
|
2155
|
+
[
|
|
2156
|
+
"up",
|
|
2157
|
+
"--workspace-folder",
|
|
2158
|
+
root,
|
|
2159
|
+
"--mount-workspace-git-root=false",
|
|
2160
|
+
"--remove-existing-container"
|
|
2161
|
+
],
|
|
2058
2162
|
root
|
|
2059
2163
|
);
|
|
2060
2164
|
}
|
|
@@ -2089,7 +2193,13 @@ import path6 from "path";
|
|
|
2089
2193
|
var realGitCredentialFill = (input) => {
|
|
2090
2194
|
return new Promise((resolve, reject) => {
|
|
2091
2195
|
const child = spawn3("git", ["credential", "fill"], {
|
|
2092
|
-
stdio: ["pipe", "pipe", "inherit"]
|
|
2196
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
2197
|
+
env: {
|
|
2198
|
+
...process.env,
|
|
2199
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
2200
|
+
GIT_ASKPASS: "",
|
|
2201
|
+
SSH_ASKPASS: ""
|
|
2202
|
+
}
|
|
2093
2203
|
});
|
|
2094
2204
|
let stdout = "";
|
|
2095
2205
|
child.stdout.on("data", (chunk) => {
|
|
@@ -2101,16 +2211,135 @@ var realGitCredentialFill = (input) => {
|
|
|
2101
2211
|
child.stdin.end();
|
|
2102
2212
|
});
|
|
2103
2213
|
};
|
|
2214
|
+
function resolveProvider(host, explicit) {
|
|
2215
|
+
const canonical = KNOWN_PROVIDER_HOSTS[host.toLowerCase()];
|
|
2216
|
+
if (canonical) return canonical;
|
|
2217
|
+
return explicit ?? "unknown";
|
|
2218
|
+
}
|
|
2104
2219
|
function uniqueHttpsHosts(repos) {
|
|
2105
|
-
const
|
|
2220
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
2106
2221
|
for (const repo of repos) {
|
|
2107
2222
|
if (!repo.url.startsWith("https://")) continue;
|
|
2223
|
+
let host;
|
|
2108
2224
|
try {
|
|
2109
|
-
|
|
2225
|
+
host = new URL(repo.url).hostname;
|
|
2110
2226
|
} catch {
|
|
2227
|
+
continue;
|
|
2111
2228
|
}
|
|
2229
|
+
if (byHost.has(host)) continue;
|
|
2230
|
+
byHost.set(host, { host, provider: resolveProvider(host, repo.provider) });
|
|
2231
|
+
}
|
|
2232
|
+
return [...byHost.values()];
|
|
2233
|
+
}
|
|
2234
|
+
function installCommandForOS(opts) {
|
|
2235
|
+
switch (process.platform) {
|
|
2236
|
+
case "darwin":
|
|
2237
|
+
return cyan2(opts.brew);
|
|
2238
|
+
case "win32":
|
|
2239
|
+
return cyan2(opts.winget);
|
|
2240
|
+
default:
|
|
2241
|
+
if (opts.linuxBrew) return cyan2(opts.linuxBrew);
|
|
2242
|
+
return `See ${opts.linuxDocsUrl} for package instructions.`;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
function providerSetupHint(host, provider) {
|
|
2246
|
+
if (provider === "github") {
|
|
2247
|
+
const isSaas = host.toLowerCase() === "github.com";
|
|
2248
|
+
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
2249
|
+
const install = installCommandForOS({
|
|
2250
|
+
brew: "brew install gh",
|
|
2251
|
+
winget: "winget install --id GitHub.cli",
|
|
2252
|
+
linuxDocsUrl: "https://github.com/cli/cli#installation"
|
|
2253
|
+
});
|
|
2254
|
+
return {
|
|
2255
|
+
title: `${host} \u2014 GitHub`,
|
|
2256
|
+
body: [
|
|
2257
|
+
"Install the GitHub CLI:",
|
|
2258
|
+
install,
|
|
2259
|
+
"",
|
|
2260
|
+
"Then run once:",
|
|
2261
|
+
cyan2(`gh auth login${hostArg}`),
|
|
2262
|
+
cyan2(`gh auth setup-git${hostArg}`),
|
|
2263
|
+
"",
|
|
2264
|
+
"`gh auth login` walks through OAuth in your browser.",
|
|
2265
|
+
"`gh auth setup-git` wires gh into git as a credential helper."
|
|
2266
|
+
].join("\n")
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
if (provider === "gitlab") {
|
|
2270
|
+
const isSaas = host.toLowerCase() === "gitlab.com";
|
|
2271
|
+
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
2272
|
+
const install = installCommandForOS({
|
|
2273
|
+
brew: "brew install glab",
|
|
2274
|
+
winget: "winget install --id GLab.GLab",
|
|
2275
|
+
linuxBrew: "brew install glab",
|
|
2276
|
+
linuxDocsUrl: "https://gitlab.com/gitlab-org/cli#installation"
|
|
2277
|
+
});
|
|
2278
|
+
return {
|
|
2279
|
+
title: `${host} \u2014 GitLab`,
|
|
2280
|
+
body: [
|
|
2281
|
+
"Install the GitLab CLI (glab):",
|
|
2282
|
+
install,
|
|
2283
|
+
"",
|
|
2284
|
+
"Then run once:",
|
|
2285
|
+
cyan2(`glab auth login${hostArg}`),
|
|
2286
|
+
"",
|
|
2287
|
+
"Choose `HTTPS` when asked for git-protocol, then accept",
|
|
2288
|
+
'"Authenticate Git with your GitLab credentials" \u2014 glab',
|
|
2289
|
+
"configures itself as the git credential helper."
|
|
2290
|
+
].join("\n")
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
if (provider === "bitbucket") {
|
|
2294
|
+
const isCloud = host.toLowerCase() === "bitbucket.org";
|
|
2295
|
+
if (isCloud) {
|
|
2296
|
+
return {
|
|
2297
|
+
title: `${host} \u2014 Bitbucket Cloud`,
|
|
2298
|
+
body: [
|
|
2299
|
+
"Bitbucket has no first-party CLI for git-credentials, so this",
|
|
2300
|
+
"is a manual one-time setup. Generate an Atlassian API token at",
|
|
2301
|
+
"https://id.atlassian.com/manage-profile/security/api-tokens",
|
|
2302
|
+
"",
|
|
2303
|
+
"Then store it via your OS credential helper:",
|
|
2304
|
+
cyan2(
|
|
2305
|
+
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-atlassian-email>\\npassword=<token>\\n'`
|
|
2306
|
+
)
|
|
2307
|
+
].join("\n")
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
return {
|
|
2311
|
+
title: `${host} \u2014 Bitbucket Data Center`,
|
|
2312
|
+
body: [
|
|
2313
|
+
"Bitbucket has no first-party CLI for git-credentials, so this",
|
|
2314
|
+
"is a manual one-time setup. Generate a personal HTTP access",
|
|
2315
|
+
`token in your Bitbucket UI: profile picture (top right on ${host})`,
|
|
2316
|
+
"\u2192 Manage account \u2192 HTTP access tokens \u2192 Create token. Give it",
|
|
2317
|
+
"at least repo-read + repo-write scopes for the repos you need.",
|
|
2318
|
+
"",
|
|
2319
|
+
"Then store it via your OS credential helper:",
|
|
2320
|
+
cyan2(
|
|
2321
|
+
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-bitbucket-username>\\npassword=<token>\\n'`
|
|
2322
|
+
)
|
|
2323
|
+
].join("\n")
|
|
2324
|
+
};
|
|
2112
2325
|
}
|
|
2113
|
-
return
|
|
2326
|
+
return {
|
|
2327
|
+
title: `${host} \u2014 Gitea`,
|
|
2328
|
+
body: [
|
|
2329
|
+
"Gitea has no first-party CLI helper for git-credentials (the",
|
|
2330
|
+
"`tea` CLI logs into its own config, not into your git credential",
|
|
2331
|
+
"helper), so this is a manual one-time setup. Generate an access",
|
|
2332
|
+
`token in your Gitea UI: profile picture (top right on ${host}) \u2192`,
|
|
2333
|
+
'Settings \u2192 Applications \u2192 "Generate New Token". Give it at',
|
|
2334
|
+
"least the `read:repository` scope (add `write:repository` if you",
|
|
2335
|
+
"need push from the container).",
|
|
2336
|
+
"",
|
|
2337
|
+
"Then store it via your OS credential helper:",
|
|
2338
|
+
cyan2(
|
|
2339
|
+
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-gitea-username>\\npassword=<token>\\n'`
|
|
2340
|
+
)
|
|
2341
|
+
].join("\n")
|
|
2342
|
+
};
|
|
2114
2343
|
}
|
|
2115
2344
|
function parseCredentialFillOutput(output) {
|
|
2116
2345
|
const result = {};
|
|
@@ -2129,17 +2358,26 @@ function formatCredentialLine(host, username, password) {
|
|
|
2129
2358
|
const encPass = encodeURIComponent(password);
|
|
2130
2359
|
return `https://${encUser}:${encPass}@${host}`;
|
|
2131
2360
|
}
|
|
2132
|
-
async function collectGitCredentials(devContainerRoot,
|
|
2361
|
+
async function collectGitCredentials(devContainerRoot, hosts, options = {}) {
|
|
2133
2362
|
const credsDir = path6.join(devContainerRoot, ".monoceros");
|
|
2134
2363
|
const credentialsPath = path6.join(credsDir, "git-credentials");
|
|
2135
|
-
const hosts = uniqueHttpsHosts(repos);
|
|
2136
2364
|
const spawnFn = options.spawn ?? realGitCredentialFill;
|
|
2137
2365
|
const logger = options.logger ?? { info: () => {
|
|
2138
2366
|
}, warn: () => {
|
|
2139
2367
|
} };
|
|
2140
2368
|
const lines = [];
|
|
2141
|
-
|
|
2142
|
-
for (const host of hosts) {
|
|
2369
|
+
const perHost = [];
|
|
2370
|
+
for (const { host, provider } of hosts) {
|
|
2371
|
+
if (provider === "unknown") {
|
|
2372
|
+
perHost.push({
|
|
2373
|
+
host,
|
|
2374
|
+
provider: "github",
|
|
2375
|
+
// placeholder — never rendered because pre-flight already bailed
|
|
2376
|
+
status: "no-credentials",
|
|
2377
|
+
detail: "provider not declared (internal: should not reach here)"
|
|
2378
|
+
});
|
|
2379
|
+
continue;
|
|
2380
|
+
}
|
|
2143
2381
|
logger.info(`Fetching credentials for ${host} from host git\u2026`);
|
|
2144
2382
|
const input = `protocol=https
|
|
2145
2383
|
host=${host}
|
|
@@ -2149,28 +2387,31 @@ host=${host}
|
|
|
2149
2387
|
try {
|
|
2150
2388
|
result = await spawnFn(input);
|
|
2151
2389
|
} catch (err) {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
);
|
|
2155
|
-
hostsSkipped += 1;
|
|
2390
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
2391
|
+
perHost.push({ host, provider, status: "spawn-error", detail });
|
|
2156
2392
|
continue;
|
|
2157
2393
|
}
|
|
2158
2394
|
if (result.exitCode !== 0) {
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2395
|
+
perHost.push({
|
|
2396
|
+
host,
|
|
2397
|
+
provider,
|
|
2398
|
+
status: "non-zero-exit",
|
|
2399
|
+
detail: `exit code ${result.exitCode}`
|
|
2400
|
+
});
|
|
2163
2401
|
continue;
|
|
2164
2402
|
}
|
|
2165
2403
|
const { username, password } = parseCredentialFillOutput(result.stdout);
|
|
2166
2404
|
if (!username || !password) {
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2405
|
+
perHost.push({
|
|
2406
|
+
host,
|
|
2407
|
+
provider,
|
|
2408
|
+
status: "no-credentials",
|
|
2409
|
+
detail: "host credential helper returned no username/password"
|
|
2410
|
+
});
|
|
2171
2411
|
continue;
|
|
2172
2412
|
}
|
|
2173
2413
|
lines.push(formatCredentialLine(host, username, password));
|
|
2414
|
+
perHost.push({ host, provider, status: "ok", detail: "" });
|
|
2174
2415
|
}
|
|
2175
2416
|
await fs6.mkdir(credsDir, { recursive: true });
|
|
2176
2417
|
await fs6.writeFile(
|
|
@@ -2182,19 +2423,204 @@ host=${host}
|
|
|
2182
2423
|
);
|
|
2183
2424
|
return {
|
|
2184
2425
|
hostsWritten: lines.length,
|
|
2185
|
-
hostsSkipped,
|
|
2426
|
+
hostsSkipped: perHost.filter((p) => p.status !== "ok").length,
|
|
2427
|
+
perHost,
|
|
2186
2428
|
credentialsPath
|
|
2187
2429
|
};
|
|
2188
2430
|
}
|
|
2431
|
+
function formatMissingCredentialsError(missing) {
|
|
2432
|
+
if (missing.length === 1) {
|
|
2433
|
+
const m = missing[0];
|
|
2434
|
+
const hint = providerSetupHint(m.host, m.provider);
|
|
2435
|
+
return [
|
|
2436
|
+
`Missing Git credentials: ${hint.title}`,
|
|
2437
|
+
"",
|
|
2438
|
+
hint.body,
|
|
2439
|
+
"",
|
|
2440
|
+
`Then re-run ${cyan2("monoceros apply")}.`
|
|
2441
|
+
].join("\n");
|
|
2442
|
+
}
|
|
2443
|
+
const lines = [
|
|
2444
|
+
`Missing Git credentials for ${missing.length} hosts:`,
|
|
2445
|
+
""
|
|
2446
|
+
];
|
|
2447
|
+
for (const m of missing) {
|
|
2448
|
+
const hint = providerSetupHint(m.host, m.provider);
|
|
2449
|
+
lines.push(hint.title);
|
|
2450
|
+
lines.push("");
|
|
2451
|
+
lines.push(hint.body);
|
|
2452
|
+
lines.push("");
|
|
2453
|
+
}
|
|
2454
|
+
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
2455
|
+
return lines.join("\n");
|
|
2456
|
+
}
|
|
2457
|
+
function formatUnknownProviderError(hosts) {
|
|
2458
|
+
const sorted = [...new Set(hosts)].sort();
|
|
2459
|
+
const lines = [
|
|
2460
|
+
sorted.length === 1 ? `Unknown Git provider for host ${sorted[0]}.` : `Unknown Git provider for ${sorted.length} hosts: ${sorted.join(", ")}.`,
|
|
2461
|
+
"",
|
|
2462
|
+
"Monoceros auto-detects only github.com / gitlab.com / bitbucket.org.",
|
|
2463
|
+
"For any other host (self-hosted GitLab, Gitea, Bitbucket Server, \u2026)",
|
|
2464
|
+
"declare the provider explicitly in the yml. Edit the repo entry:",
|
|
2465
|
+
"",
|
|
2466
|
+
cyan2(" repos:"),
|
|
2467
|
+
cyan2(` - url: https://${sorted[0]}/\u2026`),
|
|
2468
|
+
cyan2(" provider: gitlab # or: github, bitbucket, gitea"),
|
|
2469
|
+
"",
|
|
2470
|
+
`Or re-add with ${cyan2("monoceros add-repo <name> <url> --provider=<github|gitlab|bitbucket|gitea>")}.`
|
|
2471
|
+
];
|
|
2472
|
+
return lines.join("\n");
|
|
2473
|
+
}
|
|
2189
2474
|
|
|
2190
|
-
// src/devcontainer/
|
|
2475
|
+
// src/devcontainer/repo-reachability.ts
|
|
2191
2476
|
import { spawn as spawn4 } from "child_process";
|
|
2477
|
+
var realGitLsRemote = (url) => {
|
|
2478
|
+
return new Promise((resolve, reject) => {
|
|
2479
|
+
const child = spawn4("git", ["ls-remote", "--heads", "--", url], {
|
|
2480
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2481
|
+
env: {
|
|
2482
|
+
...process.env,
|
|
2483
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
2484
|
+
GIT_ASKPASS: "",
|
|
2485
|
+
SSH_ASKPASS: ""
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
let stdout = "";
|
|
2489
|
+
let stderr = "";
|
|
2490
|
+
child.stdout.on("data", (chunk) => {
|
|
2491
|
+
stdout += chunk.toString();
|
|
2492
|
+
});
|
|
2493
|
+
child.stderr.on("data", (chunk) => {
|
|
2494
|
+
stderr += chunk.toString();
|
|
2495
|
+
});
|
|
2496
|
+
child.on("error", reject);
|
|
2497
|
+
child.on(
|
|
2498
|
+
"exit",
|
|
2499
|
+
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
2500
|
+
);
|
|
2501
|
+
});
|
|
2502
|
+
};
|
|
2503
|
+
function classifyStderr(stderr) {
|
|
2504
|
+
const s = stderr.toLowerCase();
|
|
2505
|
+
if (s.includes("could not resolve host") || s.includes("name or service not known") || s.includes("temporary failure in name resolution") || s.includes("no address associated with hostname")) {
|
|
2506
|
+
return "dns";
|
|
2507
|
+
}
|
|
2508
|
+
if (s.includes("repository not found") || s.includes("may not have access") || s.includes("no longer exists") || s.includes("don't have permission") || s.includes("could not be found") || s.includes("the requested url returned error: 404")) {
|
|
2509
|
+
return "not-found-or-no-access";
|
|
2510
|
+
}
|
|
2511
|
+
if (s.includes("authentication failed") || s.includes("could not read username") || s.includes("incorrect username or password") || s.includes("the requested url returned error: 401") || s.includes("the requested url returned error: 403")) {
|
|
2512
|
+
return "auth-failed";
|
|
2513
|
+
}
|
|
2514
|
+
return "unknown";
|
|
2515
|
+
}
|
|
2516
|
+
async function checkRepoReachability(repos, options = {}) {
|
|
2517
|
+
const spawnFn = options.spawn ?? realGitLsRemote;
|
|
2518
|
+
const results = [];
|
|
2519
|
+
for (const repo of repos) {
|
|
2520
|
+
let result;
|
|
2521
|
+
try {
|
|
2522
|
+
result = await spawnFn(repo.url);
|
|
2523
|
+
} catch (err) {
|
|
2524
|
+
results.push({
|
|
2525
|
+
url: repo.url,
|
|
2526
|
+
ok: false,
|
|
2527
|
+
kind: "unknown",
|
|
2528
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
2529
|
+
});
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
if (result.exitCode === 0) {
|
|
2533
|
+
results.push({ url: repo.url, ok: true, detail: "" });
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
results.push({
|
|
2537
|
+
url: repo.url,
|
|
2538
|
+
ok: false,
|
|
2539
|
+
kind: classifyStderr(result.stderr),
|
|
2540
|
+
detail: result.stderr.trim()
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
return results;
|
|
2544
|
+
}
|
|
2545
|
+
function formatUnreachableReposError(failures) {
|
|
2546
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
2547
|
+
for (const f of failures) {
|
|
2548
|
+
const kind = f.kind ?? "unknown";
|
|
2549
|
+
const list = byKind.get(kind) ?? [];
|
|
2550
|
+
list.push(f);
|
|
2551
|
+
byKind.set(kind, list);
|
|
2552
|
+
}
|
|
2553
|
+
const totalUrls = failures.length;
|
|
2554
|
+
const lines = [
|
|
2555
|
+
totalUrls === 1 ? `Cannot reach declared repo: ${failures[0].url}` : `Cannot reach ${totalUrls} declared repos:`,
|
|
2556
|
+
""
|
|
2557
|
+
];
|
|
2558
|
+
const sectionOrder = [
|
|
2559
|
+
"not-found-or-no-access",
|
|
2560
|
+
"auth-failed",
|
|
2561
|
+
"dns",
|
|
2562
|
+
"unknown"
|
|
2563
|
+
];
|
|
2564
|
+
for (const kind of sectionOrder) {
|
|
2565
|
+
const entries = byKind.get(kind);
|
|
2566
|
+
if (!entries || entries.length === 0) continue;
|
|
2567
|
+
lines.push(headerForKind(kind));
|
|
2568
|
+
for (const e of entries) {
|
|
2569
|
+
lines.push(` \u2022 ${e.url}`);
|
|
2570
|
+
}
|
|
2571
|
+
for (const advice of adviceForKind(kind)) {
|
|
2572
|
+
lines.push(` - ${advice}`);
|
|
2573
|
+
}
|
|
2574
|
+
lines.push("");
|
|
2575
|
+
}
|
|
2576
|
+
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
2577
|
+
return lines.join("\n");
|
|
2578
|
+
}
|
|
2579
|
+
function headerForKind(kind) {
|
|
2580
|
+
switch (kind) {
|
|
2581
|
+
case "not-found-or-no-access":
|
|
2582
|
+
return "Repository not found (or your credentials don't grant access):";
|
|
2583
|
+
case "auth-failed":
|
|
2584
|
+
return "Authentication failed (credentials are present but rejected):";
|
|
2585
|
+
case "dns":
|
|
2586
|
+
return "Host unreachable (DNS / VPN / offline \u2014 git couldn't resolve the hostname):";
|
|
2587
|
+
case "unknown":
|
|
2588
|
+
return "Unrecognised git error:";
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
function adviceForKind(kind) {
|
|
2592
|
+
switch (kind) {
|
|
2593
|
+
case "not-found-or-no-access":
|
|
2594
|
+
return [
|
|
2595
|
+
"Re-check the URL for typos (case-sensitive on most hosts).",
|
|
2596
|
+
"Verify the repo still exists / is not archived in a way that hides it.",
|
|
2597
|
+
"Ensure your token covers this org / workspace and has read scope (GitHub: `repo`; GitLab: `read_repository`; Bitbucket: repo read)."
|
|
2598
|
+
];
|
|
2599
|
+
case "auth-failed":
|
|
2600
|
+
return [
|
|
2601
|
+
"Token may be expired or revoked \u2014 regenerate it from the provider UI.",
|
|
2602
|
+
"Re-run the provider CLI login (gh auth login / glab auth login) \u2014 Monoceros picks up the refreshed token on the next apply."
|
|
2603
|
+
];
|
|
2604
|
+
case "dns":
|
|
2605
|
+
return [
|
|
2606
|
+
"Check your internet / VPN \u2014 corporate Git hosts often require VPN.",
|
|
2607
|
+
"Verify the hostname spelling in the yml."
|
|
2608
|
+
];
|
|
2609
|
+
case "unknown":
|
|
2610
|
+
return [
|
|
2611
|
+
"Run `git ls-remote <url>` manually on the host to see the raw error."
|
|
2612
|
+
];
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// src/devcontainer/identity.ts
|
|
2617
|
+
import { spawn as spawn5 } from "child_process";
|
|
2192
2618
|
import { promises as fs7 } from "fs";
|
|
2193
2619
|
import path7 from "path";
|
|
2194
2620
|
import { consola as consola9 } from "consola";
|
|
2195
2621
|
var realGitConfigGet = (key) => {
|
|
2196
2622
|
return new Promise((resolve, reject) => {
|
|
2197
|
-
const child =
|
|
2623
|
+
const child = spawn5("git", ["config", "--global", "--get", key], {
|
|
2198
2624
|
stdio: ["ignore", "pipe", "inherit"]
|
|
2199
2625
|
});
|
|
2200
2626
|
let stdout = "";
|
|
@@ -2352,11 +2778,30 @@ ${sectionLine(label)}
|
|
|
2352
2778
|
...globalConfig?.defaults?.git?.user ? { defaults: globalConfig.defaults.git.user } : {},
|
|
2353
2779
|
logger: idLogger
|
|
2354
2780
|
});
|
|
2355
|
-
|
|
2356
|
-
|
|
2781
|
+
const hostsToFetch = uniqueHttpsHosts(createOpts.repos ?? []);
|
|
2782
|
+
const unknownProviderHosts = hostsToFetch.filter((h) => h.provider === "unknown").map((h) => h.host);
|
|
2783
|
+
if (unknownProviderHosts.length > 0) {
|
|
2784
|
+
throw new Error(formatUnknownProviderError(unknownProviderHosts));
|
|
2785
|
+
}
|
|
2786
|
+
if (hostsToFetch.length > 0) {
|
|
2787
|
+
const credResult = await collectGitCredentials(targetDir, hostsToFetch, {
|
|
2357
2788
|
...opts.credentialsSpawn ? { spawn: opts.credentialsSpawn } : {},
|
|
2358
2789
|
logger: idLogger
|
|
2359
2790
|
});
|
|
2791
|
+
const missing = credResult.perHost.filter((p) => p.status !== "ok");
|
|
2792
|
+
if (missing.length > 0) {
|
|
2793
|
+
throw new Error(formatMissingCredentialsError(missing));
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
const declaredRepos = createOpts.repos ?? [];
|
|
2797
|
+
if (declaredRepos.length > 0) {
|
|
2798
|
+
const reachability = await checkRepoReachability(declaredRepos, {
|
|
2799
|
+
...opts.reachabilitySpawn ? { spawn: opts.reachabilitySpawn } : {}
|
|
2800
|
+
});
|
|
2801
|
+
const unreachable = reachability.filter((r) => !r.ok);
|
|
2802
|
+
if (unreachable.length > 0) {
|
|
2803
|
+
throw new Error(formatUnreachableReposError(unreachable));
|
|
2804
|
+
}
|
|
2360
2805
|
}
|
|
2361
2806
|
section("Scaffold");
|
|
2362
2807
|
await fs8.mkdir(targetDir, { recursive: true });
|
|
@@ -2862,7 +3307,7 @@ var SCHEMA_HEADER = [
|
|
|
2862
3307
|
"# under `features:` also accepts options not shown here \u2014 check",
|
|
2863
3308
|
"# the feature's `devcontainer-feature.json` for the full list."
|
|
2864
3309
|
];
|
|
2865
|
-
function generateComposedYml(name, components, lookupManifest) {
|
|
3310
|
+
function generateComposedYml(name, components, lookupManifest, repoUrls = []) {
|
|
2866
3311
|
const merged = mergeComponents(components);
|
|
2867
3312
|
const lines = [];
|
|
2868
3313
|
for (const h of SCHEMA_HEADER) lines.push(h);
|
|
@@ -2893,9 +3338,17 @@ function generateComposedYml(name, components, lookupManifest) {
|
|
|
2893
3338
|
}
|
|
2894
3339
|
lines.push("");
|
|
2895
3340
|
}
|
|
3341
|
+
if (repoUrls.length > 0) {
|
|
3342
|
+
renderReposBlock(
|
|
3343
|
+
lines,
|
|
3344
|
+
repoUrls,
|
|
3345
|
+
/* commented */
|
|
3346
|
+
false
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
2896
3349
|
return ensureTrailingNewline(lines.join("\n"));
|
|
2897
3350
|
}
|
|
2898
|
-
function generateDocumentedYml(name, catalog, lookupManifest) {
|
|
3351
|
+
function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = []) {
|
|
2899
3352
|
const byCategory = groupByCategory(catalog);
|
|
2900
3353
|
const lines = [];
|
|
2901
3354
|
for (const h of SCHEMA_HEADER) lines.push(h);
|
|
@@ -2996,6 +3449,12 @@ function generateDocumentedYml(name, catalog, lookupManifest) {
|
|
|
2996
3449
|
}
|
|
2997
3450
|
lines.push("");
|
|
2998
3451
|
}
|
|
3452
|
+
renderReposBlock(
|
|
3453
|
+
lines,
|
|
3454
|
+
repoUrls,
|
|
3455
|
+
/* commented */
|
|
3456
|
+
repoUrls.length === 0
|
|
3457
|
+
);
|
|
2999
3458
|
return ensureTrailingNewline(lines.join("\n"));
|
|
3000
3459
|
}
|
|
3001
3460
|
var COMMENT_WIDTH = 72;
|
|
@@ -3051,6 +3510,57 @@ function emitHint(out, hint, description, linePrefix) {
|
|
|
3051
3510
|
}
|
|
3052
3511
|
out.push(`${linePrefix}${hint}:`);
|
|
3053
3512
|
}
|
|
3513
|
+
function renderReposBlock(out, urls, commented) {
|
|
3514
|
+
out.push("# Repos \u2014 git repositories cloned into projects/ during");
|
|
3515
|
+
out.push("# post-create. HTTPS-only (ADR 0006). Provider auto-detected");
|
|
3516
|
+
out.push("# for github.com, gitlab.com, bitbucket.org; for any other host");
|
|
3517
|
+
out.push("# (self-hosted GitLab, Bitbucket Data Center, Gitea/Forgejo,");
|
|
3518
|
+
out.push("# GitHub Enterprise, \u2026) declare provider explicitly.");
|
|
3519
|
+
out.push("#");
|
|
3520
|
+
if (commented) {
|
|
3521
|
+
out.push("# repos:");
|
|
3522
|
+
out.push("# - url: https://github.com/<org>/<repo>.git");
|
|
3523
|
+
out.push(
|
|
3524
|
+
"# # path: <folder> # subfolder under projects/; default: URL-derived"
|
|
3525
|
+
);
|
|
3526
|
+
out.push(
|
|
3527
|
+
"# # provider: github # github | gitlab | bitbucket | gitea"
|
|
3528
|
+
);
|
|
3529
|
+
out.push(
|
|
3530
|
+
"# # git: # per-repo committer identity override"
|
|
3531
|
+
);
|
|
3532
|
+
out.push("# # user:");
|
|
3533
|
+
out.push("# # name: Your Name");
|
|
3534
|
+
out.push("# # email: you@example.com");
|
|
3535
|
+
out.push("");
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
out.push("repos:");
|
|
3539
|
+
for (const url of urls) {
|
|
3540
|
+
const derivedPath = deriveDefaultPath(url);
|
|
3541
|
+
out.push(` - url: ${url}`);
|
|
3542
|
+
out.push(
|
|
3543
|
+
` # path: ${derivedPath} # subfolder under projects/; default: URL-derived (${derivedPath})`
|
|
3544
|
+
);
|
|
3545
|
+
out.push(
|
|
3546
|
+
" # provider: github # github | gitlab | bitbucket | gitea"
|
|
3547
|
+
);
|
|
3548
|
+
out.push(
|
|
3549
|
+
" # git: # per-repo committer identity override"
|
|
3550
|
+
);
|
|
3551
|
+
out.push(" # user:");
|
|
3552
|
+
out.push(" # name: Your Name");
|
|
3553
|
+
out.push(" # email: you@example.com");
|
|
3554
|
+
}
|
|
3555
|
+
out.push("");
|
|
3556
|
+
}
|
|
3557
|
+
function deriveDefaultPath(url) {
|
|
3558
|
+
let last = url;
|
|
3559
|
+
const slash = url.lastIndexOf("/");
|
|
3560
|
+
if (slash >= 0) last = url.slice(slash + 1);
|
|
3561
|
+
if (last.endsWith(".git")) last = last.slice(0, -4);
|
|
3562
|
+
return last || "repo";
|
|
3563
|
+
}
|
|
3054
3564
|
function wrapToComment(text, width) {
|
|
3055
3565
|
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
3056
3566
|
if (words.length === 0) return [""];
|
|
@@ -3175,13 +3685,45 @@ async function runInit(opts) {
|
|
|
3175
3685
|
}
|
|
3176
3686
|
const checkoutRoot = opts.workbenchRoot ?? workbenchCheckoutRoot();
|
|
3177
3687
|
const lookup = (ref) => loadFeatureManifestSummary(ref, checkoutRoot);
|
|
3688
|
+
const reposRaw = (opts.withRepo ?? []).map((u) => u.trim()).filter((u) => u.length > 0);
|
|
3689
|
+
const repos = [];
|
|
3690
|
+
const seenRepoUrls = /* @__PURE__ */ new Set();
|
|
3691
|
+
for (const url of reposRaw) {
|
|
3692
|
+
if (seenRepoUrls.has(url)) continue;
|
|
3693
|
+
seenRepoUrls.add(url);
|
|
3694
|
+
repos.push(url);
|
|
3695
|
+
}
|
|
3696
|
+
if (repos.length > 0) {
|
|
3697
|
+
const offending = [];
|
|
3698
|
+
for (const url of repos) {
|
|
3699
|
+
let host;
|
|
3700
|
+
try {
|
|
3701
|
+
host = url.startsWith("https://") ? new URL(url).hostname : void 0;
|
|
3702
|
+
} catch {
|
|
3703
|
+
host = void 0;
|
|
3704
|
+
}
|
|
3705
|
+
if (!host || !KNOWN_PROVIDER_HOSTS[host.toLowerCase()]) {
|
|
3706
|
+
offending.push(url);
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
if (offending.length > 0) {
|
|
3710
|
+
throw new Error(
|
|
3711
|
+
[
|
|
3712
|
+
`--with-repo only supports github.com / gitlab.com / bitbucket.org URLs.`,
|
|
3713
|
+
`These are not canonical: ${offending.join(", ")}`,
|
|
3714
|
+
`For other hosts, run \`monoceros init <name>\` first, then`,
|
|
3715
|
+
`\`monoceros add-repo <name> <url> --provider=github|gitlab|bitbucket\`.`
|
|
3716
|
+
].join("\n")
|
|
3717
|
+
);
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3178
3720
|
let text;
|
|
3179
3721
|
const requested = opts.with ?? [];
|
|
3180
3722
|
if (requested.length === 0) {
|
|
3181
|
-
text = generateDocumentedYml(opts.name, catalog, lookup);
|
|
3723
|
+
text = generateDocumentedYml(opts.name, catalog, lookup, repos);
|
|
3182
3724
|
} else {
|
|
3183
3725
|
const components = resolveComponents(catalog, requested);
|
|
3184
|
-
text = generateComposedYml(opts.name, components, lookup);
|
|
3726
|
+
text = generateComposedYml(opts.name, components, lookup, repos);
|
|
3185
3727
|
}
|
|
3186
3728
|
await fs10.mkdir(containerConfigsDir(home), { recursive: true });
|
|
3187
3729
|
await fs10.writeFile(dest, text, "utf8");
|
|
@@ -3219,14 +3761,21 @@ var initCommand = defineCommand9({
|
|
|
3219
3761
|
type: "string",
|
|
3220
3762
|
description: "Comma-separated list of component names to compose, e.g. 'node,postgres,github,claude'. Sub-components use a slash, e.g. 'atlassian/twg'. When omitted, init writes a documented default with every catalog component commented out.",
|
|
3221
3763
|
required: false
|
|
3764
|
+
},
|
|
3765
|
+
"with-repo": {
|
|
3766
|
+
type: "string",
|
|
3767
|
+
description: "Git URL of a repo to clone into projects/ on first apply. Repeatable: pass --with-repo=URL1 --with-repo=URL2 for multiple repos. Folder name derived from URL (foo.git \u2192 projects/foo/); use `monoceros add-repo --path=...` post-init for subfolder paths.",
|
|
3768
|
+
required: false
|
|
3222
3769
|
}
|
|
3223
3770
|
},
|
|
3224
3771
|
async run({ args, rawArgs }) {
|
|
3225
3772
|
try {
|
|
3226
3773
|
const withList = collectWithList(args.with, rawArgs);
|
|
3774
|
+
const withRepoList = collectWithRepoList(rawArgs);
|
|
3227
3775
|
await runInit({
|
|
3228
3776
|
name: args.name,
|
|
3229
|
-
...withList ? { with: withList } : {}
|
|
3777
|
+
...withList ? { with: withList } : {},
|
|
3778
|
+
...withRepoList.length > 0 ? { withRepo: withRepoList } : {}
|
|
3230
3779
|
});
|
|
3231
3780
|
} catch (err) {
|
|
3232
3781
|
consola13.error(err instanceof Error ? err.message : String(err));
|
|
@@ -3234,6 +3783,22 @@ var initCommand = defineCommand9({
|
|
|
3234
3783
|
}
|
|
3235
3784
|
}
|
|
3236
3785
|
});
|
|
3786
|
+
function collectWithRepoList(rawArgs) {
|
|
3787
|
+
const urls = [];
|
|
3788
|
+
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
3789
|
+
const t = rawArgs[i];
|
|
3790
|
+
if (t === "--with-repo") {
|
|
3791
|
+
const next = rawArgs[i + 1];
|
|
3792
|
+
if (typeof next === "string" && !next.startsWith("-")) {
|
|
3793
|
+
urls.push(next);
|
|
3794
|
+
i += 1;
|
|
3795
|
+
}
|
|
3796
|
+
} else if (t.startsWith("--with-repo=")) {
|
|
3797
|
+
urls.push(t.slice("--with-repo=".length));
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
return urls;
|
|
3801
|
+
}
|
|
3237
3802
|
function collectWithList(withArg, rawArgs) {
|
|
3238
3803
|
if (typeof withArg !== "string" || withArg.trim().length === 0) {
|
|
3239
3804
|
return void 0;
|
|
@@ -3881,14 +4446,22 @@ async function runShell(opts) {
|
|
|
3881
4446
|
assertContainerExists(opts.root);
|
|
3882
4447
|
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
3883
4448
|
const upCode = await spawnFn(
|
|
3884
|
-
["up", "--workspace-folder", opts.root],
|
|
4449
|
+
["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
|
|
3885
4450
|
opts.root,
|
|
3886
4451
|
{ quiet: true }
|
|
3887
4452
|
);
|
|
3888
4453
|
if (upCode !== 0) return upCode;
|
|
3889
|
-
return spawnFn(
|
|
3890
|
-
|
|
3891
|
-
|
|
4454
|
+
return spawnFn(
|
|
4455
|
+
[
|
|
4456
|
+
"exec",
|
|
4457
|
+
"--workspace-folder",
|
|
4458
|
+
opts.root,
|
|
4459
|
+
"--mount-workspace-git-root=false",
|
|
4460
|
+
"bash"
|
|
4461
|
+
],
|
|
4462
|
+
opts.root,
|
|
4463
|
+
{ interactive: true }
|
|
4464
|
+
);
|
|
3892
4465
|
}
|
|
3893
4466
|
function assertContainerExists(root) {
|
|
3894
4467
|
if (!existsSync10(path12.join(root, ".devcontainer"))) {
|
|
@@ -3908,13 +4481,19 @@ async function runInContainer(opts) {
|
|
|
3908
4481
|
assertContainerExists(opts.root);
|
|
3909
4482
|
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
3910
4483
|
const upCode = await spawnFn(
|
|
3911
|
-
["up", "--workspace-folder", opts.root],
|
|
4484
|
+
["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
|
|
3912
4485
|
opts.root,
|
|
3913
4486
|
{ quiet: true }
|
|
3914
4487
|
);
|
|
3915
4488
|
if (upCode !== 0) return upCode;
|
|
3916
4489
|
return spawnFn(
|
|
3917
|
-
[
|
|
4490
|
+
[
|
|
4491
|
+
"exec",
|
|
4492
|
+
"--workspace-folder",
|
|
4493
|
+
opts.root,
|
|
4494
|
+
"--mount-workspace-git-root=false",
|
|
4495
|
+
...opts.command
|
|
4496
|
+
],
|
|
3918
4497
|
opts.root,
|
|
3919
4498
|
{ interactive: true }
|
|
3920
4499
|
);
|