@scenar/cli 0.1.20 → 0.2.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/example-bundle/assets/index-BLQ14SEo.js +147 -0
- package/example-bundle/assets/logo-f-5LjVqJ.png +0 -0
- package/example-bundle/assets/style-Dnzx_jUO.css +1 -0
- package/example-bundle/index.html +13 -0
- package/example-bundle/pack-manifest.json +36 -0
- package/example-bundle/scenario.json +9 -0
- package/package.json +17 -5
- package/src/__tests__/pack-generate-embed-entry.test.ts +59 -6
- package/src/__tests__/publish-command.test.ts +46 -0
- package/src/__tests__/publish-flow.test.ts +220 -0
- package/src/__tests__/read-viewport.test.ts +45 -0
- package/src/__tests__/run-publish.test.ts +92 -0
- package/src/__tests__/serve-command.test.ts +44 -0
- package/src/__tests__/serve-request-path.test.ts +67 -0
- package/src/__tests__/serve-static-server.test.ts +88 -0
- package/src/__tests__/try-command.test.ts +21 -0
- package/src/api.d.ts +31 -0
- package/src/api.d.ts.map +1 -0
- package/src/api.js +28 -0
- package/src/api.js.map +1 -0
- package/src/api.ts +55 -0
- package/src/bundle/read-viewport.d.ts +23 -0
- package/src/bundle/read-viewport.d.ts.map +1 -0
- package/src/bundle/read-viewport.js +25 -0
- package/src/bundle/read-viewport.js.map +1 -0
- package/src/bundle/read-viewport.ts +36 -0
- package/src/commands/deploy.d.ts.map +1 -1
- package/src/commands/deploy.js +8 -24
- package/src/commands/deploy.js.map +1 -1
- package/src/commands/deploy.ts +10 -28
- package/src/commands/narrate.d.ts.map +1 -1
- package/src/commands/narrate.js +39 -170
- package/src/commands/narrate.js.map +1 -1
- package/src/commands/narrate.ts +48 -289
- package/src/commands/pack.d.ts.map +1 -1
- package/src/commands/pack.js +12 -112
- package/src/commands/pack.js.map +1 -1
- package/src/commands/pack.ts +12 -128
- package/src/commands/publish.d.ts +3 -0
- package/src/commands/publish.d.ts.map +1 -0
- package/src/commands/publish.js +64 -0
- package/src/commands/publish.js.map +1 -0
- package/src/commands/publish.ts +86 -0
- package/src/commands/render.d.ts.map +1 -1
- package/src/commands/render.js +12 -194
- package/src/commands/render.js.map +1 -1
- package/src/commands/render.ts +19 -286
- package/src/commands/serve.d.ts +3 -0
- package/src/commands/serve.d.ts.map +1 -0
- package/src/commands/serve.js +91 -0
- package/src/commands/serve.js.map +1 -0
- package/src/commands/serve.ts +112 -0
- package/src/commands/try.d.ts +3 -0
- package/src/commands/try.d.ts.map +1 -0
- package/src/commands/try.js +99 -0
- package/src/commands/try.js.map +1 -0
- package/src/commands/try.ts +122 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +6 -0
- package/src/index.js.map +1 -1
- package/src/index.ts +6 -0
- package/src/narrate/run-narrate.d.ts +43 -0
- package/src/narrate/run-narrate.d.ts.map +1 -0
- package/src/narrate/run-narrate.js +174 -0
- package/src/narrate/run-narrate.js.map +1 -0
- package/src/narrate/run-narrate.ts +298 -0
- package/src/pack/generate-embed-entry.d.ts +11 -0
- package/src/pack/generate-embed-entry.d.ts.map +1 -1
- package/src/pack/generate-embed-entry.js +81 -10
- package/src/pack/generate-embed-entry.js.map +1 -1
- package/src/pack/generate-embed-entry.ts +81 -10
- package/src/pack/run-pack.d.ts +34 -0
- package/src/pack/run-pack.d.ts.map +1 -0
- package/src/pack/run-pack.js +117 -0
- package/src/pack/run-pack.js.map +1 -0
- package/src/pack/run-pack.ts +163 -0
- package/src/publish/github.d.ts +11 -0
- package/src/publish/github.d.ts.map +1 -0
- package/src/publish/github.js +197 -0
- package/src/publish/github.js.map +1 -0
- package/src/publish/github.ts +216 -0
- package/src/publish/publish-flow.d.ts +110 -0
- package/src/publish/publish-flow.d.ts.map +1 -0
- package/src/publish/publish-flow.js +117 -0
- package/src/publish/publish-flow.js.map +1 -0
- package/src/publish/publish-flow.ts +199 -0
- package/src/publish/run-publish.d.ts +41 -0
- package/src/publish/run-publish.d.ts.map +1 -0
- package/src/publish/run-publish.js +77 -0
- package/src/publish/run-publish.js.map +1 -0
- package/src/publish/run-publish.ts +131 -0
- package/src/render/run-render.d.ts +31 -0
- package/src/render/run-render.d.ts.map +1 -0
- package/src/render/run-render.js +196 -0
- package/src/render/run-render.js.map +1 -0
- package/src/render/run-render.ts +287 -0
- package/src/serve/request-path.d.ts +23 -0
- package/src/serve/request-path.d.ts.map +1 -0
- package/src/serve/request-path.js +52 -0
- package/src/serve/request-path.js.map +1 -0
- package/src/serve/request-path.ts +55 -0
- package/src/serve/run-serve.d.ts +31 -0
- package/src/serve/run-serve.d.ts.map +1 -0
- package/src/serve/run-serve.js +35 -0
- package/src/serve/run-serve.js.map +1 -0
- package/src/serve/run-serve.ts +68 -0
- package/src/serve/static-server.d.ts +38 -0
- package/src/serve/static-server.d.ts.map +1 -0
- package/src/serve/static-server.js +91 -0
- package/src/serve/static-server.js.map +1 -0
- package/src/serve/static-server.ts +118 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, mkdir, rm, cp, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import type { PublishFlowDeps } from "./publish-flow.js";
|
|
6
|
+
|
|
7
|
+
/** Result of running a child process without throwing on a non-zero exit. */
|
|
8
|
+
interface RunResult {
|
|
9
|
+
readonly code: number;
|
|
10
|
+
readonly stdout: string;
|
|
11
|
+
readonly stderr: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Run a command, capturing output. Never rejects on a non-zero exit code. */
|
|
15
|
+
function run(file: string, args: string[], cwd?: string): Promise<RunResult> {
|
|
16
|
+
return new Promise<RunResult>((resolvePromise) => {
|
|
17
|
+
execFile(file, args, { cwd, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
18
|
+
const code = error && typeof (error as { code?: unknown }).code === "number"
|
|
19
|
+
? (error as { code: number }).code
|
|
20
|
+
: error
|
|
21
|
+
? 1
|
|
22
|
+
: 0;
|
|
23
|
+
resolvePromise({ code, stdout: stdout.toString(), stderr: stderr.toString() });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Commit identity for the published snapshot (Pages ignores authorship). */
|
|
29
|
+
const COMMIT_USER = "scenar-cli";
|
|
30
|
+
const COMMIT_EMAIL = "scenar-cli@users.noreply.github.com";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The branch GitHub Pages currently serves for owner/repo, or null if Pages is
|
|
34
|
+
* not enabled (the GET 404s, so `gh` exits non-zero). The authoritative state
|
|
35
|
+
* check that {@link createGithubPublisher}'s enablePages relies on.
|
|
36
|
+
*/
|
|
37
|
+
async function getPagesBranch(owner: string, repo: string): Promise<string | null> {
|
|
38
|
+
const result = await run("gh", ["api", `repos/${owner}/${repo}/pages`, "--jq", ".source.branch"]);
|
|
39
|
+
if (result.code !== 0) return null;
|
|
40
|
+
const branch = result.stdout.trim();
|
|
41
|
+
return branch.length > 0 ? branch : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The production {@link PublishFlowDeps} backed by the `gh` CLI and `git`. The
|
|
46
|
+
* `log` sink is supplied by the command; everything here shells out.
|
|
47
|
+
*
|
|
48
|
+
* Auth uses `gh` throughout: API calls go through `gh api`, and the git push
|
|
49
|
+
* borrows gh's credential helper (`gh auth git-credential`) for that one command
|
|
50
|
+
* only — so no token is ever placed on the command line or written to config.
|
|
51
|
+
*/
|
|
52
|
+
export function createGithubPublisher(): Omit<PublishFlowDeps, "log"> {
|
|
53
|
+
return {
|
|
54
|
+
async ensureToolsAvailable() {
|
|
55
|
+
const git = await run("git", ["--version"]);
|
|
56
|
+
if (git.code !== 0) {
|
|
57
|
+
throw new Error("git is not installed or not on PATH. Install git and retry.");
|
|
58
|
+
}
|
|
59
|
+
const gh = await run("gh", ["--version"]);
|
|
60
|
+
if (gh.code !== 0) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"the GitHub CLI (gh) is not installed. Install it from https://cli.github.com and retry.",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const auth = await run("gh", ["auth", "status"]);
|
|
66
|
+
if (auth.code !== 0) {
|
|
67
|
+
throw new Error("not authenticated with GitHub. Run `gh auth login` and retry.");
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async resolveOwner(explicitOrg) {
|
|
72
|
+
if (explicitOrg) return explicitOrg;
|
|
73
|
+
const result = await run("gh", ["api", "user", "--jq", ".login"]);
|
|
74
|
+
const login = result.stdout.trim();
|
|
75
|
+
if (result.code !== 0 || login.length === 0) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`could not resolve your GitHub username via gh (${result.stderr.trim() || "no output"}).`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return login;
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async repoExists(owner, repo) {
|
|
84
|
+
const result = await run("gh", ["repo", "view", `${owner}/${repo}`, "--json", "name"]);
|
|
85
|
+
return result.code === 0;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async createRepo(owner, repo, opts) {
|
|
89
|
+
const visibility = opts.private ? "--private" : "--public";
|
|
90
|
+
const result = await run("gh", [
|
|
91
|
+
"repo",
|
|
92
|
+
"create",
|
|
93
|
+
`${owner}/${repo}`,
|
|
94
|
+
visibility,
|
|
95
|
+
"--description",
|
|
96
|
+
opts.description,
|
|
97
|
+
]);
|
|
98
|
+
if (result.code !== 0) {
|
|
99
|
+
throw new Error(`failed to create ${owner}/${repo}: ${result.stderr.trim() || "unknown error"}`);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async pushBundleToPages({ bundleDir, owner, repo, path, message }) {
|
|
104
|
+
const workDir = await mkdtemp(join(tmpdir(), "scenar-publish-"));
|
|
105
|
+
// Borrow gh's credential helper for network git ops only; no token on argv.
|
|
106
|
+
const cred = ["-c", "credential.helper=!gh auth git-credential"];
|
|
107
|
+
const git = async (args: string[]) => {
|
|
108
|
+
const result = await run("git", args, workDir);
|
|
109
|
+
if (result.code !== 0) {
|
|
110
|
+
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || "unknown error"}`);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
// Stage in a throwaway repo so the user's bundle dir is never turned into
|
|
116
|
+
// a git repo.
|
|
117
|
+
const remote = `https://github.com/${owner}/${repo}.git`;
|
|
118
|
+
await git(["init", "-q"]);
|
|
119
|
+
await git(["remote", "add", "origin", remote]);
|
|
120
|
+
|
|
121
|
+
// Subdir publishes build on the existing branch so sibling tours survive;
|
|
122
|
+
// root publishes replace the whole branch (dedicated single-tour repo).
|
|
123
|
+
let basedOnExisting = false;
|
|
124
|
+
if (path) {
|
|
125
|
+
const fetched = await run("git", [...cred, "fetch", "-q", "--depth=1", "origin", "gh-pages"], workDir);
|
|
126
|
+
if (fetched.code === 0) {
|
|
127
|
+
await git(["checkout", "-q", "-B", "gh-pages", "FETCH_HEAD"]);
|
|
128
|
+
basedOnExisting = true;
|
|
129
|
+
} else {
|
|
130
|
+
await git(["checkout", "-q", "-b", "gh-pages"]);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
await git(["checkout", "-q", "-B", "gh-pages"]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Replace only the target subfolder (or the whole tree at root). The
|
|
137
|
+
// root .nojekyll makes Pages serve files verbatim (no Jekyll processing
|
|
138
|
+
// of underscore-prefixed paths, etc.).
|
|
139
|
+
const targetDir = path ? join(workDir, path) : workDir;
|
|
140
|
+
if (path) {
|
|
141
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
142
|
+
await mkdir(dirname(targetDir), { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
await cp(bundleDir, targetDir, { recursive: true });
|
|
145
|
+
await writeFile(join(workDir, ".nojekyll"), "", "utf-8");
|
|
146
|
+
|
|
147
|
+
await git(["add", "-A"]);
|
|
148
|
+
|
|
149
|
+
// Republishing identical content is a no-op — nothing to commit or push.
|
|
150
|
+
const status = await git(["status", "--porcelain"]);
|
|
151
|
+
if (status.stdout.trim().length === 0) return;
|
|
152
|
+
|
|
153
|
+
await git([
|
|
154
|
+
"-c", `user.name=${COMMIT_USER}`,
|
|
155
|
+
"-c", `user.email=${COMMIT_EMAIL}`,
|
|
156
|
+
"commit", "-q", "-m", message,
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
// Fast-forward push when built on the fetched head; otherwise force
|
|
160
|
+
// (creating or replacing the branch wholesale).
|
|
161
|
+
const pushArgs = basedOnExisting
|
|
162
|
+
? [...cred, "push", "-q", "origin", "gh-pages"]
|
|
163
|
+
: [...cred, "push", "-q", "-f", "origin", "gh-pages"];
|
|
164
|
+
const pushed = await run("git", pushArgs, workDir);
|
|
165
|
+
if (pushed.code !== 0) {
|
|
166
|
+
throw new Error(`git push failed: ${pushed.stderr.trim() || "unknown error"}`);
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
await rm(workDir, { recursive: true, force: true }).catch(() => {});
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async enablePages(owner, repo) {
|
|
174
|
+
// Verify state rather than trust POST's exit code. The Pages create
|
|
175
|
+
// endpoint can return an async 202 with an empty body, which makes
|
|
176
|
+
// `gh api` exit non-zero ("unexpected end of JSON input") even though the
|
|
177
|
+
// site was enabled. GET is the authoritative check and is naturally
|
|
178
|
+
// idempotent for re-publishes.
|
|
179
|
+
const current = await getPagesBranch(owner, repo);
|
|
180
|
+
if (current === "gh-pages") return; // already pointed at our branch
|
|
181
|
+
if (current !== null) {
|
|
182
|
+
// Enabled, but from a different branch — repoint it.
|
|
183
|
+
const put = await run("gh", [
|
|
184
|
+
"api",
|
|
185
|
+
"-X",
|
|
186
|
+
"PUT",
|
|
187
|
+
`repos/${owner}/${repo}/pages`,
|
|
188
|
+
"-f",
|
|
189
|
+
"source[branch]=gh-pages",
|
|
190
|
+
"-f",
|
|
191
|
+
"source[path]=/",
|
|
192
|
+
]);
|
|
193
|
+
if (put.code !== 0) {
|
|
194
|
+
throw new Error(`failed to repoint GitHub Pages to gh-pages: ${put.stderr.trim() || "unknown error"}`);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Not enabled yet — create it, then confirm via GET (ignore the create's
|
|
200
|
+
// noisy exit/parse behaviour and rely on the observed state).
|
|
201
|
+
const post = await run("gh", [
|
|
202
|
+
"api",
|
|
203
|
+
"-X",
|
|
204
|
+
"POST",
|
|
205
|
+
`repos/${owner}/${repo}/pages`,
|
|
206
|
+
"-f",
|
|
207
|
+
"source[branch]=gh-pages",
|
|
208
|
+
"-f",
|
|
209
|
+
"source[path]=/",
|
|
210
|
+
]);
|
|
211
|
+
const after = await getPagesBranch(owner, repo);
|
|
212
|
+
if (after !== null) return;
|
|
213
|
+
throw new Error(`failed to enable GitHub Pages: ${post.stderr.trim() || "unknown error"}`);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Pages publishing — the OSS "permanent public URL" path of Phase 1.
|
|
3
|
+
*
|
|
4
|
+
* The flow is dependency-injected (mirroring deploy-flow.ts) so its ordering and
|
|
5
|
+
* payloads are unit-testable against fakes, with zero live GitHub calls in the
|
|
6
|
+
* test suite. The real `gh`/`git` implementations live in ./github.ts.
|
|
7
|
+
*/
|
|
8
|
+
/** Identity for the target GitHub repository + how to write to it. */
|
|
9
|
+
export interface PublishOptions {
|
|
10
|
+
/** The packed bundle directory to publish. */
|
|
11
|
+
readonly bundleDir: string;
|
|
12
|
+
/** Target repository name (default: the scenario slug). */
|
|
13
|
+
readonly repo: string;
|
|
14
|
+
/**
|
|
15
|
+
* Subdirectory within the repo to publish into, normalized (no leading/
|
|
16
|
+
* trailing slashes). Empty string means the repo root — a dedicated,
|
|
17
|
+
* single-tour repo whose every publish replaces the whole branch. A non-empty
|
|
18
|
+
* path (e.g. `welcome-tour`) lets many tours coexist in one repo; publishing
|
|
19
|
+
* replaces only that subfolder and preserves its siblings.
|
|
20
|
+
*/
|
|
21
|
+
readonly path: string;
|
|
22
|
+
/** Target owner (a GitHub org); omitted means the authenticated user. */
|
|
23
|
+
readonly org?: string;
|
|
24
|
+
/** Create the repo private rather than public (Pages needs Pro for private). */
|
|
25
|
+
readonly private: boolean;
|
|
26
|
+
/** Commit message for the published snapshot. */
|
|
27
|
+
readonly message: string;
|
|
28
|
+
}
|
|
29
|
+
/** The published location. */
|
|
30
|
+
export interface PublishResult {
|
|
31
|
+
readonly owner: string;
|
|
32
|
+
readonly repo: string;
|
|
33
|
+
/** The subdirectory the embed was published into ("" for the repo root). */
|
|
34
|
+
readonly path: string;
|
|
35
|
+
/** The repository's web URL. */
|
|
36
|
+
readonly repoUrl: string;
|
|
37
|
+
/** The GitHub Pages URL the embed is served at (includes the path). */
|
|
38
|
+
readonly pagesUrl: string;
|
|
39
|
+
/** True when the repo was created by this run (vs. reused). */
|
|
40
|
+
readonly created: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The side-effecting operations the flow depends on, injected so the ordering
|
|
44
|
+
* can be tested without `gh`, `git`, or the network.
|
|
45
|
+
*/
|
|
46
|
+
export interface PublishFlowDeps {
|
|
47
|
+
/** Verify `gh` + `git` are installed and `gh` is authenticated; else throw. */
|
|
48
|
+
ensureToolsAvailable(): Promise<void>;
|
|
49
|
+
/** Resolve the target owner: the explicit org, or the authenticated user. */
|
|
50
|
+
resolveOwner(explicitOrg: string | undefined): Promise<string>;
|
|
51
|
+
/** Whether owner/repo already exists. */
|
|
52
|
+
repoExists(owner: string, repo: string): Promise<boolean>;
|
|
53
|
+
/** Create owner/repo (public unless private requested). */
|
|
54
|
+
createRepo(owner: string, repo: string, opts: {
|
|
55
|
+
private: boolean;
|
|
56
|
+
description: string;
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Publish the bundle (plus a root .nojekyll) to the repo's gh-pages branch.
|
|
60
|
+
* An empty `path` replaces the whole branch (dedicated repo); a non-empty
|
|
61
|
+
* `path` updates only that subfolder, preserving sibling tours already on the
|
|
62
|
+
* branch.
|
|
63
|
+
*/
|
|
64
|
+
pushBundleToPages(input: {
|
|
65
|
+
bundleDir: string;
|
|
66
|
+
owner: string;
|
|
67
|
+
repo: string;
|
|
68
|
+
path: string;
|
|
69
|
+
message: string;
|
|
70
|
+
}): Promise<void>;
|
|
71
|
+
/** Enable GitHub Pages from gh-pages (idempotent — a no-op if already on). */
|
|
72
|
+
enablePages(owner: string, repo: string): Promise<void>;
|
|
73
|
+
/** Progress sink (stderr in the command; captured in tests). */
|
|
74
|
+
log(message: string): void;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate a GitHub repository name. Returns an error message, or null if valid.
|
|
78
|
+
* The scenario slug (the default) is always valid; this guards an explicit
|
|
79
|
+
* `--repo` before any repo is touched.
|
|
80
|
+
*/
|
|
81
|
+
export declare function validateRepoName(repo: string): string | null;
|
|
82
|
+
/**
|
|
83
|
+
* Normalize a publish path to a clean, slash-separated subpath with no leading,
|
|
84
|
+
* trailing, or doubled slashes. `/`, `.`, and empty all normalize to "" (root).
|
|
85
|
+
*/
|
|
86
|
+
export declare function normalizePath(path: string | undefined): string;
|
|
87
|
+
/**
|
|
88
|
+
* Validate a (pre-normalized) publish subpath. Returns an error message, or null
|
|
89
|
+
* if valid. "" (root) is always valid; each segment must be a safe slug and
|
|
90
|
+
* never `..` (no directory traversal into the branch).
|
|
91
|
+
*/
|
|
92
|
+
export declare function validatePublishPath(path: string): string | null;
|
|
93
|
+
/**
|
|
94
|
+
* The GitHub Pages URL for owner/repo, optionally under a subpath. A repository
|
|
95
|
+
* named exactly `<owner>.github.io` is the user/org site and serves at the apex;
|
|
96
|
+
* every other repo serves under its name as a path (which is why packed bundles
|
|
97
|
+
* use a relative base so they work from a subpath). A non-empty `path` appends a
|
|
98
|
+
* further subdirectory so many tours can share one repo.
|
|
99
|
+
*/
|
|
100
|
+
export declare function pagesUrl(owner: string, repo: string, path?: string): string;
|
|
101
|
+
/** The repository's web URL. */
|
|
102
|
+
export declare function repoUrl(owner: string, repo: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Drive the publish: verify tooling, resolve the owner, ensure the repo exists,
|
|
105
|
+
* push the bundle to gh-pages, and enable Pages. Ordering is load-bearing — the
|
|
106
|
+
* repo must exist before the push, and the gh-pages branch must exist (from the
|
|
107
|
+
* push) before Pages can be pointed at it.
|
|
108
|
+
*/
|
|
109
|
+
export declare function runPublishFlow(deps: PublishFlowDeps, opts: PublishOptions): Promise<PublishResult>;
|
|
110
|
+
//# sourceMappingURL=publish-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-flow.d.ts","sourceRoot":"","sources":["../../../src/publish/publish-flow.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,8BAA8B;AAC9B,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,6EAA6E;IAC7E,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D,yCAAyC;IACzC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,2DAA2D;IAC3D,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxG;;;;;OAKG;IACH,iBAAiB,CAAC,KAAK,EAAE;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,8EAA8E;IAC9E,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,gEAAgE;IAChE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW5D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAM9D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS/D;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAK3E;AAED,gCAAgC;AAChC,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,aAAa,CAAC,CAuCxB"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Pages publishing — the OSS "permanent public URL" path of Phase 1.
|
|
3
|
+
*
|
|
4
|
+
* The flow is dependency-injected (mirroring deploy-flow.ts) so its ordering and
|
|
5
|
+
* payloads are unit-testable against fakes, with zero live GitHub calls in the
|
|
6
|
+
* test suite. The real `gh`/`git` implementations live in ./github.ts.
|
|
7
|
+
*/
|
|
8
|
+
/** A repo name's allowed characters (GitHub: alphanumerics, ., _, -). */
|
|
9
|
+
const REPO_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
10
|
+
/** GitHub caps repository names at 100 characters. */
|
|
11
|
+
const MAX_REPO_NAME_LENGTH = 100;
|
|
12
|
+
/** Allowed characters for a single publish path segment. */
|
|
13
|
+
const PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
14
|
+
/**
|
|
15
|
+
* Validate a GitHub repository name. Returns an error message, or null if valid.
|
|
16
|
+
* The scenario slug (the default) is always valid; this guards an explicit
|
|
17
|
+
* `--repo` before any repo is touched.
|
|
18
|
+
*/
|
|
19
|
+
export function validateRepoName(repo) {
|
|
20
|
+
if (repo.length === 0 || repo.length > MAX_REPO_NAME_LENGTH) {
|
|
21
|
+
return `repo name must be 1-${MAX_REPO_NAME_LENGTH} characters`;
|
|
22
|
+
}
|
|
23
|
+
if (repo === "." || repo === "..") {
|
|
24
|
+
return `repo name must not be "." or ".."`;
|
|
25
|
+
}
|
|
26
|
+
if (!REPO_NAME_PATTERN.test(repo)) {
|
|
27
|
+
return `invalid repo name "${repo}"; allowed: letters, digits, ".", "_", "-"`;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Normalize a publish path to a clean, slash-separated subpath with no leading,
|
|
33
|
+
* trailing, or doubled slashes. `/`, `.`, and empty all normalize to "" (root).
|
|
34
|
+
*/
|
|
35
|
+
export function normalizePath(path) {
|
|
36
|
+
return (path ?? "")
|
|
37
|
+
.split("/")
|
|
38
|
+
.map((seg) => seg.trim())
|
|
39
|
+
.filter((seg) => seg.length > 0 && seg !== ".")
|
|
40
|
+
.join("/");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate a (pre-normalized) publish subpath. Returns an error message, or null
|
|
44
|
+
* if valid. "" (root) is always valid; each segment must be a safe slug and
|
|
45
|
+
* never `..` (no directory traversal into the branch).
|
|
46
|
+
*/
|
|
47
|
+
export function validatePublishPath(path) {
|
|
48
|
+
if (path.length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
for (const seg of path.split("/")) {
|
|
51
|
+
if (seg === "..")
|
|
52
|
+
return `path must not contain ".."`;
|
|
53
|
+
if (!PATH_SEGMENT_PATTERN.test(seg)) {
|
|
54
|
+
return `invalid path segment "${seg}"; allowed: letters, digits, ".", "_", "-"`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The GitHub Pages URL for owner/repo, optionally under a subpath. A repository
|
|
61
|
+
* named exactly `<owner>.github.io` is the user/org site and serves at the apex;
|
|
62
|
+
* every other repo serves under its name as a path (which is why packed bundles
|
|
63
|
+
* use a relative base so they work from a subpath). A non-empty `path` appends a
|
|
64
|
+
* further subdirectory so many tours can share one repo.
|
|
65
|
+
*/
|
|
66
|
+
export function pagesUrl(owner, repo, path) {
|
|
67
|
+
const host = `${owner.toLowerCase()}.github.io`;
|
|
68
|
+
const base = repo.toLowerCase() === host ? `https://${host}/` : `https://${host}/${repo}/`;
|
|
69
|
+
const clean = normalizePath(path);
|
|
70
|
+
return clean ? `${base}${clean}/` : base;
|
|
71
|
+
}
|
|
72
|
+
/** The repository's web URL. */
|
|
73
|
+
export function repoUrl(owner, repo) {
|
|
74
|
+
return `https://github.com/${owner}/${repo}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Drive the publish: verify tooling, resolve the owner, ensure the repo exists,
|
|
78
|
+
* push the bundle to gh-pages, and enable Pages. Ordering is load-bearing — the
|
|
79
|
+
* repo must exist before the push, and the gh-pages branch must exist (from the
|
|
80
|
+
* push) before Pages can be pointed at it.
|
|
81
|
+
*/
|
|
82
|
+
export async function runPublishFlow(deps, opts) {
|
|
83
|
+
await deps.ensureToolsAvailable();
|
|
84
|
+
const owner = await deps.resolveOwner(opts.org);
|
|
85
|
+
const exists = await deps.repoExists(owner, opts.repo);
|
|
86
|
+
if (!exists) {
|
|
87
|
+
deps.log(`Creating ${opts.private ? "private" : "public"} repo ${owner}/${opts.repo}...`);
|
|
88
|
+
await deps.createRepo(owner, opts.repo, {
|
|
89
|
+
private: opts.private,
|
|
90
|
+
description: "Scenar embed — published with `scenar publish`.",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
deps.log(`Reusing existing repo ${owner}/${opts.repo}...`);
|
|
95
|
+
}
|
|
96
|
+
deps.log(opts.path
|
|
97
|
+
? `Publishing into ${opts.repo}/${opts.path}/ on the gh-pages branch...`
|
|
98
|
+
: "Pushing bundle to the gh-pages branch...");
|
|
99
|
+
await deps.pushBundleToPages({
|
|
100
|
+
bundleDir: opts.bundleDir,
|
|
101
|
+
owner,
|
|
102
|
+
repo: opts.repo,
|
|
103
|
+
path: opts.path,
|
|
104
|
+
message: opts.message,
|
|
105
|
+
});
|
|
106
|
+
deps.log("Enabling GitHub Pages...");
|
|
107
|
+
await deps.enablePages(owner, opts.repo);
|
|
108
|
+
return {
|
|
109
|
+
owner,
|
|
110
|
+
repo: opts.repo,
|
|
111
|
+
path: opts.path,
|
|
112
|
+
repoUrl: repoUrl(owner, opts.repo),
|
|
113
|
+
pagesUrl: pagesUrl(owner, opts.repo, opts.path),
|
|
114
|
+
created: !exists,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=publish-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-flow.js","sourceRoot":"","sources":["../../../src/publish/publish-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Pages publishing — the OSS "permanent public URL" path of Phase 1.
|
|
3
|
+
*
|
|
4
|
+
* The flow is dependency-injected (mirroring deploy-flow.ts) so its ordering and
|
|
5
|
+
* payloads are unit-testable against fakes, with zero live GitHub calls in the
|
|
6
|
+
* test suite. The real `gh`/`git` implementations live in ./github.ts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A repo name's allowed characters (GitHub: alphanumerics, ., _, -). */
|
|
10
|
+
const REPO_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
11
|
+
|
|
12
|
+
/** GitHub caps repository names at 100 characters. */
|
|
13
|
+
const MAX_REPO_NAME_LENGTH = 100;
|
|
14
|
+
|
|
15
|
+
/** Allowed characters for a single publish path segment. */
|
|
16
|
+
const PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
17
|
+
|
|
18
|
+
/** Identity for the target GitHub repository + how to write to it. */
|
|
19
|
+
export interface PublishOptions {
|
|
20
|
+
/** The packed bundle directory to publish. */
|
|
21
|
+
readonly bundleDir: string;
|
|
22
|
+
/** Target repository name (default: the scenario slug). */
|
|
23
|
+
readonly repo: string;
|
|
24
|
+
/**
|
|
25
|
+
* Subdirectory within the repo to publish into, normalized (no leading/
|
|
26
|
+
* trailing slashes). Empty string means the repo root — a dedicated,
|
|
27
|
+
* single-tour repo whose every publish replaces the whole branch. A non-empty
|
|
28
|
+
* path (e.g. `welcome-tour`) lets many tours coexist in one repo; publishing
|
|
29
|
+
* replaces only that subfolder and preserves its siblings.
|
|
30
|
+
*/
|
|
31
|
+
readonly path: string;
|
|
32
|
+
/** Target owner (a GitHub org); omitted means the authenticated user. */
|
|
33
|
+
readonly org?: string;
|
|
34
|
+
/** Create the repo private rather than public (Pages needs Pro for private). */
|
|
35
|
+
readonly private: boolean;
|
|
36
|
+
/** Commit message for the published snapshot. */
|
|
37
|
+
readonly message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The published location. */
|
|
41
|
+
export interface PublishResult {
|
|
42
|
+
readonly owner: string;
|
|
43
|
+
readonly repo: string;
|
|
44
|
+
/** The subdirectory the embed was published into ("" for the repo root). */
|
|
45
|
+
readonly path: string;
|
|
46
|
+
/** The repository's web URL. */
|
|
47
|
+
readonly repoUrl: string;
|
|
48
|
+
/** The GitHub Pages URL the embed is served at (includes the path). */
|
|
49
|
+
readonly pagesUrl: string;
|
|
50
|
+
/** True when the repo was created by this run (vs. reused). */
|
|
51
|
+
readonly created: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The side-effecting operations the flow depends on, injected so the ordering
|
|
56
|
+
* can be tested without `gh`, `git`, or the network.
|
|
57
|
+
*/
|
|
58
|
+
export interface PublishFlowDeps {
|
|
59
|
+
/** Verify `gh` + `git` are installed and `gh` is authenticated; else throw. */
|
|
60
|
+
ensureToolsAvailable(): Promise<void>;
|
|
61
|
+
/** Resolve the target owner: the explicit org, or the authenticated user. */
|
|
62
|
+
resolveOwner(explicitOrg: string | undefined): Promise<string>;
|
|
63
|
+
/** Whether owner/repo already exists. */
|
|
64
|
+
repoExists(owner: string, repo: string): Promise<boolean>;
|
|
65
|
+
/** Create owner/repo (public unless private requested). */
|
|
66
|
+
createRepo(owner: string, repo: string, opts: { private: boolean; description: string }): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Publish the bundle (plus a root .nojekyll) to the repo's gh-pages branch.
|
|
69
|
+
* An empty `path` replaces the whole branch (dedicated repo); a non-empty
|
|
70
|
+
* `path` updates only that subfolder, preserving sibling tours already on the
|
|
71
|
+
* branch.
|
|
72
|
+
*/
|
|
73
|
+
pushBundleToPages(input: {
|
|
74
|
+
bundleDir: string;
|
|
75
|
+
owner: string;
|
|
76
|
+
repo: string;
|
|
77
|
+
path: string;
|
|
78
|
+
message: string;
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
/** Enable GitHub Pages from gh-pages (idempotent — a no-op if already on). */
|
|
81
|
+
enablePages(owner: string, repo: string): Promise<void>;
|
|
82
|
+
/** Progress sink (stderr in the command; captured in tests). */
|
|
83
|
+
log(message: string): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate a GitHub repository name. Returns an error message, or null if valid.
|
|
88
|
+
* The scenario slug (the default) is always valid; this guards an explicit
|
|
89
|
+
* `--repo` before any repo is touched.
|
|
90
|
+
*/
|
|
91
|
+
export function validateRepoName(repo: string): string | null {
|
|
92
|
+
if (repo.length === 0 || repo.length > MAX_REPO_NAME_LENGTH) {
|
|
93
|
+
return `repo name must be 1-${MAX_REPO_NAME_LENGTH} characters`;
|
|
94
|
+
}
|
|
95
|
+
if (repo === "." || repo === "..") {
|
|
96
|
+
return `repo name must not be "." or ".."`;
|
|
97
|
+
}
|
|
98
|
+
if (!REPO_NAME_PATTERN.test(repo)) {
|
|
99
|
+
return `invalid repo name "${repo}"; allowed: letters, digits, ".", "_", "-"`;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Normalize a publish path to a clean, slash-separated subpath with no leading,
|
|
106
|
+
* trailing, or doubled slashes. `/`, `.`, and empty all normalize to "" (root).
|
|
107
|
+
*/
|
|
108
|
+
export function normalizePath(path: string | undefined): string {
|
|
109
|
+
return (path ?? "")
|
|
110
|
+
.split("/")
|
|
111
|
+
.map((seg) => seg.trim())
|
|
112
|
+
.filter((seg) => seg.length > 0 && seg !== ".")
|
|
113
|
+
.join("/");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validate a (pre-normalized) publish subpath. Returns an error message, or null
|
|
118
|
+
* if valid. "" (root) is always valid; each segment must be a safe slug and
|
|
119
|
+
* never `..` (no directory traversal into the branch).
|
|
120
|
+
*/
|
|
121
|
+
export function validatePublishPath(path: string): string | null {
|
|
122
|
+
if (path.length === 0) return null;
|
|
123
|
+
for (const seg of path.split("/")) {
|
|
124
|
+
if (seg === "..") return `path must not contain ".."`;
|
|
125
|
+
if (!PATH_SEGMENT_PATTERN.test(seg)) {
|
|
126
|
+
return `invalid path segment "${seg}"; allowed: letters, digits, ".", "_", "-"`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The GitHub Pages URL for owner/repo, optionally under a subpath. A repository
|
|
134
|
+
* named exactly `<owner>.github.io` is the user/org site and serves at the apex;
|
|
135
|
+
* every other repo serves under its name as a path (which is why packed bundles
|
|
136
|
+
* use a relative base so they work from a subpath). A non-empty `path` appends a
|
|
137
|
+
* further subdirectory so many tours can share one repo.
|
|
138
|
+
*/
|
|
139
|
+
export function pagesUrl(owner: string, repo: string, path?: string): string {
|
|
140
|
+
const host = `${owner.toLowerCase()}.github.io`;
|
|
141
|
+
const base = repo.toLowerCase() === host ? `https://${host}/` : `https://${host}/${repo}/`;
|
|
142
|
+
const clean = normalizePath(path);
|
|
143
|
+
return clean ? `${base}${clean}/` : base;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The repository's web URL. */
|
|
147
|
+
export function repoUrl(owner: string, repo: string): string {
|
|
148
|
+
return `https://github.com/${owner}/${repo}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Drive the publish: verify tooling, resolve the owner, ensure the repo exists,
|
|
153
|
+
* push the bundle to gh-pages, and enable Pages. Ordering is load-bearing — the
|
|
154
|
+
* repo must exist before the push, and the gh-pages branch must exist (from the
|
|
155
|
+
* push) before Pages can be pointed at it.
|
|
156
|
+
*/
|
|
157
|
+
export async function runPublishFlow(
|
|
158
|
+
deps: PublishFlowDeps,
|
|
159
|
+
opts: PublishOptions,
|
|
160
|
+
): Promise<PublishResult> {
|
|
161
|
+
await deps.ensureToolsAvailable();
|
|
162
|
+
|
|
163
|
+
const owner = await deps.resolveOwner(opts.org);
|
|
164
|
+
const exists = await deps.repoExists(owner, opts.repo);
|
|
165
|
+
if (!exists) {
|
|
166
|
+
deps.log(`Creating ${opts.private ? "private" : "public"} repo ${owner}/${opts.repo}...`);
|
|
167
|
+
await deps.createRepo(owner, opts.repo, {
|
|
168
|
+
private: opts.private,
|
|
169
|
+
description: "Scenar embed — published with `scenar publish`.",
|
|
170
|
+
});
|
|
171
|
+
} else {
|
|
172
|
+
deps.log(`Reusing existing repo ${owner}/${opts.repo}...`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
deps.log(
|
|
176
|
+
opts.path
|
|
177
|
+
? `Publishing into ${opts.repo}/${opts.path}/ on the gh-pages branch...`
|
|
178
|
+
: "Pushing bundle to the gh-pages branch...",
|
|
179
|
+
);
|
|
180
|
+
await deps.pushBundleToPages({
|
|
181
|
+
bundleDir: opts.bundleDir,
|
|
182
|
+
owner,
|
|
183
|
+
repo: opts.repo,
|
|
184
|
+
path: opts.path,
|
|
185
|
+
message: opts.message,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
deps.log("Enabling GitHub Pages...");
|
|
189
|
+
await deps.enablePages(owner, opts.repo);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
owner,
|
|
193
|
+
repo: opts.repo,
|
|
194
|
+
path: opts.path,
|
|
195
|
+
repoUrl: repoUrl(owner, opts.repo),
|
|
196
|
+
pagesUrl: pagesUrl(owner, opts.repo, opts.path),
|
|
197
|
+
created: !exists,
|
|
198
|
+
};
|
|
199
|
+
}
|