@usecontextlayer/cli 0.14.3 → 0.16.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.
Files changed (32) hide show
  1. package/README.md +38 -0
  2. package/dist/auth-DOsE8GwJ.mjs +6847 -0
  3. package/dist/auth-DOsE8GwJ.mjs.map +1 -0
  4. package/dist/auth.d.mts +23 -0
  5. package/dist/auth.d.mts.map +1 -0
  6. package/dist/auth.mjs +2 -0
  7. package/dist/cli.mjs +177 -74
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/{resources-DEdwfmku.mjs → resources-B-EYHG1j.mjs} +198 -5665
  10. package/dist/resources-B-EYHG1j.mjs.map +1 -0
  11. package/dist/rolldown-runtime-EAsCxrDo.mjs +14 -0
  12. package/dist/template/AGENTS.md +13 -0
  13. package/dist/template/README.md +46 -0
  14. package/dist/template/app/root.tsx +28 -0
  15. package/dist/template/app/routes/home.tsx +17 -3
  16. package/dist/template/databases/updates/drizzle.config.ts +8 -0
  17. package/dist/template/databases/updates/migrations/0000_create_updates.sql +6 -0
  18. package/dist/template/databases/updates/migrations/meta/0000_snapshot.json +14 -0
  19. package/dist/template/databases/updates/migrations/meta/_journal.json +13 -0
  20. package/dist/template/databases/updates/migrations/migrations.js +10 -0
  21. package/dist/template/databases/updates/schema.ts +8 -0
  22. package/dist/template/workers/app.ts +3 -2
  23. package/dist/template/workers/local-resources/index.ts +6 -0
  24. package/dist/template/workers/local-resources/wrangler.jsonc +19 -0
  25. package/dist/vite.d.mts +4427 -3
  26. package/dist/vite.d.mts.map +1 -1
  27. package/dist/vite.mjs +94 -10
  28. package/dist/vite.mjs.map +1 -1
  29. package/package.json +19 -3
  30. package/dist/resources-DEdwfmku.mjs.map +0 -1
  31. package/dist/template/workers/local-tools/index.ts +0 -1
  32. package/dist/template/workers/local-tools/wrangler.jsonc +0 -5
@@ -0,0 +1,23 @@
1
+ //#region env.d.ts
2
+ declare const env: {
3
+ CTX_AUTH_CLIENT_ID: string;
4
+ CTX_AUTH_ISSUER: string;
5
+ CTX_PLATFORM_URL: string;
6
+ };
7
+ //#endregion
8
+ //#region auth.d.ts
9
+ type AuthEnv = Pick<typeof env, "CTX_AUTH_ISSUER" | "CTX_AUTH_CLIENT_ID">;
10
+ export declare class LoginRequiredError extends Error {
11
+ constructor(reason: string, options?: ErrorOptions);
12
+ }
13
+ export declare function getAccessToken(env: AuthEnv): Promise<string>;
14
+ export declare function getCurrentUser(env: AuthEnv, headers: HeadersInit): Promise<{
15
+ id: string;
16
+ }>;
17
+ export declare function listOrganizations(env: AuthEnv, headers: HeadersInit): Promise<{
18
+ id: string;
19
+ slug: string;
20
+ }[]>;
21
+ //#endregion
22
+ export type { AuthEnv };
23
+ //# sourceMappingURL=auth.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.mts","names":[],"sources":["../env.ts","../auth.ts"],"mappings":";cAMa;;;;;;;KCCD,UAAU,YAAY;qBAYrB,2BAA2B;EACvC,YAAY,gBAAgB,UAAU;;wBAiDjB,eAAe,KAAK,UAAO;wBAqC3B,eAAe,KAAK,SAAS,SAAS,cAAW;;;wBAOjD,kBAAkB,KAAK,SAAS,SAAS,cAAW"}
package/dist/auth.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { i as listOrganizations, n as getAccessToken, r as getCurrentUser, t as LoginRequiredError } from "./auth-DOsE8GwJ.mjs";
2
+ export { LoginRequiredError, getAccessToken, getCurrentUser, listOrganizations };
package/dist/cli.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
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";
2
+ import { t as __commonJSMin } from "./rolldown-runtime-EAsCxrDo.mjs";
3
+ import { S as uuid, a as login, b as unknown, c as array, d as hostname, f as literal, g as record, h as object, i as listOrganizations, l as boolean, n as getAccessToken, p as looseObject, r as getCurrentUser, t as LoginRequiredError, v as string, x as url, y as union } from "./auth-DOsE8GwJ.mjs";
4
+ import { a as env, c as resourcesForConsumer, f as execa, i as joinSlate, l as secretNameSchema, n as saveLocalConnection, o as slateResourceConfigurationSchema, s as ownerSchema, t as prepareLocalResourceConfiguration, u as databaseQuerySchema } from "./resources-B-EYHG1j.mjs";
3
5
  import { cp, readFile, writeFile } from "node:fs/promises";
