ninedeploy 0.18.26 → 0.18.28

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 (74) hide show
  1. package/dist/commands/databaseUpgrade.d.ts +27 -0
  2. package/dist/commands/databaseUpgrade.d.ts.map +1 -0
  3. package/dist/commands/databaseUpgrade.js +151 -0
  4. package/dist/commands/databaseUpgrade.js.map +1 -0
  5. package/dist/commands/importApply.d.ts +114 -0
  6. package/dist/commands/importApply.d.ts.map +1 -0
  7. package/dist/commands/importApply.js +403 -0
  8. package/dist/commands/importApply.js.map +1 -0
  9. package/dist/commands/importCutover.d.ts +95 -0
  10. package/dist/commands/importCutover.d.ts.map +1 -0
  11. package/dist/commands/importCutover.js +293 -0
  12. package/dist/commands/importCutover.js.map +1 -0
  13. package/dist/commands/importData.d.ts +60 -0
  14. package/dist/commands/importData.d.ts.map +1 -0
  15. package/dist/commands/importData.js +286 -0
  16. package/dist/commands/importData.js.map +1 -0
  17. package/dist/commands/importPlan.d.ts +32 -0
  18. package/dist/commands/importPlan.d.ts.map +1 -0
  19. package/dist/commands/importPlan.js +138 -0
  20. package/dist/commands/importPlan.js.map +1 -0
  21. package/dist/commands/importVolumes.d.ts +38 -0
  22. package/dist/commands/importVolumes.d.ts.map +1 -0
  23. package/dist/commands/importVolumes.js +161 -0
  24. package/dist/commands/importVolumes.js.map +1 -0
  25. package/dist/index.js +59 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/importers/caprover.d.ts +13 -0
  28. package/dist/lib/importers/caprover.d.ts.map +1 -0
  29. package/dist/lib/importers/caprover.js +150 -0
  30. package/dist/lib/importers/caprover.js.map +1 -0
  31. package/dist/lib/importers/connections.d.ts +39 -0
  32. package/dist/lib/importers/connections.d.ts.map +1 -0
  33. package/dist/lib/importers/connections.js +119 -0
  34. package/dist/lib/importers/connections.js.map +1 -0
  35. package/dist/lib/importers/coolify.d.ts +10 -0
  36. package/dist/lib/importers/coolify.d.ts.map +1 -0
  37. package/dist/lib/importers/coolify.js +174 -0
  38. package/dist/lib/importers/coolify.js.map +1 -0
  39. package/dist/lib/importers/dokploy.d.ts +13 -0
  40. package/dist/lib/importers/dokploy.d.ts.map +1 -0
  41. package/dist/lib/importers/dokploy.js +183 -0
  42. package/dist/lib/importers/dokploy.js.map +1 -0
  43. package/dist/lib/importers/heroku.d.ts +50 -0
  44. package/dist/lib/importers/heroku.d.ts.map +1 -0
  45. package/dist/lib/importers/heroku.js +159 -0
  46. package/dist/lib/importers/heroku.js.map +1 -0
  47. package/dist/lib/importers/plan.d.ts +137 -0
  48. package/dist/lib/importers/plan.d.ts.map +1 -0
  49. package/dist/lib/importers/plan.js +110 -0
  50. package/dist/lib/importers/plan.js.map +1 -0
  51. package/dist/lib/importers/shell.d.ts +24 -0
  52. package/dist/lib/importers/shell.d.ts.map +1 -0
  53. package/dist/lib/importers/shell.js +45 -0
  54. package/dist/lib/importers/shell.js.map +1 -0
  55. package/dist/lib/importers/sources.d.ts +26 -0
  56. package/dist/lib/importers/sources.d.ts.map +1 -0
  57. package/dist/lib/importers/sources.js +100 -0
  58. package/dist/lib/importers/sources.js.map +1 -0
  59. package/package.json +3 -3
  60. package/src/commands/databaseUpgrade.ts +144 -0
  61. package/src/commands/importApply.ts +445 -0
  62. package/src/commands/importCutover.ts +329 -0
  63. package/src/commands/importData.ts +302 -0
  64. package/src/commands/importPlan.ts +143 -0
  65. package/src/commands/importVolumes.ts +168 -0
  66. package/src/index.ts +61 -0
  67. package/src/lib/importers/caprover.ts +141 -0
  68. package/src/lib/importers/connections.ts +133 -0
  69. package/src/lib/importers/coolify.ts +164 -0
  70. package/src/lib/importers/dokploy.ts +167 -0
  71. package/src/lib/importers/heroku.ts +168 -0
  72. package/src/lib/importers/plan.ts +209 -0
  73. package/src/lib/importers/shell.ts +56 -0
  74. package/src/lib/importers/sources.ts +96 -0
