upfynai-code 2.7.5 → 2.8.1
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/bin/cli.js +1 -1
- package/dist/agents/claude.js +197 -0
- package/dist/agents/codex.js +48 -0
- package/dist/agents/cursor.js +48 -0
- package/dist/agents/detect.js +51 -0
- package/dist/agents/exec.js +31 -0
- package/dist/agents/files.js +105 -0
- package/dist/agents/git.js +18 -0
- package/dist/agents/index.js +87 -0
- package/dist/agents/shell.js +38 -0
- package/dist/agents/utils.js +136 -0
- package/package.json +5 -2
- package/scripts/postinstall.js +9 -0
- package/scripts/prepublish.js +41 -0
- package/src/animation.js +228 -0
- package/src/connect.js +65 -515
- package/src/launch.js +39 -15
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for agent command handlers.
|
|
3
|
+
* Used by both CLI relay clients and backend server.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { promises as fsPromises } from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute a shell command and return stdout.
|
|
12
|
+
* @param {string} cmd - Command to run
|
|
13
|
+
* @param {string[]} args - Command arguments
|
|
14
|
+
* @param {object} options - { cwd, env, timeout }
|
|
15
|
+
* @returns {Promise<string>} stdout
|
|
16
|
+
*/
|
|
17
|
+
export function execCommand(cmd, args, options = {}) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const proc = spawn(cmd, args, {
|
|
20
|
+
shell: true,
|
|
21
|
+
cwd: options.cwd || os.homedir(),
|
|
22
|
+
env: { ...process.env, ...options.env },
|
|
23
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let stdout = '';
|
|
27
|
+
let stderr = '';
|
|
28
|
+
proc.stdout.on('data', (d) => { stdout += d; });
|
|
29
|
+
proc.stderr.on('data', (d) => { stderr += d; });
|
|
30
|
+
|
|
31
|
+
const timeout = setTimeout(() => {
|
|
32
|
+
proc.kill();
|
|
33
|
+
reject(new Error('Command timed out'));
|
|
34
|
+
}, options.timeout || 30000);
|
|
35
|
+
|
|
36
|
+
proc.on('close', (code) => {
|
|
37
|
+
clearTimeout(timeout);
|
|
38
|
+
if (code === 0) resolve(stdout);
|
|
39
|
+
else reject(new Error(stderr || `Exit code ${code}`));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
proc.on('error', (err) => {
|
|
43
|
+
clearTimeout(timeout);
|
|
44
|
+
reject(err);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build a file tree for a directory (recursive).
|
|
51
|
+
* @param {string} dirPath - Directory to scan
|
|
52
|
+
* @param {number} maxDepth - Maximum recursion depth
|
|
53
|
+
* @param {number} currentDepth - Current depth (internal)
|
|
54
|
+
* @param {object} options - { maxEntries, includeStats, skipDotfilesAtRoot }
|
|
55
|
+
* @returns {Promise<Array>} File tree items
|
|
56
|
+
*/
|
|
57
|
+
export async function buildFileTree(dirPath, maxDepth, currentDepth = 0, options = {}) {
|
|
58
|
+
const { maxEntries = 200, includeStats = false, skipDotfilesAtRoot = false } = options;
|
|
59
|
+
if (currentDepth >= maxDepth) return [];
|
|
60
|
+
|
|
61
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.svn', '.hg']);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
|
|
65
|
+
const items = [];
|
|
66
|
+
for (const entry of entries.slice(0, maxEntries)) {
|
|
67
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
68
|
+
if (entry.name.startsWith('.') && skipDotfilesAtRoot && currentDepth === 0) continue;
|
|
69
|
+
|
|
70
|
+
const itemPath = path.join(dirPath, entry.name);
|
|
71
|
+
const item = {
|
|
72
|
+
name: entry.name,
|
|
73
|
+
path: itemPath,
|
|
74
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (includeStats) {
|
|
78
|
+
try {
|
|
79
|
+
const stats = await fsPromises.stat(itemPath);
|
|
80
|
+
item.size = stats.size;
|
|
81
|
+
item.modified = stats.mtime.toISOString();
|
|
82
|
+
} catch { /* ignore stat errors */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (entry.isDirectory() && currentDepth < maxDepth - 1) {
|
|
86
|
+
item.children = await buildFileTree(itemPath, maxDepth, currentDepth + 1, options);
|
|
87
|
+
}
|
|
88
|
+
items.push(item);
|
|
89
|
+
}
|
|
90
|
+
return items;
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve a path that may start with ~ to an absolute path.
|
|
98
|
+
* @param {string} inputPath - Path to resolve
|
|
99
|
+
* @returns {string} Resolved absolute path
|
|
100
|
+
*/
|
|
101
|
+
export function resolveTildePath(inputPath) {
|
|
102
|
+
if (!inputPath) return os.homedir();
|
|
103
|
+
if (inputPath === '~') return os.homedir();
|
|
104
|
+
if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) {
|
|
105
|
+
return path.join(os.homedir(), inputPath.slice(2));
|
|
106
|
+
}
|
|
107
|
+
return path.resolve(inputPath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Blocked paths for file reads (security) */
|
|
111
|
+
export const BLOCKED_READ_PATTERNS = ['/etc/shadow', '/etc/passwd', '.ssh/id_rsa', '.ssh/id_ed25519', '/.env'];
|
|
112
|
+
|
|
113
|
+
/** Blocked paths for file writes (security) */
|
|
114
|
+
export const BLOCKED_WRITE_PATTERNS = [
|
|
115
|
+
'/etc/', '/usr/bin/', '/usr/sbin/',
|
|
116
|
+
'/windows/system32', '/windows/syswow64', '/program files',
|
|
117
|
+
'.ssh/', '/.env',
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/** Dangerous shell patterns to block */
|
|
121
|
+
export const DANGEROUS_SHELL_PATTERNS = [
|
|
122
|
+
'rm -rf /', 'mkfs', 'dd if=', ':(){', 'fork bomb', '> /dev/sd',
|
|
123
|
+
'format c:', 'format d:', 'format e:', 'del /s /q c:\\',
|
|
124
|
+
'rd /s /q c:\\', 'reg delete', 'bcdedit',
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if a normalized path matches any blocked patterns.
|
|
129
|
+
* @param {string} normalizedPath - Absolute path (resolved)
|
|
130
|
+
* @param {string[]} patterns - Blocked patterns to check
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
export function isBlockedPath(normalizedPath, patterns) {
|
|
134
|
+
const lower = normalizedPath.toLowerCase().replace(/\\/g, '/');
|
|
135
|
+
return patterns.some(b => lower.includes(b));
|
|
136
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "upfynai-code",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "Unified AI coding interface — access AI chat, terminal, file explorer, git, and visual canvas from any browser. Connect your local machine and code from anywhere.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
"bin/",
|
|
12
12
|
"src/",
|
|
13
13
|
"dist/",
|
|
14
|
+
"scripts/",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
|
-
"start": "node bin/cli.js"
|
|
19
|
+
"start": "node bin/cli.js",
|
|
20
|
+
"postinstall": "node scripts/postinstall.js",
|
|
21
|
+
"prepublishOnly": "node scripts/prepublish.js"
|
|
19
22
|
},
|
|
20
23
|
"keywords": [
|
|
21
24
|
"upfyn",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { playInstallAnimation } from '../src/animation.js';
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
await playInstallAnimation();
|
|
6
|
+
} catch {
|
|
7
|
+
// Silently fail — animation is cosmetic, don't break installs
|
|
8
|
+
console.log('\n ✓ upfynai-code installed. Run `uc --help` to get started.\n');
|
|
9
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Prepublish script — copies shared agents into dist/ for npm distribution.
|
|
4
|
+
*
|
|
5
|
+
* In the monorepo, connect.js imports from ../../shared/agents/.
|
|
6
|
+
* For npm, we copy those files into dist/agents/ and connect.js
|
|
7
|
+
* resolves them at runtime.
|
|
8
|
+
*/
|
|
9
|
+
import { cpSync, mkdirSync, rmSync, existsSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const cliRoot = join(__dirname, '..');
|
|
15
|
+
const distAgents = join(cliRoot, 'dist', 'agents');
|
|
16
|
+
const sharedAgents = join(cliRoot, '..', 'shared', 'agents');
|
|
17
|
+
|
|
18
|
+
// Clean
|
|
19
|
+
if (existsSync(distAgents)) rmSync(distAgents, { recursive: true });
|
|
20
|
+
mkdirSync(distAgents, { recursive: true });
|
|
21
|
+
|
|
22
|
+
// Copy all agent files
|
|
23
|
+
const files = [
|
|
24
|
+
'index.js', 'utils.js',
|
|
25
|
+
'claude.js', 'codex.js', 'cursor.js',
|
|
26
|
+
'shell.js', 'files.js', 'git.js', 'exec.js', 'detect.js',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
let copied = 0;
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
const src = join(sharedAgents, file);
|
|
32
|
+
const dest = join(distAgents, file);
|
|
33
|
+
if (existsSync(src)) {
|
|
34
|
+
cpSync(src, dest);
|
|
35
|
+
copied++;
|
|
36
|
+
} else {
|
|
37
|
+
console.warn(` [WARN] Missing: ${file}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(` [prepublish] Copied ${copied} agent files to dist/agents/`);
|
package/src/animation.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI spaceship-to-star animation for Upfyn-Code.
|
|
3
|
+
* Plays after install (postinstall) and before `uc connect` starts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const STAR_FRAMES = ['✦', '✧', '✦', '★'];
|
|
7
|
+
|
|
8
|
+
// Spaceship frames (4 animation states)
|
|
9
|
+
const SHIP = [
|
|
10
|
+
' ╱▏▔▔╲ ',
|
|
11
|
+
' ╱ ▏══ ╲ ',
|
|
12
|
+
'╱ ▏▁▁ ╲ ',
|
|
13
|
+
' ╱▏▔▔╲ ',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Simpler inline ship for the travel animation
|
|
17
|
+
const SHIP_R = [
|
|
18
|
+
' ▄▄ ',
|
|
19
|
+
' ◁━━██━━▷ ',
|
|
20
|
+
' ▀▀ ',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const SHIP_SMALL = '◁━━▶';
|
|
24
|
+
const SHIP_TRAVEL = '⟫⟫';
|
|
25
|
+
const EXHAUST_CHARS = ['░', '▒', '▓', '═', '~', '·'];
|
|
26
|
+
|
|
27
|
+
function sleep(ms) {
|
|
28
|
+
return new Promise(r => setTimeout(r, ms));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clearLines(n) {
|
|
32
|
+
for (let i = 0; i < n; i++) {
|
|
33
|
+
process.stdout.write('\x1b[1A\x1b[2K');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function dim(s) { return `\x1b[2m${s}\x1b[0m`; }
|
|
38
|
+
function cyan(s) { return `\x1b[36m${s}\x1b[0m`; }
|
|
39
|
+
function yellow(s) { return `\x1b[33m${s}\x1b[0m`; }
|
|
40
|
+
function bold(s) { return `\x1b[1m${s}\x1b[0m`; }
|
|
41
|
+
function magenta(s) { return `\x1b[35m${s}\x1b[0m`; }
|
|
42
|
+
function white(s) { return `\x1b[97m${s}\x1b[0m`; }
|
|
43
|
+
function green(s) { return `\x1b[32m${s}\x1b[0m`; }
|
|
44
|
+
function blue(s) { return `\x1b[34m${s}\x1b[0m`; }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a star field background line
|
|
48
|
+
*/
|
|
49
|
+
function starFieldLine(width, density = 0.08, frame = 0) {
|
|
50
|
+
let line = '';
|
|
51
|
+
for (let i = 0; i < width; i++) {
|
|
52
|
+
if (Math.random() < density) {
|
|
53
|
+
const stars = ['.', '·', '∘', '°', '✧'];
|
|
54
|
+
const s = stars[Math.floor(Math.random() * stars.length)];
|
|
55
|
+
line += dim(s);
|
|
56
|
+
} else {
|
|
57
|
+
line += ' ';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return line;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Main spaceship-to-star animation
|
|
65
|
+
* @param {'install'|'connect'} mode
|
|
66
|
+
*/
|
|
67
|
+
export async function playSpaceshipAnimation(mode = 'connect') {
|
|
68
|
+
const cols = Math.min(process.stdout.columns || 70, 80);
|
|
69
|
+
const totalFrames = 28;
|
|
70
|
+
const LINES = 11; // total lines we'll use
|
|
71
|
+
|
|
72
|
+
const starX = cols - 6;
|
|
73
|
+
|
|
74
|
+
// Phase 1: Launch sequence text
|
|
75
|
+
const tagline = mode === 'install'
|
|
76
|
+
? 'Installation complete!'
|
|
77
|
+
: 'Launching relay bridge...';
|
|
78
|
+
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(dim(' ─'.repeat(Math.floor(cols / 3))));
|
|
81
|
+
console.log('');
|
|
82
|
+
|
|
83
|
+
// Phase 2: Spaceship travels across the screen toward a star
|
|
84
|
+
for (let frame = 0; frame < totalFrames; frame++) {
|
|
85
|
+
const progress = frame / (totalFrames - 1); // 0 → 1
|
|
86
|
+
const shipX = Math.floor(progress * (cols - 16)) + 2;
|
|
87
|
+
|
|
88
|
+
// Build exhaust trail
|
|
89
|
+
const exhaustLen = Math.min(shipX, Math.floor(progress * 20));
|
|
90
|
+
let exhaust = '';
|
|
91
|
+
for (let e = 0; e < exhaustLen; e++) {
|
|
92
|
+
const intensity = 1 - (e / exhaustLen);
|
|
93
|
+
if (intensity > 0.7) exhaust = '▓' + exhaust;
|
|
94
|
+
else if (intensity > 0.4) exhaust = '▒' + exhaust;
|
|
95
|
+
else if (intensity > 0.2) exhaust = '░' + exhaust;
|
|
96
|
+
else exhaust = '·' + exhaust;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Star pulse
|
|
100
|
+
const starChar = STAR_FRAMES[frame % STAR_FRAMES.length];
|
|
101
|
+
const starGlow = progress > 0.7 ? yellow('✦ ') : '';
|
|
102
|
+
|
|
103
|
+
// Build the 5 display lines
|
|
104
|
+
const lines = [];
|
|
105
|
+
|
|
106
|
+
// Line 1: starfield
|
|
107
|
+
lines.push(' ' + starFieldLine(cols - 4, 0.05, frame));
|
|
108
|
+
|
|
109
|
+
// Line 2: top space
|
|
110
|
+
lines.push(' ' + starFieldLine(cols - 4, 0.03, frame));
|
|
111
|
+
|
|
112
|
+
// Line 3: ship row (main action)
|
|
113
|
+
let shipRow = '';
|
|
114
|
+
const beforeShip = Math.max(0, shipX - exhaustLen);
|
|
115
|
+
shipRow += ' '.repeat(beforeShip);
|
|
116
|
+
shipRow += dim(exhaust);
|
|
117
|
+
shipRow += cyan(' ◁━━▶');
|
|
118
|
+
// Fill to star position
|
|
119
|
+
const afterShip = Math.max(0, starX - shipX - 7);
|
|
120
|
+
// Dots between ship and star
|
|
121
|
+
let midSpace = '';
|
|
122
|
+
for (let d = 0; d < afterShip; d++) {
|
|
123
|
+
midSpace += Math.random() < 0.06 ? dim('·') : ' ';
|
|
124
|
+
}
|
|
125
|
+
shipRow += midSpace;
|
|
126
|
+
if (progress < 0.92) {
|
|
127
|
+
shipRow += starGlow + yellow(starChar);
|
|
128
|
+
} else {
|
|
129
|
+
shipRow += yellow('✦★✦');
|
|
130
|
+
}
|
|
131
|
+
lines.push(shipRow);
|
|
132
|
+
|
|
133
|
+
// Line 4: thrust glow
|
|
134
|
+
let thrustRow = ' '.repeat(Math.max(0, shipX + 1));
|
|
135
|
+
if (frame % 2 === 0) {
|
|
136
|
+
thrustRow += dim(magenta('~≈~'));
|
|
137
|
+
} else {
|
|
138
|
+
thrustRow += dim(magenta('≈~≈'));
|
|
139
|
+
}
|
|
140
|
+
lines.push(thrustRow);
|
|
141
|
+
|
|
142
|
+
// Line 5: starfield
|
|
143
|
+
lines.push(' ' + starFieldLine(cols - 4, 0.04, frame));
|
|
144
|
+
|
|
145
|
+
// Line 6: message (centered)
|
|
146
|
+
const msg = progress < 0.3
|
|
147
|
+
? dim(` ${tagline}`)
|
|
148
|
+
: progress < 0.6
|
|
149
|
+
? cyan(` ⟫ Navigating to the stars...`)
|
|
150
|
+
: progress < 0.9
|
|
151
|
+
? magenta(` ⟫⟫ Almost there...`)
|
|
152
|
+
: green(` ★ ${mode === 'install' ? 'Ready for launch!' : 'Connection established!'}`);
|
|
153
|
+
lines.push(msg);
|
|
154
|
+
|
|
155
|
+
// Line 7: progress bar
|
|
156
|
+
const barWidth = cols - 10;
|
|
157
|
+
const filled = Math.floor(progress * barWidth);
|
|
158
|
+
const bar = ' ' + dim('[')
|
|
159
|
+
+ cyan('█'.repeat(filled))
|
|
160
|
+
+ dim('░'.repeat(barWidth - filled))
|
|
161
|
+
+ dim(']')
|
|
162
|
+
+ dim(` ${Math.floor(progress * 100)}%`);
|
|
163
|
+
lines.push(bar);
|
|
164
|
+
|
|
165
|
+
// Print
|
|
166
|
+
if (frame > 0) clearLines(lines.length);
|
|
167
|
+
for (const l of lines) console.log(l);
|
|
168
|
+
|
|
169
|
+
// Speed: start slow, middle fast, end slow
|
|
170
|
+
const delay = progress < 0.2 ? 120
|
|
171
|
+
: progress > 0.85 ? 150
|
|
172
|
+
: 60;
|
|
173
|
+
await sleep(delay);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Phase 3: Final flash
|
|
177
|
+
await sleep(200);
|
|
178
|
+
clearLines(7);
|
|
179
|
+
|
|
180
|
+
// Arrival burst
|
|
181
|
+
const burstLines = [];
|
|
182
|
+
burstLines.push(' ' + starFieldLine(cols - 4, 0.06));
|
|
183
|
+
burstLines.push('');
|
|
184
|
+
burstLines.push(
|
|
185
|
+
' '.repeat(Math.floor(cols / 2 - 12))
|
|
186
|
+
+ yellow('· ✧ · ★ ')
|
|
187
|
+
+ bold(white('UPFYN'))
|
|
188
|
+
+ yellow(' ★ · ✧ ·')
|
|
189
|
+
);
|
|
190
|
+
burstLines.push('');
|
|
191
|
+
|
|
192
|
+
if (mode === 'install') {
|
|
193
|
+
burstLines.push(
|
|
194
|
+
' '.repeat(Math.floor(cols / 2 - 20))
|
|
195
|
+
+ green('✓ ') + bold('upfynai-code') + dim(' installed successfully')
|
|
196
|
+
);
|
|
197
|
+
burstLines.push('');
|
|
198
|
+
burstLines.push(dim(' Quick start:'));
|
|
199
|
+
burstLines.push(cyan(' uc login ') + dim('— authenticate'));
|
|
200
|
+
burstLines.push(cyan(' uc connect ') + dim('— bridge to cloud'));
|
|
201
|
+
burstLines.push(cyan(' uc --local ') + dim('— start local server'));
|
|
202
|
+
} else {
|
|
203
|
+
burstLines.push(
|
|
204
|
+
' '.repeat(Math.floor(cols / 2 - 16))
|
|
205
|
+
+ green('✓ ') + bold('Relay bridge activated')
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
burstLines.push('');
|
|
210
|
+
burstLines.push(dim(' ─'.repeat(Math.floor(cols / 3))));
|
|
211
|
+
burstLines.push('');
|
|
212
|
+
|
|
213
|
+
for (const l of burstLines) console.log(l);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Postinstall animation (shorter variant)
|
|
218
|
+
*/
|
|
219
|
+
export async function playInstallAnimation() {
|
|
220
|
+
return playSpaceshipAnimation('install');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Connect animation
|
|
225
|
+
*/
|
|
226
|
+
export async function playConnectAnimation() {
|
|
227
|
+
return playSpaceshipAnimation('connect');
|
|
228
|
+
}
|