@usecontextlayer/cli 0.14.2 → 0.14.3

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/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { S as execa, _ as record, a as getAccessToken, b as url, c as whoami, d as boolean, f as discriminatedUnion, g as object, h as looseObject, i as secretNameSchema, l as env, m as literal, n as readLocalManifest, o as getCurrentUser, p as hostname, r as connectionRequirementSchema, s as login, t as getLocalAppId, u as array, v as string, x as uuid, y as unknown } from "./app-CJgT3IKd.mjs";
2
+ import { C as unknown, E as execa, S as union, T as uuid, _ as literal, b as record, c as getCurrentUser, d as env, g as hostname, h as boolean, i as ownerSchema, l as login, m as array, n as saveLocalConnection, o as joinApp, p as secretNameSchema, r as appResourceConfigurationSchema, s as getAccessToken, t as prepareLocalResourceConfiguration, u as whoami, v as looseObject, w as url, x as string, y as object } from "./resources-DEdwfmku.mjs";
3
3
  import { cp, readFile, writeFile } from "node:fs/promises";
4
4
  import path, { join } from "node:path";
5
5
  import { EventEmitter } from "node:events";
@@ -3004,166 +3004,6 @@ async function runLocalAgent(name, prompt) {
3004
3004
  }
3005
3005
  }
3006
3006
  //#endregion
