context101-cli 0.1.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/bin/context101.js +11 -0
- package/package.json +26 -0
- package/src/amplify-repo.js +25 -0
- package/src/aws-profiles.js +128 -0
- package/src/bedrock-access.js +129 -0
- package/src/cdk-invoke.js +231 -0
- package/src/checks.js +165 -0
- package/src/clone.js +63 -0
- package/src/config.js +102 -0
- package/src/defaults.js +28 -0
- package/src/deploy-env-load.js +74 -0
- package/src/deploy.js +138 -0
- package/src/docker.js +120 -0
- package/src/embedding-models.js +184 -0
- package/src/env-file.js +140 -0
- package/src/exec.js +34 -0
- package/src/hosted-url.js +35 -0
- package/src/init.js +435 -0
- package/src/main.js +41 -0
- package/src/parse-args.js +320 -0
- package/src/plan.js +111 -0
- package/src/prompt.js +104 -0
- package/src/redact.js +28 -0
- package/src/repo.js +79 -0
- package/src/secrets.js +9 -0
- package/src/stacks.js +187 -0
- package/src/style.js +54 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { DRIVER_NEON, DRIVER_POSTGRES } from "./defaults.js";
|
|
2
|
+
|
|
3
|
+
const FLAG_HELP = `
|
|
4
|
+
Usage: context101 <command> [options]
|
|
5
|
+
|
|
6
|
+
init write a local secrets file (default)
|
|
7
|
+
deploy deploy the AWS stack (loads deploy-env, invokes cdk)
|
|
8
|
+
diff cdk diff with the same context flags
|
|
9
|
+
synth cdk synth with the same context flags
|
|
10
|
+
list, ls list Context101 CloudFormation deployments
|
|
11
|
+
destroy, remove, rm tear down a listed stack (name required)
|
|
12
|
+
config show deploy-env keys (values redacted)
|
|
13
|
+
config set KEY=value write one key (chmod 600; value is not printed)
|
|
14
|
+
|
|
15
|
+
CDK fails closed: a bare \`cdk deploy\` without \`-c token=\` throws
|
|
16
|
+
instead of deleting MCP / Amplify. The CLI is the front door.
|
|
17
|
+
|
|
18
|
+
context101 init
|
|
19
|
+
context101 deploy
|
|
20
|
+
npx context101-cli init
|
|
21
|
+
npx context101-cli deploy
|
|
22
|
+
|
|
23
|
+
--dry-run print the plan; write nothing, deploy nothing
|
|
24
|
+
--yes, -y accept defaults (creates RDS if no --database-url)
|
|
25
|
+
--force overwrite an existing env file
|
|
26
|
+
--dir <path> clone into this directory when not in a checkout
|
|
27
|
+
--deploy-env <path> default: <repo>/cdk/.deploy-env
|
|
28
|
+
--home write ~/.context101/deploy-env instead
|
|
29
|
+
--database-url <url> Postgres URL (also reads DATABASE_URL)
|
|
30
|
+
--create-rds CDK provisions RDS Postgres (default when no URL)
|
|
31
|
+
--database-driver ${DRIVER_NEON} | ${DRIVER_POSTGRES}
|
|
32
|
+
--database-prepare true | false
|
|
33
|
+
--aws-profile <name> also reads AWS_PROFILE; required with --yes
|
|
34
|
+
when more than one profile exists
|
|
35
|
+
--aws-access-key-id used when no profile exists (also AWS_ACCESS_KEY_ID)
|
|
36
|
+
--aws-secret-access-key
|
|
37
|
+
used when no profile exists (also AWS_SECRET_ACCESS_KEY)
|
|
38
|
+
--repo <url> watch this GitHub repo with Amplify
|
|
39
|
+
(default: skip Amplify, unless gh login is jginorio)
|
|
40
|
+
--embed-model <id> optional CDK default embedding model
|
|
41
|
+
(brains still pick any Titan/Cohere model in the app)
|
|
42
|
+
--skip-bedrock-access do not request Bedrock model access during init
|
|
43
|
+
--seed first deploy uploads knowledge/ once
|
|
44
|
+
--deploy deploy after writing (combine with --yes)
|
|
45
|
+
|
|
46
|
+
deploy / diff / synth:
|
|
47
|
+
--seed upload knowledge/ once (first deploy only)
|
|
48
|
+
--deploy-env <path>
|
|
49
|
+
--home
|
|
50
|
+
--dry-run print the command; invoke nothing
|
|
51
|
+
|
|
52
|
+
list:
|
|
53
|
+
--aws-profile <name>
|
|
54
|
+
--aws-access-key-id
|
|
55
|
+
--aws-secret-access-key
|
|
56
|
+
|
|
57
|
+
destroy <StackName>:
|
|
58
|
+
--yes, -y skip the confirmation prompt
|
|
59
|
+
--aws-profile <name>
|
|
60
|
+
--aws-access-key-id
|
|
61
|
+
--aws-secret-access-key
|
|
62
|
+
--deploy-env <path>
|
|
63
|
+
--home
|
|
64
|
+
--dry-run print the plan; destroy nothing
|
|
65
|
+
|
|
66
|
+
config:
|
|
67
|
+
--deploy-env <path>
|
|
68
|
+
--home
|
|
69
|
+
|
|
70
|
+
From this checkout (after npm install):
|
|
71
|
+
npm run context101 -- init
|
|
72
|
+
npm run context101 -- deploy
|
|
73
|
+
npx context101-cli init
|
|
74
|
+
npx context101-cli deploy
|
|
75
|
+
context101 list
|
|
76
|
+
context101 destroy Context101Stack
|
|
77
|
+
context101 config
|
|
78
|
+
|
|
79
|
+
npx context101 (unscoped) downloads Context7's MCP from npm — unrelated.
|
|
80
|
+
The publishable CLI is context101-cli; the bin name is context101.
|
|
81
|
+
`.trim();
|
|
82
|
+
|
|
83
|
+
const INIT_ONLY = new Set([
|
|
84
|
+
"--yes",
|
|
85
|
+
"-y",
|
|
86
|
+
"--force",
|
|
87
|
+
"--deploy",
|
|
88
|
+
"--dir",
|
|
89
|
+
"--database-url",
|
|
90
|
+
"--create-rds",
|
|
91
|
+
"--database-driver",
|
|
92
|
+
"--database-prepare",
|
|
93
|
+
"--aws-profile",
|
|
94
|
+
"--aws-access-key-id",
|
|
95
|
+
"--aws-secret-access-key",
|
|
96
|
+
"--repo",
|
|
97
|
+
"--embed-model",
|
|
98
|
+
"--skip-bedrock-access",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const DESTROY_FROM_INIT = new Set([
|
|
102
|
+
"--yes",
|
|
103
|
+
"-y",
|
|
104
|
+
"--aws-profile",
|
|
105
|
+
"--aws-access-key-id",
|
|
106
|
+
"--aws-secret-access-key",
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const LIST_FROM_INIT = new Set([
|
|
110
|
+
"--aws-profile",
|
|
111
|
+
"--aws-access-key-id",
|
|
112
|
+
"--aws-secret-access-key",
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const CDK_COMMANDS = new Set(["deploy", "diff", "synth"]);
|
|
116
|
+
|
|
117
|
+
const COMMANDS = {
|
|
118
|
+
init: "init",
|
|
119
|
+
deploy: "deploy",
|
|
120
|
+
diff: "diff",
|
|
121
|
+
synth: "synth",
|
|
122
|
+
list: "list",
|
|
123
|
+
ls: "list",
|
|
124
|
+
destroy: "destroy",
|
|
125
|
+
remove: "destroy",
|
|
126
|
+
rm: "destroy",
|
|
127
|
+
config: "config",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export function helpText() {
|
|
131
|
+
return FLAG_HELP;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function parseArgs(argv) {
|
|
135
|
+
const opts = {
|
|
136
|
+
command: "init",
|
|
137
|
+
help: false,
|
|
138
|
+
dryRun: false,
|
|
139
|
+
yes: false,
|
|
140
|
+
force: false,
|
|
141
|
+
deploy: false,
|
|
142
|
+
seed: false,
|
|
143
|
+
home: false,
|
|
144
|
+
dir: null,
|
|
145
|
+
envFile: null,
|
|
146
|
+
databaseUrl: null,
|
|
147
|
+
createRds: false,
|
|
148
|
+
databaseDriver: null,
|
|
149
|
+
databasePrepare: null,
|
|
150
|
+
awsProfile: null,
|
|
151
|
+
awsAccessKeyId: null,
|
|
152
|
+
awsSecretAccessKey: null,
|
|
153
|
+
repo: null,
|
|
154
|
+
embedModel: null,
|
|
155
|
+
skipBedrockAccess: false,
|
|
156
|
+
stackName: null,
|
|
157
|
+
configAction: "show",
|
|
158
|
+
configKey: null,
|
|
159
|
+
configValue: null,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const args = [...argv];
|
|
163
|
+
if (args.length === 0) return opts;
|
|
164
|
+
|
|
165
|
+
const first = args[0];
|
|
166
|
+
if (COMMANDS[first]) {
|
|
167
|
+
opts.command = COMMANDS[first];
|
|
168
|
+
args.shift();
|
|
169
|
+
} else if (first === "help" || first === "--help" || first === "-h") {
|
|
170
|
+
opts.help = true;
|
|
171
|
+
return opts;
|
|
172
|
+
} else if (!first.startsWith("-")) {
|
|
173
|
+
const err = new Error(`unknown command: ${first}`);
|
|
174
|
+
err.code = "USAGE";
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (opts.command === "config" && args[0] === "set") {
|
|
179
|
+
args.shift();
|
|
180
|
+
const pair = args.shift();
|
|
181
|
+
if (!pair || !pair.includes("=")) {
|
|
182
|
+
const err = new Error("usage: context101 config set KEY=value");
|
|
183
|
+
err.code = "USAGE";
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
const eq = pair.indexOf("=");
|
|
187
|
+
opts.configAction = "set";
|
|
188
|
+
opts.configKey = pair.slice(0, eq);
|
|
189
|
+
opts.configValue = pair.slice(eq + 1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
while (args.length) {
|
|
193
|
+
const arg = args.shift();
|
|
194
|
+
if (opts.command === "destroy" && !arg.startsWith("-")) {
|
|
195
|
+
if (opts.stackName) {
|
|
196
|
+
const err = new Error("destroy takes one stack name");
|
|
197
|
+
err.code = "USAGE";
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
opts.stackName = arg;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!flagAllowed(opts.command, arg)) {
|
|
204
|
+
const err = new Error(
|
|
205
|
+
opts.command === "init" ? `unknown flag: ${arg}` : `${arg} is an init option`
|
|
206
|
+
);
|
|
207
|
+
err.code = "USAGE";
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
if (arg === "--seed" && !CDK_COMMANDS.has(opts.command) && opts.command !== "init") {
|
|
211
|
+
const err = new Error(`--seed is a deploy option`);
|
|
212
|
+
err.code = "USAGE";
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
switch (arg) {
|
|
216
|
+
case "--help":
|
|
217
|
+
case "-h":
|
|
218
|
+
opts.help = true;
|
|
219
|
+
break;
|
|
220
|
+
case "--dry-run":
|
|
221
|
+
opts.dryRun = true;
|
|
222
|
+
break;
|
|
223
|
+
case "--yes":
|
|
224
|
+
case "-y":
|
|
225
|
+
opts.yes = true;
|
|
226
|
+
break;
|
|
227
|
+
case "--force":
|
|
228
|
+
opts.force = true;
|
|
229
|
+
break;
|
|
230
|
+
case "--deploy":
|
|
231
|
+
opts.deploy = true;
|
|
232
|
+
break;
|
|
233
|
+
case "--seed":
|
|
234
|
+
opts.seed = true;
|
|
235
|
+
break;
|
|
236
|
+
case "--home":
|
|
237
|
+
opts.home = true;
|
|
238
|
+
break;
|
|
239
|
+
case "--dir":
|
|
240
|
+
opts.dir = needValue(arg, args);
|
|
241
|
+
break;
|
|
242
|
+
case "--deploy-env":
|
|
243
|
+
opts.envFile = needValue(arg, args);
|
|
244
|
+
break;
|
|
245
|
+
case "--database-url":
|
|
246
|
+
opts.databaseUrl = needValue(arg, args);
|
|
247
|
+
break;
|
|
248
|
+
case "--create-rds":
|
|
249
|
+
opts.createRds = true;
|
|
250
|
+
break;
|
|
251
|
+
case "--database-driver":
|
|
252
|
+
opts.databaseDriver = parseDriver(needValue(arg, args));
|
|
253
|
+
break;
|
|
254
|
+
case "--database-prepare":
|
|
255
|
+
opts.databasePrepare = parseBool(needValue(arg, args));
|
|
256
|
+
break;
|
|
257
|
+
case "--aws-profile":
|
|
258
|
+
opts.awsProfile = needValue(arg, args);
|
|
259
|
+
break;
|
|
260
|
+
case "--aws-access-key-id":
|
|
261
|
+
opts.awsAccessKeyId = needValue(arg, args);
|
|
262
|
+
break;
|
|
263
|
+
case "--aws-secret-access-key":
|
|
264
|
+
opts.awsSecretAccessKey = needValue(arg, args);
|
|
265
|
+
break;
|
|
266
|
+
case "--repo":
|
|
267
|
+
opts.repo = needValue(arg, args);
|
|
268
|
+
break;
|
|
269
|
+
case "--embed-model":
|
|
270
|
+
opts.embedModel = needValue(arg, args);
|
|
271
|
+
break;
|
|
272
|
+
case "--skip-bedrock-access":
|
|
273
|
+
opts.skipBedrockAccess = true;
|
|
274
|
+
break;
|
|
275
|
+
default: {
|
|
276
|
+
const err = new Error(`unknown flag: ${arg}`);
|
|
277
|
+
err.code = "USAGE";
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return opts;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function flagAllowed(command, arg) {
|
|
287
|
+
if (!INIT_ONLY.has(arg)) return true;
|
|
288
|
+
if (command === "init") return true;
|
|
289
|
+
if (command === "destroy") return DESTROY_FROM_INIT.has(arg);
|
|
290
|
+
if (command === "list") return LIST_FROM_INIT.has(arg);
|
|
291
|
+
if (command === "config") return arg === "--home";
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function needValue(flag, args) {
|
|
296
|
+
const value = args.shift();
|
|
297
|
+
if (!value || value.startsWith("-")) {
|
|
298
|
+
const err = new Error(`${flag} needs a value`);
|
|
299
|
+
err.code = "USAGE";
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function parseDriver(value) {
|
|
306
|
+
if (value === DRIVER_NEON || value === DRIVER_POSTGRES) return value;
|
|
307
|
+
const err = new Error(
|
|
308
|
+
`--database-driver must be ${DRIVER_NEON} or ${DRIVER_POSTGRES}`
|
|
309
|
+
);
|
|
310
|
+
err.code = "USAGE";
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function parseBool(value) {
|
|
315
|
+
if (value === "true" || value === "1") return true;
|
|
316
|
+
if (value === "false" || value === "0") return false;
|
|
317
|
+
const err = new Error(`--database-prepare must be true or false`);
|
|
318
|
+
err.code = "USAGE";
|
|
319
|
+
throw err;
|
|
320
|
+
}
|
package/src/plan.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLAUDE_IMPROVE_MODEL,
|
|
3
|
+
DEPLOY_CLI,
|
|
4
|
+
TITAN_EMBED_MODEL,
|
|
5
|
+
} from "./defaults.js";
|
|
6
|
+
|
|
7
|
+
export function deployCommand(seed) {
|
|
8
|
+
return seed ? `${DEPLOY_CLI} --seed` : DEPLOY_CLI;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatDryRun(plan) {
|
|
12
|
+
const access = plan.requestBedrockAccess
|
|
13
|
+
? "request access for all Titan/Cohere embeddings (including new ids Bedrock lists; users pick later in the app)"
|
|
14
|
+
: "skip requesting access";
|
|
15
|
+
const embedDefault = plan.embedModelId
|
|
16
|
+
? `${plan.embedModelId} (--embed-model)`
|
|
17
|
+
: `${TITAN_EMBED_MODEL} (CDK default)`;
|
|
18
|
+
const models = plan.embeddingModels?.length
|
|
19
|
+
? ` ${plan.embeddingModels.map((m) => m.id).join(", ")}`
|
|
20
|
+
: null;
|
|
21
|
+
const lines = [
|
|
22
|
+
"Plan (dry-run — nothing will be written, nothing will be deployed)",
|
|
23
|
+
"",
|
|
24
|
+
` 1. Local tools: Node 20+, npm, AWS CLI v2, Docker, optional gh`,
|
|
25
|
+
plan.dockerInstalled && plan.dockerDaemon === false
|
|
26
|
+
? " docker daemon is not running — start it before deploy"
|
|
27
|
+
: null,
|
|
28
|
+
` 2. AWS account in ${plan.region} (smooth path)`,
|
|
29
|
+
plan.account
|
|
30
|
+
? ` account ${plan.account}`
|
|
31
|
+
: " aws sts get-caller-identity not confirmed",
|
|
32
|
+
plan.awsProfile
|
|
33
|
+
? ` profile ${plan.awsProfile}`
|
|
34
|
+
: plan.awsProfiles?.length > 1
|
|
35
|
+
? ` would ask which profile: ${plan.awsProfiles.join(", ")}`
|
|
36
|
+
: plan.hasAwsKeys
|
|
37
|
+
? " using AWS access keys (written to the secrets file)"
|
|
38
|
+
: " would ask for AWS access key and secret",
|
|
39
|
+
` 3. CDK bootstrap: ${bootstrapLabel(plan)}`,
|
|
40
|
+
` 4. Bedrock embeddings: ${access}`,
|
|
41
|
+
models,
|
|
42
|
+
` default: ${embedDefault}`,
|
|
43
|
+
` Claude (${CLAUDE_IMPROVE_MODEL}) for Improve — wiki is paused; skip`,
|
|
44
|
+
plan.repository
|
|
45
|
+
? ` 5. Amplify: watch ${plan.repository}`
|
|
46
|
+
: " 5. Amplify: skipped (stack only — no GitHub-watched web app)",
|
|
47
|
+
plan.repository
|
|
48
|
+
? " written as REPOSITORY in the secrets file (CDK reads it as context)"
|
|
49
|
+
: " a found-the-repo operator does not watch this checkout by default",
|
|
50
|
+
plan.createRds
|
|
51
|
+
? " 6. Postgres: CDK creates RDS (db.t3.micro, public) — no DATABASE_URL in the secrets file"
|
|
52
|
+
: ` 6. Postgres: DATABASE_URL ${plan.hasDatabaseUrl ? "provided" : "missing"}, driver ${plan.databaseDriver}, prepare ${
|
|
53
|
+
plan.databasePrepare ? "true" : "false"
|
|
54
|
+
}`,
|
|
55
|
+
` 7. Generate BETTER_AUTH_SECRET, MCP_TOKEN_PEPPER, CTX_TOKEN (not printed)`,
|
|
56
|
+
` APP_MODE=self_hosted ALLOW_PUBLIC_SIGNUP=false BILLING_ENABLED=false`,
|
|
57
|
+
` BETTER_AUTH_URL / APP_URL omitted — CDK uses the Amplify default domain`,
|
|
58
|
+
" or a domain you own. Never the hosted Context101 product.",
|
|
59
|
+
` 8. Write ${plan.envDisplay} (chmod 600)`,
|
|
60
|
+
plan.envExists
|
|
61
|
+
? " file already exists — dry-run would refuse without --force"
|
|
62
|
+
: " file does not exist yet",
|
|
63
|
+
` 9. Next: ${deployCommand(plan.seed)}`,
|
|
64
|
+
" first deploy: add --seed to upload knowledge/ once",
|
|
65
|
+
plan.repository
|
|
66
|
+
? " 10. After web is up: /setup on the Amplify domain (first admin)"
|
|
67
|
+
: " 10. Amplify skipped — run web/ locally, or re-run with --repo to watch a GitHub repo",
|
|
68
|
+
"",
|
|
69
|
+
`Would write: ${plan.envDisplay}`,
|
|
70
|
+
plan.deploy
|
|
71
|
+
? `Would deploy after writing: ${deployCommand(plan.seed)}`
|
|
72
|
+
: "Would not deploy.",
|
|
73
|
+
];
|
|
74
|
+
return lines.filter((line) => line !== null).join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function bootstrapLabel(plan) {
|
|
78
|
+
if (plan.bootstrapped === true) return `already done in ${plan.region}`;
|
|
79
|
+
if (plan.bootstrapped === false && plan.account) {
|
|
80
|
+
return `needed — npx cdk bootstrap aws://${plan.account}/${plan.region}`;
|
|
81
|
+
}
|
|
82
|
+
return "not checked (no AWS identity)";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function nextSteps(plan) {
|
|
86
|
+
const rds = plan.createRds
|
|
87
|
+
? "CDK will create RDS Postgres and apply the control-plane schema. Fetch the secret via ControlPlaneDbSecretArn — the password is never printed."
|
|
88
|
+
: null;
|
|
89
|
+
const next = [
|
|
90
|
+
`Next: ${deployCommand(plan.seed)}`,
|
|
91
|
+
plan.seed ? null : "First time? add --seed to upload the example knowledge/ files once.",
|
|
92
|
+
rds,
|
|
93
|
+
];
|
|
94
|
+
if (plan.repository) {
|
|
95
|
+
return [
|
|
96
|
+
...next,
|
|
97
|
+
"After Amplify is up (~4 min), open /setup on WebAppDefaultDomain (or your own domain).",
|
|
98
|
+
"Leave BETTER_AUTH_URL / APP_URL unset unless you bring your own host. CDK fills the Amplify default.",
|
|
99
|
+
]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join("\n");
|
|
102
|
+
}
|
|
103
|
+
return [
|
|
104
|
+
...next,
|
|
105
|
+
"Amplify is skipped — this deploy is the AWS stack only (no GitHub-watched web app).",
|
|
106
|
+
"Run the web app from web/, or re-run init with --repo when you want Amplify.",
|
|
107
|
+
"Leave BETTER_AUTH_URL / APP_URL unset unless you bring your own host.",
|
|
108
|
+
]
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join("\n");
|
|
111
|
+
}
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DRIVER_NEON,
|
|
3
|
+
DRIVER_POSTGRES,
|
|
4
|
+
SMOOTH_REGION,
|
|
5
|
+
} from "./defaults.js";
|
|
6
|
+
import { inferDriver, inferPrepare } from "./env-file.js";
|
|
7
|
+
import { normalizeRepoUrl } from "./repo.js";
|
|
8
|
+
|
|
9
|
+
export async function promptAnswers({ defaults, io, exec, env }) {
|
|
10
|
+
const { confirm, input, password, select } = await import("@inquirer/prompts");
|
|
11
|
+
|
|
12
|
+
io.write("This writes a local secrets file. Deploy afterwards with:");
|
|
13
|
+
io.write(" context101 deploy");
|
|
14
|
+
io.write("Press ^C to quit.");
|
|
15
|
+
io.write("");
|
|
16
|
+
|
|
17
|
+
const region = await input({
|
|
18
|
+
message: "AWS region",
|
|
19
|
+
default: defaults.region ?? SMOOTH_REGION,
|
|
20
|
+
});
|
|
21
|
+
if (region !== SMOOTH_REGION) {
|
|
22
|
+
io.warn(`${SMOOTH_REGION} is the smooth path (S3 Vectors + Bedrock).`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const watchByDefault = Boolean(defaults.repository);
|
|
26
|
+
const amplifyMode = await select({
|
|
27
|
+
message: "Amplify frontend",
|
|
28
|
+
default: watchByDefault ? "watch" : "skip",
|
|
29
|
+
choices: [
|
|
30
|
+
{
|
|
31
|
+
name: "Skip — deploy the stack only (no GitHub-watched web app)",
|
|
32
|
+
value: "skip",
|
|
33
|
+
},
|
|
34
|
+
{ name: "Watch a GitHub repo", value: "watch" },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let repository = "";
|
|
39
|
+
if (amplifyMode === "watch") {
|
|
40
|
+
repository = normalizeRepoUrl(
|
|
41
|
+
await input({
|
|
42
|
+
message: "GitHub repo Amplify should watch",
|
|
43
|
+
default: defaults.repository || defaults.suggestedRepo || "",
|
|
44
|
+
validate: (value) =>
|
|
45
|
+
value ? true : "needed if Amplify should watch a repo",
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dbMode = await select({
|
|
51
|
+
message: "Postgres control plane",
|
|
52
|
+
default: defaults.databaseUrl ? "url" : "rds",
|
|
53
|
+
choices: [
|
|
54
|
+
{
|
|
55
|
+
name: "Create RDS — CDK provisions Postgres (db.t3.micro, public)",
|
|
56
|
+
value: "rds",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "I have a DATABASE_URL (Neon / Supabase / existing Postgres)",
|
|
60
|
+
value: "url",
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let databaseUrl = "";
|
|
66
|
+
let createRds = dbMode === "rds";
|
|
67
|
+
let databaseDriver = DRIVER_POSTGRES;
|
|
68
|
+
let databasePrepare = true;
|
|
69
|
+
if (dbMode === "url") {
|
|
70
|
+
databaseUrl = await password({
|
|
71
|
+
message: "DATABASE_URL",
|
|
72
|
+
mask: true,
|
|
73
|
+
validate: (value) =>
|
|
74
|
+
value ? true : "needed unless CDK creates RDS",
|
|
75
|
+
});
|
|
76
|
+
createRds = false;
|
|
77
|
+
const inferredDriver = inferDriver(databaseUrl);
|
|
78
|
+
databaseDriver = await select({
|
|
79
|
+
message: "DATABASE_DRIVER",
|
|
80
|
+
default: inferredDriver,
|
|
81
|
+
choices: [
|
|
82
|
+
{ name: `${DRIVER_NEON} (Neon)`, value: DRIVER_NEON },
|
|
83
|
+
{ name: `${DRIVER_POSTGRES} (Supabase / RDS / local)`, value: DRIVER_POSTGRES },
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
databasePrepare = await confirm({
|
|
87
|
+
message: "DATABASE_PREPARE (false for Supabase transaction pooler)",
|
|
88
|
+
default: inferPrepare(databaseUrl),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
region,
|
|
94
|
+
repository,
|
|
95
|
+
embedModelId: defaults.embedModelId || "",
|
|
96
|
+
createRds,
|
|
97
|
+
databaseUrl,
|
|
98
|
+
databaseDriver,
|
|
99
|
+
databasePrepare,
|
|
100
|
+
awsProfile: defaults.awsProfile ?? null,
|
|
101
|
+
awsAccessKeyId: defaults.awsAccessKeyId ?? null,
|
|
102
|
+
awsSecretAccessKey: defaults.awsSecretAccessKey ?? null,
|
|
103
|
+
};
|
|
104
|
+
}
|
package/src/redact.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SECRET_KEYS } from "./defaults.js";
|
|
2
|
+
|
|
3
|
+
export function mask(value) {
|
|
4
|
+
if (!value) return "(empty)";
|
|
5
|
+
if (value.length <= 8) return "…";
|
|
6
|
+
return `${value.slice(0, 4)}…${value.slice(-4)}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function collectSecrets(values = {}) {
|
|
10
|
+
const secrets = [];
|
|
11
|
+
for (const key of SECRET_KEYS) {
|
|
12
|
+
const value = values[key];
|
|
13
|
+
if (typeof value === "string" && value.length > 0) secrets.push(value);
|
|
14
|
+
}
|
|
15
|
+
return secrets;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function outputContainsSecret(text, secrets) {
|
|
19
|
+
if (!text) return false;
|
|
20
|
+
return secrets.some((secret) => secret && text.includes(secret));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function assertNoSecrets(text, secrets, label = "output") {
|
|
24
|
+
const hit = (secrets || []).find((secret) => secret && text.includes(secret));
|
|
25
|
+
if (hit) {
|
|
26
|
+
throw new Error(`${label} leaked a secret`);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/repo.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DEFAULT_AMPLIFY_REPO, HOME_ENV_REL, REPO_ENV_REL } from "./defaults.js";
|
|
5
|
+
|
|
6
|
+
export function isContext101Checkout(dir, exists = existsSync) {
|
|
7
|
+
const web = exists(path.join(dir, "web", "package.json"));
|
|
8
|
+
const cdk =
|
|
9
|
+
exists(path.join(dir, "cdk", "cdk.json")) ||
|
|
10
|
+
exists(path.join(dir, "cdk", "bin", "context101.ts")) ||
|
|
11
|
+
exists(path.join(dir, "cdk", "deploy.sh"));
|
|
12
|
+
return web && cdk;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function findRepoRoot(startDir, exists = existsSync) {
|
|
16
|
+
let dir = path.resolve(startDir);
|
|
17
|
+
for (;;) {
|
|
18
|
+
if (isContext101Checkout(dir, exists)) return dir;
|
|
19
|
+
const parent = path.dirname(dir);
|
|
20
|
+
if (parent === dir) return null;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function defaultEnvPath(repoRoot, { home = false } = {}) {
|
|
26
|
+
if (home) return path.join(homedir(), HOME_ENV_REL);
|
|
27
|
+
return path.join(repoRoot, ...REPO_ENV_REL.split("/"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveEnvPath(repoRoot, opts) {
|
|
31
|
+
if (opts.envFile) {
|
|
32
|
+
return path.isAbsolute(opts.envFile)
|
|
33
|
+
? opts.envFile
|
|
34
|
+
: path.resolve(opts.cwd ?? repoRoot, opts.envFile);
|
|
35
|
+
}
|
|
36
|
+
return defaultEnvPath(repoRoot, { home: opts.home });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeRepoUrl(raw) {
|
|
40
|
+
if (!raw) return "";
|
|
41
|
+
const trimmed = raw.trim();
|
|
42
|
+
const ssh = trimmed.match(/^git@github\.com:(.+?)(?:\.git)?$/);
|
|
43
|
+
if (ssh) return `https://github.com/${ssh[1]}`;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = new URL(trimmed);
|
|
46
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
47
|
+
parsed.username = "";
|
|
48
|
+
parsed.password = "";
|
|
49
|
+
return parsed.toString().replace(/\.git\/?$/, "").replace(/\/$/, "");
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// not a URL
|
|
53
|
+
}
|
|
54
|
+
return trimmed.replace(/\.git$/, "");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function detectGitRemote(exec, repoRoot) {
|
|
58
|
+
const result = exec({
|
|
59
|
+
command: "git",
|
|
60
|
+
args: ["-C", repoRoot, "remote", "get-url", "origin"],
|
|
61
|
+
});
|
|
62
|
+
if (!result.ok) return "";
|
|
63
|
+
return normalizeRepoUrl(result.stdout.split("\n")[0] || "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function readHardcodedRepo(stackSource) {
|
|
67
|
+
const match = stackSource.match(/repository:\s*"(https:\/\/github\.com\/[^"]+)"/);
|
|
68
|
+
return match ? match[1] : DEFAULT_AMPLIFY_REPO;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function displayEnvPath(filePath, repoRoot) {
|
|
72
|
+
if (filePath.startsWith(repoRoot + path.sep)) {
|
|
73
|
+
return path.relative(repoRoot, filePath);
|
|
74
|
+
}
|
|
75
|
+
if (filePath.startsWith(homedir())) {
|
|
76
|
+
return `~${filePath.slice(homedir().length)}`;
|
|
77
|
+
}
|
|
78
|
+
return filePath;
|
|
79
|
+
}
|