@getjack/jack 0.1.30 → 0.1.32

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,119 @@
1
+ /**
2
+ * jack tokens - Manage API tokens for headless authentication
3
+ *
4
+ * Tokens are account-level (not project-scoped).
5
+ * Set JACK_API_TOKEN in your environment for CI/CD and automated pipelines.
6
+ */
7
+
8
+ import { error, info, success } from "../lib/output.ts";
9
+ import {
10
+ type TokenInfo,
11
+ createApiToken,
12
+ listApiTokens,
13
+ revokeApiToken,
14
+ } from "../lib/services/token-operations.ts";
15
+ import { Events, track } from "../lib/telemetry.ts";
16
+
17
+ export default async function tokens(
18
+ subcommand?: string,
19
+ args: string[] = [],
20
+ flags: Record<string, unknown> = {},
21
+ ): Promise<void> {
22
+ if (!subcommand) {
23
+ return showHelp();
24
+ }
25
+
26
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
27
+ return showHelp();
28
+ }
29
+
30
+ switch (subcommand) {
31
+ case "create":
32
+ case "new":
33
+ return await createToken(args, flags);
34
+ case "list":
35
+ case "ls":
36
+ return await listTokens();
37
+ case "revoke":
38
+ case "rm":
39
+ case "delete":
40
+ return await revokeToken(args);
41
+ default:
42
+ error(`Unknown subcommand: ${subcommand}`);
43
+ info("Available: create, list, revoke");
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ function showHelp(): void {
49
+ console.error("");
50
+ info("jack tokens - Manage API tokens for headless authentication");
51
+ console.error("");
52
+ console.error("Commands:");
53
+ console.error(" create [name] Create a new API token");
54
+ console.error(" list List active tokens");
55
+ console.error(" revoke <id> Revoke a token");
56
+ console.error("");
57
+ console.error("Usage:");
58
+ console.error(" Set JACK_API_TOKEN in your environment for headless auth.");
59
+ console.error(" Tokens work in CI/CD, Docker, and automated pipelines.");
60
+ console.error("");
61
+ }
62
+
63
+ async function createToken(args: string[], flags: Record<string, unknown> = {}): Promise<void> {
64
+ // Accept name from --name flag or first positional arg
65
+ let name = "CLI Token";
66
+ if (flags.name && typeof flags.name === "string") {
67
+ name = flags.name;
68
+ } else if (args[0] && !args[0].startsWith("-")) {
69
+ name = args[0];
70
+ }
71
+
72
+ const data = await createApiToken(name);
73
+
74
+ track(Events.TOKEN_CREATED);
75
+
76
+ success("Token created");
77
+ console.error("");
78
+ console.error(` ${data.token}`);
79
+ console.error("");
80
+ console.error(" Save this token -- it will not be shown again.");
81
+ console.error("");
82
+ console.error(" Usage:");
83
+ console.error(" export JACK_API_TOKEN=<token>");
84
+ console.error(" jack ship");
85
+ console.error("");
86
+ }
87
+
88
+ async function listTokens(): Promise<void> {
89
+ const tokenList = await listApiTokens();
90
+
91
+ if (tokenList.length === 0) {
92
+ info("No active tokens");
93
+ return;
94
+ }
95
+
96
+ console.error("");
97
+ for (const t of tokenList) {
98
+ const lastUsed = t.last_used_at ? `last used ${t.last_used_at}` : "never used";
99
+ console.error(` ${t.id} ${t.name} (${lastUsed})`);
100
+ }
101
+ console.error("");
102
+ }
103
+
104
+ async function revokeToken(args: string[]): Promise<void> {
105
+ const tokenId = args[0];
106
+
107
+ if (!tokenId) {
108
+ error("Missing token ID");
109
+ info("Usage: jack tokens revoke <token-id>");
110
+ info("Run 'jack tokens list' to see token IDs");
111
+ process.exit(1);
112
+ }
113
+
114
+ await revokeApiToken(tokenId);
115
+
116
+ track(Events.TOKEN_REVOKED);
117
+
118
+ success(`Token revoked: ${tokenId}`);
119
+ }
@@ -19,13 +19,17 @@ export default async function whoami(): Promise<void> {
19
19
  item(`Name: ${creds.user.first_name}${creds.user.last_name ? ` ${creds.user.last_name}` : ""}`);
20
20
  }
21
21
 
22
- const expiresIn = creds.expires_at - Math.floor(Date.now() / 1000);
23
- if (expiresIn > 0) {
24
- const hours = Math.floor(expiresIn / 3600);
25
- const minutes = Math.floor((expiresIn % 3600) / 60);
26
- item(`Token expires: ${hours}h ${minutes}m`);
22
+ if (process.env.JACK_API_TOKEN) {
23
+ item("Auth: API token");
27
24
  } else {
28
- item("Token: expired (will refresh on next request)");
25
+ const expiresIn = creds.expires_at - Math.floor(Date.now() / 1000);
26
+ if (expiresIn > 0) {
27
+ const hours = Math.floor(expiresIn / 3600);
28
+ const minutes = Math.floor((expiresIn % 3600) / 60);
29
+ item(`Token expires: ${hours}h ${minutes}m`);
30
+ } else {
31
+ item("Token: expired (will refresh on next request)");
32
+ }
29
33
  }
30
34
  console.error("");
31
35
  }
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ const cli = meow(
35
35
  login Sign in
36
36
  logout Sign out
37
37
  whoami Show current user
38
+ tokens Manage API tokens
38
39
  update Update jack to latest version
39
40
 
40
41
  Project Management
@@ -181,6 +182,9 @@ const cli = meow(
181
182
  sort: {
182
183
  type: "string",
183
184
  },
185
+ name: {
186
+ type: "string",
187
+ },
184
188
  },
185
189
  },
186
190
  );