3007
- //#region ../shared/dist/app-connections.mjs
3008
- const appConnectionSchema = connectionRequirementSchema.extend({
3009
- connection_id: string().nullable(),
3010
- slug: string()
3011
- });
3012
- const appConnectionsSchema = object({ connections: array(appConnectionSchema) });
3013
- //#endregion
3014
- //#region owner.ts
3015
- const ownerOptionsSchema = object({ org: string().min(1).optional() });
3016
- async function resolveOwner(org, headers) {
3017
- if (!org) return {
3018
- id: (await getCurrentUser(headers)).id,
3019
- type: "user"
3020
- };
3021
- const response = await fetch(new URL("/api/platform/organizations", env.CTX_AUTH_ISSUER), { headers });
3022
- if (!response.ok) throw new Error(`Organization lookup failed: HTTP ${response.status}`);
3023
- const { organizations } = object({ organizations: array(object({
3024
- id: string().min(1),
3025
- slug: string()
3026
- })) }).parse(await response.json());
3027
- const organization = organizations.find((organization) => organization.slug === org);
3028
- if (!organization) throw new Error("Organization is not in your memberships");
3029
- return {
3030
- id: organization.id,
3031
- type: "organization"
3032
- };
3033
- }
3034
- function ownerPath(owner) {
3035
- return `${owner.type === "organization" ? "org" : "user"}/${encodeURIComponent(owner.id)}`;
3036
- }
3037
- //#endregion
3038
- //#region connections.ts
3039
- async function getLocalManifest(target) {
3040
- return target === "local" ? readLocalManifest() : void 0;
3041
- }
3042
- async function resolveAppTarget(target) {
3043
- if (target !== "local") return target;
3044
- return getLocalAppId();
3045
- }
3046
- async function assignAppConnection(target, slug, connectionId) {
3047
- const appTarget = await resolveAppTarget(target);
3048
- const response = await fetch(new URL(`/api/apps/${encodeURIComponent(appTarget)}/connections/assign`, env.CTX_PLATFORM_URL), {
3049
- body: JSON.stringify({
3050
- connection_id: connectionId,
3051
- manifest: await getLocalManifest(target),
3052
- slug
3053
- }),
3054
- headers: {
3055
- authorization: `Bearer ${await getAccessToken()}`,
3056
- "content-type": "application/json"
3057
- },
3058
- method: "POST"
3059
- });
3060
- if (!response.ok) throw new Error(`Connection assignment failed: HTTP ${response.status}`);
3061
- const assignment = object({
3062
- connection_id: string(),
3063
- slug: string()
3064
- }).parse(await response.json());
3065
- console.log(`${assignment.slug} → ${assignment.connection_id}`);
3066
- }
3067
- async function listAppConnections(target) {
3068
- const appTarget = await resolveAppTarget(target);
3069
- const manifest = await getLocalManifest(target);
3070
- const response = await fetch(new URL(`/api/apps/${encodeURIComponent(appTarget)}/connections/list`, env.CTX_PLATFORM_URL), {
3071
- body: JSON.stringify({ manifest }),
3072
- headers: {
3073
- authorization: `Bearer ${await getAccessToken()}`,
3074
- "content-type": "application/json"
3075
- },
3076
- method: "POST"
3077
- });
3078
- if (!response.ok) throw new Error(`App connections request failed: HTTP ${response.status}`);
3079
- const { connections } = appConnectionsSchema.parse(await response.json());
3080
- if (!connections.length) {
3081
- console.log("No connection requirements.");
3082
- return;
3083
- }
3084
- console.table(connections.map(({ connection_id, ...requirement }) => ({
3085
- ...requirement,
3086
- connection: connection_id ?? "unassigned"
3087
- })));
3088
- }
3089
- async function addConnection(toolkit, org) {
3090
- const headers = { authorization: `Bearer ${await getAccessToken()}` };
3091
- const owner = await resolveOwner(org, headers);
3092
- const response = await fetch(new URL(`/api/connections/${ownerPath(owner)}`, env.CTX_PLATFORM_URL), {
3093
- body: JSON.stringify({ toolkit }),
3094
- headers: {
3095
- ...headers,
3096
- "content-type": "application/json"
3097
- },
3098
- method: "POST"
3099
- });
3100
- if (!response.ok) throw new Error(`Connection authorization failed: HTTP ${response.status}`);
3101
- const { redirect_url } = object({ redirect_url: url() }).parse(await response.json());
3102
- console.log(`Open this link to authorize the Owner's account:\n${redirect_url}`);
3103
- console.log(`After authorization, run ctx connections${org ? ` --org ${org}` : ""} list to see the Owner's account.`);
3104
- }
3105
- async function listConnections(org) {
3106
- const headers = { authorization: `Bearer ${await getAccessToken()}` };
3107
- const owner = await resolveOwner(org, headers);
3108
- const response = await fetch(new URL(`/api/connections/${ownerPath(owner)}`, env.CTX_PLATFORM_URL), { headers });
3109
- if (!response.ok) throw new Error(`Connection list failed: HTTP ${response.status}`);
3110
- const { connections } = object({ connections: array(object({
3111
- alias: string().nullable(),
3112
- display_name: string().nullable(),
3113
- id: string(),
3114
- status: string(),
3115
- toolkit: string()
3116
- })) }).parse(await response.json());
3117
- if (connections.length === 0) {
3118
- console.log("No connected accounts.");
3119
- return;
3120
- }
3121
- console.table(connections);
3122
- }
3123
- //#endregion
3124
- //#region git-credential.ts
3125
- async function setupGit(cli) {
3126
- const key = `credential.${new URL("/git", env.CTX_PLATFORM_URL).href}.helper`;
3127
- const command = [
3128
- process.execPath,
3129
- fileURLToPath(cli),
3130
- "git-credential"
3131
- ].map((argument) => `'${argument.replaceAll("'", "'\\''")}'`).join(" ");
3132
- await execa("git", [
3133
- "config",
3134
- "--global",
3135
- "--replace-all",
3136
- key,
3137
- ""
3138
- ]);
3139
- await execa("git", [
3140
- "config",
3141
- "--global",
3142
- "--add",
3143
- key,
3144
- `!${command}`
3145
- ]);
3146
- }
3147
- async function gitCredential(operation) {
3148
- if (operation === "capability") {
3149
- process.stdout.write("version 0\ncapability authtype\n");
3150
- return;
3151
- }
3152
- if (operation !== "get") return;
3153
- const attributes = new URLSearchParams();
3154
- const lines = createInterface({ input: process.stdin });
3155
- for await (const line of lines) {
3156
- if (line === "") break;
3157
- const separator = line.indexOf("=");
3158
- attributes.append(line.slice(0, separator), line.slice(separator + 1));
3159
- }
3160
- literal("authtype").parse(attributes.getAll("capability[]").find((c) => c === "authtype"));
3161
- process.stdout.write(`capability[]=authtype\nauthtype=Bearer\ncredential=${await getAccessToken()}\n\n`);
3162
- }
3163
- //#endregion
3164
- //#region package.json
3165
- var version = "0.14.2";
3166
- //#endregion
3167
3007
  //#region ../../node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js
3168
3008
  const getCodePointsLength = (() => {
3169
3009
  const SURROGATE_PAIR_RE = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
@@ -3499,6 +3339,11 @@ var import_src = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
3499
3339
  beep
3500
3340
  };
3501
3341
  })))();
