@nightowlsdev/cli 0.1.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/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/chunk-E2C2JZ2E.js +26 -0
- package/dist/chunk-YZIYBDZO.js +76 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +421 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +9 -0
- package/dist/mcp-3ZFDXVDT.js +39 -0
- package/dist/node-EVZ52KTR.js +19 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Night Owls contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# `@nightowlsdev/cli`
|
|
2
|
+
|
|
3
|
+
The pluggable Night Owls CLI. One thin binary — `owl` — that scaffolds a project, installs adapters, and
|
|
4
|
+
**contributes** each adapter's migrations into your `supabase/migrations/`. Each `@nightowlsdev/*` adapter
|
|
5
|
+
contributes a **declarative** manifest; the CLI discovers the installed adapters from your `package.json`
|
|
6
|
+
and acts on their manifests. All codegen lives in the CLI, so adapters stay data-only (no dependency cycle).
|
|
7
|
+
|
|
8
|
+
**Night Owls never runs DDL against your database.** It only ever *contributes* migrations into your
|
|
9
|
+
`supabase/migrations/`; you apply them with your own tooling (e.g. `supabase db push`).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @nightowlsdev/cli init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
| Command | What it does |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `owl init [plugin]` | Scaffold `nightowls.config.ts` + `.env.example` and install the selected adapters (each one **installs** its migrations into `supabase/migrations/` and runs its `init` hook). Idempotent; never clobbers an existing config. With a `[plugin]` arg (e.g. `owl init storage-supabase`), re-init **just that one** plugin against the existing project — re-apply its wiring + re-run its `init` hook (useful to integrate a plugin after the fact). Flags: `--storage <adapter>`, `--runner <adapter>`, `--no-storage`, `--no-runner`, `-y/--yes`. |
|
|
20
|
+
| `owl install <adapter>` | Add `@nightowlsdev/<adapter>` as a dependency and apply its boilerplate: merge its env vars into `.env.example`, scaffold its files (only if absent), insert its config import + wiring snippet into `nightowls.config.ts`, **install its migrations into `supabase/migrations/`**, and run its `init` hook. Idempotent. |
|
|
21
|
+
| `owl plugins` | List the installed `@nightowlsdev/*` plugins, their kind + description, and the commands each exposes. |
|
|
22
|
+
| `owl <plugin> <cmd>` | Run a plugin's own subcommand (e.g. `owl storage-supabase info`, `owl runner-nextjs routes`). `owl <plugin> --help` lists a plugin's commands; `owl <plugin> <cmd> --help` shows a command's flags. |
|
|
23
|
+
| `owl db install` | (Re-)install Night Owls' migrations into the host's classic `supabase/migrations/` as timestamped `.sql` files, so they run with the app's own via `supabase db push`. Useful after upgrading an adapter. Idempotent. Flag: `--out <dir>` (default `supabase/migrations`). |
|
|
24
|
+
| `owl db types` | Generate TypeScript types for the `nightowls` schema (`supabase gen types typescript --schema nightowls`). |
|
|
25
|
+
|
|
26
|
+
## Adapters and what `owl install` wires
|
|
27
|
+
|
|
28
|
+
Each adapter contributes env vars, a config snippet (or files + guidance), an `init` hint, and an `info`
|
|
29
|
+
command. **Install ONE storage adapter, ONE auth provider, and ONE model provider; telemetry COMPOSES
|
|
30
|
+
(install as many as you like); `runner-background` is opt-in (`runner-nextjs` is the default).** The scaffolded
|
|
31
|
+
`nightowls.config.ts` is a **starting point you complete** — fill in your `agents` (and set `MODEL_ID`; a model
|
|
32
|
+
provider wires `modelFactory` and `models.allow` is env-driven from it).
|
|
33
|
+
|
|
34
|
+
| Adapter | kind | `owl install` wiring |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `storage-supabase` | storage | env `SUPABASE_URL`, `SUPABASE_SECRET_KEY`, `DATABASE_URL`, `SUPABASE_ANON_KEY`; config `storage = createSupabaseStorage({ url, secretKey, dbUrl })`; installs its migrations into `supabase/migrations/`. Command: `info`. |
|
|
37
|
+
| `runner-nextjs` | runner | scaffolds `app/api/swarm/*` route handlers; config `runner = createNextjsRunner({ swarm, auth, storage })`. The interactive (SSE) default. Command: `routes`. |
|
|
38
|
+
| `auth-supabase` | auth | env `SUPABASE_URL`, `SUPABASE_ANON_KEY` (anon key only — verifies the user JWT, never the secret key); config `auth = supabaseAuth({ url, anonKey })`. Command: `info`. |
|
|
39
|
+
| `auth-auth0` | auth | env `AUTH0_ISSUER_BASE_URL`, `AUTH0_AUDIENCE`; config `auth = auth0Auth({ issuerBaseUrl, audience })`. **Install ONE auth provider** (reconcile a duplicate `auth = …` if you install both). Command: `info`. |
|
|
40
|
+
| `telemetry-otel` | telemetry | env `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`; config **composes** `telemetry = [...(telemetry ?? []), otelTelemetry({ url })]`. Command: `info`. |
|
|
41
|
+
| `telemetry-langfuse` | telemetry | env `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_BASEURL`; config **composes** `telemetry = [...(telemetry ?? []), langfuseTelemetry({ publicKey, secretKey, baseUrl })]`. Command: `info`. |
|
|
42
|
+
| `runner-background` | runner | **opt-in** durable HITL runner (Trigger.dev v4 / Vercel Workflow). env `DURABLE_BACKEND`, `TRIGGER_SECRET_KEY`; scaffolds `trigger/nightowls.ts` (task) + `lib/nightowls-durable.ts` (registry); config is **commented guidance** at the runner marker — you wire `createBackgroundRunner(...)` by hand. The `init` hook prints the setup steps. Command: `info`. |
|
|
43
|
+
| `mcp` | connector | connect external MCP servers as Night Owls tools (approval-gated, fenced). env `MCP_<SERVER>_URL`; config is **commented guidance** at the connector marker — MCP tools are **granted to specific agents' skills**, not assigned to a single binding. Command: `info`. |
|
|
44
|
+
| `model-vercel-gateway` | model | any model via the Vercel AI Gateway (`provider/model` ids, one key). env `AI_GATEWAY_API_KEY`, `MODEL_ID`; config `modelFactory = vercelGatewayModels()`. **Install ONE model provider** (reconcile a duplicate `modelFactory = …` if you install more than one). Command: `info`. |
|
|
45
|
+
| `model-openrouter` | model | any model via OpenRouter. env `OPENROUTER_API_KEY`, `MODEL_ID`; config `modelFactory = openrouterModels()`. Command: `info`. |
|
|
46
|
+
| `model-anthropic` | model | Anthropic Claude models (native `@ai-sdk/anthropic`). env `ANTHROPIC_API_KEY`, `MODEL_ID`; config `modelFactory = anthropicModels()`. Command: `info`. |
|
|
47
|
+
| `model-openai` | model | OpenAI models (native `@ai-sdk/openai`). env `OPENAI_API_KEY`, `MODEL_ID`; config `modelFactory = openaiModels()`. Command: `info`. |
|
|
48
|
+
|
|
49
|
+
Telemetry composing is verified at the gate: the generated `nightowls.config.ts` with storage + runner + auth
|
|
50
|
+
+ a model provider (`modelFactory = anthropicModels()`) + **both** telemetry exporters **parses and
|
|
51
|
+
typechecks** against the real `@nightowlsdev/core` + adapter types (`packages/cli/test/config-typecheck.test.ts`).
|
|
52
|
+
|
|
53
|
+
## How migrations land: install, then apply with your own tooling
|
|
54
|
+
|
|
55
|
+
Night Owls is a migration **contributor** — it never connects to your database to run DDL. `owl install`
|
|
56
|
+
(and `owl init`, which installs per adapter) **installs** each storage adapter's migrations into the
|
|
57
|
+
host's classic `supabase/migrations/` directory as timestamped files named
|
|
58
|
+
`<YYYYMMDDHHMMSS>_corale_<version>.sql` (e.g. `20260604093000_corale_0001_core.sql`). You then apply them
|
|
59
|
+
with **your own** tooling — `supabase db push` — alongside your app's own migrations. The install prints
|
|
60
|
+
the apply instruction; it does not run it.
|
|
61
|
+
|
|
62
|
+
`owl db install` is **idempotent**: it scans the out dir and skips any version already installed (matched by the
|
|
63
|
+
`_corale_<version>` filename token), so re-running it never overwrites or duplicates a file. Newly added
|
|
64
|
+
migrations are appended on the next install (e.g. after `owl install` of a newer adapter, or an explicit
|
|
65
|
+
`owl db install`). The install-time timestamp (base clock plus an index offset, so the files stay unique and
|
|
66
|
+
lexically ordered) **slots them in correctly whether you adopt Night Owls on day one or add it later** — they
|
|
67
|
+
sort after whatever the host already had.
|
|
68
|
+
|
|
69
|
+
## Plugin commands, lifecycle hooks, and help
|
|
70
|
+
|
|
71
|
+
Beyond the declarative wiring, a plugin can carry **behavior** and **docs**:
|
|
72
|
+
|
|
73
|
+
- **Subcommands** — a plugin exposes `commands`, each surfaced as `owl <plugin-name> <command-name>`.
|
|
74
|
+
The CLI discovers the installed plugins on every run and registers their command groups before parsing,
|
|
75
|
+
so `owl storage-supabase info` and `owl runner-nextjs routes` "just work". A plugin command's
|
|
76
|
+
handler gets a structural context: `{ cwd, log, args, options }`. The reference commands are **pure** —
|
|
77
|
+
no DB, no network — honoring "Night Owls never acts on its own". A plugin whose `name` would shadow a
|
|
78
|
+
built-in (`init`/`install`/`db`/`plugins`/`help`) is **skipped with a warning** (the built-in always wins).
|
|
79
|
+
- **`init` hook** — a plugin's optional `init({ cwd, log })` runs **after** the declarative wiring during
|
|
80
|
+
`owl init`, `owl install <plugin>`, and `owl init <plugin>`. This integrates a plugin into an
|
|
81
|
+
already-inited project. The hook is **idempotent and print-only** — it MUST NOT apply DDL or touch a DB.
|
|
82
|
+
- **Help** — every plugin and command carries a `description`. `owl plugins` lists them; `owl <plugin>
|
|
83
|
+
--help` and `owl <plugin> <cmd> --help` render Commander's usage from those descriptions + options.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
owl plugins # list installed plugins, their kind/description, and their commands
|
|
87
|
+
owl storage-supabase --help # a plugin's commands
|
|
88
|
+
owl storage-supabase info # a pure command: the migrations + env this adapter contributes
|
|
89
|
+
owl runner-nextjs routes # a pure command: the App Router routes this runner scaffolds
|
|
90
|
+
owl init storage-supabase # re-init one plugin: re-apply wiring + re-run its init hook
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Adapters stay import-free of `@nightowlsdev/cli`.** The `init`/command handler contexts are typed
|
|
94
|
+
**structurally** (a local `type Ctx = { cwd; log; args; options }` in the adapter) — no adapter imports the
|
|
95
|
+
CLI, so the published `.d.ts` references no `@nightowlsdev/cli` type (no dependency cycle). The CLI's
|
|
96
|
+
`conformance.test-d.ts` pins each manifest to `NightOwlsPlugin` structurally.
|
|
97
|
+
|
|
98
|
+
## The declarative `nightOwlsPlugin` contract
|
|
99
|
+
|
|
100
|
+
An adapter plugs into the CLI by exporting a `nightOwlsPlugin` plain object that **structurally** matches
|
|
101
|
+
`NightOwlsPlugin` (it does NOT import the type — that would create a cycle, since the CLI imports the adapter):
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
export interface NightOwlsPlugin {
|
|
105
|
+
name: string; // short name, e.g. "storage-supabase"
|
|
106
|
+
version: string;
|
|
107
|
+
kind?: "storage" | "runner" | "auth" | "telemetry" | "ui" | "connector";
|
|
108
|
+
pkg: string; // npm name, e.g. "@nightowlsdev/storage-supabase"
|
|
109
|
+
description?: string; // one-line summary shown by `owl plugins` + `owl <plugin> --help`
|
|
110
|
+
migrations?: { version: string; name: string; sql: string }[]; // installed into supabase/migrations/
|
|
111
|
+
env?: { key: string; example: string; comment?: string }[]; // merged into .env.example
|
|
112
|
+
files?: { path: string; contents: string }[]; // scaffolded if absent (never clobbered)
|
|
113
|
+
config?: { // inserted at the `// nightowls:<marker>` line
|
|
114
|
+
import: string;
|
|
115
|
+
snippet: string;
|
|
116
|
+
marker: "storage" | "auth" | "runner" | "telemetry" | "connector";
|
|
117
|
+
};
|
|
118
|
+
// Runs after the declarative wiring on init/install (idempotent; print-only — never applies DDL).
|
|
119
|
+
init?: (ctx: { cwd: string; log: (msg: string) => void }) => void | Promise<void>;
|
|
120
|
+
// Subcommands surfaced as `owl <plugin-name> <command-name>` (the ctx adds { args, options }).
|
|
121
|
+
commands?: {
|
|
122
|
+
name: string;
|
|
123
|
+
description: string;
|
|
124
|
+
options?: { flags: string; description: string; defaultValue?: string | boolean }[];
|
|
125
|
+
run: (ctx: {
|
|
126
|
+
cwd: string;
|
|
127
|
+
log: (msg: string) => void;
|
|
128
|
+
args: string[];
|
|
129
|
+
options: Record<string, unknown>;
|
|
130
|
+
}) => void | Promise<void>;
|
|
131
|
+
}[];
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The CLI discovers `@nightowlsdev/*` deps from the host `package.json`, dynamic-imports each one, and collects the
|
|
136
|
+
`nightOwlsPlugin` exports. Adapters without a manifest (e.g. `@nightowlsdev/react`) are ignored.
|
|
137
|
+
|
|
138
|
+
`config.snippet` is a **top-level statement** inserted above the `// nightowls:<marker>` line — it assigns the
|
|
139
|
+
scaffold's pre-declared binding (e.g. `storage = createSupabaseStorage({ … });`,
|
|
140
|
+
`runner = createNextjsRunner({ … });`), so installing several adapters into one `nightowls.config.ts` produces
|
|
141
|
+
valid, functional TypeScript. It is NOT an object-property fragment.
|
|
142
|
+
|
|
143
|
+
## The nightowls schema
|
|
144
|
+
|
|
145
|
+
Night Owls' tables live in a dedicated `nightowls` schema (not `public`). Night Owls **contributes** its migrations
|
|
146
|
+
into your `supabase/migrations/` (via `owl install` / `owl db install`); your host's migration runner
|
|
147
|
+
(`supabase db push`) applies them and tracks them in the host's own `supabase_migrations.schema_migrations`.
|
|
148
|
+
There is one migration system — the host's — and Night Owls never runs DDL itself. See
|
|
149
|
+
`docs/spec/2026-06-03-owl-schema-and-cli.md`.
|
|
150
|
+
|
|
151
|
+
## Roadmap
|
|
152
|
+
|
|
153
|
+
The agentic CLI layer (`owl doctor`, `owl mcp`, deep ts-morph config edits) is future work. Today the
|
|
154
|
+
config insertion is marker-based — the `// nightowls:<marker>` comment lines are stable insertion points.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/registry.ts
|
|
4
|
+
async function discoverPlugins(deps) {
|
|
5
|
+
const pkg = await deps.readPkgJson();
|
|
6
|
+
const names = [...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})].filter(
|
|
7
|
+
(n) => n.startsWith("@nightowlsdev/")
|
|
8
|
+
);
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const name of names) {
|
|
11
|
+
const mod = await deps.importPlugin(name).catch(() => ({}));
|
|
12
|
+
if (mod.nightOwlsPlugin) out.push({ ...mod.nightOwlsPlugin, pkg: mod.nightOwlsPlugin.pkg ?? name });
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
function nodeDiscoverDeps(cwd) {
|
|
17
|
+
return {
|
|
18
|
+
readPkgJson: async () => JSON.parse(await (await import("fs/promises")).readFile(`${cwd}/package.json`, "utf8")),
|
|
19
|
+
importPlugin: async (pkg) => (await import("./node-EVZ52KTR.js")).hostImport(cwd, pkg)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
discoverPlugins,
|
|
25
|
+
nodeDiscoverDeps
|
|
26
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/node.ts
|
|
4
|
+
async function exec(cmd, args) {
|
|
5
|
+
const { execFile } = await import("child_process");
|
|
6
|
+
const { promisify } = await import("util");
|
|
7
|
+
const run = promisify(execFile);
|
|
8
|
+
const { stdout } = await run(cmd, args, { maxBuffer: 64 * 1024 * 1024 });
|
|
9
|
+
return { stdout: stdout.toString() };
|
|
10
|
+
}
|
|
11
|
+
async function detectPackageManager(cwd) {
|
|
12
|
+
const fs = await import("fs/promises");
|
|
13
|
+
const has = async (f) => {
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(`${cwd}/${f}`);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
if (await has("pnpm-lock.yaml")) return "pnpm";
|
|
22
|
+
if (await has("yarn.lock")) return "yarn";
|
|
23
|
+
return "npm";
|
|
24
|
+
}
|
|
25
|
+
async function addDep(pm, cwd, pkg) {
|
|
26
|
+
const { spawn } = await import("child_process");
|
|
27
|
+
const args = pm === "yarn" ? ["add", pkg] : pm === "npm" ? ["install", pkg] : ["add", pkg];
|
|
28
|
+
await new Promise((resolve, reject) => {
|
|
29
|
+
const child = spawn(pm, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
30
|
+
child.on("error", reject);
|
|
31
|
+
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${pm} ${args.join(" ")} exited ${code}`)));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function isInstalled(cwd, pkg) {
|
|
35
|
+
try {
|
|
36
|
+
const fs = await import("fs/promises");
|
|
37
|
+
const json = JSON.parse(await fs.readFile(`${cwd}/package.json`, "utf8"));
|
|
38
|
+
return Boolean(json.dependencies?.[pkg] ?? json.devDependencies?.[pkg]);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function hostImport(cwd, pkg) {
|
|
44
|
+
const { createRequire } = await import("module");
|
|
45
|
+
const { pathToFileURL } = await import("url");
|
|
46
|
+
const requireFromHost = createRequire(`${cwd}/package.json`);
|
|
47
|
+
const resolved = requireFromHost.resolve(pkg);
|
|
48
|
+
return await import(pathToFileURL(resolved).href);
|
|
49
|
+
}
|
|
50
|
+
async function loadPlugin(pkg, cwd) {
|
|
51
|
+
try {
|
|
52
|
+
const mod = await hostImport(cwd, pkg);
|
|
53
|
+
return mod.nightOwlsPlugin ?? null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function nodeFs() {
|
|
59
|
+
const fs = await import("fs/promises");
|
|
60
|
+
return {
|
|
61
|
+
readFile: (p, enc) => fs.readFile(p, enc),
|
|
62
|
+
writeFile: (p, data) => fs.writeFile(p, data),
|
|
63
|
+
mkdir: (p, opts) => fs.mkdir(p, opts),
|
|
64
|
+
readdir: (p) => fs.readdir(p)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
exec,
|
|
70
|
+
detectPackageManager,
|
|
71
|
+
addDep,
|
|
72
|
+
isInstalled,
|
|
73
|
+
hostImport,
|
|
74
|
+
loadPlugin,
|
|
75
|
+
nodeFs
|
|
76
|
+
};
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
discoverPlugins,
|
|
4
|
+
nodeDiscoverDeps
|
|
5
|
+
} from "./chunk-E2C2JZ2E.js";
|
|
6
|
+
import {
|
|
7
|
+
addDep,
|
|
8
|
+
detectPackageManager,
|
|
9
|
+
exec,
|
|
10
|
+
isInstalled,
|
|
11
|
+
loadPlugin,
|
|
12
|
+
nodeFs
|
|
13
|
+
} from "./chunk-YZIYBDZO.js";
|
|
14
|
+
|
|
15
|
+
// src/cli.ts
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
|
|
18
|
+
// src/commands/db.ts
|
|
19
|
+
function collectMigrations(plugins) {
|
|
20
|
+
const all = plugins.flatMap((p) => p.migrations ?? []);
|
|
21
|
+
if (all.length === 0) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"No storage adapter installed (or it ships no migrations). Run `owl install storage-supabase` first."
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return [...all].sort((a, b) => a.version.localeCompare(b.version));
|
|
27
|
+
}
|
|
28
|
+
async function runDbTypes(deps) {
|
|
29
|
+
const schema = deps.schema ?? "nightowls";
|
|
30
|
+
const { stdout } = await deps.exec("supabase", ["gen", "types", "typescript", "--schema", schema]);
|
|
31
|
+
return stdout;
|
|
32
|
+
}
|
|
33
|
+
function migrationStamp(base, i) {
|
|
34
|
+
const d = new Date(base.getTime() + i * 1e3);
|
|
35
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
36
|
+
return `${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}`;
|
|
37
|
+
}
|
|
38
|
+
var INSTALLED_FILE = /_corale_(.+)\.sql$/;
|
|
39
|
+
async function runDbInstall(deps) {
|
|
40
|
+
const migrations = collectMigrations(deps.plugins);
|
|
41
|
+
const dir = `${deps.root.replace(/\/$/, "")}/${deps.out.replace(/^\//, "")}`;
|
|
42
|
+
await deps.fs.mkdir(dir, { recursive: true }).catch(() => void 0);
|
|
43
|
+
const existing = await deps.fs.readdir(dir).catch(() => []);
|
|
44
|
+
const installed = new Set(
|
|
45
|
+
existing.map((f) => INSTALLED_FILE.exec(f)?.[1]).filter((v) => Boolean(v))
|
|
46
|
+
);
|
|
47
|
+
const base = deps.now();
|
|
48
|
+
const written = [];
|
|
49
|
+
const skipped = [];
|
|
50
|
+
let i = 0;
|
|
51
|
+
for (const m of migrations) {
|
|
52
|
+
if (installed.has(m.version)) {
|
|
53
|
+
skipped.push(m.version);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const file = `${migrationStamp(base, i)}_corale_${m.version}.sql`;
|
|
57
|
+
await deps.fs.writeFile(`${dir}/${file}`, `${m.sql.trimStart()}
|
|
58
|
+
`);
|
|
59
|
+
written.push({ version: m.version, file });
|
|
60
|
+
i += 1;
|
|
61
|
+
}
|
|
62
|
+
return { written, skipped };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/templates.ts
|
|
66
|
+
var CONFIG_TEMPLATE = `import { defineSwarm } from "@nightowlsdev/core";
|
|
67
|
+
// nightowls:imports \u2014 adapter imports are inserted above this line by \`owl install\`.
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read your secrets here (e.g. via \`process.env\` or a validated env module). Required by the inserted
|
|
71
|
+
* adapter snippets: SUPABASE_URL, SUPABASE_SECRET_KEY, DATABASE_URL (see .env.example).
|
|
72
|
+
*/
|
|
73
|
+
const env = process.env as Record<string, string>;
|
|
74
|
+
|
|
75
|
+
// Define your agents (slug, instructions, model_id, skills). See the docs for the full shape.
|
|
76
|
+
const AGENTS: Parameters<typeof defineSwarm>[0]["agents"] = [];
|
|
77
|
+
|
|
78
|
+
// Storage adapter \u2014 \`owl install storage-supabase\` assigns \`storage = \u2026\` above the marker below
|
|
79
|
+
// (and installs its migrations into supabase/migrations/ \u2014 apply them with \`supabase db push\`).
|
|
80
|
+
// Until then this is a placeholder; a real storage adapter is required to run.
|
|
81
|
+
let storage: Parameters<typeof defineSwarm>[0]["storage"] = undefined as never;
|
|
82
|
+
// nightowls:storage
|
|
83
|
+
|
|
84
|
+
// Telemetry exporter(s) \u2014 \`owl install telemetry-*\` COMPOSES into this array (each adapter pushes
|
|
85
|
+
// its exporter above the marker). \`defineSwarm\` accepts one OR many; many are best-effort composed
|
|
86
|
+
// (a throwing exporter never breaks a run). Declared as the ARRAY member of that union so multiple
|
|
87
|
+
// exporters coexist (\`telemetry = [...(telemetry ?? []), xTelemetry({...})];\`).
|
|
88
|
+
let telemetry: Extract<NonNullable<Parameters<typeof defineSwarm>[0]["telemetry"]>, readonly unknown[]> = [];
|
|
89
|
+
// nightowls:telemetry
|
|
90
|
+
|
|
91
|
+
// Model provider \u2014 \`owl install model-<provider>\` assigns \`modelFactory = \u2026\` above the marker below
|
|
92
|
+
// (install ONE \u2014 providers are mutually exclusive, like auth). Declared as a \`let\` so the inserted
|
|
93
|
+
// snippet can reassign it; the placeholder throws until a real provider is wired. \`models.allow\` is
|
|
94
|
+
// env-driven (\`MODEL_ID\`), so the scaffold is runnable once a model plugin + MODEL_ID are set.
|
|
95
|
+
let modelFactory: Parameters<typeof defineSwarm>[0]["modelFactory"] = () => {
|
|
96
|
+
throw new Error("Install a model provider: owl install model-anthropic (or model-openai / model-vercel-gateway / model-openrouter).");
|
|
97
|
+
};
|
|
98
|
+
// nightowls:model
|
|
99
|
+
|
|
100
|
+
export const swarm = defineSwarm({
|
|
101
|
+
storage,
|
|
102
|
+
agents: AGENTS,
|
|
103
|
+
models: { allow: env.MODEL_ID ? [env.MODEL_ID] : [] },
|
|
104
|
+
modelFactory,
|
|
105
|
+
cost: { maxSteps: 12, maxCostUsd: 0.5 },
|
|
106
|
+
telemetry,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Server-side auth \u2014 \`owl install auth-*\` assigns \`auth = \u2026\` above the marker below (install ONE
|
|
110
|
+
// auth provider). Declared as a \`let\` so the inserted snippet can reassign it; the placeholder rejects
|
|
111
|
+
// every request (never trust the request body for identity \u2014 replace it with a real identity resolver).
|
|
112
|
+
let auth: { authenticate(req: Request): Promise<{ tenantId: string; userId: string; capabilities?: string[] } | null> } = {
|
|
113
|
+
authenticate: async () => null,
|
|
114
|
+
};
|
|
115
|
+
// nightowls:auth
|
|
116
|
+
|
|
117
|
+
// Runner \u2014 \`owl install runner-nextjs\` assigns \`runner = \u2026\` above the marker below. The structural
|
|
118
|
+
// type covers the three App Router handlers the scaffolded route files call (\`runner.chatRoute()\`, \u2026), so
|
|
119
|
+
// the host typechecks before AND after the assignment without the CLI importing the runner package.
|
|
120
|
+
type SwarmRunner = {
|
|
121
|
+
chatRoute(): { POST: (req: Request) => Promise<Response> };
|
|
122
|
+
resumeRoute(): { POST: (req: Request) => Promise<Response> };
|
|
123
|
+
eventsRoute(): { GET: (req: Request, ctx: { params: Promise<{ id: string }> }) => Promise<Response> };
|
|
124
|
+
};
|
|
125
|
+
let runner: SwarmRunner = undefined as never;
|
|
126
|
+
// nightowls:runner
|
|
127
|
+
export { runner };
|
|
128
|
+
|
|
129
|
+
// Connectors \u2014 \`owl install mcp\` inserts a guidance comment above the marker below. Connector tools
|
|
130
|
+
// (e.g. MCP servers) are GRANTED to specific agents' skills (approval-gated, fenced), not assigned to a
|
|
131
|
+
// single binding \u2014 so the inserted snippet is a comment, not an assignment.
|
|
132
|
+
// nightowls:connector
|
|
133
|
+
`;
|
|
134
|
+
var ENV_EXAMPLE_HEADER = `# Night Owls environment \u2014 copy to .env.local and fill in.
|
|
135
|
+
# Adapter variables are appended below by \`owl install <adapter>\`.
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
// src/codegen.ts
|
|
139
|
+
async function readIfExists(fs, path) {
|
|
140
|
+
try {
|
|
141
|
+
return await fs.readFile(path, "utf8");
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function join(root, rel) {
|
|
147
|
+
return `${root.replace(/\/$/, "")}/${rel.replace(/^\//, "")}`;
|
|
148
|
+
}
|
|
149
|
+
function envLine(e) {
|
|
150
|
+
const base = `${e.key}=${e.example}`;
|
|
151
|
+
return e.comment ? `${base} # ${e.comment}` : base;
|
|
152
|
+
}
|
|
153
|
+
async function mergeEnv(fs, envPath, env) {
|
|
154
|
+
const current = await readIfExists(fs, envPath) ?? "";
|
|
155
|
+
const additions = [];
|
|
156
|
+
for (const e of env) {
|
|
157
|
+
const present = new RegExp(`^\\s*${e.key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=`, "m").test(current);
|
|
158
|
+
if (!present) additions.push(envLine(e));
|
|
159
|
+
}
|
|
160
|
+
if (additions.length === 0) return;
|
|
161
|
+
const needsNl = current.length > 0 && !current.endsWith("\n");
|
|
162
|
+
const next = current + (needsNl ? "\n" : "") + additions.join("\n") + "\n";
|
|
163
|
+
await fs.writeFile(envPath, next);
|
|
164
|
+
}
|
|
165
|
+
async function insertConfig(fs, configPath, plugin) {
|
|
166
|
+
const cfg = plugin.config;
|
|
167
|
+
if (!cfg) return;
|
|
168
|
+
let text = await readIfExists(fs, configPath);
|
|
169
|
+
if (text === null) return;
|
|
170
|
+
let changed = false;
|
|
171
|
+
if (!text.includes(cfg.import)) {
|
|
172
|
+
text = `${cfg.import}
|
|
173
|
+
${text}`;
|
|
174
|
+
changed = true;
|
|
175
|
+
}
|
|
176
|
+
if (!text.includes(cfg.snippet)) {
|
|
177
|
+
const marker = `// nightowls:${cfg.marker}`;
|
|
178
|
+
const idx = text.indexOf(marker);
|
|
179
|
+
if (idx !== -1) {
|
|
180
|
+
const lineStart = text.lastIndexOf("\n", idx) + 1;
|
|
181
|
+
const indent = text.slice(lineStart, idx);
|
|
182
|
+
const insertion = `${indent}${cfg.snippet}
|
|
183
|
+
`;
|
|
184
|
+
text = text.slice(0, lineStart) + insertion + text.slice(lineStart);
|
|
185
|
+
changed = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (changed) await fs.writeFile(configPath, text);
|
|
189
|
+
}
|
|
190
|
+
async function scaffoldFiles(fs, root, plugin) {
|
|
191
|
+
for (const f of plugin.files ?? []) {
|
|
192
|
+
const abs = join(root, f.path);
|
|
193
|
+
if (await readIfExists(fs, abs) !== null) continue;
|
|
194
|
+
const dir = abs.slice(0, abs.lastIndexOf("/"));
|
|
195
|
+
if (dir) await fs.mkdir(dir, { recursive: true });
|
|
196
|
+
await fs.writeFile(abs, f.contents);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function applyInstall(args) {
|
|
200
|
+
const { plugin, root, fs } = args;
|
|
201
|
+
if (plugin.env?.length) await mergeEnv(fs, join(root, ".env.example"), plugin.env);
|
|
202
|
+
await scaffoldFiles(fs, root, plugin);
|
|
203
|
+
await insertConfig(fs, join(root, "nightowls.config.ts"), plugin);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/commands/install.ts
|
|
207
|
+
function toPkgName(adapter) {
|
|
208
|
+
return adapter.startsWith("@nightowlsdev/") ? adapter : `@nightowlsdev/${adapter}`;
|
|
209
|
+
}
|
|
210
|
+
async function runInstall(deps) {
|
|
211
|
+
const pkg = toPkgName(deps.adapter);
|
|
212
|
+
const already = deps.isInstalled ? await deps.isInstalled(pkg) : false;
|
|
213
|
+
let added = false;
|
|
214
|
+
if (!already) {
|
|
215
|
+
await deps.addDep(pkg);
|
|
216
|
+
added = true;
|
|
217
|
+
}
|
|
218
|
+
const plugin = await deps.loadPlugin(pkg);
|
|
219
|
+
if (!plugin) {
|
|
220
|
+
throw new Error(`${pkg} does not export a nightOwlsPlugin manifest \u2014 nothing to install.`);
|
|
221
|
+
}
|
|
222
|
+
await applyInstall({ plugin, root: deps.root, fs: deps.fs });
|
|
223
|
+
let migrations = [];
|
|
224
|
+
if (plugin.migrations?.length) {
|
|
225
|
+
const result = await runDbInstall({
|
|
226
|
+
plugins: [plugin],
|
|
227
|
+
root: deps.root,
|
|
228
|
+
out: "supabase/migrations",
|
|
229
|
+
fs: deps.fs,
|
|
230
|
+
now: deps.now ?? (() => /* @__PURE__ */ new Date())
|
|
231
|
+
});
|
|
232
|
+
migrations = result.written;
|
|
233
|
+
}
|
|
234
|
+
const log = deps.log ?? console.log;
|
|
235
|
+
let initRan = false;
|
|
236
|
+
if (plugin.init) {
|
|
237
|
+
await plugin.init({ cwd: deps.root, log });
|
|
238
|
+
initRan = true;
|
|
239
|
+
}
|
|
240
|
+
return { pkg, added, applied: true, migrations, initRan };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/commands/init.ts
|
|
244
|
+
async function readIfExists2(fs, path) {
|
|
245
|
+
try {
|
|
246
|
+
return await fs.readFile(path, "utf8");
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function join2(root, rel) {
|
|
252
|
+
return `${root.replace(/\/$/, "")}/${rel}`;
|
|
253
|
+
}
|
|
254
|
+
async function runInit(deps) {
|
|
255
|
+
const configPath = join2(deps.root, "nightowls.config.ts");
|
|
256
|
+
const envPath = join2(deps.root, ".env.example");
|
|
257
|
+
if (await readIfExists2(deps.fs, configPath) === null) {
|
|
258
|
+
await deps.fs.writeFile(configPath, CONFIG_TEMPLATE);
|
|
259
|
+
}
|
|
260
|
+
if (await readIfExists2(deps.fs, envPath) === null) {
|
|
261
|
+
await deps.fs.writeFile(envPath, ENV_EXAMPLE_HEADER);
|
|
262
|
+
}
|
|
263
|
+
const migrations = [];
|
|
264
|
+
for (const adapter of deps.adapters) {
|
|
265
|
+
const result = await runInstall({
|
|
266
|
+
adapter,
|
|
267
|
+
root: deps.root,
|
|
268
|
+
fs: deps.fs,
|
|
269
|
+
addDep: deps.addDep,
|
|
270
|
+
loadPlugin: deps.loadPlugin,
|
|
271
|
+
now: deps.now,
|
|
272
|
+
log: deps.log
|
|
273
|
+
});
|
|
274
|
+
migrations.push(...result.migrations);
|
|
275
|
+
}
|
|
276
|
+
return { migrations };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/commands/plugins.ts
|
|
280
|
+
function registerPluginCommands(program2, plugins, base) {
|
|
281
|
+
const taken = /* @__PURE__ */ new Set([...program2.commands.map((c) => c.name()), "help"]);
|
|
282
|
+
for (const p of plugins) {
|
|
283
|
+
if (!p.commands?.length) continue;
|
|
284
|
+
if (taken.has(p.name)) {
|
|
285
|
+
base.log(`(skipping plugin "${p.name}": shadows a built-in command)`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
taken.add(p.name);
|
|
289
|
+
const group = program2.command(p.name).description(p.description ?? `${p.kind ?? "plugin"} adapter (${p.pkg})`);
|
|
290
|
+
for (const cmd of p.commands) {
|
|
291
|
+
const sub = group.command(cmd.name).description(cmd.description);
|
|
292
|
+
for (const o of cmd.options ?? []) {
|
|
293
|
+
sub.option(o.flags, o.description, o.defaultValue);
|
|
294
|
+
}
|
|
295
|
+
sub.allowExcessArguments(true);
|
|
296
|
+
sub.action(async (...a) => {
|
|
297
|
+
const command = a[a.length - 1];
|
|
298
|
+
await cmd.run({ ...base, args: command.args ?? [], options: command.opts() });
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function runPluginsList(plugins, log) {
|
|
304
|
+
if (!plugins.length) {
|
|
305
|
+
log("No @nightowlsdev/* plugins found in this project.");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
log("Installed Night Owls plugins:");
|
|
309
|
+
for (const p of plugins) {
|
|
310
|
+
const cmds = p.commands?.length ? ` [${p.commands.map((c) => c.name).join(", ")}]` : "";
|
|
311
|
+
log(` ${p.name.padEnd(20)} ${(p.kind ?? "").padEnd(10)} ${p.description ?? ""}${cmds}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/cli.ts
|
|
316
|
+
function reportInstalledMigrations(migrations) {
|
|
317
|
+
if (!migrations.length) return;
|
|
318
|
+
console.log(`\u2713 Installed Night Owls's migrations into supabase/migrations/ (${migrations.length} file(s)).`);
|
|
319
|
+
console.log(" Apply them with your own tooling: supabase db push");
|
|
320
|
+
}
|
|
321
|
+
var program = new Command();
|
|
322
|
+
program.name("owl").description("The pluggable Night Owls CLI: scaffold a config, install adapters, and contribute their migrations to your supabase/migrations/.").version("0.0.0");
|
|
323
|
+
program.command("init").argument("[plugin]", "re-init just one already-installed plugin (apply its wiring + run its init hook)").description(
|
|
324
|
+
"Scaffold nightowls.config.ts + .env.example and install the selected adapters (idempotent). With a [plugin] arg, re-init just that one plugin against the existing project."
|
|
325
|
+
).option("--storage <adapter>", "storage adapter to install", "storage-supabase").option("--runner <adapter>", "runner adapter to install", "runner-nextjs").option("--no-storage", "skip installing a storage adapter").option("--no-runner", "skip installing a runner adapter").option("-y, --yes", "accept defaults (non-interactive)").action(
|
|
326
|
+
async (plugin, opts) => {
|
|
327
|
+
const cwd = process.cwd();
|
|
328
|
+
const pm = await detectPackageManager(cwd);
|
|
329
|
+
if (plugin) {
|
|
330
|
+
const pkg = toPkgName(plugin);
|
|
331
|
+
const result = await runInstall({
|
|
332
|
+
adapter: plugin,
|
|
333
|
+
root: cwd,
|
|
334
|
+
fs: await nodeFs(),
|
|
335
|
+
addDep: (p) => addDep(pm, cwd, p),
|
|
336
|
+
loadPlugin: (p) => loadPlugin(p, cwd),
|
|
337
|
+
isInstalled: (p) => isInstalled(cwd, p),
|
|
338
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
339
|
+
log: console.log
|
|
340
|
+
});
|
|
341
|
+
console.log(`${result.added ? "Added" : "Found"} ${pkg}; re-applied its env/config/files.`);
|
|
342
|
+
reportInstalledMigrations(result.migrations);
|
|
343
|
+
if (result.initRan) console.log(`\u2713 Ran ${plugin}'s init hook.`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const adapters = [];
|
|
347
|
+
if (opts.storage) adapters.push(opts.storage);
|
|
348
|
+
if (opts.runner) adapters.push(opts.runner);
|
|
349
|
+
const { migrations } = await runInit({
|
|
350
|
+
root: cwd,
|
|
351
|
+
fs: await nodeFs(),
|
|
352
|
+
adapters,
|
|
353
|
+
loadPlugin: (p) => loadPlugin(p, cwd),
|
|
354
|
+
addDep: (p) => addDep(pm, cwd, p),
|
|
355
|
+
pm,
|
|
356
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
357
|
+
log: console.log
|
|
358
|
+
});
|
|
359
|
+
console.log("Scaffolded nightowls.config.ts + .env.example.");
|
|
360
|
+
if (adapters.length) console.log(`Installed: ${adapters.join(", ")}.`);
|
|
361
|
+
reportInstalledMigrations(migrations);
|
|
362
|
+
console.log("Next: fill .env.local, then run your dev server.");
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
program.command("install").argument("<adapter>", "adapter short name, e.g. storage-supabase").description("Add a @nightowlsdev/<adapter> dependency and apply its env/config/files boilerplate (idempotent).").action(async (adapter) => {
|
|
366
|
+
const cwd = process.cwd();
|
|
367
|
+
const pm = await detectPackageManager(cwd);
|
|
368
|
+
const pkg = toPkgName(adapter);
|
|
369
|
+
const result = await runInstall({
|
|
370
|
+
adapter,
|
|
371
|
+
root: cwd,
|
|
372
|
+
fs: await nodeFs(),
|
|
373
|
+
addDep: (p) => addDep(pm, cwd, p),
|
|
374
|
+
loadPlugin: (p) => loadPlugin(p, cwd),
|
|
375
|
+
isInstalled: (p) => isInstalled(cwd, p),
|
|
376
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
377
|
+
log: console.log
|
|
378
|
+
});
|
|
379
|
+
console.log(`${result.added ? "Added" : "Found"} ${pkg}; applied its env/config/files.`);
|
|
380
|
+
reportInstalledMigrations(result.migrations);
|
|
381
|
+
if (result.initRan) console.log(`\u2713 Ran ${adapter}'s init hook.`);
|
|
382
|
+
console.log("Next: fill .env.local, then run your dev server.");
|
|
383
|
+
});
|
|
384
|
+
var db = program.command("db").description("Manage nightowls migrations (install into your supabase/migrations, generate types).");
|
|
385
|
+
db.command("types").description("Generate TypeScript types for the nightowls schema (supabase gen types).").action(async () => {
|
|
386
|
+
const out = await runDbTypes({ exec });
|
|
387
|
+
process.stdout.write(out);
|
|
388
|
+
});
|
|
389
|
+
db.command("install").description("Install Night Owls' migrations into your supabase/migrations/ (timestamped, idempotent) \u2014 apply them with `supabase db push`.").option("--out <dir>", "target migrations dir (relative to cwd)", "supabase/migrations").action(async (opts) => {
|
|
390
|
+
const cwd = process.cwd();
|
|
391
|
+
const plugins = await discoverPlugins(nodeDiscoverDeps(cwd));
|
|
392
|
+
const { written, skipped } = await runDbInstall({
|
|
393
|
+
plugins,
|
|
394
|
+
root: cwd,
|
|
395
|
+
out: opts.out,
|
|
396
|
+
fs: await nodeFs(),
|
|
397
|
+
now: () => /* @__PURE__ */ new Date()
|
|
398
|
+
});
|
|
399
|
+
for (const w of written) console.log(`wrote ${opts.out}/${w.file}`);
|
|
400
|
+
for (const version of skipped) console.log(`skipped ${version} (already installed)`);
|
|
401
|
+
if (!written.length) console.log("Nothing to install \u2014 all nightowls migrations already present.");
|
|
402
|
+
});
|
|
403
|
+
program.command("plugins").description("List installed Night Owls plugins and the commands they expose.").action(async () => {
|
|
404
|
+
const cwd = process.cwd();
|
|
405
|
+
const plugins = await discoverPlugins(nodeDiscoverDeps(cwd));
|
|
406
|
+
runPluginsList(plugins, console.log);
|
|
407
|
+
});
|
|
408
|
+
program.command("mcp").description("Run the Night Owls MCP server on stdio (exposes nightowls_* tools to external agents).").action(async () => {
|
|
409
|
+
const { runMcpServer } = await import("./mcp-3ZFDXVDT.js");
|
|
410
|
+
await runMcpServer();
|
|
411
|
+
});
|
|
412
|
+
async function main() {
|
|
413
|
+
const cwd = process.cwd();
|
|
414
|
+
const plugins = await discoverPlugins(nodeDiscoverDeps(cwd)).catch(() => []);
|
|
415
|
+
registerPluginCommands(program, plugins, { cwd, log: console.log });
|
|
416
|
+
await program.parseAsync();
|
|
417
|
+
}
|
|
418
|
+
main().catch((err) => {
|
|
419
|
+
console.error(err instanceof Error ? err.message : err);
|
|
420
|
+
process.exitCode = 1;
|
|
421
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The declarative Night Owls plugin contract.
|
|
3
|
+
*
|
|
4
|
+
* Each `@nightowlsdev/*` adapter exports a `nightOwlsPlugin` plain object that STRUCTURALLY matches
|
|
5
|
+
* `NightOwlsPlugin` — adapters do NOT import this type (that would create a dependency cycle, since the
|
|
6
|
+
* CLI also imports the adapters). All codegen (env merge, file scaffold, config insertion, migrations)
|
|
7
|
+
* lives in the CLI; adapters stay data-only.
|
|
8
|
+
*/
|
|
9
|
+
/** A single Night Owls migration: a stable `version` (ledger key), a human `name`, and `nightowls.*` SQL. */
|
|
10
|
+
interface Migration {
|
|
11
|
+
version: string;
|
|
12
|
+
name: string;
|
|
13
|
+
sql: string;
|
|
14
|
+
}
|
|
15
|
+
/** An environment variable the adapter needs. Appended to `.env.example` if its `key` is absent. */
|
|
16
|
+
interface PluginEnv {
|
|
17
|
+
key: string;
|
|
18
|
+
example: string;
|
|
19
|
+
comment?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A file scaffolded into the host (e.g. a Next.js route handler). Written only if absent (idempotent). */
|
|
22
|
+
interface PluginFile {
|
|
23
|
+
path: string;
|
|
24
|
+
contents: string;
|
|
25
|
+
}
|
|
26
|
+
/** The `import` line + the wiring `snippet` inserted at the `// nightowls:<marker>` line in the config. */
|
|
27
|
+
interface PluginConfig {
|
|
28
|
+
import: string;
|
|
29
|
+
snippet: string;
|
|
30
|
+
marker: "storage" | "auth" | "runner" | "telemetry" | "connector" | "model";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Context passed to a plugin's `init` hook and command handlers. Structural — adapters type their
|
|
34
|
+
* handlers against an equivalent shape WITHOUT importing @nightowlsdev/cli (keeps their .d.ts cycle-free).
|
|
35
|
+
*/
|
|
36
|
+
interface PluginContext {
|
|
37
|
+
/** The host project root (cwd). */
|
|
38
|
+
cwd: string;
|
|
39
|
+
/** Print a line to the user. */
|
|
40
|
+
log: (msg: string) => void;
|
|
41
|
+
}
|
|
42
|
+
/** The context a plugin command's `run` receives — the base context plus the parsed argv. */
|
|
43
|
+
interface PluginCommandContext extends PluginContext {
|
|
44
|
+
/** Positional args after `owl <plugin> <cmd>`. */
|
|
45
|
+
args: string[];
|
|
46
|
+
/** Parsed `--flag` options. */
|
|
47
|
+
options: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
/** A flag a plugin command accepts (declarative; registered on the Commander subcommand). */
|
|
50
|
+
interface PluginOption {
|
|
51
|
+
/** e.g. "--json" or "--out <dir>". */
|
|
52
|
+
flags: string;
|
|
53
|
+
description: string;
|
|
54
|
+
defaultValue?: string | boolean;
|
|
55
|
+
}
|
|
56
|
+
/** A subcommand a plugin exposes, run as `owl <plugin-name> <command-name>`. */
|
|
57
|
+
interface PluginCommand {
|
|
58
|
+
name: string;
|
|
59
|
+
description: string;
|
|
60
|
+
options?: readonly PluginOption[];
|
|
61
|
+
/** Self-contained: uses only the ctx + the adapter's own logic + Node. MUST NOT import @nightowlsdev/cli. */
|
|
62
|
+
run: (ctx: PluginCommandContext) => Promise<void> | void;
|
|
63
|
+
}
|
|
64
|
+
/** The full adapter manifest the CLI discovers, dynamic-imports, and acts on. */
|
|
65
|
+
interface NightOwlsPlugin {
|
|
66
|
+
/** Adapter short name, e.g. "storage-supabase". */
|
|
67
|
+
name: string;
|
|
68
|
+
version: string;
|
|
69
|
+
kind?: "storage" | "runner" | "auth" | "telemetry" | "ui" | "connector" | "model";
|
|
70
|
+
/** npm name, e.g. "@nightowlsdev/storage-supabase". */
|
|
71
|
+
pkg: string;
|
|
72
|
+
/** One-line summary shown by `owl plugins` and `owl <plugin> --help`. */
|
|
73
|
+
description?: string;
|
|
74
|
+
migrations?: readonly Migration[];
|
|
75
|
+
env?: readonly PluginEnv[];
|
|
76
|
+
files?: readonly PluginFile[];
|
|
77
|
+
config?: PluginConfig;
|
|
78
|
+
/**
|
|
79
|
+
* Runs during `owl init`, `owl install <plugin>`, and `owl init <plugin>` — AFTER the
|
|
80
|
+
* declarative wiring. For integration that can't be expressed declaratively (idempotent; print-only
|
|
81
|
+
* side effects preferred — never apply DDL).
|
|
82
|
+
*/
|
|
83
|
+
init?: (ctx: PluginContext) => Promise<void> | void;
|
|
84
|
+
/** Plugin-specific subcommands, exposed as `owl <plugin-name> <command-name>`. */
|
|
85
|
+
commands?: readonly PluginCommand[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface DiscoverDeps {
|
|
89
|
+
readPkgJson: () => Promise<{
|
|
90
|
+
dependencies?: Record<string, string>;
|
|
91
|
+
devDependencies?: Record<string, string>;
|
|
92
|
+
}>;
|
|
93
|
+
importPlugin: (pkg: string) => Promise<{
|
|
94
|
+
nightOwlsPlugin?: NightOwlsPlugin;
|
|
95
|
+
}>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Discover installed `@nightowlsdev/*` adapters that export a `nightOwlsPlugin` manifest. Injected deps keep this
|
|
99
|
+
* hermetic (tests pass fakes; production uses `nodeDiscoverDeps`). `@nightowlsdev/*` deps without a manifest
|
|
100
|
+
* (e.g. `@nightowlsdev/react`) are silently ignored.
|
|
101
|
+
*/
|
|
102
|
+
declare function discoverPlugins(deps: DiscoverDeps): Promise<NightOwlsPlugin[]>;
|
|
103
|
+
/** Production deps: read `<cwd>/package.json` + dynamic-import each adapter's manifest resolved from the
|
|
104
|
+
* HOST cwd (so a real host's installed adapters are discovered, not the CLI's own deps). */
|
|
105
|
+
declare function nodeDiscoverDeps(cwd: string): DiscoverDeps;
|
|
106
|
+
|
|
107
|
+
export { type DiscoverDeps, type Migration, type NightOwlsPlugin, type PluginCommand, type PluginCommandContext, type PluginConfig, type PluginContext, type PluginEnv, type PluginFile, type PluginOption, discoverPlugins, nodeDiscoverDeps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/mcp.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { createMastraLibsqlStore } from "@nightowlsdev/storage-local";
|
|
7
|
+
function doctorReport() {
|
|
8
|
+
const bootstrapStore = createMastraLibsqlStore({ url: ":memory:" });
|
|
9
|
+
return {
|
|
10
|
+
ok: true,
|
|
11
|
+
bootstrapStore: bootstrapStore ? "available (libsql :memory:)" : "unavailable",
|
|
12
|
+
node: process.version,
|
|
13
|
+
cwd: process.cwd()
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function buildMcpServer() {
|
|
17
|
+
const server = new McpServer({ name: "nightowls", version: "0.0.0" });
|
|
18
|
+
server.registerTool(
|
|
19
|
+
"nightowls_doctor",
|
|
20
|
+
{
|
|
21
|
+
title: "Night Owls doctor",
|
|
22
|
+
description: "Report Night Owls environment diagnostics (read-only).",
|
|
23
|
+
annotations: { readOnlyHint: true }
|
|
24
|
+
},
|
|
25
|
+
async () => ({ content: [{ type: "text", text: JSON.stringify(doctorReport(), null, 2) }] })
|
|
26
|
+
);
|
|
27
|
+
return server;
|
|
28
|
+
}
|
|
29
|
+
async function runMcpServer() {
|
|
30
|
+
const server = buildMcpServer();
|
|
31
|
+
const transport = new StdioServerTransport();
|
|
32
|
+
await server.connect(transport);
|
|
33
|
+
console.error("[nightowls] mcp server ready on stdio (nightowls_doctor)");
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
buildMcpServer,
|
|
37
|
+
doctorReport,
|
|
38
|
+
runMcpServer
|
|
39
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
addDep,
|
|
4
|
+
detectPackageManager,
|
|
5
|
+
exec,
|
|
6
|
+
hostImport,
|
|
7
|
+
isInstalled,
|
|
8
|
+
loadPlugin,
|
|
9
|
+
nodeFs
|
|
10
|
+
} from "./chunk-YZIYBDZO.js";
|
|
11
|
+
export {
|
|
12
|
+
addDep,
|
|
13
|
+
detectPackageManager,
|
|
14
|
+
exec,
|
|
15
|
+
hostImport,
|
|
16
|
+
isInstalled,
|
|
17
|
+
loadPlugin,
|
|
18
|
+
nodeFs
|
|
19
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nightowlsdev/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"owl": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/cueplusplus/corale.git",
|
|
24
|
+
"directory": "packages/cli"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/cueplusplus/corale#readme",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"commander": "^15.0.0",
|
|
29
|
+
"ts-morph": "^28.0.0",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
31
|
+
"@nightowlsdev/storage-local": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^24",
|
|
35
|
+
"memfs": "^4.57.0",
|
|
36
|
+
"tsup": "8.5.1",
|
|
37
|
+
"typescript": "6.0.3",
|
|
38
|
+
"vitest": "^3.2.0",
|
|
39
|
+
"@nightowlsdev/storage-supabase": "0.3.0",
|
|
40
|
+
"@nightowlsdev/core": "0.3.0",
|
|
41
|
+
"@nightowlsdev/telemetry-otel": "0.1.1",
|
|
42
|
+
"@nightowlsdev/telemetry-langfuse": "0.1.1",
|
|
43
|
+
"@nightowlsdev/runner-nextjs": "0.2.0",
|
|
44
|
+
"@nightowlsdev/runner-background": "0.2.0",
|
|
45
|
+
"@nightowlsdev/model-anthropic": "0.1.0",
|
|
46
|
+
"@nightowlsdev/auth-supabase": "0.1.1",
|
|
47
|
+
"@nightowlsdev/auth-auth0": "0.1.1",
|
|
48
|
+
"@nightowlsdev/eslint-config": "0.0.0",
|
|
49
|
+
"@nightowlsdev/mcp": "0.1.1",
|
|
50
|
+
"@nightowlsdev/tsconfig": "0.0.0",
|
|
51
|
+
"@nightowlsdev/model-openai": "0.1.0",
|
|
52
|
+
"@nightowlsdev/model-vercel-gateway": "0.1.0",
|
|
53
|
+
"@nightowlsdev/model-openrouter": "0.1.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"lint": "eslint src"
|
|
60
|
+
}
|
|
61
|
+
}
|