@openora/create 0.4.1-canary.35 → 0.4.1-canary.351
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/dist/.tsbuildinfo +1 -1
- package/dist/generated/versions.d.ts +11 -0
- package/dist/generated/versions.d.ts.map +1 -0
- package/dist/generated/versions.js +11 -0
- package/dist/generated/versions.js.map +1 -0
- package/dist/index.js +48 -93
- package/dist/index.js.map +1 -1
- package/dist/render.d.ts +15 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +125 -0
- package/dist/render.js.map +1 -0
- package/package.json +8 -6
- package/template/README.md.tpl +12 -7
- package/template/__dot__claude/settings.json.tpl +22 -12
- package/template/__dot__env.example +10 -0
- package/template/__dot__gitignore +5 -0
- package/template/__dot__rulesync/commands/check.md +11 -10
- package/template/__dot__rulesync/commands/doctor.md.tpl +17 -0
- package/template/__dot__rulesync/commands/scaffold-module.md +3 -2
- package/template/__dot__rulesync/commands/scaffold-plugin.md +19 -3
- package/template/__dot__rulesync/commands/scaffold-route.md +1 -0
- package/template/__dot__rulesync/commands/start.md +2 -2
- package/template/__dot__rulesync/hooks/_shared.mjs +26 -13
- package/template/__dot__rulesync/hooks/guard-core.mjs.tpl +76 -28
- package/template/__dot__rulesync/hooks/guard-generated.mjs +15 -13
- package/template/__dot__rulesync/hooks/guard-mr.mjs +81 -0
- package/template/__dot__rulesync/hooks/guard-subagent.mjs +22 -6
- package/template/__dot__rulesync/hooks/post-edit.mjs.tpl +116 -0
- package/template/__dot__rulesync/hooks.json +27 -4
- package/template/__dot__rulesync/rules/conventions.md +99 -119
- package/template/__dot__rulesync/rules/db-conventions.md +6 -72
- package/template/__dot__rulesync/rules/e2e-conventions.md +33 -7
- package/template/__dot__rulesync/rules/frontend-conventions.md.tpl +35 -0
- package/template/__dot__rulesync/rules/oss-boundaries.md.tpl +78 -0
- package/template/__dot__rulesync/rules/overview.md +18 -82
- package/template/__dot__rulesync/skills/create-plugin/{SKILL.md → SKILL.md.tpl} +12 -23
- package/template/__dot__rulesync/skills/create-pr/SKILL.md.tpl +38 -0
- package/template/__dot__rulesync/skills/create-task/{SKILL.md → SKILL.md.tpl} +14 -22
- package/template/__dot__rulesync/skills/create-ui-module/{SKILL.md → SKILL.md.tpl} +12 -10
- package/template/__dot__rulesync/skills/enhance-prompt/{SKILL.md → SKILL.md.tpl} +4 -4
- package/template/__dot__rulesync/skills/handoff/SKILL.md +95 -0
- package/template/__dot__rulesync/skills/review/SKILL.md.tpl +277 -0
- package/template/__dot__rulesync/skills/ship/SKILL.md.tpl +157 -0
- package/template/__dot__rulesync/skills/ship/handoff.md.tpl +48 -0
- package/template/__dot__rulesync/skills/ship/watch-pr.mjs +172 -0
- package/template/__dot__rulesync/skills/ui-verify/SKILL.md.tpl +120 -0
- package/template/__dot__rulesync/skills/ui-verify/layout-check.check.sh +58 -0
- package/template/__dot__rulesync/skills/ui-verify/layout-check.js +163 -0
- package/template/__dot__rulesync/subagents/builder.md.tpl +115 -0
- package/template/__dot__rulesync/subagents/compliance-reviewer.md +78 -0
- package/template/__dot__rulesync/subagents/debugger.md.tpl +86 -0
- package/template/__dot__rulesync/subagents/deployer.md.tpl +54 -0
- package/template/__dot__rulesync/subagents/expert.md +28 -34
- package/template/__dot__rulesync/subagents/planner.md.tpl +39 -0
- package/template/__dot__rulesync/subagents/quality-reviewer.md.tpl +112 -0
- package/template/__dot__rulesync/subagents/security-reviewer.md.tpl +85 -0
- package/template/__dot__rulesync/subagents/tester.md.tpl +102 -0
- package/template/__dot__rulesync/sync.json.tpl +26 -0
- package/template/apps/api/package.json.tpl +2 -2
- package/template/apps/api/src/extensions.config.ts.tpl +12 -0
- package/template/apps/api/src/main.ts.tpl +1 -4
- package/template/apps/api/src/seed.ts.tpl +8 -1
- package/template/docker-compose.yml +13 -0
- package/template/docs/agents/cross-repo.md.tpl +37 -0
- package/template/docs/agents/forge.md.tpl +54 -0
- package/template/docs/agents/issue-tracker.md.tpl +42 -0
- package/template/docs/standards/comments.md +16 -0
- package/template/docs/standards/database.md +77 -0
- package/template/docs/standards/enforcement.md +12 -0
- package/template/docs/standards/errors.md +30 -0
- package/template/docs/standards/frontend.md +60 -0
- package/template/docs/standards/functions.md +38 -0
- package/template/docs/standards/git-delivery.md +20 -0
- package/template/docs/standards/module-structure.md +37 -0
- package/template/docs/standards/testing.md +88 -0
- package/template/docs/standards/types.md +31 -0
- package/template/package.json.tpl +15 -9
- package/template/rulesync.jsonc +3 -3
- package/template/tools/oss-worktree.mjs.tpl +105 -0
- package/template/tools/review-precheck.mjs +531 -0
- package/template/tools/sync-agents.mjs +257 -0
- package/dist/generated/core-version.d.ts +0 -2
- package/dist/generated/core-version.d.ts.map +0 -1
- package/dist/generated/core-version.js +0 -2
- package/dist/generated/core-version.js.map +0 -1
- package/template/__dot__rulesync/commands/doctor.md +0 -16
- package/template/__dot__rulesync/hooks/post-edit.mjs +0 -57
- package/template/__dot__rulesync/rules/oss-boundaries.md +0 -31
- package/template/__dot__rulesync/skills/add-feature/SKILL.md +0 -113
- package/template/__dot__rulesync/skills/add-feature/handoff.md +0 -58
- package/template/__dot__rulesync/skills/create-pr/SKILL.md +0 -55
- package/template/__dot__rulesync/skills/review/SKILL.md +0 -110
- package/template/__dot__rulesync/subagents/builder.md +0 -93
- package/template/__dot__rulesync/subagents/debugger.md +0 -82
- package/template/__dot__rulesync/subagents/deployer.md +0 -66
- package/template/__dot__rulesync/subagents/qa.md +0 -88
- package/template/__dot__rulesync/subagents/quality-reviewer.md +0 -67
- package/template/__dot__rulesync/subagents/security-reviewer.md +0 -54
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Git and delivery
|
|
2
|
+
|
|
3
|
+
Read this before a commit, PR, or branch operation.
|
|
4
|
+
|
|
5
|
+
- Conventional commits, enforced by commitlint (husky locally, CI on the PR/MR title): `feat`, `fix`, `refactor`, `chore`,
|
|
6
|
+
`docs`, `test`, `ci`, `perf`. E.g. `feat(wallet): atomic debit command port`.
|
|
7
|
+
- Subject starts lowercase, acronyms included (`feat(pam): kyc status filter`). This applies to
|
|
8
|
+
the PR title too - squash merges derive the commit message from it.
|
|
9
|
+
- One PR = one concern. Stage files explicitly; never `git add -A` when foreign changes are in
|
|
10
|
+
the tree.
|
|
11
|
+
- Green before review: `pnpm verify` on the committed tree. It is the full gate and records the
|
|
12
|
+
verified commit; the `guard-mr` hook blocks opening the PR/MR without it.
|
|
13
|
+
- Branch off `dev`; never commit directly to a shared branch; never push without an explicit
|
|
14
|
+
per-action "yes push", except the ticket branch `/ship` opens its MR from.
|
|
15
|
+
- PR description carries intent: what / why / acceptance criteria / ticket link. No secrets,
|
|
16
|
+
internal hostnames, or PII - it is the public record.
|
|
17
|
+
- A PR that adds or changes a flow across components (request path, state machine, async job or
|
|
18
|
+
webhook, a new module, overlay or vendor binding) carries a Mermaid `flowchart` or
|
|
19
|
+
`sequenceDiagram` of it: nodes named as in the code, under ~15 nodes, refusal and failure
|
|
20
|
+
branches included. No flow, no diagram.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Package and module structure
|
|
2
|
+
|
|
3
|
+
Read this before creating a package, an overlay add-on, or wiring a new module's internal layout.
|
|
4
|
+
|
|
5
|
+
- One package = one concern, named `@<scope>/<kebab>`, with an explicit `exports` map. The
|
|
6
|
+
entrypoint IS the public API; everything else is internal and off-limits to consumers
|
|
7
|
+
(`oss-boundaries`).
|
|
8
|
+
- Overlay/add-on packages mirror the platform module shape: `contract/`, `schema/`, `service/`,
|
|
9
|
+
`router/`, `adapters/`, `__tests__/`, plus `plugin.ts` at the root as the single wiring point.
|
|
10
|
+
|
|
11
|
+
| Layer | File | Holds | Must NOT hold |
|
|
12
|
+
| -------- | --------------------------- | --------------------------------------------------------------- | -------------------------------- |
|
|
13
|
+
| schema | `schema/index.ts` | Drizzle `pgTable`s; row types via `$inferSelect`/`$inferInsert` | logic |
|
|
14
|
+
| contract | `contract/index.ts` | oRPC route contract + req/res Zod schemas | logic, transport wiring |
|
|
15
|
+
| service | `service/<name>.service.ts` | ALL business logic; emits events after DB commit | HTTP/transport knowledge |
|
|
16
|
+
| router | `router/index.ts` | thin oRPC wiring: resolve caller, call service, map errors | business rules |
|
|
17
|
+
| plugin | `plugin.ts` | DI wiring only: `ctx.provide(...)`, route registration | logic |
|
|
18
|
+
| adapters | `adapters/<vendor>/` | concrete impls of adapter ports | being imported by another module |
|
|
19
|
+
|
|
20
|
+
- Each overlay owns its `drizzle.config.ts` and its own migration history - never share one
|
|
21
|
+
migration folder across packages.
|
|
22
|
+
- Pin exact dependency versions in every package; a package never depends on an app.
|
|
23
|
+
- Add a dependency deliberately - std lib or a few lines often beat a tree.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// bad - a service reaches into the container and hides what it depends on
|
|
27
|
+
export class WalletService {
|
|
28
|
+
constructor(private readonly container: Container) {}
|
|
29
|
+
}
|
|
30
|
+
// good - deps are constructor params of their port type; plugin.ts does the resolving
|
|
31
|
+
export class WalletService {
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly db: DrizzleService,
|
|
34
|
+
private readonly payments: PaymentAdapter,
|
|
35
|
+
) {}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Read this before adding or restructuring a test.
|
|
4
|
+
|
|
5
|
+
## When a test is written
|
|
6
|
+
|
|
7
|
+
A feature or fix PR carries **its own tests**, written in the same branch from the manual
|
|
8
|
+
verification pass that proves the change works. There is no separate test PR.
|
|
9
|
+
|
|
10
|
+
Before the PR opens:
|
|
11
|
+
|
|
12
|
+
1. Drive the change against the running stack. Reach for the **Playwright CLI** first
|
|
13
|
+
(`playwright-cli`, or a throwaway spec that clicks through the flow) - it costs a fraction of
|
|
14
|
+
the tokens a browser MCP does. Use a browser MCP only for a live console or network read the CLI
|
|
15
|
+
cannot give you. No videos in this pass, except a fix's "before" clip, taken when you reproduce
|
|
16
|
+
the bug: the code can still change before review is clean. Every changed screen gets the
|
|
17
|
+
`ui-verify` skill's screenshot review: four shots (mobile and desktop, light and dark), each
|
|
18
|
+
read for layout defects and given a PASS or FAIL line. Screenshots are for the agent, videos
|
|
19
|
+
for the reviewer.
|
|
20
|
+
2. Capture the evidence once, on the final commit, after the tests are green and review is clean:
|
|
21
|
+
one short video per acceptance criterion, ending on the outcome it names, and the "after" twin
|
|
22
|
+
of a fix's "before" clip. Run every browser spec that drives an AC in one command with video on
|
|
23
|
+
(`test.use({ video: 'on' })` or the repo's record switch); `playwright-cli` with `video-start` /
|
|
24
|
+
`video-stop` only for an AC no spec drives. An API-only change attaches the request/response
|
|
25
|
+
trace instead.
|
|
26
|
+
3. Attach that evidence **to the PR description**, not only to your report - a reviewer confirms a
|
|
27
|
+
change by looking at it.
|
|
28
|
+
4. Write the tests that pass exercised, in this branch, at the tier the table below names. Include
|
|
29
|
+
the hostile paths and the authz negatives. A fix carries the test that fails without it.
|
|
30
|
+
5. End the description with a **"Tests"** list: one line per test added, naming its tier
|
|
31
|
+
(unit / API E2E / browser E2E) and path.
|
|
32
|
+
|
|
33
|
+
A changed behaviour with no test is a review `[WARN]`, never a `[BLOCK]`, except on the paths below.
|
|
34
|
+
|
|
35
|
+
## Tests that catch the bug
|
|
36
|
+
|
|
37
|
+
A test that still passes when the code it covers is broken checks nothing. On a money, KYC,
|
|
38
|
+
responsible-gambling or authz path, break each changed check on purpose (flip a condition, move a
|
|
39
|
+
boundary by one, drop an owner check, change the sign of an amount) and run the tests: at least one
|
|
40
|
+
must fail. The tester does this in its pass and lists each break as a mutation row in its verdict
|
|
41
|
+
table; a break no test catches on those paths holds the PR until a test catches it. Where the repo
|
|
42
|
+
has a `test:mutation` script, it does the breaking for you; the repo-wide mutation score is shown,
|
|
43
|
+
not a gate.
|
|
44
|
+
|
|
45
|
+
## Pick the tier
|
|
46
|
+
|
|
47
|
+
Pick the OUTERMOST tier that can reach the behaviour - a test earns its keep by running real code,
|
|
48
|
+
not by being cheap.
|
|
49
|
+
|
|
50
|
+
| What you changed | Tier |
|
|
51
|
+
| ------------------------------------------------------------- | --------------------------------------------- |
|
|
52
|
+
| A screen, a form, a player/admin journey | Browser E2E (`apps/e2e/tests/<app>/**`) |
|
|
53
|
+
| An API route, an overlay, a vendor adapter, anything with SQL | API E2E (`apps/e2e/tests/api/**`) |
|
|
54
|
+
| A pure function - parser, resolver, mapper, money calculation | Unit (`src/__tests__/<name>.test.ts`, Vitest) |
|
|
55
|
+
|
|
56
|
+
The API tier drives the real API over HTTP against real Postgres, with each external vendor
|
|
57
|
+
replaced by a stub HTTP server the API is pointed at by env. It is the default tier for
|
|
58
|
+
`apps/api/src/extensions/**`: an overlay is wiring, and wiring is exactly what a unit test skips.
|
|
59
|
+
|
|
60
|
+
## What is real, what is doubled
|
|
61
|
+
|
|
62
|
+
- **Anything that touches the database is tested against real Postgres.** Never fake a query
|
|
63
|
+
builder: a stubbed chain proves a call order, not a result, so it misses the regressions that
|
|
64
|
+
matter - a unique-index conflict, a wrong `where`, a lost race, a rollback that never happened.
|
|
65
|
+
- **External vendors are stubbed at their HTTP boundary** (a `node:http` stand-in the API's
|
|
66
|
+
base-URL env var points at), never by mocking our own adapter - a mocked adapter skips exactly
|
|
67
|
+
the wiring the test exists to check. Every vendor adapter therefore accepts a base-URL override.
|
|
68
|
+
- **A spy assertion is never the point of a test.** `expect(client.x).toHaveBeenCalledWith(...)`
|
|
69
|
+
only restates the line above it. A spy may stand in for an outbound vendor call, never for the
|
|
70
|
+
thing under test.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// bad - a mocked builder chain proves a call order, not a result
|
|
74
|
+
const db = { select: vi.fn().mockReturnValue({ from: vi.fn().mockResolvedValue([row]) }) };
|
|
75
|
+
expect(db.select).toHaveBeenCalled();
|
|
76
|
+
// good - assert the outcome
|
|
77
|
+
expect(await service.get(id)).toEqual(row);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## How to write them
|
|
81
|
+
|
|
82
|
+
- Test behavior, not implementation - tests must survive a safe refactor (assert outputs, not
|
|
83
|
+
private caches).
|
|
84
|
+
- Cover the change in the same PR as the change; always include the authz negatives.
|
|
85
|
+
- Drive a vendor's inbound side the way the vendor does: post the real webhook shape to the real
|
|
86
|
+
route with a signature the stub's key material produces. Never call the adapter directly.
|
|
87
|
+
- Deterministic and isolated: no shared mutable state, no real outbound network, seedable data.
|
|
88
|
+
Own the rows a test creates and clean them up - the API suite shares one database.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Types
|
|
2
|
+
|
|
3
|
+
Read this before adding or changing a schema, type, or enum-like value set.
|
|
4
|
+
|
|
5
|
+
- One source of truth per shape - infer, never hand-write a type that already exists:
|
|
6
|
+
`z.infer<typeof UserSchema>`, `typeof users.$inferSelect`, `Omit<User, 'id'>`.
|
|
7
|
+
- Schema-first at every boundary (HTTP, config, env, messages, events): validate once at the edge,
|
|
8
|
+
trust the type after. oRPC + Zod does this for routes; do the same elsewhere.
|
|
9
|
+
- Never re-infer a schema you imported - the owning contract exports the type once; import it.
|
|
10
|
+
- No `any` outside tests - `unknown` + narrowing.
|
|
11
|
+
- No `as` casts to silence the compiler - fix the root cause (`as const` is fine).
|
|
12
|
+
- Never `!` non-null assertions - narrow explicitly, or restructure so the value is provably
|
|
13
|
+
present (carry it on the object instead of re-looking it up).
|
|
14
|
+
- Under `noUncheckedIndexedAccess`: `.at()`, destructure-with-default
|
|
15
|
+
(`const [first = ''] = parts`), or an explicit guard - never `arr[i]!`.
|
|
16
|
+
- `type` over `interface` (lint-enforced).
|
|
17
|
+
- Type entity ids through their owning type (`playerId: Player['id']`), never a bare `string`.
|
|
18
|
+
- Derive related schemas with `.pick()/.omit()/.partial()/.extend()/.merge()` - never re-type
|
|
19
|
+
fields.
|
|
20
|
+
- Enum-like value sets are a values + schema + type triple declared once on the contract surface:
|
|
21
|
+
`X_STATUSES = [...] as const` -> `XStatusSchema = z.enum(X_STATUSES)` -> inferred `XStatus`.
|
|
22
|
+
Never a TS `enum`, never a second hand-typed copy - import the one from `@openora/*`.
|
|
23
|
+
- Make illegal states unrepresentable - discriminated unions
|
|
24
|
+
(`{ status: 'ok'; data } | { status: 'error'; error }`) over optional-flag soup.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// bad - hand-written duplicate of an inferrable type
|
|
28
|
+
type User = { id: string; email: string; roles: string[] };
|
|
29
|
+
// good - infer from the schema that already validates the shape
|
|
30
|
+
type User = z.infer<typeof UserSchema>;
|
|
31
|
+
```
|
|
@@ -8,25 +8,31 @@
|
|
|
8
8
|
"build": "turbo run build",
|
|
9
9
|
"check:types": "turbo run check:types",
|
|
10
10
|
"check:lint": "oxlint .",
|
|
11
|
+
"test:unit": "turbo run test:unit",
|
|
12
|
+
"verify": "turbo run check:types check:lint test:unit && node .rulesync/hooks/guard-mr.mjs --stamp",
|
|
11
13
|
"gen:agents": "rulesync generate",
|
|
12
|
-
"
|
|
14
|
+
"sync:agents": "node tools/sync-agents.mjs",
|
|
15
|
+
"oss:worktree": "node tools/oss-worktree.mjs",
|
|
16
|
+
"review:precheck": "node tools/review-precheck.mjs",
|
|
17
|
+
"prepare": "node tools/sync-agents.mjs && rulesync generate",
|
|
13
18
|
"db:migrate": "pnpm -F @{{name}}/api exec openora-migrate",
|
|
14
19
|
"db:seed": "pnpm -F @{{name}}/api db:seed",
|
|
15
20
|
"gen": "turbo gen"
|
|
16
21
|
},
|
|
17
22
|
"devDependencies": {
|
|
23
|
+
"@openora/create": "{{coreVersion}}",
|
|
18
24
|
"@openora/mcp": "latest",
|
|
19
|
-
"@turbo/gen": "
|
|
20
|
-
"@types/node": "
|
|
21
|
-
"oxlint": "
|
|
22
|
-
"rulesync": "
|
|
23
|
-
"tsx": "
|
|
24
|
-
"turbo": "
|
|
25
|
-
"typescript": "
|
|
25
|
+
"@turbo/gen": "{{turboVersion}}",
|
|
26
|
+
"@types/node": "{{typesNodeVersion}}",
|
|
27
|
+
"oxlint": "{{oxlintVersion}}",
|
|
28
|
+
"rulesync": "{{rulesyncVersion}}",
|
|
29
|
+
"tsx": "{{tsxVersion}}",
|
|
30
|
+
"turbo": "{{turboVersion}}",
|
|
31
|
+
"typescript": "{{typescriptVersion}}"
|
|
26
32
|
},
|
|
27
33
|
"engines": {
|
|
28
34
|
"node": ">=22.0.0",
|
|
29
35
|
"pnpm": ">=10.0.0"
|
|
30
36
|
},
|
|
31
|
-
"packageManager": "
|
|
37
|
+
"packageManager": "{{packageManager}}"
|
|
32
38
|
}
|
package/template/rulesync.jsonc
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
// AGENTS.md stays a clean human brief (no simulated-command boilerplate).
|
|
9
9
|
// Hooks + permissions are NOT managed here - .claude/settings.json is hand-maintained
|
|
10
10
|
// (it holds the guard hooks), so rulesync never clobbers it.
|
|
11
|
-
// codexcli: rules (AGENTS.md)
|
|
12
|
-
// subagents are simulation-only (kept off)
|
|
11
|
+
// codexcli: rules (AGENTS.md), mcp (.codex/config.toml) and hooks (.codex/hooks.json, so
|
|
12
|
+
// the guards judge its apply_patch edits) - its commands/subagents are simulation-only (kept off).
|
|
13
13
|
"targets": {
|
|
14
14
|
"agentsmd": ["rules"],
|
|
15
15
|
"claudecode": ["rules", "mcp", "subagents", "commands", "skills"],
|
|
16
16
|
"copilot": ["rules", "mcp", "subagents", "commands", "hooks"],
|
|
17
|
-
"codexcli": ["rules", "mcp"],
|
|
17
|
+
"codexcli": ["rules", "mcp", "hooks"],
|
|
18
18
|
},
|
|
19
19
|
|
|
20
20
|
// rulesync emits Copilot agents/prompts natively - no simulation needed.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// A git worktree of the sibling OSS checkout - the only place an agent in this repo may change
|
|
3
|
+
// OSS code (the guard-core hook allows {{ossFromRoot}}/.worktrees/ and denies the rest of the
|
|
4
|
+
// checkout). Same command for every agent and every human, so a paired change looks the same
|
|
5
|
+
// on every machine.
|
|
6
|
+
//
|
|
7
|
+
// pnpm oss:worktree <branch> create or reuse the worktree, install its deps
|
|
8
|
+
// pnpm oss:worktree <branch> --link ...then build it and point `pnpm link:oss` at it
|
|
9
|
+
// pnpm oss:worktree <branch> --remove remove it; relinks the main checkout if it was linked
|
|
10
|
+
//
|
|
11
|
+
// Use the same branch name as this repo's branch: that is how skills pair the two requests.
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { join, relative, resolve } from 'node:path';
|
|
15
|
+
// oxlint-disable no-console
|
|
16
|
+
|
|
17
|
+
// This repo is often checked out as a git worktree, whose `.git` is a file pointing at
|
|
18
|
+
// <main>/.git/worktrees/<name>. The sibling OSS checkout sits next to the MAIN checkout, so
|
|
19
|
+
// resolve from there or every path below lands inside the worktree.
|
|
20
|
+
const mainCheckoutOf = (dir) => {
|
|
21
|
+
try {
|
|
22
|
+
const pointer = readFileSync(join(dir, '.git'), 'utf8');
|
|
23
|
+
const gitdir = pointer.match(/^gitdir:\s*(.*[/\\]worktrees[/\\][^/\\]+?)\s*$/m)?.[1];
|
|
24
|
+
return gitdir ? resolve(dir, gitdir, '..', '..', '..') : dir;
|
|
25
|
+
} catch {
|
|
26
|
+
return dir; // `.git` is a directory (the main checkout) or missing
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const OSS = resolve(mainCheckoutOf(process.cwd()), '{{ossFromRoot}}');
|
|
31
|
+
const USAGE = 'usage: pnpm oss:worktree <branch> [--link | --remove]';
|
|
32
|
+
|
|
33
|
+
const die = (message) => {
|
|
34
|
+
console.error(`oss:worktree - ${message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const [branch, flag] = process.argv.slice(2);
|
|
39
|
+
if (!branch || branch.startsWith('--') || (flag && !['--link', '--remove'].includes(flag))) {
|
|
40
|
+
die(USAGE);
|
|
41
|
+
}
|
|
42
|
+
if (!existsSync(join(OSS, '.git'))) {
|
|
43
|
+
die(`no OSS checkout at ${OSS} - clone it next to this repo first.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const worktree = join(OSS, '.worktrees', branch.replaceAll('/', '+'));
|
|
47
|
+
const worktreeFromHere = relative(process.cwd(), worktree);
|
|
48
|
+
// A fresh worktree has no node_modules yet, so the checkout's own post-checkout hook would run
|
|
49
|
+
// against tools it cannot resolve and fail the command. The install below does that setup.
|
|
50
|
+
const GIT = ['-C', OSS, '-c', 'core.hooksPath=/dev/null'];
|
|
51
|
+
const git = (...args) =>
|
|
52
|
+
execFileSync('git', [...GIT, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] }).trim();
|
|
53
|
+
const gitOrEmpty = (...args) => {
|
|
54
|
+
try {
|
|
55
|
+
return execFileSync('git', [...GIT, ...args], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
56
|
+
} catch {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const run = (cmd, args) => execFileSync(cmd, args, { stdio: 'inherit' });
|
|
61
|
+
const hasLinkScript = () =>
|
|
62
|
+
Boolean(JSON.parse(readFileSync('package.json', 'utf8')).scripts?.['link:oss']);
|
|
63
|
+
const isLinkedTo = (dir) =>
|
|
64
|
+
existsSync('pnpm-workspace.yaml') &&
|
|
65
|
+
readFileSync('pnpm-workspace.yaml', 'utf8').includes(`link:${relative(process.cwd(), dir)}/`);
|
|
66
|
+
|
|
67
|
+
if (flag === '--remove') {
|
|
68
|
+
const wasLinked = isLinkedTo(worktree);
|
|
69
|
+
if (existsSync(worktree)) {
|
|
70
|
+
try {
|
|
71
|
+
git('worktree', 'remove', worktree);
|
|
72
|
+
} catch {
|
|
73
|
+
die(`${worktreeFromHere} has uncommitted changes - commit or discard them, then rerun.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (wasLinked && hasLinkScript()) {
|
|
77
|
+
run('pnpm', ['link:oss']);
|
|
78
|
+
}
|
|
79
|
+
console.log(`removed ${worktreeFromHere}`);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!existsSync(worktree)) {
|
|
84
|
+
git('fetch', 'origin', '--quiet');
|
|
85
|
+
const base = gitOrEmpty('rev-parse', '--abbrev-ref', 'origin/HEAD') || 'origin/dev';
|
|
86
|
+
if (gitOrEmpty('rev-parse', '--verify', '--quiet', `refs/heads/${branch}`)) {
|
|
87
|
+
git('worktree', 'add', worktree, branch);
|
|
88
|
+
} else if (gitOrEmpty('rev-parse', '--verify', '--quiet', `refs/remotes/origin/${branch}`)) {
|
|
89
|
+
git('worktree', 'add', '--track', '-b', branch, worktree, `origin/${branch}`);
|
|
90
|
+
} else {
|
|
91
|
+
git('worktree', 'add', '--no-track', '-b', branch, worktree, base);
|
|
92
|
+
}
|
|
93
|
+
run('pnpm', ['-C', worktree, 'install', '--frozen-lockfile']);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (flag === '--link') {
|
|
97
|
+
if (!hasLinkScript()) {
|
|
98
|
+
die('this repo has no `link:oss` script - link the worktree through pnpm overrides by hand.');
|
|
99
|
+
}
|
|
100
|
+
// @openora/* resolve from dist/, so a linked worktree works only once it is built.
|
|
101
|
+
run('pnpm', ['-C', worktree, 'build']);
|
|
102
|
+
run('pnpm', ['link:oss', worktreeFromHere]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(worktreeFromHere);
|