@basaltkit/cli 1.2.0 → 1.2.2

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 CHANGED
@@ -1,3 +1,9 @@
1
+ <p align="center">
2
+ <a href="https://basaltkit-docs.pages.dev">
3
+ <img src="https://basaltkit-docs.pages.dev/social-card.png" alt="Basalt" width="440">
4
+ </a>
5
+ </p>
6
+
1
7
  # @basaltkit/cli
2
8
 
3
9
  Terminal command framework (the `basalt` command) for Basalt applications: define your own commands, run them against the already-booted application, and inspect HTTP routes and scheduled tasks. You need it when you want to give your application its own "command line" — for example `basalt routes` or `basalt db:seed`.
@@ -203,6 +209,8 @@ A Basalt plugin that registers the commands in the `'commands'` metadata bucket,
203
209
 
204
210
  A minimal argument parser: the first "loose" word is the command; `--key=value` and `--flag` become flags. Returns `{ command: string | undefined, args: string[], flags: Record<string, string | boolean> }`.
205
211
 
212
+ A bare `--no-<name>` is the conventional **negation**: it yields `flags['<name>'] === false`, not `flags['no-<name>'] === true`. That is what makes documented opt-outs such as `basalt dev --no-routes` take effect — commands test `flags['routes'] !== false`. Only the bare form negates: `--no-cache=x` still parses as the literal key `no-cache`.
213
+
206
214
  ### `consoleIo(): CommandIo`
207
215
 
208
216
  A `CommandIo` implementation that writes to the console (`console.log` / `console.error`; `table` renders via `renderTable`). It's the default for `runCli`.
