Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ jobs:
RELEASE_DIR: ${{ runner.temp }}/gkit-release
RELEASE_ID: ${{ steps.release.outputs.release_id }}
run: |
for name in "gkit-$VERSION.tgz" gkit.tgz SHA256SUMS; do
for name in "gkit-$VERSION.tgz" gkit.tgz gkit SHA256SUMS; do
path="$RELEASE_DIR/$name"
local_digest="sha256:$(sha256sum "$path" | cut -d ' ' -f 1)"
asset_json=$(gh api "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID" \
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ install command.

## Install

Team machines install the standalone bundle (the bare `gkit` release asset, one executable JS file; embedded manifests and docs unpack to `~/.cache/gkit/assets/`) into `~/.local/bin` via the workspace `update-local-tool` skill. Anyone else:

Install the latest stable release globally:

```bash
Expand Down
4 changes: 2 additions & 2 deletions packages/gkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gkit",
"version": "0.3.4",
"version": "0.3.5",
"private": true,
"description": "Profile-bound, agent-first growth provider CLI.",
"bin": {
Expand All @@ -15,7 +15,7 @@
"sideEffects": false,
"scripts": {
"cli": "bun run ./src/cli.ts",
"build": "bun build ./bin/gkit.js --target=bun --outfile=dist/gkit.js",
"build": "bun build ./bin/gkit.js --target=bun --minify --define GKIT_STANDALONE=true --outfile=dist/gkit.js && chmod +x dist/gkit.js",
"check-types": "tsc --noEmit",
"generate:dataforseo": "bun run ./scripts/generate-dataforseo.ts",
"generate:dataforseo:check": "bun run ./scripts/generate-dataforseo.ts --check",
Expand Down
32 changes: 30 additions & 2 deletions packages/gkit/scripts/verify-package-artifact.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { constants } from "node:fs";
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { tmpdir } from "node:os";
import { basename, join, resolve } from "node:path";
Expand Down Expand Up @@ -140,6 +140,27 @@ try {
throw new Error("The installed gkit artifact could not read its provider documentation.");
}

// The bare bundle is the team-machine install: it must run with nothing next to it.
const bundleDirectory = join(consumerDirectory, "standalone");
await mkdir(bundleDirectory, { recursive: true });
const bundlePath = join(bundleDirectory, "gkit");
await copyFile(join(packageRoot, "dist", "gkit.js"), bundlePath);
await chmod(bundlePath, 0o755);
const standaloneEnvironment = { XDG_CACHE_HOME: join(consumerDirectory, "standalone-cache") };
await runCommand([bundlePath, "--schema", "gsc"], bundleDirectory, standaloneEnvironment);
const standaloneDocs = await runCommand(
[bundlePath, "docs", "--provider", "gsc"],
bundleDirectory,
standaloneEnvironment,
);
const standaloneCapabilities = await readFile(
join(standaloneDocs.stdout.trim(), "capabilities.md"),
"utf8",
);
if (!standaloneCapabilities.includes("gsc.properties.list")) {
throw new Error("The standalone gkit bundle could not read its embedded documentation.");
}

if (outputDirectory) {
await writeReleaseAssets(tarballPath, packageJson.version, outputDirectory);
}
Expand Down Expand Up @@ -193,12 +214,19 @@ async function writeReleaseAssets(
const stablePath = join(destination, stableName);
await copyFile(tarballPath, exactPath, constants.COPYFILE_EXCL);
await copyFile(tarballPath, stablePath, constants.COPYFILE_EXCL);
const bundleName = "gkit";
const bundlePath = join(destination, bundleName);
await copyFile(join(packageRoot, "dist", "gkit.js"), bundlePath, constants.COPYFILE_EXCL);
await chmod(bundlePath, 0o755);
const digest = createHash("sha256")
.update(await readFile(tarballPath))
.digest("hex");
const bundleDigest = createHash("sha256")
.update(await readFile(bundlePath))
.digest("hex");
await writeFile(
join(destination, "SHA256SUMS"),
`${digest} ${exactName}\n${digest} ${stableName}\n`,
`${digest} ${exactName}\n${digest} ${stableName}\n${bundleDigest} ${bundleName}\n`,
{ encoding: "utf8", flag: "wx" },
);
}
Expand Down
48 changes: 48 additions & 0 deletions packages/gkit/src/assets.embed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Every runtime-read asset, inlined into the standalone bundle (bun text/json imports).
// Keep in sync with generated/*/manifest.json and docs/providers/**; assets.test.ts fails on drift.

import a0 from "../docs/providers/bing/capabilities.md" with { type: "text" };
import a1 from "../docs/providers/bing/inventory.md" with { type: "text" };
import a2 from "../docs/providers/dataforseo/backlinks.md" with { type: "text" };
import a3 from "../docs/providers/dataforseo/inventory.md" with { type: "text" };
import a4 from "../docs/providers/google-ads/capabilities.md" with { type: "text" };
import a5 from "../docs/providers/google-ads/inventory.md" with { type: "text" };
import a6 from "../docs/providers/gsc/capabilities.md" with { type: "text" };
import a7 from "../docs/providers/gsc/inventory.md" with { type: "text" };
import a8 from "../docs/providers/hubspot/capabilities.md" with { type: "text" };
import a9 from "../docs/providers/hubspot/inventory.md" with { type: "text" };
import a10 from "../docs/providers/pagespeed/capabilities.md" with { type: "text" };
import a11 from "../docs/providers/pagespeed/inventory.md" with { type: "text" };
import a12 from "../docs/providers/posthog/capabilities.md" with { type: "text" };
import a13 from "../docs/providers/posthog/inventory.md" with { type: "text" };
import a14 from "../generated/bing/manifest.json" with { type: "json" };
import a15 from "../generated/dataforseo/manifest.json" with { type: "json" };
import a16 from "../generated/google-ads/manifest.json" with { type: "json" };
import a17 from "../generated/gsc/manifest.json" with { type: "json" };
import a18 from "../generated/hubspot/manifest.json" with { type: "json" };
import a19 from "../generated/pagespeed/manifest.json" with { type: "json" };
import a20 from "../generated/posthog/manifest.json" with { type: "json" };

export const embeddedAssets: Readonly<Record<string, string>> = {
"docs/providers/bing/capabilities.md": a0,
"docs/providers/bing/inventory.md": a1,
"docs/providers/dataforseo/backlinks.md": a2,
"docs/providers/dataforseo/inventory.md": a3,
"docs/providers/google-ads/capabilities.md": a4,
"docs/providers/google-ads/inventory.md": a5,
"docs/providers/gsc/capabilities.md": a6,
"docs/providers/gsc/inventory.md": a7,
"docs/providers/hubspot/capabilities.md": a8,
"docs/providers/hubspot/inventory.md": a9,
"docs/providers/pagespeed/capabilities.md": a10,
"docs/providers/pagespeed/inventory.md": a11,
"docs/providers/posthog/capabilities.md": a12,
"docs/providers/posthog/inventory.md": a13,
"generated/bing/manifest.json": `${JSON.stringify(a14, null, 2)}\n`,
"generated/dataforseo/manifest.json": `${JSON.stringify(a15, null, 2)}\n`,
"generated/google-ads/manifest.json": `${JSON.stringify(a16, null, 2)}\n`,
"generated/gsc/manifest.json": `${JSON.stringify(a17, null, 2)}\n`,
"generated/hubspot/manifest.json": `${JSON.stringify(a18, null, 2)}\n`,
"generated/pagespeed/manifest.json": `${JSON.stringify(a19, null, 2)}\n`,
"generated/posthog/manifest.json": `${JSON.stringify(a20, null, 2)}\n`,
};
26 changes: 26 additions & 0 deletions packages/gkit/src/assets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { readdirSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";


const root = fileURLToPath(new URL("..", import.meta.url));

function walk(dir: string): string[] {
return readdirSync(`${root}${dir}`, { withFileTypes: true }).flatMap((entry) =>
entry.isDirectory() ? walk(`${dir}/${entry.name}`) : [`${dir}/${entry.name}`],
);
}

describe("embedded assets", () => {
it("cover every runtime-read manifest and provider doc", () => {
const onDisk = [
...walk("generated").filter((path) => path.endsWith("/manifest.json")),
...walk("docs/providers"),
].sort();
// Read as text: vitest cannot import the bun text-import module itself.
const embedded = [...readFileSync(`${root}src/assets.embed.ts`, "utf8").matchAll(/^ {2}"([^"]+)":/gm)].map(
(match) => match[1],
);
expect(embedded.sort()).toEqual(onDisk);
});
});
42 changes: 42 additions & 0 deletions packages/gkit/src/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

// Set by `bun build --define` for the standalone release bundle (package.json `build`).
declare const GKIT_STANDALONE: boolean | undefined;

/**
* Root directory holding `generated/` and `docs/`.
*
* From source (and tests) that is the package root. The standalone bundle has no
* package root next to it, so its embedded copy is written once to a
* content-addressed cache directory and read from there — agents are handed real
* directories by `gkit docs`, and manifests keep loading from files.
*/
export async function resolveAssetRoot(): Promise<string> {
if (typeof GKIT_STANDALONE === "undefined" || !GKIT_STANDALONE) {
return fileURLToPath(new URL("..", import.meta.url));
}
const { embeddedAssets } = await import("./assets.embed");
const key = Bun.hash(JSON.stringify(embeddedAssets)).toString(36);
const cacheHome = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
const root = join(cacheHome, "gkit", "assets", key);
if (existsSync(join(root, ".complete"))) return root;

const staging = `${root}.tmp-${process.pid}`;
rmSync(staging, { recursive: true, force: true });
for (const [path, text] of Object.entries(embeddedAssets)) {
const target = join(staging, path);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, text);
}
writeFileSync(join(staging, ".complete"), "");
try {
renameSync(staging, root);
} catch {
// A concurrent run won the race with identical content.
rmSync(staging, { recursive: true, force: true });
}
return root;
}
64 changes: 24 additions & 40 deletions packages/gkit/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { fileURLToPath } from "node:url";
import { join } from "node:path";

import { resolveAssetRoot } from "./assets";

import { ensureTrailingNewline, formatBareSkill } from "@celados/argc/skill";

Expand Down Expand Up @@ -41,45 +43,27 @@ import { journalSummary, needsReconciliation, operationView } from "./operations
import { loadExecutableManifest } from "./manifest";
import { renderGkitSchema } from "./schema";

export const dataForSeoManifestPath = fileURLToPath(
new URL("../generated/dataforseo/manifest.json", import.meta.url),
);
export const bingManifestPath = fileURLToPath(
new URL("../generated/bing/manifest.json", import.meta.url),
);
export const bingDocsDirectory = fileURLToPath(new URL("../docs/providers/bing", import.meta.url));
export const dataForSeoDocsDirectory = fileURLToPath(
new URL("../docs/providers/dataforseo", import.meta.url),
);
export const postHogManifestPath = fileURLToPath(
new URL("../generated/posthog/manifest.json", import.meta.url),
);
export const postHogDocsDirectory = fileURLToPath(
new URL("../docs/providers/posthog", import.meta.url),
);
export const googleAdsManifestPath = fileURLToPath(
new URL("../generated/google-ads/manifest.json", import.meta.url),
);
export const googleAdsDocsDirectory = fileURLToPath(
new URL("../docs/providers/google-ads", import.meta.url),
);
export const gscManifestPath = fileURLToPath(
new URL("../generated/gsc/manifest.json", import.meta.url),
);
export const gscDocsDirectory = fileURLToPath(new URL("../docs/providers/gsc", import.meta.url));
export const hubSpotManifestPath = fileURLToPath(
new URL("../generated/hubspot/manifest.json", import.meta.url),
);
export const hubSpotDocsDirectory = fileURLToPath(
new URL("../docs/providers/hubspot", import.meta.url),
);
export const pageSpeedManifestPath = fileURLToPath(
new URL("../generated/pagespeed/manifest.json", import.meta.url),
);
export const pageSpeedDocsDirectory = fileURLToPath(
new URL("../docs/providers/pagespeed", import.meta.url),
);
export const providerDocsDirectory = fileURLToPath(new URL("../docs/providers", import.meta.url));
const assetRoot = await resolveAssetRoot();
const manifestPathFor = (provider: string): string =>
join(assetRoot, "generated", provider, "manifest.json");
const docsDirectoryFor = (provider: string): string =>
join(assetRoot, "docs", "providers", provider);

export const dataForSeoManifestPath = manifestPathFor("dataforseo");
export const bingManifestPath = manifestPathFor("bing");
export const bingDocsDirectory = docsDirectoryFor("bing");
export const dataForSeoDocsDirectory = docsDirectoryFor("dataforseo");
export const postHogManifestPath = manifestPathFor("posthog");
export const postHogDocsDirectory = docsDirectoryFor("posthog");
export const googleAdsManifestPath = manifestPathFor("google-ads");
export const googleAdsDocsDirectory = docsDirectoryFor("google-ads");
export const gscManifestPath = manifestPathFor("gsc");
export const gscDocsDirectory = docsDirectoryFor("gsc");
export const hubSpotManifestPath = manifestPathFor("hubspot");
export const hubSpotDocsDirectory = docsDirectoryFor("hubspot");
export const pageSpeedManifestPath = manifestPathFor("pagespeed");
export const pageSpeedDocsDirectory = docsDirectoryFor("pagespeed");
export const providerDocsDirectory = join(assetRoot, "docs", "providers");

type TerminalEmitter = {
writeText(text: string): Promise<void>;
Expand Down
Loading