@cat-factory/cli 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/args.d.ts +40 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +153 -0
- package/dist/args.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +42 -0
- package/dist/bin.js.map +1 -0
- package/dist/bootstrap.d.ts +23 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +171 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/env.d.ts +38 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +150 -0
- package/dist/env.js.map +1 -0
- package/dist/gitignore.d.ts +15 -0
- package/dist/gitignore.d.ts.map +1 -0
- package/dist/gitignore.js +46 -0
- package/dist/gitignore.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +25 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +74 -0
- package/dist/io.js.map +1 -0
- package/dist/plan.d.ts +29 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +144 -0
- package/dist/plan.js.map +1 -0
- package/dist/secrets.d.ts +25 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +9 -0
- package/dist/secrets.js.map +1 -0
- package/dist/slug.d.ts +3 -0
- package/dist/slug.d.ts.map +1 -0
- package/dist/slug.js +19 -0
- package/dist/slug.js.map +1 -0
- package/dist/templates.d.ts +29 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +189 -0
- package/dist/templates.js.map +1 -0
- package/dist/vcs.d.ts +20 -0
- package/dist/vcs.d.ts.map +1 -0
- package/dist/vcs.js +52 -0
- package/dist/vcs.js.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Savin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# @cat-factory/cli
|
|
2
|
+
|
|
3
|
+
The bootstrap CLI for [cat-factory](https://github.com/kibertoad/cat-factory) — the Agent
|
|
4
|
+
Architecture Board. One command scaffolds a **local-mode deployment** you can run on your own
|
|
5
|
+
machine: a Node/local backend (`@cat-factory/local-server`) and the frontend SPA
|
|
6
|
+
(`@cat-factory/app`), mirroring the [`deploy/local`](../../../deploy/local) and
|
|
7
|
+
[`deploy/frontend`](../../../deploy/frontend) example deployments in this repo — but depending on
|
|
8
|
+
the **published** libraries, so the generated project stands alone outside the monorepo.
|
|
9
|
+
|
|
10
|
+
It does the fiddly setup for you:
|
|
11
|
+
|
|
12
|
+
- **Generates the crypto secrets** in the exact formats the server requires —
|
|
13
|
+
`AUTH_SESSION_SECRET` (32 random bytes, hex) and `ENCRYPTION_KEY` (32 random bytes, base64).
|
|
14
|
+
- **Mints a source-control token.** Pick GitHub or GitLab; the CLI opens your browser at the
|
|
15
|
+
provider's "create a personal access token" page with the right **scopes pre-selected**
|
|
16
|
+
(GitHub classic `repo,workflow`; GitLab `api`), then reads the token you paste back. Both
|
|
17
|
+
providers are first-class in local mode: the token authenticates the agent containers' git
|
|
18
|
+
clone/push, and the CI gate / mergeability / real merge / repo-link flows all run against the
|
|
19
|
+
provider's real API (GitLab via `@cat-factory/gitlab`, GitHub via the PAT client). For a
|
|
20
|
+
self-managed GitLab instance, set `GITLAB_API_BASE` in `local/.env`.
|
|
21
|
+
- **Populates and gitignores the `.env` files.** It writes `local/.env` (DB URL, the generated
|
|
22
|
+
secrets, your PAT, the harness image) and `frontend/.env` (`NUXT_PUBLIC_API_BASE`), and writes
|
|
23
|
+
(or merges into) a `.gitignore` so those secret files are never committed.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
No install needed — run it with your package manager's runner:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm create @cat-factory/cli@latest # or:
|
|
31
|
+
pnpm dlx @cat-factory/cli
|
|
32
|
+
npx @cat-factory/cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Interactive by default (powered by [`@clack/prompts`](https://www.npmjs.com/package/@clack/prompts))
|
|
36
|
+
— it asks for the project name and app title, lets you pick the source-control provider and
|
|
37
|
+
container runtime from a menu, asks for the database URL and API base, opens the browser to create
|
|
38
|
+
the token, and reads it back via a masked password prompt. Ctrl-C cancels cleanly at any step.
|
|
39
|
+
|
|
40
|
+
### Non-interactive
|
|
41
|
+
|
|
42
|
+
Drive it entirely with flags (handy for scripts / CI):
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npx @cat-factory/cli init \
|
|
46
|
+
--yes \
|
|
47
|
+
--dir my-cats \
|
|
48
|
+
--provider github \
|
|
49
|
+
--token "$GITHUB_PAT" \
|
|
50
|
+
--db-url "postgres://cat:cat@localhost:5432/catfactory" \
|
|
51
|
+
--api-base "http://localhost:8787"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Options
|
|
55
|
+
|
|
56
|
+
| Flag | Default | Meaning |
|
|
57
|
+
| ------------------------- | --------------------------------------- | ------------------------------------------------------------- |
|
|
58
|
+
| `-d, --dir <path>` | `./<name>` | Target directory. |
|
|
59
|
+
| `--name <name>` | `cat-factory` | Project name slug (used for the scaffolded names). |
|
|
60
|
+
| `--title <title>` | `Agent Architecture Board` | Frontend app title. |
|
|
61
|
+
| `--provider <p>` | `github` | Source control: `github` or `gitlab`. |
|
|
62
|
+
| `--token <token>` | (prompted) | PAT value; skips the browser/paste flow. |
|
|
63
|
+
| `--db-url <url>` | `postgres://cat:cat@…` | Postgres `DATABASE_URL`. |
|
|
64
|
+
| `--api-base <url>` | `http://localhost:<port>` | Backend API base baked into the SPA. |
|
|
65
|
+
| `--port <n>` | `8787` | Backend HTTP port (also sets the SPA's api-base). |
|
|
66
|
+
| `--harness-image <ref>` | `ghcr.io/…/cat-factory-executor:latest` | Executor-harness image agent jobs run as. |
|
|
67
|
+
| `--container-runtime <r>` | `docker` | Agent runtime: `docker`/`podman`/`orbstack`/`colima`/`apple`. |
|
|
68
|
+
| `--no-open` | off | Print the token URL but don't open the browser. |
|
|
69
|
+
| `-y, --yes` | off | Non-interactive: use defaults/flags, never prompt. |
|
|
70
|
+
| `-f, --force` | off | Overwrite existing files. |
|
|
71
|
+
| `-h, --help` | | Show help. |
|
|
72
|
+
| `-v, --version` | | Show the CLI version. |
|
|
73
|
+
|
|
74
|
+
## What it scaffolds
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
<dir>/
|
|
78
|
+
.gitignore # ignores .env / .env.* (keeps .env.example), build output
|
|
79
|
+
README.md # generated, project-specific run instructions
|
|
80
|
+
local/ # backend — @cat-factory/local-server
|
|
81
|
+
package.json
|
|
82
|
+
src/main.ts # one-line startLocal() entry
|
|
83
|
+
docker-compose.yml # local Postgres (creds derived from --db-url)
|
|
84
|
+
tsconfig.json
|
|
85
|
+
.env # generated, gitignored: DATABASE_URL, secrets, PAT, harness image
|
|
86
|
+
.env.example # documented template
|
|
87
|
+
frontend/ # SPA — extends the @cat-factory/app Nuxt layer
|
|
88
|
+
package.json
|
|
89
|
+
nuxt.config.ts
|
|
90
|
+
wrangler.toml # Cloudflare Pages config (optional deploy target)
|
|
91
|
+
.env # generated, gitignored: NUXT_PUBLIC_API_BASE
|
|
92
|
+
.env.example
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Running the scaffolded project
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
cd <dir>
|
|
99
|
+
# backend
|
|
100
|
+
cd local && npm install && npm run db:up && npm start # serves :8787
|
|
101
|
+
# frontend (second terminal)
|
|
102
|
+
cd ../frontend && npm install && npm run dev # Nuxt dev on :3000
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
You still need to:
|
|
106
|
+
|
|
107
|
+
- **Pull the executor image** the agent jobs run as: `docker pull ghcr.io/kibertoad/cat-factory-executor:latest`.
|
|
108
|
+
- **Configure at least one model provider** in `local/.env` (the simplest is Cloudflare Workers
|
|
109
|
+
AI over REST: `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_API_TOKEN`; or a direct vendor key like
|
|
110
|
+
`ANTHROPIC_API_KEY`). Without one, no model is selectable and pipelines can't start.
|
|
111
|
+
|
|
112
|
+
The generated `README.md` repeats these steps with your chosen values, and links to the full
|
|
113
|
+
[local-mode docs](../../../deploy/local/README.md) (container-runtime matrix, repo linking, the
|
|
114
|
+
Tester's Docker-in-Docker / ephemeral environments, the warm container pool, etc.).
|
|
115
|
+
|
|
116
|
+
## Security notes
|
|
117
|
+
|
|
118
|
+
- The `.env` files hold secrets and are **gitignored** by the scaffolded `.gitignore`. Never
|
|
119
|
+
commit them. If you scaffold into an existing git repo, the CLI **merges** the required ignore
|
|
120
|
+
rules into your existing `.gitignore` rather than clobbering it.
|
|
121
|
+
- The pasted token is **not echoed** to the terminal.
|
|
122
|
+
- Keep `AUTH_SESSION_SECRET` and `ENCRYPTION_KEY` **stable**: regenerating the session secret
|
|
123
|
+
forces a re-login, and regenerating the encryption key orphans every encrypted-at-rest
|
|
124
|
+
credential.
|
|
125
|
+
|
|
126
|
+
## Programmatic API
|
|
127
|
+
|
|
128
|
+
The bin is a thin shell over the package's exported functions, which are pure and reusable:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { buildPlan, generateSecrets, patCreationUrl } from '@cat-factory/cli'
|
|
132
|
+
|
|
133
|
+
const secrets = generateSecrets()
|
|
134
|
+
const files = buildPlan({ projectName: 'my-cats', /* … */ ...secrets })
|
|
135
|
+
// files: { path, content, secret? }[] — write them wherever you like
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
See `src/index.ts` for the full surface (`bootstrap`, `parseArgs`, `buildLocalEnv`,
|
|
139
|
+
`buildGitignore`, `mergeGitignore`, the VCS URL helpers, …).
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ContainerRuntime } from './templates.js';
|
|
2
|
+
import { type VcsProvider } from './vcs.js';
|
|
3
|
+
/** Parsed, validated CLI options. Unset optionals are resolved later (defaults / prompts). */
|
|
4
|
+
export interface CliOptions {
|
|
5
|
+
/** The subcommand. Only `init` exists today; the default when omitted. */
|
|
6
|
+
command: 'init' | 'help' | 'version';
|
|
7
|
+
dir?: string;
|
|
8
|
+
projectName?: string;
|
|
9
|
+
appTitle?: string;
|
|
10
|
+
provider?: VcsProvider;
|
|
11
|
+
token?: string;
|
|
12
|
+
databaseUrl?: string;
|
|
13
|
+
apiBase?: string;
|
|
14
|
+
port?: number;
|
|
15
|
+
harnessImage?: string;
|
|
16
|
+
/** Container runtime that spawns agent jobs (`LOCAL_CONTAINER_RUNTIME`). */
|
|
17
|
+
containerRuntime?: ContainerRuntime;
|
|
18
|
+
/** Skip opening the browser at the token-creation URL (still prints it). */
|
|
19
|
+
noOpen: boolean;
|
|
20
|
+
/** Non-interactive: never prompt; use defaults/flags. Fails if a required value is missing. */
|
|
21
|
+
yes: boolean;
|
|
22
|
+
/** Overwrite existing files instead of refusing. */
|
|
23
|
+
force: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare class ArgError extends Error {
|
|
26
|
+
}
|
|
27
|
+
/** Parse `process.argv.slice(2)` into {@link CliOptions}. Throws {@link ArgError} on bad input. */
|
|
28
|
+
export declare function parseArgs(argv: string[]): CliOptions;
|
|
29
|
+
/** Resolved default values for any option the user didn't supply. */
|
|
30
|
+
export declare const OPTION_DEFAULTS: {
|
|
31
|
+
readonly projectName: 'cat-factory';
|
|
32
|
+
readonly appTitle: 'Agent Architecture Board';
|
|
33
|
+
readonly provider: VcsProvider;
|
|
34
|
+
readonly databaseUrl: 'postgres://cat:cat@localhost:5432/catfactory';
|
|
35
|
+
readonly port: 8787;
|
|
36
|
+
readonly harnessImage: "ghcr.io/kibertoad/cat-factory-executor:latest";
|
|
37
|
+
readonly containerRuntime: ContainerRuntime;
|
|
38
|
+
};
|
|
39
|
+
export declare const HELP_TEXT = "cat-factory \u2014 bootstrap a local cat-factory deployment\n\nUsage:\n cat-factory [init] [options]\n\nScaffolds a local-mode backend (local/) and frontend SPA (frontend/), generates the\ncrypto secrets, mints a GitHub/GitLab personal access token (opens your browser), and\nwrites gitignored .env files.\n\nOptions:\n -d, --dir <path> Target directory (default: ./<name>)\n --name <name> Project name slug (default: cat-factory)\n --title <title> Frontend app title (default: Agent Architecture Board)\n --provider <p> Source control: github | gitlab (default: github)\n --token <token> PAT value (skips the browser/paste flow)\n --db-url <url> Postgres DATABASE_URL\n --api-base <url> Backend API base for the SPA (default: http://localhost:<port>)\n --port <n> Backend HTTP port (default: 8787; also sets the SPA's api-base)\n --harness-image <ref> Executor-harness image (default: ghcr.io ...:latest)\n --container-runtime <r> Agent container runtime: docker | podman | orbstack | colima | apple\n --no-open Don't open the browser (just print the token URL)\n -y, --yes Non-interactive: use defaults/flags, never prompt\n -f, --force Overwrite existing files\n -h, --help Show this help\n -v, --version Show the CLI version\n";
|
|
40
|
+
//# sourceMappingURL=args.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,gBAAgB,EAAyB,MAAM,gBAAgB,CAAA;AACjG,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAE1D,8FAA8F;AAC9F,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACpC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,WAAW,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IACnC,4EAA4E;IAC5E,MAAM,EAAE,OAAO,CAAA;IACf,+FAA+F;IAC/F,GAAG,EAAE,OAAO,CAAA;IACZ,oDAAoD;IACpD,KAAK,EAAE,OAAO,CAAA;CACf;AAQD,qBAAa,QAAS,SAAQ,KAAK;CAAG;AAEtC,mGAAmG;AACnG,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAoFpD;AAwBD,qEAAqE;AACrE,eAAO,MAAM,eAAe;aAC1B,WAAW,EAAE,aAAa;aAC1B,QAAQ,EAAE,0BAA0B;aACpC,QAAQ,EAAc,WAAW;aACjC,WAAW,EAAE,8CAA8C;aAI3D,IAAI,EAAE,IAAI;aACV,YAAY;aACZ,gBAAgB,EAAc,gBAAgB;CACtC,CAAA;AAEV,eAAO,MAAM,SAAS,k3CAyBrB,CAAA"}
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { CONTAINER_RUNTIMES, DEFAULT_HARNESS_IMAGE } from './templates.js';
|
|
2
|
+
import { VCS_PROVIDERS } from './vcs.js';
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
noOpen: false,
|
|
5
|
+
yes: false,
|
|
6
|
+
force: false,
|
|
7
|
+
};
|
|
8
|
+
export class ArgError extends Error {
|
|
9
|
+
}
|
|
10
|
+
/** Parse `process.argv.slice(2)` into {@link CliOptions}. Throws {@link ArgError} on bad input. */
|
|
11
|
+
export function parseArgs(argv) {
|
|
12
|
+
const opts = { command: 'init', ...DEFAULTS };
|
|
13
|
+
let commandSet = false;
|
|
14
|
+
const takeValue = (flag, inline, rest) => {
|
|
15
|
+
if (inline !== undefined)
|
|
16
|
+
return inline;
|
|
17
|
+
const next = rest.shift();
|
|
18
|
+
if (next === undefined)
|
|
19
|
+
throw new ArgError(`Missing value for ${flag}`);
|
|
20
|
+
return next;
|
|
21
|
+
};
|
|
22
|
+
const queue = [...argv];
|
|
23
|
+
while (queue.length > 0) {
|
|
24
|
+
const raw = queue.shift();
|
|
25
|
+
const eq = raw.indexOf('=');
|
|
26
|
+
const flag = raw.startsWith('--') && eq !== -1 ? raw.slice(0, eq) : raw;
|
|
27
|
+
const inline = raw.startsWith('--') && eq !== -1 ? raw.slice(eq + 1) : undefined;
|
|
28
|
+
switch (flag) {
|
|
29
|
+
case 'init':
|
|
30
|
+
if (!commandSet) {
|
|
31
|
+
opts.command = 'init';
|
|
32
|
+
commandSet = true;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
case 'help':
|
|
36
|
+
case '--help':
|
|
37
|
+
case '-h':
|
|
38
|
+
opts.command = 'help';
|
|
39
|
+
commandSet = true;
|
|
40
|
+
break;
|
|
41
|
+
case 'version':
|
|
42
|
+
case '--version':
|
|
43
|
+
case '-v':
|
|
44
|
+
opts.command = 'version';
|
|
45
|
+
commandSet = true;
|
|
46
|
+
break;
|
|
47
|
+
case '--dir':
|
|
48
|
+
case '-d':
|
|
49
|
+
opts.dir = takeValue(flag, inline, queue);
|
|
50
|
+
break;
|
|
51
|
+
case '--name':
|
|
52
|
+
opts.projectName = takeValue(flag, inline, queue);
|
|
53
|
+
break;
|
|
54
|
+
case '--title':
|
|
55
|
+
opts.appTitle = takeValue(flag, inline, queue);
|
|
56
|
+
break;
|
|
57
|
+
case '--provider':
|
|
58
|
+
opts.provider = parseProvider(takeValue(flag, inline, queue));
|
|
59
|
+
break;
|
|
60
|
+
case '--token':
|
|
61
|
+
opts.token = takeValue(flag, inline, queue);
|
|
62
|
+
break;
|
|
63
|
+
case '--db-url':
|
|
64
|
+
opts.databaseUrl = takeValue(flag, inline, queue);
|
|
65
|
+
break;
|
|
66
|
+
case '--api-base':
|
|
67
|
+
opts.apiBase = takeValue(flag, inline, queue);
|
|
68
|
+
break;
|
|
69
|
+
case '--port':
|
|
70
|
+
opts.port = parsePort(takeValue(flag, inline, queue));
|
|
71
|
+
break;
|
|
72
|
+
case '--harness-image':
|
|
73
|
+
opts.harnessImage = takeValue(flag, inline, queue);
|
|
74
|
+
break;
|
|
75
|
+
case '--container-runtime':
|
|
76
|
+
opts.containerRuntime = parseContainerRuntime(takeValue(flag, inline, queue));
|
|
77
|
+
break;
|
|
78
|
+
case '--no-open':
|
|
79
|
+
opts.noOpen = true;
|
|
80
|
+
break;
|
|
81
|
+
case '--yes':
|
|
82
|
+
case '-y':
|
|
83
|
+
opts.yes = true;
|
|
84
|
+
break;
|
|
85
|
+
case '--force':
|
|
86
|
+
case '-f':
|
|
87
|
+
opts.force = true;
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
throw new ArgError(`Unknown argument: ${raw}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return opts;
|
|
94
|
+
}
|
|
95
|
+
function parseProvider(value) {
|
|
96
|
+
const v = value.toLowerCase();
|
|
97
|
+
if (VCS_PROVIDERS.includes(v))
|
|
98
|
+
return v;
|
|
99
|
+
throw new ArgError(`Invalid --provider "${value}" (expected: ${VCS_PROVIDERS.join(' | ')})`);
|
|
100
|
+
}
|
|
101
|
+
function parseContainerRuntime(value) {
|
|
102
|
+
const v = value.toLowerCase();
|
|
103
|
+
if (CONTAINER_RUNTIMES.includes(v))
|
|
104
|
+
return v;
|
|
105
|
+
throw new ArgError(`Invalid --container-runtime "${value}" (expected: ${CONTAINER_RUNTIMES.join(' | ')})`);
|
|
106
|
+
}
|
|
107
|
+
function parsePort(value) {
|
|
108
|
+
const n = Number(value);
|
|
109
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
110
|
+
throw new ArgError(`Invalid --port "${value}" (expected an integer 1-65535)`);
|
|
111
|
+
}
|
|
112
|
+
return n;
|
|
113
|
+
}
|
|
114
|
+
/** Resolved default values for any option the user didn't supply. */
|
|
115
|
+
export const OPTION_DEFAULTS = {
|
|
116
|
+
projectName: 'cat-factory',
|
|
117
|
+
appTitle: 'Agent Architecture Board',
|
|
118
|
+
provider: 'github',
|
|
119
|
+
databaseUrl: 'postgres://cat:cat@localhost:5432/catfactory',
|
|
120
|
+
// The SPA's API base has no standalone default: it is derived from `port`
|
|
121
|
+
// (`http://localhost:<port>`) in bootstrap.ts so a custom --port can't leave the frontend
|
|
122
|
+
// pointed at the wrong backend.
|
|
123
|
+
port: 8787,
|
|
124
|
+
harnessImage: DEFAULT_HARNESS_IMAGE,
|
|
125
|
+
containerRuntime: 'docker',
|
|
126
|
+
};
|
|
127
|
+
export const HELP_TEXT = `cat-factory — bootstrap a local cat-factory deployment
|
|
128
|
+
|
|
129
|
+
Usage:
|
|
130
|
+
cat-factory [init] [options]
|
|
131
|
+
|
|
132
|
+
Scaffolds a local-mode backend (local/) and frontend SPA (frontend/), generates the
|
|
133
|
+
crypto secrets, mints a GitHub/GitLab personal access token (opens your browser), and
|
|
134
|
+
writes gitignored .env files.
|
|
135
|
+
|
|
136
|
+
Options:
|
|
137
|
+
-d, --dir <path> Target directory (default: ./<name>)
|
|
138
|
+
--name <name> Project name slug (default: cat-factory)
|
|
139
|
+
--title <title> Frontend app title (default: Agent Architecture Board)
|
|
140
|
+
--provider <p> Source control: github | gitlab (default: github)
|
|
141
|
+
--token <token> PAT value (skips the browser/paste flow)
|
|
142
|
+
--db-url <url> Postgres DATABASE_URL
|
|
143
|
+
--api-base <url> Backend API base for the SPA (default: http://localhost:<port>)
|
|
144
|
+
--port <n> Backend HTTP port (default: 8787; also sets the SPA's api-base)
|
|
145
|
+
--harness-image <ref> Executor-harness image (default: ghcr.io ...:latest)
|
|
146
|
+
--container-runtime <r> Agent container runtime: docker | podman | orbstack | colima | apple
|
|
147
|
+
--no-open Don't open the browser (just print the token URL)
|
|
148
|
+
-y, --yes Non-interactive: use defaults/flags, never prompt
|
|
149
|
+
-f, --force Overwrite existing files
|
|
150
|
+
-h, --help Show this help
|
|
151
|
+
-v, --version Show the CLI version
|
|
152
|
+
`;
|
|
153
|
+
//# sourceMappingURL=args.js.map
|
package/dist/args.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"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"}
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ArgError, HELP_TEXT, parseArgs } from './args.js';
|
|
5
|
+
import { bootstrap } from './bootstrap.js';
|
|
6
|
+
function readVersion() {
|
|
7
|
+
try {
|
|
8
|
+
const pkgUrl = new URL('../package.json', import.meta.url);
|
|
9
|
+
const pkg = JSON.parse(readFileSync(fileURLToPath(pkgUrl), 'utf8'));
|
|
10
|
+
return pkg.version ?? '0.0.0';
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return '0.0.0';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function main() {
|
|
17
|
+
let options;
|
|
18
|
+
try {
|
|
19
|
+
options = parseArgs(process.argv.slice(2));
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (err instanceof ArgError) {
|
|
23
|
+
process.stderr.write(`${err.message}\n\n${HELP_TEXT}`);
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
if (options.command === 'help') {
|
|
29
|
+
process.stdout.write(HELP_TEXT);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (options.command === 'version') {
|
|
33
|
+
process.stdout.write(`${readVersion()}\n`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
await bootstrap(options);
|
|
37
|
+
}
|
|
38
|
+
main().catch((err) => {
|
|
39
|
+
process.stderr.write(`\ncat-factory: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
|
42
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C,SAAS,WAAW;IAClB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,iBAAiB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAA;QAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAyB,CAAA;QAC3F,OAAO,GAAG,CAAC,OAAO,IAAI,OAAO,CAAA;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAA;IAChB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,OAAO,CAAA;IACX,IAAI,CAAC;QACH,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;YAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,OAAO,OAAO,SAAS,EAAE,CAAC,CAAA;YACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjB,CAAC;QACD,MAAM,GAAG,CAAA;IACX,CAAC;IAED,IAAI,OAAO,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;QAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;QAC/B,OAAM;IACR,CAAC;IACD,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAClC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,WAAW,EAAE,IAAI,CAAC,CAAA;QAC1C,OAAM;IACR,CAAC;IAED,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;AAC1B,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kBAAkB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC5F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type CliOptions } from './args.js';
|
|
2
|
+
import { type Io } from './io.js';
|
|
3
|
+
import { type RandomBytes } from './secrets.js';
|
|
4
|
+
/** The filesystem operations the writer needs — injectable so the flow is testable. */
|
|
5
|
+
export interface FileSystem {
|
|
6
|
+
existsSync(path: string): boolean;
|
|
7
|
+
mkdirSync(path: string, options: {
|
|
8
|
+
recursive: true;
|
|
9
|
+
}): void;
|
|
10
|
+
readFileSync(path: string, encoding: 'utf8'): string;
|
|
11
|
+
writeFileSync(path: string, data: string): void;
|
|
12
|
+
}
|
|
13
|
+
export interface BootstrapDeps {
|
|
14
|
+
io?: Io;
|
|
15
|
+
fs?: FileSystem;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
randomBytes?: RandomBytes;
|
|
18
|
+
}
|
|
19
|
+
export declare class BootstrapError extends Error {
|
|
20
|
+
}
|
|
21
|
+
/** Run the interactive (or flag-driven) bootstrap. Returns the absolute project dir. */
|
|
22
|
+
export declare function bootstrap(options: CliOptions, deps?: BootstrapDeps): Promise<string>;
|
|
23
|
+
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,UAAU,EAAmB,MAAM,WAAW,CAAA;AAC5D,OAAO,EAAmB,KAAK,EAAE,EAAE,MAAM,SAAS,CAAA;AAElD,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAKhE,uFAAuF;AACvF,MAAM,WAAW,UAAU;IACzB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IACjC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,SAAS,EAAE,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;IAC3D,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACpD,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD;AAaD,MAAM,WAAW,aAAa;IAC5B,EAAE,CAAC,EAAE,EAAE,CAAA;IACP,EAAE,CAAC,EAAE,UAAU,CAAA;IACf,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED,qBAAa,cAAe,SAAQ,KAAK;CAAG;AAE5C,wFAAwF;AACxF,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAuF9F"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as nodeFs from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { OPTION_DEFAULTS } from './args.js';
|
|
4
|
+
import { createConsoleIo } from './io.js';
|
|
5
|
+
import { buildPlan } from './plan.js';
|
|
6
|
+
import { generateSecrets } from './secrets.js';
|
|
7
|
+
import { slugifyProjectName } from './slug.js';
|
|
8
|
+
import { CONTAINER_RUNTIMES } from './templates.js';
|
|
9
|
+
import { patCreationUrl, providerLabel, VCS_PROVIDERS } from './vcs.js';
|
|
10
|
+
const realFs = {
|
|
11
|
+
existsSync: (p) => nodeFs.existsSync(p),
|
|
12
|
+
mkdirSync: (p, o) => {
|
|
13
|
+
nodeFs.mkdirSync(p, o);
|
|
14
|
+
},
|
|
15
|
+
readFileSync: (p, e) => nodeFs.readFileSync(p, e),
|
|
16
|
+
writeFileSync: (p, d) => {
|
|
17
|
+
nodeFs.writeFileSync(p, d);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export class BootstrapError extends Error {
|
|
21
|
+
}
|
|
22
|
+
/** Run the interactive (or flag-driven) bootstrap. Returns the absolute project dir. */
|
|
23
|
+
export async function bootstrap(options, deps = {}) {
|
|
24
|
+
const io = deps.io ?? createConsoleIo();
|
|
25
|
+
const fs = deps.fs ?? realFs;
|
|
26
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
27
|
+
io.info('\ncat-factory — scaffold a local deployment\n');
|
|
28
|
+
const rawProjectName = options.projectName ??
|
|
29
|
+
(options.yes
|
|
30
|
+
? OPTION_DEFAULTS.projectName
|
|
31
|
+
: await io.question('Project name', OPTION_DEFAULTS.projectName));
|
|
32
|
+
// The name is used verbatim as the generated packages' npm `name`, so it must be a valid slug.
|
|
33
|
+
const projectName = slugifyProjectName(rawProjectName, OPTION_DEFAULTS.projectName);
|
|
34
|
+
if (projectName !== rawProjectName) {
|
|
35
|
+
io.warn(`Using "${projectName}" as the project slug (npm package names must be lowercased).`);
|
|
36
|
+
}
|
|
37
|
+
const targetDir = resolve(cwd, options.dir ?? projectName);
|
|
38
|
+
const appTitle = options.appTitle ??
|
|
39
|
+
(options.yes
|
|
40
|
+
? OPTION_DEFAULTS.appTitle
|
|
41
|
+
: await io.question('App title', OPTION_DEFAULTS.appTitle));
|
|
42
|
+
const provider = await resolveProvider(options, io);
|
|
43
|
+
const databaseUrl = options.databaseUrl ??
|
|
44
|
+
(options.yes
|
|
45
|
+
? OPTION_DEFAULTS.databaseUrl
|
|
46
|
+
: await io.question('Postgres DATABASE_URL', OPTION_DEFAULTS.databaseUrl));
|
|
47
|
+
// Resolve the port first: the SPA's api-base defaults to it, so a non-default --port can't
|
|
48
|
+
// silently leave the frontend pointed at the wrong backend.
|
|
49
|
+
const port = options.port ?? OPTION_DEFAULTS.port;
|
|
50
|
+
const defaultApiBase = `http://localhost:${port}`;
|
|
51
|
+
const apiBase = options.apiBase ??
|
|
52
|
+
(options.yes
|
|
53
|
+
? defaultApiBase
|
|
54
|
+
: await io.question('Backend API base (for the SPA)', defaultApiBase));
|
|
55
|
+
const harnessImage = options.harnessImage ?? OPTION_DEFAULTS.harnessImage;
|
|
56
|
+
const containerRuntime = await resolveContainerRuntime(options, io);
|
|
57
|
+
const corsAllowedOrigins = 'http://localhost:3000';
|
|
58
|
+
const token = await resolveToken(options, provider, io);
|
|
59
|
+
const secrets = generateSecrets(deps.randomBytes);
|
|
60
|
+
io.info('\nGenerated AUTH_SESSION_SECRET (hex) and ENCRYPTION_KEY (base64).');
|
|
61
|
+
const gitignorePath = join(targetDir, '.gitignore');
|
|
62
|
+
const existingGitignore = fs.existsSync(gitignorePath)
|
|
63
|
+
? fs.readFileSync(gitignorePath, 'utf8')
|
|
64
|
+
: undefined;
|
|
65
|
+
const plan = buildPlan({
|
|
66
|
+
projectName,
|
|
67
|
+
appTitle,
|
|
68
|
+
provider,
|
|
69
|
+
token,
|
|
70
|
+
databaseUrl,
|
|
71
|
+
apiBase,
|
|
72
|
+
port,
|
|
73
|
+
corsAllowedOrigins,
|
|
74
|
+
harnessImage,
|
|
75
|
+
containerRuntime,
|
|
76
|
+
authSessionSecret: secrets.authSessionSecret,
|
|
77
|
+
encryptionKey: secrets.encryptionKey,
|
|
78
|
+
existingGitignore,
|
|
79
|
+
});
|
|
80
|
+
writePlan(plan, targetDir, fs, io, options.force);
|
|
81
|
+
printNextSteps(io, {
|
|
82
|
+
targetDir,
|
|
83
|
+
projectName,
|
|
84
|
+
provider,
|
|
85
|
+
port,
|
|
86
|
+
apiBase,
|
|
87
|
+
tokenProvided: token !== '',
|
|
88
|
+
alreadyGitRepo: fs.existsSync(join(targetDir, '.git')),
|
|
89
|
+
});
|
|
90
|
+
return targetDir;
|
|
91
|
+
}
|
|
92
|
+
async function resolveProvider(options, io) {
|
|
93
|
+
if (options.provider)
|
|
94
|
+
return options.provider;
|
|
95
|
+
if (options.yes)
|
|
96
|
+
return OPTION_DEFAULTS.provider;
|
|
97
|
+
return io.select('Source control', VCS_PROVIDERS.map((value) => ({ value, label: providerLabel(value) })), OPTION_DEFAULTS.provider);
|
|
98
|
+
}
|
|
99
|
+
async function resolveContainerRuntime(options, io) {
|
|
100
|
+
if (options.containerRuntime)
|
|
101
|
+
return options.containerRuntime;
|
|
102
|
+
if (options.yes)
|
|
103
|
+
return OPTION_DEFAULTS.containerRuntime;
|
|
104
|
+
return io.select('Container runtime that spawns agent jobs', CONTAINER_RUNTIMES.map((value) => ({ value, label: value })), OPTION_DEFAULTS.containerRuntime);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the PAT: an explicit `--token` wins; otherwise (interactive only) print the pre-scoped
|
|
108
|
+
* creation URL, open the browser at it, and read the pasted token back. In `--yes` mode with no
|
|
109
|
+
* token, leave it blank (the var is still written, present-but-empty).
|
|
110
|
+
*/
|
|
111
|
+
async function resolveToken(options, provider, io) {
|
|
112
|
+
if (options.token !== undefined)
|
|
113
|
+
return options.token;
|
|
114
|
+
if (options.yes)
|
|
115
|
+
return '';
|
|
116
|
+
const url = patCreationUrl(provider);
|
|
117
|
+
const label = providerLabel(provider);
|
|
118
|
+
io.info(`\nCreate a ${label} personal access token (scopes pre-selected):\n ${url}`);
|
|
119
|
+
if (!options.noOpen) {
|
|
120
|
+
const open = await io.confirm('Open this URL in your browser now?', true);
|
|
121
|
+
if (open)
|
|
122
|
+
await io.openBrowser(url);
|
|
123
|
+
}
|
|
124
|
+
const token = await io.secret(`Paste your ${label} token here (or leave blank to add later)`);
|
|
125
|
+
if (token === '')
|
|
126
|
+
io.warn('No token entered — the var will be written blank; set it before running agents.');
|
|
127
|
+
return token;
|
|
128
|
+
}
|
|
129
|
+
function writePlan(plan, targetDir, fs, io, force) {
|
|
130
|
+
io.info(`\nWriting to ${targetDir}`);
|
|
131
|
+
for (const file of plan) {
|
|
132
|
+
const abs = join(targetDir, file.path);
|
|
133
|
+
// `.gitignore` is merged (existingGitignore folded in by buildPlan), so always (re)write it.
|
|
134
|
+
const isMergeable = file.path === '.gitignore';
|
|
135
|
+
if (fs.existsSync(abs) && !force && !isMergeable) {
|
|
136
|
+
io.warn(` skip ${file.path} (exists; use --force to overwrite)`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
fs.mkdirSync(dirname(abs), { recursive: true });
|
|
140
|
+
fs.writeFileSync(abs, file.content);
|
|
141
|
+
io.info(` write ${file.path}${file.secret ? ' (secret — gitignored)' : ''}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function printNextSteps(io, input) {
|
|
145
|
+
const lines = [
|
|
146
|
+
'',
|
|
147
|
+
'Done. Next steps:',
|
|
148
|
+
'',
|
|
149
|
+
` cd ${input.targetDir}`,
|
|
150
|
+
' # backend',
|
|
151
|
+
' cd local && npm install && npm run db:up && npm start',
|
|
152
|
+
' # frontend (in a second terminal)',
|
|
153
|
+
' cd frontend && npm install && npm run dev',
|
|
154
|
+
'',
|
|
155
|
+
` Backend API: http://localhost:${input.port}`,
|
|
156
|
+
' Frontend SPA: http://localhost:3000',
|
|
157
|
+
'',
|
|
158
|
+
'Reminders:',
|
|
159
|
+
` - Pull the executor image: docker pull ${OPTION_DEFAULTS.harnessImage}`,
|
|
160
|
+
' - Configure at least one model provider in local/.env (e.g. CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN).',
|
|
161
|
+
];
|
|
162
|
+
if (!input.tokenProvided) {
|
|
163
|
+
lines.push(` - Set your ${providerLabel(input.provider)} token in local/.env before running agents.`);
|
|
164
|
+
}
|
|
165
|
+
lines.push(' - local/.env and frontend/.env hold secrets and are gitignored — never commit them.');
|
|
166
|
+
if (!input.alreadyGitRepo) {
|
|
167
|
+
lines.push(` - Not a git repo yet — run \`git init\` in ${input.targetDir} to start tracking (the`, ' generated .gitignore already keeps the .env secrets out).');
|
|
168
|
+
}
|
|
169
|
+
io.info(lines.join('\n'));
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"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"}
|