@getjack/jack 0.1.16 → 0.1.19
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/README.md +1 -1
- package/package.json +1 -1
- package/src/commands/clone.ts +62 -40
- package/src/commands/community.ts +47 -0
- package/src/commands/init.ts +6 -0
- package/src/commands/services.ts +354 -9
- package/src/commands/sync.ts +9 -0
- package/src/commands/update.ts +10 -0
- package/src/index.ts +7 -0
- package/src/lib/control-plane.ts +62 -0
- package/src/lib/hooks.ts +20 -0
- package/src/lib/managed-deploy.ts +26 -2
- package/src/lib/output.ts +21 -3
- package/src/lib/progress.ts +160 -0
- package/src/lib/project-operations.ts +381 -93
- package/src/lib/services/db-create.ts +6 -3
- package/src/lib/services/db-execute.ts +485 -0
- package/src/lib/services/sql-classifier.test.ts +404 -0
- package/src/lib/services/sql-classifier.ts +346 -0
- package/src/lib/storage/file-filter.ts +4 -0
- package/src/lib/telemetry.ts +3 -0
- package/src/lib/version-check.ts +14 -0
- package/src/lib/wrangler-config.test.ts +322 -0
- package/src/lib/wrangler-config.ts +649 -0
- package/src/lib/zip-packager.ts +38 -0
- package/src/lib/zip-utils.ts +38 -0
- package/src/mcp/tools/index.ts +161 -0
- package/src/templates/index.ts +4 -0
- package/src/templates/types.ts +12 -0
- package/templates/api/AGENTS.md +33 -0
- package/templates/hello/AGENTS.md +33 -0
- package/templates/miniapp/.jack.json +4 -5
- package/templates/miniapp/AGENTS.md +33 -0
- package/templates/nextjs/AGENTS.md +33 -0
package/src/commands/update.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* jack update - Self-update to the latest version
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { debug } from "../lib/debug.ts";
|
|
5
6
|
import { error, info, success, warn } from "../lib/output.ts";
|
|
6
7
|
import {
|
|
7
8
|
checkForUpdate,
|
|
@@ -13,8 +14,13 @@ import {
|
|
|
13
14
|
export default async function update(): Promise<void> {
|
|
14
15
|
const currentVersion = getCurrentVersion();
|
|
15
16
|
|
|
17
|
+
debug("Update check started");
|
|
18
|
+
debug(`Current version: ${currentVersion}`);
|
|
19
|
+
debug(`Exec path: ${process.argv[1]}`);
|
|
20
|
+
|
|
16
21
|
// Check if running via bunx
|
|
17
22
|
if (isRunningViaBunx()) {
|
|
23
|
+
debug("Detected bunx execution");
|
|
18
24
|
info(`Running via bunx (current: v${currentVersion})`);
|
|
19
25
|
info("bunx automatically uses cached packages.");
|
|
20
26
|
info("To get the latest version, run:");
|
|
@@ -28,7 +34,9 @@ export default async function update(): Promise<void> {
|
|
|
28
34
|
info(`Current version: v${currentVersion}`);
|
|
29
35
|
|
|
30
36
|
// Check for updates
|
|
37
|
+
debug("Fetching latest version from npm...");
|
|
31
38
|
const latestVersion = await checkForUpdate();
|
|
39
|
+
debug(`Latest version from npm: ${latestVersion ?? "none (you're up to date)"}`);
|
|
32
40
|
|
|
33
41
|
if (!latestVersion) {
|
|
34
42
|
success("You're on the latest version!");
|
|
@@ -38,7 +46,9 @@ export default async function update(): Promise<void> {
|
|
|
38
46
|
info(`New version available: v${latestVersion}`);
|
|
39
47
|
info("Updating...");
|
|
40
48
|
|
|
49
|
+
debug("Running: bun add -g @getjack/jack@latest");
|
|
41
50
|
const result = await performUpdate();
|
|
51
|
+
debug(`Update result: ${JSON.stringify(result)}`);
|
|
42
52
|
|
|
43
53
|
if (result.success) {
|
|
44
54
|
success(`Updated to v${result.version ?? latestVersion}`);
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ const cli = meow(
|
|
|
49
49
|
mcp MCP server for AI agents
|
|
50
50
|
telemetry Usage data settings
|
|
51
51
|
feedback Share feedback or report issues
|
|
52
|
+
community Join the jack Discord
|
|
52
53
|
|
|
53
54
|
Run 'jack <command> --help' for command-specific options.
|
|
54
55
|
|
|
@@ -370,6 +371,12 @@ try {
|
|
|
370
371
|
await withTelemetry("feedback", feedback)();
|
|
371
372
|
break;
|
|
372
373
|
}
|
|
374
|
+
case "community":
|
|
375
|
+
case "discord": {
|
|
376
|
+
const { default: community } = await import("./commands/community.ts");
|
|
377
|
+
await withTelemetry("community", community)();
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
373
380
|
case "link": {
|
|
374
381
|
const { default: link } = await import("./commands/link.ts");
|
|
375
382
|
await withTelemetry("link", link)(args[0], { byo: cli.flags.byo });
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -371,6 +371,46 @@ export async function createProjectResource(
|
|
|
371
371
|
return data.resource;
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
export interface DeleteResourceResponse {
|
|
375
|
+
success: boolean;
|
|
376
|
+
resource_id: string;
|
|
377
|
+
deleted_at: string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Delete a resource from a managed project.
|
|
382
|
+
* Uses DELETE /v1/projects/:id/resources/:id endpoint.
|
|
383
|
+
*/
|
|
384
|
+
export async function deleteProjectResource(
|
|
385
|
+
projectId: string,
|
|
386
|
+
resourceId: string,
|
|
387
|
+
): Promise<DeleteResourceResponse> {
|
|
388
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
389
|
+
|
|
390
|
+
const response = await authFetch(
|
|
391
|
+
`${getControlApiUrl()}/v1/projects/${projectId}/resources/${resourceId}`,
|
|
392
|
+
{ method: "DELETE" },
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Handle 404 gracefully - resource may already be deleted
|
|
396
|
+
if (response.status === 404) {
|
|
397
|
+
return {
|
|
398
|
+
success: true,
|
|
399
|
+
resource_id: resourceId,
|
|
400
|
+
deleted_at: new Date().toISOString(),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
406
|
+
message?: string;
|
|
407
|
+
};
|
|
408
|
+
throw new Error(err.message || `Failed to delete resource: ${response.status}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return response.json() as Promise<DeleteResourceResponse>;
|
|
412
|
+
}
|
|
413
|
+
|
|
374
414
|
/**
|
|
375
415
|
* Sync project tags to the control plane.
|
|
376
416
|
* Fire-and-forget: errors are logged but not thrown.
|
|
@@ -580,3 +620,25 @@ export async function publishProject(projectId: string): Promise<PublishProjectR
|
|
|
580
620
|
|
|
581
621
|
return response.json() as Promise<PublishProjectResponse>;
|
|
582
622
|
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Download the source snapshot for a project.
|
|
626
|
+
* Returns the zip file contents as a Buffer.
|
|
627
|
+
* Used by jack clone to restore managed projects.
|
|
628
|
+
*/
|
|
629
|
+
export async function downloadProjectSource(slug: string): Promise<Buffer> {
|
|
630
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
631
|
+
|
|
632
|
+
const response = await authFetch(
|
|
633
|
+
`${getControlApiUrl()}/v1/projects/by-slug/${encodeURIComponent(slug)}/source`,
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
if (!response.ok) {
|
|
637
|
+
if (response.status === 404) {
|
|
638
|
+
throw new Error("Project source not found. Deploy first with 'jack ship'.");
|
|
639
|
+
}
|
|
640
|
+
throw new Error(`Failed to download source: ${response.status}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return Buffer.from(await response.arrayBuffer());
|
|
644
|
+
}
|
package/src/lib/hooks.ts
CHANGED
|
@@ -478,6 +478,26 @@ const actionHandlers: {
|
|
|
478
478
|
if (action.successMessage) {
|
|
479
479
|
ui.success(substituteVars(action.successMessage, context));
|
|
480
480
|
}
|
|
481
|
+
|
|
482
|
+
// Redeploy if deployAfter is set and we have a valid project directory
|
|
483
|
+
if (action.deployAfter && context.projectDir) {
|
|
484
|
+
const deployMsg = action.deployMessage || "Deploying...";
|
|
485
|
+
ui.info(deployMsg);
|
|
486
|
+
|
|
487
|
+
const proc = Bun.spawn(["wrangler", "deploy"], {
|
|
488
|
+
cwd: context.projectDir,
|
|
489
|
+
stdout: "ignore",
|
|
490
|
+
stderr: "pipe",
|
|
491
|
+
});
|
|
492
|
+
await proc.exited;
|
|
493
|
+
|
|
494
|
+
if (proc.exitCode === 0) {
|
|
495
|
+
ui.success("Deployed");
|
|
496
|
+
} else {
|
|
497
|
+
const stderr = await new Response(proc.stderr).text();
|
|
498
|
+
ui.warn(`Deploy failed: ${stderr.slice(0, 200)}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
481
501
|
}
|
|
482
502
|
|
|
483
503
|
return true;
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
* Isolates managed deployment logic from BYO (wrangler) path.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { stat } from "node:fs/promises";
|
|
7
8
|
import { validateBindings } from "./binding-validator.ts";
|
|
8
9
|
import { buildProject, parseWranglerConfig } from "./build-helper.ts";
|
|
9
10
|
import { createManagedProject, syncProjectTags, uploadSourceSnapshot } from "./control-plane.ts";
|
|
10
11
|
import { debug } from "./debug.ts";
|
|
11
12
|
import { uploadDeployment } from "./deploy-upload.ts";
|
|
12
13
|
import { JackError, JackErrorCode } from "./errors.ts";
|
|
14
|
+
import { formatSize } from "./format.ts";
|
|
15
|
+
import { createUploadProgress } from "./progress.ts";
|
|
13
16
|
import type { OperationReporter } from "./project-operations.ts";
|
|
14
17
|
import { getProjectTags } from "./tags.ts";
|
|
15
18
|
import { Events, track } from "./telemetry.ts";
|
|
@@ -120,7 +123,28 @@ export async function deployCodeToManagedProject(
|
|
|
120
123
|
reporter?.success("Packaged artifacts");
|
|
121
124
|
|
|
122
125
|
// Step 4: Upload to control plane
|
|
123
|
-
|
|
126
|
+
// Calculate total upload size for progress display
|
|
127
|
+
const fileSizes = await Promise.all([
|
|
128
|
+
stat(pkg.bundleZipPath).then((s) => s.size),
|
|
129
|
+
stat(pkg.sourceZipPath).then((s) => s.size),
|
|
130
|
+
stat(pkg.manifestPath).then((s) => s.size),
|
|
131
|
+
pkg.schemaPath ? stat(pkg.schemaPath).then((s) => s.size) : Promise.resolve(0),
|
|
132
|
+
pkg.secretsPath ? stat(pkg.secretsPath).then((s) => s.size) : Promise.resolve(0),
|
|
133
|
+
pkg.assetsZipPath ? stat(pkg.assetsZipPath).then((s) => s.size) : Promise.resolve(0),
|
|
134
|
+
]);
|
|
135
|
+
const totalUploadSize = fileSizes.reduce((sum, size) => sum + size, 0);
|
|
136
|
+
debug(`Upload size: ${formatSize(totalUploadSize)}`);
|
|
137
|
+
|
|
138
|
+
// Stop the reporter spinner - we'll use our own progress display
|
|
139
|
+
reporter?.stop();
|
|
140
|
+
|
|
141
|
+
// Use custom progress with pulsing bar (since fetch doesn't support upload progress)
|
|
142
|
+
const uploadProgress = createUploadProgress({
|
|
143
|
+
totalSize: totalUploadSize,
|
|
144
|
+
label: "Uploading to jack cloud",
|
|
145
|
+
});
|
|
146
|
+
uploadProgress.start();
|
|
147
|
+
|
|
124
148
|
const result = await uploadDeployment({
|
|
125
149
|
projectId,
|
|
126
150
|
bundleZipPath: pkg.bundleZipPath,
|
|
@@ -132,7 +156,7 @@ export async function deployCodeToManagedProject(
|
|
|
132
156
|
assetManifest: pkg.assetManifest ?? undefined,
|
|
133
157
|
});
|
|
134
158
|
|
|
135
|
-
|
|
159
|
+
uploadProgress.complete();
|
|
136
160
|
reporter?.success("Deployed to jack cloud");
|
|
137
161
|
|
|
138
162
|
// Track success
|
package/src/lib/output.ts
CHANGED
|
@@ -2,6 +2,19 @@ import yoctoSpinner from "yocto-spinner";
|
|
|
2
2
|
|
|
3
3
|
const isColorEnabled = !process.env.NO_COLOR && process.stderr.isTTY !== false;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* ANSI color codes for terminal output
|
|
7
|
+
*/
|
|
8
|
+
export const colors = {
|
|
9
|
+
green: isColorEnabled ? "\x1b[32m" : "",
|
|
10
|
+
red: isColorEnabled ? "\x1b[31m" : "",
|
|
11
|
+
yellow: isColorEnabled ? "\x1b[33m" : "",
|
|
12
|
+
cyan: isColorEnabled ? "\x1b[36m" : "",
|
|
13
|
+
dim: isColorEnabled ? "\x1b[90m" : "",
|
|
14
|
+
bold: isColorEnabled ? "\x1b[1m" : "",
|
|
15
|
+
reset: isColorEnabled ? "\x1b[0m" : "",
|
|
16
|
+
};
|
|
17
|
+
|
|
5
18
|
let currentSpinner: ReturnType<typeof yoctoSpinner> | null = null;
|
|
6
19
|
|
|
7
20
|
/**
|
|
@@ -95,8 +108,10 @@ export function item(message: string): void {
|
|
|
95
108
|
console.error(` ${message}`);
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Random neon purple for cyberpunk styling
|
|
113
|
+
*/
|
|
114
|
+
export function getRandomPurple(): string {
|
|
100
115
|
const purples = [177, 165, 141, 129];
|
|
101
116
|
const colorCode = purples[Math.floor(Math.random() * purples.length)];
|
|
102
117
|
return `\x1b[38;5;${colorCode}m`;
|
|
@@ -117,12 +132,15 @@ export function box(title: string, lines: string[]): void {
|
|
|
117
132
|
const fill = "▓".repeat(innerWidth);
|
|
118
133
|
const gradient = "░".repeat(innerWidth);
|
|
119
134
|
|
|
135
|
+
// Pad plain text first, then apply colors (ANSI codes break padEnd calculation)
|
|
120
136
|
const pad = (text: string) => ` ${text.padEnd(maxLen)} `;
|
|
137
|
+
const padTitle = (text: string) =>
|
|
138
|
+
` ${bold}${text}${reset}${purple}${" ".repeat(maxLen - text.length)} `;
|
|
121
139
|
|
|
122
140
|
console.error("");
|
|
123
141
|
console.error(` ${purple}╔${bar}╗${reset}`);
|
|
124
142
|
console.error(` ${purple}║${fill}║${reset}`);
|
|
125
|
-
console.error(` ${purple}║${
|
|
143
|
+
console.error(` ${purple}║${padTitle(title)}║${reset}`);
|
|
126
144
|
console.error(` ${purple}║${"─".repeat(innerWidth)}║${reset}`);
|
|
127
145
|
for (const line of lines) {
|
|
128
146
|
console.error(` ${purple}║${pad(line)}║${reset}`);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress tracker with delayed bar display
|
|
3
|
+
* Shows a simple spinner initially, then reveals a progress bar
|
|
4
|
+
* after a configurable delay (aligned with SPIRIT.md principles)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatSize } from "./format.ts";
|
|
8
|
+
import { colors, getRandomPurple } from "./output.ts";
|
|
9
|
+
|
|
10
|
+
export interface ProgressOptions {
|
|
11
|
+
total: number;
|
|
12
|
+
delayMs?: number;
|
|
13
|
+
barWidth?: number;
|
|
14
|
+
label?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UploadProgressOptions {
|
|
18
|
+
totalSize: number;
|
|
19
|
+
delayMs?: number;
|
|
20
|
+
barWidth?: number;
|
|
21
|
+
label?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a progress tracker that shows a spinner initially,
|
|
28
|
+
* then reveals a progress bar after a delay.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* const progress = createProgressTracker({ total: fileSize });
|
|
32
|
+
* progress.start();
|
|
33
|
+
* // ... during upload
|
|
34
|
+
* progress.update(bytesUploaded);
|
|
35
|
+
* // ... when done
|
|
36
|
+
* progress.complete();
|
|
37
|
+
*/
|
|
38
|
+
export function createProgressTracker(options: ProgressOptions) {
|
|
39
|
+
const { total, delayMs = 2000, barWidth = 25, label = "Uploading" } = options;
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
let frame = 0;
|
|
42
|
+
let intervalId: Timer | null = null;
|
|
43
|
+
let current = 0;
|
|
44
|
+
|
|
45
|
+
function render() {
|
|
46
|
+
const elapsed = Date.now() - startTime;
|
|
47
|
+
const pct = Math.min(Math.round((current / total) * 100), 100);
|
|
48
|
+
|
|
49
|
+
clearLine();
|
|
50
|
+
|
|
51
|
+
if (elapsed < delayMs) {
|
|
52
|
+
// Just spinner for first N seconds
|
|
53
|
+
process.stderr.write(
|
|
54
|
+
`${colors.cyan}${frames[frame++ % frames.length]}${colors.reset} ${label}...`,
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
// Show progress bar after delay
|
|
58
|
+
const purple = getRandomPurple();
|
|
59
|
+
const filled = Math.round((pct / 100) * barWidth);
|
|
60
|
+
const empty = barWidth - filled;
|
|
61
|
+
const bar = "▓".repeat(filled) + "░".repeat(empty);
|
|
62
|
+
|
|
63
|
+
process.stderr.write(
|
|
64
|
+
`${colors.cyan}${frames[frame++ % frames.length]}${colors.reset} ${label} ${purple}[${bar}]${colors.reset} ${pct}% ${colors.dim}(${formatSize(current)} / ${formatSize(total)})${colors.reset}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
start() {
|
|
71
|
+
render();
|
|
72
|
+
intervalId = setInterval(render, 80);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
update(bytes: number) {
|
|
76
|
+
current = bytes;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
complete() {
|
|
80
|
+
if (intervalId) {
|
|
81
|
+
clearInterval(intervalId);
|
|
82
|
+
intervalId = null;
|
|
83
|
+
}
|
|
84
|
+
clearLine();
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates an upload progress indicator for operations where we know the total
|
|
91
|
+
* size but can't track byte-level progress (e.g., fetch uploads).
|
|
92
|
+
*
|
|
93
|
+
* Shows spinner first, then after delay shows an animated bar with size info.
|
|
94
|
+
* The bar pulses to indicate activity without false progress claims.
|
|
95
|
+
*/
|
|
96
|
+
export function createUploadProgress(options: UploadProgressOptions) {
|
|
97
|
+
const { totalSize, delayMs = 2000, barWidth = 25, label = "Uploading" } = options;
|
|
98
|
+
const startTime = Date.now();
|
|
99
|
+
let frame = 0;
|
|
100
|
+
let pulsePos = 0;
|
|
101
|
+
let intervalId: Timer | null = null;
|
|
102
|
+
|
|
103
|
+
function render() {
|
|
104
|
+
const elapsed = Date.now() - startTime;
|
|
105
|
+
const elapsedSec = (elapsed / 1000).toFixed(1);
|
|
106
|
+
|
|
107
|
+
clearLine();
|
|
108
|
+
|
|
109
|
+
if (elapsed < delayMs) {
|
|
110
|
+
// Just spinner for first N seconds
|
|
111
|
+
process.stderr.write(
|
|
112
|
+
`${colors.cyan}${frames[frame++ % frames.length]}${colors.reset} ${label}...`,
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
// Show pulsing bar after delay (indicates activity without false progress)
|
|
116
|
+
const purple = getRandomPurple();
|
|
117
|
+
|
|
118
|
+
// Create pulsing effect - a bright section that moves across the bar
|
|
119
|
+
const pulseWidth = 5;
|
|
120
|
+
pulsePos = (pulsePos + 1) % (barWidth + pulseWidth);
|
|
121
|
+
|
|
122
|
+
let bar = "";
|
|
123
|
+
for (let i = 0; i < barWidth; i++) {
|
|
124
|
+
const distFromPulse = Math.abs(i - pulsePos);
|
|
125
|
+
if (distFromPulse < pulseWidth) {
|
|
126
|
+
bar += "▓";
|
|
127
|
+
} else {
|
|
128
|
+
bar += "░";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
process.stderr.write(
|
|
133
|
+
`${colors.cyan}${frames[frame++ % frames.length]}${colors.reset} ${label} ${purple}[${bar}]${colors.reset} ${colors.dim}${formatSize(totalSize)} • ${elapsedSec}s${colors.reset}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
start() {
|
|
140
|
+
if (process.stderr.isTTY) {
|
|
141
|
+
render();
|
|
142
|
+
intervalId = setInterval(render, 80);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
complete() {
|
|
147
|
+
if (intervalId) {
|
|
148
|
+
clearInterval(intervalId);
|
|
149
|
+
intervalId = null;
|
|
150
|
+
}
|
|
151
|
+
clearLine();
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function clearLine() {
|
|
157
|
+
if (process.stderr.isTTY) {
|
|
158
|
+
process.stderr.write("\r\x1b[K");
|
|
159
|
+
}
|
|
160
|
+
}
|