4
6
  import path, { join } from "node:path";
5
7
  import { EventEmitter } from "node:events";
@@ -9,9 +11,6 @@ import process$1, { stdin, stdout } from "node:process";
9
11
  import { stripVTControlCharacters, styleText } from "node:util";
10
12
  import { fileURLToPath } from "node:url";
11
13
  import l__default, { createInterface } from "node:readline";
12
- //#region \0rolldown/runtime.js
13
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
14
- //#endregion
15
14
  //#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
16
15
  /**
17
16
  * CommanderError class
@@ -2968,13 +2967,40 @@ function useColor() {
2968
2967
  new Command();
2969
2968
  //#endregion
2970
2969
  //#region agents.ts
2971
- const platformInstructions = "You are a ContextLayer app agent. The user message is the task for this run. Work in the app directory and return the task's result.";
2970
+ const platformInstructions = "You are an agent of a ContextLayer slate.The user message is the task for this run. Work in the checkout and return the task's result. Use database_query to read and save persistent data in declared databases. Files are scratch data; schema changes belong in explicitly authored SQL migrations.";
2972
2971
  async function runLocalAgent(name, prompt) {
2973
2972
  const instructions = await readFile(join("agents", name, "instructions.md"), "utf8");
2974
- const { query } = await import("@anthropic-ai/claude-agent-sdk");
2973
+ const { createServer } = await import("vite");
2974
+ const { getPreparedLocalBindings, openLocalDatabases } = await import("./vite.mjs");
2975
+ const server = await createServer({
2976
+ logLevel: "error",
2977
+ server: {
2978
+ host: "127.0.0.1",
2979
+ port: 0
2980
+ }
2981
+ });
2982
+ await server.listen();
2983
+ const bindings = getPreparedLocalBindings(server);
2984
+ const configuration = resourcesForConsumer(bindings.configuration, {
2985
+ name,
2986
+ type: "agent"
2987
+ });
2988
+ const storage = await openLocalDatabases(bindings.workerName, configuration.databases);
2989
+ const { query, createSdkMcpServer, tool } = await import("@anthropic-ai/claude-agent-sdk");
2975
2990
  const run = query({
2976
2991
  options: {
2992
+ allowedTools: ["mcp__databases__database_query"],
2977
2993
  cwd: process.cwd(),
2994
+ mcpServers: { databases: createSdkMcpServer({
2995
+ name: "databases",
2996
+ tools: [tool("database_query", "Query or update a declared persistent SQLite database using parameterized SQL.", databaseQuerySchema.shape, async ({ slug, sql, params }) => {
2997
+ const result = await storage.databases.query(slug, sql, params);
2998
+ return { content: [{
2999
+ text: JSON.stringify(result),
3000
+ type: "text"
3001
+ }] };
3002
+ })]
3003
+ }) },
2978
3004
  settingSources: [],
2979
3005
  systemPrompt: {
2980
3006
  append: `${platformInstructions}\n\n${instructions}`,
@@ -3001,6 +3027,8 @@ async function runLocalAgent(name, prompt) {
3001
3027
  throw new Error("Claude ended without a result");
3002
3028
  } finally {
3003
3029
  run.close();
3030
+ await storage.dispose();
3031
+ await server.close();
3004
3032
  }
3005
3033
  }
3006
3034
  //#endregion
@@ -3852,34 +3880,28 @@ ${f}
3852
3880
  };
3853
3881
  `${styleText("gray", S_BAR)}`;
3854
3882
  //#endregion
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
3883
  //#region ../shared/dist/connections.mjs
3860
3884
  const connectionSchema = object({
3861
3885
  alias: string().nullable(),
3862
3886
  display_name: string().nullable(),
3863
3887
  id: string(),
3864
- status: string(),
3888
+ status: literal("ACTIVE"),
3865
3889
  toolkit: string()
3866
3890
  });
3867
3891
  const connectionsSchema = object({ connections: array(connectionSchema) });
3868
3892
  //#endregion
3893
+ //#region ../shared/dist/slate-connections.mjs
3894
+ const slateConnectionSchema = slateResourceConfigurationSchema.shape.connections.valueType.extend({ slug: string() });
3895
+ const slateConnectionsSchema = object({ connections: array(slateConnectionSchema) });
3896
+ //#endregion
3869
3897
  //#region owner.ts
3870
3898
  const ownerOptionsSchema = object({ org: string().min(1).optional() });
3871
3899
  async function resolveOwner(org, headers) {
3872
3900
  if (!org) return {
3873
- id: (await getCurrentUser(headers)).id,
3874
- type: "user"
3901
+ id: (await getCurrentUser(env, headers)).id,
3902
+ type: "personal"
3875
3903
  };
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);
3904
+ const organization = (await listOrganizations(env, headers)).find((organization) => organization.slug === org);
3883
3905
  if (!organization) throw new Error("Organization is not in your memberships");
3884
3906
  return {
3885
3907
  id: organization.id,
@@ -3887,7 +3909,7 @@ async function resolveOwner(org, headers) {
3887
3909
  };
3888
3910
  }
3889
3911
  function ownerPath(owner) {
3890
- return `${owner.type === "organization" ? "org" : "user"}/${encodeURIComponent(owner.id)}`;
3912
+ return `${owner.type === "organization" ? "org" : "personal"}/${encodeURIComponent(owner.id)}`;
3891
3913
  }
3892
3914
  //#endregion
3893
3915
  //#region connections.ts
@@ -3902,7 +3924,7 @@ const connectionAssignmentSchema = object({
3902
3924
  path: ["connection_id"]
3903
3925
  });
3904
3926
  });
3905
- async function assignAppConnection(target, slug, connectionId) {
3927
+ async function assignSlateConnection(target, slug, connectionId) {
3906
3928
  if (target === "local") {
3907
3929
  const configuration = await prepareLocalResourceConfiguration();
3908
3930
  const requirement = new Map(Object.entries(configuration.connections)).get(slug);
@@ -3910,37 +3932,38 @@ async function assignAppConnection(target, slug, connectionId) {
3910
3932
  if (connectionId === void 0 && !process.stdin.isTTY) throw new Error("Supply a connection ID when assigning without a terminal");
3911
3933
  const url = new URL("/api/connections", env.CTX_PLATFORM_URL);
3912
3934
  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}`);
3935
+ const response = await fetch(url, { headers: { authorization: `Bearer ${await getAccessToken(env)}` } });
3936
+ if (!response.ok) throw new Error(`Personal connected account list failed: HTTP ${response.status}`);
3915
3937
  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}.`);
3938
+ if (connections.length === 0) throw new Error(`No personal connected accounts for ${requirement.toolkit}. Run npx @usecontextlayer/cli connections add ${requirement.toolkit} to connect an account in your browser, then rerun this assignment.`);
3917
3939
  if (connectionId === void 0) {
3918
- const selected = await select({
3919
- message: `Your ${requirement.toolkit} account for ${slug}`,
3940
+ const accountId = await select({
3941
+ message: `Assign a connected ${requirement.toolkit} account to ${slug}`,
3920
3942
  options: connections.map((connection) => ({
3921
3943
  label: `${connection.display_name ?? connection.alias ?? connection.id} (${connection.id}; ${connection.status})`,
3922
3944
  value: connection.id
3923
3945
  })),
3924
3946
  output: process.stderr
3925
3947
  });
3926
- if (isCancel(selected)) {
3948
+ if (isCancel(accountId)) {
3927
3949
  process.exitCode = 130;
3928
3950
  return;
3929
3951
  }
3930
- connectionId = selected;
3952
+ connectionId = accountId;
3931
3953
  }
3932
- if (!connections.some(({ id }) => id === connectionId)) throw new Error(`Account ${connectionId} is not one of your ${requirement.toolkit} accounts`);
3954
+ if (!connections.some(({ id }) => id === connectionId)) throw new Error(`Connected account ${connectionId} is not one of your connected ${requirement.toolkit} accounts`);
3933
3955
  await saveLocalConnection(slug, connectionId);
3934
3956
  console.log(`${slug} → ${connectionId}`);
3957
+ console.log("Start or restart pnpm dev to use this assignment.");
3935
3958
  return;
3936
3959
  }
3937
- const response = await fetch(new URL(`/api/apps/${encodeURIComponent(target)}/connections/assign`, env.CTX_PLATFORM_URL), {
3960
+ const response = await fetch(new URL(`/api/slates/${encodeURIComponent(target)}/connections/assign`, env.CTX_PLATFORM_URL), {
3938
3961
  body: JSON.stringify({
3939
3962
  connection_id: connectionId,
3940
3963
  slug
3941
3964
  }),
3942
3965
  headers: {
3943
- authorization: `Bearer ${await getAccessToken()}`,
3966
+ authorization: `Bearer ${await getAccessToken(env)}`,
3944
3967
  "content-type": "application/json"
3945
3968
  },
3946
3969
  method: "POST"
@@ -3952,7 +3975,7 @@ async function assignAppConnection(target, slug, connectionId) {
3952
3975
  }).parse(await response.json());
3953
3976
  console.log(`${assignment.slug} → ${assignment.connection_id}`);
3954
3977
  }
3955
- async function listAppConnections(target) {
3978
+ async function listSlateConnections(target) {
3956
3979
  if (target === "local") {
3957
3980
  const { connections } = await prepareLocalResourceConfiguration();
3958
3981
  if (Object.keys(connections).length === 0) {
@@ -3960,7 +3983,7 @@ async function listAppConnections(target) {
3960
3983
  return;
3961
3984
  }
3962
3985
  console.table(Object.entries(connections).map(([slug, { connection_id, consumers, mode, toolkit }]) => ({
3963
- connection: connection_id ?? "unassigned",
3986
+ connected_account: connection_id ?? "unassigned",
3964
3987
  consumers: consumers.map((consumer) => consumer.type === "app" ? "app" : `agent:${consumer.name}`).join(", "),
3965
3988
  mode,
3966
3989
  slug,
@@ -3968,27 +3991,27 @@ async function listAppConnections(target) {
3968
3991
  })));
3969
3992
  return;
3970
3993
  }
3971
- const response = await fetch(new URL(`/api/apps/${encodeURIComponent(target)}/connections/list`, env.CTX_PLATFORM_URL), {
3994
+ const response = await fetch(new URL(`/api/slates/${encodeURIComponent(target)}/connections/list`, env.CTX_PLATFORM_URL), {
3972
3995
  body: JSON.stringify({}),
3973
3996
  headers: {
3974
- authorization: `Bearer ${await getAccessToken()}`,
3997
+ authorization: `Bearer ${await getAccessToken(env)}`,
3975
3998
  "content-type": "application/json"
3976
3999
  },
3977
4000
  method: "POST"
3978
4001
  });
3979
- if (!response.ok) throw new Error(`App connections request failed: HTTP ${response.status}`);
3980
- const { connections } = appConnectionsSchema.parse(await response.json());
4002
+ if (!response.ok) throw new Error(`Slate connections request failed: HTTP ${response.status}`);
4003
+ const { connections } = slateConnectionsSchema.parse(await response.json());
3981
4004
  if (!connections.length) {
3982
4005
  console.log("No connection requirements.");
3983
4006
  return;
3984
4007
  }
3985
4008
  console.table(connections.map(({ connection_id, ...requirement }) => ({
3986
4009
  ...requirement,
3987
- connection: connection_id ?? "unassigned"
4010
+ connected_account: connection_id ?? "unassigned"
3988
4011
  })));
3989
4012
  }
3990
- async function addConnection(toolkit, org) {
3991
- const headers = { authorization: `Bearer ${await getAccessToken()}` };
4013
+ async function connectAccount(toolkit, org) {
4014
+ const headers = { authorization: `Bearer ${await getAccessToken(env)}` };
3992
4015
  const owner = await resolveOwner(org, headers);
3993
4016
  const response = await fetch(new URL(`/api/connections/${ownerPath(owner)}`, env.CTX_PLATFORM_URL), {
3994
4017
  body: JSON.stringify({ toolkit }),
@@ -3998,16 +4021,18 @@ async function addConnection(toolkit, org) {
3998
4021
  },
3999
4022
  method: "POST"
4000
4023
  });
4001
- if (!response.ok) throw new Error(`Connection authorization failed: HTTP ${response.status}`);
4024
+ if (!response.ok) throw new Error(`Connecting account failed: HTTP ${response.status}`);
4002
4025
  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.`);
4026
+ console.log(`Open this link to connect the Owner's account:\n${redirect_url}`);
4027
+ console.log("After connecting in your browser, assign the connected account to a slate's declared connection.");
4028
+ if (!org) console.log("From your checkout, assign your connected account to a connection declared in slate.json:\n npx @usecontextlayer/cli connection local assign <slug>");
4029
+ console.log(`For a hosted slate, get the connected account ID and assign it:\n npx @usecontextlayer/cli connections${org ? ` --org '${org.replaceAll("'", "'\\''")}'` : ""} list\n npx @usecontextlayer/cli connection <SlateID-or-hostname> assign <slug> <connection_id>`);
4005
4030
  }
4006
4031
  async function listConnections(org) {
4007
- const headers = { authorization: `Bearer ${await getAccessToken()}` };
4032
+ const headers = { authorization: `Bearer ${await getAccessToken(env)}` };
4008
4033
  const owner = await resolveOwner(org, headers);
4009
4034
  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}`);
4035
+ if (!response.ok) throw new Error(`Connected account list failed: HTTP ${response.status}`);
4011
4036
  const { connections } = connectionsSchema.parse(await response.json());
4012
4037
  if (connections.length === 0) {
4013
4038
  console.log("No connected accounts.");
@@ -4017,7 +4042,7 @@ async function listConnections(org) {
4017
4042
  }
4018
4043
  //#endregion
4019
4044
  //#region git-credential.ts
4020
- async function setupGit(cli) {
4045
+ async function configureGit(cli) {
4021
4046
  const key = `credential.${new URL("/git", env.CTX_PLATFORM_URL).href}.helper`;
4022
4047
  const command = [
4023
4048
  process.execPath,
@@ -4053,11 +4078,11 @@ async function gitCredential(operation) {
4053
4078
  attributes.append(line.slice(0, separator), line.slice(separator + 1));
4054
4079
  }
4055
4080
  literal("authtype").parse(attributes.getAll("capability[]").find((c) => c === "authtype"));
4056
- process.stdout.write(`capability[]=authtype\nauthtype=Bearer\ncredential=${await getAccessToken()}\n\n`);
4081
+ process.stdout.write(`capability[]=authtype\nauthtype=Bearer\ncredential=${await getAccessToken(env)}\n\n`);
4057
4082
  }
4058
4083
  //#endregion
4059
4084
  //#region package.json
4060
- var version = "0.14.3";
4085
+ var version = "0.16.0";
4061
4086
  //#endregion
4062
4087
  //#region secrets.ts
4063
4088
  const secretOptionsSchema = ownerOptionsSchema.extend({ stdin: boolean().default(false) });
@@ -4071,7 +4096,7 @@ async function resolveSecretUrl(name, org, headers) {
4071
4096
  return new URL(`/api/secrets/${ownerPath(owner)}/${encodeURIComponent(name)}`, env.CTX_PLATFORM_URL);
4072
4097
  }
4073
4098
  async function setSecret(name, options) {
4074
- const headers = { authorization: `Bearer ${await getAccessToken()}` };
4099
+ const headers = { authorization: `Bearer ${await getAccessToken(env)}` };
4075
4100
  const url = await resolveSecretUrl(name, options.org, headers);
4076
4101
  let value;
4077
4102
  if (options.stdin) {
@@ -4103,7 +4128,7 @@ async function setSecret(name, options) {
4103
4128
  console.log(JSON.stringify(confirmationSchema.parse(await response.json())));
4104
4129
  }
4105
4130
  async function dropSecret(name, org) {
4106
- const headers = { authorization: `Bearer ${await getAccessToken()}` };
4131
+ const headers = { authorization: `Bearer ${await getAccessToken(env)}` };
4107
4132
  const response = await fetch(await resolveSecretUrl(name, org, headers), {
4108
4133
  headers,
4109
4134
  method: "DELETE"
@@ -4114,9 +4139,31 @@ async function dropSecret(name, org) {
4114
4139
  //#endregion
4115
4140
  //#region cli.ts
4116
4141
  const program = new Command().name("ctx").description("ContextLayer CLI").version(version);
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) => {
4142
+ program.addHelpText("afterAll", "\nRun without a global install: npx @usecontextlayer/cli <command>");
4143
+ async function authenticate(command) {
4144
+ try {
4145
+ return await getCurrentUser(env, { authorization: `Bearer ${await getAccessToken(env)}` });
4146
+ } catch (error) {
4147
+ if (error instanceof LoginRequiredError) command.error(error.message);
4148
+ throw error;
4149
+ }
4150
+ }
4151
+ async function requireLogin(command) {
4152
+ await authenticate(command);
4153
+ }
4154
+ program.command("init <directory>").description("Create a private ContextLayer slate on main with React Router, Tailwind, and shadcn; requires login, Node.js 24+, pnpm, and Git author identity; sets the Git remote and prints how to start and deploy the slate").option("--org <slug>", "Create the slate under an organization you belong to").hook("preAction", requireLogin).action(async (directory, options) => {
4118
4155
  const input = object({ organization_slug: string().min(1).optional() }).parse({ organization_slug: options.org });
4119
- const accessToken = await getAccessToken();
4156
+ const accessToken = await getAccessToken(env);
4157
+ const gitIdentity = await execa("git", ["var", "GIT_AUTHOR_IDENT"], {
4158
+ reject: false,
4159
+ stdio: [
4160
+ "inherit",
4161
+ "ignore",
4162
+ "inherit"
4163
+ ]
4164
+ });
4165
+ process.exitCode = gitIdentity.exitCode;
4166
+ if (gitIdentity.exitCode !== 0) return;
4120
4167
  const result = await execa("pnpm", [
4121
4168
  "create",
4122
4169
  "cloudflare@latest",
@@ -4128,6 +4175,11 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4128
4175
  "--",
4129
4176
  "--yes"
4130
4177
  ], {
4178
+ env: {
4179
+ GIT_CONFIG_COUNT: "1",
4180
+ GIT_CONFIG_KEY_0: "init.defaultBranch",
4181
+ GIT_CONFIG_VALUE_0: "main"
4182
+ },
4131
4183
  reject: false,
4132
4184
  stdio: "inherit"
4133
4185
  });
@@ -4135,7 +4187,18 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4135
4187
  if (result.exitCode !== 0) return;
4136
4188
  const tsconfigPath = join(directory, "tsconfig.json");
4137
4189
  const tsconfig = looseObject({ compilerOptions: record(string(), unknown()) }).parse(JSON.parse(await readFile(tsconfigPath, "utf8")));
4138
- tsconfig.compilerOptions.paths = { "~/*": ["./app/*"] };
4190
+ tsconfig.compilerOptions.paths = {
4191
+ "@/*": ["./*"],
4192
+ "~/*": ["./app/*"]
4193
+ };
4194
+ const cloudflareTsconfigPath = join(directory, "tsconfig.cloudflare.json");
4195
+ const cloudflareTsconfig = looseObject({
4196
+ compilerOptions: record(string(), unknown()),
4197
+ include: array(string())
4198
+ }).parse(JSON.parse(await readFile(cloudflareTsconfigPath, "utf8")));
4199
+ cloudflareTsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths;
4200
+ cloudflareTsconfig.include.push("databases/**/schema.ts");
4201
+ await writeFile(cloudflareTsconfigPath, `${JSON.stringify(cloudflareTsconfig, null, 2)}\n`);
4139
4202
  await writeFile(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}\n`);
4140
4203
  const ui = await execa("pnpm", [
4141
4204
  "dlx",
@@ -4172,7 +4235,8 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4172
4235
  const install = await execa("pnpm", [
4173
4236
  "add",
4174
4237
  `@usecontextlayer/sdk@${version}`,
4175
- "zod@^4.4.3"
4238
+ "zod@^4.4.3",
4239
+ "drizzle-orm@0.45.3"
4176
4240
  ], {
4177
4241
  cwd: directory,
4178
4242
  reject: false,
@@ -4180,7 +4244,22 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4180
4244
  });
4181
4245
  process.exitCode = install.exitCode;
4182
4246
  if (install.exitCode !== 0) return;
4183
- const response = await fetch(new URL("/api/apps", env.CTX_PLATFORM_URL), {
4247
+ const migrationTool = await execa("pnpm", [
4248
+ "add",
4249
+ "-D",
4250
+ "drizzle-kit@0.31.11"
4251
+ ], {
4252
+ cwd: directory,
4253
+ reject: false,
4254
+ stdio: "inherit"
4255
+ });
4256
+ process.exitCode = migrationTool.exitCode;
4257
+ if (migrationTool.exitCode !== 0) return;
4258
+ const packagePath = join(directory, "package.json");
4259
+ const checkoutPackage = looseObject({ scripts: record(string(), string()) }).parse(JSON.parse(await readFile(packagePath, "utf8")));
4260
+ checkoutPackage.scripts["db:migration"] = "drizzle-kit generate --custom";
4261
+ await writeFile(packagePath, `${JSON.stringify(checkoutPackage, null, 2)}\n`);
4262
+ const response = await fetch(new URL("/api/slates", env.CTX_PLATFORM_URL), {
4184
4263
  body: JSON.stringify(input),
4185
4264
  headers: {
4186
4265
  authorization: `Bearer ${accessToken}`,
@@ -4188,12 +4267,12 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4188
4267
  },
4189
4268
  method: "POST"
4190
4269
  });
4191
- if (!response.ok) throw new Error(`App creation failed: HTTP ${response.status}`);
4192
- const app = object({
4270
+ if (!response.ok) throw new Error(`Slate creation failed: HTTP ${response.status}`);
4271
+ const slate = object({
4193
4272
  id: uuid(),
4194
4273
  public_hostname: hostname()
4195
4274
  }).parse(await response.json());
4196
- const remote = new URL(`/git/${app.id}`, env.CTX_PLATFORM_URL);
4275
+ const remote = new URL(`/git/${slate.id}`, env.CTX_PLATFORM_URL);
4197
4276
  const git = await execa("git", [
4198
4277
  "remote",
4199
4278
  "add",
@@ -4205,41 +4284,65 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4205
4284
  stdio: "inherit"
4206
4285
  });
4207
4286
  process.exitCode = git.exitCode;
4287
+ if (git.exitCode !== 0) return;
4288
+ console.log(`
4289
+ ContextLayer slate created: ${slate.id}
4290
+ Dashboard: ${new URL(`/dashboard/slates/${slate.id}`, env.CTX_PLATFORM_URL).href}
4291
+ Hosted URL: https://${slate.public_hostname}/ (available after deployment)
4292
+
4293
+ Start locally:
4294
+ cd '${directory.replaceAll("'", "'\\''")}'
4295
+ pnpm dev
4296
+ Open the local URL printed by the development server.
4297
+
4298
+ Deploy your changes:
4299
+ git add .
4300
+ git commit
4301
+ git push -u origin main
4302
+
4303
+ See README.md for the development and connection workflow.
4304
+ `);
4208
4305
  });
4209
4306
  program.command("login").description("Sign in by approving a browser code and configure Git credentials for ContextLayer repositories").action(async () => {
4210
- await login();
4211
- await setupGit(new URL(import.meta.url));
4307
+ await login(env);
4308
+ await configureGit(new URL(import.meta.url));
4212
4309
  console.log("Git credentials configured for ContextLayer.");
4213
4310
  });
4214
- program.command("git-credential <operation>").description("Git credential helper using the saved login; invoked automatically by Git").action(gitCredential);
4215
- program.command("whoami").description("Show your ContextLayer user ID using the saved login").action(whoami);
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([
4311
+ program.command("git-credential <operation>").description("Git credential helper using the saved login; invoked automatically by Git").hook("preAction", async (command) => {
4312
+ if (command.args[0] === "get") await requireLogin(command);
4313
+ }).action(gitCredential);
4314
+ program.command("whoami").description("Show your ContextLayer user ID verified by the auth server").action(async (_options, command) => {
4315
+ console.log((await authenticate(command)).id);
4316
+ });
4317
+ program.command("join").argument("<target>", "SlateID, public hostname, or local (run from the checkout root)").description("Join a private slate you can view using ctx login, enabling member-scoped unattended work; returns slate and user IDs").hook("preAction", requireLogin).action(async (target) => {
4318
+ await joinSlate(union([
4218
4319
  literal("local"),
4219
4320
  uuid(),
4220
4321
  hostname()
4221
4322
  ]).parse(target));
4222
4323
  });
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) => {
4324
+ const connections = program.command("connections").description("Manage Owner-owned connected accounts or list app and agent connections and assignments; local reads only the current checkout and its saved development assignments").option("--org <slug>", "Manage this organization's connected accounts; defaults to the signed-in user").argument("[target]", "SlateID or public hostname (pushed requirements), or local (current checkout; no platform request)").addArgument(new Argument("[operation]", "Slate connection operation").choices(["list"])).hook("preAction", async (command, actionCommand) => {
4325
+ if (actionCommand !== command || command.args[0] !== "local") await requireLogin(actionCommand);
4326
+ }).action(async (target, operation) => {
4224
4327
  if (!target || !operation) connections.error("Expected: ctx connections <target> list");
4225
- await listAppConnections(target);
4328
+ await listSlateConnections(target);
4226
4329
  });
4227
- connections.command("list").description("List the selected Owner's Composio accounts with their toolkit, identity, and status").action(async (_options, command) => {
4330
+ connections.command("list").description("List the selected Owner's connected accounts with their toolkit, identity, and status").action(async (_options, command) => {
4228
4331
  await listConnections(ownerOptionsSchema.parse(command.optsWithGlobals()).org);
4229
4332
  });
4230
- connections.command("add <toolkit>").description("Print a Composio authorization link for the selected Owner's account (for example, gmail)").action(async (toolkit, _options, command) => {
4333
+ connections.command("add <toolkit>").description("Connect the selected Owner's account in your browser (for example, gmail), then assign the connected account to a slate's declared connection with ctx connection").action(async (toolkit, _options, command) => {
4231
4334
  const options = ownerOptionsSchema.parse(command.optsWithGlobals());
4232
- await addConnection(string().min(1).parse(toolkit), options.org);
4335
+ await connectAccount(string().min(1).parse(toolkit), options.org);
4233
4336
  });
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) => {
4337
+ program.command("connection").description("Assign a connected account to a declared connection: local saves a personal connected account assignment in .contextlayer/connections.json; SlateID/hostname saves a hosted assignment under its declared Owner").argument("<target>", "SlateID, public hostname, or local (run from the checkout root)").addArgument(new Argument("<operation>").choices(["assign"])).argument("<slug>", "Connection slug declared in slate.json (for example, work-email)").argument("[connection_id]", "Connected account ID; omit locally to assign a connected account for the declared toolkit interactively; required for hosted targets").hook("preAction", requireLogin).action(async (target, _operation, slug, connectionId) => {
4235
4338
  const input = connectionAssignmentSchema.parse({
4236
4339
  connection_id: connectionId,
4237
4340
  slug,
4238
4341
  target
4239
4342
  });
4240
- await assignAppConnection(input.target, input.slug, input.connection_id);
4343
+ await assignSlateConnection(input.target, input.slug, input.connection_id);
4241
4344
  });
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) => {
4345
+ program.command("agent").addArgument(new Argument("<target>", "Run from the checkout 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; Vite startup also requires ctx login when the app declares connections").action(async (_target, _operation, name, prompt) => {
4243
4346
  const input = object({
4244
4347
  name: string().min(1),
4245
4348
  prompt: string().min(1)
@@ -4249,7 +4352,7 @@ program.command("agent").addArgument(new Argument("<target>", "Run from the app
4249
4352
  });
4250
4353
  console.log(await runLocalAgent(input.name, input.prompt));
4251
4354
  });
4252
- const secrets = program.command("secrets").description("Save or drop Owner-owned shared secrets through the authenticated platform").option("--org <org_slug>", "Use this organization's Owner; defaults to the signed-in user");
4355
+ const secrets = program.command("secrets").description("Save or drop Owner-owned shared secrets through the authenticated platform").option("--org <slug>", "Use this organization's Owner; defaults to the signed-in user").hook("preAction", requireLogin);
4253
4356
  secrets.command("set").argument("<name>", "Secret name: uppercase letters, digits, and underscores; cannot start with a digit").description("Save a secret with a masked password prompt; returns only Owner and name").option("--stdin", "Read the exact UTF-8 value from stdin instead of prompting; preserves whitespace and newlines").action(async (name, _options, command) => {
4254
4357
  await setSecret(secretNameSchema.parse(name), secretOptionsSchema.parse(command.optsWithGlobals()));
4255
4358
  });