@@ -208,6 +212,7 @@ const [command, ...args] = cli.input;
208
212
  os: process.platform,
209
213
  arch: process.arch,
210
214
  node_version: process.version,
215
+ auth_method: process.env.JACK_API_TOKEN ? "token" : "oauth",
211
216
  });
212
217
 
213
218
  // Update lastIdentifyDate
@@ -220,6 +225,7 @@ const [command, ...args] = cli.input;
220
225
  os: process.platform,
221
226
  arch: process.arch,
222
227
  node_version: process.version,
228
+ auth_method: process.env.JACK_API_TOKEN ? "token" : "oauth",
223
229
  });
224
230
  }
225
231
  })();
@@ -285,6 +291,7 @@ try {
285
291
  managed: cli.flags.managed,
286
292
  byo: cli.flags.byo,
287
293
  dryRun: cli.flags.dryRun,
294
+ json: cli.flags.json,
288
295
  });
289
296
  break;
290
297
  }
@@ -389,6 +396,7 @@ try {
389
396
 
390
397
  await withTelemetry("services", services, { subcommand })(args[0], serviceArgs, {
391
398
  project: cli.flags.project,
399
+ json: cli.flags.json,
392
400
  });
393
401
  break;
394
402
  }
@@ -399,6 +407,15 @@ try {
399
407
  });
400
408
  break;
401
409
  }
410
+ case "tokens": {
411
+ const { default: tokens } = await import("./commands/tokens.ts");
412
+ await withTelemetry("tokens", tokens, { subcommand: args[0] })(
413
+ args[0],
414
+ args.slice(1),
415
+ cli.flags,
416
+ );
417
+ break;
418
+ }
402
419
  case "domain": {
403
420
  const { default: domain } = await import("./commands/domain.ts");
404
421
  await withTelemetry("domain", domain, { subcommand: args[0] })(args[0], args.slice(1));
@@ -73,6 +73,16 @@ export async function refreshToken(refreshTokenValue: string): Promise<TokenResp
73
73
  }
74
74
 
