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/src/init.js ADDED
@@ -0,0 +1,435 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ ALLOW_PUBLIC_SIGNUP,
5
+ APP_MODE,
6
+ BILLING_ENABLED,
7
+ DRIVER_POSTGRES,
8
+ EXAMPLE_ENV_REL,
9
+ SMOOTH_REGION,
10
+ } from "./defaults.js";
11
+ import { defaultAmplifyRepository, detectGithubLogin } from "./amplify-repo.js";
12
+ import {
13
+ classifyGithubToken,
14
+ githubTokenWorksForAmplify,
15
+ printChecks,
16
+ runChecks,
17
+ } from "./checks.js";
18
+ import {
19
+ formatAccessResult,
20
+ requestEmbeddingModelAccess,
21
+ } from "./bedrock-access.js";
22
+ import {
23
+ isKnownEmbeddingModel,
24
+ listEmbeddingModels,
25
+ } from "./embedding-models.js";
26
+ import {
27
+ inferDriver,
28
+ inferPrepare,
29
+ readExampleToken,
30
+ writeDeployEnv,
31
+ } from "./env-file.js";
32
+ import { startDeploy } from "./deploy.js";
33
+ import { createExec } from "./exec.js";
34
+ import { formatDryRun, nextSteps } from "./plan.js";
35
+ import { ensureRepoRoot } from "./clone.js";
36
+ import {
37
+ detectGitRemote,
38
+ displayEnvPath,
39
+ findRepoRoot,
40
+ normalizeRepoUrl,
41
+ resolveEnvPath,
42
+ } from "./repo.js";
43
+ import { generateCtxToken, generateSecret } from "./secrets.js";
44
+ import { banner, writers } from "./style.js";
45
+ import { listAwsProfiles, resolveAwsAuth } from "./aws-profiles.js";
46
+
47
+ export async function runInit(opts, ctx) {
48
+ const io = writers(ctx);
49
+ const exec = ctx.exec ?? createExec(ctx.env);
50
+
51
+ banner(ctx);
52
+ if (opts.dryRun) {
53
+ io.dim("dry-run — no files, no secrets written, no deploy");
54
+ io.write("");
55
+ }
56
+
57
+ const checkout = ensureRepoRoot({
58
+ cwd: ctx.cwd,
59
+ dir: opts.dir,
60
+ exec,
61
+ io,
62
+ dryRun: opts.dryRun,
63
+ });
64
+ if (checkout.error) {
65
+ io.err(checkout.error);
66
+ return 1;
67
+ }
68
+ const repoRoot = checkout.repoRoot;
69
+ if (!repoRoot || (!opts.dryRun && !findRepoRoot(repoRoot))) {
70
+ io.err("run this from a Context101 checkout (needs cdk/ and web/).");
71
+ return 1;
72
+ }
73
+ if (checkout.wouldClone && opts.dryRun) {
74
+ io.write("");
75
+ }
76
+
77
+ const region = opts.region ?? SMOOTH_REGION;
78
+ const env = ctx.env ?? {};
79
+ const tty = Boolean(ctx.stdin && ctx.stdin.isTTY && ctx.stdout && ctx.stdout.isTTY);
80
+ const profiles = listAwsProfiles({ exec, env });
81
+ const resolved = resolveAwsAuth({
82
+ explicitProfile: opts.awsProfile || env.AWS_PROFILE || null,
83
+ accessKeyId: opts.awsAccessKeyId || env.AWS_ACCESS_KEY_ID || null,
84
+ secretAccessKey: opts.awsSecretAccessKey || env.AWS_SECRET_ACCESS_KEY || null,
85
+ profiles,
86
+ yes: opts.yes,
87
+ dryRun: opts.dryRun,
88
+ });
89
+ let awsProfile = resolved.profile;
90
+ let awsAccessKeyId = resolved.accessKeyId;
91
+ let awsSecretAccessKey = resolved.secretAccessKey;
92
+ if (resolved.source === "ask-profile" && !opts.dryRun) {
93
+ if (resolved.error && (opts.yes || !tty)) {
94
+ io.err(resolved.error);
95
+ return 1;
96
+ }
97
+ if (!tty) {
98
+ io.err(
99
+ `multiple AWS profiles (${profiles.join(", ")}). Pass --aws-profile or run in a TTY.`
100
+ );
101
+ return 1;
102
+ }
103
+ awsProfile = await (ctx.chooseProfile ?? chooseAwsProfile)(profiles, {
104
+ current: opts.awsProfile || env.AWS_PROFILE || null,
105
+ });
106
+ }
107
+ if (resolved.source === "ask-keys" && !opts.dryRun) {
108
+ if (resolved.error && (opts.yes || !tty)) {
109
+ io.err(resolved.error);
110
+ return 1;
111
+ }
112
+ if (!tty) {
113
+ io.err(
114
+ "no AWS profiles. Pass --aws-access-key-id and --aws-secret-access-key, or configure a profile."
115
+ );
116
+ return 1;
117
+ }
118
+ const keys = await (ctx.promptAwsKeys ?? promptAwsKeys)();
119
+ awsAccessKeyId = keys.accessKeyId;
120
+ awsSecretAccessKey = keys.secretAccessKey;
121
+ }
122
+ const awsEnv = withAwsAuth(env, {
123
+ profile: awsProfile,
124
+ accessKeyId: awsAccessKeyId,
125
+ secretAccessKey: awsSecretAccessKey,
126
+ });
127
+ const checks = runChecks({
128
+ exec,
129
+ env: awsEnv,
130
+ region,
131
+ dryRun: opts.dryRun,
132
+ });
133
+ printChecks(
134
+ {
135
+ ...checks,
136
+ awsProfile,
137
+ awsProfiles: profiles,
138
+ hasAwsKeys: Boolean(awsAccessKeyId && awsSecretAccessKey),
139
+ awsAuthSource: resolved.source,
140
+ },
141
+ io
142
+ );
143
+ if (checks.docker?.hint && !checks.docker.daemon) {
144
+ io.write(checks.docker.hint);
145
+ }
146
+ if (checks.bootstrap.ok === false && checks.aws.identity?.account) {
147
+ io.warn(
148
+ `CDK bootstrap needed: npx cdk bootstrap aws://${checks.aws.identity.account}/${region}`
149
+ );
150
+ }
151
+ io.write("");
152
+
153
+ let answers;
154
+ try {
155
+ answers = await collectAnswers(opts, {
156
+ repoRoot,
157
+ checks,
158
+ exec,
159
+ io,
160
+ env,
161
+ awsEnv,
162
+ tty,
163
+ awsProfile,
164
+ awsAccessKeyId,
165
+ awsSecretAccessKey,
166
+ promptAnswers: ctx.promptAnswers,
167
+ });
168
+ } catch (error) {
169
+ if (error && error.code === "USAGE") {
170
+ io.err(error.message);
171
+ return 1;
172
+ }
173
+ throw error;
174
+ }
175
+ if (answers === null) return 1;
176
+
177
+ const envPath = resolveEnvPath(repoRoot, {
178
+ envFile: answers.envFile ?? opts.envFile,
179
+ home: answers.home ?? opts.home,
180
+ cwd: ctx.cwd,
181
+ });
182
+ const envDisplay = displayEnvPath(envPath, repoRoot);
183
+ const envExists = existsSync(envPath);
184
+
185
+ const plan = {
186
+ region: answers.region,
187
+ account: checks.aws.identity?.account ?? "",
188
+ awsProfile,
189
+ awsProfiles: profiles,
190
+ hasAwsKeys: Boolean(awsAccessKeyId && awsSecretAccessKey),
191
+ bootstrapped: checks.bootstrap.ok,
192
+ repository: answers.repository,
193
+ embedModelId: answers.embedModelId || "",
194
+ embeddingModels: answers.embeddingModels || [],
195
+ requestBedrockAccess: Boolean(answers.requestBedrockAccess),
196
+ hasDatabaseUrl: Boolean(answers.databaseUrl),
197
+ createRds: Boolean(answers.createRds),
198
+ databaseDriver: answers.databaseDriver,
199
+ databasePrepare: answers.databasePrepare,
200
+ envDisplay,
201
+ envExists,
202
+ seed: answers.seed,
203
+ deploy: answers.deploy,
204
+ dockerInstalled: Boolean(checks.docker?.installed),
205
+ dockerDaemon: Boolean(checks.docker?.daemon),
206
+ };
207
+
208
+ if (opts.dryRun) {
209
+ io.write(formatDryRun(plan));
210
+ io.write("");
211
+ return 0;
212
+ }
213
+
214
+ if (envExists && !opts.force) {
215
+ io.err(`${envDisplay} already exists. Re-run with --force to overwrite.`);
216
+ return 1;
217
+ }
218
+
219
+ const secrets = {
220
+ CTX_TOKEN: generateCtxToken(),
221
+ BETTER_AUTH_SECRET: generateSecret(),
222
+ MCP_TOKEN_PEPPER: generateSecret(),
223
+ ...(answers.createRds ? {} : { DATABASE_URL: answers.databaseUrl }),
224
+ };
225
+ if (answers.ghToken) secrets.CTX_GH_TOKEN = answers.ghToken;
226
+
227
+ const exampleToken = await readExampleToken(
228
+ path.join(repoRoot, ...EXAMPLE_ENV_REL.split("/"))
229
+ );
230
+ if (exampleToken && secrets.CTX_TOKEN === exampleToken) {
231
+ io.err("refusing to write the example CTX_TOKEN — generated a collision; re-run.");
232
+ return 1;
233
+ }
234
+
235
+ const values = {
236
+ ...secrets,
237
+ AWS_PROFILE: answers.awsProfile,
238
+ AWS_ACCESS_KEY_ID: answers.awsAccessKeyId,
239
+ AWS_SECRET_ACCESS_KEY: answers.awsSecretAccessKey,
240
+ AWS_REGION: answers.region,
241
+ DATABASE_DRIVER: answers.databaseDriver,
242
+ DATABASE_PREPARE: answers.databasePrepare,
243
+ CREATE_RDS: answers.createRds ? "true" : "",
244
+ APP_MODE,
245
+ ALLOW_PUBLIC_SIGNUP,
246
+ BILLING_ENABLED,
247
+ REPOSITORY: answers.repository || "",
248
+ EMBED_MODEL_ID: answers.embedModelId || "",
249
+ };
250
+
251
+ await writeDeployEnv(envPath, values);
252
+
253
+ io.ok(`wrote ${envDisplay} (chmod 600)`);
254
+ if (answers.requestBedrockAccess) {
255
+ printBedrockAccess(
256
+ requestEmbeddingModelAccess({
257
+ exec,
258
+ env: awsEnv,
259
+ region: answers.region,
260
+ models: answers.embeddingModels,
261
+ }),
262
+ io
263
+ );
264
+ }
265
+ if (answers.repository && !githubReadyForAmplify(checks, answers)) {
266
+ io.warn(
267
+ "no Amplify-capable GitHub PAT yet — set CTX_GH_TOKEN to a ghp_ or github_pat_ token before deploy"
268
+ );
269
+ }
270
+ io.write("");
271
+ io.write(nextSteps(plan));
272
+ io.write("");
273
+
274
+ if (!answers.deploy) return 0;
275
+
276
+ if (answers.repository && !githubReadyForAmplify(checks, answers)) {
277
+ io.err(
278
+ "not deploying: Amplify needs a GitHub PAT (ghp_ / github_pat_). Installation and gh OAuth tokens cannot create repo webhooks and will roll the stack back."
279
+ );
280
+ return 1;
281
+ }
282
+
283
+ return startDeploy({
284
+ io,
285
+ ctx,
286
+ repoRoot,
287
+ seed: answers.seed,
288
+ env: awsEnv,
289
+ home: answers.home ?? opts.home,
290
+ envFile: answers.envFile ?? opts.envFile,
291
+ dockerDaemon: Boolean(checks.docker?.daemon),
292
+ dockerHint: checks.docker?.hint,
293
+ });
294
+ }
295
+
296
+ async function collectAnswers(opts, ctx) {
297
+ const { repoRoot, exec, io, env, awsEnv, tty } = ctx;
298
+ const remote = detectGitRemote(exec, repoRoot);
299
+ const ghLogin = detectGithubLogin(exec);
300
+ const repository = defaultAmplifyRepository({
301
+ repo: opts.repo ? normalizeRepoUrl(opts.repo) : "",
302
+ ghLogin,
303
+ });
304
+ const databaseUrl = opts.databaseUrl || env.DATABASE_URL || "";
305
+ const awsProfile = ctx.awsProfile ?? null;
306
+ const awsAccessKeyId = ctx.awsAccessKeyId ?? null;
307
+ const awsSecretAccessKey = ctx.awsSecretAccessKey ?? null;
308
+ if (opts.embedModel && !isKnownEmbeddingModel(opts.embedModel)) {
309
+ const error = new Error(
310
+ `--embed-model must be a Bedrock Titan or Cohere embedding id (got ${opts.embedModel})`
311
+ );
312
+ error.code = "USAGE";
313
+ throw error;
314
+ }
315
+ const catalog = listEmbeddingModels({
316
+ exec,
317
+ env: awsEnv,
318
+ region: SMOOTH_REGION,
319
+ });
320
+
321
+ if (opts.dryRun || opts.yes) {
322
+ const createRds = !databaseUrl;
323
+ return {
324
+ region: SMOOTH_REGION,
325
+ repository,
326
+ embedModelId: opts.embedModel || "",
327
+ embeddingModels: catalog.models,
328
+ requestBedrockAccess: !opts.skipBedrockAccess,
329
+ databaseUrl: createRds ? "" : databaseUrl,
330
+ createRds,
331
+ databaseDriver:
332
+ opts.databaseDriver ||
333
+ (createRds ? DRIVER_POSTGRES : inferDriver(databaseUrl)),
334
+ databasePrepare:
335
+ opts.databasePrepare == null
336
+ ? createRds
337
+ ? true
338
+ : inferPrepare(databaseUrl)
339
+ : opts.databasePrepare,
340
+ awsProfile,
341
+ awsAccessKeyId,
342
+ awsSecretAccessKey,
343
+ home: opts.home,
344
+ envFile: opts.envFile,
345
+ seed: opts.seed,
346
+ deploy: Boolean(opts.deploy && opts.yes && !opts.dryRun),
347
+ ghToken: null,
348
+ };
349
+ }
350
+
351
+ if (!tty) {
352
+ io.err("not a TTY. Re-run with --yes or --dry-run.");
353
+ return null;
354
+ }
355
+
356
+ const prompt = ctx.promptAnswers ?? (await import("./prompt.js")).promptAnswers;
357
+ const prompted = await prompt({
358
+ defaults: {
359
+ repoRoot,
360
+ region: SMOOTH_REGION,
361
+ repository,
362
+ suggestedRepo: remote,
363
+ embedModelId: opts.embedModel || "",
364
+ databaseUrl,
365
+ awsProfile,
366
+ awsAccessKeyId,
367
+ awsSecretAccessKey,
368
+ },
369
+ io,
370
+ exec,
371
+ env: awsEnv,
372
+ });
373
+ return {
374
+ ...prompted,
375
+ embeddingModels: catalog.models,
376
+ requestBedrockAccess: !opts.skipBedrockAccess,
377
+ createRds: prompted.createRds ?? !prompted.databaseUrl,
378
+ home: opts.home,
379
+ envFile: opts.envFile,
380
+ seed: opts.seed,
381
+ deploy: Boolean(opts.deploy && !opts.dryRun),
382
+ ghToken: null,
383
+ };
384
+ }
385
+
386
+ function withAwsAuth(env, { profile, accessKeyId, secretAccessKey } = {}) {
387
+ const next = { ...env };
388
+ if (profile) next.AWS_PROFILE = profile;
389
+ if (accessKeyId) next.AWS_ACCESS_KEY_ID = accessKeyId;
390
+ if (secretAccessKey) next.AWS_SECRET_ACCESS_KEY = secretAccessKey;
391
+ return next;
392
+ }
393
+
394
+ function printBedrockAccess(results, io) {
395
+ io.write("Bedrock embedding access:");
396
+ for (const result of results) {
397
+ const line = formatAccessResult(result);
398
+ if (result.status === "needs-console" || result.status === "failed") {
399
+ io.warn(line);
400
+ } else {
401
+ io.dim(` ${line}`);
402
+ }
403
+ }
404
+ }
405
+
406
+ function githubReadyForAmplify(checks, answers) {
407
+ if (answers.ghToken) {
408
+ return githubTokenWorksForAmplify(classifyGithubToken(answers.ghToken));
409
+ }
410
+ return Boolean(checks.gh.amplifyOk);
411
+ }
412
+
413
+ async function chooseAwsProfile(profiles, { current } = {}) {
414
+ const { select } = await import("@inquirer/prompts");
415
+ const fallback = current && profiles.includes(current) ? current : profiles[0];
416
+ return select({
417
+ message: "AWS profile to deploy to",
418
+ default: fallback,
419
+ choices: profiles.map((name) => ({ name, value: name })),
420
+ });
421
+ }
422
+
423
+ async function promptAwsKeys() {
424
+ const { input, password } = await import("@inquirer/prompts");
425
+ const accessKeyId = await input({
426
+ message: "AWS access key ID",
427
+ validate: (value) => (value ? true : "needed to deploy"),
428
+ });
429
+ const secretAccessKey = await password({
430
+ message: "AWS secret access key",
431
+ mask: true,
432
+ validate: (value) => (value ? true : "needed to deploy"),
433
+ });
434
+ return { accessKeyId, secretAccessKey };
435
+ }
package/src/main.js ADDED
@@ -0,0 +1,41 @@
1
+ import { helpText, parseArgs } from "./parse-args.js";
2
+ import { runConfig } from "./config.js";
3
+ import { runDeploy } from "./deploy.js";
4
+ import { runInit } from "./init.js";
5
+ import { runDestroy, runList } from "./stacks.js";
6
+ import { writers } from "./style.js";
7
+
8
+ export async function main(argv, ctx) {
9
+ const io = writers(ctx);
10
+ let opts;
11
+ try {
12
+ opts = parseArgs(argv);
13
+ } catch (error) {
14
+ if (error && error.code === "USAGE") {
15
+ io.err(error.message);
16
+ io.write(helpText());
17
+ return 1;
18
+ }
19
+ throw error;
20
+ }
21
+
22
+ if (opts.help) {
23
+ io.write(helpText());
24
+ return 0;
25
+ }
26
+
27
+ if (opts.command === "deploy" || opts.command === "diff" || opts.command === "synth") {
28
+ return runDeploy(opts, ctx);
29
+ }
30
+ if (opts.command === "list") {
31
+ return runList(opts, ctx);
32
+ }
33
+ if (opts.command === "destroy") {
34
+ return runDestroy(opts, ctx);
35
+ }
36
+ if (opts.command === "config") {
37
+ return runConfig(opts, ctx);
38
+ }
39
+
40
+ return runInit(opts, ctx);
41
+ }