@@ -0,0 +1,16 @@
1
+ import { type CommandDefinition } from './command.js';
2
+ /** Route entries written by HTTP adapters into the 'http:routes' bucket. */
3
+ export interface RouteMetadata {
4
+ method: string;
5
+ url: string;
6
+ [key: string]: unknown;
7
+ }
8
+ /** Schedule entries written by the scheduler into the 'schedule:entries' bucket. */
9
+ export interface ScheduleMetadata {
10
+ name: string;
11
+ cron: string;
12
+ timezone: string;
13
+ }
14
+ export declare const routesCommand: CommandDefinition;
15
+ export declare const scheduleListCommand: CommandDefinition;
16
+ export declare function builtinCommands(): CommandDefinition[];
@@ -0,0 +1,36 @@
1
+ import { METADATA } from '@basaltkit/core';
2
+ import { defineCommand } from './command.js';
3
+ import { devCommand } from './dev.js';
4
+ import { upgradeCommand } from './upgrade.js';
5
+ import { publishCommand } from './publish.js';
6
+ export const routesCommand = defineCommand({
7
+ name: 'routes',
8
+ description: 'List the HTTP routes registered by the app',
9
+ handle({ container, io }) {
10
+ const routes = container.has(METADATA)
11
+ ? container.get(METADATA).get('http:routes')
12
+ : [];
13
+ if (routes.length === 0) {
14
+ io.log('No routes registered.');
15
+ return;
16
+ }
17
+ io.table(routes.map(({ method, url }) => ({ method, url })));
18
+ },
19
+ });
20
+ export const scheduleListCommand = defineCommand({
21
+ name: 'schedule:list',
22
+ description: 'List scheduled tasks and their cron expressions',
23
+ handle({ container, io }) {
24
+ const entries = container.has(METADATA)
25
+ ? container.get(METADATA).get('schedule:entries')
26
+ : [];
27
+ if (entries.length === 0) {
28
+ io.log('No scheduled tasks.');
29
+ return;
30
+ }
31
+ io.table(entries.map(({ name, cron, timezone }) => ({ name, cron, timezone })));
32
+ },
33
+ });
34
+ export function builtinCommands() {
35
+ return [routesCommand, scheduleListCommand, devCommand, upgradeCommand, publishCommand];
36
+ }
@@ -0,0 +1,25 @@
1
+ import type { Container, BasaltApp } from '@basaltkit/core';
2
+ /** Output surface — swappable in tests to capture what a command prints. */
3
+ export interface CommandIo {
4
+ log(message: string): void;
5
+ error(message: string): void;
6
+ table(rows: Record<string, unknown>[]): void;
7
+ /** Ask a yes/no question. Resolves true only on an explicit yes. */
8
+ confirm(question: string): Promise<boolean>;
9
+ }
10
+ export interface CommandContext {
11
+ app: BasaltApp;
12
+ container: Container;
13
+ io: CommandIo;
14
+ /** Positional arguments after the command name. */
15
+ args: string[];
16
+ /** Parsed flags: `--key=value` → string, `--flag` → true. */
17
+ flags: Record<string, string | boolean>;
18
+ }
19
+ export interface CommandDefinition {
20
+ /** Command name, by convention `noun:verb` (e.g. `tenant:migrate`). */
21
+ name: string;
22
+ description?: string;
23
+ handle(context: CommandContext): void | number | Promise<void | number>;
24
+ }
25
+ export declare function defineCommand(command: CommandDefinition): CommandDefinition;
@@ -0,0 +1,3 @@
1
+ export function defineCommand(command) {
2
+ return command;
3
+ }
package/dist/dev.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { type CommandDefinition } from './command.js';
2
+ /** Default entry files probed in order when `--entry` is not given. */
3
+ export declare const DEV_ENTRY_CANDIDATES: string[];
4
+ /**
5
+ * Picks the first candidate that exists. Pure — `exists` is injected so the
6
+ * resolution is testable without touching disk.
7
+ */
8
+ export declare function resolveDevEntry(candidates: string[], exists: (path: string) => boolean): string | undefined;
9
+ export interface DevRunner {
10
+ command: string;
11
+ args: string[];
12
+ }
13
+ /**
14
+ * Chooses how to run the entry with watch/restart, delegating the actual
15
+ * watching to the runtime (no bespoke watcher). Pure and testable:
16
+ *
17
+ * - `.ts` with tsx available → `tsx watch <entry>`
18
+ * - `.ts` without tsx → `node --watch --experimental-strip-types <entry>` (Node ≥ 22.6)
19
+ * - `.js` → `node --watch <entry>`
20
+ */
21
+ export declare function resolveDevRunner(entry: string, options?: {
22
+ tsx?: boolean;
23
+ }): DevRunner;
24
+ /** A route as published into the `http:routes` metadata bucket by an adapter. */
25
+ export interface DevRoute {
26
+ method: string;
27
+ url: string;
28
+ meta?: Record<string, unknown>;
29
+ }
30
+ /**
31
+ * Formats the registered routes into printable rows (method, url, and a compact
32
+ * flags column for auth/rate-limit) — sorted by url then method. Pure: this is
33
+ * what `basalt dev` prints on boot, and `pnpm dev` (plain tsx watch) can't.
34
+ */
35
+ export declare function devRouteRows(routes: DevRoute[]): {
36
+ method: string;
37
+ url: string;
38
+ flags: string;
39
+ }[];
40
+ /**
41
+ * `basalt dev [--entry=<file>] [--worker] [--queue=<name>] [--no-routes]`
42
+ *
43
+ * A richer dev loop than a bare `tsx watch`: prints the app's **route table** on
44
+ * boot (the app is already booted by the CLI runner, so routes are known), then
45
+ * runs the server with watch/restart — and, with `--worker`, also starts a
46
+ * watched queue worker alongside it (server + worker in one command, the real
47
+ * producer/worker topology).
48
+ */
49
+ export declare const devCommand: CommandDefinition;
package/dist/dev.js ADDED
@@ -0,0 +1,126 @@
1
+ import { ensureMetadata } from '@basaltkit/core';
2
+ import { defineCommand } from './command.js';
3
+ /** Default entry files probed in order when `--entry` is not given. */
4
+ export const DEV_ENTRY_CANDIDATES = ['src/main.ts', 'src/server.ts', 'src/index.ts', 'src/main.js', 'src/index.js'];
5
+ /**
6
+ * Picks the first candidate that exists. Pure — `exists` is injected so the
7
+ * resolution is testable without touching disk.
8
+ */
9
+ export function resolveDevEntry(candidates, exists) {
10
+ return candidates.find(exists);
11
+ }
12
+ /**
13
+ * Chooses how to run the entry with watch/restart, delegating the actual
14
+ * watching to the runtime (no bespoke watcher). Pure and testable:
15
+ *
16
+ * - `.ts` with tsx available → `tsx watch <entry>`
17
+ * - `.ts` without tsx → `node --watch --experimental-strip-types <entry>` (Node ≥ 22.6)
18
+ * - `.js` → `node --watch <entry>`
19
+ */
20
+ export function resolveDevRunner(entry, options = {}) {
21
+ const isTs = /\.tsx?$/.test(entry);
22
+ if (isTs && options.tsx)
23
+ return { command: 'tsx', args: ['watch', entry] };
24
+ if (isTs)
25
+ return { command: 'node', args: ['--watch', '--experimental-strip-types', entry] };
26
+ return { command: 'node', args: ['--watch', entry] };
27
+ }
28
+ /**
29
+ * Formats the registered routes into printable rows (method, url, and a compact
30
+ * flags column for auth/rate-limit) — sorted by url then method. Pure: this is
31
+ * what `basalt dev` prints on boot, and `pnpm dev` (plain tsx watch) can't.
32
+ */
33
+ export function devRouteRows(routes) {
34
+ return [...routes]
35
+ .sort((a, b) => a.url.localeCompare(b.url) || a.method.localeCompare(b.method))
36
+ .map((r) => {
37
+ const flags = [];
38
+ if (r.meta?.['auth'] === true)
39
+ flags.push('auth');
40
+ if (r.meta?.['rateLimit'])
41
+ flags.push('rate-limit');
42
+ const tags = r.meta?.['tags'];
43
+ if (Array.isArray(tags) && tags.length)
44
+ flags.push(...tags.map(String));
45
+ return { method: r.method.toUpperCase(), url: r.url, flags: flags.join(', ') };
46
+ });
47
+ }
48
+ /**
49
+ * `basalt dev [--entry=<file>] [--worker] [--queue=<name>] [--no-routes]`
50
+ *
51
+ * A richer dev loop than a bare `tsx watch`: prints the app's **route table** on
52
+ * boot (the app is already booted by the CLI runner, so routes are known), then
53
+ * runs the server with watch/restart — and, with `--worker`, also starts a
54
+ * watched queue worker alongside it (server + worker in one command, the real
55
+ * producer/worker topology).
56
+ */
57
+ export const devCommand = defineCommand({
58
+ name: 'dev',
59
+ description: 'Run the app with watch + restart, print the route table, and (--worker) an embedded queue worker',
60
+ async handle({ container, io, flags }) {
61
+ const { existsSync } = await import('node:fs');
62
+ const entry = typeof flags['entry'] === 'string'
63
+ ? flags['entry']
64
+ : resolveDevEntry(DEV_ENTRY_CANDIDATES, existsSync);
65
+ if (!entry) {
66
+ io.error(`No entry file found. Looked for ${DEV_ENTRY_CANDIDATES.join(', ')}. Pass --entry=<file>.`);
67
+ return 1;
68
+ }
69
+ // The route table — the app was already booted by runCli, so `http:routes`
70
+ // is populated by whichever adapter is in use. Adapter-agnostic (metadata only).
71
+ if (flags['routes'] !== false) {
72
+ const routes = ensureMetadata(container).get('http:routes');
73
+ if (routes.length > 0) {
74
+ io.log(`Routes (${routes.length}):`);
75
+ io.table(devRouteRows(routes));
76
+ }
77
+ }
78
+ const tsx = await canResolve('tsx');
79
+ const server = resolveDevRunner(entry, { tsx });
80
+ const runners = [{ label: 'server', runner: server }];
81
+ // Embedded worker: a watched `queue:work` on the same CLI bin, so jobs
82
+ // process in dev without a second terminal. Separate process (correct
83
+ // producer/worker split) — each restarts independently on file change.
84
+ if (flags['worker'] === true || flags['w'] === true) {
85
+ const bin = process.argv[1] ?? entry;
86
+ const workerBase = resolveDevRunner(bin, { tsx });
87
+ const queue = typeof flags['queue'] === 'string' ? [`--queue=${flags['queue']}`] : [];
88
+ runners.push({ label: 'worker', runner: { command: workerBase.command, args: [...workerBase.args, 'queue:work', ...queue] } });
89
+ }
90
+ for (const { label, runner } of runners) {
91
+ io.log(`▶ ${label}: ${runner.command} ${runner.args.join(' ')}`);
92
+ }
93
+ const { spawn } = await import('node:child_process');
94
+ return await new Promise((resolve) => {
95
+ const children = runners.map(({ runner }) => spawn(runner.command, runner.args, { stdio: 'inherit' }));
96
+ const stop = () => children.forEach((c) => c.kill('SIGINT'));
97
+ process.once('SIGINT', stop);
98
+ let done = false;
99
+ const finish = (code) => {
100
+ if (done)
101
+ return;
102
+ done = true;
103
+ process.removeListener('SIGINT', stop);
104
+ children.forEach((c) => c.kill('SIGINT'));
105
+ resolve(code);
106
+ };
107
+ // The server is primary — when it exits, tear everything down.
108
+ children[0]?.on('exit', (code) => finish(code ?? 0));
109
+ children.forEach((child, i) => child.on('error', (error) => {
110
+ io.error(`Failed to start ${runners[i]?.label}: ${error.message}`);
111
+ finish(1);
112
+ }));
113
+ });
114
+ },
115
+ });
116
+ /** True if a module specifier resolves from the app's node_modules. */
117
+ async function canResolve(specifier) {
118
+ try {
119
+ const { createRequire } = await import('node:module');
120
+ createRequire(`${process.cwd()}/`).resolve(specifier);
121
+ return true;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
package/dist/index.d.ts CHANGED
@@ -1,217 +1,9 @@
1
- import * as _basaltkit_core from '@basaltkit/core';
2
- import { BasaltApp, Container } from '@basaltkit/core';
3
-
4
- /** Output surface — swappable in tests to capture what a command prints. */
5
- interface CommandIo {
6
- log(message: string): void;
7
- error(message: string): void;
8
- table(rows: Record<string, unknown>[]): void;
9
- /** Ask a yes/no question. Resolves true only on an explicit yes. */
10
- confirm(question: string): Promise<boolean>;
11
- }
12
- interface CommandContext {
13
- app: BasaltApp;
14
- container: Container;
15
- io: CommandIo;
16
- /** Positional arguments after the command name. */
17
- args: string[];
18
- /** Parsed flags: `--key=value` → string, `--flag` → true. */
19
- flags: Record<string, string | boolean>;
20
- }
21
- interface CommandDefinition {
22
- /** Command name, by convention `noun:verb` (e.g. `tenant:migrate`). */
23
- name: string;
24
- description?: string;
25
- handle(context: CommandContext): void | number | Promise<void | number>;
26
- }
27
- declare function defineCommand(command: CommandDefinition): CommandDefinition;
28
-
29
- interface RunCliOptions {
30
- app: BasaltApp;
31
- /** Defaults to process.argv.slice(2). */
32
- argv?: string[];
33
- /** Defaults to console output — swap in tests. */
34
- io?: CommandIo;
35
- }
36
- /**
37
- * Boots the app, resolves the command (built-ins + everything registered in
38
- * the 'commands' metadata bucket), runs it and shuts the app down.
39
- * Returns the process exit code instead of calling process.exit — the bin
40
- * wrapper decides what to do with it.
41
- */
42
- declare function runCli(options: RunCliOptions): Promise<number>;
43
-
44
- /** Registers app-defined commands into the 'commands' metadata bucket. */
45
- declare function commandsPlugin(commands: CommandDefinition[]): _basaltkit_core.BasaltPlugin<unknown>;
46
-
47
- /** Renders rows as an aligned text table (no external dependencies). */
48
- declare function renderTable(rows: Record<string, unknown>[]): string;
49
- declare function consoleIo(): CommandIo;
50
- /**
51
- * In-memory IO for tests. `answers` feeds `confirm()` in order (defaults to yes
52
- * once the queue is drained), so a test can drive an interactive command.
53
- */
54
- declare function memoryIo(options?: {
55
- answers?: boolean[];
56
- }): CommandIo & {
57
- lines: string[];
58
- errors: string[];
59
- };
60
-
61
- interface ParsedArgv {
62
- command: string | undefined;
63
- args: string[];
64
- flags: Record<string, string | boolean>;
65
- }
66
- /** Minimal argv parser: first bare word is the command; `--key=value` and `--flag` become flags. */
67
- declare function parseArgv(argv: string[]): ParsedArgv;
68
-
69
- /** Route entries written by HTTP adapters into the 'http:routes' bucket. */
70
- interface RouteMetadata {
71
- method: string;
72
- url: string;
73
- [key: string]: unknown;
74
- }
75
- /** Schedule entries written by the scheduler into the 'schedule:entries' bucket. */
76
- interface ScheduleMetadata {
77
- name: string;
78
- cron: string;
79
- timezone: string;
80
- }
81
- declare const routesCommand: CommandDefinition;
82
- declare const scheduleListCommand: CommandDefinition;
83
- declare function builtinCommands(): CommandDefinition[];
84
-
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 };
1
+ export { defineCommand, type CommandDefinition, type CommandContext, type CommandIo } from './command.js';
2
+ export { runCli, type RunCliOptions } from './runner.js';
3
+ export { commandsPlugin } from './plugin.js';
4
+ export { consoleIo, memoryIo, renderTable } from './io.js';
5
+ export { parseArgv, type ParsedArgv } from './parse.js';
6
+ export { builtinCommands, routesCommand, scheduleListCommand, type RouteMetadata, type ScheduleMetadata, } from './builtins.js';
7
+ export { devCommand, resolveDevEntry, resolveDevRunner, devRouteRows, DEV_ENTRY_CANDIDATES, type DevRunner, type DevRoute, } from './dev.js';
8
+ export { upgradeCommand, runUpgrade, MIGRATIONS, renameMachizeScope, nodeUpgradeFs, type Migration, type Edit, type UpgradeFs, type UpgradeReport, } from './upgrade.js';
9
+ export { publishCommand, runPublish, PUBLISHABLES, nodePublishFs, type Publishable, type PublishableFile, type PublishFs, type PublishResult, } from './publish.js';