3342
+ function findCursor(s, o, l) {
3343
+ if (!l.some((r) => !r.disabled)) return s;
3344
+ const t = s + o, n = Math.max(l.length - 1, 0), e = t < 0 ? n : t > n ? 0 : t;
3345
+ return l[e]?.disabled ? findCursor(e, o < 0 ? -1 : 1, l) : e;
3346
+ }
3502
3347
  const settings = {
3503
3348
  actions: /* @__PURE__ */ new Set([
3504
3349
  "up",
@@ -3580,7 +3425,19 @@ function setRawMode(e, r) {
3580
3425
  const o = e;
3581
3426
  o.isTTY && o.setRawMode(r);
3582
3427
  }
3428
+ const getColumns = (e) => "columns" in e && typeof e.columns == "number" ? e.columns : 80;
3583
3429
  const getRows = (e) => "rows" in e && typeof e.rows == "number" ? e.rows : 20;
3430
+ function wrapTextWithPrefix(e, r, o, n = o, s = o, t) {
3431
+ return wrapAnsi(r, getColumns(e ?? stdout) - o.length, {
3432
+ hard: true,
3433
+ trim: false
3434
+ }).split(`
3435
+ `).map((c, i, m) => {
3436
+ const d = t ? t(c, i) : c;
3437
+ return i === 0 ? `${n}${d}` : i === m.length - 1 ? `${s}${d}` : `${o}${d}`;
3438
+ }).join(`
3439
+ `);
3440
+ }
3584
3441
  function runValidation(e, a) {
3585
3442
  if ("~standard" in e) {
3586
3443
  const n = e["~standard"].validate(a);
@@ -3797,6 +3654,32 @@ let u$1 = class u extends y {
3797
3654
  });
3798
3655
  }
3799
3656
  };
3657
+ let n$1 = class n extends y {
3658
+ options;
3659
+ cursor = 0;
3660
+ get _selectedValue() {
3661
+ return this.options[this.cursor];
3662
+ }
3663
+ changeValue() {
3664
+ const e = this._selectedValue;
3665
+ this.value = e === void 0 ? void 0 : e.value;
3666
+ }
3667
+ constructor(e) {
3668
+ super(e, false), this.options = e.options;
3669
+ const o = this.options.findIndex(({ value: s }) => s === e.initialValue), t = o === -1 ? 0 : o;
3670
+ this.cursor = this.options[t]?.disabled ? findCursor(t, 1, this.options) : t, this.changeValue(), this.on("cursor", (s) => {
3671
+ switch (s) {
3672
+ case "left":
3673
+ case "up":
3674
+ this.cursor = findCursor(this.cursor, -1, this.options);
3675
+ break;
3676
+ case "down":
3677
+ case "right": this.cursor = findCursor(this.cursor, 1, this.options);
3678
+ }
3679
+ this.changeValue();
3680
+ });
3681
+ }
3682
+ };
3800
3683
  //#endregion
3801
3684
  //#region ../../node_modules/.pnpm/@clack+prompts@1.8.1/node_modules/@clack/prompts/dist/index.mjs
3802
3685
  function isUnicodeSupported() {
@@ -3811,6 +3694,8 @@ const S_STEP_ERROR = unicodeOr("▲", "x");
3811
3694
  const S_STEP_SUBMIT = unicodeOr("◇", "o");
3812
3695
  const S_BAR = unicodeOr("│", "|");
3813
3696
  const S_BAR_END = unicodeOr("└", "—");
3697
+ const S_RADIO_ACTIVE = unicodeOr("●", ">");
3698
+ const S_RADIO_INACTIVE = unicodeOr("○", " ");
3814
3699
  const S_PASSWORD_MASK = unicodeOr("▪", "•");
3815
3700
  const symbol = (o) => {
3816
3701
  switch (o) {
@@ -3822,6 +3707,63 @@ const symbol = (o) => {
3822
3707
  case "validating": return styleText("dim", S_STEP_ACTIVE);
3823
3708
  }
3824
3709
  };
3710
+ const symbolBar = (o) => {
3711
+ switch (o) {
3712
+ case "initial":
3713
+ case "active": return styleText("cyan", S_BAR);
3714
+ case "cancel": return styleText("red", S_BAR);
3715
+ case "error": return styleText("yellow", S_BAR);
3716
+ case "submit": return styleText("green", S_BAR);
3717
+ }
3718
+ };
3719
+ function formatInstructionFooter(o, e) {
3720
+ const r = [`${e ? `${styleText("cyan", S_BAR)} ` : ""}${o.join(" • ")}`];
3721
+ return e && r.push(styleText("cyan", S_BAR_END)), r;
3722
+ }
3723
+ const I = (l, e, w, p, b, C = false) => {
3724
+ let r = e, O = 0;
3725
+ if (C) for (let i = p - 1; i >= w; i--) {
3726
+ const m = l[i];
3727
+ if (m && (r -= m.length), O++, r <= b) break;
3728
+ }
3729
+ else for (let i = w; i < p; i++) {
3730
+ const m = l[i];
3731
+ if (m && (r -= m.length), O++, r <= b) break;
3732
+ }
3733
+ return {
3734
+ lineCount: r,
3735
+ removals: O
3736
+ };
3737
+ };
3738
+ const limitOptions = ({ cursor: l, options: e, style: w, output: p = process.stdout, maxItems: b = Number.POSITIVE_INFINITY, columnPadding: C = 0, rowPadding: r = 4 }) => {
3739
+ const i = getColumns(p) - C, m = getRows(p), M = styleText("dim", "..."), v = Math.max(m - r, 0), a = Math.max(Math.min(b, v), 5);
3740
+ let f = 0;
3741
+ l >= a - 3 && (f = Math.max(Math.min(l - a + 3, e.length - a), 0));
3742
+ let d = a < e.length && f > 0, c = a < e.length && f + a < e.length;
3743
+ const W = Math.min(f + a, e.length), s = [];
3744
+ let g = 0;
3745
+ d && g++, c && g++;
3746
+ const T = f + (d ? 1 : 0), y = W - (c ? 1 : 0);
3747
+ for (let t = T; t < y; t++) {
3748
+ const n = e[t], h = wrapAnsi(n ? w(n, t === l) : "", i, {
3749
+ hard: true,
3750
+ trim: false
3751
+ }).split(`
3752
+ `);
3753
+ s.push(h), g += h.length;
3754
+ }
3755
+ if (g > v) {
3756
+ let t = 0, n = 0, o = g;
3757
+ const h = l - T;
3758
+ let u = v;
3759
+ const L = () => I(s, o, 0, h, u), E = () => I(s, o, h + 1, s.length, u, true);
3760
+ d ? ({lineCount: o, removals: t} = L(), o > u && (c || (u -= 1), {lineCount: o, removals: n} = E())) : (c || (u -= 1), {lineCount: o, removals: n} = E(), o > u && (u -= 1, {lineCount: o, removals: t} = L())), t > 0 && (d = true, s.splice(0, t)), n > 0 && (c = true, s.splice(s.length - n, n));
3761
+ }
3762
+ const x = [];
3763
+ d && x.push(M);
3764
+ for (const t of s) for (const n of t) x.push(n);
3765
+ return c && x.push(M), x;
3766
+ };
3825
3767
  `${styleText("dim", "↑/↓")}`, `${styleText("dim", "Space:")}`, `${styleText("dim", "Enter:")}`;
3826
3768
  const password = (e) => new u$1({
3827
3769
  validate: e.validate,
@@ -3850,21 +3792,272 @@ ${r ? styleText("cyan", S_BAR_END) : ""}
3850
3792
  }
3851
3793
  }
3852
3794
  }).prompt();
3853
- `${styleText("dim", "↑/↓")}`, `${styleText("dim", "Enter:")}`;
3795
+ const SELECT_INSTRUCTIONS = [`${styleText("dim", "↑/↓")} to navigate`, `${styleText("dim", "Enter:")} confirm`];
3796
+ const c = (t, o) => t.includes(`
3797
+ `) ? t.split(`
3798
+ `).map((d) => o(d)).join(`
3799
+ `) : o(t);
3800
+ const select = (t) => {
3801
+ const o = (n, m) => {
3802
+ if (n === void 0) return "";
3803
+ const s = n.label ?? String(n.value);
3804
+ switch (m) {
3805
+ case "disabled": return `${styleText("gray", S_RADIO_INACTIVE)} ${c(s, (i) => styleText("gray", i))}${n.hint ? ` ${styleText("dim", `(${n.hint ?? "disabled"})`)}` : ""}`;
3806
+ case "selected": return `${c(s, (i) => styleText("dim", i))}`;
3807
+ case "active": return `${styleText("green", S_RADIO_ACTIVE)} ${s}${n.hint ? ` ${styleText("dim", `(${n.hint})`)}` : ""}`;
3808
+ case "cancelled": return `${c(s, (i) => styleText(["strikethrough", "dim"], i))}`;
3809
+ default: return `${styleText("dim", S_RADIO_INACTIVE)} ${c(s, (i) => styleText("dim", i))}`;
3810
+ }
3811
+ }, d = t.showInstructions ?? true;
3812
+ return new n$1({
3813
+ options: t.options,
3814
+ signal: t.signal,
3815
+ input: t.input,
3816
+ output: t.output,
3817
+ initialValue: t.initialValue,
3818
+ render() {
3819
+ const n = t.withGuide ?? settings.withGuide, m = `${symbol(this.state)} `, s = `${symbolBar(this.state)} `, i = wrapTextWithPrefix(t.output, t.message, s, m), u = `${n ? `${styleText("gray", S_BAR)}
3820
+ ` : ""}${i}
3821
+ `;
3822
+ switch (this.state) {
3823
+ case "submit": {
3824
+ const r = n ? `${styleText("gray", S_BAR)} ` : "";
3825
+ return `${u}${wrapTextWithPrefix(t.output, o(this.options[this.cursor], "selected"), r)}`;
3826
+ }
3827
+ case "cancel": {
3828
+ const r = n ? `${styleText("gray", S_BAR)} ` : "";
3829
+ return `${u}${wrapTextWithPrefix(t.output, o(this.options[this.cursor], "cancelled"), r)}${n ? `
3830
+ ${styleText("gray", S_BAR)}` : ""}`;
3831
+ }
3832
+ default: {
3833
+ const r = n ? `${styleText("cyan", S_BAR)} ` : "", a = u.split(`
3834
+ `).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS, n) : n ? [styleText("cyan", S_BAR_END)] : [], f = p.join(`
3835
+ `), b = p.length + 1;
3836
+ return `${u}${r}${limitOptions({
3837
+ output: t.output,
3838
+ cursor: this.cursor,
3839
+ options: this.options,
3840
+ maxItems: t.maxItems,
3841
+ columnPadding: r.length,
3842
+ rowPadding: a + b,
3843
+ style: (g, x) => o(g, g.disabled ? "disabled" : x ? "active" : "inactive")
3844
+ }).join(`
3845
+ ${r}`)}
3846
+ ${f}
3847
+ `;
3848
+ }
3849
+ }
3850
+ }
3851
+ }).prompt();
3852
+ };
3854
3853
  `${styleText("gray", S_BAR)}`;
3855
3854
  //#endregion
3856
- //#region ../shared/dist/app-usage.mjs
3857
- const ownerSchema = discriminatedUnion("type", [object({
3858
- id: string().min(1),
3859
- type: literal("user")
3860
- }), object({
3861
- id: string().min(1),
3862
- type: literal("organization")
3863
- })]);
3864
- object({
3865
- id: uuid(),
3866
- owner: ownerSchema
3855
+ //#region ../shared/dist/app-connections.mjs
3856
+ const appConnectionSchema = appResourceConfigurationSchema.shape.connections.valueType.extend({ slug: string() });
3857
+ const appConnectionsSchema = object({ connections: array(appConnectionSchema) });
3858
+ //#endregion
3859
+ //#region ../shared/dist/connections.mjs
3860
+ const connectionSchema = object({
3861
+ alias: string().nullable(),
3862
+ display_name: string().nullable(),
3863
+ id: string(),
3864
+ status: string(),
3865
+ toolkit: string()
3866
+ });
3867
+ const connectionsSchema = object({ connections: array(connectionSchema) });
3868
+ //#endregion
3869
+ //#region owner.ts
3870
+ const ownerOptionsSchema = object({ org: string().min(1).optional() });
3871
+ async function resolveOwner(org, headers) {
3872
+ if (!org) return {
3873
+ id: (await getCurrentUser(headers)).id,
3874
+ type: "user"
3875
+ };
3876
+ const response = await fetch(new URL("/api/platform/organizations", env.CTX_AUTH_ISSUER), { headers });
3877
+ if (!response.ok) throw new Error(`Organization lookup failed: HTTP ${response.status}`);
3878
+ const { organizations } = object({ organizations: array(object({
3879
+ id: string().min(1),
3880
+ slug: string()
3881
+ })) }).parse(await response.json());
3882
+ const organization = organizations.find((organization) => organization.slug === org);
3883
+ if (!organization) throw new Error("Organization is not in your memberships");
3884
+ return {
3885
+ id: organization.id,
3886
+ type: "organization"
3887
+ };
3888
+ }
3889
+ function ownerPath(owner) {
3890
+ return `${owner.type === "organization" ? "org" : "user"}/${encodeURIComponent(owner.id)}`;
3891
+ }
3892
+ //#endregion
3893
+ //#region connections.ts
3894
+ const connectionAssignmentSchema = object({
3895
+ connection_id: string().min(1).optional(),
3896
+ slug: string().min(1),
3897
+ target: string().min(1)
3898
+ }).superRefine((input, ctx) => {
3899
+ if (input.target !== "local" && input.connection_id === void 0) ctx.addIssue({
3900
+ code: "custom",
3901
+ message: "Hosted assignment requires a connection ID",
3902
+ path: ["connection_id"]
3903
+ });
3867
3904
  });
3905
+ async function assignAppConnection(target, slug, connectionId) {
3906
+ if (target === "local") {
3907
+ const configuration = await prepareLocalResourceConfiguration();
3908
+ const requirement = new Map(Object.entries(configuration.connections)).get(slug);
3909
+ if (!requirement) throw new Error(`No local connection requirement for "${slug}"`);
3910
+ if (connectionId === void 0 && !process.stdin.isTTY) throw new Error("Supply a connection ID when assigning without a terminal");
3911
+ const url = new URL("/api/connections", env.CTX_PLATFORM_URL);
3912
+ url.searchParams.set("toolkit", requirement.toolkit);
3913
+ const response = await fetch(url, { headers: { authorization: `Bearer ${await getAccessToken()}` } });
3914
+ if (!response.ok) throw new Error(`Personal connection list failed: HTTP ${response.status}`);
3915
+ const { connections } = connectionsSchema.parse(await response.json());
3916
+ if (connections.length === 0) throw new Error(`No personal ${requirement.toolkit} accounts. Run ctx connections add ${requirement.toolkit}.`);
3917
+ if (connectionId === void 0) {
3918
+ const selected = await select({
3919
+ message: `Your ${requirement.toolkit} account for ${slug}`,
3920
+ options: connections.map((connection) => ({
3921
+ label: `${connection.display_name ?? connection.alias ?? connection.id} (${connection.id}; ${connection.status})`,
3922
+ value: connection.id
3923
+ })),
3924
+ output: process.stderr
3925
+ });
3926
+ if (isCancel(selected)) {
3927
+ process.exitCode = 130;
3928
+ return;
3929
+ }
3930
+ connectionId = selected;
3931
+ }
3932
+ if (!connections.some(({ id }) => id === connectionId)) throw new Error(`Account ${connectionId} is not one of your ${requirement.toolkit} accounts`);
3933
+ await saveLocalConnection(slug, connectionId);
3934
+ console.log(`${slug} → ${connectionId}`);
3935
+ return;
3936
+ }
3937
+ const response = await fetch(new URL(`/api/apps/${encodeURIComponent(target)}/connections/assign`, env.CTX_PLATFORM_URL), {
3938
+ body: JSON.stringify({
3939
+ connection_id: connectionId,
3940
+ slug
3941
+ }),
3942
+ headers: {
3943
+ authorization: `Bearer ${await getAccessToken()}`,
3944
+ "content-type": "application/json"
3945
+ },
3946
+ method: "POST"
3947
+ });
3948
+ if (!response.ok) throw new Error(`Connection assignment failed: HTTP ${response.status}`);
3949
+ const assignment = object({
3950
+ connection_id: string(),
3951
+ slug: string()
3952
+ }).parse(await response.json());
3953
+ console.log(`${assignment.slug} → ${assignment.connection_id}`);
3954
+ }
3955
+ async function listAppConnections(target) {
3956
+ if (target === "local") {
3957
+ const { connections } = await prepareLocalResourceConfiguration();
3958
+ if (Object.keys(connections).length === 0) {
3959
+ console.log("No connection requirements.");
3960
+ return;
3961
+ }
3962
+ console.table(Object.entries(connections).map(([slug, { connection_id, consumers, mode, toolkit }]) => ({
3963
+ connection: connection_id ?? "unassigned",
3964
+ consumers: consumers.map((consumer) => consumer.type === "app" ? "app" : `agent:${consumer.name}`).join(", "),
3965
+ mode,
3966
+ slug,
3967
+ toolkit
3968
+ })));
3969
+ return;
3970
+ }
3971
+ const response = await fetch(new URL(`/api/apps/${encodeURIComponent(target)}/connections/list`, env.CTX_PLATFORM_URL), {
3972
+ body: JSON.stringify({}),
3973
+ headers: {
3974
+ authorization: `Bearer ${await getAccessToken()}`,
3975
+ "content-type": "application/json"
3976
+ },
3977
+ method: "POST"
3978
+ });
3979
+ if (!response.ok) throw new Error(`App connections request failed: HTTP ${response.status}`);
3980
+ const { connections } = appConnectionsSchema.parse(await response.json());
3981
+ if (!connections.length) {
3982
+ console.log("No connection requirements.");
3983
+ return;
3984
+ }
3985
+ console.table(connections.map(({ connection_id, ...requirement }) => ({
3986
+ ...requirement,
3987
+ connection: connection_id ?? "unassigned"
3988
+ })));
3989
+ }
3990
+ async function addConnection(toolkit, org) {
3991
+ const headers = { authorization: `Bearer ${await getAccessToken()}` };
3992
+ const owner = await resolveOwner(org, headers);
3993
+ const response = await fetch(new URL(`/api/connections/${ownerPath(owner)}`, env.CTX_PLATFORM_URL), {
3994
+ body: JSON.stringify({ toolkit }),
3995
+ headers: {
3996
+ ...headers,
3997
+ "content-type": "application/json"
3998
+ },
3999
+ method: "POST"
4000
+ });
4001
+ if (!response.ok) throw new Error(`Connection authorization failed: HTTP ${response.status}`);
4002
+ const { redirect_url } = object({ redirect_url: url() }).parse(await response.json());
4003
+ console.log(`Open this link to authorize the Owner's account:\n${redirect_url}`);
4004
+ console.log(`After authorization, run ctx connections${org ? ` --org ${org}` : ""} list to see the Owner's account.`);
4005
+ }
4006
+ async function listConnections(org) {
4007
+ const headers = { authorization: `Bearer ${await getAccessToken()}` };
4008
+ const owner = await resolveOwner(org, headers);
4009
+ const response = await fetch(new URL(`/api/connections/${ownerPath(owner)}`, env.CTX_PLATFORM_URL), { headers });
4010
+ if (!response.ok) throw new Error(`Connection list failed: HTTP ${response.status}`);
4011
+ const { connections } = connectionsSchema.parse(await response.json());
4012
+ if (connections.length === 0) {
4013
+ console.log("No connected accounts.");
4014
+ return;
4015
+ }
4016
+ console.table(connections);
4017
+ }
4018
+ //#endregion
4019
+ //#region git-credential.ts
4020
+ async function setupGit(cli) {
4021
+ const key = `credential.${new URL("/git", env.CTX_PLATFORM_URL).href}.helper`;
4022
+ const command = [
4023
+ process.execPath,
4024
+ fileURLToPath(cli),
4025
+ "git-credential"
4026
+ ].map((argument) => `'${argument.replaceAll("'", "'\\''")}'`).join(" ");
4027
+ await execa("git", [
4028
+ "config",
4029
+ "--global",
4030
+ "--replace-all",
4031
+ key,
4032
+ ""
4033
+ ]);
4034
+ await execa("git", [
4035
+ "config",
4036
+ "--global",
4037
+ "--add",
4038
+ key,
4039
+ `!${command}`
4040
+ ]);
4041
+ }
4042
+ async function gitCredential(operation) {
4043
+ if (operation === "capability") {
4044
+ process.stdout.write("version 0\ncapability authtype\n");
4045
+ return;
4046
+ }
4047
+ if (operation !== "get") return;
4048
+ const attributes = new URLSearchParams();
4049
+ const lines = createInterface({ input: process.stdin });
4050
+ for await (const line of lines) {
4051
+ if (line === "") break;
4052
+ const separator = line.indexOf("=");
4053
+ attributes.append(line.slice(0, separator), line.slice(separator + 1));
4054
+ }
4055
+ literal("authtype").parse(attributes.getAll("capability[]").find((c) => c === "authtype"));
4056
+ process.stdout.write(`capability[]=authtype\nauthtype=Bearer\ncredential=${await getAccessToken()}\n\n`);
4057
+ }
4058
+ //#endregion
4059
+ //#region package.json
4060
+ var version = "0.14.3";
3868
4061
  //#endregion
3869
4062
  //#region secrets.ts
3870
4063
  const secretOptionsSchema = ownerOptionsSchema.extend({ stdin: boolean().default(false) });
@@ -3921,7 +4114,7 @@ async function dropSecret(name, org) {
3921
4114
  //#endregion
3922
4115
  //#region cli.ts
3923
4116
  const program = new Command().name("ctx").description("ContextLayer CLI").version(version);
3924
- program.command("init <directory>").description("Create a Cloudflare React Router project with Tailwind and shadcn Base UI, register an app owned by you or an organization (ctx login), and set its immutable Git remote").option("--org <slug>", "Create the app under an organization you belong to").action(async (directory, options) => {
4117
+ program.command("init <directory>").description("Create a Cloudflare React Router project with Tailwind and shadcn Base UI, register and join a private app owned by you or an organization (ctx login), and set its immutable Git remote").option("--org <slug>", "Create the app under an organization you belong to").action(async (directory, options) => {
3925
4118
  const input = object({ organization_slug: string().min(1).optional() }).parse({ organization_slug: options.org });
3926
4119
  const accessToken = await getAccessToken();
3927
4120
  const result = await execa("pnpm", [
@@ -4020,7 +4213,14 @@ program.command("login").description("Sign in by approving a browser code and co
4020
4213
  });
4021
4214
  program.command("git-credential <operation>").description("Git credential helper using the saved login; invoked automatically by Git").action(gitCredential);
4022
4215
  program.command("whoami").description("Show your ContextLayer user ID using the saved login").action(whoami);
4023
- const connections = program.command("connections").description("Manage Owner-owned connected accounts or list an app's requirements").option("--org <org_slug>", "Manage this organization's connections; defaults to the signed-in user").argument("[target]", "AppID, public hostname, or local (run from the app root)").addArgument(new Argument("[operation]", "App connection operation").choices(["list"])).action(async (target, operation) => {
4216
+ program.command("join").argument("<target>", "AppID, public hostname, or local (run from the app root)").description("Join a private app you can view using ctx login, enabling member-scoped unattended work; returns app and user IDs").action(async (target) => {
4217
+ await joinApp(union([
4218
+ literal("local"),
4219
+ uuid(),
4220
+ hostname()
4221
+ ]).parse(target));
4222
+ });
4223
+ const connections = program.command("connections").description("Manage Owner-owned connected accounts or list app and agent connection requirements; local reads only the current folder and its saved development choices").option("--org <org_slug>", "Manage this organization's connections; defaults to the signed-in user").argument("[target]", "AppID or public hostname (pushed requirements), or local (current folder; no platform request)").addArgument(new Argument("[operation]", "App connection operation").choices(["list"])).action(async (target, operation) => {
4024
4224
  if (!target || !operation) connections.error("Expected: ctx connections <target> list");
4025
4225
  await listAppConnections(target);
4026
4226
  });
@@ -4031,8 +4231,13 @@ connections.command("add <toolkit>").description("Print a Composio authorization
4031
4231
  const options = ownerOptionsSchema.parse(command.optsWithGlobals());
4032
4232
  await addConnection(string().min(1).parse(toolkit), options.org);
4033
4233
  });
4034
- program.command("connection").description("Assign a connection using the requirement’s viewer or owner mode; owner mode requires app management").argument("<target>", "AppID, public hostname, or local (run from the app root)").addArgument(new Argument("<operation>").choices(["assign"])).argument("<slug>", "Connection requirement slug").argument("<connection_id>", "Composio connected-account ID").action(async (target, _operation, slug, connectionId) => {
4035
- await assignAppConnection(target, slug, connectionId);
4234
+ program.command("connection").description("Bind a declared slug: local saves your personal account in .contextlayer/connections.json; AppID/hostname saves a hosted assignment under its declared Owner").argument("<target>", "AppID, public hostname, or local (run from the app root)").addArgument(new Argument("<operation>").choices(["assign"])).argument("<slug>", "Connection requirement slug").argument("[connection_id]", "Connected account ID; omit locally to choose your accounts for the declared toolkit; required for hosted targets").action(async (target, _operation, slug, connectionId) => {
4235
+ const input = connectionAssignmentSchema.parse({
4236
+ connection_id: connectionId,
4237
+ slug,
4238
+ target
4239
+ });
4240
+ await assignAppConnection(input.target, input.slug, input.connection_id);
4036
4241
  });
4037
4242
  program.command("agent").addArgument(new Argument("<target>", "Run from the app root").choices(["local"])).addArgument(new Argument("<operation>").choices(["run"])).argument("<name>", "Agent directory under agents/").argument("<prompt>", "Task to send as the user message").description("Run agents/<name>/instructions.md locally with Claude Agent SDK and native Claude authentication; no platform server or ctx login required").action(async (_target, _operation, name, prompt) => {
4038
4243
  const input = object({