@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/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { fetchDeployments } from "../lib/control-plane.ts";
|
|
2
|
+
import { error, info, item } from "../lib/output.ts";
|
|
3
|
+
import { readProjectLink } from "../lib/project-link.ts";
|
|
4
|
+
import { getProjectNameFromDir } from "../lib/storage/index.ts";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_LIMIT = 5;
|
|
7
|
+
const ALL_LIMIT = 50;
|
|
8
|
+
|
|
9
|
+
function humanizeSource(source: string): string {
|
|
10
|
+
if (source.startsWith("code:")) return "cli";
|
|
11
|
+
if (source.startsWith("prebuilt:")) return "template";
|
|
12
|
+
if (source.startsWith("template:")) return "template";
|
|
13
|
+
if (source.startsWith("rollback:")) return "rollback";
|
|
14
|
+
return source;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function humanizeStatus(status: string): string {
|
|
18
|
+
if (status === "queued") return "interrupted";
|
|
19
|
+
return status;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface DeploysOptions {
|
|
23
|
+
all?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* List recent deployments for a project
|
|
28
|
+
*/
|
|
29
|
+
export default async function deploys(options: DeploysOptions = {}): Promise<void> {
|
|
30
|
+
const link = await readProjectLink(process.cwd());
|
|
31
|
+
|
|
32
|
+
if (!link) {
|
|
33
|
+
error("Not a jack project");
|
|
34
|
+
info("Run this command from a linked project directory");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (link.deploy_mode !== "managed") {
|
|
39
|
+
info("Deploy history is available for managed projects only");
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let result;
|
|
44
|
+
try {
|
|
45
|
+
result = await fetchDeployments(link.project_id);
|
|
46
|
+
} catch {
|
|
47
|
+
error("Could not fetch deployments");
|
|
48
|
+
info("Check your network connection and try again");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (result.deployments.length === 0) {
|
|
53
|
+
info("No deployments yet");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let projectName: string;
|
|
58
|
+
try {
|
|
59
|
+
projectName = await getProjectNameFromDir(process.cwd());
|
|
60
|
+
} catch {
|
|
61
|
+
projectName = link.project_id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const limit = options.all ? ALL_LIMIT : DEFAULT_LIMIT;
|
|
65
|
+
const shown = result.deployments.slice(0, limit);
|
|
66
|
+
|
|
67
|
+
console.error("");
|
|
68
|
+
info(`Deployments for ${projectName} (${result.total} total)`);
|
|
69
|
+
console.error("");
|
|
70
|
+
|
|
71
|
+
// icon + id status source time
|
|
72
|
+
console.error(" ID Status Source Deployed");
|
|
73
|
+
console.error(" ── ────── ────── ────────");
|
|
74
|
+
|
|
75
|
+
for (const deploy of shown) {
|
|
76
|
+
const time = new Date(deploy.created_at).toLocaleString();
|
|
77
|
+
const shortId = deploy.id.length > 12 ? deploy.id.slice(4, 12) : deploy.id;
|
|
78
|
+
const status = humanizeStatus(deploy.status);
|
|
79
|
+
const source = humanizeSource(deploy.source);
|
|
80
|
+
const icon = status === "live" ? "✓" : status === "failed" ? "✗" : "○";
|
|
81
|
+
item(`${icon} ${shortId} ${status.padEnd(12)} ${source.padEnd(10)} ${time}`);
|
|
82
|
+
if (deploy.message) {
|
|
83
|
+
console.error(` "${deploy.message}"`);
|
|
84
|
+
}
|
|
85
|
+
if (deploy.error_message) {
|
|
86
|
+
console.error(` ${deploy.error_message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!options.all && result.total > DEFAULT_LIMIT) {
|
|
91
|
+
console.error("");
|
|
92
|
+
info(`Showing ${shown.length} of ${result.total}. Use --all to see more.`);
|
|
93
|
+
}
|
|
94
|
+
console.error("");
|
|
95
|
+
}
|
package/src/commands/link.ts
CHANGED
|
@@ -32,6 +32,14 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// Ensure hooks are installed for existing projects (idempotent)
|
|
36
|
+
try {
|
|
37
|
+
const { installClaudeCodeHooks } = await import("../lib/claude-hooks-installer.ts");
|
|
38
|
+
await installClaudeCodeHooks(process.cwd());
|
|
39
|
+
} catch {
|
|
40
|
+
// Non-critical
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
error("This directory is already linked");
|
|
36
44
|
info(`Linked to: ${projectDisplay}`);
|
|
37
45
|
info("To re-link, first run: jack unlink");
|
package/src/commands/mcp.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -38,11 +39,19 @@ export default async function mcp(subcommand?: string, options: McpOptions = {})
|
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
if (subcommand === "context") {
|
|
43
|
+
await outputProjectContext();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
error(
|
|
48
|
+
"Unknown subcommand. Use: jack mcp serve, jack mcp install, jack mcp test, or jack mcp context",
|
|
49
|
+
);
|
|
42
50
|
info("Usage:");
|
|
43
51
|
info(" jack mcp serve [--project /path] [--debug] Start MCP server");
|
|
44
52
|
info(" jack mcp install Install/repair MCP config for AI agents");
|
|
45
53
|
info(" jack mcp test Test MCP server connectivity");
|
|
54
|
+
info(" jack mcp context Output project context for hooks");
|
|
46
55
|
process.exit(1);
|
|
47
56
|
}
|
|
48
57
|
|
|
@@ -85,14 +94,14 @@ async function installMcpConfig(): Promise<void> {
|
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
if (skipped.length > 0) {
|
|
88
|
-
info(
|
|
97
|
+
info("\nSkipped (not installed):");
|
|
89
98
|
for (const app of skipped) {
|
|
90
99
|
item(` ${app}`);
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
if (failed.length > 0) {
|
|
95
|
-
error(
|
|
104
|
+
error("\nFailed to install:");
|
|
96
105
|
for (const app of failed) {
|
|
97
106
|
item(` ${app}`);
|
|
98
107
|
}
|
|
@@ -105,6 +114,172 @@ async function installMcpConfig(): Promise<void> {
|
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Parse wrangler config (jsonc or toml) and extract binding info.
|
|
119
|
+
* Returns null on any failure — never blocks session start.
|
|
120
|
+
*/
|
|
121
|
+
async function parseWranglerBindings(cwd: string): Promise<{
|
|
122
|
+
databases: string[];
|
|
123
|
+
buckets: string[];
|
|
124
|
+
vectorize: string[];
|
|
125
|
+
ai: boolean;
|
|
126
|
+
kv: string[];
|
|
127
|
+
} | null> {
|
|
128
|
+
try {
|
|
129
|
+
const jsoncPath = join(cwd, "wrangler.jsonc");
|
|
130
|
+
const tomlPath = join(cwd, "wrangler.toml");
|
|
131
|
+
|
|
132
|
+
let config: Record<string, unknown> | null = null;
|
|
133
|
+
|
|
134
|
+
if (existsSync(jsoncPath)) {
|
|
135
|
+
const content = await readFile(jsoncPath, "utf-8");
|
|
136
|
+
const jsonContent = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
137
|
+
config = JSON.parse(jsonContent);
|
|
138
|
+
} else if (existsSync(tomlPath)) {
|
|
139
|
+
// For toml, just check for key patterns — not worth a full parser here
|
|
140
|
+
const content = await readFile(tomlPath, "utf-8");
|
|
141
|
+
return {
|
|
142
|
+
databases: content.includes("d1_databases") ? ["(see wrangler.toml)"] : [],
|
|
143
|
+
buckets: content.includes("r2_buckets") ? ["(see wrangler.toml)"] : [],
|
|
144
|
+
vectorize: content.includes("vectorize") ? ["(see wrangler.toml)"] : [],
|
|
145
|
+
ai: content.includes("[ai]"),
|
|
146
|
+
kv: content.includes("kv_namespaces") ? ["(see wrangler.toml)"] : [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!config) return null;
|
|
151
|
+
|
|
152
|
+
const databases =
|
|
153
|
+
(config.d1_databases as { database_name?: string; binding?: string }[])?.map(
|
|
154
|
+
(d) => d.database_name || d.binding || "unknown",
|
|
155
|
+
) ?? [];
|
|
156
|
+
const buckets =
|
|
157
|
+
(config.r2_buckets as { bucket_name?: string; binding?: string }[])?.map(
|
|
158
|
+
(b) => b.bucket_name || b.binding || "unknown",
|
|
159
|
+
) ?? [];
|
|
160
|
+
const vectorize =
|
|
161
|
+
(config.vectorize as { index_name?: string; binding?: string }[])?.map(
|
|
162
|
+
(v) => v.index_name || v.binding || "unknown",
|
|
163
|
+
) ?? [];
|
|
164
|
+
const ai = !!config.ai;
|
|
165
|
+
const kv =
|
|
166
|
+
(config.kv_namespaces as { binding?: string }[])?.map((k) => k.binding || "unknown") ?? [];
|
|
167
|
+
|
|
168
|
+
return { databases, buckets, vectorize, ai, kv };
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function outputProjectContext(): Promise<void> {
|
|
175
|
+
try {
|
|
176
|
+
const cwd = process.cwd();
|
|
177
|
+
const { readProjectLink, readTemplateMetadata } = await import("../lib/project-link.ts");
|
|
178
|
+
const link = await readProjectLink(cwd);
|
|
179
|
+
|
|
180
|
+
// Silent exit if not a jack project — don't leak into non-jack sessions
|
|
181
|
+
if (!link) return;
|
|
182
|
+
|
|
183
|
+
// Fire-and-forget telemetry (no latency added)
|
|
184
|
+
const { track, Events } = await import("../lib/telemetry.ts");
|
|
185
|
+
track(Events.HOOK_SESSION_CONTEXT, {
|
|
186
|
+
deploy_mode: link.deploy_mode,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const sections: string[] = [];
|
|
190
|
+
|
|
191
|
+
const { getProjectNameFromDir } = await import("../lib/storage/index.ts");
|
|
192
|
+
let name = "unknown";
|
|
193
|
+
try {
|
|
194
|
+
name = await getProjectNameFromDir(cwd);
|
|
195
|
+
} catch {
|
|
196
|
+
// No wrangler config
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// --- Section 1: Project identity ---
|
|
200
|
+
const lines = [`# Jack Project: ${name}`, ""];
|
|
201
|
+
if (link.deploy_mode === "managed" && link.owner_username) {
|
|
202
|
+
lines.push(`- **URL:** https://${link.owner_username}-${name}.runjack.xyz`);
|
|
203
|
+
}
|
|
204
|
+
lines.push(`- **Project ID:** ${link.project_id}`);
|
|
205
|
+
lines.push(
|
|
206
|
+
`- **Deploy mode:** ${link.deploy_mode === "managed" ? "Jack Cloud (managed)" : "BYO (bring your own Cloudflare account)"}`,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Template origin
|
|
210
|
+
const templateMeta = await readTemplateMetadata(cwd);
|
|
211
|
+
if (templateMeta) {
|
|
212
|
+
lines.push(`- **Template:** ${templateMeta.name} (${templateMeta.type})`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Section 2: Detected services ---
|
|
216
|
+
const bindings = await parseWranglerBindings(cwd);
|
|
217
|
+
if (bindings) {
|
|
218
|
+
const services: string[] = [];
|
|
219
|
+
if (bindings.databases.length > 0)
|
|
220
|
+
services.push(`D1 databases: ${bindings.databases.join(", ")}`);
|
|
221
|
+
if (bindings.buckets.length > 0) services.push(`R2 storage: ${bindings.buckets.join(", ")}`);
|
|
222
|
+
if (bindings.vectorize.length > 0)
|
|
223
|
+
services.push(`Vectorize indexes: ${bindings.vectorize.join(", ")}`);
|
|
224
|
+
if (bindings.kv.length > 0) services.push(`KV namespaces: ${bindings.kv.join(", ")}`);
|
|
225
|
+
if (bindings.ai) services.push("AI (Workers AI)");
|
|
226
|
+
|
|
227
|
+
if (services.length > 0) {
|
|
228
|
+
lines.push("");
|
|
229
|
+
lines.push("### Services");
|
|
230
|
+
for (const svc of services) {
|
|
231
|
+
lines.push(`- ${svc}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- Section 3: Mode-specific guidance ---
|
|
237
|
+
lines.push("");
|
|
238
|
+
lines.push("## How to work with this project");
|
|
239
|
+
lines.push("");
|
|
240
|
+
|
|
241
|
+
if (link.deploy_mode === "managed") {
|
|
242
|
+
lines.push("This is a **Jack Cloud** project. All infrastructure is managed by jack.");
|
|
243
|
+
lines.push("");
|
|
244
|
+
lines.push("- Deploy: `mcp__jack__deploy_project` or `jack ship`");
|
|
245
|
+
lines.push("- Database: `mcp__jack__execute_sql` or `jack services db query`");
|
|
246
|
+
lines.push("- Logs: `mcp__jack__tail_logs` or `jack logs`");
|
|
247
|
+
lines.push("- Status: `mcp__jack__get_project_status` or `jack info`");
|
|
248
|
+
lines.push("");
|
|
249
|
+
lines.push(
|
|
250
|
+
"**Do NOT run `wrangler` commands.** This project uses Jack Cloud — there are no local Cloudflare credentials.",
|
|
251
|
+
);
|
|
252
|
+
lines.push(
|
|
253
|
+
"The `wrangler.jsonc` file is only used for local dev and build configuration, not for deployment.",
|
|
254
|
+
);
|
|
255
|
+
} else {
|
|
256
|
+
lines.push("This is a **BYO** (Bring Your Own) project deployed to your Cloudflare account.");
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push("- Deploy: `mcp__jack__deploy_project` or `jack ship`");
|
|
259
|
+
lines.push("- Logs: `mcp__jack__tail_logs` or `jack logs`");
|
|
260
|
+
lines.push("- Status: `mcp__jack__get_project_status` or `jack info`");
|
|
261
|
+
lines.push("");
|
|
262
|
+
lines.push(
|
|
263
|
+
"Prefer `mcp__jack__*` tools or `jack` CLI over raw `wrangler` commands for consistency.",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lines.push("");
|
|
268
|
+
lines.push(
|
|
269
|
+
'To fork/clone a project: `mcp__jack__create_project` with `template: "username/slug"` or `template: "my-project"`.',
|
|
270
|
+
);
|
|
271
|
+
lines.push("");
|
|
272
|
+
lines.push(
|
|
273
|
+
"**Always prefer `mcp__jack__*` tools over CLI commands or wrangler** — they are cloud-aware and work in all deploy modes.",
|
|
274
|
+
);
|
|
275
|
+
sections.push(lines.join("\n"));
|
|
276
|
+
|
|
277
|
+
console.log(sections.join("\n\n---\n\n"));
|
|
278
|
+
} catch {
|
|
279
|
+
// Silent on failure — never break the session
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
108
283
|
/**
|
|
109
284
|
* Test MCP server by spawning it and sending test requests
|
|
110
285
|
*/
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { rollbackDeployment } from "../lib/control-plane.ts";
|
|
2
|
+
import { error, info, spinner } from "../lib/output.ts";
|
|
3
|
+
import { readProjectLink } from "../lib/project-link.ts";
|
|
4
|
+
import { getProjectNameFromDir } from "../lib/storage/index.ts";
|
|
5
|
+
|
|
6
|
+
/** Shorten a deployment ID for display: "dep_a1b2c3d4-..." → "a1b2c3d4" */
|
|
7
|
+
function shortDeployId(id: string): string {
|
|
8
|
+
return id.startsWith("dep_") ? id.slice(4, 12) : id.slice(0, 8);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RollbackOptions {
|
|
12
|
+
to?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Rollback to a previous deployment
|
|
17
|
+
*/
|
|
18
|
+
export default async function rollback(options: RollbackOptions = {}): Promise<void> {
|
|
19
|
+
const link = await readProjectLink(process.cwd());
|
|
20
|
+
|
|
21
|
+
if (!link) {
|
|
22
|
+
error("Not a jack project");
|
|
23
|
+
info("Run this command from a linked project directory");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (link.deploy_mode !== "managed") {
|
|
28
|
+
error("Rollback is available for managed projects only");
|
|
29
|
+
info("BYO projects can be rolled back using wrangler directly");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let projectName: string;
|
|
34
|
+
try {
|
|
35
|
+
projectName = await getProjectNameFromDir(process.cwd());
|
|
36
|
+
} catch {
|
|
37
|
+
projectName = link.project_id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const targetLabel = options.to ? shortDeployId(options.to) : "previous version";
|
|
41
|
+
const spin = spinner(`Rolling back ${projectName} to ${targetLabel}`);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const result = await rollbackDeployment(link.project_id, options.to);
|
|
45
|
+
spin.success(`Rolled back to ${shortDeployId(result.deployment.id)}`);
|
|
46
|
+
info(`New deployment: ${result.deployment.id}`);
|
|
47
|
+
info("Code rolled back. Database state is unchanged.");
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : "Rollback failed";
|
|
50
|
+
spin.error(message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/commands/services.ts
CHANGED
|
@@ -99,6 +99,7 @@ async function resolveDatabaseInfo(projectName: string): Promise<ResolvedDatabas
|
|
|
99
99
|
|
|
100
100
|
interface ServiceOptions {
|
|
101
101
|
project?: string;
|
|
102
|
+
json?: boolean;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export default async function services(
|
|
@@ -209,17 +210,23 @@ async function resolveProjectName(options: ServiceOptions): Promise<string> {
|
|
|
209
210
|
* Show database information
|
|
210
211
|
*/
|
|
211
212
|
async function dbInfo(options: ServiceOptions): Promise<void> {
|
|
213
|
+
const jsonOutput = options.json ?? false;
|
|
212
214
|
const projectName = await resolveProjectName(options);
|
|
213
215
|
const projectDir = process.cwd();
|
|
214
216
|
const link = await readProjectLink(projectDir);
|
|
215
217
|
|
|
216
218
|
// For managed projects, use control plane API (no wrangler dependency)
|
|
217
219
|
if (link?.deploy_mode === "managed") {
|
|
218
|
-
outputSpinner.start("Fetching database info...");
|
|
220
|
+
if (!jsonOutput) outputSpinner.start("Fetching database info...");
|
|
219
221
|
try {
|
|
220
222
|
const { getManagedDatabaseInfo } = await import("../lib/control-plane.ts");
|
|
221
223
|
const dbInfo = await getManagedDatabaseInfo(link.project_id);
|
|
222
|
-
outputSpinner.stop();
|
|
224
|
+
if (!jsonOutput) outputSpinner.stop();
|
|
225
|
+
|
|
226
|
+
if (jsonOutput) {
|
|
227
|
+
console.log(JSON.stringify({ name: dbInfo.name, id: dbInfo.id, sizeBytes: dbInfo.sizeBytes, numTables: dbInfo.numTables, source: "managed" }));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
223
230
|
|
|
224
231
|
console.error("");
|
|
225
232
|
success(`Database: ${dbInfo.name}`);
|
|
@@ -231,7 +238,14 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
|
|
|
231
238
|
console.error("");
|
|
232
239
|
return;
|
|
233
240
|
} catch (err) {
|
|
234
|
-
outputSpinner.stop();
|
|
241
|
+
if (!jsonOutput) outputSpinner.stop();
|
|
242
|
+
if (jsonOutput) {
|
|
243
|
+
const msg = err instanceof Error && err.message.includes("No database found")
|
|
244
|
+
? "No database configured. Run 'jack services db create' to create one."
|
|
245
|
+
: `Failed to fetch database info: ${err instanceof Error ? err.message : String(err)}`;
|
|
246
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
235
249
|
console.error("");
|
|
236
250
|
if (err instanceof Error && err.message.includes("No database found")) {
|
|
237
251
|
error("No database found for this project");
|
|
@@ -248,6 +262,10 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
|
|
|
248
262
|
const dbInfo = await resolveDatabaseInfo(projectName);
|
|
249
263
|
|
|
250
264
|
if (!dbInfo) {
|
|
265
|
+
if (jsonOutput) {
|
|
266
|
+
console.log(JSON.stringify({ success: false, error: "No database found for this project" }));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
251
269
|
console.error("");
|
|
252
270
|
error("No database found for this project");
|
|
253
271
|
info("Create one with: jack services db create");
|
|
@@ -256,11 +274,15 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
|
|
|
256
274
|
}
|
|
257
275
|
|
|
258
276
|
// Fetch detailed database info via wrangler
|
|
259
|
-
outputSpinner.start("Fetching database info...");
|
|
277
|
+
if (!jsonOutput) outputSpinner.start("Fetching database info...");
|
|
260
278
|
const wranglerDbInfo = await getWranglerDatabaseInfo(dbInfo.name);
|
|
261
|
-
outputSpinner.stop();
|
|
279
|
+
if (!jsonOutput) outputSpinner.stop();
|
|
262
280
|
|
|
263
281
|
if (!wranglerDbInfo) {
|
|
282
|
+
if (jsonOutput) {
|
|
283
|
+
console.log(JSON.stringify({ success: false, error: "Database not found" }));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
264
286
|
console.error("");
|
|
265
287
|
error("Database not found");
|
|
266
288
|
info("It may have been deleted");
|
|
@@ -268,6 +290,11 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
|
|
|
268
290
|
process.exit(1);
|
|
269
291
|
}
|
|
270
292
|
|
|
293
|
+
if (jsonOutput) {
|
|
294
|
+
console.log(JSON.stringify({ name: wranglerDbInfo.name, id: dbInfo.id || wranglerDbInfo.id, sizeBytes: wranglerDbInfo.sizeBytes, numTables: wranglerDbInfo.numTables, source: "byo" }));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
271
298
|
// Display info
|
|
272
299
|
console.error("");
|
|
273
300
|
success(`Database: ${wranglerDbInfo.name}`);
|
|
@@ -624,10 +651,16 @@ async function dbCreate(args: string[], options: ServiceOptions): Promise<void>
|
|
|
624
651
|
* List all databases in the project
|
|
625
652
|
*/
|
|
626
653
|
async function dbList(options: ServiceOptions): Promise<void> {
|
|
627
|
-
|
|
654
|
+
const jsonOutput = options.json ?? false;
|
|
655
|
+
if (!jsonOutput) outputSpinner.start("Fetching databases...");
|
|
628
656
|
try {
|
|
629
657
|
const databases = await listDatabases(process.cwd());
|
|
630
|
-
outputSpinner.stop();
|
|
658
|
+
if (!jsonOutput) outputSpinner.stop();
|
|
659
|
+
|
|
660
|
+
if (jsonOutput) {
|
|
661
|
+
console.log(JSON.stringify(databases));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
631
664
|
|
|
632
665
|
if (databases.length === 0) {
|
|
633
666
|
console.error("");
|
|
@@ -654,7 +687,11 @@ async function dbList(options: ServiceOptions): Promise<void> {
|
|
|
654
687
|
console.error("");
|
|
655
688
|
}
|
|
656
689
|
} catch (err) {
|
|
657
|
-
outputSpinner.stop();
|
|
690
|
+
if (!jsonOutput) outputSpinner.stop();
|
|
691
|
+
if (jsonOutput) {
|
|
692
|
+
console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
658
695
|
console.error("");
|
|
659
696
|
error(`Failed to list databases: ${err instanceof Error ? err.message : String(err)}`);
|
|
660
697
|
process.exit(1);
|
|
@@ -729,7 +766,8 @@ function parseExecuteArgs(args: string[]): ExecuteArgs {
|
|
|
729
766
|
/**
|
|
730
767
|
* Execute SQL against the database
|
|
731
768
|
*/
|
|
732
|
-
async function dbExecute(args: string[],
|
|
769
|
+
async function dbExecute(args: string[], options: ServiceOptions): Promise<void> {
|
|
770
|
+
const jsonOutput = options.json ?? false;
|
|
733
771
|
const execArgs = parseExecuteArgs(args);
|
|
734
772
|
|
|
735
773
|
// Validate input
|
|
@@ -766,7 +804,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
766
804
|
const projectDir = process.cwd();
|
|
767
805
|
|
|
768
806
|
try {
|
|
769
|
-
outputSpinner.start("Executing SQL...");
|
|
807
|
+
if (!jsonOutput) outputSpinner.start("Executing SQL...");
|
|
770
808
|
|
|
771
809
|
let result;
|
|
772
810
|
if (execArgs.filePath) {
|
|
@@ -823,7 +861,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
823
861
|
}
|
|
824
862
|
|
|
825
863
|
// NOW execute with confirmation
|
|
826
|
-
outputSpinner.start("Executing SQL...");
|
|
864
|
+
if (!jsonOutput) outputSpinner.start("Executing SQL...");
|
|
827
865
|
if (execArgs.filePath) {
|
|
828
866
|
result = await executeSqlFile({
|
|
829
867
|
projectDir,
|
|
@@ -857,12 +895,37 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
857
895
|
error_type: "execution_failed",
|
|
858
896
|
});
|
|
859
897
|
|
|
898
|
+
if (jsonOutput) {
|
|
899
|
+
console.log(JSON.stringify({ success: false, error: result.error || "SQL execution failed" }));
|
|
900
|
+
process.exit(1);
|
|
901
|
+
}
|
|
902
|
+
|
|
860
903
|
console.error("");
|
|
861
904
|
error(result.error || "SQL execution failed");
|
|
862
905
|
console.error("");
|
|
863
906
|
process.exit(1);
|
|
864
907
|
}
|
|
865
908
|
|
|
909
|
+
// Track telemetry
|
|
910
|
+
track(Events.SQL_EXECUTED, {
|
|
911
|
+
success: true,
|
|
912
|
+
risk_level: result.risk,
|
|
913
|
+
statement_count: result.statements.length,
|
|
914
|
+
from_file: !!execArgs.filePath,
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// JSON output mode — structured result for agents
|
|
918
|
+
if (jsonOutput) {
|
|
919
|
+
console.log(JSON.stringify({
|
|
920
|
+
success: true,
|
|
921
|
+
results: result.results ?? [],
|
|
922
|
+
meta: result.meta,
|
|
923
|
+
risk: result.risk,
|
|
924
|
+
warning: result.warning,
|
|
925
|
+
}));
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
866
929
|
// Show results
|
|
867
930
|
console.error("");
|
|
868
931
|
success(`SQL executed (${getRiskDescription(result.risk)})`);
|
|
@@ -886,14 +949,6 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
886
949
|
console.log(JSON.stringify(result.results, null, 2));
|
|
887
950
|
}
|
|
888
951
|
console.error("");
|
|
889
|
-
|
|
890
|
-
// Track telemetry
|
|
891
|
-
track(Events.SQL_EXECUTED, {
|
|
892
|
-
success: true,
|
|
893
|
-
risk_level: result.risk,
|
|
894
|
-
statement_count: result.statements.length,
|
|
895
|
-
from_file: !!execArgs.filePath,
|
|
896
|
-
});
|
|
897
952
|
} catch (err) {
|
|
898
953
|
outputSpinner.stop();
|
|
899
954
|
|
|
@@ -904,6 +959,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
904
959
|
risk_level: err.risk,
|
|
905
960
|
});
|
|
906
961
|
|
|
962
|
+
if (jsonOutput) {
|
|
963
|
+
console.log(JSON.stringify({ success: false, error: err.message, suggestion: "Add --write flag" }));
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
|
|
907
967
|
console.error("");
|
|
908
968
|
error(err.message);
|
|
909
969
|
info("Add the --write flag to allow data modification:");
|
|
@@ -919,6 +979,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
919
979
|
risk_level: "destructive",
|
|
920
980
|
});
|
|
921
981
|
|
|
982
|
+
if (jsonOutput) {
|
|
983
|
+
console.log(JSON.stringify({ success: false, error: err.message, suggestion: "Destructive operations require confirmation via CLI" }));
|
|
984
|
+
process.exit(1);
|
|
985
|
+
}
|
|
986
|
+
|
|
922
987
|
console.error("");
|
|
923
988
|
error(err.message);
|
|
924
989
|
info("Destructive operations require confirmation via CLI.");
|
|
@@ -931,6 +996,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
931
996
|
error_type: "unknown",
|
|
932
997
|
});
|
|
933
998
|
|
|
999
|
+
if (jsonOutput) {
|
|
1000
|
+
console.log(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }));
|
|
1001
|
+
process.exit(1);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
934
1004
|
console.error("");
|
|
935
1005
|
error(`SQL execution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
936
1006
|
console.error("");
|
|
@@ -1284,7 +1354,17 @@ async function cronCreate(args: string[], options: ServiceOptions): Promise<void
|
|
|
1284
1354
|
item(`Schedule: ${result.description}`);
|
|
1285
1355
|
item(`Next run: ${result.nextRunAt}`);
|
|
1286
1356
|
console.error("");
|
|
1287
|
-
|
|
1357
|
+
warn("Your worker must handle POST /__scheduled for crons to work.");
|
|
1358
|
+
console.error("");
|
|
1359
|
+
console.error(" Example (Hono):");
|
|
1360
|
+
console.error("");
|
|
1361
|
+
console.error(" app.post('/__scheduled', async (c) => {");
|
|
1362
|
+
console.error(" // your cron logic here");
|
|
1363
|
+
console.error(" return c.json({ ok: true });");
|
|
1364
|
+
console.error(" });");
|
|
1365
|
+
console.error("");
|
|
1366
|
+
info("Cloudflare's native scheduled() export does NOT work with Jack Cloud.");
|
|
1367
|
+
info("Jack invokes crons via HTTP, not the Workers scheduled event.");
|
|
1288
1368
|
console.error("");
|
|
1289
1369
|
} catch (err) {
|
|
1290
1370
|
outputSpinner.stop();
|