@@ -0,0 +1,100 @@
1
+ import { stripUserinfo } from './plan.js';
2
+ export class SourceError extends Error {
3
+ }
4
+ /** CapRover asked for a two-factor code (status 1114). */
5
+ export class OtpRequiredError extends SourceError {
6
+ }
7
+ /** `https://host[:port][/prefix]` without a trailing slash or userinfo. */
8
+ export function normaliseBase(raw) {
9
+ let u;
10
+ try {
11
+ u = new URL(raw);
12
+ }
13
+ catch {
14
+ throw new SourceError(`Not a URL: ${stripUserinfo(raw)}`);
15
+ }
16
+ if (u.protocol !== 'https:' && u.protocol !== 'http:')
17
+ throw new SourceError('The source URL must start with https:// (or http://).');
18
+ return stripUserinfo(`${u.origin}${u.pathname}`).replace(/\/+$/, '');
19
+ }
20
+ async function body(res) {
21
+ const text = await res.text();
22
+ if (!text)
23
+ return null;
24
+ try {
25
+ return JSON.parse(text);
26
+ }
27
+ catch {
28
+ return text;
29
+ }
30
+ }
31
+ function httpError(res, what, hint = '') {
32
+ return new SourceError(`${what} answered HTTP ${res.status}${hint ? `: ${hint}` : ''}`);
33
+ }
34
+ /** Coolify: `Authorization: Bearer <token>`; a plain `read` token is enough. */
35
+ export function coolifySource(base, token, f = fetch) {
36
+ return async (path) => {
37
+ const res = await f(`${base}${path}`, { headers: { authorization: `Bearer ${token}`, accept: 'application/json' } });
38
+ const data = await body(res);
39
+ if (res.ok)
40
+ return data;
41
+ const message = typeof data === 'object' && data && 'message' in data ? String(data.message) : '';
42
+ if (res.status === 403 && /api is disabled/i.test(message))
43
+ throw httpError(res, 'Coolify', 'the API is off. Turn it on under Settings → Advanced → API access.');
44
+ if (res.status === 401)
45
+ throw httpError(res, 'Coolify', 'the token was refused (create one under Keys & Tokens → API tokens, with "read").');
46
+ if (res.status === 403)
47
+ throw httpError(res, 'Coolify', message || 'the token lacks access (IP allow-list, scope or ability).');
48
+ throw httpError(res, `Coolify ${path}`, message);
49
+ };
50
+ }
51
+ /** Dokploy: `x-api-key`, tRPC procedures as `GET /api/<procedure>?<input>`. */
52
+ export function dokploySource(base, apiKey, f = fetch) {
53
+ return async (path) => {
54
+ const res = await f(`${base}${path}`, { headers: { 'x-api-key': apiKey, accept: 'application/json' } });
55
+ const data = await body(res);
56
+ if (res.ok)
57
+ return data;
58
+ // Dokploy answers a plain 401 for a wrong key and for one past its rate limit alike (checked live on v0.31.0).
59
+ if (res.status === 401)
60
+ throw httpError(res, 'Dokploy', 'the API key was refused: it is wrong, belongs to another organization, or has used up its rate limit. A Dokploy key allows 10 requests a day unless it was created with rate limiting off, and a plan makes 2 plus one per application, compose stack and database. Create one under Profile → API keys with rate limiting off.');
61
+ const message = typeof data === 'object' && data && 'message' in data ? String(data.message) : '';
62
+ throw httpError(res, `Dokploy ${path.split('?')[0]}`, message);
63
+ };
64
+ }
65
+ /**
66
+ * CapRover: sign in once (never retried: each wrong password starts a 30 s
67
+ * back-off), then send `x-captain-auth`. Errors arrive with HTTP 200 and a
68
+ * non-100 `status` in the envelope.
69
+ */
70
+ export async function caproverSource(base, password, otp, f = fetch) {
71
+ const res = await f(`${base}/api/v2/login/`, {
72
+ method: 'POST',
73
+ headers: { 'content-type': 'application/json', 'x-namespace': 'captain' },
74
+ body: JSON.stringify(otp ? { password, otpToken: otp } : { password }),
75
+ });
76
+ if (!res.ok)
77
+ throw httpError(res, 'CapRover sign-in');
78
+ const env = (await body(res));
79
+ if (env?.status === 1114)
80
+ throw new OtpRequiredError('CapRover asks for a two-factor code.');
81
+ if (env?.status === 1113)
82
+ throw new SourceError('CapRover is pausing sign-ins after a wrong password; wait 30 seconds and try again.');
83
+ if (env?.status === 1105)
84
+ throw new SourceError('CapRover refused the password (or the two-factor code).');
85
+ const token = env?.data?.token;
86
+ if (env?.status !== 100 || typeof token !== 'string')
87
+ throw new SourceError(`CapRover sign-in failed (status ${env?.status ?? '?'}).`);
88
+ return async (path) => {
89
+ const r = await f(`${base}${path}`, { headers: { 'x-captain-auth': token, 'x-namespace': 'captain', accept: 'application/json' } });
90
+ if (!r.ok)
91
+ throw httpError(r, `CapRover ${path}`);
92
+ const e = (await body(r));
93
+ if (e?.status === 1106)
94
+ throw new SourceError('CapRover no longer accepts the session (it restarted, or the password changed); run the import again.');
95
+ if (e?.status !== 100)
96
+ throw new SourceError(`CapRover ${path} failed (status ${e?.status ?? '?'}${e?.description ? `: ${e.description}` : ''}).`);
97
+ return e.data;
98
+ };
99
+ }
100
+ //# sourceMappingURL=sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sources.js","sourceRoot":"","sources":["../../../src/lib/importers/sources.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ninedeploy",
3
- "version": "0.18.26",
3
+ "version": "0.18.28",
4
4
  "description": "Interactive CLI for NineDeploy self-hosted PaaS",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,8 +23,8 @@