75
75
  export async function getValidAccessToken(): Promise<string | null> {
76
+ // Priority 1: API token from environment
77
+ const apiToken = process.env.JACK_API_TOKEN;
78
+ if (apiToken) {
79
+ if (!apiToken.startsWith("jkt_")) {
80
+ console.error("Warning: JACK_API_TOKEN should start with 'jkt_'");
81
+ }
82
+ return apiToken;
83
+ }
84
+
85
+ // Priority 2: Stored OAuth credentials
76
86
  const creds = await getCredentials();
77
87
  if (!creds) {
78
88
  return null;
@@ -102,7 +112,7 @@ export async function getValidAccessToken(): Promise<string | null> {
102
112
  export async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
103
113
  const token = await getValidAccessToken();
104
114
  if (!token) {
105
- throw new Error("Not authenticated. Run 'jack login' first.");
115
+ throw new Error("Not authenticated. Run 'jack login' or set JACK_API_TOKEN.");
106
116
  }
107
117
 
108
118
  return fetch(url, {
@@ -13,7 +13,7 @@ export async function requireAuth(): Promise<string> {
13
13
  throw new JackError(
14
14
  JackErrorCode.AUTH_FAILED,
15
15
  "Not logged in",
16
- "Run 'jack login' to sign in to jack cloud",
16
+ "Run 'jack login' to sign in, or set JACK_API_TOKEN for headless use",
17
17
  );
18
18
  }
19
19
 
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { text } from "@clack/prompts";
5
5
  import {
6
+ applyReferralCode,
6
7
  checkUsernameAvailable,
7
8
  getCurrentUserProfile,
8
9
  registerUser,
@@ -114,6 +115,11 @@ export async function runLoginFlow(options?: LoginFlowOptions): Promise<LoginFlo
114
115
  isNewUser = await promptForUsername(tokens.user.email, tokens.user.first_name);
115
116
  }
116
117
 
118
+ // Prompt for referral code for new users only (one-time, no retry)
119
+ if (isNewUser && process.stdout.isTTY) {
120
+ await promptForReferral();
121
+ }
122
+
117
123
  console.error("");
118
124
  const displayName = tokens.user.first_name || "you";
119
125
  if (isNewUser) {
@@ -294,3 +300,31 @@ function normalizeToUsername(input: string): string {
294
300
  .replace(/^-+|-+$/g, "")
295
301
  .slice(0, 39);
296
302
  }
303
+
304
+ /**
305
+ * Prompt new users for a referral code (one-time, no retry on failure).
306
+ */
307
+ async function promptForReferral(): Promise<void> {
308
+ console.error("");
309
+ const referralInput = await text({
310
+ message: "Were you referred by someone? Enter their username (or press Enter to skip):",
311
+ });
312
+
313
+ if (isCancel(referralInput)) {
314
+ return;
315
+ }
316
+
317
+ const code = referralInput.trim();
318
+ if (!code) {
319
+ return;
320
+ }
321
+
322
+ try {
323
+ const result = await applyReferralCode(code);
324
+ if (result.applied) {
325
+ success("Referral applied! You'll both get a bonus when you upgrade.");
326
+ }
327
+ } catch {
328
+ // Silently continue - referral is not critical
329
+ }
330
+ }
@@ -51,6 +51,9 @@ export type AuthState = "logged-in" | "not-logged-in" | "session-expired";
51
51
  * - "session-expired": had credentials but refresh failed
52
52
  */
53
53
  export async function getAuthState(): Promise<AuthState> {
54
+ // API token always counts as logged in
55
+ if (process.env.JACK_API_TOKEN) return "logged-in";
56
+
54
57
  const creds = await getCredentials();
55
58
  if (!creds) return "not-logged-in";
56
59
 
@@ -666,6 +666,35 @@ export async function setUsername(username: string): Promise<SetUsernameResponse
666
666
  return response.json() as Promise<SetUsernameResponse>;
667
667
  }
668
668
 
669
+ export interface ApplyReferralResult {
670
+ applied: boolean;
671
+ reason?: "invalid" | "self_referral" | "already_referred";
672
+ }
673
+
674
+ /**
675
+ * Apply a referral code (username) for the current user.
676
+ * Returns whether the code was applied successfully.
677
+ */
678
+ export async function applyReferralCode(code: string): Promise<ApplyReferralResult> {
679
+ const { authFetch } = await import("./auth/index.ts");
680
+
681
+ const response = await authFetch(`${getControlApiUrl()}/v1/referral/apply`, {
682
+ method: "POST",
683
+ headers: { "Content-Type": "application/json" },
684
+ body: JSON.stringify({ code }),
685
+ });
686
+
687
+ if (response.status === 429) {
688
+ return { applied: false, reason: "invalid" };
689
+ }
690
+
691
+ if (!response.ok) {
692
+ return { applied: false, reason: "invalid" };
693
+ }
694
+
695
+ return response.json() as Promise<ApplyReferralResult>;
696
+ }
697
+
669
698
  /**
670
699
  * Get the current user's profile including username.
671
700
  */
@@ -777,6 +806,36 @@ export interface LogSessionInfo {
777
806
  expires_at: string;
778
807
  }
779
808
 
809
+ // ============================================================================
810
+ // Cron Schedule Types
811
+ // ============================================================================
812
+
813
+ export interface CronScheduleInfo {
814
+ id: string;
815
+ expression: string;
816
+ description: string;
817
+ enabled: boolean;
818
+ next_run_at: string;
819
+ last_run_at: string | null;
820
+ last_run_status: string | null;
821
+ last_run_duration_ms: number | null;
822
+ consecutive_failures: number;
823
+ created_at: string;
824
+ }
825
+
826
+ export interface CreateCronScheduleResponse {
827
+ id: string;
828
+ expression: string;
829
+ description: string;
830
+ next_run_at: string;
831
+ }
832
+
833
+ export interface TriggerCronScheduleResponse {
834
+ triggered: boolean;
835
+ status: string;
836
+ duration_ms: number;
837
+ }
838
+
780
839
  export interface StartLogSessionResponse {
781
840
  success: boolean;
782
841
  session: LogSessionInfo;
@@ -807,3 +866,100 @@ export async function startLogSession(
807
866
 
808
867
  return response.json() as Promise<StartLogSessionResponse>;
809
868
  }
869
+
870
+ // ============================================================================
871
+ // Cron Schedule Operations
872
+ // ============================================================================
873
+
874
+ /**
875
+ * Create a cron schedule for a managed project.
876
+ */
877
+ export async function createCronSchedule(
878
+ projectId: string,
879
+ expression: string,
880
+ ): Promise<CreateCronScheduleResponse> {
881
+ const { authFetch } = await import("./auth/index.ts");
882
+
883
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/crons`, {
884
+ method: "POST",
885
+ headers: { "Content-Type": "application/json" },
886
+ body: JSON.stringify({ expression }),
887
+ });
888
+
889
+ if (!response.ok) {
890
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
891
+ message?: string;
892
+ };
893
+ throw new Error(err.message || `Failed to create cron schedule: ${response.status}`);
894
+ }
895
+
896
+ return response.json() as Promise<CreateCronScheduleResponse>;
897
+ }
898
+
899
+ /**
900
+ * List all cron schedules for a managed project.
901
+ */
902
+ export async function listCronSchedules(projectId: string): Promise<CronScheduleInfo[]> {
903
+ const { authFetch } = await import("./auth/index.ts");
904
+
905
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/crons`);
906
+
907
+ if (!response.ok) {
908
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
909
+ message?: string;
910
+ };
911
+ throw new Error(err.message || `Failed to list cron schedules: ${response.status}`);
912
+ }
913
+
914
+ const data = (await response.json()) as { schedules: CronScheduleInfo[] };
915
+ return data.schedules;
916
+ }
917
+
918
+ /**
919
+ * Delete a cron schedule from a managed project.
920
+ */
921
+ export async function deleteCronSchedule(projectId: string, cronId: string): Promise<void> {
922
+ const { authFetch } = await import("./auth/index.ts");
923
+
924
+ const response = await authFetch(
925
+ `${getControlApiUrl()}/v1/projects/${projectId}/crons/${cronId}`,
926
+ { method: "DELETE" },
927
+ );
928
+
929
+ if (response.status === 404) {
930
+ // Already deleted, treat as success
931
+ return;
932
+ }
933
+
934
+ if (!response.ok) {
935
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
936
+ message?: string;
937
+ };
938
+ throw new Error(err.message || `Failed to delete cron schedule: ${response.status}`);
939
+ }
940
+ }
941
+
942
+ /**
943
+ * Manually trigger a cron schedule on a managed project.
944
+ */
945
+ export async function triggerCronSchedule(
946
+ projectId: string,
947
+ expression: string,
948
+ ): Promise<TriggerCronScheduleResponse> {
949
+ const { authFetch } = await import("./auth/index.ts");
950
+
951
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/crons/trigger`, {
952
+ method: "POST",
953
+ headers: { "Content-Type": "application/json" },
954
+ body: JSON.stringify({ expression }),
955
+ });
956
+
957
+ if (!response.ok) {
958
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
959
+ message?: string;
960
+ };
961
+ throw new Error(err.message || `Failed to trigger cron schedule: ${response.status}`);
962
+ }
963
+
964
+ return response.json() as Promise<TriggerCronScheduleResponse>;
965
+ }
@@ -9,6 +9,7 @@ import { CONFIG_DIR } from "./config.ts";
9
9
  * MCP server configuration structure
10
10
  */
11
11
  export interface McpServerConfig {
12
+ type: "stdio";
12
13
  command: string;
13
14
  args: string[];
14
15
  env?: Record<string, string>;
@@ -48,19 +49,40 @@ export const APP_MCP_CONFIGS: Record<string, AppMcpConfig> = {
48
49
  const JACK_MCP_CONFIG_DIR = join(CONFIG_DIR, "mcp");
49
50
  const JACK_MCP_CONFIG_PATH = join(JACK_MCP_CONFIG_DIR, "config.json");
50
51
 
52
+ /**
53
+ * Find the jack binary path
54
+ * Checks common install locations
55
+ */
56
+ function findJackBinary(): string {
57
+ const bunBin = join(homedir(), ".bun", "bin", "jack");
58
+ const npmBin = join(homedir(), ".npm-global", "bin", "jack");
59
+ const homebrewBin = "/opt/homebrew/bin/jack";
60
+ const usrLocalBin = "/usr/local/bin/jack";
61
+
62
+ // Check in order of priority
63
+ for (const path of [bunBin, npmBin, homebrewBin, usrLocalBin]) {
64
+ if (existsSync(path)) {
65
+ return path;
66
+ }
67
+ }
68
+
69
+ // Fallback to just "jack" and hope PATH works
70
+ return "jack";
71
+ }
72
+
51
73
  /**
52
74
  * Returns the jack MCP server configuration
53
- * Includes PATH with common install locations so Claude Desktop can find jack
75
+ * Uses full path to jack binary for reliability
54
76
  */
55
77
  export function getJackMcpConfig(): McpServerConfig {
56
- // Build PATH with common locations where jack might be installed
57
- // ~/.bun/bin is where `bun link` installs global commands
78
+ // Build PATH with common locations (still needed for child processes)
58
79
  const bunBin = join(homedir(), ".bun", "bin");
59
80
  const npmBin = join(homedir(), ".npm-global", "bin");
60
81
  const defaultPaths = [bunBin, npmBin, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
61
82
 
62
83
  return {
63
- command: "jack",
84
+ type: "stdio",
85
+ command: findJackBinary(),
64
86
  args: ["mcp", "serve"],
65
87
  env: {
66
88
  PATH: defaultPaths.join(":"),
package/src/lib/output.ts CHANGED
@@ -179,7 +179,8 @@ export function box(title: string, lines: string[]): void {
179
179
  const gradient = "░".repeat(innerWidth);
180
180
 
181
181
  // Truncate text if too long for box
182
- const truncate = (text: string) => (text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text);
182
+ const truncate = (text: string) =>
183
+ text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text;
183
184
 
184
185
  // Pad plain text first, then apply colors (ANSI codes break padEnd calculation)
185
186
  const pad = (text: string) => ` ${truncate(text).padEnd(maxLen)} `;
@@ -226,7 +227,8 @@ export function celebrate(title: string, lines: string[]): void {
226
227
  const space = " ".repeat(innerWidth);
227
228
 
228
229
  // Truncate text if too long for box
229
- const truncate = (text: string) => (text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text);
230
+ const truncate = (text: string) =>
231
+ text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text;
230
232
 
231
233
  // Center text based on visual length, then apply colors
232
234
  const center = (text: string, applyBold = false) => {
package/src/lib/picker.ts CHANGED
@@ -86,7 +86,9 @@ export function requireTTY(): void {
86
86
  * Interactive project picker using @clack/core primitives
87
87
  * @param options.cloudOnly - If true, only shows cloud-only projects (for linking)
88
88
  */
89
- export async function pickProject(options?: PickProjectOptions): Promise<PickerResult | PickerCancelResult> {
89
+ export async function pickProject(
90
+ options?: PickProjectOptions,
91
+ ): Promise<PickerResult | PickerCancelResult> {
90
92
  // Fetch all projects
91
93
  let allProjects: ProjectListItem[];
92
94
  try {
@@ -47,7 +47,7 @@ import { debug, isDebug, printTimingSummary, timerEnd, timerStart } from "./debu
47
47
  import { ensureWranglerInstalled, validateModeAvailability } from "./deploy-mode.ts";
48
48
  import { detectSecrets, generateEnvFile, generateSecretsJson } from "./env-parser.ts";
49
49
  import { JackError, JackErrorCode } from "./errors.ts";
50
- import { type HookOutput, runHook } from "./hooks.ts";
50
+ import { type HookOutput, promptSelect, runHook } from "./hooks.ts";
51
51
  import { loadTemplateKeywords, matchTemplateByIntent } from "./intent.ts";
52
52
  import {
53
53
  type ManagedCreateResult,
@@ -1605,13 +1605,36 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1605
1605
  // User is logged into Jack Cloud - create managed project
1606
1606
  const orphanedProjectName = await getProjectNameFromDir(projectPath);
1607
1607
 
1608
- reporter.info(`Linking "${orphanedProjectName}" to jack cloud...`);
1609
-
1610
- // Get username for URL construction
1608
+ // Get username for confirmation prompt and URL construction
1611
1609
  const { getCurrentUserProfile } = await import("./control-plane.ts");
1612
1610
  const profile = await getCurrentUserProfile();
1613
1611
  const ownerUsername = profile?.username ?? undefined;
1614
1612
 
1613
+ // Confirm before creating new project
1614
+ if (interactive) {
1615
+ reporter.info("This project isn't linked to jack cloud.");
1616
+ const choice = await promptSelect(
1617
+ ["Yes", "No"],
1618
+ `Create new project "${orphanedProjectName}" under @${ownerUsername ?? "unknown"}?`,
1619
+ );
1620
+ if (choice !== 0) {
1621
+ reporter.info("Cancelled. To link to an existing project, use: jack link <project-id>");
1622
+ return {
1623
+ projectName: orphanedProjectName,
1624
+ workerUrl: null,
1625
+ deployMode: "managed" as DeployMode,
1626
+ };
1627
+ }
1628
+ } else {
1629
+ throw new JackError(
1630
+ JackErrorCode.PROJECT_NOT_FOUND,
1631
+ "Project not linked to jack cloud (non-interactive mode)",
1632
+ "Run interactively or use: jack link <project-id>",
1633
+ );
1634
+ }
1635
+
1636
+ reporter.info(`Linking "${orphanedProjectName}" to jack cloud...`);
1637
+
1615
1638
  // Create managed project on jack cloud
1616
1639
  const remoteResult = await createManagedProjectRemote(orphanedProjectName, reporter, {
1617
1640
  usePrebuilt: false,
@@ -1716,6 +1739,17 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1716
1739
  );
1717
1740
  }
1718
1741
 
1742
+ // Show current identity for visibility (managed mode only, not dry run)
1743
+ if (!dryRun) {
1744
+ const { getCurrentUserProfile } = await import("./control-plane.ts");
1745
+ const profile = await getCurrentUserProfile();
1746
+ if (profile?.username) {
1747
+ reporter.info(`Deploying as @${profile.username}...`);
1748
+ } else if (profile?.email) {
1749
+ reporter.info(`Deploying as ${profile.email}...`);
1750
+ }
1751
+ }
1752
+
1719
1753
  // Dry run: build for validation then stop before actual deployment
1720
1754
  // (deployToManagedProject handles its own build, so only build here for dry-run)
1721
1755
  if (dryRun) {