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.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/main.js";
3
+
4
+ const code = await main(process.argv.slice(2), {
5
+ cwd: process.cwd(),
6
+ env: process.env,
7
+ stdout: process.stdout,
8
+ stderr: process.stderr,
9
+ stdin: process.stdin,
10
+ });
11
+ process.exit(code);
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "context101-cli",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Context101 self-host CLI. Use `npx context101-cli` or the `context101` bin. The public npm package named context101 is Context7's MCP, not this tool.",
6
+ "type": "module",
7
+ "bin": {
8
+ "context101": "./bin/context101.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src"
13
+ ],
14
+ "scripts": {
15
+ "test": "node --test test/*.test.js"
16
+ },
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "dependencies": {
21
+ "@inquirer/prompts": "^7.8.4"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ }
26
+ }
@@ -0,0 +1,25 @@
1
+ import { DEFAULT_AMPLIFY_REPO } from "./defaults.js";
2
+
3
+ /** Only this GitHub login gets a default Amplify watch target. */
4
+ export const AMPLIFY_OWNER_LOGIN = "jginorio";
5
+
6
+ export function detectGithubLogin(exec) {
7
+ if (!exec) return "";
8
+ const result = exec({
9
+ command: "gh",
10
+ args: ["api", "user", "--jq", ".login"],
11
+ });
12
+ if (!result.ok) return "";
13
+ return String(result.stdout || "").trim();
14
+ }
15
+
16
+ /**
17
+ * Amplify is opt-in. A checkout of this repo must not watch
18
+ * jginorio/context101 unless that user is logged in on the machine,
19
+ * or they passed --repo.
20
+ */
21
+ export function defaultAmplifyRepository({ repo, ghLogin } = {}) {
22
+ if (repo) return repo;
23
+ if (ghLogin === AMPLIFY_OWNER_LOGIN) return DEFAULT_AMPLIFY_REPO;
24
+ return "";
25
+ }
@@ -0,0 +1,128 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function listAwsProfiles({ exec, env = {}, readFile = readFileSync } = {}) {
5
+ if (exec) {
6
+ const cli = exec({
7
+ command: "aws",
8
+ args: ["configure", "list-profiles"],
9
+ env,
10
+ });
11
+ if (cli.ok) return unique(splitLines(cli.stdout));
12
+ }
13
+
14
+ const home = env.HOME || env.USERPROFILE || "";
15
+ const credPath =
16
+ env.AWS_SHARED_CREDENTIALS_FILE ||
17
+ (home ? path.join(home, ".aws", "credentials") : "");
18
+ const configPath =
19
+ env.AWS_CONFIG_FILE || (home ? path.join(home, ".aws", "config") : "");
20
+
21
+ return unique([
22
+ ...parseAwsCredentialsProfiles(readOptional(readFile, credPath)),
23
+ ...parseAwsConfigProfiles(readOptional(readFile, configPath)),
24
+ ]);
25
+ }
26
+
27
+ export function parseAwsCredentialsProfiles(text) {
28
+ return sectionNames(text).filter((name) => !name.startsWith("sso-session"));
29
+ }
30
+
31
+ export function parseAwsConfigProfiles(text) {
32
+ const names = [];
33
+ for (const raw of sectionNames(text)) {
34
+ if (raw.startsWith("sso-session")) continue;
35
+ if (raw.startsWith("profile ")) names.push(raw.slice("profile ".length).trim());
36
+ else names.push(raw);
37
+ }
38
+ return names.filter(Boolean);
39
+ }
40
+
41
+ export function resolveAwsProfile({ explicit, profiles, yes, dryRun }) {
42
+ return resolveAwsAuth({
43
+ explicitProfile: explicit,
44
+ profiles,
45
+ yes,
46
+ dryRun,
47
+ });
48
+ }
49
+
50
+ export function resolveAwsAuth({
51
+ explicitProfile,
52
+ accessKeyId,
53
+ secretAccessKey,
54
+ profiles,
55
+ yes,
56
+ dryRun,
57
+ }) {
58
+ const list = Array.isArray(profiles) ? unique(profiles) : [];
59
+ const keyId = accessKeyId || null;
60
+ const secret = secretAccessKey || null;
61
+ const empty = {
62
+ profile: null,
63
+ accessKeyId: null,
64
+ secretAccessKey: null,
65
+ profiles: list,
66
+ };
67
+
68
+ if (explicitProfile) {
69
+ return { ...empty, profile: explicitProfile, source: "flag" };
70
+ }
71
+ if (list.length === 1) {
72
+ return { ...empty, profile: list[0], source: "only" };
73
+ }
74
+ if (list.length > 1) {
75
+ if (yes && !dryRun) {
76
+ return {
77
+ ...empty,
78
+ source: "ask-profile",
79
+ error: `multiple AWS profiles (${list.join(", ")}). Pass --aws-profile <name>.`,
80
+ };
81
+ }
82
+ return { ...empty, source: "ask-profile" };
83
+ }
84
+ if (keyId && secret) {
85
+ return { ...empty, accessKeyId: keyId, secretAccessKey: secret, source: "keys" };
86
+ }
87
+ if (keyId || secret) {
88
+ return {
89
+ ...empty,
90
+ source: "ask-keys",
91
+ error:
92
+ "both --aws-access-key-id and --aws-secret-access-key (or AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) are required.",
93
+ };
94
+ }
95
+ if (yes && !dryRun) {
96
+ return { ...empty, source: "default-chain" };
97
+ }
98
+ return { ...empty, source: "ask-keys" };
99
+ }
100
+
101
+ function sectionNames(text) {
102
+ if (!text) return [];
103
+ const names = [];
104
+ for (const match of String(text).matchAll(/^\[([^\]]+)\]/gm)) {
105
+ names.push(match[1].trim());
106
+ }
107
+ return names;
108
+ }
109
+
110
+ function splitLines(text) {
111
+ return String(text ?? "")
112
+ .split(/\r?\n/)
113
+ .map((line) => line.trim())
114
+ .filter(Boolean);
115
+ }
116
+
117
+ function unique(values) {
118
+ return [...new Set(values.filter(Boolean))];
119
+ }
120
+
121
+ function readOptional(readFile, filePath) {
122
+ if (!filePath) return "";
123
+ try {
124
+ return readFile(filePath, "utf8");
125
+ } catch {
126
+ return "";
127
+ }
128
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Request Bedrock model access for every embedding model we support.
3
+ * Amazon first-party models are auto-enabled. Cohere (Marketplace) needs
4
+ * a foundation-model agreement. Never print offer tokens.
5
+ */
6
+
7
+ export function parseAvailability(payload) {
8
+ const status = payload?.agreementAvailability?.status;
9
+ const entitlement = payload?.entitlementAvailability;
10
+ return {
11
+ available: status === "AVAILABLE" || entitlement === "AVAILABLE",
12
+ status: status || entitlement || "unknown",
13
+ };
14
+ }
15
+
16
+ export function pickOfferToken(payload) {
17
+ const offers = Array.isArray(payload?.offers) ? payload.offers : [];
18
+ const pub = offers.find((offer) => offer.offerType === "PUBLIC") || offers[0];
19
+ return pub?.offerToken ? String(pub.offerToken) : "";
20
+ }
21
+
22
+ function awsJson(exec, env, args) {
23
+ const result = exec({
24
+ command: "aws",
25
+ args,
26
+ env,
27
+ });
28
+ if (!result.ok) {
29
+ return { ok: false, error: result.stderr || result.stdout || "aws failed", data: null };
30
+ }
31
+ try {
32
+ return { ok: true, error: null, data: JSON.parse(result.stdout || "{}") };
33
+ } catch {
34
+ return { ok: false, error: "could not parse aws json", data: null };
35
+ }
36
+ }
37
+
38
+ export function requestEmbeddingModelAccess({
39
+ exec,
40
+ env,
41
+ region,
42
+ models,
43
+ dryRun = false,
44
+ } = {}) {
45
+ const results = [];
46
+ for (const model of models || []) {
47
+ if (dryRun) {
48
+ results.push({ id: model.id, status: "would-request" });
49
+ continue;
50
+ }
51
+ if (model.provider === "aws") {
52
+ results.push({ id: model.id, status: "first-party" });
53
+ continue;
54
+ }
55
+
56
+ const availability = awsJson(exec, env, [
57
+ "bedrock",
58
+ "get-foundation-model-availability",
59
+ "--model-id",
60
+ model.id,
61
+ "--region",
62
+ region,
63
+ "--output",
64
+ "json",
65
+ ]);
66
+ if (availability.ok && parseAvailability(availability.data).available) {
67
+ results.push({ id: model.id, status: "already-available" });
68
+ continue;
69
+ }
70
+
71
+ const offers = awsJson(exec, env, [
72
+ "bedrock",
73
+ "list-foundation-model-agreement-offers",
74
+ "--model-id",
75
+ model.id,
76
+ "--region",
77
+ region,
78
+ "--output",
79
+ "json",
80
+ ]);
81
+ const token = offers.ok ? pickOfferToken(offers.data) : "";
82
+ if (!token) {
83
+ results.push({
84
+ id: model.id,
85
+ status: "needs-console",
86
+ detail: "no Marketplace offer (enable in console → Model access)",
87
+ });
88
+ continue;
89
+ }
90
+
91
+ const created = exec({
92
+ command: "aws",
93
+ args: [
94
+ "bedrock",
95
+ "create-foundation-model-agreement",
96
+ "--model-id",
97
+ model.id,
98
+ "--offer-token",
99
+ token,
100
+ "--region",
101
+ region,
102
+ ],
103
+ env,
104
+ });
105
+ if (created.ok) {
106
+ results.push({ id: model.id, status: "granted" });
107
+ continue;
108
+ }
109
+ const err = `${created.stderr || created.stdout || ""}`;
110
+ if (/already|exist|Conflict/i.test(err)) {
111
+ results.push({ id: model.id, status: "already-available" });
112
+ continue;
113
+ }
114
+ results.push({
115
+ id: model.id,
116
+ status: "needs-console",
117
+ detail: "could not create agreement — enable in console → Model access",
118
+ });
119
+ }
120
+ return results;
121
+ }
122
+
123
+ export function formatAccessResult(result) {
124
+ if (result.status === "first-party") return `${result.id} Amazon (auto-enabled)`;
125
+ if (result.status === "already-available") return `${result.id} already available`;
126
+ if (result.status === "granted") return `${result.id} access granted`;
127
+ if (result.status === "would-request") return `${result.id} would request`;
128
+ return `${result.id} ${result.detail || "enable in console → Model access"}`;
129
+ }
@@ -0,0 +1,231 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import {
4
+ classifyGithubToken,
5
+ githubTokenWorksForAmplify,
6
+ } from "./checks.js";
7
+ import { findDeployEnvPath, readDeployEnvFile } from "./deploy-env-load.js";
8
+ import { isHostedContext101Url } from "./hosted-url.js";
9
+ import { mask } from "./redact.js";
10
+
11
+ export const CONTEXT_KEYS = [
12
+ "DATABASE_URL",
13
+ "DATABASE_DRIVER",
14
+ "DATABASE_PREPARE",
15
+ "BETTER_AUTH_SECRET",
16
+ "BETTER_AUTH_URL",
17
+ "MCP_TOKEN_PEPPER",
18
+ "APP_MODE",
19
+ "ALLOW_PUBLIC_SIGNUP",
20
+ "BILLING_ENABLED",
21
+ "APP_URL",
22
+ "MARKETING_URL",
23
+ "MCP_PUBLIC_HOST",
24
+ "MCP_DOMAIN_CERT_ARN",
25
+ "MCP_APPRUNNER",
26
+ "SES_REGION",
27
+ "SES_FROM_EMAIL",
28
+ "SES_REPLY_TO_EMAIL",
29
+ "REPOSITORY",
30
+ "EMBED_MODEL_ID",
31
+ "CREATE_RDS",
32
+ ];
33
+
34
+ const SECRET_CONTEXT = new Set([
35
+ "token",
36
+ "githubToken",
37
+ "DATABASE_URL",
38
+ "BETTER_AUTH_SECRET",
39
+ "MCP_TOKEN_PEPPER",
40
+ ]);
41
+
42
+ export function resolveDeployContext({
43
+ repoRoot,
44
+ env = {},
45
+ home = false,
46
+ envFile = null,
47
+ cwd,
48
+ exec,
49
+ } = {}) {
50
+ const filePath = findDeployEnvPath({ repoRoot, envFile, home, cwd });
51
+ const file = readDeployEnvFile(filePath);
52
+ const values = { ...file.values };
53
+
54
+ const token = String(env.CTX_TOKEN || values.CTX_TOKEN || "").trim();
55
+ let githubToken = String(env.CTX_GH_TOKEN || values.CTX_GH_TOKEN || "").trim();
56
+ if (!githubToken && exec) {
57
+ const gh = exec({ command: "gh", args: ["auth", "token"], env });
58
+ if (gh.ok) githubToken = String(gh.stdout || "").trim();
59
+ }
60
+
61
+ const awsProfile = env.AWS_PROFILE || values.AWS_PROFILE || "";
62
+ const repository = String(values.REPOSITORY || env.REPOSITORY || "").trim();
63
+
64
+ return {
65
+ filePath: file.exists ? file.path : filePath,
66
+ fileExists: file.exists,
67
+ declared: file.declared,
68
+ values,
69
+ token,
70
+ githubToken,
71
+ awsProfile,
72
+ repository,
73
+ };
74
+ }
75
+
76
+ export function assertDeployTokens(context, { action = "deploy" } = {}) {
77
+ if (action === "destroy") return;
78
+ if (!context.token) {
79
+ const error = new Error(
80
+ "Missing CTX_TOKEN. Run `context101 init` or `context101 config set CTX_TOKEN=…`, then `context101 deploy`."
81
+ );
82
+ error.code = "USAGE";
83
+ throw error;
84
+ }
85
+ if (context.repository) {
86
+ const kind = classifyGithubToken(context.githubToken);
87
+ if (!githubTokenWorksForAmplify(kind)) {
88
+ const error = new Error(
89
+ "GitHub token is not a personal access token (need ghp_ or github_pat_). Amplify CreateApp calls list-repository-webhooks; ghs_ / gho_ tokens 403 and roll the stack back. Set CTX_GH_TOKEN to a classic PAT with repo scope."
90
+ );
91
+ error.code = "USAGE";
92
+ throw error;
93
+ }
94
+ }
95
+ }
96
+
97
+ export function buildCdkArgs({
98
+ action = "deploy",
99
+ seed = false,
100
+ context,
101
+ extraArgs = [],
102
+ stackName = null,
103
+ env = {},
104
+ } = {}) {
105
+ const args = [action];
106
+ if (action === "destroy" && stackName) args.push(stackName);
107
+ if (seed) args.push("-c", "seed=true");
108
+ if (context.token) args.push("-c", `token=${context.token}`);
109
+ if (context.repository && context.githubToken) {
110
+ args.push("-c", `githubToken=${context.githubToken}`);
111
+ }
112
+
113
+ for (const key of CONTEXT_KEYS) {
114
+ if (context.fileExists && !context.declared.has(key)) continue;
115
+ const value = context.fileExists
116
+ ? context.values[key]
117
+ : context.values[key] || env[key] || "";
118
+ if (!value) continue;
119
+ if (isHostedContext101Url(value)) {
120
+ if (context.fileExists && context.declared.has(key)) {
121
+ const error = new Error(
122
+ `${key} in the env file is the hosted Context101 product, not a self-host URL. Omit it so CDK uses the Amplify default domain, or set a domain you own.`
123
+ );
124
+ error.code = "USAGE";
125
+ throw error;
126
+ }
127
+ continue;
128
+ }
129
+ args.push("-c", `${key}=${value}`);
130
+ }
131
+
132
+ if (action === "deploy") args.push("--require-approval", "never");
133
+ if (action === "destroy") args.push("--force");
134
+ args.push(...extraArgs);
135
+ return args;
136
+ }
137
+
138
+ export function formatCdkPreview({ action, context, args, seed }) {
139
+ const lines = [`cdk ${action}`];
140
+ if (context.fileExists) {
141
+ lines.push(` env file: ${displayEnv(context.filePath)}`);
142
+ }
143
+ if (context.awsProfile) lines.push(` AWS_PROFILE: ${context.awsProfile}`);
144
+ lines.push(` token: ${mask(context.token)}`);
145
+ if (context.repository) {
146
+ lines.push(` githubToken: ${mask(context.githubToken)}`);
147
+ } else {
148
+ lines.push(" githubToken: (skipped — no REPOSITORY)");
149
+ }
150
+ for (const [flag, value] of contextPairs(args)) {
151
+ if (flag === "token" || flag === "githubToken" || flag === "seed") continue;
152
+ const shown = SECRET_CONTEXT.has(flag) ? `${flag}: ${mask(value)}` : `${flag}: ${value}`;
153
+ lines.push(` ${shown}`);
154
+ }
155
+ if (seed) lines.push(" seed: true");
156
+ return lines.join("\n");
157
+ }
158
+
159
+ function contextPairs(args) {
160
+ const pairs = [];
161
+ for (let i = 0; i < args.length; i += 1) {
162
+ if (args[i] === "-c" && args[i + 1]) {
163
+ const raw = args[i + 1];
164
+ const eq = raw.indexOf("=");
165
+ if (eq > 0) pairs.push([raw.slice(0, eq), raw.slice(eq + 1)]);
166
+ i += 1;
167
+ }
168
+ }
169
+ return pairs;
170
+ }
171
+
172
+ function displayEnv(filePath) {
173
+ if (!filePath) return "";
174
+ const home = `${process.env.HOME || ""}/.context101/deploy-env`;
175
+ if (filePath === home || filePath.endsWith("/.context101/deploy-env")) {
176
+ return "~/.context101/deploy-env";
177
+ }
178
+ if (filePath.endsWith("/cdk/.deploy-env") || filePath.endsWith("cdk/.deploy-env")) {
179
+ return "cdk/.deploy-env";
180
+ }
181
+ return filePath;
182
+ }
183
+
184
+ export function runCdk({
185
+ repoRoot,
186
+ action = "deploy",
187
+ seed = false,
188
+ extraArgs = [],
189
+ stackName = null,
190
+ env = {},
191
+ home = false,
192
+ envFile = null,
193
+ cwd,
194
+ exec,
195
+ stdio = "inherit",
196
+ } = {}) {
197
+ const context = resolveDeployContext({
198
+ repoRoot,
199
+ env,
200
+ home,
201
+ envFile,
202
+ cwd,
203
+ exec,
204
+ });
205
+ assertDeployTokens(context, { action });
206
+ const args = buildCdkArgs({
207
+ action,
208
+ seed,
209
+ context,
210
+ extraArgs,
211
+ stackName,
212
+ env,
213
+ });
214
+ const childEnv = { ...env };
215
+ if (context.awsProfile) childEnv.AWS_PROFILE = context.awsProfile;
216
+ if (context.values.AWS_ACCESS_KEY_ID && !childEnv.AWS_ACCESS_KEY_ID) {
217
+ childEnv.AWS_ACCESS_KEY_ID = context.values.AWS_ACCESS_KEY_ID;
218
+ }
219
+ if (context.values.AWS_SECRET_ACCESS_KEY && !childEnv.AWS_SECRET_ACCESS_KEY) {
220
+ childEnv.AWS_SECRET_ACCESS_KEY = context.values.AWS_SECRET_ACCESS_KEY;
221
+ }
222
+ return new Promise((resolve, reject) => {
223
+ const child = spawn("npx", ["cdk", ...args], {
224
+ cwd: path.join(repoRoot, "cdk"),
225
+ env: childEnv,
226
+ stdio,
227
+ });
228
+ child.on("error", reject);
229
+ child.on("exit", (code) => resolve(code ?? 1));
230
+ });
231
+ }