@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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/bin/tgcloud.js +2 -0
  4. package/package.json +36 -0
  5. package/src/api/client.js +136 -0
  6. package/src/api/endpoints.js +124 -0
  7. package/src/cli.js +51 -0
  8. package/src/commands/add.js +133 -0
  9. package/src/commands/completion.js +298 -0
  10. package/src/commands/deploy.js +319 -0
  11. package/src/commands/diff.js +57 -0
  12. package/src/commands/fetch.js +23 -0
  13. package/src/commands/init.js +226 -0
  14. package/src/commands/login.js +56 -0
  15. package/src/commands/migrate.js +142 -0
  16. package/src/commands/pull.js +156 -0
  17. package/src/commands/reset.js +95 -0
  18. package/src/commands/run.js +145 -0
  19. package/src/commands/status.js +72 -0
  20. package/src/commands/webhook.js +139 -0
  21. package/src/core/capabilities.js +83 -0
  22. package/src/core/credentials.js +229 -0
  23. package/src/core/db-changes.js +221 -0
  24. package/src/core/file-diff.js +11 -0
  25. package/src/core/hasher.js +11 -0
  26. package/src/core/local-diff.js +57 -0
  27. package/src/core/migration-flow.js +356 -0
  28. package/src/core/project-layout.js +294 -0
  29. package/src/core/project-root.js +47 -0
  30. package/src/core/run-output.js +134 -0
  31. package/src/core/scanner.js +115 -0
  32. package/src/core/snapshot.js +132 -0
  33. package/src/core/state.js +92 -0
  34. package/src/core/sync.js +50 -0
  35. package/src/templates/AGENTS.md +95 -0
  36. package/src/templates/docs/tgcloud-sdk.md +347 -0
  37. package/src/templates/gitignore +2 -0
  38. package/src/templates/handlers/_default_.js +6 -0
  39. package/src/templates/handlers/message.js +13 -0
  40. package/src/templates/lib/_default_.js +2 -0
  41. package/src/templates/schema.js +10 -0
  42. package/src/utils/logger.js +54 -0
  43. package/src/utils/pager.js +76 -0
  44. package/src/utils/prompt.js +220 -0
  45. 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
+ }
@@ -0,0 +1,5 @@
1
+ import ora from 'ora';
2
+
3
+ export function createSpinner(text) {
4
+ return ora({ text, color: 'cyan' });
5
+ }