@basaltkit/cli 1.0.0 → 1.2.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/LICENSE +1 -1
- package/README.md +8 -2
- package/dist/index.d.ts +133 -1
- package/dist/index.js +306 -6
- package/package.json +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -87,8 +87,14 @@ Without registering anything, `runCli` always provides:
|
|
|
87
87
|
| `basalt list` (or `basalt` with no arguments) | Lists all available commands, with their descriptions |
|
|
88
88
|
| `basalt routes` | Lists the HTTP routes registered by the application (read from the `http:routes` metadata bucket, populated by HTTP adapters such as `@basaltkit/fastify`) |
|
|
89
89
|
| `basalt schedule:list` | Lists scheduled tasks and their cron expressions (read from the `schedule:entries` bucket, populated by `@basaltkit/scheduler`) |
|
|
90
|
+
| `basalt dev [--entry] [--worker] [--queue] [--no-routes]` | Runs the app with watch + restart, **prints the route table on boot**, and with `--worker` also starts a watched `queue:work` alongside it (server + worker in one command). Delegates watching to `tsx watch` / `node --watch`. |
|
|
91
|
+
| `basalt upgrade [--dry] [--only=<id>]` | Applies framework upgrade codemods (ships the `@machize/*` → `@basaltkit/*` scope rename; `--dry` previews) |
|
|
92
|
+
| `basalt publish [<id>] [--force]` | Copies a bundled stub group into the app — `dockerfile`, `ci`, `editorconfig` (run with no id to list) |
|
|
90
93
|
|
|
91
|
-
If you also install `@basaltkit/generator`, you gain the `make:*` commands
|
|
94
|
+
If you also install `@basaltkit/generator`, you gain the `make:*` commands. Feature
|
|
95
|
+
plugins register their own: `queue:work|stats|retry` (`@basaltkit/queue`),
|
|
96
|
+
`tenant:list|create|migrate|seed|run` (`@basaltkit/tenancy`), `generate:docs`
|
|
97
|
+
(`@basaltkit/http`), and `mail:preview` (`@basaltkit/mailer`).
|
|
92
98
|
|
|
93
99
|
### Defining a command with arguments and flags
|
|
94
100
|
|
|
@@ -211,7 +217,7 @@ Renders the rows as an aligned text table, with no external dependencies. Return
|
|
|
211
217
|
|
|
212
218
|
### `builtinCommands(): CommandDefinition[]`
|
|
213
219
|
|
|
214
|
-
Returns `[routesCommand, scheduleListCommand]`. *(Advanced — `runCli` already includes them automatically.)*
|
|
220
|
+
Returns `[routesCommand, scheduleListCommand, devCommand, upgradeCommand, publishCommand]`. *(Advanced — `runCli` already includes them automatically.)*
|
|
215
221
|
|
|
216
222
|
### `routesCommand` / `scheduleListCommand`
|
|
217
223
|
|
package/dist/index.d.ts
CHANGED
|
@@ -82,4 +82,136 @@ declare const routesCommand: CommandDefinition;
|
|
|
82
82
|
declare const scheduleListCommand: CommandDefinition;
|
|
83
83
|
declare function builtinCommands(): CommandDefinition[];
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
/** Default entry files probed in order when `--entry` is not given. */
|
|
86
|
+
declare const DEV_ENTRY_CANDIDATES: string[];
|
|
87
|
+
/**
|
|
88
|
+
* Picks the first candidate that exists. Pure — `exists` is injected so the
|
|
89
|
+
* resolution is testable without touching disk.
|
|
90
|
+
*/
|
|
91
|
+
declare function resolveDevEntry(candidates: string[], exists: (path: string) => boolean): string | undefined;
|
|
92
|
+
interface DevRunner {
|
|
93
|
+
command: string;
|
|
94
|
+
args: string[];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Chooses how to run the entry with watch/restart, delegating the actual
|
|
98
|
+
* watching to the runtime (no bespoke watcher). Pure and testable:
|
|
99
|
+
*
|
|
100
|
+
* - `.ts` with tsx available → `tsx watch <entry>`
|
|
101
|
+
* - `.ts` without tsx → `node --watch --experimental-strip-types <entry>` (Node ≥ 22.6)
|
|
102
|
+
* - `.js` → `node --watch <entry>`
|
|
103
|
+
*/
|
|
104
|
+
declare function resolveDevRunner(entry: string, options?: {
|
|
105
|
+
tsx?: boolean;
|
|
106
|
+
}): DevRunner;
|
|
107
|
+
/** A route as published into the `http:routes` metadata bucket by an adapter. */
|
|
108
|
+
interface DevRoute {
|
|
109
|
+
method: string;
|
|
110
|
+
url: string;
|
|
111
|
+
meta?: Record<string, unknown>;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Formats the registered routes into printable rows (method, url, and a compact
|
|
115
|
+
* flags column for auth/rate-limit) — sorted by url then method. Pure: this is
|
|
116
|
+
* what `basalt dev` prints on boot, and `pnpm dev` (plain tsx watch) can't.
|
|
117
|
+
*/
|
|
118
|
+
declare function devRouteRows(routes: DevRoute[]): {
|
|
119
|
+
method: string;
|
|
120
|
+
url: string;
|
|
121
|
+
flags: string;
|
|
122
|
+
}[];
|
|
123
|
+
/**
|
|
124
|
+
* `basalt dev [--entry=<file>] [--worker] [--queue=<name>] [--no-routes]`
|
|
125
|
+
*
|
|
126
|
+
* A richer dev loop than a bare `tsx watch`: prints the app's **route table** on
|
|
127
|
+
* boot (the app is already booted by the CLI runner, so routes are known), then
|
|
128
|
+
* runs the server with watch/restart — and, with `--worker`, also starts a
|
|
129
|
+
* watched queue worker alongside it (server + worker in one command, the real
|
|
130
|
+
* producer/worker topology).
|
|
131
|
+
*/
|
|
132
|
+
declare const devCommand: CommandDefinition;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Filesystem surface an upgrade migration works against. Injected so migrations
|
|
136
|
+
* (and `runUpgrade`) are testable with an in-memory tree — no disk needed.
|
|
137
|
+
*/
|
|
138
|
+
interface UpgradeFs {
|
|
139
|
+
/** Recursively lists files under `dir` (relative paths). Skips node_modules/dist/.git. */
|
|
140
|
+
list(dir: string): Promise<string[]>;
|
|
141
|
+
read(path: string): Promise<string>;
|
|
142
|
+
write(path: string, content: string): Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
/** A single edit a migration wants to make — surfaced for `--dry` before writing. */
|
|
145
|
+
interface Edit {
|
|
146
|
+
path: string;
|
|
147
|
+
before: string;
|
|
148
|
+
after: string;
|
|
149
|
+
}
|
|
150
|
+
/** A versioned, idempotent codemod. `plan` computes edits without writing them. */
|
|
151
|
+
interface Migration {
|
|
152
|
+
id: string;
|
|
153
|
+
description: string;
|
|
154
|
+
plan(fs: UpgradeFs, dir: string): Promise<Edit[]>;
|
|
155
|
+
}
|
|
156
|
+
interface UpgradeReport {
|
|
157
|
+
migration: string;
|
|
158
|
+
changed: string[];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A codemod that rewrites the deprecated `@machize/*` npm scope to `@basaltkit/*`
|
|
162
|
+
* across package.json and source imports. Unambiguous (scoped rename), so a plain
|
|
163
|
+
* text replace is safe and idempotent.
|
|
164
|
+
*/
|
|
165
|
+
declare const renameMachizeScope: Migration;
|
|
166
|
+
/** All migrations shipped with the CLI, in application order. */
|
|
167
|
+
declare const MIGRATIONS: Migration[];
|
|
168
|
+
/**
|
|
169
|
+
* Runs the selected migrations. With `dry`, computes and reports edits without
|
|
170
|
+
* writing. Returns a per-migration report of the files it changed.
|
|
171
|
+
*/
|
|
172
|
+
declare function runUpgrade(migrations: Migration[], fs: UpgradeFs, options: {
|
|
173
|
+
dir: string;
|
|
174
|
+
dry?: boolean;
|
|
175
|
+
only?: string;
|
|
176
|
+
}): Promise<UpgradeReport[]>;
|
|
177
|
+
/** Node-backed {@link UpgradeFs}. Recursive, skipping build/vcs directories. */
|
|
178
|
+
declare function nodeUpgradeFs(): UpgradeFs;
|
|
179
|
+
/** `basalt upgrade [--dir=<path>] [--dry] [--only=<migration-id>]` */
|
|
180
|
+
declare const upgradeCommand: CommandDefinition;
|
|
181
|
+
|
|
182
|
+
/** One file a publishable drops into the app. */
|
|
183
|
+
interface PublishableFile {
|
|
184
|
+
/** Path relative to the target dir, e.g. `Dockerfile` or `.github/workflows/ci.yml`. */
|
|
185
|
+
path: string;
|
|
186
|
+
content: string;
|
|
187
|
+
}
|
|
188
|
+
/** A named group of stub files an app can copy in and then own (à la vendor:publish). */
|
|
189
|
+
interface Publishable {
|
|
190
|
+
id: string;
|
|
191
|
+
description: string;
|
|
192
|
+
files(): PublishableFile[];
|
|
193
|
+
}
|
|
194
|
+
/** Filesystem surface for publishing — injected so `runPublish` is testable. */
|
|
195
|
+
interface PublishFs {
|
|
196
|
+
exists(path: string): Promise<boolean>;
|
|
197
|
+
write(path: string, content: string): Promise<void>;
|
|
198
|
+
}
|
|
199
|
+
interface PublishResult {
|
|
200
|
+
written: string[];
|
|
201
|
+
skipped: string[];
|
|
202
|
+
}
|
|
203
|
+
/** Stubs bundled with the CLI. Apps can register more via the metadata bucket. */
|
|
204
|
+
declare const PUBLISHABLES: Publishable[];
|
|
205
|
+
/**
|
|
206
|
+
* Copies a publishable's files into the target, skipping any that already exist
|
|
207
|
+
* unless `force` is set. Returns which files were written vs skipped.
|
|
208
|
+
*/
|
|
209
|
+
declare function runPublish(publishable: Publishable, fs: PublishFs, options?: {
|
|
210
|
+
force?: boolean;
|
|
211
|
+
}): Promise<PublishResult>;
|
|
212
|
+
/** Node-backed {@link PublishFs} rooted at `dir` (creates parent directories). */
|
|
213
|
+
declare function nodePublishFs(dir: string): PublishFs;
|
|
214
|
+
/** `basalt publish [<id>] [--dir=<path>] [--force]` — list, or copy a stub group. */
|
|
215
|
+
declare const publishCommand: CommandDefinition;
|
|
216
|
+
|
|
217
|
+
export { type CommandContext, type CommandDefinition, type CommandIo, DEV_ENTRY_CANDIDATES, type DevRoute, type DevRunner, type Edit, MIGRATIONS, type Migration, PUBLISHABLES, type ParsedArgv, type PublishFs, type PublishResult, type Publishable, type PublishableFile, type RouteMetadata, type RunCliOptions, type ScheduleMetadata, type UpgradeFs, type UpgradeReport, builtinCommands, commandsPlugin, consoleIo, defineCommand, devCommand, devRouteRows, memoryIo, nodePublishFs, nodeUpgradeFs, parseArgv, publishCommand, renameMachizeScope, renderTable, resolveDevEntry, resolveDevRunner, routesCommand, runCli, runPublish, runUpgrade, scheduleListCommand, upgradeCommand };
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,296 @@ function defineCommand(command) {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
// src/runner.ts
|
|
7
|
-
import { ensureMetadata } from "@basaltkit/core";
|
|
7
|
+
import { ensureMetadata as ensureMetadata2 } from "@basaltkit/core";
|
|
8
8
|
|
|
9
9
|
// src/builtins.ts
|
|
10
10
|
import { METADATA } from "@basaltkit/core";
|
|
11
|
+
|
|
12
|
+
// src/dev.ts
|
|
13
|
+
import { ensureMetadata } from "@basaltkit/core";
|
|
14
|
+
var DEV_ENTRY_CANDIDATES = ["src/main.ts", "src/server.ts", "src/index.ts", "src/main.js", "src/index.js"];
|
|
15
|
+
function resolveDevEntry(candidates, exists) {
|
|
16
|
+
return candidates.find(exists);
|
|
17
|
+
}
|
|
18
|
+
function resolveDevRunner(entry, options = {}) {
|
|
19
|
+
const isTs = /\.tsx?$/.test(entry);
|
|
20
|
+
if (isTs && options.tsx) return { command: "tsx", args: ["watch", entry] };
|
|
21
|
+
if (isTs) return { command: "node", args: ["--watch", "--experimental-strip-types", entry] };
|
|
22
|
+
return { command: "node", args: ["--watch", entry] };
|
|
23
|
+
}
|
|
24
|
+
function devRouteRows(routes) {
|
|
25
|
+
return [...routes].sort((a, b) => a.url.localeCompare(b.url) || a.method.localeCompare(b.method)).map((r) => {
|
|
26
|
+
const flags = [];
|
|
27
|
+
if (r.meta?.["auth"] === true) flags.push("auth");
|
|
28
|
+
if (r.meta?.["rateLimit"]) flags.push("rate-limit");
|
|
29
|
+
const tags = r.meta?.["tags"];
|
|
30
|
+
if (Array.isArray(tags) && tags.length) flags.push(...tags.map(String));
|
|
31
|
+
return { method: r.method.toUpperCase(), url: r.url, flags: flags.join(", ") };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
var devCommand = defineCommand({
|
|
35
|
+
name: "dev",
|
|
36
|
+
description: "Run the app with watch + restart, print the route table, and (--worker) an embedded queue worker",
|
|
37
|
+
async handle({ container, io, flags }) {
|
|
38
|
+
const { existsSync } = await import("fs");
|
|
39
|
+
const entry = typeof flags["entry"] === "string" ? flags["entry"] : resolveDevEntry(DEV_ENTRY_CANDIDATES, existsSync);
|
|
40
|
+
if (!entry) {
|
|
41
|
+
io.error(
|
|
42
|
+
`No entry file found. Looked for ${DEV_ENTRY_CANDIDATES.join(", ")}. Pass --entry=<file>.`
|
|
43
|
+
);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
if (flags["routes"] !== false) {
|
|
47
|
+
const routes = ensureMetadata(container).get("http:routes");
|
|
48
|
+
if (routes.length > 0) {
|
|
49
|
+
io.log(`Routes (${routes.length}):`);
|
|
50
|
+
io.table(devRouteRows(routes));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const tsx = await canResolve("tsx");
|
|
54
|
+
const server = resolveDevRunner(entry, { tsx });
|
|
55
|
+
const runners = [{ label: "server", runner: server }];
|
|
56
|
+
if (flags["worker"] === true || flags["w"] === true) {
|
|
57
|
+
const bin = process.argv[1] ?? entry;
|
|
58
|
+
const workerBase = resolveDevRunner(bin, { tsx });
|
|
59
|
+
const queue = typeof flags["queue"] === "string" ? [`--queue=${flags["queue"]}`] : [];
|
|
60
|
+
runners.push({ label: "worker", runner: { command: workerBase.command, args: [...workerBase.args, "queue:work", ...queue] } });
|
|
61
|
+
}
|
|
62
|
+
for (const { label, runner } of runners) {
|
|
63
|
+
io.log(`\u25B6 ${label}: ${runner.command} ${runner.args.join(" ")}`);
|
|
64
|
+
}
|
|
65
|
+
const { spawn } = await import("child_process");
|
|
66
|
+
return await new Promise((resolve) => {
|
|
67
|
+
const children = runners.map(({ runner }) => spawn(runner.command, runner.args, { stdio: "inherit" }));
|
|
68
|
+
const stop = () => children.forEach((c) => c.kill("SIGINT"));
|
|
69
|
+
process.once("SIGINT", stop);
|
|
70
|
+
let done = false;
|
|
71
|
+
const finish = (code) => {
|
|
72
|
+
if (done) return;
|
|
73
|
+
done = true;
|
|
74
|
+
process.removeListener("SIGINT", stop);
|
|
75
|
+
children.forEach((c) => c.kill("SIGINT"));
|
|
76
|
+
resolve(code);
|
|
77
|
+
};
|
|
78
|
+
children[0]?.on("exit", (code) => finish(code ?? 0));
|
|
79
|
+
children.forEach(
|
|
80
|
+
(child, i) => child.on("error", (error) => {
|
|
81
|
+
io.error(`Failed to start ${runners[i]?.label}: ${error.message}`);
|
|
82
|
+
finish(1);
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
async function canResolve(specifier) {
|
|
89
|
+
try {
|
|
90
|
+
const { createRequire } = await import("module");
|
|
91
|
+
createRequire(`${process.cwd()}/`).resolve(specifier);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/upgrade.ts
|
|
99
|
+
var renameMachizeScope = {
|
|
100
|
+
id: "rename-machize-scope",
|
|
101
|
+
description: "Rewrite the deprecated @machize/* scope to @basaltkit/*",
|
|
102
|
+
async plan(fs, dir) {
|
|
103
|
+
const files = (await fs.list(dir)).filter((p) => /\.(t|j)sx?$|\.json$/.test(p));
|
|
104
|
+
const edits = [];
|
|
105
|
+
for (const path of files) {
|
|
106
|
+
const before = await fs.read(path);
|
|
107
|
+
if (!before.includes("@machize/")) continue;
|
|
108
|
+
const after = before.split("@machize/").join("@basaltkit/");
|
|
109
|
+
if (after !== before) edits.push({ path, before, after });
|
|
110
|
+
}
|
|
111
|
+
return edits;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var MIGRATIONS = [renameMachizeScope];
|
|
115
|
+
async function runUpgrade(migrations, fs, options) {
|
|
116
|
+
const selected = options.only ? migrations.filter((m) => m.id === options.only) : migrations;
|
|
117
|
+
const reports = [];
|
|
118
|
+
for (const migration of selected) {
|
|
119
|
+
const edits = await migration.plan(fs, options.dir);
|
|
120
|
+
if (!options.dry) {
|
|
121
|
+
for (const edit of edits) await fs.write(edit.path, edit.after);
|
|
122
|
+
}
|
|
123
|
+
reports.push({ migration: migration.id, changed: edits.map((e) => e.path) });
|
|
124
|
+
}
|
|
125
|
+
return reports;
|
|
126
|
+
}
|
|
127
|
+
function nodeUpgradeFs() {
|
|
128
|
+
const SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", ".next", "build", "coverage"]);
|
|
129
|
+
return {
|
|
130
|
+
async list(dir) {
|
|
131
|
+
const { readdir } = await import("fs/promises");
|
|
132
|
+
const { join, relative } = await import("path");
|
|
133
|
+
const out = [];
|
|
134
|
+
const walk = async (current) => {
|
|
135
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
if (!SKIP.has(entry.name)) await walk(join(current, entry.name));
|
|
139
|
+
} else {
|
|
140
|
+
out.push(relative(dir, join(current, entry.name)));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
await walk(dir);
|
|
145
|
+
return out;
|
|
146
|
+
},
|
|
147
|
+
async read(path) {
|
|
148
|
+
const { readFile } = await import("fs/promises");
|
|
149
|
+
const { isAbsolute, join } = await import("path");
|
|
150
|
+
return readFile(isAbsolute(path) ? path : join(process.cwd(), path), "utf8");
|
|
151
|
+
},
|
|
152
|
+
async write(path, content) {
|
|
153
|
+
const { writeFile } = await import("fs/promises");
|
|
154
|
+
const { isAbsolute, join } = await import("path");
|
|
155
|
+
await writeFile(isAbsolute(path) ? path : join(process.cwd(), path), content, "utf8");
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
var upgradeCommand = defineCommand({
|
|
160
|
+
name: "upgrade",
|
|
161
|
+
description: "Apply framework upgrade codemods (--dry to preview, --only=<id>)",
|
|
162
|
+
async handle({ io, flags }) {
|
|
163
|
+
const dir = typeof flags["dir"] === "string" ? flags["dir"] : process.cwd();
|
|
164
|
+
const dry = flags["dry"] === true;
|
|
165
|
+
const only = typeof flags["only"] === "string" ? flags["only"] : void 0;
|
|
166
|
+
if (only && !MIGRATIONS.some((m) => m.id === only)) {
|
|
167
|
+
io.error(`Unknown migration "${only}". Available: ${MIGRATIONS.map((m) => m.id).join(", ")}.`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
const reports = await runUpgrade(MIGRATIONS, nodeUpgradeFs(), {
|
|
171
|
+
dir,
|
|
172
|
+
dry,
|
|
173
|
+
...only ? { only } : {}
|
|
174
|
+
});
|
|
175
|
+
let total = 0;
|
|
176
|
+
for (const report of reports) {
|
|
177
|
+
if (report.changed.length === 0) continue;
|
|
178
|
+
total += report.changed.length;
|
|
179
|
+
io.log(`${dry ? "[dry] " : ""}${report.migration}: ${report.changed.length} file(s)`);
|
|
180
|
+
for (const path of report.changed) io.log(` ${path}`);
|
|
181
|
+
}
|
|
182
|
+
if (total === 0) io.log("Nothing to upgrade \u2014 everything is up to date.");
|
|
183
|
+
else if (dry) io.log(`
|
|
184
|
+
${total} file(s) would change. Re-run without --dry to apply.`);
|
|
185
|
+
else io.log(`
|
|
186
|
+
Applied ${total} change(s).`);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// src/publish.ts
|
|
192
|
+
var DOCKERFILE = `# syntax=docker/dockerfile:1
|
|
193
|
+
FROM node:22-slim AS base
|
|
194
|
+
ENV NODE_ENV=production
|
|
195
|
+
WORKDIR /app
|
|
196
|
+
|
|
197
|
+
FROM base AS deps
|
|
198
|
+
COPY package.json pnpm-lock.yaml* ./
|
|
199
|
+
RUN corepack enable && pnpm install --prod --frozen-lockfile
|
|
200
|
+
|
|
201
|
+
FROM base AS run
|
|
202
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
203
|
+
COPY . .
|
|
204
|
+
EXPOSE 3000
|
|
205
|
+
CMD ["node", "dist/main.js"]
|
|
206
|
+
`;
|
|
207
|
+
var CI_WORKFLOW = `name: ci
|
|
208
|
+
on:
|
|
209
|
+
push: { branches: [main] }
|
|
210
|
+
pull_request:
|
|
211
|
+
jobs:
|
|
212
|
+
build:
|
|
213
|
+
runs-on: ubuntu-latest
|
|
214
|
+
steps:
|
|
215
|
+
- uses: actions/checkout@v4
|
|
216
|
+
- uses: pnpm/action-setup@v4
|
|
217
|
+
- uses: actions/setup-node@v4
|
|
218
|
+
with: { node-version: 22, cache: pnpm }
|
|
219
|
+
- run: pnpm install --frozen-lockfile
|
|
220
|
+
- run: pnpm run build
|
|
221
|
+
- run: pnpm run test
|
|
222
|
+
`;
|
|
223
|
+
var EDITORCONFIG = `root = true
|
|
224
|
+
|
|
225
|
+
[*]
|
|
226
|
+
charset = utf-8
|
|
227
|
+
end_of_line = lf
|
|
228
|
+
insert_final_newline = true
|
|
229
|
+
indent_style = space
|
|
230
|
+
indent_size = 2
|
|
231
|
+
trim_trailing_whitespace = true
|
|
232
|
+
`;
|
|
233
|
+
var PUBLISHABLES = [
|
|
234
|
+
{ id: "dockerfile", description: "Production multi-stage Dockerfile", files: () => [{ path: "Dockerfile", content: DOCKERFILE }] },
|
|
235
|
+
{ id: "ci", description: "GitHub Actions CI workflow", files: () => [{ path: ".github/workflows/ci.yml", content: CI_WORKFLOW }] },
|
|
236
|
+
{ id: "editorconfig", description: "Shared .editorconfig", files: () => [{ path: ".editorconfig", content: EDITORCONFIG }] }
|
|
237
|
+
];
|
|
238
|
+
async function runPublish(publishable, fs, options = {}) {
|
|
239
|
+
const written = [];
|
|
240
|
+
const skipped = [];
|
|
241
|
+
for (const file of publishable.files()) {
|
|
242
|
+
if (!options.force && await fs.exists(file.path)) {
|
|
243
|
+
skipped.push(file.path);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
await fs.write(file.path, file.content);
|
|
247
|
+
written.push(file.path);
|
|
248
|
+
}
|
|
249
|
+
return { written, skipped };
|
|
250
|
+
}
|
|
251
|
+
function nodePublishFs(dir) {
|
|
252
|
+
return {
|
|
253
|
+
async exists(path) {
|
|
254
|
+
const { access } = await import("fs/promises");
|
|
255
|
+
const { join } = await import("path");
|
|
256
|
+
return access(join(dir, path)).then(
|
|
257
|
+
() => true,
|
|
258
|
+
() => false
|
|
259
|
+
);
|
|
260
|
+
},
|
|
261
|
+
async write(path, content) {
|
|
262
|
+
const { mkdir, writeFile } = await import("fs/promises");
|
|
263
|
+
const { dirname, join } = await import("path");
|
|
264
|
+
const target = join(dir, path);
|
|
265
|
+
await mkdir(dirname(target), { recursive: true });
|
|
266
|
+
await writeFile(target, content, "utf8");
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
var publishCommand = defineCommand({
|
|
271
|
+
name: "publish",
|
|
272
|
+
description: "Copy a publishable stub group into the app (run with no id to list)",
|
|
273
|
+
async handle({ io, args, flags }) {
|
|
274
|
+
const id = args[0];
|
|
275
|
+
if (!id) {
|
|
276
|
+
io.log("Publishable groups:");
|
|
277
|
+
io.table(PUBLISHABLES.map((p) => ({ id: p.id, description: p.description })));
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
const publishable = PUBLISHABLES.find((p) => p.id === id);
|
|
281
|
+
if (!publishable) {
|
|
282
|
+
io.error(`Unknown publishable "${id}". Available: ${PUBLISHABLES.map((p) => p.id).join(", ")}.`);
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
285
|
+
const dir = typeof flags["dir"] === "string" ? flags["dir"] : process.cwd();
|
|
286
|
+
const result = await runPublish(publishable, nodePublishFs(dir), { force: flags["force"] === true });
|
|
287
|
+
for (const path of result.written) io.log(` wrote ${path}`);
|
|
288
|
+
for (const path of result.skipped) io.log(` skipped ${path} (exists \u2014 use --force to overwrite)`);
|
|
289
|
+
if (result.written.length === 0 && result.skipped.length > 0) {
|
|
290
|
+
io.log("Nothing written. Re-run with --force to overwrite existing files.");
|
|
291
|
+
}
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// src/builtins.ts
|
|
11
297
|
var routesCommand = defineCommand({
|
|
12
298
|
name: "routes",
|
|
13
299
|
description: "List the HTTP routes registered by the app",
|
|
@@ -33,7 +319,7 @@ var scheduleListCommand = defineCommand({
|
|
|
33
319
|
}
|
|
34
320
|
});
|
|
35
321
|
function builtinCommands() {
|
|
36
|
-
return [routesCommand, scheduleListCommand];
|
|
322
|
+
return [routesCommand, scheduleListCommand, devCommand, upgradeCommand, publishCommand];
|
|
37
323
|
}
|
|
38
324
|
|
|
39
325
|
// src/io.ts
|
|
@@ -109,7 +395,7 @@ async function runCli(options) {
|
|
|
109
395
|
const { command, args, flags } = parseArgv(options.argv ?? process.argv.slice(2));
|
|
110
396
|
if (app.phase === "created") await app.boot();
|
|
111
397
|
try {
|
|
112
|
-
const registered =
|
|
398
|
+
const registered = ensureMetadata2(app.container).get("commands");
|
|
113
399
|
const commands = [...builtinCommands(), ...registered];
|
|
114
400
|
if (command === void 0 || command === "list") {
|
|
115
401
|
io.log("Available commands:");
|
|
@@ -131,25 +417,39 @@ async function runCli(options) {
|
|
|
131
417
|
}
|
|
132
418
|
|
|
133
419
|
// src/plugin.ts
|
|
134
|
-
import { definePlugin, ensureMetadata as
|
|
420
|
+
import { definePlugin, ensureMetadata as ensureMetadata3 } from "@basaltkit/core";
|
|
135
421
|
function commandsPlugin(commands) {
|
|
136
422
|
return definePlugin({
|
|
137
423
|
name: "basalt:commands",
|
|
138
424
|
register({ container }) {
|
|
139
|
-
const metadata =
|
|
425
|
+
const metadata = ensureMetadata3(container);
|
|
140
426
|
for (const command of commands) metadata.add("commands", command);
|
|
141
427
|
}
|
|
142
428
|
});
|
|
143
429
|
}
|
|
144
430
|
export {
|
|
431
|
+
DEV_ENTRY_CANDIDATES,
|
|
432
|
+
MIGRATIONS,
|
|
433
|
+
PUBLISHABLES,
|
|
145
434
|
builtinCommands,
|
|
146
435
|
commandsPlugin,
|
|
147
436
|
consoleIo,
|
|
148
437
|
defineCommand,
|
|
438
|
+
devCommand,
|
|
439
|
+
devRouteRows,
|
|
149
440
|
memoryIo,
|
|
441
|
+
nodePublishFs,
|
|
442
|
+
nodeUpgradeFs,
|
|
150
443
|
parseArgv,
|
|
444
|
+
publishCommand,
|
|
445
|
+
renameMachizeScope,
|
|
151
446
|
renderTable,
|
|
447
|
+
resolveDevEntry,
|
|
448
|
+
resolveDevRunner,
|
|
152
449
|
routesCommand,
|
|
153
450
|
runCli,
|
|
154
|
-
|
|
451
|
+
runPublish,
|
|
452
|
+
runUpgrade,
|
|
453
|
+
scheduleListCommand,
|
|
454
|
+
upgradeCommand
|
|
155
455
|
};
|
package/package.json
CHANGED