@scenar/cli 0.1.20 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -5
- 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/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/index.d.ts.map +1 -1
- package/src/index.js +4 -0
- package/src/index.js.map +1 -1
- package/src/index.ts +4 -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/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,163 @@
|
|
|
1
|
+
import { resolve, join, basename } from "node:path";
|
|
2
|
+
import { stat, mkdir, writeFile, rm, readdir, copyFile, access } from "node:fs/promises";
|
|
3
|
+
import { detectRenderExport } from "../render/detect-render-export.js";
|
|
4
|
+
import { resolveProvidersPath } from "../render/resolve-providers.js";
|
|
5
|
+
import { generateEmbedEntry, generateEmbedHtml } from "./generate-embed-entry.js";
|
|
6
|
+
import { runViteBuild } from "./build.js";
|
|
7
|
+
import {
|
|
8
|
+
buildPackManifest,
|
|
9
|
+
writePackManifest,
|
|
10
|
+
writeScenarioJson,
|
|
11
|
+
verifyManifestFilesExist,
|
|
12
|
+
type PackManifest,
|
|
13
|
+
} from "./pack-manifest.js";
|
|
14
|
+
import { DEFAULT_VIEWPORT } from "./viewport.js";
|
|
15
|
+
|
|
16
|
+
/** Generator version stamped into scenario.json (kept in sync with the CLI). */
|
|
17
|
+
const PACK_GENERATOR_VERSION = "0.0.1";
|
|
18
|
+
|
|
19
|
+
/** Options for {@link runPack}. Paths may be relative; they are resolved here. */
|
|
20
|
+
export interface RunPackOptions {
|
|
21
|
+
/** Path to a scenario directory (must contain steps.ts + a renderStep export). */
|
|
22
|
+
readonly scenarioDir: string;
|
|
23
|
+
/** Output directory for the bundle (default: ./<scenario-id>-bundle). */
|
|
24
|
+
readonly outDir?: string;
|
|
25
|
+
/** Canonical viewport width in px (default: {@link DEFAULT_VIEWPORT}.width). */
|
|
26
|
+
readonly width?: number;
|
|
27
|
+
/** Shell/container height in px (default: {@link DEFAULT_VIEWPORT}.height). */
|
|
28
|
+
readonly shellHeight?: number;
|
|
29
|
+
/** Keep the generated entry directory for debugging (default: false). */
|
|
30
|
+
readonly keepTemp?: boolean;
|
|
31
|
+
/** Progress sink for mid-operation messages. */
|
|
32
|
+
readonly onLog?: (message: string) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The outcome of a successful pack. */
|
|
36
|
+
export interface PackResult {
|
|
37
|
+
readonly scenarioId: string;
|
|
38
|
+
readonly outDir: string;
|
|
39
|
+
readonly renderFilePath: string;
|
|
40
|
+
readonly providersPath: string | null;
|
|
41
|
+
readonly hasNarration: boolean;
|
|
42
|
+
readonly manifest: PackManifest;
|
|
43
|
+
readonly totalBytes: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bundle a scenario directory into a self-contained, hosted embed — the pure
|
|
48
|
+
* orchestration behind `scenar pack`, with no process/exit coupling so both the
|
|
49
|
+
* CLI command and the MCP server can call it. Throws on any failure (invalid
|
|
50
|
+
* directory, bundler error, allowlist violation); the caller formats it.
|
|
51
|
+
*/
|
|
52
|
+
export async function runPack(options: RunPackOptions): Promise<PackResult> {
|
|
53
|
+
const onLog = options.onLog ?? (() => {});
|
|
54
|
+
const scenarioDir = resolve(options.scenarioDir);
|
|
55
|
+
|
|
56
|
+
const info = await stat(scenarioDir).catch(() => null);
|
|
57
|
+
if (!info) {
|
|
58
|
+
throw new Error(`${options.scenarioDir} does not exist.`);
|
|
59
|
+
}
|
|
60
|
+
if (!info.isDirectory()) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`${options.scenarioDir} is not a directory. ` +
|
|
63
|
+
"The pack command requires a scenario directory (with steps.ts).",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const scenarioId = basename(scenarioDir);
|
|
68
|
+
const outDir = options.outDir ? resolve(options.outDir) : resolve(`./${scenarioId}-bundle`);
|
|
69
|
+
const width = options.width ?? DEFAULT_VIEWPORT.width;
|
|
70
|
+
const shellHeight = options.shellHeight ?? DEFAULT_VIEWPORT.height;
|
|
71
|
+
|
|
72
|
+
// The entry must live inside the scenario directory so the bundler's
|
|
73
|
+
// node_modules resolution walks up into the consumer project.
|
|
74
|
+
const tempDir = join(scenarioDir, ".scenar-pack");
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const renderFilePath = await detectRenderExport(scenarioDir);
|
|
78
|
+
const providersPath = await resolveProvidersPath(scenarioDir);
|
|
79
|
+
const hasNarration = await fileExists(join(scenarioDir, "narration", "manifest.json"));
|
|
80
|
+
|
|
81
|
+
onLog(`Scenario: ${scenarioId}`);
|
|
82
|
+
onLog(`Render: ${renderFilePath}`);
|
|
83
|
+
onLog(`Providers: ${providersPath ?? "none"}`);
|
|
84
|
+
onLog(`Narration: ${hasNarration ? "yes (manifest found)" : "none"}`);
|
|
85
|
+
onLog(`Output: ${outDir}`);
|
|
86
|
+
|
|
87
|
+
// 1. Generate the browser entry + index.html into the temp dir.
|
|
88
|
+
const entrySource = generateEmbedEntry({
|
|
89
|
+
scenarioDir,
|
|
90
|
+
renderFilePath,
|
|
91
|
+
scenarioId,
|
|
92
|
+
hasNarration,
|
|
93
|
+
providersPath,
|
|
94
|
+
canonicalWidth: width,
|
|
95
|
+
shellHeight,
|
|
96
|
+
});
|
|
97
|
+
await mkdir(tempDir, { recursive: true });
|
|
98
|
+
const entryFileName = "entry.tsx";
|
|
99
|
+
const entryHtmlPath = join(tempDir, "index.html");
|
|
100
|
+
await writeFile(join(tempDir, entryFileName), entrySource, "utf-8");
|
|
101
|
+
await writeFile(entryHtmlPath, generateEmbedHtml(scenarioId, entryFileName), "utf-8");
|
|
102
|
+
|
|
103
|
+
// 2. Build the static bundle with Vite.
|
|
104
|
+
onLog("Bundling embed with Vite...");
|
|
105
|
+
await runViteBuild({ root: tempDir, outDir, entryHtmlPath });
|
|
106
|
+
|
|
107
|
+
// 3. Copy the narration manifest + audio. The embed fetches
|
|
108
|
+
// ./narration/manifest.json at runtime and resolves each clip's
|
|
109
|
+
// relative src against it, so both must ship in the bundle.
|
|
110
|
+
if (hasNarration) {
|
|
111
|
+
await copyNarration(scenarioDir, outDir);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 4. Write the required scenario.json descriptor, recording the canonical
|
|
115
|
+
// viewport baked into the bundle so `deploy`/`serve`/`publish` can derive
|
|
116
|
+
// a correctly-proportioned embed snippet (DD-004).
|
|
117
|
+
await writeScenarioJson(outDir, scenarioId, PACK_GENERATOR_VERSION, {
|
|
118
|
+
width,
|
|
119
|
+
height: shellHeight,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 5. Compute + write the deploy-facing pack manifest (validates the bundle
|
|
123
|
+
// against the backend allowlist; throws on any violation).
|
|
124
|
+
const manifest = await buildPackManifest(outDir, scenarioId);
|
|
125
|
+
await verifyManifestFilesExist(outDir, manifest);
|
|
126
|
+
await writePackManifest(outDir, manifest);
|
|
127
|
+
|
|
128
|
+
const totalBytes = manifest.files.reduce((sum, f) => sum + f.sizeBytes, 0);
|
|
129
|
+
return { scenarioId, outDir, renderFilePath, providersPath, hasNarration, manifest, totalBytes };
|
|
130
|
+
} finally {
|
|
131
|
+
if (!options.keepTemp) {
|
|
132
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Copy the narration manifest and every *.mp3 from <scenarioDir>/narration into
|
|
139
|
+
* <outDir>/narration. The manifest is the runtime index the embed fetches; the
|
|
140
|
+
* mp3s are the clips it references by relative src.
|
|
141
|
+
*/
|
|
142
|
+
async function copyNarration(scenarioDir: string, outDir: string): Promise<void> {
|
|
143
|
+
const srcDir = join(scenarioDir, "narration");
|
|
144
|
+
const destDir = join(outDir, "narration");
|
|
145
|
+
await mkdir(destDir, { recursive: true });
|
|
146
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
if (!entry.isFile()) continue;
|
|
149
|
+
const name = entry.name.toLowerCase();
|
|
150
|
+
if (name === "manifest.json" || name.endsWith(".mp3")) {
|
|
151
|
+
await copyFile(join(srcDir, entry.name), join(destDir, entry.name));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
157
|
+
try {
|
|
158
|
+
await access(path);
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PublishFlowDeps } from "./publish-flow.js";
|
|
2
|
+
/**
|
|
3
|
+
* The production {@link PublishFlowDeps} backed by the `gh` CLI and `git`. The
|
|
4
|
+
* `log` sink is supplied by the command; everything here shells out.
|
|
5
|
+
*
|
|
6
|
+
* Auth uses `gh` throughout: API calls go through `gh api`, and the git push
|
|
7
|
+
* borrows gh's credential helper (`gh auth git-credential`) for that one command
|
|
8
|
+
* only — so no token is ever placed on the command line or written to config.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createGithubPublisher(): Omit<PublishFlowDeps, "log">;
|
|
11
|
+
//# sourceMappingURL=github.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../../src/publish/github.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAuCzD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAAC,eAAe,EAAE,KAAK,CAAC,CAoKpE"}
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
/** Run a command, capturing output. Never rejects on a non-zero exit code. */
|
|
6
|
+
function run(file, args, cwd) {
|
|
7
|
+
return new Promise((resolvePromise) => {
|
|
8
|
+
execFile(file, args, { cwd, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
9
|
+
const code = error && typeof error.code === "number"
|
|
10
|
+
? error.code
|
|
11
|
+
: error
|
|
12
|
+
? 1
|
|
13
|
+
: 0;
|
|
14
|
+
resolvePromise({ code, stdout: stdout.toString(), stderr: stderr.toString() });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** Commit identity for the published snapshot (Pages ignores authorship). */
|
|
19
|
+
const COMMIT_USER = "scenar-cli";
|
|
20
|
+
const COMMIT_EMAIL = "scenar-cli@users.noreply.github.com";
|
|
21
|
+
/**
|
|
22
|
+
* The branch GitHub Pages currently serves for owner/repo, or null if Pages is
|
|
23
|
+
* not enabled (the GET 404s, so `gh` exits non-zero). The authoritative state
|
|
24
|
+
* check that {@link createGithubPublisher}'s enablePages relies on.
|
|
25
|
+
*/
|
|
26
|
+
async function getPagesBranch(owner, repo) {
|
|
27
|
+
const result = await run("gh", ["api", `repos/${owner}/${repo}/pages`, "--jq", ".source.branch"]);
|
|
28
|
+
if (result.code !== 0)
|
|
29
|
+
return null;
|
|
30
|
+
const branch = result.stdout.trim();
|
|
31
|
+
return branch.length > 0 ? branch : null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The production {@link PublishFlowDeps} backed by the `gh` CLI and `git`. The
|
|
35
|
+
* `log` sink is supplied by the command; everything here shells out.
|
|
36
|
+
*
|
|
37
|
+
* Auth uses `gh` throughout: API calls go through `gh api`, and the git push
|
|
38
|
+
* borrows gh's credential helper (`gh auth git-credential`) for that one command
|
|
39
|
+
* only — so no token is ever placed on the command line or written to config.
|
|
40
|
+
*/
|
|
41
|
+
export function createGithubPublisher() {
|
|
42
|
+
return {
|
|
43
|
+
async ensureToolsAvailable() {
|
|
44
|
+
const git = await run("git", ["--version"]);
|
|
45
|
+
if (git.code !== 0) {
|
|
46
|
+
throw new Error("git is not installed or not on PATH. Install git and retry.");
|
|
47
|
+
}
|
|
48
|
+
const gh = await run("gh", ["--version"]);
|
|
49
|
+
if (gh.code !== 0) {
|
|
50
|
+
throw new Error("the GitHub CLI (gh) is not installed. Install it from https://cli.github.com and retry.");
|
|
51
|
+
}
|
|
52
|
+
const auth = await run("gh", ["auth", "status"]);
|
|
53
|
+
if (auth.code !== 0) {
|
|
54
|
+
throw new Error("not authenticated with GitHub. Run `gh auth login` and retry.");
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
async resolveOwner(explicitOrg) {
|
|
58
|
+
if (explicitOrg)
|
|
59
|
+
return explicitOrg;
|
|
60
|
+
const result = await run("gh", ["api", "user", "--jq", ".login"]);
|
|
61
|
+
const login = result.stdout.trim();
|
|
62
|
+
if (result.code !== 0 || login.length === 0) {
|
|
63
|
+
throw new Error(`could not resolve your GitHub username via gh (${result.stderr.trim() || "no output"}).`);
|
|
64
|
+
}
|
|
65
|
+
return login;
|
|
66
|
+
},
|
|
67
|
+
async repoExists(owner, repo) {
|
|
68
|
+
const result = await run("gh", ["repo", "view", `${owner}/${repo}`, "--json", "name"]);
|
|
69
|
+
return result.code === 0;
|
|
70
|
+
},
|
|
71
|
+
async createRepo(owner, repo, opts) {
|
|
72
|
+
const visibility = opts.private ? "--private" : "--public";
|
|
73
|
+
const result = await run("gh", [
|
|
74
|
+
"repo",
|
|
75
|
+
"create",
|
|
76
|
+
`${owner}/${repo}`,
|
|
77
|
+
visibility,
|
|
78
|
+
"--description",
|
|
79
|
+
opts.description,
|
|
80
|
+
]);
|
|
81
|
+
if (result.code !== 0) {
|
|
82
|
+
throw new Error(`failed to create ${owner}/${repo}: ${result.stderr.trim() || "unknown error"}`);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
async pushBundleToPages({ bundleDir, owner, repo, path, message }) {
|
|
86
|
+
const workDir = await mkdtemp(join(tmpdir(), "scenar-publish-"));
|
|
87
|
+
// Borrow gh's credential helper for network git ops only; no token on argv.
|
|
88
|
+
const cred = ["-c", "credential.helper=!gh auth git-credential"];
|
|
89
|
+
const git = async (args) => {
|
|
90
|
+
const result = await run("git", args, workDir);
|
|
91
|
+
if (result.code !== 0) {
|
|
92
|
+
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || "unknown error"}`);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
// Stage in a throwaway repo so the user's bundle dir is never turned into
|
|
98
|
+
// a git repo.
|
|
99
|
+
const remote = `https://github.com/${owner}/${repo}.git`;
|
|
100
|
+
await git(["init", "-q"]);
|
|
101
|
+
await git(["remote", "add", "origin", remote]);
|
|
102
|
+
// Subdir publishes build on the existing branch so sibling tours survive;
|
|
103
|
+
// root publishes replace the whole branch (dedicated single-tour repo).
|
|
104
|
+
let basedOnExisting = false;
|
|
105
|
+
if (path) {
|
|
106
|
+
const fetched = await run("git", [...cred, "fetch", "-q", "--depth=1", "origin", "gh-pages"], workDir);
|
|
107
|
+
if (fetched.code === 0) {
|
|
108
|
+
await git(["checkout", "-q", "-B", "gh-pages", "FETCH_HEAD"]);
|
|
109
|
+
basedOnExisting = true;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
await git(["checkout", "-q", "-b", "gh-pages"]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
await git(["checkout", "-q", "-B", "gh-pages"]);
|
|
117
|
+
}
|
|
118
|
+
// Replace only the target subfolder (or the whole tree at root). The
|
|
119
|
+
// root .nojekyll makes Pages serve files verbatim (no Jekyll processing
|
|
120
|
+
// of underscore-prefixed paths, etc.).
|
|
121
|
+
const targetDir = path ? join(workDir, path) : workDir;
|
|
122
|
+
if (path) {
|
|
123
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
124
|
+
await mkdir(dirname(targetDir), { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
await cp(bundleDir, targetDir, { recursive: true });
|
|
127
|
+
await writeFile(join(workDir, ".nojekyll"), "", "utf-8");
|
|
128
|
+
await git(["add", "-A"]);
|
|
129
|
+
// Republishing identical content is a no-op — nothing to commit or push.
|
|
130
|
+
const status = await git(["status", "--porcelain"]);
|
|
131
|
+
if (status.stdout.trim().length === 0)
|
|
132
|
+
return;
|
|
133
|
+
await git([
|
|
134
|
+
"-c", `user.name=${COMMIT_USER}`,
|
|
135
|
+
"-c", `user.email=${COMMIT_EMAIL}`,
|
|
136
|
+
"commit", "-q", "-m", message,
|
|
137
|
+
]);
|
|
138
|
+
// Fast-forward push when built on the fetched head; otherwise force
|
|
139
|
+
// (creating or replacing the branch wholesale).
|
|
140
|
+
const pushArgs = basedOnExisting
|
|
141
|
+
? [...cred, "push", "-q", "origin", "gh-pages"]
|
|
142
|
+
: [...cred, "push", "-q", "-f", "origin", "gh-pages"];
|
|
143
|
+
const pushed = await run("git", pushArgs, workDir);
|
|
144
|
+
if (pushed.code !== 0) {
|
|
145
|
+
throw new Error(`git push failed: ${pushed.stderr.trim() || "unknown error"}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await rm(workDir, { recursive: true, force: true }).catch(() => { });
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
async enablePages(owner, repo) {
|
|
153
|
+
// Verify state rather than trust POST's exit code. The Pages create
|
|
154
|
+
// endpoint can return an async 202 with an empty body, which makes
|
|
155
|
+
// `gh api` exit non-zero ("unexpected end of JSON input") even though the
|
|
156
|
+
// site was enabled. GET is the authoritative check and is naturally
|
|
157
|
+
// idempotent for re-publishes.
|
|
158
|
+
const current = await getPagesBranch(owner, repo);
|
|
159
|
+
if (current === "gh-pages")
|
|
160
|
+
return; // already pointed at our branch
|
|
161
|
+
if (current !== null) {
|
|
162
|
+
// Enabled, but from a different branch — repoint it.
|
|
163
|
+
const put = await run("gh", [
|
|
164
|
+
"api",
|
|
165
|
+
"-X",
|
|
166
|
+
"PUT",
|
|
167
|
+
`repos/${owner}/${repo}/pages`,
|
|
168
|
+
"-f",
|
|
169
|
+
"source[branch]=gh-pages",
|
|
170
|
+
"-f",
|
|
171
|
+
"source[path]=/",
|
|
172
|
+
]);
|
|
173
|
+
if (put.code !== 0) {
|
|
174
|
+
throw new Error(`failed to repoint GitHub Pages to gh-pages: ${put.stderr.trim() || "unknown error"}`);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Not enabled yet — create it, then confirm via GET (ignore the create's
|
|
179
|
+
// noisy exit/parse behaviour and rely on the observed state).
|
|
180
|
+
const post = await run("gh", [
|
|
181
|
+
"api",
|
|
182
|
+
"-X",
|
|
183
|
+
"POST",
|
|
184
|
+
`repos/${owner}/${repo}/pages`,
|
|
185
|
+
"-f",
|
|
186
|
+
"source[branch]=gh-pages",
|
|
187
|
+
"-f",
|
|
188
|
+
"source[path]=/",
|
|
189
|
+
]);
|
|
190
|
+
const after = await getPagesBranch(owner, repo);
|
|
191
|
+
if (after !== null)
|
|
192
|
+
return;
|
|
193
|
+
throw new Error(`failed to enable GitHub Pages: ${post.stderr.trim() || "unknown error"}`);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.js","sourceRoot":"","sources":["../../../src/publish/github.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
+
}
|