create-kelpie 0.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 +661 -0
- package/README.md +72 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +41 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +172 -0
- package/dist/options.js.map +1 -0
- package/dist/scaffold.d.ts +36 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +106 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +53 -0
- package/src/index.ts +59 -0
- package/src/options.ts +229 -0
- package/src/scaffold.ts +146 -0
- package/templates/README.md +97 -0
- package/templates/docker-compose.yml +26 -0
- package/templates/env +27 -0
- package/templates/gitignore +6 -0
- package/templates/kelpie.config.ts +21 -0
- package/templates/kelpie.ui.config.ts +14 -0
- package/templates/package.json +35 -0
- package/templates/src/server.ts +118 -0
- package/templates/tsconfig.server.json +20 -0
- package/templates/tsconfig.web.json +21 -0
- package/templates/vite.config.ts +60 -0
- package/templates/web/index.html +12 -0
- package/templates/web/main.tsx +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# create-kelpie
|
|
2
|
+
|
|
3
|
+
Scaffolds a self-hosted [Kelpie](https://github.com/velvet-tiger/kelpie) install.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create kelpie@latest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Kelpie is an open-source, agent-native CRM and company brain. It is composed at
|
|
10
|
+
build time rather than shipped as a binary: an **assembly** is a small project
|
|
11
|
+
that lists its modules and boots the runtime. This writes you one.
|
|
12
|
+
|
|
13
|
+
There is nothing to clone. `@kelpie/server` and `@kelpie/ui` arrive as ordinary
|
|
14
|
+
dependencies, and the files this writes are yours to edit and commit.
|
|
15
|
+
|
|
16
|
+
## What it writes
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
package.json @kelpie/server and @kelpie/ui, pinned to this version
|
|
20
|
+
kelpie.config.ts the server module list
|
|
21
|
+
kelpie.ui.config.ts the UI module list
|
|
22
|
+
src/server.ts the entry point: config, modules, migrations, serve
|
|
23
|
+
web/index.html the web entry
|
|
24
|
+
web/main.tsx
|
|
25
|
+
vite.config.ts dev server, proxying /v1 to the API
|
|
26
|
+
tsconfig.server.json
|
|
27
|
+
tsconfig.web.json
|
|
28
|
+
.env with a SECRET_ENCRYPTION_KEY generated for this project
|
|
29
|
+
.gitignore
|
|
30
|
+
README.md how to run it, and what every variable does
|
|
31
|
+
docker-compose.yml Postgres, unless you said no
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
It writes files and stops. It does not run `npm install`, start Postgres, or
|
|
35
|
+
boot anything, so when something fails you are looking at your own terminal
|
|
36
|
+
rather than at output a scaffolder swallowed.
|
|
37
|
+
|
|
38
|
+
## Options
|
|
39
|
+
|
|
40
|
+
It prompts for what it cannot infer. Every answer is also a flag, so it runs
|
|
41
|
+
unattended:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm create kelpie@latest crm -- --yes --database-url postgres://user:pass@db:5432/kelpie --no-docker
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Flag | Does |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `--name <name>` | Package name. Defaults to the directory name |
|
|
50
|
+
| `--database-url <url>` | `postgres://` connection string |
|
|
51
|
+
| `--port <port>` | API port, default 3000 |
|
|
52
|
+
| `--web-port <port>` | Dev server port, default 5173 |
|
|
53
|
+
| `--database-port <port>` | Host port for the bundled Postgres, default 5432 |
|
|
54
|
+
| `--email-from <address>` | Address transactional mail comes from |
|
|
55
|
+
| `--docker`, `--no-docker` | Write a `docker-compose.yml` for Postgres |
|
|
56
|
+
| `--yes` | Take every default, never prompt |
|
|
57
|
+
|
|
58
|
+
Without a terminal to prompt on, `--yes` is required. A scaffolder that quietly
|
|
59
|
+
invents a database URL in CI writes a project that fails at boot, a long way
|
|
60
|
+
from the cause.
|
|
61
|
+
|
|
62
|
+
It refuses a directory that already contains anything.
|
|
63
|
+
|
|
64
|
+
## Versions
|
|
65
|
+
|
|
66
|
+
The generated manifest pins `@kelpie/server` and `@kelpie/ui` at this package's
|
|
67
|
+
own version, and the four release together. `create-kelpie@0.3.0` writes a
|
|
68
|
+
project asking for `@kelpie/server@^0.3.0`.
|
|
69
|
+
|
|
70
|
+
## Licence
|
|
71
|
+
|
|
72
|
+
AGPL-3.0-only.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npm create kelpie` lands here.
|
|
4
|
+
*
|
|
5
|
+
* It writes an assembly and stops. It does not run `npm install`, start
|
|
6
|
+
* Postgres, or boot anything: the generated README says what to do next, and a
|
|
7
|
+
* scaffolder that installs for you is a scaffolder you cannot read the output
|
|
8
|
+
* of when the install fails.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;GAOG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npm create kelpie` lands here.
|
|
4
|
+
*
|
|
5
|
+
* It writes an assembly and stops. It does not run `npm install`, start
|
|
6
|
+
* Postgres, or boot anything: the generated README says what to do next, and a
|
|
7
|
+
* scaffolder that installs for you is a scaffolder you cannot read the output
|
|
8
|
+
* of when the install fails.
|
|
9
|
+
*/
|
|
10
|
+
import { relative } from 'node:path';
|
|
11
|
+
import { USAGE, parseArguments, resolveOptions } from './options.js';
|
|
12
|
+
import { ScaffoldError, scaffold } from './scaffold.js';
|
|
13
|
+
function report(message) {
|
|
14
|
+
process.stdout.write(`${message}\n`);
|
|
15
|
+
}
|
|
16
|
+
async function main() {
|
|
17
|
+
const parsed = parseArguments(process.argv.slice(2));
|
|
18
|
+
if (parsed.help) {
|
|
19
|
+
report(USAGE);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const options = await resolveOptions(parsed, process.stdin.isTTY === true);
|
|
23
|
+
const written = scaffold(options);
|
|
24
|
+
const here = relative(process.cwd(), options.directory) || '.';
|
|
25
|
+
report(`\nWrote ${written.length} files into ${here}/`);
|
|
26
|
+
report('');
|
|
27
|
+
report('Next:');
|
|
28
|
+
report(` cd ${here}`);
|
|
29
|
+
report(' npm install');
|
|
30
|
+
if (options.docker) {
|
|
31
|
+
report(' docker compose up --detach --wait');
|
|
32
|
+
}
|
|
33
|
+
report(' npm run dev');
|
|
34
|
+
report('');
|
|
35
|
+
report(`Then open http://localhost:${options.webPort}/signup to create your account.`);
|
|
36
|
+
report(`${here}/README.md has the rest, including what every variable in .env does.`);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
await main();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof ScaffoldError) {
|
|
43
|
+
process.stderr.write(`${error.message}\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEpC,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACpE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAEvD,SAAS,MAAM,CAAC,OAAe;IAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAA;AACtC,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAEpD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,MAAM,CAAC,KAAK,CAAC,CAAA;QAEb,OAAM;IACR,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC,CAAA;IAC1E,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAA;IACjC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,SAAS,CAAC,IAAI,GAAG,CAAA;IAE9D,MAAM,CAAC,WAAW,OAAO,CAAC,MAAM,eAAe,IAAI,GAAG,CAAC,CAAA;IACvD,MAAM,CAAC,EAAE,CAAC,CAAA;IACV,MAAM,CAAC,OAAO,CAAC,CAAA;IACf,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAA;IACtB,MAAM,CAAC,eAAe,CAAC,CAAA;IAEvB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,CAAC,qCAAqC,CAAC,CAAA;IAC/C,CAAC;IAED,MAAM,CAAC,eAAe,CAAC,CAAA;IACvB,MAAM,CAAC,EAAE,CAAC,CAAA;IACV,MAAM,CAAC,8BAA8B,OAAO,CAAC,OAAO,iCAAiC,CAAC,CAAA;IACtF,MAAM,CAAC,GAAG,IAAI,sEAAsE,CAAC,CAAA;AACvF,CAAC;AAED,IAAI,CAAC;IACH,MAAM,IAAI,EAAE,CAAA;AACd,CAAC;AAAC,OAAO,KAAc,EAAE,CAAC;IACxB,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;QACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,OAAO,IAAI,CAAC,CAAA;QAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,MAAM,KAAK,CAAA;AACb,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns argv and, where it has to, the operator, into `ScaffoldOptions`.
|
|
3
|
+
*
|
|
4
|
+
* Everything non-deterministic lives here: the generated key, the prompts, and
|
|
5
|
+
* reading this package's own version. `scaffold` itself is a pure function of
|
|
6
|
+
* what this produces, which is what makes it testable.
|
|
7
|
+
*/
|
|
8
|
+
import type { ScaffoldOptions } from './scaffold.ts';
|
|
9
|
+
export declare const USAGE = "Scaffold a self-hosted Kelpie assembly.\n\n npm create kelpie [directory] [options]\n\nOptions:\n --name <name> Package name. Defaults to the directory name\n --database-url <url> postgres:// connection string\n --port <port> API port (default 3000)\n --web-port <port> Dev server port (default 5173)\n --database-port <port> Host port for the bundled Postgres (default 5432)\n --email-from <address> Address transactional mail comes from\n --docker, --no-docker Write a docker-compose.yml for Postgres (default yes)\n --yes Take every default; never prompt\n --help This message\n\nWith --yes and no --database-url, the connection string points at the bundled\nPostgres. Without a terminal to prompt on, --yes is required.";
|
|
10
|
+
export interface ParsedArguments {
|
|
11
|
+
readonly directory: string | undefined;
|
|
12
|
+
readonly name: string | undefined;
|
|
13
|
+
readonly databaseUrl: string | undefined;
|
|
14
|
+
readonly emailFrom: string | undefined;
|
|
15
|
+
readonly port: number;
|
|
16
|
+
readonly webPort: number;
|
|
17
|
+
readonly databasePort: number;
|
|
18
|
+
readonly docker: boolean | undefined;
|
|
19
|
+
readonly yes: boolean;
|
|
20
|
+
readonly help: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* npm package names are lowercase, and a directory name often is not. Anything
|
|
24
|
+
* that cannot be salvaged falls back rather than failing: the name is cosmetic
|
|
25
|
+
* in a private package.
|
|
26
|
+
*/
|
|
27
|
+
export declare function toPackageName(directoryName: string): string;
|
|
28
|
+
export declare function parseArguments(argv: readonly string[]): ParsedArguments;
|
|
29
|
+
export declare function generateSecretEncryptionKey(): string;
|
|
30
|
+
export declare function bundledDatabaseUrl(port: number): string;
|
|
31
|
+
/** This package's own version, which is the core version a scaffold pins. */
|
|
32
|
+
export declare function readOwnVersion(): string;
|
|
33
|
+
/**
|
|
34
|
+
* Fills the gaps argv left, asking where there is a terminal to ask on.
|
|
35
|
+
*
|
|
36
|
+
* Without one, `--yes` is required rather than assumed. A scaffolder that
|
|
37
|
+
* silently invents a database URL in CI writes a project that fails at boot,
|
|
38
|
+
* some distance from the cause.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveOptions(parsed: ParsedArguments, interactive: boolean): Promise<ScaffoldOptions>;
|
|
41
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAapD,eAAO,MAAM,KAAK,+yBAgB4C,CAAA;AAE9D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CACvB;AAgBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAQ3D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,eAAe,CAqCvE;AAED,wBAAgB,2BAA2B,IAAI,MAAM,CAEpD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,IAAI,MAAM,CAevC;AAgCD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,CAsC5G"}
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns argv and, where it has to, the operator, into `ScaffoldOptions`.
|
|
3
|
+
*
|
|
4
|
+
* Everything non-deterministic lives here: the generated key, the prompts, and
|
|
5
|
+
* reading this package's own version. `scaffold` itself is a pure function of
|
|
6
|
+
* what this produces, which is what makes it testable.
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { basename, resolve } from 'node:path';
|
|
11
|
+
import { createInterface } from 'node:readline/promises';
|
|
12
|
+
import { parseArgs } from 'node:util';
|
|
13
|
+
import { ScaffoldError } from './scaffold.js';
|
|
14
|
+
const DEFAULT_DIRECTORY = 'kelpie';
|
|
15
|
+
const DEFAULT_PORT = 3000;
|
|
16
|
+
const DEFAULT_WEB_PORT = 5173;
|
|
17
|
+
const DEFAULT_DATABASE_PORT = 5432;
|
|
18
|
+
const DEFAULT_EMAIL_FROM = 'kelpie@example.com';
|
|
19
|
+
/** The service requires 32 bytes; anything shorter is rejected at boot. */
|
|
20
|
+
const SECRET_KEY_BYTES = 32;
|
|
21
|
+
const HIGHEST_PORT = 65535;
|
|
22
|
+
export const USAGE = `Scaffold a self-hosted Kelpie assembly.
|
|
23
|
+
|
|
24
|
+
npm create kelpie [directory] [options]
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--name <name> Package name. Defaults to the directory name
|
|
28
|
+
--database-url <url> postgres:// connection string
|
|
29
|
+
--port <port> API port (default ${DEFAULT_PORT})
|
|
30
|
+
--web-port <port> Dev server port (default ${DEFAULT_WEB_PORT})
|
|
31
|
+
--database-port <port> Host port for the bundled Postgres (default ${DEFAULT_DATABASE_PORT})
|
|
32
|
+
--email-from <address> Address transactional mail comes from
|
|
33
|
+
--docker, --no-docker Write a docker-compose.yml for Postgres (default yes)
|
|
34
|
+
--yes Take every default; never prompt
|
|
35
|
+
--help This message
|
|
36
|
+
|
|
37
|
+
With --yes and no --database-url, the connection string points at the bundled
|
|
38
|
+
Postgres. Without a terminal to prompt on, --yes is required.`;
|
|
39
|
+
function parsePort(name, value, fallback) {
|
|
40
|
+
if (value === undefined) {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
const port = Number(value);
|
|
44
|
+
if (!Number.isInteger(port) || port < 1 || port > HIGHEST_PORT) {
|
|
45
|
+
throw new ScaffoldError(`${name} must be a port number between 1 and ${HIGHEST_PORT}. It is "${value}".`);
|
|
46
|
+
}
|
|
47
|
+
return port;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* npm package names are lowercase, and a directory name often is not. Anything
|
|
51
|
+
* that cannot be salvaged falls back rather than failing: the name is cosmetic
|
|
52
|
+
* in a private package.
|
|
53
|
+
*/
|
|
54
|
+
export function toPackageName(directoryName) {
|
|
55
|
+
const cleaned = directoryName
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
58
|
+
.replace(/^[._-]+/, '')
|
|
59
|
+
.replace(/-+$/, '');
|
|
60
|
+
return cleaned.length > 0 ? cleaned : DEFAULT_DIRECTORY;
|
|
61
|
+
}
|
|
62
|
+
export function parseArguments(argv) {
|
|
63
|
+
const { values, positionals } = parseArgs({
|
|
64
|
+
args: [...argv],
|
|
65
|
+
allowPositionals: true,
|
|
66
|
+
options: {
|
|
67
|
+
name: { type: 'string' },
|
|
68
|
+
'database-url': { type: 'string' },
|
|
69
|
+
'email-from': { type: 'string' },
|
|
70
|
+
port: { type: 'string' },
|
|
71
|
+
'web-port': { type: 'string' },
|
|
72
|
+
'database-port': { type: 'string' },
|
|
73
|
+
docker: { type: 'boolean' },
|
|
74
|
+
// parseArgs has no native --no-x, so the negation is its own flag.
|
|
75
|
+
'no-docker': { type: 'boolean' },
|
|
76
|
+
yes: { type: 'boolean', short: 'y' },
|
|
77
|
+
help: { type: 'boolean', short: 'h' },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
if (values.docker === true && values['no-docker'] === true) {
|
|
81
|
+
throw new ScaffoldError('--docker and --no-docker contradict each other.');
|
|
82
|
+
}
|
|
83
|
+
const docker = values['no-docker'] === true ? false : values.docker === true ? true : undefined;
|
|
84
|
+
return {
|
|
85
|
+
directory: positionals[0],
|
|
86
|
+
name: values.name,
|
|
87
|
+
databaseUrl: values['database-url'],
|
|
88
|
+
emailFrom: values['email-from'],
|
|
89
|
+
port: parsePort('--port', values.port, DEFAULT_PORT),
|
|
90
|
+
webPort: parsePort('--web-port', values['web-port'], DEFAULT_WEB_PORT),
|
|
91
|
+
databasePort: parsePort('--database-port', values['database-port'], DEFAULT_DATABASE_PORT),
|
|
92
|
+
docker,
|
|
93
|
+
yes: values.yes === true,
|
|
94
|
+
help: values.help === true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function generateSecretEncryptionKey() {
|
|
98
|
+
return randomBytes(SECRET_KEY_BYTES).toString('base64');
|
|
99
|
+
}
|
|
100
|
+
export function bundledDatabaseUrl(port) {
|
|
101
|
+
return `postgres://kelpie:kelpie@localhost:${port}/kelpie`;
|
|
102
|
+
}
|
|
103
|
+
/** This package's own version, which is the core version a scaffold pins. */
|
|
104
|
+
export function readOwnVersion() {
|
|
105
|
+
const manifestUrl = new URL('../package.json', import.meta.url);
|
|
106
|
+
const parsed = JSON.parse(readFileSync(manifestUrl, 'utf8'));
|
|
107
|
+
if (typeof parsed !== 'object' || parsed === null || !('version' in parsed)) {
|
|
108
|
+
throw new ScaffoldError('create-kelpie cannot read its own version.');
|
|
109
|
+
}
|
|
110
|
+
const { version } = parsed;
|
|
111
|
+
if (typeof version !== 'string') {
|
|
112
|
+
throw new ScaffoldError('create-kelpie has a non-string version in its manifest.');
|
|
113
|
+
}
|
|
114
|
+
return version;
|
|
115
|
+
}
|
|
116
|
+
function createPrompter() {
|
|
117
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
118
|
+
return {
|
|
119
|
+
async ask(question, fallback) {
|
|
120
|
+
const answer = (await rl.question(`${question} (${fallback}) `)).trim();
|
|
121
|
+
return answer.length > 0 ? answer : fallback;
|
|
122
|
+
},
|
|
123
|
+
async confirm(question, fallback) {
|
|
124
|
+
const answer = (await rl.question(`${question} (${fallback ? 'Y/n' : 'y/N'}) `)).trim().toLowerCase();
|
|
125
|
+
if (answer.length === 0) {
|
|
126
|
+
return fallback;
|
|
127
|
+
}
|
|
128
|
+
return answer.startsWith('y');
|
|
129
|
+
},
|
|
130
|
+
close() {
|
|
131
|
+
rl.close();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Fills the gaps argv left, asking where there is a terminal to ask on.
|
|
137
|
+
*
|
|
138
|
+
* Without one, `--yes` is required rather than assumed. A scaffolder that
|
|
139
|
+
* silently invents a database URL in CI writes a project that fails at boot,
|
|
140
|
+
* some distance from the cause.
|
|
141
|
+
*/
|
|
142
|
+
export async function resolveOptions(parsed, interactive) {
|
|
143
|
+
if (!interactive && !parsed.yes) {
|
|
144
|
+
throw new ScaffoldError('There is no terminal to prompt on. Pass --yes to take the defaults, and --database-url to point at your database.');
|
|
145
|
+
}
|
|
146
|
+
const prompter = interactive && !parsed.yes ? createPrompter() : undefined;
|
|
147
|
+
try {
|
|
148
|
+
const directoryName = parsed.directory ?? (await prompter?.ask('Directory', DEFAULT_DIRECTORY)) ?? DEFAULT_DIRECTORY;
|
|
149
|
+
const directory = resolve(process.cwd(), directoryName);
|
|
150
|
+
const projectName = parsed.name ?? toPackageName(basename(directory));
|
|
151
|
+
const docker = parsed.docker ?? (await prompter?.confirm('Write a docker-compose.yml for Postgres?', true)) ?? true;
|
|
152
|
+
const defaultDatabaseUrl = bundledDatabaseUrl(parsed.databasePort);
|
|
153
|
+
const databaseUrl = parsed.databaseUrl ?? (await prompter?.ask('DATABASE_URL', defaultDatabaseUrl)) ?? defaultDatabaseUrl;
|
|
154
|
+
const emailFrom = parsed.emailFrom ?? (await prompter?.ask('Send mail from', DEFAULT_EMAIL_FROM)) ?? DEFAULT_EMAIL_FROM;
|
|
155
|
+
return {
|
|
156
|
+
directory,
|
|
157
|
+
projectName,
|
|
158
|
+
databaseUrl,
|
|
159
|
+
emailFrom,
|
|
160
|
+
port: parsed.port,
|
|
161
|
+
webPort: parsed.webPort,
|
|
162
|
+
databasePort: parsed.databasePort,
|
|
163
|
+
docker,
|
|
164
|
+
secretEncryptionKey: generateSecretEncryptionKey(),
|
|
165
|
+
coreVersion: readOwnVersion(),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
prompter?.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes a Kelpie assembly into a directory.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is deterministic: the caller supplies the generated secret and
|
|
5
|
+
* every other value, so a test can assert on exact output. `resolveOptions` in
|
|
6
|
+
* `options.ts` is where the non-deterministic parts live.
|
|
7
|
+
*/
|
|
8
|
+
export interface ScaffoldOptions {
|
|
9
|
+
/** Absolute path of the directory to write into. Created if missing. */
|
|
10
|
+
readonly directory: string;
|
|
11
|
+
readonly projectName: string;
|
|
12
|
+
readonly databaseUrl: string;
|
|
13
|
+
readonly emailFrom: string;
|
|
14
|
+
readonly port: number;
|
|
15
|
+
readonly webPort: number;
|
|
16
|
+
/** The host port `docker-compose.yml` publishes Postgres on. Ignored without `docker`. */
|
|
17
|
+
readonly databasePort: number;
|
|
18
|
+
readonly docker: boolean;
|
|
19
|
+
/** 32 bytes of base64. Generated per project by `resolveOptions`. */
|
|
20
|
+
readonly secretEncryptionKey: string;
|
|
21
|
+
/** The `@kelpie/*` version range the generated manifest asks for. */
|
|
22
|
+
readonly coreVersion: string;
|
|
23
|
+
}
|
|
24
|
+
/** A precondition failed. Reported as a message rather than a stack. */
|
|
25
|
+
export declare class ScaffoldError extends Error {
|
|
26
|
+
constructor(message: string);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Refuses a directory that already holds anything, so a mistyped path cannot
|
|
30
|
+
* overwrite a project. Dotfiles count; a directory with a `.git` in it is
|
|
31
|
+
* someone's repository.
|
|
32
|
+
*/
|
|
33
|
+
export declare function assertWritable(directory: string): void;
|
|
34
|
+
/** Writes the assembly. Returns the output paths, relative to the directory, in write order. */
|
|
35
|
+
export declare function scaffold(options: ScaffoldOptions): readonly string[];
|
|
36
|
+
//# sourceMappingURL=scaffold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,qEAAqE;IACrE,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAA;IACpC,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,wEAAwE;AACxE,qBAAa,aAAc,SAAQ,KAAK;IACtC,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AA2DD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAkBtD;AAED,gGAAgG;AAChG,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,SAAS,MAAM,EAAE,CAsBpE"}
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes a Kelpie assembly into a directory.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is deterministic: the caller supplies the generated secret and
|
|
5
|
+
* every other value, so a test can assert on exact output. `resolveOptions` in
|
|
6
|
+
* `options.ts` is where the non-deterministic parts live.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, readFileSync, readdirSync, existsSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
/** Resolves the same from `src/` and from `dist/`; both sit one level down. */
|
|
12
|
+
const templateDirectory = fileURLToPath(new URL('../templates/', import.meta.url));
|
|
13
|
+
/** A precondition failed. Reported as a message rather than a stack. */
|
|
14
|
+
export class ScaffoldError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ScaffoldError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Template path to output path.
|
|
22
|
+
*
|
|
23
|
+
* `env` and `gitignore` are renamed rather than stored under their real names
|
|
24
|
+
* because npm silently drops a file called `.gitignore` from a published
|
|
25
|
+
* tarball, and a stray `.env` in the package directory is a trap regardless.
|
|
26
|
+
*/
|
|
27
|
+
const TEMPLATE_FILES = new Map([
|
|
28
|
+
['package.json', 'package.json'],
|
|
29
|
+
['kelpie.config.ts', 'kelpie.config.ts'],
|
|
30
|
+
['kelpie.ui.config.ts', 'kelpie.ui.config.ts'],
|
|
31
|
+
['src/server.ts', 'src/server.ts'],
|
|
32
|
+
['web/index.html', 'web/index.html'],
|
|
33
|
+
['web/main.tsx', 'web/main.tsx'],
|
|
34
|
+
['vite.config.ts', 'vite.config.ts'],
|
|
35
|
+
['tsconfig.server.json', 'tsconfig.server.json'],
|
|
36
|
+
['tsconfig.web.json', 'tsconfig.web.json'],
|
|
37
|
+
['README.md', 'README.md'],
|
|
38
|
+
['env', '.env'],
|
|
39
|
+
['gitignore', '.gitignore'],
|
|
40
|
+
]);
|
|
41
|
+
/** Written only when the project takes the bundled Postgres. */
|
|
42
|
+
const DOCKER_TEMPLATE = 'docker-compose.yml';
|
|
43
|
+
/** Anything left matching this after substitution is a token nobody filled in. */
|
|
44
|
+
const UNRESOLVED_TOKEN = /__[A-Z][A-Z0-9_]*__/;
|
|
45
|
+
function tokensFor(options) {
|
|
46
|
+
return new Map([
|
|
47
|
+
['__PROJECT_NAME__', options.projectName],
|
|
48
|
+
['__CORE_VERSION__', options.coreVersion],
|
|
49
|
+
['__DATABASE_URL__', options.databaseUrl],
|
|
50
|
+
['__DATABASE_PORT__', String(options.databasePort)],
|
|
51
|
+
['__EMAIL_FROM__', options.emailFrom],
|
|
52
|
+
['__PORT__', String(options.port)],
|
|
53
|
+
['__WEB_PORT__', String(options.webPort)],
|
|
54
|
+
['__SECRET_ENCRYPTION_KEY__', options.secretEncryptionKey],
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
function render(template, tokens, source) {
|
|
58
|
+
let rendered = template;
|
|
59
|
+
for (const [token, value] of tokens) {
|
|
60
|
+
rendered = rendered.replaceAll(token, value);
|
|
61
|
+
}
|
|
62
|
+
const leftover = UNRESOLVED_TOKEN.exec(rendered);
|
|
63
|
+
if (leftover !== null) {
|
|
64
|
+
throw new ScaffoldError(`The template ${source} still contains ${leftover[0]} after substitution.`);
|
|
65
|
+
}
|
|
66
|
+
return rendered;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Refuses a directory that already holds anything, so a mistyped path cannot
|
|
70
|
+
* overwrite a project. Dotfiles count; a directory with a `.git` in it is
|
|
71
|
+
* someone's repository.
|
|
72
|
+
*/
|
|
73
|
+
export function assertWritable(directory) {
|
|
74
|
+
if (!existsSync(directory)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const entries = readdirSync(directory);
|
|
78
|
+
if (entries.length > 0) {
|
|
79
|
+
throw new ScaffoldError(`${directory} is not empty. It holds ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'}, ` +
|
|
80
|
+
'including ' +
|
|
81
|
+
entries
|
|
82
|
+
.slice(0, 3)
|
|
83
|
+
.map((entry) => `"${entry}"`)
|
|
84
|
+
.join(', ') +
|
|
85
|
+
'. Pick an empty directory, or a path that does not exist yet.');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Writes the assembly. Returns the output paths, relative to the directory, in write order. */
|
|
89
|
+
export function scaffold(options) {
|
|
90
|
+
assertWritable(options.directory);
|
|
91
|
+
const tokens = tokensFor(options);
|
|
92
|
+
const files = new Map(TEMPLATE_FILES);
|
|
93
|
+
if (options.docker) {
|
|
94
|
+
files.set(DOCKER_TEMPLATE, DOCKER_TEMPLATE);
|
|
95
|
+
}
|
|
96
|
+
const written = [];
|
|
97
|
+
for (const [source, target] of files) {
|
|
98
|
+
const template = readFileSync(join(templateDirectory, source), 'utf8');
|
|
99
|
+
const destination = join(options.directory, target);
|
|
100
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
101
|
+
writeFileSync(destination, render(template, tokens, source));
|
|
102
|
+
written.push(target);
|
|
103
|
+
}
|
|
104
|
+
return written;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-kelpie",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Scaffold a self-hosted Kelpie assembly. Run it with npm create kelpie.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kelpie",
|
|
7
|
+
"crm",
|
|
8
|
+
"scaffold",
|
|
9
|
+
"create",
|
|
10
|
+
"self-host"
|
|
11
|
+
],
|
|
12
|
+
"license": "AGPL-3.0-only",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/velvet-tiger/kelpie.git",
|
|
16
|
+
"directory": "packages/create-kelpie"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/velvet-tiger/kelpie/tree/main/packages/create-kelpie#readme",
|
|
19
|
+
"bugs": "https://github.com/velvet-tiger/kelpie/issues",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"create-kelpie": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"src",
|
|
33
|
+
"!src/**/*.test.ts",
|
|
34
|
+
"templates"
|
|
35
|
+
],
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"kelpie-source": "./src/index.ts",
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"default": "./dist/index.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc --project tsconfig.build.json",
|
|
45
|
+
"typecheck": "tsc --project tsconfig.json",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^26.1.2",
|
|
51
|
+
"vitest": "^4.1.10"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `npm create kelpie` lands here.
|
|
5
|
+
*
|
|
6
|
+
* It writes an assembly and stops. It does not run `npm install`, start
|
|
7
|
+
* Postgres, or boot anything: the generated README says what to do next, and a
|
|
8
|
+
* scaffolder that installs for you is a scaffolder you cannot read the output
|
|
9
|
+
* of when the install fails.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { relative } from 'node:path'
|
|
13
|
+
|
|
14
|
+
import { USAGE, parseArguments, resolveOptions } from './options.ts'
|
|
15
|
+
import { ScaffoldError, scaffold } from './scaffold.ts'
|
|
16
|
+
|
|
17
|
+
function report(message: string): void {
|
|
18
|
+
process.stdout.write(`${message}\n`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function main(): Promise<void> {
|
|
22
|
+
const parsed = parseArguments(process.argv.slice(2))
|
|
23
|
+
|
|
24
|
+
if (parsed.help) {
|
|
25
|
+
report(USAGE)
|
|
26
|
+
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const options = await resolveOptions(parsed, process.stdin.isTTY === true)
|
|
31
|
+
const written = scaffold(options)
|
|
32
|
+
const here = relative(process.cwd(), options.directory) || '.'
|
|
33
|
+
|
|
34
|
+
report(`\nWrote ${written.length} files into ${here}/`)
|
|
35
|
+
report('')
|
|
36
|
+
report('Next:')
|
|
37
|
+
report(` cd ${here}`)
|
|
38
|
+
report(' npm install')
|
|
39
|
+
|
|
40
|
+
if (options.docker) {
|
|
41
|
+
report(' docker compose up --detach --wait')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
report(' npm run dev')
|
|
45
|
+
report('')
|
|
46
|
+
report(`Then open http://localhost:${options.webPort}/signup to create your account.`)
|
|
47
|
+
report(`${here}/README.md has the rest, including what every variable in .env does.`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await main()
|
|
52
|
+
} catch (error: unknown) {
|
|
53
|
+
if (error instanceof ScaffoldError) {
|
|
54
|
+
process.stderr.write(`${error.message}\n`)
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw error
|
|
59
|
+
}
|