Skip to content

Commit b46f659

Browse files
aglagocursoragent
andcommitted
Release v0.1.0: CLI --local, templates, and publishing docs.
Add --local for monorepo file: links, graceful Ctrl+C via withSpinner, template-bound module selection with optional customize, structure flag docs, and publishing guide plus restricted GitHub Packages publish. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a6c4878 commit b46f659

25 files changed

Lines changed: 821 additions & 171 deletions

.cursor/skills/genesis/SKILL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ genesis update
4141
| Monolith (default) | `--structure monolith` | Next.js app at repo root |
4242
| Monorepo | `--structure monorepo` | Turborepo, app in `apps/web/` |
4343

44-
| Template | Default modules |
45-
|----------|-----------------|
46-
| `custom` | none |
47-
| `informational-site` | branding |
48-
| `saas-app` | auth, branding, payments, dashboard |
49-
| `ecommerce` | payments, dashboard |
44+
| Template | Bundled modules | Module selection |
45+
|----------|-----------------|------------------|
46+
| `custom` | none | Full picker always |
47+
| `informational-site` | branding | Auto; Customize? for emails/analytics. Blocks auth, payments, dashboard |
48+
| `saas-app` | auth, branding, payments, dashboard, notifications | Auto; Customize? for emails, uploads, analytics |
49+
| `ecommerce` | payments, dashboard | Auto; Customize? for auth, branding, uploads, etc. |
5050

5151
**Monorepo dev CLI:** `node cli/dist/index.js create my-app` (after `npm run build` at repo root)
5252

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ jobs:
2525

2626
- run: npm run build
2727

28-
- run: npm publish --workspaces --if-present
28+
- run: npm publish --workspaces --if-present --access restricted
2929
env:
3030
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.npmrc.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Point @genesis packages at GitHub Packages.
2+
# Copy to your project root or ~/.npmrc and set GITHUB_TOKEN.
3+
4+
@genesis:registry=https://npm.pkg.github.com
5+
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ Enable developers to create production-ready web applications in minutes by asse
2121
npm install
2222
npm run build
2323

24-
# Create a new project
25-
node cli/dist/index.js create my-app
24+
# Create a new project (link local packages while unpublished)
25+
node cli/dist/index.js create my-app --local
2626

27-
# Or after publishing
27+
# Or after publishing to GitHub Packages (see docs/publishing.md)
2828
npx @genesis/cli create my-app
2929
```
3030

USAGE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Documentation index for using Genesis. Detailed guides live in the [`docs/`](doc
1212
| [docs/configuration.md](docs/configuration.md) | Environment variables and `genesis.config.ts` |
1313
| [docs/workflows.md](docs/workflows.md) | Portfolio, SaaS, e-commerce, incremental setups |
1414
| [docs/troubleshooting.md](docs/troubleshooting.md) | Common errors and fixes |
15+
| [docs/publishing.md](docs/publishing.md) | Local `--local` linking, Git tags, GitHub Packages |
1516

1617
## Modules
1718

@@ -29,11 +30,11 @@ Documentation index for using Genesis. Detailed guides live in the [`docs/`](doc
2930
## Quick Reference
3031

3132
```bash
32-
# Create a project
33-
genesis create my-app
33+
# Create a project (local monorepo — while packages are unpublished)
34+
node cli/dist/index.js create my-app --local
3435

3536
# Non-interactive SaaS starter
36-
genesis create acme -y -t saas-app
37+
genesis create acme -y -t saas-app --local
3738

3839
# Add a module to an existing project
3940
genesis add notifications