23
23
  "ninedeploy": "./dist/index.js"
24
24
  },
25
25
  "dependencies": {
26
- "@ninedeploy/sdk": "0.18.26",
27
- "@ninedeploy/schemas": "0.18.26",
26
+ "@ninedeploy/sdk": "0.18.28",
27
+ "@ninedeploy/schemas": "0.18.28",
28
28
  "commander": "^15.0.0",
29
29
  "ws": "^8.21.3"
30
30
  },
@@ -0,0 +1,144 @@
1
+ /**
2
+ * `ninedeploy databases upgrade …` — O2 major-version upgrades (PostgreSQL,
3
+ * MySQL and MariaDB on the panel host).
4
+ *
5
+ * status <dbId> current release, targets, and the upgrade history
6
+ * start <dbId> --to <release> [--yes] [--no-wait]
7
+ * stop, move into a new volume (PostgreSQL: dump and
8
+ * restore; MySQL/MariaDB: copy and upgrade in place),
9
+ * compare, switch
10
+ * rollback <dbId> <upgradeId> [--yes] point the database back at the previous volume
11
+ * finalize <dbId> <upgradeId> [--yes] delete the volume the database no longer uses
12
+ */
13
+ import type { NineDeployClient } from '../client.js';
14
+ import { prompt } from '../prompts.js';
15
+ import { c, error, header, info, kv, spinner, success, table } from '../lib/format.js';
16
+
17
+ type Upgrade = Awaited<ReturnType<NineDeployClient['databases']['upgrades']['start']>>;
18
+
19
+ const LABELS: Record<string, string> = { postgres: 'PostgreSQL', mysql: 'MySQL', mariadb: 'MariaDB' };
20
+ const label = (engine: string) => LABELS[engine] ?? engine;
21
+ /** `fromSeries`/`toSeries` arrive from 0.18.28; older panels send only the majors. */
22
+ const fromOf = (u: Upgrade) => u.fromSeries ?? String(u.fromMajor);
23
+ const toOf = (u: Upgrade) => u.toSeries ?? String(u.toMajor);
24
+
25
+ const num = (v: string | undefined, usage: string): number | null => {
26
+ const n = Number(v);
27
+ if (!Number.isInteger(n) || n <= 0) {
28
+ error(usage);
29
+ process.exitCode = 1;
30
+ return null;
31
+ }
32
+ return n;
33
+ };
34
+ const fail = (err: unknown): void => {
35
+ error(err instanceof Error ? err.message : String(err));
36
+ process.exitCode = 1;
37
+ };
38
+ const confirm = async (word: string, question: string): Promise<boolean> => {
39
+ const answer = await prompt(`${question} Type "${word}" to continue`, '');
40
+ if (answer.trim() !== word) {
41
+ info('Aborted.');
42
+ return false;
43
+ }
44
+ return true;
45
+ };
46
+
47
+ function describeOutcome(u: Upgrade, dbId: number): void {
48
+ if (u.status === 'completed') {
49
+ success(`Upgraded to ${label(u.engine)} ${toOf(u)}.`);
50
+ if (u.verification) {
51
+ const extra = u.verification.users != null ? `, ${u.verification.users} account(s)` : `, ${u.verification.sequences} sequence(s)`;
52
+ info(`Compared before the switch: ${u.verification.tables} table(s), ${u.verification.rows} row(s)${extra}.`);
53
+ }
54
+ info(`The previous volume ${u.previousVolume} is kept. Check your services, then: ninedeploy databases upgrade finalize ${dbId} ${u.id}`);
55
+ info(`To go back to ${label(u.engine)} ${fromOf(u)}: ninedeploy databases upgrade rollback ${dbId} ${u.id}`);
56
+ } else {
57
+ error(`The upgrade ${u.status === 'failed' ? 'failed' : `ended ${u.status}`}: ${u.error ?? 'no reason recorded'}`);
58
+ process.exitCode = 1;
59
+ }
60
+ }
61
+
62
+ export async function upgradeStatus(client: NineDeployClient, dbIdStr: string): Promise<void> {
63
+ const id = num(dbIdStr, 'Usage: ninedeploy databases upgrade status <dbId>');
64
+ if (id === null) return;
65
+ let options: Awaited<ReturnType<NineDeployClient['databases']['upgrades']['options']>>;
66
+ let history: Upgrade[];
67
+ try {
68
+ [options, history] = await spinner('Reading the cluster', () => Promise.all([client.databases.upgrades.options(id), client.databases.upgrades.list(id)]));
69
+ } catch (err) { fail(err); return; }
70
+ header('Major upgrade');
71
+ const name = label(options.engine ?? 'postgres');
72
+ const current = options.currentSeries ?? (options.currentMajor != null ? String(options.currentMajor) : null);
73
+ kv('Running', current != null ? `${name} ${current}` : c.dim('unknown'));
74
+ if (options.refusal) info(options.refusal.message);
75
+ else kv('Can move to', (options.versions ?? options.targets.map(String)).map((v) => `${name} ${v}`).join(', '));
76
+ if (history.length) {
77
+ table(history.map((u) => ({
78
+ id: u.id,
79
+ upgrade: `${fromOf(u)} → ${toOf(u)}`,
80
+ status: u.status === 'running' ? `running (${u.phase ?? '…'})` : u.status,
81
+ started: u.createdAt,
82
+ kept: u.status === 'completed' ? u.previousVolume : u.status === 'rolled_back' ? u.targetVolume : '',
83
+ error: u.error ?? '',
84
+ })));
85
+ }
86
+ }
87
+
88
+ export async function upgradeStart(
89
+ client: NineDeployClient,
90
+ dbIdStr: string,
91
+ opts: { to?: string; yes?: boolean; wait?: boolean; pollMs?: number } = {},
92
+ ): Promise<void> {
93
+ const usage = 'Usage: ninedeploy databases upgrade start <dbId> --to <release> [--yes] [--no-wait]';
94
+ const id = num(dbIdStr, usage);
95
+ if (id === null) return;
96
+ const to = (opts.to ?? '').trim();
97
+ if (!/^[0-9]{1,2}([.][0-9]{1,2})?$/.test(to)) {
98
+ error(`--to must be a release, e.g. --to 18 (PostgreSQL), --to 8.4 (MySQL) or --to 11.8 (MariaDB). ${usage}`);
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+ if (!opts.yes && !(await confirm('upgrade', `The database stops for the whole upgrade (a safety dump, the move to ${to} in a new volume, a comparison). Attached services lose their connection until it is back.`))) return;
103
+ // A whole number goes as toMajor, which panels from before MySQL/MariaDB upgrades also accept.
104
+ const input = /^[0-9]+$/.test(to) ? { toMajor: Number(to) } : { toVersion: to };
105
+ let admitted: Upgrade;
106
+ try { admitted = await spinner('Checking the database', () => client.databases.upgrades.start(id, input)); } catch (err) { fail(err); return; }
107
+ const target = `${label(admitted.engine)} ${toOf(admitted)}`;
108
+ if (opts.wait === false) {
109
+ success(`Upgrade #${admitted.id} to ${target} is running. Follow it with: ninedeploy databases upgrade status ${id}`);
110
+ return;
111
+ }
112
+ let done: Upgrade;
113
+ try {
114
+ done = await spinner(`Upgrading to ${target}`, () => client.databases.upgrades.wait(id, admitted.id, opts.pollMs ? { intervalMs: opts.pollMs } : {}));
115
+ } catch (err) { fail(err); return; }
116
+ describeOutcome(done, id);
117
+ }
118
+
119
+ export async function upgradeRollback(client: NineDeployClient, dbIdStr: string, upgradeIdStr: string, opts: { yes?: boolean } = {}): Promise<void> {
120
+ const usage = 'Usage: ninedeploy databases upgrade rollback <dbId> <upgradeId> [--yes]';
121
+ const id = num(dbIdStr, usage);
122
+ if (id === null) return;
123
+ const upgradeId = num(upgradeIdStr, usage);
124
+ if (upgradeId === null) return;
125
+ if (!opts.yes && !(await confirm('rollback', 'The database restarts on its previous volume. Writes made since the upgrade stay on the upgraded volume and are NOT carried back.'))) return;
126
+ try {
127
+ const u = await spinner('Rolling back', () => client.databases.upgrades.rollback(id, upgradeId));
128
+ success(`The database runs on ${label(u.engine)} ${fromOf(u)} again (${u.previousVolume}).`);
129
+ info(`The upgraded volume ${u.targetVolume} is kept. Delete it with: ninedeploy databases upgrade finalize ${id} ${u.id}`);
130
+ } catch (err) { fail(err); }
131
+ }
132
+
133
+ export async function upgradeFinalize(client: NineDeployClient, dbIdStr: string, upgradeIdStr: string, opts: { yes?: boolean } = {}): Promise<void> {
134
+ const usage = 'Usage: ninedeploy databases upgrade finalize <dbId> <upgradeId> [--yes]';
135
+ const id = num(dbIdStr, usage);
136
+ if (id === null) return;
137
+ const upgradeId = num(upgradeIdStr, usage);
138
+ if (upgradeId === null) return;
139
+ if (!opts.yes && !(await confirm('finalize', 'The volume the database no longer uses is DELETED; after this there is no rollback.'))) return;
140
+ try {
141
+ const u = await spinner('Finalizing', () => client.databases.upgrades.finalize(id, upgradeId));
142
+ success(`Deleted ${u.removedVolume}.`);
143
+ } catch (err) { fail(err); }
144
+ }