@tgcloud/cli 0.1.0
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/LICENSE +21 -0
- package/README.md +53 -0
- package/bin/tgcloud.js +2 -0
- package/package.json +36 -0
- package/src/api/client.js +136 -0
- package/src/api/endpoints.js +124 -0
- package/src/cli.js +51 -0
- package/src/commands/add.js +133 -0
- package/src/commands/completion.js +298 -0
- package/src/commands/deploy.js +319 -0
- package/src/commands/diff.js +57 -0
- package/src/commands/fetch.js +23 -0
- package/src/commands/init.js +226 -0
- package/src/commands/login.js +56 -0
- package/src/commands/migrate.js +142 -0
- package/src/commands/pull.js +156 -0
- package/src/commands/reset.js +95 -0
- package/src/commands/run.js +145 -0
- package/src/commands/status.js +72 -0
- package/src/commands/webhook.js +139 -0
- package/src/core/capabilities.js +83 -0
- package/src/core/credentials.js +229 -0
- package/src/core/db-changes.js +221 -0
- package/src/core/file-diff.js +11 -0
- package/src/core/hasher.js +11 -0
- package/src/core/local-diff.js +57 -0
- package/src/core/migration-flow.js +356 -0
- package/src/core/project-layout.js +294 -0
- package/src/core/project-root.js +47 -0
- package/src/core/run-output.js +134 -0
- package/src/core/scanner.js +115 -0
- package/src/core/snapshot.js +132 -0
- package/src/core/state.js +92 -0
- package/src/core/sync.js +50 -0
- package/src/templates/AGENTS.md +95 -0
- package/src/templates/docs/tgcloud-sdk.md +347 -0
- package/src/templates/gitignore +2 -0
- package/src/templates/handlers/_default_.js +6 -0
- package/src/templates/handlers/message.js +13 -0
- package/src/templates/lib/_default_.js +2 -0
- package/src/templates/schema.js +10 -0
- package/src/utils/logger.js +54 -0
- package/src/utils/pager.js +76 -0
- package/src/utils/prompt.js +220 -0
- package/src/utils/spinner.js +5 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
|
|
3
|
+
// A single persistent line reader feeding a queue, shared across all prompts in
|
|
4
|
+
// the process.
|
|
5
|
+
//
|
|
6
|
+
// Why not readline.question per prompt: question() only captures the line that
|
|
7
|
+
// arrives WHILE it is waiting. With piped / non-TTY input (scripts, CI, tests)
|
|
8
|
+
// the producer writes every line at once and ends stdin; lines that arrive
|
|
9
|
+
// between prompts (or before the next question() is set up) are emitted by
|
|
10
|
+
// readline with no one listening and are lost — so a multi-step prompt sequence
|
|
11
|
+
// silently stops applying. Buffering every 'line' into a queue decouples input
|
|
12
|
+
// capture from prompt timing, so early/piped lines survive until consumed.
|
|
13
|
+
//
|
|
14
|
+
// Call closePrompts() once the interactive flow ends so the open stdin handle
|
|
15
|
+
// doesn't keep the process alive.
|
|
16
|
+
let _rl;
|
|
17
|
+
const _lines = []; // buffered input lines not yet consumed
|
|
18
|
+
let _waiter = null; // resolver for a prompt awaiting the next line
|
|
19
|
+
let _closed = false;
|
|
20
|
+
|
|
21
|
+
function ensureReader() {
|
|
22
|
+
if (_rl) return;
|
|
23
|
+
_rl = createInterface({ input: process.stdin });
|
|
24
|
+
_rl.on('line', (line) => {
|
|
25
|
+
if (_waiter) {
|
|
26
|
+
const w = _waiter;
|
|
27
|
+
_waiter = null;
|
|
28
|
+
w(line);
|
|
29
|
+
} else {
|
|
30
|
+
_lines.push(line);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
_rl.on('close', () => {
|
|
34
|
+
_closed = true;
|
|
35
|
+
if (_waiter) {
|
|
36
|
+
const w = _waiter;
|
|
37
|
+
_waiter = null;
|
|
38
|
+
w(null); // EOF — no more input
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nextLine() {
|
|
44
|
+
ensureReader();
|
|
45
|
+
if (_lines.length) return Promise.resolve(_lines.shift());
|
|
46
|
+
if (_closed) return Promise.resolve(null);
|
|
47
|
+
return new Promise((resolve) => { _waiter = resolve; });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function closePrompts() {
|
|
51
|
+
if (_rl) {
|
|
52
|
+
_rl.close();
|
|
53
|
+
_rl = undefined;
|
|
54
|
+
}
|
|
55
|
+
_lines.length = 0;
|
|
56
|
+
_waiter = null;
|
|
57
|
+
_closed = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function ask(question) {
|
|
61
|
+
// The reader has no output stream, so print the prompt ourselves; in a TTY the
|
|
62
|
+
// terminal echoes the typed answer, in a pipe there's nothing to echo.
|
|
63
|
+
process.stdout.write(question);
|
|
64
|
+
const line = await nextLine();
|
|
65
|
+
return line == null ? '' : line.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function confirm(question) {
|
|
69
|
+
// Uppercase N signals the default: empty input (or anything but y/yes) → false,
|
|
70
|
+
// matching askLetter's uppercased-default convention.
|
|
71
|
+
const answer = await ask(`${question} (y/N) `);
|
|
72
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Ask the user to pick one of several single-letter choices.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} question — e.g. `chalk.red('Apply drop_table old_sessions?')`
|
|
79
|
+
* @param {string} choices — allowed letters, e.g. "ynaq". First letter is
|
|
80
|
+
* the default (shown uppercase); hitting Enter
|
|
81
|
+
* returns it.
|
|
82
|
+
* Returns the chosen letter (lowercase).
|
|
83
|
+
* Reprompts on invalid input.
|
|
84
|
+
*/
|
|
85
|
+
export async function askChoice(question, choices) {
|
|
86
|
+
const lower = choices.toLowerCase();
|
|
87
|
+
const def = lower[0];
|
|
88
|
+
const label = lower.split('').map((c, i) => (i === 0 ? c.toUpperCase() : c)).join('/');
|
|
89
|
+
|
|
90
|
+
while (true) {
|
|
91
|
+
const raw = await ask(`${question} (${label}) `);
|
|
92
|
+
const input = raw.trim().toLowerCase();
|
|
93
|
+
if (input === '') return def;
|
|
94
|
+
if (input.length === 1 && lower.includes(input)) return input;
|
|
95
|
+
// invalid → reprompt
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Returned by askLetter when input ends before a choice is made (Ctrl-D at a
|
|
100
|
+
// TTY, or piped input exhausted). Callers guarding destructive actions must
|
|
101
|
+
// treat this as "abort", never as the first option — see the migration flow.
|
|
102
|
+
export const EOF = Symbol('prompt-eof');
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Like `askChoice` but with descriptive labels rendered as `[k]label`.
|
|
106
|
+
*
|
|
107
|
+
* A default is applied on empty input ONLY when an option is explicitly marked
|
|
108
|
+
* `default: true` (rendered uppercase). With no explicit default, empty input
|
|
109
|
+
* reprompts — so a bare Enter can never trigger a destructive choice by accident.
|
|
110
|
+
* End-of-input returns the EOF sentinel rather than silently picking an option.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} question
|
|
113
|
+
* @param {Array<{key:string,label:string,default?:boolean}>} options
|
|
114
|
+
* Returns the chosen key (lowercase), or EOF. Reprompts on empty (no default) or invalid input.
|
|
115
|
+
*/
|
|
116
|
+
export async function askLetter(question, options) {
|
|
117
|
+
const explicit = options.find((o) => o.default);
|
|
118
|
+
const def = explicit ? explicit.key.toLowerCase() : null;
|
|
119
|
+
// If the label starts with the key letter (e.g. key='y' label='yes'),
|
|
120
|
+
// render it as `[y]es` rather than the duplicating `[y]yes`; the default
|
|
121
|
+
// option's key is shown uppercase so the Enter behavior is visible.
|
|
122
|
+
const renderOpt = (o) => {
|
|
123
|
+
const k = o.key.toLowerCase();
|
|
124
|
+
const shown = o.default ? o.key.toUpperCase() : o.key;
|
|
125
|
+
const lbl = o.label;
|
|
126
|
+
return lbl.toLowerCase().startsWith(k)
|
|
127
|
+
? `[${shown}]${lbl.slice(1)}`
|
|
128
|
+
: `[${shown}]${lbl}`;
|
|
129
|
+
};
|
|
130
|
+
const opts = options.map(renderOpt).join(' / ');
|
|
131
|
+
const valid = new Set(options.map((o) => o.key.toLowerCase()));
|
|
132
|
+
|
|
133
|
+
while (true) {
|
|
134
|
+
process.stdout.write(`${question} ${opts} `);
|
|
135
|
+
const line = await nextLine();
|
|
136
|
+
if (line == null) return EOF; // Ctrl-D / end of piped input → abort
|
|
137
|
+
const input = line.trim().toLowerCase();
|
|
138
|
+
if (input === '') {
|
|
139
|
+
if (def) return def;
|
|
140
|
+
continue; // no explicit default → never auto-pick
|
|
141
|
+
}
|
|
142
|
+
if (input.length === 1 && valid.has(input)) return input;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Block until the user presses Enter. Used in non-interactive blocks of the
|
|
148
|
+
* migration walk-through (manual / undocumented sections) to acknowledge
|
|
149
|
+
* the message before continuing.
|
|
150
|
+
*/
|
|
151
|
+
export async function waitForEnter(prompt = 'Press Enter to continue...') {
|
|
152
|
+
process.stdout.write(prompt);
|
|
153
|
+
await nextLine();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Prompt for a secret input without echoing characters.
|
|
158
|
+
* Uses raw mode on stdin to suppress echo; no external dependency.
|
|
159
|
+
*
|
|
160
|
+
* If the current stdin isn't a TTY (e.g. piped), falls back to reading a line
|
|
161
|
+
* without masking — secret won't be echoed visibly since it's not interactive.
|
|
162
|
+
*/
|
|
163
|
+
export function askPassword(question) {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const stdin = process.stdin;
|
|
166
|
+
const stdout = process.stdout;
|
|
167
|
+
|
|
168
|
+
stdout.write(question);
|
|
169
|
+
|
|
170
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
|
|
171
|
+
// Non-interactive (piped/CI): read through the SHARED line queue, not a
|
|
172
|
+
// private readline. A private interface on stdin never fires its callback
|
|
173
|
+
// on EOF, so the login loop hangs and the process exits 0 without saving.
|
|
174
|
+
// nextLine() resolves null on EOF, letting the caller stop instead of
|
|
175
|
+
// spinning on empty input. No echo suppression — not a terminal.
|
|
176
|
+
nextLine().then((line) => resolve(line == null ? null : line.trim()));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let input = '';
|
|
181
|
+
stdin.setRawMode(true);
|
|
182
|
+
stdin.resume();
|
|
183
|
+
stdin.setEncoding('utf8');
|
|
184
|
+
|
|
185
|
+
const cleanup = () => {
|
|
186
|
+
stdin.setRawMode(false);
|
|
187
|
+
stdin.pause();
|
|
188
|
+
stdin.removeListener('data', onData);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const onData = (chunk) => {
|
|
192
|
+
// chunk may contain multiple characters (e.g. pasted token)
|
|
193
|
+
for (const ch of chunk) {
|
|
194
|
+
if (ch === '\r' || ch === '\n') {
|
|
195
|
+
cleanup();
|
|
196
|
+
stdout.write('\n');
|
|
197
|
+
resolve(input.trim());
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (ch === '\u0003') {
|
|
201
|
+
// Ctrl-C
|
|
202
|
+
cleanup();
|
|
203
|
+
stdout.write('\n');
|
|
204
|
+
process.exit(130);
|
|
205
|
+
}
|
|
206
|
+
if (ch === '\u007f' || ch === '\b') {
|
|
207
|
+
// Backspace
|
|
208
|
+
input = input.slice(0, -1);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Ignore other control characters
|
|
212
|
+
const code = ch.charCodeAt(0);
|
|
213
|
+
if (code < 32) continue;
|
|
214
|
+
input += ch;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
stdin.on('data', onData);
|
|
219
|
+
});
|
|
220
|
+
}
|