ninedeploy 0.10.6 → 0.16.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/LICENSE +21 -21
- package/README.md +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +81 -22
- package/dist/client.js.map +1 -1
- package/dist/commands/backupPolicy.d.ts +29 -0
- package/dist/commands/backupPolicy.d.ts.map +1 -0
- package/dist/commands/backupPolicy.js +111 -0
- package/dist/commands/backupPolicy.js.map +1 -0
- package/dist/commands/certificates.d.ts +18 -7
- package/dist/commands/certificates.d.ts.map +1 -1
- package/dist/commands/certificates.js +121 -1
- package/dist/commands/certificates.js.map +1 -1
- package/dist/commands/databaseAccess.d.ts +37 -0
- package/dist/commands/databaseAccess.d.ts.map +1 -0
- package/dist/commands/databaseAccess.js +306 -0
- package/dist/commands/databaseAccess.js.map +1 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +28 -7
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/githubApps.d.ts +40 -0
- package/dist/commands/githubApps.d.ts.map +1 -0
- package/dist/commands/githubApps.js +278 -0
- package/dist/commands/githubApps.js.map +1 -0
- package/dist/commands/hostIps.d.ts +21 -0
- package/dist/commands/hostIps.d.ts.map +1 -0
- package/dist/commands/hostIps.js +68 -0
- package/dist/commands/hostIps.js.map +1 -0
- package/dist/commands/housekeeping.d.ts +7 -0
- package/dist/commands/housekeeping.d.ts.map +1 -1
- package/dist/commands/housekeeping.js +16 -0
- package/dist/commands/housekeeping.js.map +1 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +17 -2
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/manage.d.ts +26 -10
- package/dist/commands/manage.d.ts.map +1 -1
- package/dist/commands/manage.js +219 -65
- package/dist/commands/manage.js.map +1 -1
- package/dist/commands/manifest.d.ts.map +1 -1
- package/dist/commands/manifest.js +9 -1
- package/dist/commands/manifest.js.map +1 -1
- package/dist/commands/misc.d.ts +4 -2
- package/dist/commands/misc.d.ts.map +1 -1
- package/dist/commands/misc.js +25 -11
- package/dist/commands/misc.js.map +1 -1
- package/dist/commands/multiNode.d.ts +55 -0
- package/dist/commands/multiNode.d.ts.map +1 -0
- package/dist/commands/multiNode.js +438 -0
- package/dist/commands/multiNode.js.map +1 -0
- package/dist/commands/notifications.d.ts +1 -1
- package/dist/commands/operations.d.ts +89 -0
- package/dist/commands/operations.d.ts.map +1 -0
- package/dist/commands/operations.js +468 -0
- package/dist/commands/operations.js.map +1 -0
- package/dist/commands/panelBackup.d.ts +28 -0
- package/dist/commands/panelBackup.d.ts.map +1 -0
- package/dist/commands/panelBackup.js +153 -0
- package/dist/commands/panelBackup.js.map +1 -0
- package/dist/commands/plugins.d.ts +1 -1
- package/dist/commands/plugins.d.ts.map +1 -1
- package/dist/commands/plugins.js +7 -3
- package/dist/commands/plugins.js.map +1 -1
- package/dist/commands/proxyConfig.d.ts +16 -0
- package/dist/commands/proxyConfig.d.ts.map +1 -0
- package/dist/commands/proxyConfig.js +122 -0
- package/dist/commands/proxyConfig.js.map +1 -0
- package/dist/commands/secretProviders.d.ts +59 -0
- package/dist/commands/secretProviders.d.ts.map +1 -0
- package/dist/commands/secretProviders.js +212 -0
- package/dist/commands/secretProviders.js.map +1 -0
- package/dist/commands/services.d.ts +17 -0
- package/dist/commands/services.d.ts.map +1 -1
- package/dist/commands/services.js +55 -6
- package/dist/commands/services.js.map +1 -1
- package/dist/commands/setup.d.ts +3 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +19 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/sources.d.ts +10 -3
- package/dist/commands/sources.d.ts.map +1 -1
- package/dist/commands/sources.js +116 -18
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/templates.d.ts.map +1 -1
- package/dist/commands/templates.js +8 -1
- package/dist/commands/templates.js.map +1 -1
- package/dist/commands/upload.d.ts +24 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +96 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/index.js +455 -24
- package/dist/index.js.map +1 -1
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +5 -4
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/panelBackupFile.d.ts +3 -0
- package/dist/lib/panelBackupFile.d.ts.map +1 -0
- package/dist/lib/panelBackupFile.js +71 -0
- package/dist/lib/panelBackupFile.js.map +1 -0
- package/dist/lib/serverRunner.d.ts +6 -0
- package/dist/lib/serverRunner.d.ts.map +1 -1
- package/dist/lib/serverRunner.js +18 -3
- package/dist/lib/serverRunner.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +9 -2
- package/dist/prompts.js.map +1 -1
- package/package.json +6 -5
- package/src/client.ts +137 -0
- package/src/commands/backupPolicy.ts +123 -0
- package/src/commands/branding.ts +83 -0
- package/src/commands/buildCache.ts +72 -0
- package/src/commands/certificates.ts +208 -0
- package/src/commands/communityTemplates.ts +97 -0
- package/src/commands/configCenter.ts +71 -0
- package/src/commands/configPresets.ts +215 -0
- package/src/commands/databaseAccess.ts +333 -0
- package/src/commands/demo.ts +22 -0
- package/src/commands/deploy.ts +284 -0
- package/src/commands/doctor.ts +170 -0
- package/src/commands/domains.ts +170 -0
- package/src/commands/egress.ts +135 -0
- package/src/commands/emailTemplates.ts +112 -0
- package/src/commands/firewall.ts +82 -0
- package/src/commands/githubApps.ts +292 -0
- package/src/commands/hostIps.ts +71 -0
- package/src/commands/housekeeping.ts +106 -0
- package/src/commands/login.ts +58 -0
- package/src/commands/logs.ts +91 -0
- package/src/commands/manage.ts +714 -0
- package/src/commands/manifest.ts +314 -0
- package/src/commands/metrics.ts +69 -0
- package/src/commands/misc.ts +307 -0
- package/src/commands/multiNode.ts +442 -0
- package/src/commands/notifications.ts +174 -0
- package/src/commands/operations.ts +519 -0
- package/src/commands/panelBackup.ts +167 -0
- package/src/commands/pgbouncer.ts +65 -0
- package/src/commands/plugins.ts +123 -0
- package/src/commands/proxyConfig.ts +117 -0
- package/src/commands/secretProviders.ts +228 -0
- package/src/commands/server.ts +126 -0
- package/src/commands/services.ts +299 -0
- package/src/commands/setup.ts +94 -0
- package/src/commands/sources.ts +317 -0
- package/src/commands/sso.ts +106 -0
- package/src/commands/templates.ts +158 -0
- package/src/commands/token.ts +66 -0
- package/src/commands/upload.ts +111 -0
- package/src/commands/webhooks.ts +93 -0
- package/src/commands/workspaces.ts +63 -0
- package/src/config.ts +53 -0
- package/src/index.ts +1303 -0
- package/src/lib/format.ts +182 -0
- package/src/lib/panelBackupFile.ts +67 -0
- package/src/lib/serverRunner.ts +223 -0
- package/src/prompts.ts +238 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import type { NineDeployClient } from '../client.js';
|
|
6
|
+
import { error, info, spinner, success } from '../lib/format.js';
|
|
7
|
+
|
|
8
|
+
/** The panel's limit for one upload (POST /v1/services/:id/source). */
|
|
9
|
+
export const UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
/** Never part of a build context: VCS data, dependency trees, build output, local env files. */
|
|
12
|
+
export const DEFAULT_EXCLUDES = ['.git', 'node_modules', '.next', '.nuxt', '.svelte-kit', '.turbo', '.cache', '.venv', '__pycache__', '.env', '.env.*', '.DS_Store'];
|
|
13
|
+
|
|
14
|
+
/** `tar -czf` of `dir` (its contents, not the folder) into a buffer, with the excludes. */
|
|
15
|
+
export async function packDirectory(dir: string, excludes: string[]): Promise<Buffer> {
|
|
16
|
+
const work = mkdtempSync(path.join(tmpdir(), 'nd-up-'));
|
|
17
|
+
const out = path.join(work, 'source.tar.gz');
|
|
18
|
+
try {
|
|
19
|
+
await new Promise<void>((resolve, reject) => {
|
|
20
|
+
// Relative -f with cwd: GNU tar reads a drive-letter colon (C:\…) as a remote host.
|
|
21
|
+
const args = ['-czf', path.basename(out), ...excludes.map((e) => `--exclude=${e}`), '-C', dir, '.'];
|
|
22
|
+
const child = spawn('tar', args, { cwd: work, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
23
|
+
let stderr = '';
|
|
24
|
+
child.stderr.on('data', (d: Buffer) => (stderr += d.toString()));
|
|
25
|
+
child.on('error', reject);
|
|
26
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`tar exited ${code}: ${stderr.trim()}`))));
|
|
27
|
+
});
|
|
28
|
+
return readFileSync(out);
|
|
29
|
+
} finally {
|
|
30
|
+
rmSync(work, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DeployUpOptions {
|
|
35
|
+
service?: string;
|
|
36
|
+
create?: string;
|
|
37
|
+
port?: string;
|
|
38
|
+
deploy?: boolean;
|
|
39
|
+
exclude?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `ninedeploy deploy up [dir]` (0.16.1): pack a local folder (or send a
|
|
44
|
+
* ready .zip / .tar.gz) and deploy it as the service's source — no Git
|
|
45
|
+
* repository needed. `--service` names an existing upload-sourced service
|
|
46
|
+
* (id or slug); `--create <name>` makes a new docker service for it.
|
|
47
|
+
*/
|
|
48
|
+
export async function deployUp(
|
|
49
|
+
client: NineDeployClient,
|
|
50
|
+
target: string | undefined,
|
|
51
|
+
opts: DeployUpOptions,
|
|
52
|
+
deps: { pack?: typeof packDirectory } = {},
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const pack = deps.pack ?? packDirectory;
|
|
55
|
+
const where = path.resolve(target ?? '.');
|
|
56
|
+
if (!existsSync(where)) return error(`${where} does not exist.`);
|
|
57
|
+
if (!opts.service && !opts.create) {
|
|
58
|
+
return error('Name the service: --service <id|slug> for an existing one, or --create <name> for a new one.');
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
let archive: Buffer;
|
|
62
|
+
if (statSync(where).isDirectory()) {
|
|
63
|
+
const excludes = [...DEFAULT_EXCLUDES, ...(opts.exclude ?? [])];
|
|
64
|
+
archive = await spinner(`Packing ${where}`, () => pack(where, excludes));
|
|
65
|
+
} else if (/\.(zip|tar\.gz|tgz)$/i.test(where)) {
|
|
66
|
+
archive = readFileSync(where);
|
|
67
|
+
} else {
|
|
68
|
+
return error('Pass a folder, a .zip or a .tar.gz.');
|
|
69
|
+
}
|
|
70
|
+
if (archive.length > UPLOAD_MAX_BYTES) {
|
|
71
|
+
return error(
|
|
72
|
+
`The archive is ${(archive.length / 1024 / 1024).toFixed(1)} MiB; the panel takes up to ${UPLOAD_MAX_BYTES / 1024 / 1024} MiB. ` +
|
|
73
|
+
'Exclude build output or dependencies with --exclude <pattern>.',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let serviceId: number;
|
|
78
|
+
if (opts.create) {
|
|
79
|
+
const port = opts.port ? Number(opts.port) : undefined;
|
|
80
|
+
if (opts.port && (!Number.isInteger(port) || port! < 1 || port! > 65535)) return error('--port must be a port number.');
|
|
81
|
+
const svc = await spinner(`Creating service ${opts.create}`, () =>
|
|
82
|
+
client.services.create({ name: opts.create!, type: 'docker', ...(port ? { port } : {}) }),
|
|
83
|
+
);
|
|
84
|
+
serviceId = svc.id;
|
|
85
|
+
info(`Service #${svc.id} (${svc.slug}) created.`);
|
|
86
|
+
} else {
|
|
87
|
+
const ref = opts.service!;
|
|
88
|
+
if (/^\d+$/.test(ref)) {
|
|
89
|
+
serviceId = Number(ref);
|
|
90
|
+
} else {
|
|
91
|
+
const found = (await client.services.list()).find((s) => s.slug === ref || s.name === ref);
|
|
92
|
+
if (!found) return error(`No service named "${ref}".`);
|
|
93
|
+
serviceId = found.id;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sizeMiB = (archive.length / 1024 / 1024).toFixed(1);
|
|
98
|
+
const res = await spinner(`Uploading ${sizeMiB} MiB`, () =>
|
|
99
|
+
client.services.uploadSource(serviceId, new Uint8Array(archive.buffer, archive.byteOffset, archive.length), { deploy: opts.deploy !== false }),
|
|
100
|
+
);
|
|
101
|
+
success(`Uploaded ${res.sha.slice(0, 7)} (${res.entries} entries).`);
|
|
102
|
+
if (res.deploymentId != null) {
|
|
103
|
+
success(`Deployment #${res.deploymentId} queued.`);
|
|
104
|
+
info(`Watch logs: ninedeploy services logs ${serviceId}`);
|
|
105
|
+
} else {
|
|
106
|
+
info(`Stored only. Deploy it with: ninedeploy services deploy ${serviceId}`);
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
error(err instanceof Error ? err.message : String(err));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { NineDeployClient } from '../client.js';
|
|
2
|
+
import { prompt } from '../prompts.js';
|
|
3
|
+
import { c, error, header, info, kv, spinner, success, table } from '../lib/format.js';
|
|
4
|
+
|
|
5
|
+
/** `ninedeploy webhooks list <serviceId>` */
|
|
6
|
+
export async function webhooksList(client: NineDeployClient, svcArg: string): Promise<void> {
|
|
7
|
+
const serviceId = Number(svcArg);
|
|
8
|
+
if (!serviceId) return error('Usage: ninedeploy webhooks list <serviceId>');
|
|
9
|
+
header(`Webhooks for service #${serviceId}`);
|
|
10
|
+
const rows = await spinner('Fetching', () => client.webhooks.list(serviceId));
|
|
11
|
+
if (rows.length === 0) {
|
|
12
|
+
info('No webhooks yet. Add one with `ninedeploy webhooks add`.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
table(
|
|
16
|
+
rows.map((w) => ({
|
|
17
|
+
id: w.id,
|
|
18
|
+
branch: w.branch,
|
|
19
|
+
source: w.sourceId ?? c.gray('—'),
|
|
20
|
+
active: w.active ? c.green('✓') : c.red('✗'),
|
|
21
|
+
url: w.url,
|
|
22
|
+
})),
|
|
23
|
+
['id', 'branch', 'source', 'active', 'url'],
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `ninedeploy webhooks add <serviceId> [branch]`
|
|
29
|
+
*
|
|
30
|
+
* Returns the secret ONCE so the operator can paste it into GitHub as the
|
|
31
|
+
* webhook "Secret" value. The CLI never stores it; the panel only ever sees
|
|
32
|
+
* the HMAC digest.
|
|
33
|
+
*/
|
|
34
|
+
export async function webhooksAdd(client: NineDeployClient, svcArg: string, branchArg?: string): Promise<void> {
|
|
35
|
+
const serviceId = Number(svcArg);
|
|
36
|
+
if (!serviceId) return error('Usage: ninedeploy webhooks add <serviceId> [branch]');
|
|
37
|
+
const branch = branchArg ?? (await prompt('Branch to deploy on push', 'main'));
|
|
38
|
+
if (!branch) return error('Branch is required');
|
|
39
|
+
|
|
40
|
+
const watchPaths = (await prompt('Watch paths (newline/comma-separated globs; empty = all files)', '')) || undefined;
|
|
41
|
+
|
|
42
|
+
const created = await spinner('Creating webhook', () =>
|
|
43
|
+
client.webhooks.create(serviceId, { branch, watchPaths }),
|
|
44
|
+
);
|
|
45
|
+
success(`Webhook #${created.id} created for branch "${created.branch}"`);
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(` ${c.bold('URL')}`);
|
|
48
|
+
console.log(` ${c.cyan(created.url)}`);
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(` ${c.bold('Secret (shown once — paste into GitHub):')}`);
|
|
51
|
+
console.log(` ${c.green(created.secret)}`);
|
|
52
|
+
console.log();
|
|
53
|
+
console.log(` ${c.gray('GitHub steps: Repo → Settings → Webhooks → Add')}`);
|
|
54
|
+
console.log(` ${c.gray(' • Payload URL : the URL above')}`);
|
|
55
|
+
console.log(` ${c.gray(' • Content type : application/json')}`);
|
|
56
|
+
console.log(` ${c.gray(' • Secret : the secret above')}`);
|
|
57
|
+
console.log(` ${c.gray(' • SSL : enabled (the panel runs behind HTTPS via Traefik)')}`);
|
|
58
|
+
console.log(` ${c.gray(' • Events : "Just the push event."')}`);
|
|
59
|
+
console.log();
|
|
60
|
+
info('Tip: set `NINEDEPLOY_PUBLIC_URL` on the server to a public HTTPS URL so the URL is reachable from GitHub.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `ninedeploy webhooks remove <serviceId> <hookId>` */
|
|
64
|
+
export async function webhooksRemove(client: NineDeployClient, svcArg: string, hookArg: string): Promise<void> {
|
|
65
|
+
const serviceId = Number(svcArg);
|
|
66
|
+
const hookId = Number(hookArg);
|
|
67
|
+
if (!serviceId || !hookId) return error('Usage: ninedeploy webhooks remove <serviceId> <hookId>');
|
|
68
|
+
const confirm = await prompt(`Type "delete" to confirm removal of webhook #${hookId}`, '');
|
|
69
|
+
if (confirm.trim() !== 'delete') {
|
|
70
|
+
info('Aborted.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await spinner('Removing webhook', () => client.webhooks.remove(serviceId, hookId));
|
|
74
|
+
success(`Webhook #${hookId} removed.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `ninedeploy webhooks show <serviceId> <hookId>` */
|
|
78
|
+
export async function webhooksShow(client: NineDeployClient, svcArg: string, hookArg: string): Promise<void> {
|
|
79
|
+
const serviceId = Number(svcArg);
|
|
80
|
+
const hookId = Number(hookArg);
|
|
81
|
+
if (!serviceId || !hookId) return error('Usage: ninedeploy webhooks show <serviceId> <hookId>');
|
|
82
|
+
header(`Webhook #${hookId}`);
|
|
83
|
+
const rows = await spinner('Fetching', () => client.webhooks.list(serviceId));
|
|
84
|
+
const w = rows.find((x) => x.id === hookId);
|
|
85
|
+
if (!w) return error(`Webhook #${hookId} not found on service #${serviceId}`);
|
|
86
|
+
kv('ID', w.id);
|
|
87
|
+
kv('Branch', w.branch);
|
|
88
|
+
kv('Source', w.sourceId ?? c.gray('— (inherits service default)'));
|
|
89
|
+
kv('Active', w.active ? c.green('yes') : c.red('no'));
|
|
90
|
+
kv('Watch paths', w.watchPaths || c.gray('— (all files)'));
|
|
91
|
+
kv('URL', w.url);
|
|
92
|
+
kv('Created', w.createdAt);
|
|
93
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { NineDeployClient } from '@ninedeploy/sdk';
|
|
2
|
+
import { spinner, table } from '../lib/format.js';
|
|
3
|
+
|
|
4
|
+
export async function workspacesList(client: NineDeployClient): Promise<void> {
|
|
5
|
+
const list = await spinner('Fetching workspaces', () => client.workspaces.list());
|
|
6
|
+
if (list.length === 0) {
|
|
7
|
+
console.log(' No workspaces found.');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
table(
|
|
11
|
+
list.map((w) => ({
|
|
12
|
+
id: w.id,
|
|
13
|
+
name: w.name,
|
|
14
|
+
slug: w.slug,
|
|
15
|
+
role: w.myRole,
|
|
16
|
+
members: w.memberCount,
|
|
17
|
+
projects: w.projectCount,
|
|
18
|
+
created: new Date(w.createdAt).toLocaleDateString(),
|
|
19
|
+
})),
|
|
20
|
+
['id', 'name', 'slug', 'role', 'members', 'projects', 'created'],
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function workspacesGet(client: NineDeployClient, id: string): Promise<void> {
|
|
25
|
+
const wsId = Number.parseInt(id, 10);
|
|
26
|
+
if (Number.isNaN(wsId)) {
|
|
27
|
+
throw new Error('Workspace ID must be an integer');
|
|
28
|
+
}
|
|
29
|
+
const ws = await spinner('Fetching workspace', () => client.workspaces.get(wsId));
|
|
30
|
+
console.log(`\n Workspace #${ws.id}: ${ws.name} (${ws.slug})`);
|
|
31
|
+
console.log(` Role: ${ws.myRole}`);
|
|
32
|
+
if (ws.description) console.log(` Description: ${ws.description}`);
|
|
33
|
+
console.log(` Created: ${new Date(ws.createdAt).toLocaleString()}`);
|
|
34
|
+
console.log('\n Members:');
|
|
35
|
+
table(
|
|
36
|
+
ws.members.map((m) => ({
|
|
37
|
+
userId: m.userId,
|
|
38
|
+
nameEmail: m.name ? `${m.name} <${m.email}>` : m.email,
|
|
39
|
+
role: m.role,
|
|
40
|
+
joined: new Date(m.createdAt).toLocaleDateString(),
|
|
41
|
+
})),
|
|
42
|
+
['userId', 'nameEmail', 'role', 'joined'],
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function workspacesCreate(client: NineDeployClient, name: string, opts: { description?: string }): Promise<void> {
|
|
47
|
+
const ws = await spinner('Creating workspace', () =>
|
|
48
|
+
client.workspaces.create({
|
|
49
|
+
name,
|
|
50
|
+
description: opts.description,
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
console.log(` ✓ Workspace #${ws.id} created: ${ws.name} (${ws.slug})`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function workspacesDelete(client: NineDeployClient, id: string): Promise<void> {
|
|
57
|
+
const wsId = Number.parseInt(id, 10);
|
|
58
|
+
if (Number.isNaN(wsId)) {
|
|
59
|
+
throw new Error('Workspace ID must be an integer');
|
|
60
|
+
}
|
|
61
|
+
await spinner('Deleting workspace', () => client.workspaces.delete(wsId));
|
|
62
|
+
console.log(` ✓ Workspace #${wsId} deleted.`);
|
|
63
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = path.join(homedir(), '.ninedeploy');
|
|
7
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
8
|
+
|
|
9
|
+
export interface CliConfig {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
token?: string;
|
|
12
|
+
/** Long-lived token used to mint fresh access tokens (see client.ts). */
|
|
13
|
+
refreshToken?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULTS: CliConfig = { baseUrl: 'http://localhost:3000' };
|
|
17
|
+
|
|
18
|
+
export function loadConfig(): CliConfig {
|
|
19
|
+
try {
|
|
20
|
+
if (existsSync(CONFIG_FILE)) {
|
|
21
|
+
return { ...DEFAULTS, ...(JSON.parse(readFileSync(CONFIG_FILE, 'utf8')) as Partial<CliConfig>) };
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
// A torn/partial write used to fall back to defaults SILENTLY — the user
|
|
25
|
+
// was logged out with no hint the file was ever corrupt. Say so.
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.error(`⚠ Could not read ${CONFIG_FILE} (${err instanceof Error ? err.message : String(err)}); using defaults.`);
|
|
28
|
+
}
|
|
29
|
+
return { ...DEFAULTS };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function saveConfig(config: CliConfig): void {
|
|
33
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
+
// Temp-file + rename: a crash mid-write must never leave a half-written
|
|
35
|
+
// config behind (the previous direct write could, logging the user out on
|
|
36
|
+
// the next command with no explanation).
|
|
37
|
+
const tmp = `${CONFIG_FILE}.${randomUUID()}.tmp`;
|
|
38
|
+
writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
39
|
+
try {
|
|
40
|
+
renameSync(tmp, CONFIG_FILE);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
try { unlinkSync(tmp); } catch { /* already gone */ }
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
// writeFileSync's mode only applies at CREATION — re-assert 0600 so a file
|
|
46
|
+
// that ever ended up group/world-readable (restore, copy, old version)
|
|
47
|
+
// holding the bearer token is tightened on every save.
|
|
48
|
+
try {
|
|
49
|
+
chmodSync(CONFIG_FILE, 0o600);
|
|
50
|
+
} catch {
|
|
51
|
+
/* best-effort on exotic filesystems */
|
|
52
|
+
}
|
|
53
|
+
}
|