@m6d/cortex-cli 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -128
- package/package.json +16 -5
- package/src/cli.ts +19 -12
- package/src/commands/apply.ts +186 -0
- package/src/commands/{cc.ts → management.ts} +173 -112
- package/src/commands/new.ts +83 -84
- package/src/commands/setup.ts +610 -0
- package/src/contracts/charts/chart-spec.ts +1 -1
- package/src/contracts/client-tools/index.ts +3 -3
- package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
- package/src/contracts/datasets/index.ts +3 -0
- package/src/contracts/egress-policy/index.ts +16 -0
- package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
- package/src/contracts/{egress → egress-policy}/url.ts +22 -9
- package/src/contracts/errors/index.ts +38 -0
- package/src/contracts/json/index.ts +5 -0
- package/src/contracts/manifest/index.ts +973 -0
- package/src/contracts/manifest-file/index.ts +16 -0
- package/src/contracts/markdown/index.ts +1 -1
- package/src/contracts/pattern/index.ts +276 -0
- package/src/contracts/server/index.ts +349 -0
- package/src/contracts/server/reserved-names.fixtures.json +18 -0
- package/src/contracts/triggers/index.ts +112 -0
- package/src/contracts/wire/index.ts +67 -33
- package/src/{cc → management}/client.ts +33 -7
- package/src/{cc → management}/flags.ts +2 -1
- package/src/{cc → management}/profiles.ts +10 -8
- package/src/scaffold/files.ts +448 -284
- package/src/scaffold/ports.ts +12 -24
- package/src/scaffold/templates/cspell.json.txt +41 -0
- package/src/scaffold/templates/datasets.ts.txt +52 -0
- package/src/scaffold/templates/dockerignore.txt +6 -0
- package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
- package/src/scaffold/templates/gitignore.txt +1 -1
- package/src/scaffold/templates/server.ts.txt +40 -0
- package/src/scaffold/templates/setup.ts.txt +44 -0
- package/src/ui/prompts.ts +24 -38
- package/src/ui/report.ts +5 -15
- package/tsconfig.json +2 -3
- package/src/commands/graph.ts +0 -228
- package/src/commands/swagger.ts +0 -247
- package/src/config/load.ts +0 -122
- package/src/config/validate.ts +0 -46
- package/src/contracts/credentials/index.ts +0 -183
- package/src/contracts/egress/index.ts +0 -120
- package/src/contracts/graph/clients/embed.ts +0 -62
- package/src/contracts/graph/clients/neo4j.ts +0 -85
- package/src/contracts/graph/helpers.ts +0 -55
- package/src/contracts/graph/index.ts +0 -30
- package/src/contracts/graph/schema.ts +0 -65
- package/src/contracts/graph/types.ts +0 -125
- package/src/contracts/rich-text/index.ts +0 -220
- package/src/contracts/runtime/api.ts +0 -345
- package/src/contracts/runtime/auth.ts +0 -41
- package/src/contracts/runtime/grammar.ts +0 -169
- package/src/contracts/runtime/index.ts +0 -15
- package/src/contracts/runtime/models.ts +0 -50
- package/src/contracts/runtime/signature.ts +0 -255
- package/src/contracts/runtime/sync.ts +0 -196
- package/src/contracts/runtime/triggers.ts +0 -267
- package/src/graph/expand-domains.ts +0 -286
- package/src/graph/generate-cypher.ts +0 -201
- package/src/graph/seed.ts +0 -199
- package/src/graph/validate.ts +0 -78
- package/src/scaffold/features.ts +0 -190
- package/src/scaffold/templates/domains-README.md.txt +0 -16
- package/src/scaffold/templates/index.ts.txt +0 -18
- package/src/swagger/extract-endpoints.ts +0 -269
- package/src/swagger/generated-block.ts +0 -74
- package/src/swagger/openapi-schema.ts +0 -168
- package/src/swagger/parse-response.ts +0 -88
- package/src/swagger/types.ts +0 -25
- /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
- /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
- /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
- /package/src/{cc → management}/output.ts +0 -0
package/src/scaffold/ports.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host ports for the
|
|
3
|
-
* time.
|
|
2
|
+
* Host ports for the Cortex stack a generated project runs, chosen once at
|
|
3
|
+
* scaffold time.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* these — it is the project's to choose, and stays the documented default.
|
|
5
|
+
* Only the app and the engine are published: the browser and this
|
|
6
|
+
* project's callers reach them, and nothing outside the stack reaches its
|
|
7
|
+
* databases. The backend listens on the third. Fixed ports would collide with
|
|
8
|
+
* the next project scaffolded on the same machine, so each project draws its
|
|
9
|
+
* own. Container ports stay canonical.
|
|
11
10
|
*
|
|
12
11
|
* Every generated file spells its ports `{{port.<name>}}` and gets them filled in
|
|
13
|
-
* by `applyPorts`. That is the point of the token: compose and
|
|
14
|
-
* drift apart, because neither holds a number of its own.
|
|
12
|
+
* by `applyPorts`. That is the point of the token: compose and the README
|
|
13
|
+
* cannot drift apart, because neither holds a number of its own.
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
16
|
/** High, unprivileged, and clear of the ephemeral range macOS and Linux hand out. */
|
|
@@ -51,11 +50,7 @@ function allocate(taken: Set<number>) {
|
|
|
51
50
|
return first ?? FIRST;
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
/**
|
|
55
|
-
* Every port a generated project can need, allocated together so they are
|
|
56
|
-
* distinct within the project. Features that were not selected simply never
|
|
57
|
-
* mention theirs.
|
|
58
|
-
*/
|
|
53
|
+
/** Every port a generated project publishes, allocated together so they are distinct. */
|
|
59
54
|
export function allocatePorts() {
|
|
60
55
|
const taken = new Set<number>();
|
|
61
56
|
|
|
@@ -65,19 +60,12 @@ export function allocatePorts() {
|
|
|
65
60
|
return port;
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
return {
|
|
69
|
-
postgres: next(),
|
|
70
|
-
redis: next(),
|
|
71
|
-
minio: next(),
|
|
72
|
-
minioConsole: next(),
|
|
73
|
-
neo4j: next(),
|
|
74
|
-
neo4jBolt: next(),
|
|
75
|
-
};
|
|
63
|
+
return { cortex: next(), engine: next(), server: next() };
|
|
76
64
|
}
|
|
77
65
|
|
|
78
66
|
export type Ports = ReturnType<typeof allocatePorts>;
|
|
79
67
|
|
|
80
|
-
/** Fills in `{{port
|
|
68
|
+
/** Fills in every `{{port.<name>}}`. Applied to every file, once, at the end. */
|
|
81
69
|
export function applyPorts(contents: string, ports: Ports) {
|
|
82
70
|
return Object.entries(ports).reduce(
|
|
83
71
|
(text, [name, port]) => text.replaceAll(`{{port.${name}}}`, String(port)),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2",
|
|
3
|
+
"language": "en",
|
|
4
|
+
"words": ["mprocs"],
|
|
5
|
+
"ignorePaths": [
|
|
6
|
+
"node_modules/**",
|
|
7
|
+
"out/**",
|
|
8
|
+
"build/**",
|
|
9
|
+
"**/dist/**",
|
|
10
|
+
"coverage/**",
|
|
11
|
+
"*.tsbuildinfo",
|
|
12
|
+
"bun.lock",
|
|
13
|
+
"package-lock.json",
|
|
14
|
+
"pnpm-lock.yaml",
|
|
15
|
+
"yarn.lock",
|
|
16
|
+
"*.db",
|
|
17
|
+
"*.sqlite",
|
|
18
|
+
"scratch.*",
|
|
19
|
+
".env*",
|
|
20
|
+
"**/*.min.js",
|
|
21
|
+
"**/*.min.css",
|
|
22
|
+
"**/db/migrations/**",
|
|
23
|
+
"**/i18n/**",
|
|
24
|
+
"docs",
|
|
25
|
+
".claude",
|
|
26
|
+
".angular",
|
|
27
|
+
".vercel/**",
|
|
28
|
+
"vendor/**",
|
|
29
|
+
".gitignore"
|
|
30
|
+
],
|
|
31
|
+
"dictionaries": [
|
|
32
|
+
"typescript",
|
|
33
|
+
"node",
|
|
34
|
+
"npm",
|
|
35
|
+
"html",
|
|
36
|
+
"css",
|
|
37
|
+
"bash",
|
|
38
|
+
"companies",
|
|
39
|
+
"softwareTerms"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { defineDatasetHost } from "@m6d/cortex";
|
|
3
|
+
import { drizzleDataset } from "@m6d/cortex/drizzle";
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* This backend as a dataset host: an `orders` table agents ask grouped
|
|
9
|
+
* questions of, such as revenue per product by month. It lives in memory and
|
|
10
|
+
* is seeded on every boot; point `db` and the table at your own database.
|
|
11
|
+
* The host answers whoever reaches its URL, so `resolveUser` is where yours
|
|
12
|
+
* verifies the token Cortex forwards, and `scoped` narrows rows to that user.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const orders = sqliteTable("orders", {
|
|
16
|
+
id: integer().primaryKey(),
|
|
17
|
+
product: text().notNull(),
|
|
18
|
+
quantity: integer().notNull(),
|
|
19
|
+
total: real().notNull(),
|
|
20
|
+
/** ISO text, which the dataset reads as a date. */
|
|
21
|
+
placedAt: text().notNull(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const sqlite = new Database(":memory:");
|
|
25
|
+
sqlite.run(
|
|
26
|
+
"create table orders (id integer primary key, product text not null, quantity integer not null, total real not null, placed_at text not null)",
|
|
27
|
+
);
|
|
28
|
+
const db = drizzle({ client: sqlite, casing: "snake_case" });
|
|
29
|
+
db.insert(orders)
|
|
30
|
+
.values([
|
|
31
|
+
{ product: "Notebook", quantity: 3, total: 12, placedAt: "2026-01-05" },
|
|
32
|
+
{ product: "Pen", quantity: 10, total: 15, placedAt: "2026-01-19" },
|
|
33
|
+
{ product: "Notebook", quantity: 1, total: 4, placedAt: "2026-02-02" },
|
|
34
|
+
{ product: "Backpack", quantity: 1, total: 49, placedAt: "2026-02-14" },
|
|
35
|
+
{ product: "Pen", quantity: 4, total: 6, placedAt: "2026-03-08" },
|
|
36
|
+
])
|
|
37
|
+
.run();
|
|
38
|
+
|
|
39
|
+
export const datasets = defineDatasetHost({
|
|
40
|
+
expose: [
|
|
41
|
+
drizzleDataset({
|
|
42
|
+
name: "orders",
|
|
43
|
+
description: "One row per order: what was bought, how many, for how much, and when.",
|
|
44
|
+
table: orders,
|
|
45
|
+
dialect: "sqlite",
|
|
46
|
+
db,
|
|
47
|
+
types: { placedAt: "date" },
|
|
48
|
+
scoped: () => undefined,
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
resolveUser: () => ({}),
|
|
52
|
+
});
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
|
|
3
3
|
|
|
4
|
-
// The
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
4
|
+
// The roots everything else hangs off: the backend, the manifest
|
|
5
|
+
// `cortex apply` loads, and the env setup `bun install` runs. Left to
|
|
6
|
+
// auto-detection, framework heuristics mark far too much as reachable and
|
|
7
|
+
// unused exports stop being reportable.
|
|
8
|
+
"entry": ["index.ts", "cortex.manifest.ts", "setup.ts"],
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
// zod, an extra route on `server.app`. A requirement that lives in another
|
|
13
|
-
// package's `peerDependencies` is not something reachability analysis can
|
|
14
|
-
// see, so it is stated here instead. Once your own code imports one, its
|
|
15
|
-
// entry is doing nothing and can go.
|
|
16
|
-
"ignoreDependencies": ["@tanstack/ai", "@tanstack/ai-openai", "hono", "jose", "openai", "zod"],
|
|
10
|
+
// The CLI is run as `cortex`, a binary nothing here imports, so
|
|
11
|
+
// reachability analysis cannot see that the project uses it.
|
|
12
|
+
"ignoreDependencies": ["@m6d/cortex-cli"],
|
|
17
13
|
|
|
18
14
|
"duplicates": {
|
|
19
15
|
// fallow's default is 0, which means "no limit": clone groups get printed
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineServer } from "@m6d/cortex";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This backend as a Cortex server: the variable its agents' prompts read,
|
|
6
|
+
* and the native tool it runs for them. Cortex presents CORTEX_SERVER_KEY as
|
|
7
|
+
* the bearer of every call, and nothing below runs for any other.
|
|
8
|
+
*/
|
|
9
|
+
export const server = defineServer({
|
|
10
|
+
key: process.env["CORTEX_SERVER_KEY"]!,
|
|
11
|
+
variables: {
|
|
12
|
+
// Resolved once per thread; prompts read it as {{userName}}.
|
|
13
|
+
userName: {
|
|
14
|
+
description: "Display name of the signed-in user",
|
|
15
|
+
example: "Sara",
|
|
16
|
+
type: "string",
|
|
17
|
+
resolve: ({ user }) => {
|
|
18
|
+
const name = user?.claims["name"];
|
|
19
|
+
return typeof name === "string" ? name : "guest";
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
tools: {
|
|
24
|
+
rollDice: {
|
|
25
|
+
description: "Roll dice and report each face and the total.",
|
|
26
|
+
input: z.object({
|
|
27
|
+
count: z.number().int().min(1).max(10).describe("How many dice to roll"),
|
|
28
|
+
sides: z.number().int().min(2).max(100).default(6).describe("Faces per die"),
|
|
29
|
+
}),
|
|
30
|
+
readOnly: true,
|
|
31
|
+
run: ({ count, sides }) => {
|
|
32
|
+
const faces = Array.from(
|
|
33
|
+
{ length: count },
|
|
34
|
+
() => 1 + Math.floor(Math.random() * sides),
|
|
35
|
+
);
|
|
36
|
+
return { faces, total: faces.reduce((sum, face) => sum + face, 0) };
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Writes .env on every `bun install`: it starts from the committed
|
|
6
|
+
* .env.example, or from .env when there is one, and each value left blank
|
|
7
|
+
* that is listed below is generated here, the Compose project this
|
|
8
|
+
* checkout's stack runs as among them. A value already set is never
|
|
9
|
+
* changed, so a fresh clone gets a stack and secrets of its own, and a rerun
|
|
10
|
+
* changes nothing. Without either file, as in an image build, it writes
|
|
11
|
+
* nothing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const token = () => randomBytes(32).toString("base64url");
|
|
15
|
+
const key = () => randomBytes(32).toString("base64");
|
|
16
|
+
|
|
17
|
+
/** The package name as Compose takes a project name: lowercase words and hyphens. */
|
|
18
|
+
const { name } = JSON.parse(readFileSync("package.json", "utf8")) as { name: string };
|
|
19
|
+
const project =
|
|
20
|
+
name
|
|
21
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
22
|
+
.slice(0, 40)
|
|
23
|
+
.replace(/^-+|-+$/g, "") || "cortex";
|
|
24
|
+
|
|
25
|
+
const GENERATED = [
|
|
26
|
+
["COMPOSE_PROJECT_NAME", () => `${project}-${randomBytes(4).toString("hex")}`],
|
|
27
|
+
["CORTEX_SERVER_KEY", token],
|
|
28
|
+
["DEFAULT_ADMIN_PASSWORD", token],
|
|
29
|
+
// Cortex takes management keys only under this prefix. cspell:ignore mgmt
|
|
30
|
+
["BOOTSTRAP_MANAGEMENT_KEY", () => `cortex_mgmt_${token()}`],
|
|
31
|
+
["BETTER_AUTH_SECRET", key],
|
|
32
|
+
["SECRETS_MASTER_KEY", key],
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
const source = existsSync(".env") ? ".env" : ".env.example";
|
|
36
|
+
if (existsSync(source)) {
|
|
37
|
+
// Only its owner reads a file of secrets, however it was first created.
|
|
38
|
+
if (source === ".env") chmodSync(".env", 0o600);
|
|
39
|
+
let text = readFileSync(source, "utf8");
|
|
40
|
+
for (const [variable, generate] of GENERATED) {
|
|
41
|
+
text = text.replace(new RegExp(`^${variable}=$`, "m"), `${variable}=${generate()}`);
|
|
42
|
+
}
|
|
43
|
+
writeFileSync(".env", text, { mode: 0o600 });
|
|
44
|
+
}
|
package/src/ui/prompts.ts
CHANGED
|
@@ -8,20 +8,16 @@
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
cancel,
|
|
11
|
+
confirm,
|
|
11
12
|
intro,
|
|
12
13
|
isCancel,
|
|
13
14
|
log,
|
|
14
|
-
multiselect,
|
|
15
15
|
outro,
|
|
16
16
|
password,
|
|
17
|
-
select,
|
|
18
17
|
spinner,
|
|
19
18
|
text,
|
|
20
19
|
} from "@clack/prompts";
|
|
21
20
|
|
|
22
|
-
import { FEATURES, type Feature } from "@/scaffold/features";
|
|
23
|
-
import type { Database } from "@/scaffold/files";
|
|
24
|
-
|
|
25
21
|
export { intro, log, outro };
|
|
26
22
|
|
|
27
23
|
async function orExit<T>(answer: Promise<T | symbol>) {
|
|
@@ -54,52 +50,42 @@ export function askName(check: (name: string) => string | undefined) {
|
|
|
54
50
|
);
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*/
|
|
61
|
-
export function askDatabase() {
|
|
62
|
-
return orExit(
|
|
63
|
-
select<Database>({
|
|
64
|
-
message: "Database",
|
|
65
|
-
initialValue: "postgres",
|
|
66
|
-
options: [
|
|
67
|
-
{ value: "postgres", hint: "docker-compose.yml runs one for you" },
|
|
68
|
-
{ value: "mssql", hint: "normally an existing external instance" },
|
|
69
|
-
],
|
|
70
|
-
}),
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** One multi-select for the whole catalogue — minus anything a flag settled. */
|
|
75
|
-
export function askFeatures(undecided: Feature[]) {
|
|
76
|
-
return orExit(
|
|
77
|
-
multiselect<Feature>({
|
|
78
|
-
message: "Which optional features?",
|
|
79
|
-
required: false,
|
|
80
|
-
options: undecided.map((feature) => ({ value: feature, hint: FEATURES[feature].hint })),
|
|
81
|
-
}),
|
|
82
|
-
);
|
|
53
|
+
/** A yes-or-no question a flag left open, answered no unless the caller says otherwise. */
|
|
54
|
+
export function askConfirm(message: string, initialValue = false) {
|
|
55
|
+
return orExit(confirm({ message, initialValue }));
|
|
83
56
|
}
|
|
84
57
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
58
|
+
/**
|
|
59
|
+
* A required value a command was not given: asked by name, re-asked while
|
|
60
|
+
* empty or while `check` returns a complaint about the trimmed value.
|
|
61
|
+
*/
|
|
62
|
+
export function askValue(
|
|
63
|
+
name: string,
|
|
64
|
+
hint?: string,
|
|
65
|
+
check: (value: string) => string | undefined = () => undefined,
|
|
66
|
+
) {
|
|
87
67
|
return orExit(
|
|
88
68
|
text({
|
|
89
69
|
message: name,
|
|
90
70
|
placeholder: hint,
|
|
91
|
-
validate: (value)
|
|
92
|
-
(value ?? "").trim()
|
|
71
|
+
validate: function (value) {
|
|
72
|
+
const candidate = (value ?? "").trim();
|
|
73
|
+
return candidate.length === 0 ? `${name} is required.` : check(candidate);
|
|
74
|
+
},
|
|
93
75
|
}),
|
|
94
76
|
);
|
|
95
77
|
}
|
|
96
78
|
|
|
97
|
-
/** A credential: typed masked, never echoed. */
|
|
98
|
-
export function askSecret(
|
|
79
|
+
/** A credential: typed masked, never echoed, and re-asked while `check` complains. */
|
|
80
|
+
export function askSecret(
|
|
81
|
+
name: string,
|
|
82
|
+
check: (value: string) => string | undefined = () => undefined,
|
|
83
|
+
) {
|
|
99
84
|
return orExit(
|
|
100
85
|
password({
|
|
101
86
|
message: name,
|
|
102
|
-
validate: (value) =>
|
|
87
|
+
validate: (value) =>
|
|
88
|
+
(value ?? "").length === 0 ? `${name} is required.` : check(value ?? ""),
|
|
103
89
|
}),
|
|
104
90
|
);
|
|
105
91
|
}
|
package/src/ui/report.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The output grammar, shared by every command
|
|
2
|
+
* The output grammar, shared by every command.
|
|
3
3
|
* The report layout is deliberately frozen; this file is the only
|
|
4
4
|
* place that knows the box characters.
|
|
5
5
|
*/
|
|
@@ -31,12 +31,7 @@ function describe(error: unknown) {
|
|
|
31
31
|
// anything opened the box above it.
|
|
32
32
|
let open = false;
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
export function plural(count: number, noun: string) {
|
|
36
|
-
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** `┌ cortex graph seed` — opens a run. */
|
|
34
|
+
/** `┌ cortex login` — opens a run. */
|
|
40
35
|
export function header(title: string) {
|
|
41
36
|
open = true;
|
|
42
37
|
write(process.stdout, ["", `${pc.gray("┌")} ${title}`, BAR]);
|
|
@@ -50,20 +45,15 @@ export function input(label: string, value: string) {
|
|
|
50
45
|
write(process.stdout, [`${pc.green("◇")} ${label.padEnd(9)} ${value}`]);
|
|
51
46
|
}
|
|
52
47
|
|
|
53
|
-
export function blank() {
|
|
54
|
-
write(process.stdout, [BAR]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
48
|
/**
|
|
58
|
-
* `│
|
|
59
|
-
*
|
|
60
|
-
* is the label column: findings fit in 10, domain names need 14.
|
|
49
|
+
* `│ name orders` — a row in the body. An empty label indents to the same
|
|
50
|
+
* gutter, for a continuation line. `width` is the label column.
|
|
61
51
|
*/
|
|
62
52
|
export function row(label: string, text: string, width = 10) {
|
|
63
53
|
write(process.stdout, [`${BAR} ${label.padEnd(width - 2)} ${text}`]);
|
|
64
54
|
}
|
|
65
55
|
|
|
66
|
-
/** `└
|
|
56
|
+
/** `└ authenticated with ada@example.com` — closes a run. */
|
|
67
57
|
export function footer(text: string) {
|
|
68
58
|
write(process.stdout, [`${pc.gray("└")} ${text}`, ""]);
|
|
69
59
|
}
|
package/tsconfig.json
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
// Environment setup & latest features
|
|
6
6
|
"lib": ["ESNext"],
|
|
7
7
|
// Named explicitly: auto-inclusion of @types/* is an editor-dependent
|
|
8
|
-
// heuristic, and `Bun`/`Response` come from here.
|
|
9
|
-
// tsconfig for the same fix.
|
|
8
|
+
// heuristic, and `Bun`/`Response` come from here.
|
|
10
9
|
"types": ["bun"],
|
|
11
10
|
"target": "ESNext",
|
|
12
11
|
"module": "Preserve",
|
|
@@ -18,7 +17,7 @@
|
|
|
18
17
|
"resolveJsonModule": true,
|
|
19
18
|
"noEmit": true,
|
|
20
19
|
"paths": {
|
|
21
|
-
//
|
|
20
|
+
// @cortex/contracts is a private workspace package
|
|
22
21
|
// that never publishes, so the CLI carries its own copy: `prepack`
|
|
23
22
|
// vendors it into src/contracts, where the first candidate finds it in
|
|
24
23
|
// the published tarball. Inside the repo that copy doesn't exist and
|