@getjack/jack 0.1.31 → 0.1.33
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/package.json +1 -1
- package/src/commands/deploys.ts +95 -0
- package/src/commands/link.ts +8 -0
- package/src/commands/mcp.ts +179 -4
- package/src/commands/rollback.ts +53 -0
- package/src/commands/services.ts +100 -20
- package/src/commands/ship.ts +30 -3
- package/src/commands/tokens.ts +134 -0
- package/src/commands/whoami.ts +51 -12
- package/src/index.ts +33 -0
- package/src/lib/agent-files.ts +54 -4
- package/src/lib/agent-integration.ts +4 -166
- package/src/lib/auth/client.ts +11 -1
- package/src/lib/auth/guard.ts +1 -1
- package/src/lib/auth/store.ts +3 -0
- package/src/lib/claude-hooks-installer.ts +55 -0
- package/src/lib/control-plane.ts +78 -40
- package/src/lib/debug.ts +2 -1
- package/src/lib/deploy-upload.ts +6 -0
- package/src/lib/hooks.ts +3 -1
- package/src/lib/managed-deploy.ts +12 -9
- package/src/lib/project-link.ts +6 -0
- package/src/lib/project-operations.ts +68 -22
- package/src/lib/services/token-operations.ts +84 -0
- package/src/lib/telemetry.ts +6 -0
- package/src/mcp/README.md +1 -1
- package/src/mcp/resources/index.ts +174 -16
- package/src/mcp/server.ts +23 -0
- package/src/mcp/tools/index.ts +133 -17
- package/src/mcp/types.ts +1 -0
- package/src/mcp/utils.ts +2 -1
- package/src/templates/index.ts +25 -73
- package/templates/CLAUDE.md +41 -0
- package/templates/ai-chat/.jack.json +10 -5
- package/templates/ai-chat/bun.lock +50 -1
- package/templates/ai-chat/package.json +5 -0
- package/templates/ai-chat/public/app.js +73 -0
- package/templates/ai-chat/public/index.html +14 -197
- package/templates/ai-chat/schema.sql +14 -0
- package/templates/ai-chat/src/index.ts +86 -102
- package/templates/ai-chat/wrangler.jsonc +8 -1
- package/templates/cron/.jack.json +66 -0
- package/templates/cron/bun.lock +23 -0
- package/templates/cron/package.json +16 -0
- package/templates/cron/schema.sql +24 -0
- package/templates/cron/src/index.ts +117 -0
- package/templates/cron/src/jobs.ts +139 -0
- package/templates/cron/src/webhooks.ts +95 -0
- package/templates/cron/tsconfig.json +17 -0
- package/templates/cron/wrangler.jsonc +11 -0
- package/templates/miniapp/.jack.json +1 -1
- package/templates/nextjs/.jack.json +1 -1
- package/templates/nextjs-auth/.jack.json +44 -0
- package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
- package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
- package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
- package/templates/nextjs-auth/app/error.tsx +44 -0
- package/templates/nextjs-auth/app/globals.css +1 -0
- package/templates/nextjs-auth/app/health/route.ts +3 -0
- package/templates/nextjs-auth/app/layout.tsx +24 -0
- package/templates/nextjs-auth/app/login/page.tsx +10 -0
- package/templates/nextjs-auth/app/page.tsx +86 -0
- package/templates/nextjs-auth/app/signup/page.tsx +10 -0
- package/templates/nextjs-auth/bun.lock +1065 -0
- package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
- package/templates/nextjs-auth/components/auth-form.tsx +191 -0
- package/templates/nextjs-auth/components/header.tsx +50 -0
- package/templates/nextjs-auth/components/user-menu.tsx +23 -0
- package/templates/nextjs-auth/lib/auth-client.ts +3 -0
- package/templates/nextjs-auth/lib/auth.ts +43 -0
- package/templates/nextjs-auth/lib/utils.ts +6 -0
- package/templates/nextjs-auth/middleware.ts +33 -0
- package/templates/nextjs-auth/next.config.ts +8 -0
- package/templates/nextjs-auth/open-next.config.ts +6 -0
- package/templates/nextjs-auth/package.json +33 -0
- package/templates/nextjs-auth/postcss.config.mjs +8 -0
- package/templates/nextjs-auth/schema.sql +49 -0
- package/templates/nextjs-auth/tsconfig.json +28 -0
- package/templates/nextjs-auth/wrangler.jsonc +23 -0
- package/templates/nextjs-clerk/.jack.json +54 -0
- package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
- package/templates/nextjs-clerk/app/globals.css +1 -0
- package/templates/nextjs-clerk/app/health/route.ts +3 -0
- package/templates/nextjs-clerk/app/layout.tsx +26 -0
- package/templates/nextjs-clerk/app/page.tsx +86 -0
- package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
- package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
- package/templates/nextjs-clerk/bun.lock +1055 -0
- package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
- package/templates/nextjs-clerk/components/header.tsx +40 -0
- package/templates/nextjs-clerk/lib/utils.ts +6 -0
- package/templates/nextjs-clerk/middleware.ts +18 -0
- package/templates/nextjs-clerk/next.config.ts +8 -0
- package/templates/nextjs-clerk/open-next.config.ts +6 -0
- package/templates/nextjs-clerk/package.json +31 -0
- package/templates/nextjs-clerk/postcss.config.mjs +8 -0
- package/templates/nextjs-clerk/tsconfig.json +28 -0
- package/templates/nextjs-clerk/wrangler.jsonc +17 -0
- package/templates/nextjs-shadcn/.jack.json +34 -0
- package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
- package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
- package/templates/nextjs-shadcn/app/globals.css +126 -0
- package/templates/nextjs-shadcn/app/health/route.ts +3 -0
- package/templates/nextjs-shadcn/app/layout.tsx +24 -0
- package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
- package/templates/nextjs-shadcn/app/page.tsx +180 -0
- package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
- package/templates/nextjs-shadcn/bun.lock +1789 -0
- package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
- package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
- package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
- package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
- package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
- package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
- package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
- package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
- package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
- package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
- package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
- package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
- package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
- package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
- package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
- package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
- package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
- package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
- package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
- package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
- package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
- package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
- package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
- package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
- package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
- package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
- package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
- package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
- package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
- package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
- package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
- package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
- package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
- package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
- package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
- package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
- package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
- package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
- package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
- package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
- package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
- package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
- package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
- package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
- package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
- package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
- package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
- package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
- package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
- package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
- package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
- package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
- package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
- package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
- package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
- package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
- package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
- package/templates/nextjs-shadcn/components.json +23 -0
- package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
- package/templates/nextjs-shadcn/lib/utils.ts +6 -0
- package/templates/nextjs-shadcn/next-env.d.ts +6 -0
- package/templates/nextjs-shadcn/next.config.ts +8 -0
- package/templates/nextjs-shadcn/open-next.config.ts +6 -0
- package/templates/nextjs-shadcn/package.json +55 -0
- package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
- package/templates/nextjs-shadcn/tsconfig.json +28 -0
- package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
- package/templates/resend/.jack.json +64 -0
- package/templates/resend/bun.lock +23 -0
- package/templates/resend/package.json +16 -0
- package/templates/resend/schema.sql +13 -0
- package/templates/resend/src/email.ts +165 -0
- package/templates/resend/src/index.ts +108 -0
- package/templates/resend/tsconfig.json +17 -0
- package/templates/resend/wrangler.jsonc +11 -0
- package/templates/saas/.jack.json +1 -1
- package/templates/ai-chat/public/chat.js +0 -149
package/src/commands/ship.ts
CHANGED
|
@@ -3,26 +3,53 @@ import { createReporter, output } from "../lib/output.ts";
|
|
|
3
3
|
import { deployProject } from "../lib/project-operations.ts";
|
|
4
4
|
|
|
5
5
|
export default async function ship(
|
|
6
|
-
options: { managed?: boolean; byo?: boolean; dryRun?: boolean } = {},
|
|
6
|
+
options: { managed?: boolean; byo?: boolean; dryRun?: boolean; json?: boolean; message?: string } = {},
|
|
7
7
|
): Promise<void> {
|
|
8
8
|
const isCi = process.env.CI === "true" || process.env.CI === "1";
|
|
9
|
+
const jsonOutput = options.json ?? false;
|
|
9
10
|
try {
|
|
10
11
|
const result = await deployProject({
|
|
11
12
|
projectPath: process.cwd(),
|
|
12
|
-
reporter: createReporter(),
|
|
13
|
-
interactive: !isCi,
|
|
13
|
+
reporter: jsonOutput ? undefined : createReporter(),
|
|
14
|
+
interactive: !isCi && !jsonOutput,
|
|
14
15
|
includeSecrets: !options.dryRun,
|
|
15
16
|
includeSync: !options.dryRun,
|
|
16
17
|
managed: options.managed,
|
|
17
18
|
byo: options.byo,
|
|
18
19
|
dryRun: options.dryRun,
|
|
20
|
+
message: options.message,
|
|
19
21
|
});
|
|
20
22
|
|
|
23
|
+
if (jsonOutput) {
|
|
24
|
+
console.log(
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
success: true,
|
|
27
|
+
projectName: result.projectName,
|
|
28
|
+
url: result.workerUrl,
|
|
29
|
+
deployMode: result.deployMode,
|
|
30
|
+
...(options.message && { message: options.message }),
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
if (!result.workerUrl && result.deployOutput) {
|
|
22
37
|
console.error(result.deployOutput);
|
|
23
38
|
}
|
|
24
39
|
} catch (error) {
|
|
25
40
|
const details = getErrorDetails(error);
|
|
41
|
+
|
|
42
|
+
if (jsonOutput) {
|
|
43
|
+
console.log(
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
success: false,
|
|
46
|
+
error: details.message,
|
|
47
|
+
suggestion: details.suggestion,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
process.exit(details.meta?.exitCode ?? 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
if (!details.meta?.reported) {
|
|
27
54
|
output.error(details.message);
|
|
28
55
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
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(" --expires <days> Token expires after N days");
|
|
55
|
+
console.error(" list List active tokens");
|
|
56
|
+
console.error(" revoke <id> Revoke a token");
|
|
57
|
+
console.error("");
|
|
58
|
+
console.error("Usage:");
|
|
59
|
+
console.error(" Set JACK_API_TOKEN in your environment for headless auth.");
|
|
60
|
+
console.error(" Tokens work in CI/CD, Docker, and automated pipelines.");
|
|
61
|
+
console.error("");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function createToken(args: string[], flags: Record<string, unknown> = {}): Promise<void> {
|
|
65
|
+
// Accept name from --name flag or first positional arg
|
|
66
|
+
let name = "CLI Token";
|
|
67
|
+
if (flags.name && typeof flags.name === "string") {
|
|
68
|
+
name = flags.name;
|
|
69
|
+
} else if (args[0] && !args[0].startsWith("-")) {
|
|
70
|
+
name = args[0];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Parse --expires <days> flag
|
|
74
|
+
let expiresInDays: number | undefined;
|
|
75
|
+
if (flags.expires !== undefined) {
|
|
76
|
+
const parsed = Number(flags.expires);
|
|
77
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
78
|
+
error("--expires must be a positive integer (number of days)");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
expiresInDays = parsed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = await createApiToken(name, expiresInDays);
|
|
85
|
+
|
|
86
|
+
track(Events.TOKEN_CREATED);
|
|
87
|
+
|
|
88
|
+
success("Token created");
|
|
89
|
+
console.error("");
|
|
90
|
+
console.error(` ${data.token}`);
|
|
91
|
+
console.error("");
|
|
92
|
+
console.error(" Save this token -- it will not be shown again.");
|
|
93
|
+
if (data.expires_at) {
|
|
94
|
+
console.error(` Expires: ${data.expires_at}`);
|
|
95
|
+
}
|
|
96
|
+
console.error("");
|
|
97
|
+
console.error(" Usage:");
|
|
98
|
+
console.error(" export JACK_API_TOKEN=<token>");
|
|
99
|
+
console.error(" jack ship");
|
|
100
|
+
console.error("");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function listTokens(): Promise<void> {
|
|
104
|
+
const tokenList = await listApiTokens();
|
|
105
|
+
|
|
106
|
+
if (tokenList.length === 0) {
|
|
107
|
+
info("No active tokens");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.error("");
|
|
112
|
+
for (const t of tokenList) {
|
|
113
|
+
const lastUsed = t.last_used_at ? `last used ${t.last_used_at}` : "never used";
|
|
114
|
+
console.error(` ${t.id} ${t.name} (${lastUsed})`);
|
|
115
|
+
}
|
|
116
|
+
console.error("");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function revokeToken(args: string[]): Promise<void> {
|
|
120
|
+
const tokenId = args[0];
|
|
121
|
+
|
|
122
|
+
if (!tokenId) {
|
|
123
|
+
error("Missing token ID");
|
|
124
|
+
info("Usage: jack tokens revoke <token-id>");
|
|
125
|
+
info("Run 'jack tokens list' to see token IDs");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await revokeApiToken(tokenId);
|
|
130
|
+
|
|
131
|
+
track(Events.TOKEN_REVOKED);
|
|
132
|
+
|
|
133
|
+
success(`Token revoked: ${tokenId}`);
|
|
134
|
+
}
|
package/src/commands/whoami.ts
CHANGED
|
@@ -1,31 +1,70 @@
|
|
|
1
|
+
import { authFetch } from "../lib/auth/index.ts";
|
|
1
2
|
import { getCredentials } from "../lib/auth/store.ts";
|
|
2
|
-
import {
|
|
3
|
+
import { getControlApiUrl } from "../lib/control-plane.ts";
|
|
4
|
+
import { error, info, item, success } from "../lib/output.ts";
|
|
3
5
|
|
|
4
6
|
export default async function whoami(): Promise<void> {
|
|
7
|
+
const apiToken = process.env.JACK_API_TOKEN;
|
|
5
8
|
const creds = await getCredentials();
|
|
6
9
|
|
|
7
|
-
if (!creds) {
|
|
10
|
+
if (!apiToken && !creds) {
|
|
8
11
|
info("Not logged in");
|
|
9
12
|
info("Run 'jack login' to sign in");
|
|
10
13
|
return;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
console.error("");
|
|
17
|
+
|
|
18
|
+
if (apiToken && !creds) {
|
|
19
|
+
// Token-only: fetch user info from control plane
|
|
20
|
+
try {
|
|
21
|
+
const res = await authFetch(`${getControlApiUrl()}/v1/me`);
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
error("API token is invalid or expired");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const data = (await res.json()) as {
|
|
27
|
+
user?: { email?: string; id?: string; first_name?: string; last_name?: string };
|
|
28
|
+
};
|
|
29
|
+
if (data.user) {
|
|
30
|
+
success("Logged in");
|
|
31
|
+
if (data.user.email) item(`Email: ${data.user.email}`);
|
|
32
|
+
if (data.user.id) item(`ID: ${data.user.id}`);
|
|
33
|
+
if (data.user.first_name) {
|
|
34
|
+
item(`Name: ${data.user.first_name}${data.user.last_name ? ` ${data.user.last_name}` : ""}`);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
success("Authenticated");
|
|
38
|
+
}
|
|
39
|
+
item(`Auth: API token (${apiToken.slice(4, 12)}...)`);
|
|
40
|
+
} catch {
|
|
41
|
+
error("Failed to reach control plane");
|
|
42
|
+
item(`Auth: API token (${apiToken.slice(4, 12)}...)`);
|
|
43
|
+
}
|
|
44
|
+
console.error("");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Has stored creds (with or without API token)
|
|
14
49
|
success("Logged in");
|
|
15
|
-
item(`Email: ${creds
|
|
16
|
-
item(`ID: ${creds
|
|
50
|
+
item(`Email: ${creds!.user.email}`);
|
|
51
|
+
item(`ID: ${creds!.user.id}`);
|
|
17
52
|
|
|
18
|
-
if (creds
|
|
19
|
-
item(`Name: ${creds
|
|
53
|
+
if (creds!.user.first_name) {
|
|
54
|
+
item(`Name: ${creds!.user.first_name}${creds!.user.last_name ? ` ${creds!.user.last_name}` : ""}`);
|
|
20
55
|
}
|
|
21
56
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const hours = Math.floor(expiresIn / 3600);
|
|
25
|
-
const minutes = Math.floor((expiresIn % 3600) / 60);
|
|
26
|
-
item(`Token expires: ${hours}h ${minutes}m`);
|
|
57
|
+
if (apiToken) {
|
|
58
|
+
item("Auth: API token");
|
|
27
59
|
} else {
|
|
28
|
-
|
|
60
|
+
const expiresIn = creds!.expires_at - Math.floor(Date.now() / 1000);
|
|
61
|
+
if (expiresIn > 0) {
|
|
62
|
+
const hours = Math.floor(expiresIn / 3600);
|
|
63
|
+
const minutes = Math.floor((expiresIn % 3600) / 60);
|
|
64
|
+
item(`Token expires: ${hours}h ${minutes}m`);
|
|
65
|
+
} else {
|
|
66
|
+
item("Token: expired (will refresh on next request)");
|
|
67
|
+
}
|
|
29
68
|
}
|
|
30
69
|
console.error("");
|
|
31
70
|
}
|
package/src/index.ts
CHANGED
|
@@ -18,10 +18,12 @@ const cli = meow(
|
|
|
18
18
|
new <name> [path] Create and deploy a project
|
|
19
19
|
vibe "<phrase>" Create from an idea
|
|
20
20
|
ship Push changes to production
|
|
21
|
+
rollback Roll back to previous deploy
|
|
21
22
|
|
|
22
23
|
Projects
|
|
23
24
|
open [name] Open in browser
|
|
24
25
|
logs Stream live logs
|
|
26
|
+
deploys List recent deployments
|
|
25
27
|
down [name] Undeploy from cloud
|
|
26
28
|
ls List all projects
|
|
27
29
|
info [name] Show project details
|
|
@@ -35,6 +37,7 @@ const cli = meow(
|
|
|
35
37
|
login Sign in
|
|
36
38
|
logout Sign out
|
|
37
39
|
whoami Show current user
|
|
40
|
+
tokens Manage API tokens
|
|
38
41
|
update Update jack to latest version
|
|
39
42
|
|
|
40
43
|
Project Management
|
|
@@ -181,6 +184,12 @@ const cli = meow(
|
|
|
181
184
|
sort: {
|
|
182
185
|
type: "string",
|
|
183
186
|
},
|
|
187
|
+
name: {
|
|
188
|
+
type: "string",
|
|
189
|
+
},
|
|
190
|
+
to: {
|
|
191
|
+
type: "string",
|
|
192
|
+
},
|
|
184
193
|
},
|
|
185
194
|
},
|
|
186
195
|
);
|
|
@@ -208,6 +217,7 @@ const [command, ...args] = cli.input;
|
|
|
208
217
|
os: process.platform,
|
|
209
218
|
arch: process.arch,
|
|
210
219
|
node_version: process.version,
|
|
220
|
+
auth_method: process.env.JACK_API_TOKEN ? "token" : "oauth",
|
|
211
221
|
});
|
|
212
222
|
|
|
213
223
|
// Update lastIdentifyDate
|
|
@@ -220,6 +230,7 @@ const [command, ...args] = cli.input;
|
|
|
220
230
|
os: process.platform,
|
|
221
231
|
arch: process.arch,
|
|
222
232
|
node_version: process.version,
|
|
233
|
+
auth_method: process.env.JACK_API_TOKEN ? "token" : "oauth",
|
|
223
234
|
});
|
|
224
235
|
}
|
|
225
236
|
})();
|
|
@@ -285,6 +296,8 @@ try {
|
|
|
285
296
|
managed: cli.flags.managed,
|
|
286
297
|
byo: cli.flags.byo,
|
|
287
298
|
dryRun: cli.flags.dryRun,
|
|
299
|
+
json: cli.flags.json,
|
|
300
|
+
message: cli.flags.message,
|
|
288
301
|
});
|
|
289
302
|
break;
|
|
290
303
|
}
|
|
@@ -294,6 +307,16 @@ try {
|
|
|
294
307
|
await withTelemetry("logs", logs)({ label: cli.flags.label });
|
|
295
308
|
break;
|
|
296
309
|
}
|
|
310
|
+
case "deploys": {
|
|
311
|
+
const { default: deploys } = await import("./commands/deploys.ts");
|
|
312
|
+
await withTelemetry("deploys", deploys)({ all: cli.flags.all });
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case "rollback": {
|
|
316
|
+
const { default: rollback } = await import("./commands/rollback.ts");
|
|
317
|
+
await withTelemetry("rollback", rollback)({ to: cli.flags.to });
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
297
320
|
case "agents": {
|
|
298
321
|
const { default: agents } = await import("./commands/agents.ts");
|
|
299
322
|
await withTelemetry("agents", agents, { subcommand: args[0] })(args[0], args.slice(1), {
|
|
@@ -389,6 +412,7 @@ try {
|
|
|
389
412
|
|
|
390
413
|
await withTelemetry("services", services, { subcommand })(args[0], serviceArgs, {
|
|
391
414
|
project: cli.flags.project,
|
|
415
|
+
json: cli.flags.json,
|
|
392
416
|
});
|
|
393
417
|
break;
|
|
394
418
|
}
|
|
@@ -399,6 +423,15 @@ try {
|
|
|
399
423
|
});
|
|
400
424
|
break;
|
|
401
425
|
}
|
|
426
|
+
case "tokens": {
|
|
427
|
+
const { default: tokens } = await import("./commands/tokens.ts");
|
|
428
|
+
await withTelemetry("tokens", tokens, { subcommand: args[0] })(
|
|
429
|
+
args[0],
|
|
430
|
+
args.slice(1),
|
|
431
|
+
cli.flags,
|
|
432
|
+
);
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
402
435
|
case "domain": {
|
|
403
436
|
const { default: domain } = await import("./commands/domain.ts");
|
|
404
437
|
await withTelemetry("domain", domain, { subcommand: args[0] })(args[0], args.slice(1));
|
package/src/lib/agent-files.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { AgentConfig, AgentDefinition } from "./agents.ts";
|
|
|
7
7
|
* Template for AGENTS.md
|
|
8
8
|
*/
|
|
9
9
|
function generateAgentsMd(projectName: string, template: Template): string {
|
|
10
|
-
const summary = template.agentContext?.summary || "A
|
|
10
|
+
const summary = template.agentContext?.summary || "A jack project";
|
|
11
11
|
const fullText = template.agentContext?.full_text || "";
|
|
12
12
|
|
|
13
13
|
return `# ${projectName}
|
|
@@ -16,16 +16,66 @@ function generateAgentsMd(projectName: string, template: Template): string {
|
|
|
16
16
|
|
|
17
17
|
## Deployment
|
|
18
18
|
|
|
19
|
-
This project is deployed
|
|
19
|
+
This project is deployed and managed via jack:
|
|
20
20
|
|
|
21
21
|
\`\`\`bash
|
|
22
|
-
jack ship # Deploy to
|
|
22
|
+
jack ship # Deploy to production
|
|
23
23
|
jack logs # Stream production logs
|
|
24
24
|
\`\`\`
|
|
25
25
|
|
|
26
26
|
All deployment is handled by jack. Never run \`wrangler\` commands directly.
|
|
27
27
|
|
|
28
|
+
## Quick Commands
|
|
29
|
+
|
|
30
|
+
| Command | What it does |
|
|
31
|
+
|---------|--------------|
|
|
32
|
+
| \`jack ship\` | Deploy to production |
|
|
33
|
+
| \`jack logs\` | Stream live logs |
|
|
34
|
+
| \`jack services\` | Manage databases, KV, and other bindings |
|
|
35
|
+
| \`jack secrets\` | Manage environment secrets |
|
|
36
|
+
|
|
37
|
+
## Services & Bindings
|
|
38
|
+
|
|
39
|
+
Jack manages your project's services. To add a database:
|
|
40
|
+
|
|
41
|
+
\`\`\`bash
|
|
42
|
+
jack services db create
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
To query it:
|
|
46
|
+
|
|
47
|
+
\`\`\`bash
|
|
48
|
+
jack services db query "SELECT * FROM users"
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
28
51
|
${fullText}
|
|
52
|
+
|
|
53
|
+
## For AI Agents
|
|
54
|
+
|
|
55
|
+
### MCP Tools
|
|
56
|
+
|
|
57
|
+
If jack MCP is connected, **always prefer these tools over CLI commands or wrangler**:
|
|
58
|
+
|
|
59
|
+
| Tool | Use for |
|
|
60
|
+
|------|---------|
|
|
61
|
+
| \`mcp__jack__create_project\` | Create a new project (supports forking via \`template: "username/slug"\`) |
|
|
62
|
+
| \`mcp__jack__deploy_project\` | Deploy changes |
|
|
63
|
+
| \`mcp__jack__get_project_status\` | Check deployment status and URL |
|
|
64
|
+
| \`mcp__jack__list_projects\` | List all projects |
|
|
65
|
+
| \`mcp__jack__create_database\` | Create a database |
|
|
66
|
+
| \`mcp__jack__execute_sql\` | Query the database (read-only by default) |
|
|
67
|
+
| \`mcp__jack__list_databases\` | List project databases |
|
|
68
|
+
| \`mcp__jack__create_storage_bucket\` | Create object storage |
|
|
69
|
+
| \`mcp__jack__create_vectorize_index\` | Create vector search index |
|
|
70
|
+
| \`mcp__jack__tail_logs\` | Debug with live log samples |
|
|
71
|
+
| \`mcp__jack__start_log_session\` | Start real-time log stream |
|
|
72
|
+
| \`mcp__jack__create_cron\` | Create scheduled tasks |
|
|
73
|
+
| \`mcp__jack__list_domains\` | List custom domains |
|
|
74
|
+
| \`mcp__jack__connect_domain\` | Add a custom domain |
|
|
75
|
+
|
|
76
|
+
### Documentation
|
|
77
|
+
|
|
78
|
+
Full jack documentation: https://docs.getjack.org/llms-full.txt
|
|
29
79
|
`;
|
|
30
80
|
}
|
|
31
81
|
|
|
@@ -39,7 +89,7 @@ See [AGENTS.md](./AGENTS.md) for complete project context and deployment instruc
|
|
|
39
89
|
|
|
40
90
|
## Quick Commands
|
|
41
91
|
|
|
42
|
-
- **Deploy**: \`jack ship\` - Deploy to
|
|
92
|
+
- **Deploy**: \`jack ship\` - Deploy to production
|
|
43
93
|
- **Logs**: \`jack logs\` - Stream production logs
|
|
44
94
|
|
|
45
95
|
## Important
|
|
@@ -1,165 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent integration module
|
|
3
3
|
*
|
|
4
|
-
* Ensures AI agents have
|
|
4
|
+
* Ensures AI agents have MCP configured for jack projects.
|
|
5
5
|
* Called during both project creation and first BYO deploy.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import type { Template } from "../templates/types.ts";
|
|
11
8
|
import { installMcpConfigsToAllApps, isAppInstalled } from "./mcp-config.ts";
|
|
12
9
|
|
|
13
10
|
export interface EnsureAgentResult {
|
|
14
11
|
mcpInstalled: string[];
|
|
15
|
-
jackMdCreated: boolean;
|
|
16
|
-
referencesAdded: string[];
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
export interface EnsureAgentOptions {
|
|
20
|
-
template?: Template;
|
|
21
15
|
silent?: boolean;
|
|
22
|
-
projectName?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Generate JACK.md content
|
|
27
|
-
* Uses template agentContext if available, otherwise generic jack instructions
|
|
28
|
-
*/
|
|
29
|
-
export function generateJackMd(projectName?: string, template?: Template): string {
|
|
30
|
-
const header = projectName ? `# ${projectName}\n\n` : "# Jack\n\n";
|
|
31
|
-
|
|
32
|
-
const templateSummary = template?.agentContext?.summary;
|
|
33
|
-
const templateFullText = template?.agentContext?.full_text;
|
|
34
|
-
|
|
35
|
-
const summarySection = templateSummary ? `> ${templateSummary}\n\n` : "";
|
|
36
|
-
|
|
37
|
-
const templateSection = templateFullText ? `${templateFullText}\n\n` : "";
|
|
38
|
-
|
|
39
|
-
return `${header}${summarySection}This project is deployed and managed via jack.
|
|
40
|
-
|
|
41
|
-
## Quick Commands
|
|
42
|
-
|
|
43
|
-
| Command | What it does |
|
|
44
|
-
|---------|--------------|
|
|
45
|
-
| \`jack ship\` | Deploy to production |
|
|
46
|
-
| \`jack logs\` | Stream live logs |
|
|
47
|
-
| \`jack services\` | Manage databases, KV, and other bindings |
|
|
48
|
-
| \`jack secrets\` | Manage environment secrets |
|
|
49
|
-
|
|
50
|
-
## Important
|
|
51
|
-
|
|
52
|
-
- **Never run \`wrangler\` commands directly** - jack handles all infrastructure
|
|
53
|
-
- Use \`jack services db\` to create and query databases
|
|
54
|
-
- Secrets sync automatically across deploys
|
|
55
|
-
|
|
56
|
-
## Services & Bindings
|
|
57
|
-
|
|
58
|
-
Jack manages your project's services. To add a database:
|
|
59
|
-
|
|
60
|
-
\`\`\`bash
|
|
61
|
-
jack services db create
|
|
62
|
-
\`\`\`
|
|
63
|
-
|
|
64
|
-
To query it:
|
|
65
|
-
|
|
66
|
-
\`\`\`bash
|
|
67
|
-
jack services db query "SELECT * FROM users"
|
|
68
|
-
\`\`\`
|
|
69
|
-
|
|
70
|
-
More bindings (KV, R2, queues) coming soon.
|
|
71
|
-
|
|
72
|
-
${templateSection}## For AI Agents
|
|
73
|
-
|
|
74
|
-
### MCP Tools
|
|
75
|
-
|
|
76
|
-
If jack MCP is connected, prefer these tools over CLI commands:
|
|
77
|
-
|
|
78
|
-
| Tool | Use for |
|
|
79
|
-
|------|---------|
|
|
80
|
-
| \`mcp__jack__deploy_project\` | Deploy changes |
|
|
81
|
-
| \`mcp__jack__create_database\` | Create a new database |
|
|
82
|
-
| \`mcp__jack__execute_sql\` | Query the database |
|
|
83
|
-
| \`mcp__jack__list_projects\` | List all projects |
|
|
84
|
-
| \`mcp__jack__get_project_status\` | Check deployment status |
|
|
85
|
-
|
|
86
|
-
### Documentation
|
|
87
|
-
|
|
88
|
-
Full jack documentation: https://docs.getjack.org/llms-full.txt
|
|
89
|
-
`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Create JACK.md if it doesn't exist
|
|
94
|
-
*/
|
|
95
|
-
async function ensureJackMd(
|
|
96
|
-
projectPath: string,
|
|
97
|
-
projectName?: string,
|
|
98
|
-
template?: Template,
|
|
99
|
-
): Promise<boolean> {
|
|
100
|
-
const jackMdPath = join(projectPath, "JACK.md");
|
|
101
|
-
|
|
102
|
-
if (existsSync(jackMdPath)) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const content = generateJackMd(projectName, template);
|
|
107
|
-
await Bun.write(jackMdPath, content);
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Append JACK.md reference to existing agent files (CLAUDE.md, AGENTS.md)
|
|
113
|
-
*/
|
|
114
|
-
async function appendJackMdReferences(projectPath: string): Promise<string[]> {
|
|
115
|
-
const filesToCheck = ["CLAUDE.md", "AGENTS.md"];
|
|
116
|
-
const referencesAdded: string[] = [];
|
|
117
|
-
const jackMdPath = join(projectPath, "JACK.md");
|
|
118
|
-
|
|
119
|
-
// Only add references if JACK.md exists
|
|
120
|
-
if (!existsSync(jackMdPath)) {
|
|
121
|
-
return referencesAdded;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const referenceBlock = `<!-- Added by jack -->
|
|
125
|
-
> **Jack project** - See [JACK.md](./JACK.md) for deployment, services, and bindings.
|
|
126
|
-
|
|
127
|
-
`;
|
|
128
|
-
|
|
129
|
-
for (const filename of filesToCheck) {
|
|
130
|
-
const filePath = join(projectPath, filename);
|
|
131
|
-
|
|
132
|
-
if (!existsSync(filePath)) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const content = await Bun.file(filePath).text();
|
|
138
|
-
|
|
139
|
-
// Skip if reference already exists
|
|
140
|
-
if (content.includes("JACK.md") || content.includes("<!-- Added by jack -->")) {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Find position after first heading, or prepend if no heading
|
|
145
|
-
const headingMatch = content.match(/^#[^\n]*\n/m);
|
|
146
|
-
let newContent: string;
|
|
147
|
-
|
|
148
|
-
if (headingMatch && headingMatch.index !== undefined) {
|
|
149
|
-
const insertPos = headingMatch.index + headingMatch[0].length;
|
|
150
|
-
newContent = content.slice(0, insertPos) + "\n" + referenceBlock + content.slice(insertPos);
|
|
151
|
-
} else {
|
|
152
|
-
newContent = referenceBlock + content;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
await Bun.write(filePath, newContent);
|
|
156
|
-
referencesAdded.push(filename);
|
|
157
|
-
} catch {
|
|
158
|
-
// Ignore errors reading/writing individual files
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return referencesAdded;
|
|
163
16
|
}
|
|
164
17
|
|
|
165
18
|
/**
|
|
@@ -186,31 +39,16 @@ async function ensureMcpConfigured(): Promise<string[]> {
|
|
|
186
39
|
/**
|
|
187
40
|
* Ensure agent integration is set up for a project
|
|
188
41
|
*
|
|
189
|
-
*
|
|
190
|
-
* 1. Creates JACK.md if not exists (with template context if available)
|
|
191
|
-
* 2. Appends JACK.md reference to existing CLAUDE.md/AGENTS.md
|
|
192
|
-
* 3. Installs MCP config to detected AI apps
|
|
193
|
-
*
|
|
42
|
+
* Installs MCP config to detected AI apps.
|
|
194
43
|
* Safe to call multiple times - all operations are idempotent.
|
|
195
44
|
*/
|
|
196
45
|
export async function ensureAgentIntegration(
|
|
197
|
-
|
|
198
|
-
|
|
46
|
+
_projectPath: string,
|
|
47
|
+
_options: EnsureAgentOptions = {},
|
|
199
48
|
): Promise<EnsureAgentResult> {
|
|
200
|
-
const { template, projectName } = options;
|
|
201
|
-
|
|
202
|
-
// 1. Create JACK.md if not exists
|
|
203
|
-
const jackMdCreated = await ensureJackMd(projectPath, projectName, template);
|
|
204
|
-
|
|
205
|
-
// 2. Append references to existing agent files
|
|
206
|
-
const referencesAdded = await appendJackMdReferences(projectPath);
|
|
207
|
-
|
|
208
|
-
// 3. Ensure MCP is configured
|
|
209
49
|
const mcpInstalled = await ensureMcpConfigured();
|
|
210
50
|
|
|
211
51
|
return {
|
|
212
52
|
mcpInstalled,
|
|
213
|
-
jackMdCreated,
|
|
214
|
-
referencesAdded,
|
|
215
53
|
};
|
|
216
54
|
}
|
package/src/lib/auth/client.ts
CHANGED
|
@@ -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'
|
|
115
|
+
throw new Error("Not authenticated. Run 'jack login' or set JACK_API_TOKEN.");
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
return fetch(url, {
|
package/src/lib/auth/guard.ts
CHANGED
|
@@ -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
|
|
16
|
+
"Run 'jack login' to sign in, or set JACK_API_TOKEN for headless use",
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
|