cli/src/__tests__/exit.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, it, expect } from "vitest";
2+
import { isUserCancellation } from "../utils/exit.js";
3+
4+
describe("exit utils", () => {
5+
it("detects ExitPromptError from inquirer", () => {
6+
expect(isUserCancellation({ name: "ExitPromptError", message: "User force closed the prompt" })).toBe(true);
7+
});
8+
9+
it("detects force closed prompt message", () => {
10+
expect(isUserCancellation(new Error("User force closed the prompt with 0 null"))).toBe(true);
11+
});
12+
13+
it("returns false for regular errors", () => {
14+
expect(isUserCancellation(new Error("Something broke"))).toBe(false);
15+
expect(isUserCancellation(null)).toBe(false);
16+
});
17+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, it, expect } from "vitest";
2+
import fs from "fs-extra";
3+
import path from "path";
4+
import os from "os";
5+
import { resolveLocalPackageRef, usesLocalPackages } from "../utils/local-packages.js";
6+
import { getGenesisRoot } from "../utils/manifests.js";
7+
8+
describe("local-packages", () => {
9+
it("resolves file: paths relative to the target app", () => {
10+
const genesisRoot = getGenesisRoot();
11+
const targetDir = path.join(genesisRoot, "tmp-test-app");
12+
13+
const ref = resolveLocalPackageRef(targetDir, "@genesis/branding");
14+
expect(ref).toBe("file:../packages/branding");
15+
});
16+
17+
it("detects file: linked dependencies", async () => {
18+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "genesis-local-"));
19+
await fs.writeJson(path.join(dir, "package.json"), {
20+
dependencies: {
21+
"@genesis/core": "file:../packages/core",
22+
next: "^15.0.0",
23+
},
24+
});
25+
26+
expect(usesLocalPackages(dir)).toBe(true);
27+
await fs.remove(dir);
28+
});
29+
30+
it("returns false when using registry versions", async () => {
31+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "genesis-local-"));
32+
await fs.writeJson(path.join(dir, "package.json"), {
33+
dependencies: { "@genesis/core": "*" },
34+
});
35+
36+
expect(usesLocalPackages(dir)).toBe(false);
37+
await fs.remove(dir);
38+
});
39+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, it, expect } from "vitest";
2+
import {
3+
getTemplateDefinition,
4+
resolveModulesFromFlag,
5+
mergeTemplateModules,
6+
getSelectableModulesForTemplate,
7+
} from "../utils/templates.js";
8+
9+
describe("template definitions", () => {
10+
it("informational-site requires branding and excludes auth", () => {
11+
const def = getTemplateDefinition("informational-site");
12+
expect(def.requiredModules).toEqual(["branding"]);
13+
expect(def.excludedModules).toContain("auth");
14+
expect(def.excludedModules).toContain("payments");
15+
});
16+
17+
it("saas-app includes notifications per PRD", () => {
18+
const def = getTemplateDefinition("saas-app");
19+
expect(def.requiredModules).toContain("notifications");
20+
expect(def.requiredModules).toContain("auth");
21+
});
22+
23+
it("custom allows free choice", () => {
24+
expect(getTemplateDefinition("custom").freeChoice).toBe(true);
25+
});
26+
27+
it("merges required modules when customizing", () => {
28+
const result = mergeTemplateModules("informational-site", ["branding", "emails"]);
29+
expect(result).toEqual(["branding", "emails"]);
30+
});
31+
32+
it("blocks excluded modules when customizing", () => {
33+
const result = mergeTemplateModules("informational-site", ["branding", "auth"]);
34+
expect(result).toEqual(["branding"]);
35+
});
36+
37+
it("resolveModulesFromFlag always includes required", () => {
38+
const result = resolveModulesFromFlag("ecommerce", ["auth"]);
39+
expect(result).toContain("payments");
40+
expect(result).toContain("dashboard");
41+
expect(result).toContain("auth");
42+
});
43+
44+
it("resolveModulesFromFlag filters excluded on informational", () => {
45+
const result = resolveModulesFromFlag("informational-site", ["auth", "emails"]);
46+
expect(result).toEqual(["branding", "emails"]);
47+
});
48+
49+
it("getSelectableModulesForTemplate hides excluded", () => {
50+
const selectable = getSelectableModulesForTemplate("informational-site", [
51+
"auth",
52+
"branding",
53+
"emails",
54+
"payments",
55+
]);
56+
expect(selectable).toContain("branding");
57+
expect(selectable).toContain("emails");
58+
expect(selectable).not.toContain("auth");
59+
expect(selectable).not.toContain("payments");
60+
});
61+
});

cli/src/commands/add.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import path from "path";
22
import chalk from "chalk";
3-
import ora from "ora";
43
import { confirm } from "@inquirer/prompts";
54
import type { ModuleId } from "@genesis/core";
65
import { resolveModuleOrder } from "@genesis/core";
@@ -13,8 +12,14 @@ import {
1312
} from "../utils/scaffold.js";
1413
import { linkGenesisPackages } from "../utils/template.js";
1514
import { findGenesisAppDir } from "../utils/structure.js";
15+
import { withSpinner } from "../utils/exit.js";
16+
import { usesLocalPackages } from "../utils/local-packages.js";
1617

17-
export async function addCommand(moduleName: string): Promise<void> {
18+
export interface AddOptions {
19+
local?: boolean;
20+
}
21+
22+
export async function addCommand(moduleName: string, options: AddOptions = {}): Promise<void> {
1823
const manifests = await loadAllManifests();
1924
const manifest = getManifestById(moduleName as ModuleId);
2025

@@ -38,14 +43,18 @@ export async function addCommand(moduleName: string): Promise<void> {
3843
if (missingDeps.length > 0) {
3944
console.log(chalk.yellow(`Installing dependencies first: ${missingDeps.join(", ")}`));
4045
for (const dep of missingDeps) {
41-
await addCommand(dep);
46+
await addCommand(dep, options);
4247
}
4348
}
4449

45-
const spinner = ora(`Adding ${manifest.name}...`).start();
50+
const useLocal = options.local ?? usesLocalPackages(targetDir);
4651

47-
try {
48-
linkGenesisPackages(targetDir, [manifest.npmPackage, "@genesis/core", "@genesis/database", "@genesis/ui"]);
52+
await withSpinner(`Adding ${manifest.name}...`, async (spinner) => {
53+
linkGenesisPackages(
54+
targetDir,
55+
[manifest.npmPackage, "@genesis/core", "@genesis/database", "@genesis/ui"],
56+
{ local: useLocal },
57+
);
4958

5059
const allModuleIds = [...existing, moduleName] as ModuleId[];
5160
const orderedIds = resolveModuleOrder(allModuleIds, manifests);
@@ -75,8 +84,5 @@ export async function addCommand(moduleName: string): Promise<void> {
7584
console.log(chalk.dim(` ${envVar.key}${envVar.description}`));
7685
}
7786
}
78-
} catch (error) {
79-
spinner.fail(chalk.red(`Failed to add ${moduleName}`));
80-
throw error;
81-
}
87+
});
8288
}

0 commit comments

Comments
 (0)