Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc7b58f15b | |||
| c4025cd2f7 | |||
| ee1e07bb5e | |||
| 91af3dca86 | |||
| baa355bee4 |
@@ -14,6 +14,8 @@
|
|||||||
* the route answers before the actual `git worktree add` runs, so this
|
* the route answers before the actual `git worktree add` runs, so this
|
||||||
* component polls `GET /api/lanes/:id` until that finishes before running the
|
* component polls `GET /api/lanes/:id` until that finishes before running the
|
||||||
* auto-setup calls against a cwd that must actually exist on disk first.
|
* auto-setup calls against a cwd that must actually exist on disk first.
|
||||||
|
* An optional "Create/update readme file" checkbox starts the lane's terminal
|
||||||
|
* run right after creation with `/sg-all:sg-md-writing` as its first message.
|
||||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -94,6 +96,7 @@ export function AddLaneModal({
|
|||||||
const [branches, setBranches] = useState<string[] | null>(null);
|
const [branches, setBranches] = useState<string[] | null>(null);
|
||||||
const [base, setBase] = useState("");
|
const [base, setBase] = useState("");
|
||||||
const [pipeline, setPipeline] = useState("default");
|
const [pipeline, setPipeline] = useState("default");
|
||||||
|
const [writeDocs, setWriteDocs] = useState(false);
|
||||||
const [pipelines, setPipelines] = useState<{ id: string; name: string; nodes: unknown[] }[]>([]);
|
const [pipelines, setPipelines] = useState<{ id: string; name: string; nodes: unknown[] }[]>([]);
|
||||||
const [branchesError, setBranchesError] = useState<string | null>(null);
|
const [branchesError, setBranchesError] = useState<string | null>(null);
|
||||||
const [tool, setTool] = useState<LaneTool>("claude");
|
const [tool, setTool] = useState<LaneTool>("claude");
|
||||||
@@ -114,6 +117,7 @@ export function AddLaneModal({
|
|||||||
setBranches(null);
|
setBranches(null);
|
||||||
setBase("");
|
setBase("");
|
||||||
setPipeline("default");
|
setPipeline("default");
|
||||||
|
setWriteDocs(false);
|
||||||
setTool("claude");
|
setTool("claude");
|
||||||
setBranchesError(null);
|
setBranchesError(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -177,6 +181,14 @@ export function AddLaneModal({
|
|||||||
return () => window.clearTimeout(timer);
|
return () => window.clearTimeout(timer);
|
||||||
}, [mode, sourceRepo, t]);
|
}, [mode, sourceRepo, t]);
|
||||||
|
|
||||||
|
// The checkbox below runs the docs skill by opening the lane's own terminal
|
||||||
|
// run with the slash command as its first message — a skill only exists
|
||||||
|
// inside a CLI session, so there is nothing server-side to call instead.
|
||||||
|
// Claude-only: codex has no slash commands (same reason LaneConsolePane
|
||||||
|
// prefills prose for a codex lane).
|
||||||
|
const startDocsRun = (laneId: number, cwd: string) =>
|
||||||
|
api.run.start({ laneId, cwd, initialPrompt: "/sg-all:sg-md-writing" });
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const repo = sourceRepo.trim();
|
const repo = sourceRepo.trim();
|
||||||
const name = title.trim();
|
const name = title.trim();
|
||||||
@@ -192,6 +204,7 @@ export function AddLaneModal({
|
|||||||
pipeline,
|
pipeline,
|
||||||
tool,
|
tool,
|
||||||
});
|
});
|
||||||
|
if (writeDocs && tool === "claude") await startDocsRun(result.lane.id, result.lane.cwd);
|
||||||
onAdded(result.lane);
|
onAdded(result.lane);
|
||||||
reset();
|
reset();
|
||||||
onClose();
|
onClose();
|
||||||
@@ -242,6 +255,10 @@ export function AddLaneModal({
|
|||||||
mcp: mcpOutcome.status === "fulfilled" ? "ok" : "failed",
|
mcp: mcpOutcome.status === "fulfilled" ? "ok" : "failed",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (writeDocs && tool === "claude") {
|
||||||
|
await startDocsRun(result.lane.id, provisionedLane.cwd);
|
||||||
|
}
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
console.info("[add-lane] auto-setup result:", {
|
console.info("[add-lane] auto-setup result:", {
|
||||||
profile:
|
profile:
|
||||||
@@ -390,6 +407,38 @@ export function AddLaneModal({
|
|||||||
<p className="mt-1 text-[10px] text-fg-muted">{t("addLanePipelineHint")}</p>
|
<p className="mt-1 text-[10px] text-fg-muted">{t("addLanePipelineHint")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{tool === "claude" && (
|
||||||
|
<label
|
||||||
|
// Card that lights up when ticked, matching LaneStripCard's
|
||||||
|
// selected/unselected treatment.
|
||||||
|
className={`flex cursor-pointer items-start gap-2.5 rounded-lg border p-3 shadow-sm transition-colors ${
|
||||||
|
writeDocs
|
||||||
|
? "border-accent bg-accent/10"
|
||||||
|
: "border-border bg-surface-2 hover:border-border-light hover:bg-surface-3"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
// The wrapping label also holds the hint, so name the input
|
||||||
|
// itself — otherwise its accessible name is label + hint.
|
||||||
|
aria-label={t("addLaneWriteDocsLabel")}
|
||||||
|
checked={writeDocs}
|
||||||
|
onChange={(e) => setWriteDocs(e.target.checked)}
|
||||||
|
className="mt-0.5 h-3.5 w-3.5 shrink-0 accent-accent"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
className={`block text-xs font-medium ${writeDocs ? "text-accent" : "text-fg-secondary"}`}
|
||||||
|
>
|
||||||
|
{t("addLaneWriteDocsLabel")}
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 block text-[10px] text-fg-muted">
|
||||||
|
{t("addLaneWriteDocsHint")}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
{mode === "worktree" && branches && (
|
{mode === "worktree" && branches && (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-fg-secondary" htmlFor="add-lane-base">
|
<label className="mb-1 block text-xs text-fg-secondary" htmlFor="add-lane-base">
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ vi.mock("../../../lib/api", () => ({
|
|||||||
agentsInstall: vi.fn(),
|
agentsInstall: vi.fn(),
|
||||||
mcpSync: vi.fn(),
|
mcpSync: vi.fn(),
|
||||||
},
|
},
|
||||||
|
run: { start: vi.fn() },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -398,6 +399,42 @@ describe("AddLaneModal — folder browse", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("AddLaneModal — create/update readme", () => {
|
||||||
|
it("opens the new lane's terminal on the docs skill only when the box is ticked", async () => {
|
||||||
|
vi.mocked(api.run.start)
|
||||||
|
.mockReset()
|
||||||
|
.mockResolvedValue({} as never);
|
||||||
|
vi.mocked(api.lanes.ensure).mockResolvedValue({
|
||||||
|
lane: laneFixture({ status: "idle", cwd: "/Users/tester/projects/repo" }),
|
||||||
|
created: true,
|
||||||
|
});
|
||||||
|
renderModal();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Repo" }));
|
||||||
|
const repoField = screen.getByLabelText("Directory");
|
||||||
|
await focusField(user, repoField);
|
||||||
|
await user.type(repoField, "/Users/tester/projects/repo");
|
||||||
|
await user.click(screen.getByLabelText("Create/update readme file"));
|
||||||
|
await user.click(screen.getByRole("button", { name: "Add lane" }));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(api.run.start).toHaveBeenCalledWith({
|
||||||
|
laneId: 9,
|
||||||
|
cwd: "/Users/tester/projects/repo",
|
||||||
|
initialPrompt: "/sg-all:sg-md-writing",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is hidden for a codex lane, which has no slash commands", async () => {
|
||||||
|
renderModal();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
await user.click(screen.getByRole("button", { name: "Codex" }));
|
||||||
|
expect(screen.queryByLabelText("Create/update readme file")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("AddLaneModal — pipeline template", () => {
|
describe("AddLaneModal — pipeline template", () => {
|
||||||
it("offers every template the server reports and sends the chosen one when adopting a repo", async () => {
|
it("offers every template the server reports and sends the chosen one when adopting a repo", async () => {
|
||||||
// Creation is the ONLY point the UI can set a template, so a lane born on
|
// Creation is the ONLY point the UI can set a template, so a lane born on
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
"addLaneToolClaude": "Claude Code",
|
"addLaneToolClaude": "Claude Code",
|
||||||
"addLaneToolCodex": "Codex",
|
"addLaneToolCodex": "Codex",
|
||||||
"addLaneToolHint": "Which CLI drives this lane. Cannot be changed later.",
|
"addLaneToolHint": "Which CLI drives this lane. Cannot be changed later.",
|
||||||
|
"addLaneWriteDocsHint": "Opens the lane's terminal on creation and runs the /sg-all:sg-md-writing skill there.",
|
||||||
|
"addLaneWriteDocsLabel": "Create/update readme file",
|
||||||
"mode.repo": "Repo",
|
"mode.repo": "Repo",
|
||||||
"mode.repoHint": "Adopt this directory as a lane, as-is — no worktree, no new branch.",
|
"mode.repoHint": "Adopt this directory as a lane, as-is — no worktree, no new branch.",
|
||||||
"mode.worktree": "Worktree",
|
"mode.worktree": "Worktree",
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
"addLaneToolClaude": "Claude Code",
|
"addLaneToolClaude": "Claude Code",
|
||||||
"addLaneToolCodex": "Codex",
|
"addLaneToolCodex": "Codex",
|
||||||
"addLaneToolHint": "CLI nào điều khiển lane này. Không thể thay đổi sau này.",
|
"addLaneToolHint": "CLI nào điều khiển lane này. Không thể thay đổi sau này.",
|
||||||
|
"addLaneWriteDocsHint": "Khi tạo lane sẽ mở terminal của lane và chạy skill /sg-all:sg-md-writing tại đó.",
|
||||||
|
"addLaneWriteDocsLabel": "Tạo/cập nhật file readme",
|
||||||
"mode.repo": "Repo",
|
"mode.repo": "Repo",
|
||||||
"mode.repoHint": "Gắn thẳng thư mục này làm lane, nguyên trạng — không worktree, không nhánh mới.",
|
"mode.repoHint": "Gắn thẳng thư mục này làm lane, nguyên trạng — không worktree, không nhánh mới.",
|
||||||
"mode.worktree": "Worktree",
|
"mode.worktree": "Worktree",
|
||||||
|
|||||||
@@ -9537,7 +9537,7 @@ exports[`screen snapshots > Settings 1`] = `
|
|||||||
<p
|
<p
|
||||||
class="text-[10px] text-status-warning/90 mt-1"
|
class="text-[10px] text-status-warning/90 mt-1"
|
||||||
>
|
>
|
||||||
UI build v1.5.8 (rebuild client to match)
|
UI build v1.5.10 (rebuild client to match)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ The modal has a segmented toggle mirroring the CLI's two modes:
|
|||||||
- **Repo** — adopts the given directory as-is (maps to `agenthub lanes add --cwd`). No branch fields; the auto-setup summary above does not run (adopting is instant, nothing to wait on).
|
- **Repo** — adopts the given directory as-is (maps to `agenthub lanes add --cwd`). No branch fields; the auto-setup summary above does not run (adopting is instant, nothing to wait on).
|
||||||
- **Worktree** (default) — provisions a managed worktree (maps to `agenthub lanes add --repo`). Unlike the CLI, the modal requires you to type the new branch's name yourself rather than deriving one from the title — the underlying route still derives one when `branch` is omitted, so the CLI's behavior is unchanged.
|
- **Worktree** (default) — provisions a managed worktree (maps to `agenthub lanes add --repo`). Unlike the CLI, the modal requires you to type the new branch's name yourself rather than deriving one from the title — the underlying route still derives one when `branch` is omitted, so the CLI's behavior is unchanged.
|
||||||
|
|
||||||
|
Under the *Pipeline template* select, a **Create/update readme file** checkbox (Claude lanes only — Codex has no slash commands) starts the new lane's terminal run right after creation with `/sg-all:sg-md-writing` as its first message, so the lane opens already writing/refreshing its `CLAUDE.md`/`AGENTS.md` set. It requires the `sg-all` plugin to be installed in Claude Code; without it the session simply reports an unknown command. Leave it unticked to create the lane without starting a run.
|
||||||
|
|
||||||
Both modes' path field has a **Browse** button next to it, opening a small folder browser (`GET /api/lanes/browse?path=<abs>`) instead of a native OS picker — a browser cannot hand a web page an absolute filesystem path from a native dialog, so this dashboard (local-first, server and browser on the same machine) lists directories server-side instead: click a subfolder to descend, ".." to go up, "Select this folder" to fill the path field. Git repos are marked in the listing.
|
Both modes' path field has a **Browse** button next to it, opening a small folder browser (`GET /api/lanes/browse?path=<abs>`) instead of a native OS picker — a browser cannot hand a web page an absolute filesystem path from a native dialog, so this dashboard (local-first, server and browser on the same machine) lists directories server-side instead: click a subfolder to descend, ".." to go up, "Select this folder" to fill the path field. Git repos are marked in the listing.
|
||||||
|
|
||||||
## Lane tool: Claude or Codex
|
## Lane tool: Claude or Codex
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: Agent Dashboard for Claude Code API
|
title: Agent Dashboard for Claude Code API
|
||||||
version: 1.5.8
|
version: 1.5.10
|
||||||
description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
|
description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
|
||||||
contact:
|
contact:
|
||||||
name: Nguyễn Ngọc Trí Vĩ
|
name: Nguyễn Ngọc Trí Vĩ
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "1.5.8",
|
"version": "1.5.10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "1.5.8",
|
"version": "1.5.10",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "1.5.8",
|
"version": "1.5.10",
|
||||||
"bin": {
|
"bin": {
|
||||||
"agenthub": "bin/agenthub.js"
|
"agenthub": "bin/agenthub.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ function makePluginInstall(name, opts = {}) {
|
|||||||
source = { source: "url", url: "https://forge.example.com/Owner/Repo.git", ref: "main" },
|
source = { source: "url", url: "https://forge.example.com/Owner/Repo.git", ref: "main" },
|
||||||
version = "1.5.1",
|
version = "1.5.1",
|
||||||
knownMarketplaces = true,
|
knownMarketplaces = true,
|
||||||
|
repository,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
const home = path.join(tmpDir, name, ".claude");
|
const home = path.join(tmpDir, name, ".claude");
|
||||||
@@ -74,7 +75,7 @@ function makePluginInstall(name, opts = {}) {
|
|||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(mktDir, ".claude-plugin", "marketplace.json"),
|
path.join(mktDir, ".claude-plugin", "marketplace.json"),
|
||||||
JSON.stringify({ name: "sg", plugins: [{ name: "agenthub", source }] })
|
JSON.stringify({ name: "sg", repository, plugins: [{ name: "agenthub", source }] })
|
||||||
);
|
);
|
||||||
if (knownMarketplaces) {
|
if (knownMarketplaces) {
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -128,10 +129,31 @@ describe("pluginMarketplaceUrl", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to the marketplace's repository for a relative source", () => {
|
||||||
|
// This project's own entry is `"source": "./"` — the plugin IS the
|
||||||
|
// marketplace repo. Without this the dashboard answered `no_remote` and
|
||||||
|
// could never see a release on any plugin-cache install.
|
||||||
|
makePluginInstall("relsrc", {
|
||||||
|
source: "./",
|
||||||
|
repository: "https://forge.example.com/Owner/Repo",
|
||||||
|
});
|
||||||
|
assert.equal(pluginMarketplaceUrl("sg", "agenthub"), "https://forge.example.com/Owner/Repo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for a relative source when the manifest declares no repository", () => {
|
||||||
|
makePluginInstall("relnorepo", { source: "./plugins/agenthub" });
|
||||||
|
assert.equal(pluginMarketplaceUrl("sg", "agenthub"), null);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns null for a github owner/repo shorthand rather than guessing a host", () => {
|
it("returns null for a github owner/repo shorthand rather than guessing a host", () => {
|
||||||
// Inventing github.com here would produce an API URL that 404s and a
|
// Inventing github.com here would produce an API URL that 404s and a
|
||||||
// confidently wrong "up to date".
|
// confidently wrong "up to date".
|
||||||
makePluginInstall("ghshort", { source: { source: "github", repo: "Owner/Repo" } });
|
// Not even when the marketplace declares a repository: a shorthand source
|
||||||
|
// names a DIFFERENT repo, so borrowing the marketplace's is a wrong answer.
|
||||||
|
makePluginInstall("ghshort", {
|
||||||
|
source: { source: "github", repo: "Owner/Repo" },
|
||||||
|
repository: "https://forge.example.com/Owner/Marketplace",
|
||||||
|
});
|
||||||
assert.equal(pluginMarketplaceUrl("sg", "agenthub"), null);
|
assert.equal(pluginMarketplaceUrl("sg", "agenthub"), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,12 @@
|
|||||||
* rather than reporting a false "up to date".
|
* rather than reporting a false "up to date".
|
||||||
*
|
*
|
||||||
* **Two independent paths, chosen by install kind.** A checkout derives the API
|
* **Two independent paths, chosen by install kind.** A checkout derives the API
|
||||||
* URL from its `origin` remote. A plugin-cache install does NOT: when a
|
* URL from its `origin` remote. A plugin-cache install does NOT: Claude Code
|
||||||
* marketplace entry declares `"source": "url"` (this project's own does) Claude
|
* unpacks an archive, so the cache directory has no `.git` and there is no
|
||||||
* Code unpacks an archive, so the cache directory has no `.git` and there is no
|
|
||||||
* remote to read. That path is answered entirely without git — the repository
|
* remote to read. That path is answered entirely without git — the repository
|
||||||
* URL comes from the marketplace manifest on disk (`pluginMarketplaceUrl`) and
|
* URL comes from the marketplace manifest on disk (`pluginMarketplaceUrl`,
|
||||||
|
* which for this project's own relative `"source": "./"` entry resolves to the
|
||||||
|
* manifest's top-level `repository`) and
|
||||||
* the package.json version is the only comparison signal. Getting this wrong is
|
* the package.json version is the only comparison signal. Getting this wrong is
|
||||||
* what shipped in 1.5.1: the `.git` probe ran first, returned "not a git clone",
|
* what shipped in 1.5.1: the `.git` probe ran first, returned "not a git clone",
|
||||||
* and skipped the release layer, so `update_available` stayed false however many
|
* and skipped the release layer, so `update_available` stayed false however many
|
||||||
@@ -346,11 +347,24 @@ function pluginMarketplaceUrl(marketplace, plugin) {
|
|||||||
const entry = (manifest?.plugins || []).find((p) => p && p.name === plugin);
|
const entry = (manifest?.plugins || []).find((p) => p && p.name === plugin);
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
|
|
||||||
// `source` is either a string (a bare URL or owner/repo) or an object with
|
// `source` is either a string (a bare URL, a relative path, or owner/repo)
|
||||||
// its own `source` discriminator plus `url`/`repo`.
|
// or an object with its own `source` discriminator plus `url`/`repo`.
|
||||||
const src = entry.source;
|
const src = entry.source;
|
||||||
if (typeof src === "string") return src.includes("://") ? src : null;
|
if (typeof src === "string" && src.includes("://")) return src;
|
||||||
if (src && typeof src.url === "string" && src.url) return src.url;
|
if (src && typeof src.url === "string" && src.url) return src.url;
|
||||||
|
|
||||||
|
// A RELATIVE source ("./", "./plugins/x") means the plugin ships inside the
|
||||||
|
// marketplace repository itself — this project's own entry is exactly that
|
||||||
|
// ("source": "./") — so the manifest's declared `repository` is where its
|
||||||
|
// releases are published. Without this fallback every plugin-cache install
|
||||||
|
// of this very plugin answered `no_remote` and could never see a release.
|
||||||
|
const relative = typeof src === "string" && /^\.{0,2}\//.test(src);
|
||||||
|
if (relative) {
|
||||||
|
const repo = manifest?.repository;
|
||||||
|
const url = typeof repo === "string" ? repo : repo?.url;
|
||||||
|
if (typeof url === "string" && url.includes("://")) return url;
|
||||||
|
}
|
||||||
|
|
||||||
// A `github`-style `repo: "owner/repo"` shorthand carries no host; this
|
// A `github`-style `repo: "owner/repo"` shorthand carries no host; this
|
||||||
// build is distributed from its own forge, so rather than guessing
|
// build is distributed from its own forge, so rather than guessing
|
||||||
// github.com the caller reports "remote_not_http" and prints the manual
|
// github.com the caller reports "remote_not_http" and prints the manual
|
||||||
|
|||||||
Reference in New Issue
Block a user